|
|
@@ -0,0 +1,901 @@
|
|
|
+<template>
|
|
|
+ <div ref="tableContainer" style="margin-top: 10px; position: relative">
|
|
|
+ <!-- 合并选择框 -->
|
|
|
+ <div v-if="isSelecting" class="selection-box" :style="selectionStyle"></div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div v-if="edit">
|
|
|
+ <el-button type="primary" @click="addColumn(-1)">新增列</el-button>
|
|
|
+ <el-button type="primary" @click="addRow(-1)">新增行</el-button>
|
|
|
+ <el-button type="primary" @click="copy">复制表格</el-button>
|
|
|
+ <el-button type="primary" @click="printTable">预览表格</el-button>
|
|
|
+ <el-checkbox v-model="isMerge" style="margin-left: 10px">合并单元格</el-checkbox>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 表格主体 -->
|
|
|
+ <div class="table" style="margin-top: 10px">
|
|
|
+ <table
|
|
|
+ class="custom-table"
|
|
|
+ @mousedown="startSelecting"
|
|
|
+ @mousemove="updateSelection"
|
|
|
+ @mouseup="stopSelecting"
|
|
|
+ >
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, rowIndex) in tableData" :key="'row-' + rowIndex">
|
|
|
+ <template v-for="(cell, colIndex) in row">
|
|
|
+ <td
|
|
|
+ v-if="visibleMap[rowIndex + '-' + colIndex]"
|
|
|
+ :key="cell.id"
|
|
|
+ :rowspan="cell.rowspan"
|
|
|
+ :colspan="cell.colspan"
|
|
|
+ :style="getCellStyle(cell)"
|
|
|
+ class="tableTd"
|
|
|
+ :data-row="rowIndex"
|
|
|
+ :data-col="colIndex"
|
|
|
+ @mouseenter="onCellEnter(rowIndex, colIndex)"
|
|
|
+ @contextmenu.prevent="onRightClick($event, rowIndex, colIndex)"
|
|
|
+ >
|
|
|
+ <!-- 表头操作图标 -->
|
|
|
+ <i
|
|
|
+ v-if="edit && rowIndex === 0"
|
|
|
+ class="el-icon-delete delete"
|
|
|
+ @click.stop="removeColumn(colIndex)"
|
|
|
+ ></i>
|
|
|
+ <i
|
|
|
+ v-if="edit && rowIndex === 0"
|
|
|
+ class="el-icon-circle-plus-outline add"
|
|
|
+ @click.stop="addColumn(colIndex)"
|
|
|
+ ></i>
|
|
|
+ <i
|
|
|
+ v-if="edit && rowIndex !== 0 && isLastVisibleCell(rowIndex, colIndex)"
|
|
|
+ class="el-icon-delete deleteRow"
|
|
|
+ @click.stop="removeRow(rowIndex)"
|
|
|
+ ></i>
|
|
|
+ <i
|
|
|
+ v-if="edit && rowIndex !== 0 && colIndex !== row.length - 1"
|
|
|
+ class="el-icon-circle-plus-outline addRow"
|
|
|
+ @click.stop="addRow(rowIndex)"
|
|
|
+ ></i>
|
|
|
+
|
|
|
+ <!-- 复选框模式 -->
|
|
|
+ <template v-if="cell.mode === 'checkbox'">
|
|
|
+ <div class="checkbox-group">
|
|
|
+ <div
|
|
|
+ v-for="cb in cell.checkboxes"
|
|
|
+ :key="cb.id"
|
|
|
+ class="checkbox-item"
|
|
|
+ >
|
|
|
+ <input type="checkbox" v-model="cb.checked" />
|
|
|
+ <input
|
|
|
+ v-if="edit"
|
|
|
+ type="text"
|
|
|
+ v-model="cb.label"
|
|
|
+ class="checkbox-label-input"
|
|
|
+ @input="onCheckboxLabelInput(rowIndex, colIndex, cb.id)"
|
|
|
+ />
|
|
|
+ <span v-else>{{ cb.label }}</span>
|
|
|
+ <i
|
|
|
+ v-if="edit"
|
|
|
+ class="el-icon-delete"
|
|
|
+ @click.stop="removeCheckbox(rowIndex, colIndex, cb.id)"
|
|
|
+ ></i>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="edit"
|
|
|
+ class="add-checkbox"
|
|
|
+ @click="addCheckbox(rowIndex, colIndex)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-circle-plus-outline"></i> 新增选项
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 文本模式 -->
|
|
|
+ <template v-else>
|
|
|
+ <textarea
|
|
|
+ v-model="cell.value"
|
|
|
+ class="templateInput"
|
|
|
+ :id="cell.id"
|
|
|
+ :ref="cell.id + 'ref'"
|
|
|
+ :readonly="cell.readonly === 2 || readonly"
|
|
|
+ @mousedown="onCellMouseDown"
|
|
|
+ @click="inputClick(cell, rowIndex === 0 ? 'columns' : null, $event)"
|
|
|
+ @input="onInput($event)"
|
|
|
+ autocomplete="off"
|
|
|
+ ></textarea>
|
|
|
+ </template>
|
|
|
+ </td>
|
|
|
+ </template>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { generateRandomString } from '@/utils/util';
|
|
|
+import VueDraggable from 'vuedraggable';
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: { VueDraggable },
|
|
|
+ props: {
|
|
|
+ id: { type: String, default: '' },
|
|
|
+ edit: { type: Boolean, default: true },
|
|
|
+ readonly: { type: Boolean, default: false }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ form: null,
|
|
|
+ valueObj: {},
|
|
|
+ tableData: [],
|
|
|
+ units: {},
|
|
|
+ equation: {},
|
|
|
+ domId: '',
|
|
|
+ currentRowIndex: 0,
|
|
|
+ currentColumnIndex: 0,
|
|
|
+ // 合并相关
|
|
|
+ isMerge: false,
|
|
|
+ isSelecting: false,
|
|
|
+ startX: 0,
|
|
|
+ startY: 0,
|
|
|
+ endX: 0,
|
|
|
+ endY: 0,
|
|
|
+ startRow: -1,
|
|
|
+ startCol: -1,
|
|
|
+ endRow: -1,
|
|
|
+ endCol: -1,
|
|
|
+ // 防抖定时器
|
|
|
+ calcTimer: null
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ visibleMap() {
|
|
|
+ const map = {};
|
|
|
+ const rows = this.tableData.length;
|
|
|
+ if (!rows) return map;
|
|
|
+ const cols = this.tableData[0]?.length || 0;
|
|
|
+
|
|
|
+ for (let r = 0; r < rows; r++) {
|
|
|
+ for (let c = 0; c < cols; c++) {
|
|
|
+ const key = `${r}-${c}`;
|
|
|
+ let hidden = false;
|
|
|
+
|
|
|
+ for (let rr = r - 1; rr >= 0; rr--) {
|
|
|
+ for (let cc = 0; cc < this.tableData[rr].length; cc++) {
|
|
|
+ const cell = this.tableData[rr][cc];
|
|
|
+ if (
|
|
|
+ cell.rowspan > 1 &&
|
|
|
+ rr + cell.rowspan > r &&
|
|
|
+ cc <= c &&
|
|
|
+ cc + cell.colspan > c
|
|
|
+ ) {
|
|
|
+ hidden = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (hidden) break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hidden) {
|
|
|
+ for (let cc = c - 1; cc >= 0; cc--) {
|
|
|
+ const cell = this.tableData[r][cc];
|
|
|
+ if (cell.colspan > 1 && cc + cell.colspan > c) {
|
|
|
+ hidden = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ map[key] = !hidden;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ },
|
|
|
+
|
|
|
+ selectionStyle() {
|
|
|
+ const container = this.$refs.tableContainer;
|
|
|
+ if (!container) return {};
|
|
|
+ const rect = container.getBoundingClientRect();
|
|
|
+ return {
|
|
|
+ position: 'absolute',
|
|
|
+ zIndex: 100,
|
|
|
+ left: `${Math.min(this.startX, this.endX) - rect.left}px`,
|
|
|
+ top: `${Math.min(this.startY, this.endY) - rect.top}px`,
|
|
|
+ width: `${Math.abs(this.endX - this.startX)}px`,
|
|
|
+ height: `${Math.abs(this.endY - this.startY)}px`,
|
|
|
+ border: '2px dashed black',
|
|
|
+ pointerEvents: 'none'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // ========== 工具 ==========
|
|
|
+ getCellStyle(cell) {
|
|
|
+ let width = parseFloat(cell.style?.width || cell.width || 100);
|
|
|
+ if (isNaN(width)) width = 100;
|
|
|
+ return { ...cell.style, width: width + 'px' };
|
|
|
+ },
|
|
|
+
|
|
|
+ getInput(width) {
|
|
|
+ const w = parseFloat(width);
|
|
|
+ const safeWidth = isNaN(w) ? 100 : w;
|
|
|
+ return {
|
|
|
+ id: generateRandomString(5),
|
|
|
+ value: '',
|
|
|
+ rowspan: 1,
|
|
|
+ colspan: 1,
|
|
|
+ width: safeWidth,
|
|
|
+ style: { width: safeWidth },
|
|
|
+ readonly: 1,
|
|
|
+ mode: 'text',
|
|
|
+ checkboxes: []
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ getIndex(id) {
|
|
|
+ for (let r = 0; r < this.tableData.length; r++) {
|
|
|
+ for (let c = 0; c < this.tableData[r].length; c++) {
|
|
|
+ if (this.tableData[r][c].id === id)
|
|
|
+ return { rowIndex: r, colIndex: c };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return { rowIndex: -1, colIndex: -1 };
|
|
|
+ },
|
|
|
+
|
|
|
+ isLastVisibleCell(rowIndex, colIndex) {
|
|
|
+ const row = this.tableData[rowIndex];
|
|
|
+ for (let c = row.length - 1; c >= 0; c--) {
|
|
|
+ if (this.visibleMap[`${rowIndex}-${c}`]) {
|
|
|
+ return c === colIndex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 自动调整高度 ==========
|
|
|
+ autoResizeAll() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.$el.querySelectorAll('.templateInput').forEach((ta) => {
|
|
|
+ ta.style.height = 'auto';
|
|
|
+ ta.style.height = ta.scrollHeight + 'px';
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 行列操作 ==========
|
|
|
+ addColumn(colIndex) {
|
|
|
+ if (colIndex === -1) {
|
|
|
+ if (this.tableData.length === 0) {
|
|
|
+ this.tableData = [[this.getInput()]];
|
|
|
+ } else {
|
|
|
+ this.tableData.forEach((row) => row.push(this.getInput()));
|
|
|
+ }
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let insertAt = colIndex + 1;
|
|
|
+ this.tableData.forEach((row) => {
|
|
|
+ const cell = row[colIndex];
|
|
|
+ if (cell && cell.colspan > 1) {
|
|
|
+ insertAt = Math.max(insertAt, colIndex + cell.colspan);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.tableData.forEach((row) => {
|
|
|
+ let covered = false;
|
|
|
+ for (let c = 0; c < insertAt && c < row.length; c++) {
|
|
|
+ const cell = row[c];
|
|
|
+ if (cell.colspan > 1 && c + cell.colspan > insertAt) {
|
|
|
+ cell.colspan += 1;
|
|
|
+ covered = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!covered) {
|
|
|
+ const raw = row[colIndex]?.style?.width ?? row[colIndex]?.width ?? 100;
|
|
|
+ row.splice(insertAt, 0, this.getInput(raw));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ },
|
|
|
+
|
|
|
+ removeColumn(colIndex) {
|
|
|
+ for (let r = 0; r < this.tableData.length; r++) {
|
|
|
+ const row = this.tableData[r];
|
|
|
+ for (let rr = r - 1; rr >= 0; rr--) {
|
|
|
+ for (let cc = 0; cc < this.tableData[rr].length; cc++) {
|
|
|
+ const cell = this.tableData[rr][cc];
|
|
|
+ if (
|
|
|
+ cell.rowspan > 1 &&
|
|
|
+ rr + cell.rowspan > r &&
|
|
|
+ cell.colspan > 1 &&
|
|
|
+ cc <= colIndex &&
|
|
|
+ cc + cell.colspan > colIndex
|
|
|
+ ) {
|
|
|
+ cell.colspan -= 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (let c = colIndex - 1; c >= 0; c--) {
|
|
|
+ const leftCell = row[c];
|
|
|
+ if (leftCell && leftCell.colspan > 1 && c + leftCell.colspan > colIndex) {
|
|
|
+ leftCell.colspan -= 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const cell = row[colIndex];
|
|
|
+ if (cell && cell.colspan > 1 && colIndex + 1 < row.length) {
|
|
|
+ row[colIndex + 1].colspan = cell.colspan - 1;
|
|
|
+ row[colIndex + 1].rowspan = cell.rowspan;
|
|
|
+ row[colIndex + 1].style = { ...cell.style };
|
|
|
+ }
|
|
|
+ row.splice(colIndex, 1);
|
|
|
+ }
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ },
|
|
|
+
|
|
|
+ addRow(rowIndex) {
|
|
|
+ if (rowIndex === -1) {
|
|
|
+ if (this.tableData.length === 0) {
|
|
|
+ this.tableData = [[this.getInput()]];
|
|
|
+ } else {
|
|
|
+ this.tableData.push(this.tableData[0].map(() => this.getInput()));
|
|
|
+ }
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let insertAt = rowIndex + 1;
|
|
|
+ for (let c = 0; c < this.tableData[rowIndex].length; c++) {
|
|
|
+ const cell = this.tableData[rowIndex][c];
|
|
|
+ if (cell.rowspan > 1) {
|
|
|
+ insertAt = Math.max(insertAt, rowIndex + cell.rowspan);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const newRow = [];
|
|
|
+ for (let c = 0; c < this.tableData[0].length; c++) {
|
|
|
+ let covered = false;
|
|
|
+ for (let r = 0; r < insertAt && r < this.tableData.length; r++) {
|
|
|
+ const cell = this.tableData[r][c];
|
|
|
+ if (cell.rowspan > 1 && r + cell.rowspan > insertAt) {
|
|
|
+ cell.rowspan += 1;
|
|
|
+ covered = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!covered) {
|
|
|
+ const raw = this.tableData[0][c]?.style?.width ?? this.tableData[0][c]?.width ?? 100;
|
|
|
+ newRow.push(this.getInput(raw));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.tableData.splice(insertAt, 0, newRow);
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ },
|
|
|
+
|
|
|
+ removeRow(rowIndex) {
|
|
|
+ for (let c = 0; c < this.tableData[rowIndex].length; c++) {
|
|
|
+ const cell = this.tableData[rowIndex][c];
|
|
|
+ if (cell.rowspan > 1) {
|
|
|
+ const next = this.tableData[rowIndex + 1]?.[c];
|
|
|
+ if (next) {
|
|
|
+ next.rowspan = cell.rowspan - 1;
|
|
|
+ next.colspan = cell.colspan;
|
|
|
+ next.value = cell.value;
|
|
|
+ next.style = { ...cell.style };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.tableData.splice(rowIndex, 1);
|
|
|
+ this.$nextTick(this.autoResizeAll);
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 合并 ==========
|
|
|
+ startSelecting(event) {
|
|
|
+ if (!this.isMerge) return;
|
|
|
+ this.isSelecting = true;
|
|
|
+ const rect = this.$refs.tableContainer.getBoundingClientRect();
|
|
|
+ this.startX = event.clientX;
|
|
|
+ this.startY = event.clientY;
|
|
|
+ this.endX = this.startX;
|
|
|
+ this.endY = this.startY;
|
|
|
+
|
|
|
+ const td = event.target.closest('td');
|
|
|
+ if (td) {
|
|
|
+ this.startRow = Number(td.dataset.row);
|
|
|
+ this.startCol = Number(td.dataset.col);
|
|
|
+ this.endRow = this.startRow;
|
|
|
+ this.endCol = this.startCol;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ updateSelection(event) {
|
|
|
+ if (this.isSelecting) {
|
|
|
+ this.endX = event.clientX;
|
|
|
+ this.endY = event.clientY;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ stopSelecting() {
|
|
|
+ if (!this.isSelecting) return;
|
|
|
+ this.isSelecting = false;
|
|
|
+ this.calculateSelectedItems();
|
|
|
+ this.startRow = this.startCol = this.endRow = this.endCol = -1;
|
|
|
+ },
|
|
|
+
|
|
|
+ onCellEnter(rowIndex, colIndex) {
|
|
|
+ if (this.isSelecting) {
|
|
|
+ this.endRow = rowIndex;
|
|
|
+ this.endCol = colIndex;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ calculateSelectedItems() {
|
|
|
+ if (
|
|
|
+ this.startRow < 0 ||
|
|
|
+ this.startCol < 0 ||
|
|
|
+ this.endRow < 0 ||
|
|
|
+ this.endCol < 0
|
|
|
+ )
|
|
|
+ return;
|
|
|
+ if (this.startRow === this.endRow && this.startCol === this.endCol)
|
|
|
+ return;
|
|
|
+
|
|
|
+ const minRow = Math.min(this.startRow, this.endRow);
|
|
|
+ const maxRow = Math.max(this.startRow, this.endRow);
|
|
|
+ const minCol = Math.min(this.startCol, this.endCol);
|
|
|
+ const maxCol = Math.max(this.startCol, this.endCol);
|
|
|
+
|
|
|
+ if (minRow === 0 && maxRow > 0) return;
|
|
|
+
|
|
|
+ for (let r = minRow; r <= maxRow; r++) {
|
|
|
+ for (let c = minCol; c <= maxCol; c++) {
|
|
|
+ const cell = this.tableData[r]?.[c];
|
|
|
+ if (!cell) return;
|
|
|
+ if ((cell.rowspan > 1 || cell.colspan > 1) && (r !== minRow || c !== minCol)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const cell = this.tableData[minRow][minCol];
|
|
|
+ this.$set(cell, 'rowspan', maxRow - minRow + 1);
|
|
|
+ this.$set(cell, 'colspan', maxCol - minCol + 1);
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ const ref = this.$refs[cell.id + 'ref'];
|
|
|
+ const ta = Array.isArray(ref) ? ref[0] : ref;
|
|
|
+ const td = ta?.closest?.('td');
|
|
|
+ if (td) {
|
|
|
+ const realWidth = td.offsetWidth;
|
|
|
+ this.$set(cell, 'width', realWidth);
|
|
|
+ this.$set(cell.style, 'width', realWidth);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ this.startRow = this.startCol = this.endRow = this.endCol = -1;
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 右键菜单 ==========
|
|
|
+ onRightClick(event, rowIndex, colIndex) {
|
|
|
+ event.preventDefault();
|
|
|
+ this.currentRowIndex = rowIndex;
|
|
|
+ this.currentColumnIndex = colIndex;
|
|
|
+
|
|
|
+ const existingMenu = document.querySelector('.custom-context-menu');
|
|
|
+ if (existingMenu) existingMenu.remove();
|
|
|
+
|
|
|
+ const menu = document.createElement('div');
|
|
|
+ menu.className = 'custom-context-menu';
|
|
|
+ menu.style.cssText = `
|
|
|
+ position: fixed;
|
|
|
+ left: ${event.clientX}px;
|
|
|
+ top: ${event.clientY}px;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ box-shadow: 2px 2px 8px rgba(0,0,0,0.2);
|
|
|
+ z-index: 10000;
|
|
|
+ padding: 5px 0;
|
|
|
+ min-width: 120px;
|
|
|
+ `;
|
|
|
+
|
|
|
+ const addItem = (text, onClick, isSeparator = false) => {
|
|
|
+ if (isSeparator) {
|
|
|
+ const hr = document.createElement('hr');
|
|
|
+ hr.style.margin = '5px 0';
|
|
|
+ menu.appendChild(hr);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const item = document.createElement('div');
|
|
|
+ item.textContent = text;
|
|
|
+ item.style.cssText = 'padding: 5px 15px; cursor: pointer; white-space: nowrap;';
|
|
|
+ item.onmouseenter = () => (item.style.backgroundColor = '#f0f0f0');
|
|
|
+ item.onmouseleave = () => (item.style.backgroundColor = '');
|
|
|
+ item.onclick = () => {
|
|
|
+ if (onClick) onClick();
|
|
|
+ menu.remove();
|
|
|
+ document.removeEventListener('click', closeMenu);
|
|
|
+ };
|
|
|
+ menu.appendChild(item);
|
|
|
+ };
|
|
|
+
|
|
|
+ const closeMenu = (e) => {
|
|
|
+ if (!menu.contains(e.target)) {
|
|
|
+ menu.remove();
|
|
|
+ document.removeEventListener('click', closeMenu);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if (rowIndex === 0) {
|
|
|
+ addItem('插入列', () => this.addColumn(colIndex));
|
|
|
+ addItem('删除列', () => this.removeColumn(colIndex));
|
|
|
+ } else {
|
|
|
+ const cell = this.tableData[rowIndex][colIndex];
|
|
|
+ addItem('插入行', () => this.addRow(rowIndex));
|
|
|
+ addItem('删除行', () => this.removeRow(rowIndex));
|
|
|
+ addItem('', null, true);
|
|
|
+ if (cell.mode === 'text' || !cell.mode) {
|
|
|
+ addItem('转为复选框模式', () => this.enableCheckboxMode(rowIndex, colIndex));
|
|
|
+ } else {
|
|
|
+ addItem('转为文本模式', () => this.disableCheckboxMode(rowIndex, colIndex));
|
|
|
+ addItem('添加复选框', () => this.addCheckbox(rowIndex, colIndex));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ document.body.appendChild(menu);
|
|
|
+ setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 复选框 ==========
|
|
|
+ enableCheckboxMode(rowIndex, colIndex) {
|
|
|
+ const cell = this.tableData[rowIndex][colIndex];
|
|
|
+ if (!cell) return;
|
|
|
+ const currentText = cell.value || '';
|
|
|
+ this.$set(cell, 'mode', 'checkbox');
|
|
|
+ this.$set(cell, 'checkboxes', [{ id: generateRandomString(6), label: currentText, checked: false }]);
|
|
|
+ cell.value = '';
|
|
|
+ },
|
|
|
+
|
|
|
+ disableCheckboxMode(rowIndex, colIndex) {
|
|
|
+ const cell = this.tableData[rowIndex][colIndex];
|
|
|
+ if (!cell) return;
|
|
|
+ const text = cell.checkboxes.map(cb => cb.label).filter(Boolean).join('; ');
|
|
|
+ this.$set(cell, 'mode', 'text');
|
|
|
+ this.$set(cell, 'value', text);
|
|
|
+ this.$set(cell, 'checkboxes', []);
|
|
|
+ },
|
|
|
+
|
|
|
+ addCheckbox(rowIndex, colIndex) {
|
|
|
+ const cell = this.tableData[rowIndex][colIndex];
|
|
|
+ if (!cell || cell.mode !== 'checkbox') return;
|
|
|
+ if (!cell.checkboxes) cell.checkboxes = [];
|
|
|
+ cell.checkboxes.push({ id: generateRandomString(6), label: '', checked: false });
|
|
|
+ this.$forceUpdate();
|
|
|
+ },
|
|
|
+
|
|
|
+ removeCheckbox(rowIndex, colIndex, cbId) {
|
|
|
+ const cell = this.tableData[rowIndex][colIndex];
|
|
|
+ if (!cell || cell.mode !== 'checkbox') return;
|
|
|
+ const idx = cell.checkboxes.findIndex(cb => cb.id === cbId);
|
|
|
+ if (idx !== -1) {
|
|
|
+ cell.checkboxes.splice(idx, 1);
|
|
|
+ if (cell.checkboxes.length === 0) {
|
|
|
+ this.disableCheckboxMode(rowIndex, colIndex);
|
|
|
+ } else {
|
|
|
+ this.$forceUpdate();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onCheckboxLabelInput() {},
|
|
|
+
|
|
|
+ // ========== 输入 ==========
|
|
|
+ onInput(event) {
|
|
|
+ const ta = event.target;
|
|
|
+ ta.style.height = 'auto';
|
|
|
+ ta.style.height = ta.scrollHeight + 'px';
|
|
|
+ this.debouncedCalculation();
|
|
|
+ },
|
|
|
+
|
|
|
+ debouncedCalculation() {
|
|
|
+ if (this.calcTimer) clearTimeout(this.calcTimer);
|
|
|
+ this.calcTimer = setTimeout(() => {
|
|
|
+ this.calculation();
|
|
|
+ this.calcTimer = null;
|
|
|
+ }, 300);
|
|
|
+ },
|
|
|
+
|
|
|
+ calculation() {
|
|
|
+ this.$emit('calculation');
|
|
|
+ },
|
|
|
+
|
|
|
+ onCellMouseDown(event) {
|
|
|
+ if (this.isMerge) event.preventDefault();
|
|
|
+ },
|
|
|
+
|
|
|
+ inputClick(item, type, event) {
|
|
|
+ if (!this.edit) return;
|
|
|
+ this.domId = item.id;
|
|
|
+ const ref = this.$refs[item.id + 'ref'];
|
|
|
+ const ta = Array.isArray(ref) ? ref[0] : ref;
|
|
|
+ const td = ta?.closest?.('td');
|
|
|
+ const realWidth = td ? td.offsetWidth : parseFloat(item.style?.width || item.width || 100);
|
|
|
+
|
|
|
+ this.$emit('editShow', {
|
|
|
+ templateDivRef: 'customTextRef' + this.id,
|
|
|
+ domObj: {
|
|
|
+ ...item,
|
|
|
+ width: realWidth,
|
|
|
+ isNoWidth: type === 'columns',
|
|
|
+ equation: this.equation[item.id],
|
|
|
+ units: this.units[item.id] || {}
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 数据通信 ==========
|
|
|
+ equationValue({ domId, value }) {
|
|
|
+ const { rowIndex, colIndex } = this.getIndex(domId);
|
|
|
+ if (rowIndex >= 0 && colIndex >= 0) {
|
|
|
+ this.$set(this.tableData[rowIndex][colIndex], 'value', value);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getValue() {
|
|
|
+ return {
|
|
|
+ form: null,
|
|
|
+ equation: this.equation,
|
|
|
+ units: this.units,
|
|
|
+ valueObj: {
|
|
|
+ tableData: this.tableData
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ init({ form, valueObj, equation, units }) {
|
|
|
+ this.form = form;
|
|
|
+ this.tableData = valueObj.tableData || valueObj.columns || [];
|
|
|
+ this.equation = equation || {};
|
|
|
+ this.units = units || {};
|
|
|
+ this.autoResizeAll();
|
|
|
+ },
|
|
|
+
|
|
|
+ editInputChange(domObj) {
|
|
|
+ const data = JSON.parse(JSON.stringify(domObj));
|
|
|
+ if (data.equation) this.equation[data.id] = data.equation;
|
|
|
+ if (data.units) this.units[data.id] = data.units;
|
|
|
+
|
|
|
+ const w = Number(data.width);
|
|
|
+ if (!isNaN(w)) {
|
|
|
+ data.style = { ...(data.style || {}), width: w };
|
|
|
+ data.width = w;
|
|
|
+ } else if (data.style && data.style.width != null) {
|
|
|
+ data.width = Number(data.style.width);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { rowIndex, colIndex } = this.getIndex(this.domId);
|
|
|
+ if (rowIndex >= 0 && colIndex >= 0) {
|
|
|
+ this.$set(this.tableData[rowIndex], colIndex, data);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ copy() {
|
|
|
+ const tableData = JSON.parse(JSON.stringify(this.tableData));
|
|
|
+ tableData.forEach(row => row.forEach(cell => cell.id = generateRandomString(5)));
|
|
|
+ this.$emit('copy', {
|
|
|
+ value: null,
|
|
|
+ equation: { ...this.equation },
|
|
|
+ units: { ...this.units },
|
|
|
+ valueObj: { tableData }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 打印 ==========
|
|
|
+ printTable() {
|
|
|
+ const tableEl = this.$el.querySelector('.custom-table');
|
|
|
+ if (!tableEl) return;
|
|
|
+
|
|
|
+ const clone = tableEl.cloneNode(true);
|
|
|
+ clone.querySelectorAll('.delete, .add, .deleteRow, .addRow, .selection-box')
|
|
|
+ .forEach(el => el.remove());
|
|
|
+
|
|
|
+ clone.querySelectorAll('textarea').forEach(ta => {
|
|
|
+ const div = document.createElement('div');
|
|
|
+ div.textContent = ta.value;
|
|
|
+ div.style.cssText = 'text-align:center;word-break:break-all;padding:4px;white-space:pre-wrap;';
|
|
|
+ ta.parentNode.replaceChild(div, ta);
|
|
|
+ });
|
|
|
+
|
|
|
+ for (let r = 0; r < this.tableData.length; r++) {
|
|
|
+ for (let c = 0; c < this.tableData[r].length; c++) {
|
|
|
+ const cell = this.tableData[r][c];
|
|
|
+ if (cell.mode === 'checkbox' && Array.isArray(cell.checkboxes)) {
|
|
|
+ const td = clone.querySelector(`td[data-row="${r}"][data-col="${c}"]`);
|
|
|
+ if (td) {
|
|
|
+ const container = document.createElement('div');
|
|
|
+ container.style.cssText = 'display:flex; flex-wrap:wrap; gap:5px; justify-content:center;';
|
|
|
+ cell.checkboxes.forEach(cb => {
|
|
|
+ const item = document.createElement('div');
|
|
|
+ item.style.cssText = 'display:inline-flex; align-items:center; gap:5px; white-space:nowrap;';
|
|
|
+ const symbolSpan = document.createElement('span');
|
|
|
+ symbolSpan.textContent = cb.checked ? '☑' : '☐';
|
|
|
+ symbolSpan.style.fontSize = '14px';
|
|
|
+ const labelSpan = document.createElement('span');
|
|
|
+ labelSpan.textContent = cb.label || '';
|
|
|
+ item.appendChild(symbolSpan);
|
|
|
+ item.appendChild(labelSpan);
|
|
|
+ container.appendChild(item);
|
|
|
+ });
|
|
|
+ td.innerHTML = '';
|
|
|
+ td.appendChild(container);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const iframe = document.createElement('iframe');
|
|
|
+ iframe.style.cssText = 'position:absolute;width:0;height:0;border:0;';
|
|
|
+ document.body.appendChild(iframe);
|
|
|
+ const doc = iframe.contentWindow.document;
|
|
|
+ doc.write(`
|
|
|
+ <!DOCTYPE html>
|
|
|
+ <html>
|
|
|
+ <head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <style>
|
|
|
+ body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
|
|
+ table { width: 100%; border-collapse: collapse; table-layout: auto; }
|
|
|
+ td { border: 1px solid #000; vertical-align: middle; text-align: center; padding: 1px; font-size: 12px;padding:0px }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>${clone.outerHTML}</body>
|
|
|
+ </html>
|
|
|
+ `);
|
|
|
+ doc.close();
|
|
|
+ iframe.contentWindow.focus();
|
|
|
+ iframe.contentWindow.print();
|
|
|
+ setTimeout(() => document.body.removeChild(iframe), 1000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.custom-table {
|
|
|
+ border-collapse: collapse;
|
|
|
+ width: auto;
|
|
|
+ td {
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 0;
|
|
|
+ min-height: 20px;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+}
|
|
|
+:deep(.templateInput) {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 20px;
|
|
|
+ height: auto;
|
|
|
+ border: none;
|
|
|
+ text-align: center;
|
|
|
+ background-color: #fff;
|
|
|
+ resize: none;
|
|
|
+ padding: 4px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
+ display: block;
|
|
|
+ &:focus {
|
|
|
+ border-color: #66afe9;
|
|
|
+ outline: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+.deleteRow {
|
|
|
+ display: block !important;
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0px;
|
|
|
+ right: -15px;
|
|
|
+ color: #f56c6c;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+.delete,
|
|
|
+.add,
|
|
|
+.addRow {
|
|
|
+ display: none !important;
|
|
|
+}
|
|
|
+.tableTd:hover {
|
|
|
+ .delete {
|
|
|
+ display: block !important;
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ top: 0;
|
|
|
+ color: #f56c6c;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ .add {
|
|
|
+ display: block !important;
|
|
|
+ position: absolute;
|
|
|
+ right: 20px;
|
|
|
+ top: 0;
|
|
|
+ color: #409eff;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ .addRow {
|
|
|
+ display: block !important;
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0px;
|
|
|
+ right: 10px;
|
|
|
+ color: #409eff;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+}
|
|
|
+.checkbox-group {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 4px;
|
|
|
+ .checkbox-item {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ background: #fff;
|
|
|
+ input[type='checkbox'] {
|
|
|
+ margin: 0;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .checkbox-label-input {
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 2px;
|
|
|
+ padding: 2px 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ min-width: 4em;
|
|
|
+ width: auto;
|
|
|
+ background: #fff;
|
|
|
+ &:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #409eff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .el-icon-delete {
|
|
|
+ color: #f56c6c;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .add-checkbox {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ color: #409eff;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: nowrap;
|
|
|
+ &:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+@media print {
|
|
|
+ .add-checkbox,
|
|
|
+ .checkbox-item .el-icon-delete {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+ .checkbox-label-input {
|
|
|
+ border: none !important;
|
|
|
+ background: transparent !important;
|
|
|
+ padding: 0 !important;
|
|
|
+ min-width: auto !important;
|
|
|
+ width: auto !important;
|
|
|
+ }
|
|
|
+ .checkbox-item {
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|