import { ref, reactive, toRaw, computed, watch, type Ref, isRef } from "vue"; import { Options, Menu, DEFAULT_MENU, Column, Record } from "./type"; import Drag from "./drag"; import { deepCopy, readTextToClipboard, validatorDecimal } from "./util"; import { saveAs } from 'file-saver'; import * as XLSX from 'xlsx'; export default class extends Drag { constructor(options: Options) { super(options); // 注册菜单 const menus = toRaw(this.props.menus) || this.getDefaultMenus(); menus.forEach((menu: Menu | string) => { if (!menu) return; const key = typeof menu === "string" ? menu : menu.key ?? this.createId(); if (!this[key] && key === menu) return; // 新菜单 if (!this[key]) this.registerMenu(key, menu as Menu); else this.registerMenu(key, Object.assign(this[key], menu)); }); const closeMenus = (e: MouseEvent) => { if (e.button === 2) return; this.closeMenus(); }; watch(this.showMenus, (value) => { let stopWatch: any; if (value) { this.resetMenuStatus(); stopWatch = watch(this.selectedState, (value) => { if (value) { this.resetMenuStatus(); } }); document.body.addEventListener("mouseup", closeMenus); document.body.addEventListener("click", closeMenus); document.body.addEventListener("wheel", closeMenus); } else { stopWatch?.(); document.body.removeEventListener("mouseup", closeMenus); document.body.removeEventListener("click", closeMenus); document.body.removeEventListener("wheel", closeMenus); Promise.resolve().then(() => { this.resetMenuStatus(); }); } }); } showMenus = ref(false); menuList = ref([]); menuPosition = reactive({ left: "0px", top: "0px", transform: "none", }); openMenus() { this.showMenus.value = false; if (this.props.menus?.length === 0) { return; } // const { left, top } = this.table.value?.getBoundingClientRect?.() ?? {}; this.menuPosition.left = event.clientX + "px"; this.menuPosition.top = event.clientY + "px"; const offsetBottom = innerHeight - event.clientY; if (offsetBottom - this.menuList.value.length * 30 < 30) { this.menuPosition.transform = "translateY(-100%)"; } else { this.menuPosition.transform = "translateY(0)"; } if ( this.selectedState.cells || this.getMaxEndX() - this.getMinStartX() > 0 || (this.getMaxEndY() - this.getMinStartY() > 0 && !this.selectedState.selectedTh) ) { this.showMenus.value = true; return; } let node: HTMLElement = event?.target; while (!["TD", "TH"].includes(node.tagName)) { if (node.tagName === "TABLE") return; node = node.parentElement; } // 行选择、或者表头行号 if ( node.classList.contains("row-check") || (node.tagName === "TH" && node.classList.contains("row-index")) ) return; this.resetSelectedState(); const { colindex, rowindex } = node.dataset; if (node.tagName === "TH") { this.selectedState.selectedTh = true; this.selectedState.startX = this.selectedState.endX = Number(colindex); this.selectedState.startY = -1; this.selectedState.endY = this.dataSource.value.length - 1; } else if (node.tagName === "TD" && node.classList.contains("row-index")) { this.selectedState.startY = this.selectedState.endY = Number(rowindex); this.selectedState.startX = this.selectedState.endX = -1; } else { if (this.props.customEditer?.disabledSelectCell) { this.selectedState.startX = this.selectedState.endX = -1; this.selectedState.startY = this.selectedState.endY = Number(rowindex); } else { this.selectedState.startX = this.selectedState.endX = Number(colindex); this.selectedState.startY = this.selectedState.endY = Number(rowindex); } } this.showMenus.value = true; this.callbackSelected(); } async menuClickHandel(item: Menu) { const va = await item.vForm?.validateFields?.(); if (va?.errorFields?.length > 0) throw ""; const needAddHistoryRecord = this.props.openUndo && ![DEFAULT_MENU.COPY, DEFAULT_MENU.REDO, DEFAULT_MENU.UNDO].includes( item.key ); if (needAddHistoryRecord && this.emptyHistoryRecord()) this.addHistoryRecord(); this.closeMenus(); await item.handlerBefore?.(item, this.selectAreasData.value, this); await item.handler?.(item, this.selectAreasData.value, this); await item.handlerAfter?.(item); if (item.inputProps) { item.inputProps.value = item.inputDefaultValue; } if (needAddHistoryRecord) this.addHistoryRecord(); // 如果是删除行菜单,重置选取标识 if (item.key === DEFAULT_MENU.DELETE_ROWS) this.resetSelectedState(); } closeMenus() { this.showMenus.value = false; } /** * 自定义菜单、覆盖默认菜单配置 * @param key 菜单项唯一key * @param option 菜单配置 */ registerMenu(key: string, option: Menu) { const exIndex = this.menuList.value.findIndex((item) => item.key === key); if (exIndex > -1) { Object.assign(this.menuList.value[exIndex], option); } else { this.menuList.value.push(option); } } resetMenuStatus() { this.menuList.value?.forEach(async (item: Menu) => { if (item.inputProps) item.inputProps.value = item.inputDefaultValue; item.vForm?.clearValidate?.(); if (typeof item.disabled !== "function") return; item._disabled = await item.disabled( item, this.selectAreasData.value, this ); }); } // 检查菜单项是否要将其显示 visableMenu(menu: Menu) { if (typeof menu.hide === "function") return !menu.hide(menu, this.selectAreasData.value, this); if (isRef(menu.hide)) return !menu.hide.value; return !menu.hide; } private getDefaultMenus(): Menu[] { return [ this[DEFAULT_MENU.COPY], this[DEFAULT_MENU.PAETS], this[DEFAULT_MENU.COPY_DOWN], this[DEFAULT_MENU.ADD_STEP_DOWN], this[DEFAULT_MENU.INSERT_BEFORE_ROWS], this[DEFAULT_MENU.INSERT_AFTER_ROWS], this[DEFAULT_MENU.INSERT_COPY_AFTER_ROWS], this[DEFAULT_MENU.UNDO], this[DEFAULT_MENU.REDO], this[DEFAULT_MENU.DELETE_ROWS], // this[DEFAULT_MENU.EXPORT_TXT], ]; } private validatorInputValue(item: Menu) { const val = item.inputProps?.value; const precision: number | undefined = item.inputPrecision; if (precision !== undefined) { return validatorDecimal(val, { maxDecimal: precision, message: precision === 0 ? /*`只能输入整数`*/ this.getMessage("input.mustInt").value : /*`只能输入${precision}位小数`*/ this.getMessage( "input.maxDecimal" ).value.replace?.("[]", `${precision}`), }); } return Promise.resolve(); } [DEFAULT_MENU.COPY] = { key: DEFAULT_MENU.COPY, label: /*复制*/ this.getMessage("menus.copyLabel"), hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records); }, disabled: () => { return !this.AuthActionCopy(); }, handler: () => this.copyHandel(), }; [DEFAULT_MENU.PAETS] = { key: DEFAULT_MENU.PAETS, label: /*粘贴*/ this.getMessage("menus.paetsLabel"), hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: async () => { return !this.AuthActionPaste(); }, handler: () => this.pasteHandel(), }; [DEFAULT_MENU.UNDO] = { key: DEFAULT_MENU.UNDO, label: /*撤销*/ this.getMessage("menus.undoLabel"), hide: (menu, { records }) => { return ( !this.props.openUndo || this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead() ); }, disabled: () => !this.AuthActionUndo(), handler: () => this.undo(), }; [DEFAULT_MENU.REDO] = { key: DEFAULT_MENU.REDO, label: /*恢复*/ this.getMessage("menus.redoLabel"), hide: (menu, { records }) => { return ( !this.props.openUndo || this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead() ); }, disabled: () => !this.AuthActionRedo(), handler: () => this.redo(), }; [DEFAULT_MENU.COPY_DOWN] = { key: DEFAULT_MENU.COPY_DOWN, label: /*向下复制*/ this.getMessage("menus.copyDownLabel"), hide: (menu, { records }) => { if (!this.selectAreasData.value.columns.length) return true; return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: () => { return !this.AuthActionCopyDown(); }, handler: () => this.copydownHandel(), }; [DEFAULT_MENU.ADD_STEP_DOWN] = { key: DEFAULT_MENU.ADD_STEP_DOWN, label: /*'向下填充-递增'*/ this.getMessage("menus.addStepDownLabel"), input: true, inputDefaultValue: 1, inputProps: { min: 0, max: 999999, value: 1, }, hide: (menu, { records }) => { if (!this.selectAreasData.value.columns.length) return true; return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, validator: (...arg: any[]) => this.validatorInputValue(...arg), disabled: () => { return !this.AuthActionAddStepDown(); }, handler: (item: Menu) => { this.addStepdownHandel(item.inputProps?.value ?? 1); }, }; [DEFAULT_MENU.INSERT_BEFORE_ROWS] = { key: DEFAULT_MENU.INSERT_BEFORE_ROWS, label: /*'上方插入[input]行'*/ this.getMessage("menus.insertBeforeLabel"), input: true, inputDefaultValue: 1, inputPrecision: 0, validator: (...arg: any[]) => this.validatorInputValue(...arg), inputProps: { min: 0, max: 999999, value: 1, }, hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: () => !this.AuthActionInsertBeforeRow(), handler: (item: Menu) => { if (this.getMinStartY() !== undefined) { this.insertRows( this.getMinStartY(), "before", item.inputProps?.value ?? 1 ); } }, }; [DEFAULT_MENU.INSERT_AFTER_ROWS] = { key: DEFAULT_MENU.INSERT_AFTER_ROWS, label: /*'下方插入[input]行'*/ this.getMessage("menus.insertAfterLabel"), input: true, inputDefaultValue: 1, inputPrecision: 0, validator: (...arg: any[]) => this.validatorInputValue(...arg), inputProps: { min: 0, max: 999999, value: 1, }, hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: () => !this.AuthActionInsertAfterRow(), handler: (item: Menu) => { if (this.getMaxEndY() !== undefined) { this.insertRows( this.getMaxEndY(), "after", item.inputProps?.value ?? 1 ); } }, }; [DEFAULT_MENU.INSERT_COPY_AFTER_ROWS] = { key: DEFAULT_MENU.INSERT_COPY_AFTER_ROWS, label: /*'向下复制[input]行'*/ this.getMessage( "menus.insertAfterCopyLabel" ), input: true, inputDefaultValue: 1, inputPrecision: 0, validator: (...arg: any[]) => this.validatorInputValue(...arg), inputProps: { min: 0, max: 999999, value: 1, }, hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: () => !this.AuthActionCpyAfterRows(), handler: (item: Menu) => { if (this.getMaxEndY() !== undefined) { this.insertCopyRows( this.getMaxEndY(), "after", item.inputProps?.value ?? 1, [this.dataSource.value[this.getMaxEndY()]] ); } }, }; [DEFAULT_MENU.DELETE_ROWS] = { key: DEFAULT_MENU.DELETE_ROWS, label: /*'删除选中行'*/ this.getMessage("menus.deleteRowsLabel"), hide: (menu, { records }) => { return this.checkHasReadonly(records) || this.checkHasDisable(records) || this.checkHasSelectThead(); }, disabled: () => { return !this.AuthActionDeleteRow(); }, handler: () => this.deleteRows(), }; [DEFAULT_MENU.EXPORT_TXT] = { key: DEFAULT_MENU.EXPORT_TXT, label: /*'导出txt'*/ this.getMessage("menus.exportTxt"), // disabled: () => { // return this.checkHasSelectThead(); // }, handler: (...arg) => this.$$exportTxt(...arg), }; [DEFAULT_MENU.EXPORT_EXCEL] = { key: DEFAULT_MENU.EXPORT_EXCEL, label: /*'导出Excel'*/ this.getMessage("menus.exportExcel"), // disabled: () => { // return this.checkHasSelectThead(); // }, handler: (...arg) => this.$$exportExcel(...arg), }; private $$getMinStartX() { if (this.selectedState.startX === -1) return 0; return Math.min( this.selectedState.startX ?? 0, this.selectedState.endX ?? 0 ); } private $$getMaxEndX() { if (this.selectedState.startX === -1) return this.tileColumns.value.length - 1; return Math.max( this.selectedState.startX ?? 0, this.selectedState.endX ?? 0 ); } private $$getChoseColumns() { return this.tileColumns.value.slice( this.$$getMinStartX(), this.$$getMaxEndX() + 1 ); } private $$exportTxt (menu) { const oldSelectState = deepCopy(this.selectedState) this.checkAllRows() const data = this.getCopyDataAnyWhereSelectedDisabledCopySource() Object.assign(this.selectedState, oldSelectState) const signalBlob = new Blob([data], { type: "text/plain;charset=utf-8" }); saveAs(signalBlob, `${menu.fileName || new Date().toLocaleString()}.txt`) } private $$exportExcel (menu) { const oldSelectState = deepCopy(this.selectedState) this.checkAllRows() const data = this.getCopyDataAnyWhereSelectedDisabledCopySource() Object.assign(this.selectedState, oldSelectState) const arr = data.split(this.warpSplit).map(v => v.split(this.cellSplit)) const ws = XLSX.utils.aoa_to_sheet(arr); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); // 生成Excel文件并使用file-saver保存 const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'binary' }); const blob = new Blob([this.s2ab(wbout)], { type: 'application/octet-stream' }); saveAs(blob, `${menu.fileName || new Date().toLocaleString()}.xlsx`); } private s2ab(s: string) { // 将字符串转换为ArrayBuffer(用于file-saver) const buf = new ArrayBuffer(s.length); const view = new Uint8Array(buf); for (let i = 0; i < s.length; i++) view[i] = s.charCodeAt(i) & 0xFF; return buf; } }