| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- <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 ... ></i> -->
- <div style="display: flex; align-items: center">
- <span>{{ cell.label }}</span>
- <template v-if="cell.mode === 'checkbox'">
- <div class="checkbox-group" style="flex: 1">
- <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-if="cell.mode === 'person'">
- <div
- style="
- width: 100%;
- position: relative;
- min-height: 30px;
- flex: 1;
- "
- >
- <!-- 人员选择组件 -->
- <personSelect
- v-model="cell.value"
- :disabled="readonly || cell.readonly === 2"
- :readonly="readonly || cell.readonly === 2"
- @change="onPersonChange(rowIndex, colIndex)"
- style="width: 95%"
- :init="false"
- :selectList="userPage"
- />
- <!-- 编辑图标(仅在编辑模式下显示) -->
- <i
- v-if="edit"
- class="el-icon-edit person-edit-icon"
- @click.stop="openPersonConfig(cell, rowIndex, colIndex)"
- ></i>
- </div>
- </template>
- <!-- ========== 🆕 部门选择模式 ========== -->
- <template v-else-if="cell.mode === 'dept'">
- <div
- style="
- width: 100%;
- position: relative;
- min-height: 30px;
- flex: 1;
- "
- >
- <deptSelect
- v-model="cell.value"
- :disabled="readonly || cell.readonly === 2"
- :isBindPlan="readonly || cell.readonly === 2"
- @change="onDeptChange(rowIndex, colIndex)"
- style="width: 95%"
- placeholder="请选择部门"
- :init="false"
- :selectList="deptList"
- />
- <i
- v-if="edit"
- class="el-icon-edit person-edit-icon"
- @click.stop="openPersonConfig(cell, rowIndex, colIndex)"
- ></i>
- </div>
- </template>
- <!-- ========== 文本模式 ========== -->
- <template v-else>
- <textarea
- v-model="cell.value"
- class="templateInput"
- :style="{
- textAlign: cell.textAlign || 'left',
- flex: 1
- }"
- :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>
- </div>
- <!-- ========== 复选框模式 ========== -->
- </td>
- </template>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </template>
- <script>
- import { generateRandomString } from '@/utils/util';
- import personSelect from '@/components/CommomSelect/person-select.vue';
- import deptSelect from '@/components/CommomSelect/dept-select.vue'; // 🆕 导入部门选择组件
- import { getUserPage } from '@/api/system/organization';
- import { listOrganizations } from '@/api/system/organization'; // 🆕 导入部门列表接口
- export default {
- // 🆕 注册人员选择组件
- components: { personSelect, deptSelect },
- props: {
- id: { type: String, default: '' },
- edit: { type: Boolean, default: true },
- readonly: { type: Boolean, default: false }
- },
- data() {
- return {
- userPage: [],
- deptList: [],
- 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,
- // 右键菜单关闭监听
- menuCloseHandler: null,
- userMap: {},
- userMapLoaded: false
- };
- },
- 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'
- };
- }
- },
- beforeDestroy() {
- if (this.calcTimer) {
- clearTimeout(this.calcTimer);
- this.calcTimer = null;
- }
- if (this.menuCloseHandler) {
- document.removeEventListener('click', this.menuCloseHandler);
- this.menuCloseHandler = null;
- }
- },
- created() {
- getUserPage({ pageNum: 1, size: -1 }).then((res) => {
- this.userPage = res.list;
- });
- listOrganizations({ pageNum: 1, size: -1 }).then((res) => {
- this.deptList = res
- });
- },
- methods: {
- getTransitions(key) {
- return [
- { target: 'text', label: '转为文本模式', method: 'disableDeptMode' },
- {
- target: 'checkbox',
- label: '转为复选框模式',
- method: 'enableCheckboxMode'
- },
- {
- target: 'person',
- label: '转为人员选择模式',
- method: 'enablePersonMode'
- },
- {
- target: 'dept',
- label: '转为部门选择模式',
- method: 'enableDeptMode'
- }
- ].filter((item) => item.target !== key);
- },
- // ========== 工具 ==========
- getCellStyle(cell) {
- let width = parseFloat(cell.style?.width || cell.width || 100);
- if (isNaN(width)) width = 100;
- return {
- ...cell.style,
- width: width + 'px',
- 'text-align': cell.textAlign || 'left' // 默认居中
- };
- },
- getInput(width) {
- const w = parseFloat(width);
- const safeWidth = isNaN(w) ? 100 : w;
- return {
- id: generateRandomString(5),
- value: '',
- rowspan: 1,
- colspan: 1,
- label: '',
- width: safeWidth,
- style: { width: safeWidth },
- readonly: 1,
- mode: 'text', // 'text' | 'checkbox' | 'person'
- textAlign: 'left',
- 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;
- },
- /**
- * 检查指定 id 在当前表格中是否重复(排除某个单元格)
- * @param {string} id - 要检查的 id
- * @param {string} excludeId - 排除的单元格 id(通常是当前编辑的)
- * @returns {boolean} 是否重复
- */
- isIdDuplicate(id, excludeId) {
- // 遍历所有行和列
- 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.id === id && cell.id !== excludeId) {
- return true;
- }
- }
- }
- 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();
- if (this.menuCloseHandler) {
- document.removeEventListener('click', this.menuCloseHandler);
- this.menuCloseHandler = null;
- }
- // 创建菜单容器
- 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();
- if (this.menuCloseHandler) {
- document.removeEventListener('click', this.menuCloseHandler);
- this.menuCloseHandler = null;
- }
- };
- menu.appendChild(item);
- };
- // 关闭菜单的全局监听
- this.menuCloseHandler = (e) => {
- if (!menu.contains(e.target)) {
- menu.remove();
- document.removeEventListener('click', this.menuCloseHandler);
- this.menuCloseHandler = null;
- }
- };
- // ---- 行操作菜单 ----
- if (rowIndex === 0) {
- addItem('插入列', () => this.addColumn(colIndex));
- addItem('删除列', () => this.removeColumn(colIndex));
- addItem('', null, true);
- } else {
- addItem('插入行', () => this.addRow(rowIndex));
- addItem('删除行', () => this.removeRow(rowIndex));
- addItem('', null, true);
- }
- // ---- 模式切换
- const cell = this.tableData[rowIndex][colIndex];
- if (cell) {
- const currentMode = cell.mode || 'text';
- this.getTransitions(currentMode).forEach(({ label, method }) => {
- addItem(label, () => this[method](rowIndex, colIndex));
- });
- // 复选框模式额外选项
- if (currentMode === 'checkbox') {
- addItem('添加复选框', () => this.addCheckbox(rowIndex, colIndex));
- }
- }
- // 渲染菜单
- document.body.appendChild(menu);
- setTimeout(() => {
- document.addEventListener('click', this.menuCloseHandler);
- }, 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() {},
- enableDeptMode(rowIndex, colIndex) {
- const cell = this.tableData[rowIndex][colIndex];
- if (!cell) return;
- this.$set(cell, 'mode', 'dept');
- this.$forceUpdate();
- },
- disableDeptMode(rowIndex, colIndex) {
- const cell = this.tableData[rowIndex][colIndex];
- if (!cell) return;
- this.$set(cell, 'mode', 'text');
- this.$forceUpdate();
- },
- onDeptChange() {},
- // ========== 🆕 人员选择 ==========
- enablePersonMode(rowIndex, colIndex) {
- const cell = this.tableData[rowIndex][colIndex];
- if (!cell) return;
- // 保留当前文本作为选中值(如果 person-select 能解析,否则可置空)
- this.$set(cell, 'mode', 'person');
- // 如果希望保留当前值,可保留 cell.value
- this.$forceUpdate();
- },
- disablePersonMode(rowIndex, colIndex) {
- const cell = this.tableData[rowIndex][colIndex];
- if (!cell) return;
- // 将当前人员选择的值转换为文本(显示人员名称或ID)
- this.$set(cell, 'mode', 'text');
- // 注意:cell.value 存储的是人员ID或名称,如果希望显示标签,需要映射,这里直接显示原值
- this.$forceUpdate();
- },
- onPersonChange(rowIndex, colIndex) {
- // 触发公式计算
- // this.calculation();
- },
- // ========== 输入 ==========
- 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();
- },
- openPersonConfig(cell, rowIndex, colIndex) {
- if (!this.edit) return;
- this.domId = cell.id;
- // 获取单元格真实宽度
- let realWidth = cell.width || 100;
- const td = this.$el.querySelector(
- `td[data-row="${rowIndex}"][data-col="${colIndex}"]`
- );
- if (td) {
- realWidth = td.offsetWidth || realWidth;
- }
- this.$emit('editShow', {
- templateDivRef: 'customTextRef' + this.id,
- domObj: {
- ...cell,
- width: realWidth,
- isNoWidth: false,
- equation: this.equation[cell.id],
- units: this.units[cell.id] || {}
- }
- });
- },
- 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 }
- });
- },
- async fetchDeptMap() {
- if (this.deptMapLoaded) return;
- try {
- const res = await listOrganizations({ pageNum: 1, size: -1 });
- // 递归展平树形数据
- const flatten = (nodes) => {
- let result = [];
- nodes.forEach((node) => {
- result.push({ id: node.id, name: node.name });
- if (node.children && node.children.length) {
- result = result.concat(flatten(node.children));
- }
- });
- return result;
- };
- const flatList = flatten(res || []);
- const map = {};
- flatList.forEach((dept) => {
- map[dept.id] = dept.name || '未知部门';
- });
- this.deptMap = map;
- this.deptMapLoaded = true;
- } catch (error) {
- console.warn('获取部门列表失败,打印时将显示ID', error);
- }
- },
- // ========== 🆕 获取用户映射(供打印使用) ==========
- async fetchUserMap() {
- if (this.userMapLoaded) return;
- try {
- const res = await getUserPage({ pageNum: 1, size: -1 });
- if (res && res.list) {
- const map = {};
- res.list.forEach((user) => {
- map[user.id] =
- user.name || user.realName || user.username || '未知';
- });
- this.userMap = map;
- this.userMapLoaded = true;
- }
- } catch (error) {
- console.warn('获取人员列表失败,打印时将显示ID', error);
- }
- },
- // ========== 🆕 打印(异步 + 人员名称映射) ==========
- async printTable() {
- await this.fetchUserMap();
- await this.fetchDeptMap();
- 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());
- // 遍历所有单元格,根据模式重新构建显示内容(保留 label)
- 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];
- const td = clone.querySelector(
- `td[data-row="${r}"][data-col="${c}"]`
- );
- if (!td) continue;
- const label = cell.label || '';
- let displayHtml = '';
- td.style.padding = '3px';
- if (cell.mode === 'checkbox' && Array.isArray(cell.checkboxes)) {
- const items = cell.checkboxes
- .map((cb) => {
- const symbol = cb.checked ? '☑' : '☐';
- return `<span style="margin:0 4px;">${symbol} ${
- cb.label || ''
- }</span>`;
- })
- .join('');
- displayHtml = `<div style="display:flex; align-items:center; justify-content:center; gap:5px; flex-wrap:wrap;">${
- label ? `<span >${label}</span>` : ''
- }${items}</div>`;
- } else if (cell.mode === 'person') {
- const userName = this.userMap[cell.value] || cell.value || '';
- displayHtml = `<div style="display:flex; align-items:center; justify-content:center; gap:5px;">${
- label ? `<span >${label}</span>` : ''
- }<span style="flex:1">${userName}</span></div>`;
- } else if (cell.mode === 'dept') {
- const deptName = this.deptMap[cell.value] || cell.value || '';
- displayHtml = `<div style="display:flex; align-items:center; justify-content:center; gap:5px;">${
- label ? `<span >${label}</span>` : ''
- }<span style="flex:1">${deptName}</span></div>`;
- } else {
- console.log(cell.textAlign);
- // 文本模式
- displayHtml = `<div style="display:flex; align-items:center; justify-content:${
- cell.textAlign == 'left'
- ? 'flex-start'
- : cell.textAlign == 'right'
- ? 'flex-end'
- : 'center'
- };">${label ? `<span >${label}</span>` : ''}<span>${
- cell.value || ''
- }</span></div>`;
- }
- td.innerHTML = displayHtml;
- }
- }
- // 打印 iframe(保持不变)
- 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; 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;
- }
- }
- .person-edit-icon {
- position: absolute;
- right: 4px;
- top: 50%;
- transform: translateY(-50%);
- color: #409eff;
- font-size: 14px;
- cursor: pointer;
- opacity: 0.6;
- transition: opacity 0.2s;
- &:hover {
- opacity: 1;
- }
- }
- </style>
|