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> |