forked from sheetjs/sheetjs
242 lines
4.7 KiB
Vue
242 lines
4.7 KiB
Vue
|
<script setup lang="ts">
|
||
|
import { ref } from "vue";
|
||
|
import { read, utils, writeFile, WorkBook } from "xlsx";
|
||
|
|
||
|
import VueTableLite from "vue3-table-lite/ts";
|
||
|
|
||
|
type DataSet = {
|
||
|
[index: string]: WorkBook;
|
||
|
};
|
||
|
|
||
|
type Row = any[];
|
||
|
|
||
|
type Column = {
|
||
|
field: string;
|
||
|
label: string;
|
||
|
display: (row: Row) => string;
|
||
|
};
|
||
|
|
||
|
const currFileName = ref<string>("");
|
||
|
const currSheet = ref<string>("");
|
||
|
const sheets = ref<string[]>([]);
|
||
|
const workBook = ref<DataSet>({} as DataSet);
|
||
|
const rows = ref<Row[]>([]);
|
||
|
const columns = ref<Column[]>([]);
|
||
|
|
||
|
const exportTypes: string[] = ["xlsx", "xlsb", "csv", "html"];
|
||
|
|
||
|
let cell = 0;
|
||
|
|
||
|
function resetCell() {
|
||
|
cell = 0;
|
||
|
}
|
||
|
|
||
|
function display(col: number): (row: Row) => string {
|
||
|
return function (row: Row) {
|
||
|
return `<span
|
||
|
style="user-select: none; display: block"
|
||
|
position="${Math.floor(cell++ / columns.value.length)}.${col}"
|
||
|
onblur="endEdit(event)"
|
||
|
ondblclick="startEdit(event)"
|
||
|
onkeydown="endEdit(event)">${row[col] ?? " "}</span>`;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
window.startEdit = function (ev) {
|
||
|
ev.target.contentEditable = true;
|
||
|
ev.target.focus();
|
||
|
};
|
||
|
|
||
|
window.endEdit = function (ev) {
|
||
|
if (ev.key === undefined || ev.key === "Enter") {
|
||
|
const pos = ev.target.getAttribute("position").split(".");
|
||
|
|
||
|
ev.target.contentEditable = false;
|
||
|
|
||
|
rows.value[pos[0]][pos[1]] = ev.target.innerText;
|
||
|
|
||
|
workBook.value[currSheet.value] = utils.json_to_sheet(rows.value, {
|
||
|
header: columns.value.map((col: Column) => col.field),
|
||
|
skipHeader: true,
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function getRowsCols(
|
||
|
data: DataSet,
|
||
|
sheetName: string
|
||
|
): {
|
||
|
rows: Row[];
|
||
|
cols: Column[];
|
||
|
} {
|
||
|
const rows: Row[] = utils.sheet_to_json(data[sheetName], { header: 1 });
|
||
|
let cols: Column[] = [];
|
||
|
|
||
|
for (let row of rows) {
|
||
|
const keys: string[] = Object.keys(row);
|
||
|
|
||
|
if (keys.length > cols.length) {
|
||
|
cols = keys.map((key) => {
|
||
|
return {
|
||
|
field: key,
|
||
|
label: utils.encode_col(+key),
|
||
|
display: display(key),
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return { rows, cols };
|
||
|
}
|
||
|
|
||
|
async function importFile(ev: ChangeEvent<HTMLInputElement>): Promise<void> {
|
||
|
const file = ev.target.files[0];
|
||
|
const data = read(await file.arrayBuffer());
|
||
|
|
||
|
currFileName.value = file.name;
|
||
|
currSheet.value = data.SheetNames?.[0];
|
||
|
sheets.value = data.SheetNames;
|
||
|
workBook.value = data.Sheets;
|
||
|
|
||
|
selectSheet(currSheet.value);
|
||
|
}
|
||
|
|
||
|
function exportFile(type: string): void {
|
||
|
const wb = utils.book_new();
|
||
|
|
||
|
sheets.value.forEach((sheet) => {
|
||
|
utils.book_append_sheet(wb, workBook.value[sheet], sheet);
|
||
|
});
|
||
|
|
||
|
writeFile(wb, `sheet.${type}`);
|
||
|
}
|
||
|
|
||
|
function selectSheet(sheet: string): void {
|
||
|
const { rows: newRows, cols: newCols } = getRowsCols(workBook.value, sheet);
|
||
|
|
||
|
resetCell();
|
||
|
|
||
|
rows.value = newRows;
|
||
|
columns.value = newCols;
|
||
|
currSheet.value = sheet;
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<template>
|
||
|
<header class="imp-exp">
|
||
|
<div class="import">
|
||
|
<input type="file" id="import" @change="importFile" />
|
||
|
<label for="import">import</label>
|
||
|
</div>
|
||
|
<span>{{ currFileName || "vue-modify demo" }}</span>
|
||
|
<div class="export">
|
||
|
<span>export</span>
|
||
|
<ul>
|
||
|
<li v-for="type in exportTypes" @click="exportFile(type)">
|
||
|
{{ `.${type}` }}
|
||
|
</li>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</header>
|
||
|
<div class="sheets">
|
||
|
<span
|
||
|
v-for="sheet in sheets"
|
||
|
@click="selectSheet(sheet)"
|
||
|
:class="[currSheet === sheet ? 'selected' : '']"
|
||
|
>
|
||
|
{{ sheet }}
|
||
|
</span>
|
||
|
</div>
|
||
|
<vue-table-lite
|
||
|
:is-static-mode="true"
|
||
|
:page-size="50"
|
||
|
:columns="columns"
|
||
|
:rows="rows"
|
||
|
></vue-table-lite>
|
||
|
</template>
|
||
|
|
||
|
<style>
|
||
|
.imp-exp {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
padding: 0.5rem;
|
||
|
font-family: mono;
|
||
|
color: #212529;
|
||
|
}
|
||
|
|
||
|
.import {
|
||
|
font-size: medium;
|
||
|
}
|
||
|
|
||
|
.import input {
|
||
|
position: absolute;
|
||
|
opacity: 0;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.import label {
|
||
|
background-color: white;
|
||
|
border: 1px solid;
|
||
|
padding: 0.3rem;
|
||
|
}
|
||
|
|
||
|
.export: hover {
|
||
|
border-bottom: none;
|
||
|
}
|
||
|
|
||
|
.export:hover ul {
|
||
|
display: block;
|
||
|
}
|
||
|
|
||
|
.export span {
|
||
|
padding: 0.3rem;
|
||
|
border: 1px solid;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.export ul {
|
||
|
display: none;
|
||
|
position: absolute;
|
||
|
z-index: 5;
|
||
|
background-color: white;
|
||
|
list-style: none;
|
||
|
padding: 0.3rem;
|
||
|
border: 1px solid;
|
||
|
margin-top: 0.3rem;
|
||
|
border-top: none;
|
||
|
}
|
||
|
|
||
|
.export ul li {
|
||
|
padding: 0.3rem;
|
||
|
text-align: center;
|
||
|
}
|
||
|
|
||
|
.export ul li:hover {
|
||
|
background-color: lightgray;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.sheets {
|
||
|
display: flex;
|
||
|
justify-content: center;
|
||
|
margin: 0.3rem;
|
||
|
color: #212529;
|
||
|
}
|
||
|
|
||
|
.sheets span {
|
||
|
border: 1px solid;
|
||
|
padding: 0.5rem;
|
||
|
margin: 0.3rem;
|
||
|
}
|
||
|
|
||
|
.sheets span:hover:not(.selected) {
|
||
|
background-color: lightgray;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.selected {
|
||
|
background-color: #343a40;
|
||
|
color: white;
|
||
|
}
|
||
|
</style>
|