// ==UserScript== // @name 表格排序与筛选增强(支持iframe) // @namespace http://tampermonkey.net/ // @version 3.13 // @description 在表格顶部添加排序和筛选行,支持字符串升序/降序排序和多种筛选方式,支持多列排序与多列筛选;原始查询刷新时自动撤销筛选/排序,避免污染页面渲染;支持计算列(按列名表达式生成新列,可参与排序/筛选) // @author wangjiati // @homepageURL https://scriptcat.org/zh-CN/script-show-page/7261 // @match https://mh.jlcerp.com/* // @match https://nw.jlcerp.com/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // 存储表格原始数据和筛选状态 const tableData = new Map(); // 拖拽相关变量 let dragData = { isDragging: false, startX: 0, startY: 0, initialX: 0, initialY: 0 }; // 添加全局样式 GM_addStyle(` /* 排序和筛选控制行 */ .table-control-row { background-color: #f8f9fa !important; border-bottom: 2px solid #dee2e6 !important; max-height: 24px !important; height: 24px !important; line-height: 20px !important; } .table-control-row td, .table-control-row th { max-height: 24px !important; height: 24px !important; padding: 2px 4px !important; vertical-align: middle !important; } /* 控制容器 */ .control-container { display: flex !important; flex-direction: column !important; gap: 4px !important; min-height: 24px !important; height: 24px !important; max-height: 24px !important; padding: 2px !important; } /* 按钮容器 */ .btn-container { display: flex !important; gap: 2px !important; justify-content: center !important; max-height: 18px !important; min-height: 18px !important; height: 18px !important; } /* 基础按钮样式 */ .control-btn { padding: 0px 0px !important; border: 1px solid #ccc !important; border-radius: 3px !important; cursor: pointer !important; font-size: 11px !important; background: white !important; transition: all 0.2s !important; user-select: none !important; min-width: 10px !important; text-align: center !important; line-height: 1.2 !important; } .control-btn:hover { background-color: #e9ecef !important; transform: translateY(-1px) !important; } /* 排序按钮 */ .sort-btn { color: #495057 !important; } .sort-btn.active-asc { background-color: #d1e7dd !important; color: #0f5132 !important; border-color: #badbcc !important; } .sort-btn.active-desc { background-color: #f8d7da !important; color: #842029 !important; border-color: #f5c2c7 !important; } /* 筛选按钮 */ .filter-btn { color: #495057 !important; } .filter-btn.active { background-color: #cfe2ff !important; color: #084298 !important; border-color: #b6d4fe !important; } /* 筛选指示器 */ .filter-indicator { font-size: 9px !important; color: #6c757d !important; margin-top: 0 !important; text-align: center !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 100% !important; padding: 0 2px !important; max-height: 10px !important; min-height: 10px !important; height: 10px !important; line-height: 10px !important; } /* 筛选弹窗 */ .filter-modal-overlay { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 999999 !important; display: flex !important; justify-content: center !important; align-items: flex-start !important; padding-top: 50px !important; pointer-events: none !important; } .filter-modal { background-color: white !important; border-radius: 6px !important; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15) !important; width: 280px !important; max-width: 90vw !important; z-index: 1000000 !important; font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif !important; border: 1px solid #ccc !important; pointer-events: auto !important; cursor: move !important; position: fixed !important; } .filter-modal-header { padding: 10px 12px !important; border-bottom: 1px solid #dee2e6 !important; background-color: #f8f9fa !important; border-radius: 6px 6px 0 0 !important; font-weight: 600 !important; font-size: 14px !important; color: #212529 !important; display: flex !important; justify-content: space-between !important; align-items: center !important; cursor: move !important; } .filter-modal-close { background: none !important; border: none !important; font-size: 16px !important; cursor: pointer !important; color: #6c757d !important; padding: 0 !important; width: 20px !important; height: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 3px !important; } .filter-modal-close:hover { background-color: #e9ecef !important; color: #495057 !important; } .filter-modal-body { padding: 12px !important; } .filter-form-group { margin-bottom: 12px !important; } .filter-form-label { display: block !important; margin-bottom: 4px !important; font-weight: 500 !important; color: #495057 !important; font-size: 13px !important; } .filter-form-input { width: 100% !important; padding: 6px 8px !important; border: 1px solid #ced4da !important; border-radius: 3px !important; font-size: 13px !important; box-sizing: border-box !important; } .filter-form-input:focus { outline: none !important; border-color: #86b7fe !important; box-shadow: 0 0 0 0.15rem rgba(13, 110, 253, 0.25) !important; } .filter-algorithm-options { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 6px !important; } .filter-algorithm-option { display: flex !important; align-items: center !important; padding: 6px 8px !important; border: 1px solid #dee2e6 !important; border-radius: 3px !important; cursor: pointer !important; transition: all 0.2s !important; font-size: 12px !important; } .filter-algorithm-option:hover { background-color: #f8f9fa !important; border-color: #86b7fe !important; } .filter-algorithm-option.selected { background-color: #e7f1ff !important; border-color: #86b7fe !important; color: #0d6efd !important; } .filter-algorithm-radio { margin-right: 6px !important; } .filter-algorithm-label { flex: 1 !important; font-size: 12px !important; } .filter-modal-footer { padding: 10px 12px !important; border-top: 1px solid #dee2e6 !important; display: flex !important; justify-content: flex-end !important; gap: 8px !important; } .filter-modal-btn { padding: 5px 10px !important; border-radius: 3px !important; font-size: 12px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s !important; border: 1px solid transparent !important; } .filter-modal-btn-apply { background-color: #0d6efd !important; color: white !important; border-color: #0d6efd !important; } .filter-modal-btn-apply:hover { background-color: #0b5ed7 !important; border-color: #0a58ca !important; } .filter-modal-btn-clear { background-color: #6c757d !important; color: white !important; border-color: #6c757d !important; } .filter-modal-btn-clear:hover { background-color: #5c636a !important; border-color: #565e64 !important; } .filter-modal-btn-cancel { background-color: white !important; color: #6c757d !important; border-color: #dee2e6 !important; } .filter-modal-btn-cancel:hover { background-color: #f8f9fa !important; color: #495057 !important; } /* 高亮匹配的单元格 */ .filter-match { background-color: #fff3cd !important; border: 1px solid #ffecb5 !important; } /* 隐藏的行 */ .row-filtered-out { display: none !important; } /* 表格处理标记 */ .table-processed { position: relative; } /* 隐藏控制行按钮 (X) */ .control-hide-btn { padding: 0px 4px !important; border: 1px solid #e55353 !important; background: #fff5f5 !important; color: #a71d2a !important; border-radius: 3px !important; font-size: 12px !important; line-height: 14px !important; } /* 行号指示器 */ .row-num-cell { position: relative !important; } .row-num-indicator { position: absolute !important; left: 0 !important; top: 0 !important; transform: none !important; background: #fff !important; color: #000 !important; font-size: 8px !important; line-height: 1 !important; padding: 1px 1px !important; border-radius: 0 3px 3px 0 !important; opacity: 1 !important; pointer-events: none !important; white-space: nowrap !important; z-index: 10 !important; font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif !important; box-shadow: 1px 1px 3px rgba(0,0,0,0.15) !important; } /* 双条件筛选:并排输入框行 */ .filter-dual-input-row { display: flex !important; align-items: center !important; gap: 6px !important; margin-bottom: 12px !important; } .filter-dual-input-row .filter-form-input { flex: 1 1 0 !important; min-width: 0 !important; } /* 并且/或者 组合下拉 */ .filter-combiner { flex: 0 0 auto !important; padding: 6px 4px !important; border: 1px solid #ced4da !important; border-radius: 3px !important; font-size: 13px !important; background: white !important; color: #495057 !important; cursor: pointer !important; } /* 算法 tab 切换条(对应条件1/条件2) */ .filter-algo-tabs { display: flex !important; gap: 0 !important; margin-bottom: 8px !important; border-bottom: 1px solid #dee2e6 !important; } .filter-algo-tab { flex: 1 !important; text-align: center !important; padding: 6px 8px !important; cursor: pointer !important; font-size: 13px !important; color: #6c757d !important; border-bottom: 2px solid transparent !important; transition: all 0.2s !important; user-select: none !important; } .filter-algo-tab:hover { color: #495057 !important; } .filter-algo-tab.active { color: #0d6efd !important; border-bottom-color: #0d6efd !important; font-weight: 600 !important; } /* 计算列配置弹窗 */ .calc-modal { width: 520px !important; max-width: 92vw !important; } .calc-row { display: flex !important; align-items: center !important; gap: 6px !important; padding: 4px 6px !important; border: 1px solid #eee !important; border-radius: 3px !important; margin-bottom: 4px !important; font-size: 12px !important; } .calc-row .calc-row-name { font-weight: 600 !important; min-width: 56px !important; } .calc-row .calc-row-expr { flex: 1 !important; color: #555 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } .calc-row button { padding: 1px 6px !important; font-size: 12px !important; cursor: pointer !important; } .calc-edit { border-top: 1px solid #dee2e6 !important; padding-top: 10px !important; margin-top: 8px !important; } .calc-edit textarea { width: 100% !important; box-sizing: border-box !important; padding: 4px 6px !important; border: 1px solid #ced4da !important; border-radius: 3px !important; font-size: 12px !important; font-family: monospace !important; } .calc-msg-ok { color: #0f5132 !important; font-size: 12px !important; } .calc-msg-err { color: #842029 !important; font-size: 12px !important; } .calc-help-modal { width: 480px !important; max-height: 70vh !important; overflow: auto !important; } /* 排序按钮(无排序态) */ .sort-btn { color: #6c757d !important; } `); // ===== 表达式引擎(与 src/expr-engine.js 保持一致,以测试源为准)===== const ExprEngine = (function () { const IS_IDENT = ch => /^[一-鿿㐀-䶿a-zA-Z0-9_]$/.test(ch); const IS_DIGIT = ch => ch >= '0' && ch <= '9'; function tokenize(expr) { const tokens = []; let i = 0; const n = expr.length; while (i < n) { const ch = expr[i]; if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; } if (ch === '[') { // 方括号列名 let j = i + 1; while (j < n && expr[j] !== ']') j++; tokens.push({ type: 'col', value: expr.slice(i + 1, j).trim() }); i = j + 1; continue; } if (IS_DIGIT(ch) || (ch === '.' && IS_DIGIT(expr[i + 1]))) { // 数字 let j = i, dot = false; while (j < n && (IS_DIGIT(expr[j]) || (expr[j] === '.' && !dot))) { if (expr[j] === '.') dot = true; j++; } tokens.push({ type: 'num', value: parseFloat(expr.slice(i, j)) }); i = j; continue; } if (ch === "'" || ch === '"') { // 字符串字面量 const quote = ch; let j = i + 1, s = ''; while (j < n && expr[j] !== quote) { if (expr[j] === '\\' && j + 1 < n) { s += expr[j + 1]; j += 2; } else { s += expr[j]; j++; } } tokens.push({ type: 'str', value: s }); i = j + 1; continue; } const two = expr.slice(i, i + 2); if (two === '&&') { tokens.push({ type: 'logic', value: 'and' }); i += 2; continue; } if (two === '||') { tokens.push({ type: 'logic', value: 'or' }); i += 2; continue; } if (two === '>=' || two === '<=' || two === '!=') { tokens.push({ type: 'op', value: two }); i += 2; continue; } if ('+-*/%'.indexOf(ch) >= 0) { tokens.push({ type: 'op', value: ch }); i++; continue; } if (ch === '>' || ch === '<' || ch === '=') { tokens.push({ type: 'op', value: ch }); i++; continue; } if (ch === '&') { tokens.push({ type: 'op', value: '&' }); i++; continue; } // 字符串拼接 if (ch === '(') { tokens.push({ type: 'lparen' }); i++; continue; } if (ch === ')') { tokens.push({ type: 'rparen' }); i++; continue; } if (ch === ',') { tokens.push({ type: 'comma' }); i++; continue; } if (ch === '?') { tokens.push({ type: 'question' }); i++; continue; } if (ch === ':') { tokens.push({ type: 'colon' }); i++; continue; } if (IS_IDENT(ch)) { // 标识符:列名/函数/逻辑词 let j = i; while (j < n && IS_IDENT(expr[j])) j++; const word = expr.slice(i, j); let k = j; while (k < n && (expr[k] === ' ' || expr[k] === '\t')) k++; const lw = word.toLowerCase(); if (expr[k] === '(') tokens.push({ type: 'func', value: word }); else if (lw === 'and' || lw === 'or' || lw === 'not') tokens.push({ type: 'logic', value: lw }); else tokens.push({ type: 'col', value: word }); i = j; continue; } throw new Error('词法错误:无法识别字符 "' + ch + '" 在位置 ' + i); } return tokens; } function toNum(v) { if (typeof v === 'number') return v; if (v === null || v === undefined) return NaN; return parseFloat(String(v).replace(/[¥$€,\s]/g, '')); } function toBool(v) { if (typeof v === 'boolean') return v; if (typeof v === 'number') return v !== 0; return !!v && String(v).trim() !== ''; } function binOp(op, a, b) { if (op === '&') return toStr(a) + toStr(b); // 字符串拼接 if ('+-*/%'.indexOf(op) >= 0) { const x = toNum(a), y = toNum(b); if (op === '+') return x + y; if (op === '-') return x - y; if (op === '*') return x * y; if (op === '/') { if (y === 0) throw new Error('除零错误'); return x / y; } if (op === '%') { if (y === 0) throw new Error('模零错误'); return x % y; } } const nx = toNum(a), ny = toNum(b); const numeric = !isNaN(nx) && !isNaN(ny); const A = numeric ? nx : String(a); const B = numeric ? ny : String(b); switch (op) { case '>': return A > B; case '<': return A < B; case '>=': return A >= B; case '<=': return A <= B; case '=': return A === B; case '!=': return A !== B; } throw new Error('未知运算符: ' + op); } function toStr(v) { return v === null || v === undefined ? '' : String(v); } function parseDate(v) { const s = toStr(v).trim(); const m = s.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/); if (m) return new Date(+m[1], +m[2] - 1, +m[3], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)); const d = new Date(s); return isNaN(d.getTime()) ? null : d; } const FUNCS = { round: (x, n) => { const p = Math.pow(10, n || 0); return Math.round((toNum(x) + (n > 0 ? 1e-9 : 0)) * p) / p; }, abs: x => Math.abs(toNum(x)), floor: x => Math.floor(toNum(x)), ceil: x => Math.ceil(toNum(x)), max: function () { return Math.max.apply(null, [].map.call(arguments, toNum)); }, min: function () { return Math.min.apply(null, [].map.call(arguments, toNum)); }, mod: (a, b) => { const y = toNum(b); if (y === 0) throw new Error('模零错误'); return toNum(a) % y; }, concat: function () { return [].map.call(arguments, toStr).join(''); }, left: (s, n) => toStr(s).slice(0, toNum(n)), right: (s, n) => toStr(s).slice(-toNum(n)), mid: (s, start, len) => toStr(s).substr(toNum(start) - 1, toNum(len)), len: s => toStr(s).length, replace: (s, a, b) => toStr(s).split(toStr(a)).join(toStr(b)), upper: s => toStr(s).toUpperCase(), lower: s => toStr(s).toLowerCase(), trim: s => toStr(s).trim(), contains: (s, sub) => toStr(s).indexOf(toStr(sub)) >= 0, if: (cond, a, b) => (toBool(cond) ? a : b), date: s => { const d = parseDate(s); if (!d) throw new Error('无法解析日期: ' + toStr(s)); return d.getTime(); }, today: () => { const d = new Date(); d.setHours(0, 0, 0, 0); return d.getTime(); }, year: s => { const d = parseDate(s); if (!d) throw new Error('无效日期'); return d.getFullYear(); }, month: s => { const d = parseDate(s); if (!d) throw new Error('无效日期'); return d.getMonth() + 1; }, day: s => { const d = parseDate(s); if (!d) throw new Error('无效日期'); return d.getDate(); }, datediff: (a, b, unit) => { const d1 = parseDate(a), d2 = parseDate(b); if (!d1 || !d2) throw new Error('无效日期'); const ms = d1.getTime() - d2.getTime(); if (unit === 'month') return (d1.getFullYear() - d2.getFullYear()) * 12 + (d1.getMonth() - d2.getMonth()); if (unit === 'second') return Math.round(ms / 1000); if (unit === 'minute') return Math.round(ms / 60000); if (unit === 'hour') return Math.round(ms / 3600000); return Math.round(ms / 86400000); }, dateadd: (s, n, unit) => { const d = parseDate(s); if (!d) throw new Error('无效日期'); const c = new Date(d.getTime()); if (unit === 'month') c.setMonth(c.getMonth() + toNum(n)); else c.setDate(c.getDate() + toNum(n)); return c.getTime(); } }; function parse(tokens) { let pos = 0; const peek = () => tokens[pos]; function expect(type) { const t = tokens[pos]; if (!t || t.type !== type) throw new Error('语法错误:期望 ' + type); pos++; return t; } function parseTernary() { const cond = parseLogic(); if (peek() && peek().type === 'question') { pos++; const then = parseTernary(); expect('colon'); const els = parseTernary(); return { t: 'ternary', c: cond, a: then, b: els }; } return cond; } function parseLogic() { let left = parseComp(); while (peek() && peek().type === 'logic' && (peek().value === 'and' || peek().value === 'or')) { const op = peek().value; pos++; left = { t: 'logic', op, l: left, r: parseComp() }; } return left; } function parseComp() { let left = parseConcat(); while (peek() && peek().type === 'op' && ['>', '<', '>=', '<=', '=', '!='].indexOf(peek().value) >= 0) { const op = peek().value; pos++; left = { t: 'bin', op, l: left, r: parseConcat() }; } return left; } function parseConcat() { let left = parseAdd(); while (peek() && peek().type === 'op' && peek().value === '&') { pos++; left = { t: 'bin', op: '&', l: left, r: parseAdd() }; } return left; } function parseAdd() { let left = parseMul(); while (peek() && peek().type === 'op' && (peek().value === '+' || peek().value === '-')) { const op = peek().value; pos++; left = { t: 'bin', op, l: left, r: parseMul() }; } return left; } function parseMul() { let left = parseUnary(); while (peek() && peek().type === 'op' && (peek().value === '*' || peek().value === '/' || peek().value === '%')) { const op = peek().value; pos++; left = { t: 'bin', op, l: left, r: parseUnary() }; } return left; } function parseUnary() { if (peek() && peek().type === 'op' && peek().value === '-') { pos++; return { t: 'unary', e: parseUnary() }; } return parseAtom(); } function parseAtom() { const t = peek(); if (!t) throw new Error('语法错误:意外的表达式结尾'); if (t.type === 'num') { pos++; return { t: 'num', v: t.value }; } if (t.type === 'str') { pos++; return { t: 'str', v: t.value }; } if (t.type === 'col') { pos++; return { t: 'col', name: t.value }; } if (t.type === 'func') { pos++; expect('lparen'); const args = []; if (peek() && peek().type !== 'rparen') { args.push(parseTernary()); while (peek() && peek().type === 'comma') { pos++; args.push(parseTernary()); } } expect('rparen'); return { t: 'call', name: t.value, args }; } if (t.type === 'lparen') { pos++; const e = parseTernary(); expect('rparen'); return e; } throw new Error('语法错误:意外的 token ' + JSON.stringify(t)); } const ast = parseTernary(); if (pos < tokens.length) throw new Error('语法错误:未消费的 token ' + JSON.stringify(tokens[pos])); return ast; } function evalAst(node, scope) { switch (node.t) { case 'num': return node.v; case 'str': return node.v; case 'col': if (!Object.prototype.hasOwnProperty.call(scope, node.name)) throw new Error('未知列名: ' + node.name); return scope[node.name]; case 'unary': return -toNum(evalAst(node.e, scope)); case 'bin': return binOp(node.op, evalAst(node.l, scope), evalAst(node.r, scope)); case 'logic': { const lv = toBool(evalAst(node.l, scope)); if (node.op === 'and') return lv && toBool(evalAst(node.r, scope)); return lv || toBool(evalAst(node.r, scope)); } case 'ternary': return toBool(evalAst(node.c, scope)) ? evalAst(node.a, scope) : evalAst(node.b, scope); case 'call': if (!FUNCS[node.name]) throw new Error('未知函数: ' + node.name); return FUNCS[node.name].apply(null, node.args.map(a => evalAst(a, scope))); } throw new Error('求值错误: ' + JSON.stringify(node)); } function collectColumns(ast) { const set = {}; (function walk(n) { if (!n) return; if (n.t === 'col') set[n.name] = true; else if (n.t === 'unary') walk(n.e); else if (n.t === 'bin') { walk(n.l); walk(n.r); } else if (n.t === 'logic') { walk(n.l); walk(n.r); } else if (n.t === 'ternary') { walk(n.c); walk(n.a); walk(n.b); } else if (n.t === 'call') n.args.forEach(walk); })(ast); return Object.keys(set); } function validate(expr, knownColumns) { let ast; try { ast = parse(tokenize(expr)); } catch (e) { return { ok: false, error: e.message, usedColumns: [] }; } const used = collectColumns(ast); const known = {}; (knownColumns || []).forEach(c => { known[c] = true; }); for (let i = 0; i < used.length; i++) { if (!known[used[i]]) return { ok: false, error: '未知列名: ' + used[i], usedColumns: used }; } try { const probe = {}; used.forEach(c => { probe[c] = 0; }); evalAst(ast, probe); } catch (e) { if (!/除零|模零|无效日期|无法解析日期/.test(e.message)) { return { ok: false, error: e.message, usedColumns: used }; } } return { ok: true, error: null, usedColumns: used }; } return { tokenize, parse, evalAst, validate, toNum, parseDate, FUNCS }; })(); // ===== 表格适配器:隔离 el-table 双 table 细节 ===== const ElTableAdapter = { getRoot(bodyTable) { return bodyTable.closest('.el-table'); }, getHeaderTable(bodyTable) { const root = this.getRoot(bodyTable); return root ? root.querySelector('.el-table__header') : null; }, readHeaderNames(bodyTable) { const ht = this.getHeaderTable(bodyTable); if (!ht) return []; const trs = ht.querySelectorAll('thead tr'); const tr = trs.length ? trs[trs.length - 1] : ht.querySelector('thead tr'); if (!tr) return []; // 过滤掉 gutter(滚动条占位)th,否则表头列数比表体多 1,导致计算列 colIndex 错位/串列 return Array.from(tr.children) .filter(th => !/gutter/.test(th.className || '')) .map(th => (th.textContent || '').trim()); }, appendColumn(bodyTable, name, width) { const w = width || 120; const ht = this.getHeaderTable(bodyTable); if (ht) { // 表头单元格:插在 gutter 之前(避免与表体错位),并补 el-table 的 .cell 结构 const trs = ht.querySelectorAll('thead tr'); const headTr = trs.length ? trs[trs.length - 1] : ht.querySelector('thead tr'); if (headTr) { const th = document.createElement('th'); th.className = 'el-table__cell'; th.setAttribute('data-calc-col', name); const cellDiv = document.createElement('div'); cellDiv.className = 'cell'; cellDiv.textContent = name; th.appendChild(cellDiv); const gutterTh = headTr.querySelector('th.gutter, th[class*="gutter"]'); if (gutterTh) headTr.insertBefore(th, gutterTh); else headTr.appendChild(th); } // 表头 colgroup:计算列 col 插在 gutter col 之前 const cg = ht.querySelector('colgroup'); if (cg) { const col = document.createElement('col'); col.width = w; col.style.width = w + 'px'; col.setAttribute('data-calc-col', ''); const gutterCol = cg.querySelector('col[name="gutter"]'); if (gutterCol) cg.insertBefore(col, gutterCol); else cg.appendChild(col); } } const rows = getTableAllDataRows(bodyTable); rows.forEach(row => { const td = document.createElement('td'); td.className = 'el-table__cell'; td.setAttribute('data-calc-col', name); const cellDiv = document.createElement('div'); cellDiv.className = 'cell'; cellDiv.style.textAlign = 'right'; td.appendChild(cellDiv); row.appendChild(td); }); const controlRow = bodyTable.querySelector('.table-control-row'); if (controlRow) { const cell = document.createElement('td'); cell.setAttribute('data-calc-col', name); controlRow.appendChild(cell); } const bcg = bodyTable.querySelector('colgroup'); if (bcg) { const col = document.createElement('col'); col.width = w; col.style.width = w + 'px'; col.setAttribute('data-calc-col', ''); bcg.appendChild(col); } }, writeCell(bodyTable, rowIndex, name, value, resultType) { const rows = getTableAllDataRows(bodyTable); const row = rows[rowIndex]; if (!row) return; const td = row.querySelector('td[data-calc-col="' + cssEscape(name) + '"]'); if (!td) return; const target = td.querySelector('.cell') || td; if (value instanceof Error) { target.textContent = '#ERR: ' + value.message; td.style.color = '#c00'; } else { target.textContent = formatValue(value, resultType); target.style.textAlign = (resultType === 'number' || typeof value === 'number') ? 'right' : 'left'; td.style.backgroundColor = '#fafcff'; } }, removeAllCalcCols(bodyTable) { [bodyTable, this.getHeaderTable(bodyTable)].forEach(tbl => { if (!tbl) return; tbl.querySelectorAll('[data-calc-col]').forEach(el => el.remove()); }); } }; function cssEscape(s) { return String(s).replace(/["\\]/g, '\\$&'); } function formatValue(v, resultType) { if (typeof v === 'boolean') return v ? '是' : '否'; if (v === null || v === undefined) return ''; return String(v); } // ===== 计算列管理器(内联自 src/calc-manager.js)===== function computeRowValues(defs, rawValues) { const scope = Object.assign({}, rawValues); const out = {}; for (const d of defs) { const ast = ExprEngine.parse(ExprEngine.tokenize(d.expr)); const val = ExprEngine.evalAst(ast, scope); out[d.name] = val; scope[d.name] = val; // 后续列可引用(链式) } return out; } // ===== 持久化(内联自 src/calc-store.js)===== function makeCalcKey(url, headerNames) { return String(url) + '::' + headerNames.join('|'); } function MakeTableCalcStore(table, headers) { if (typeof GM_getValue === 'undefined') return null; const key = 'calcCols_' + makeCalcKey(location.href, headers); return { save(defs) { GM_setValue(key, JSON.stringify(defs)); }, load() { const raw = GM_getValue(key, undefined); if (!raw) return null; try { return JSON.parse(raw); } catch (e) { return null; } }, clear() { GM_setValue(key, ''); } }; } // ===== 计算列应用 / 持久化加载 / 保存 ===== function loadCalcColumnsIfPersisted(table) { const tableId = generateTableId(table); const st = tableData.get(tableId); if (!st || !st.calcPersist) return; const headers = origHeaders(table); const store = MakeTableCalcStore(table, headers); let saved = store && store.load(); // 迁移:旧版本曾把配置存到"含计算列名后缀"的污染 key。若原始 key 无值, // 扫描污染 key,取 id 时间戳最新的迁移过来,并删除污染 key。 if ((!saved || !Array.isArray(saved) || !saved.length) && typeof GM_listValues !== 'undefined') { const baseKey = 'calcCols_' + makeCalcKey(location.href, headers); const polluted = GM_listValues().filter(k => k.startsWith(baseKey) && k !== baseKey); let best = null, bestTs = 0; polluted.forEach(k => { let v; try { v = JSON.parse(GM_getValue(k, '[]') || '[]'); } catch (e) { return; } if (!Array.isArray(v) || !v.length) return; let ts = 0; v.forEach(d => { const m = /cc_(\d+)/.exec(d.id || ''); if (m) ts = Math.max(ts, +m[1]); }); if (ts > bestTs) { bestTs = ts; best = v; } }); if (best) { saved = best; if (store) store.save(best); polluted.forEach(k => GM_deleteValue(k)); } } if (saved && Array.isArray(saved) && saved.length) st.calcColumns = saved; } // 测量计算列内容最大宽(表头 + 数据行 .cell 文本;canvas 量文本,不触发 reflow) function measureCalcColWidth(table, name) { const root = table.closest('.el-table'); if (!root) return 0; const sample = root.querySelector('th[data-calc-col="' + cssEscape(name) + '"] .cell, td[data-calc-col="' + cssEscape(name) + '"] .cell'); let font = '14px "Microsoft YaHei", sans-serif'; if (sample) { const cs = getComputedStyle(sample); if (cs.fontSize && cs.fontFamily) font = cs.fontSize + ' ' + cs.fontFamily; } const cv = measureCalcColWidth._cv || (measureCalcColWidth._cv = document.createElement('canvas')); const ctx = cv.getContext('2d'); ctx.font = font; let max = 0; const ht = root.querySelector('.el-table__header'); const headCell = ht && ht.querySelector('th[data-calc-col="' + cssEscape(name) + '"] .cell'); if (headCell) max = Math.max(max, ctx.measureText(headCell.textContent || '').width); const cells = table.querySelectorAll('td[data-calc-col="' + cssEscape(name) + '"] > .cell'); for (let i = 0; i < cells.length; i++) max = Math.max(max, ctx.measureText(cells[i].textContent || '').width); return max ? Math.ceil(max) + 28 : 0; // +cell 左右内边距/余量 } // 计算列自适应:无记忆宽的计算列按内容设宽;有记忆宽的跳过(由 applyColWidths 锁记忆值) function autoFitCalcColumns(table) { const st = tableData.get(generateTableId(table)); if (!st || !st.calcColumns || !st.calcColumns.length) return; st.calcColumns.forEach(def => { if (st.colWidths && Object.prototype.hasOwnProperty.call(st.colWidths, def.name)) return; const w = measureCalcColWidth(table, def.name); if (w) eachColByName(table, def.name, col => setColStyleWidth(col, w)); }); } function applyCalcColumns(table) { const tableId = generateTableId(table); const st = tableData.get(tableId); if (!st || !st.calcColumns || !st.calcColumns.length) return; const headers = origHeaders(table); const rows = getTableAllDataRows(table); st.calcColumns.forEach(def => ElTableAdapter.appendColumn(table, def.name, def.width)); rows.forEach((row, idx) => { const raw = {}; headers.forEach((h, i) => { raw[h] = (row.cells[i] ? row.cells[i].textContent.trim() : ''); }); let vals; try { vals = computeRowValues(st.calcColumns, raw); } catch (e) { vals = {}; st.calcColumns.forEach(d => { vals[d.name] = new Error(e.message); }); } st.calcColumns.forEach(def => ElTableAdapter.writeCell(table, idx, def.name, vals[def.name], def.resultType)); }); refreshCalcControlButtons(table, headers.length); // 计算列内容自适应宽度(无记忆宽的按内容撑开;有记忆的留给 applyColWidths 锁定) autoFitCalcColumns(table); // 同步表头/表体 table 总宽,使水平滚动条覆盖计算列 syncTableWidth(table); // 重应用筛选与排序(计算列重渲染后筛选/高亮需重新生效,修复"计算列筛选丢失") applyAllFilters(table); const cc = (table.querySelector('.table-control-row') || {}).cells?.length || 0; for (let i = 0; i < cc; i++) updateControlUI(table, i); } function saveCalcColumns(table) { const tableId = generateTableId(table); const st = tableData.get(tableId); if (!st || !st.calcPersist) return; const headers = origHeaders(table); const store = MakeTableCalcStore(table, headers); if (store) store.save(st.calcColumns); } // ===== 隐藏列管理 ===== function colHiddenStoreKey(table) { return 'colHidden_' + makeCalcKey(location.href, origHeaders(table)); } function loadHiddenCols(table) { const st = tableData.get(generateTableId(table)); if (!st || typeof GM_getValue === 'undefined') return; try { const v = JSON.parse(GM_getValue(colHiddenStoreKey(table), '[]') || '[]'); if (Array.isArray(v)) st.hiddenCols = v; } catch (e) {} } function saveHiddenCols(table) { const st = tableData.get(generateTableId(table)); if (!st || typeof GM_setValue === 'undefined') return; GM_setValue(colHiddenStoreKey(table), JSON.stringify(st.hiddenCols || [])); } function getColIndexByName(table, name) { const headers = origHeaders(table); const i = headers.indexOf(name); if (i >= 0) return i; const st = tableData.get(generateTableId(table)); if (st && st.calcColumns) { const ci = st.calcColumns.findIndex(d => d.name === name); if (ci >= 0) return headers.length + ci; } return -1; } // 取父元素第 n 个非 gutter 子元素 function nthNonGutterChild(parent, n) { let ci = 0; for (const child of parent.children) { if (/gutter/.test(child.getAttribute('name') || '') || /gutter/.test(child.className || '')) continue; if (ci === n) return child; ci++; } return null; } // 隐藏/显示单列:仅切 display,不碰 col 宽度(宽度与隐藏正交,统一由 applyColWidths 管理) function setHidden(table, name, hidden) { const idx = getColIndexByName(table, name); if (idx < 0) return; const root = table.closest('.el-table'); if (!root) return; const disp = hidden ? 'none' : ''; Array.from(root.querySelectorAll('table')).forEach(tbl => { const col = findColByIndex(tbl, idx); if (col) col.style.display = disp; }); const ht = root.querySelector('.el-table__header'); if (ht) { const trs = ht.querySelectorAll('thead tr'); const tr = trs[trs.length - 1]; if (tr) { const th = nthNonGutterChild(tr, idx); if (th) th.style.display = disp; } } getTableAllDataRows(table).forEach(row => { const td = nthNonGutterChild(row, idx); if (td) td.style.display = disp; }); const controlRow = table.querySelector('.table-control-row'); if (controlRow) { // 保护工具列(原始最后列,👁/➕/X 入口),不隐藏,避免无法再打开列管理 if (idx !== origHeaders(table).length - 1) { const cell = nthNonGutterChild(controlRow, idx); if (cell) cell.style.display = disp; } } } function applyHiddenCols(table) { const st = tableData.get(generateTableId(table)); if (!st) return; const allNames = origHeaders(table).concat((st.calcColumns || []).map(d => d.name)); allNames.forEach(n => setHidden(table, n, false)); (st.hiddenCols || []).forEach(n => setHidden(table, n, true)); syncTableWidth(table); } // 列管理对话框(隐藏/显示) function showColManagerModal(table) { document.querySelectorAll('.filter-modal-overlay').forEach(el => el.remove()); const st = tableData.get(generateTableId(table)); const allNames = origHeaders(table).concat((st.calcColumns || []).map(d => d.name)); const hiddenSet = new Set(st.hiddenCols || []); const overlay = document.createElement('div'); overlay.className = 'filter-modal-overlay'; const modal = document.createElement('div'); modal.className = 'filter-modal calc-modal'; modal.innerHTML = `
列管理(隐藏/显示)
`; overlay.appendChild(modal); document.body.appendChild(overlay); const listEl = modal.querySelector('#colmgr-list'); function render() { listEl.innerHTML = ''; allNames.forEach(n => { const isHidden = hiddenSet.has(n); const memW = (st.colWidths && st.colWidths[n]) || ''; const curW = readColWidthByName(table, n) || ''; const row = document.createElement('div'); row.className = 'calc-row'; row.innerHTML = `${n}${isHidden ? '已隐藏' : '显示中'} · 宽${curW || '?'}${memW ? '(记:' + memW + ')' : ''}`; const btn = document.createElement('button'); btn.textContent = isHidden ? '显示' : '隐藏'; btn.onclick = () => { if (hiddenSet.has(n)) { hiddenSet.delete(n); setHidden(table, n, false); } else { hiddenSet.add(n); setHidden(table, n, true); } st.hiddenCols = Array.from(hiddenSet); saveHiddenCols(table); syncTableWidth(table); render(); }; row.appendChild(btn); // 列宽记忆:有记忆可清除,无记忆可记住当前 const wBtn = document.createElement('button'); if (memW) { wBtn.textContent = '清除列宽'; wBtn.title = '清除该列列宽记忆(恢复时不再覆盖此列)'; wBtn.onclick = () => { clearColWidth(table, n); render(); }; } else { wBtn.textContent = '记住列宽'; wBtn.title = '把当前列宽存为记忆'; wBtn.onclick = () => { const w = readColWidthByName(table, n); if (w) { saveColWidth(table, n, w); render(); } }; } row.appendChild(wBtn); listEl.appendChild(row); }); } modal.querySelector('.filter-modal-close').onclick = () => overlay.remove(); modal.querySelector('#colmgr-close').onclick = () => overlay.remove(); modal.querySelector('#colmgr-showall').onclick = () => { hiddenSet.clear(); st.hiddenCols = []; saveHiddenCols(table); applyHiddenCols(table); render(); }; // 添加计算列:转计算列编辑器(与“全部显示”并排) modal.querySelector('#colmgr-addcalc').onclick = () => { overlay.remove(); showCalcColumnModal(table); }; // 全部清除列宽记忆:清空存储 + 移除当前所有 inline 宽度干预(交还 el-table 自治) modal.querySelector('#colmgr-clearwidth').onclick = () => { if (!confirm('确定清除本表所有列宽记忆吗?')) return; st.colWidths = {}; persistColWidths(table); const root = table.closest('.el-table'); if (root) [root.querySelector('.el-table__header'), root.querySelector('.el-table__body')].forEach(tbl => { if (!tbl) return; tbl.querySelectorAll('colgroup col').forEach(c => { if (!/gutter/.test(c.getAttribute('name') || '')) c.style.width = ''; }); }); syncTableWidth(table); render(); }; overlay.onclick = e => { if (e.target === overlay) overlay.remove(); }; setupModalDrag(modal, modal.querySelector('.filter-modal-header')); render(); } // 计算列的控制行格子:仅排序/筛选/清除按钮(不挂 hideBtn/➕,避免与原始最后一列混淆) function createCalcControlContainer(table, columnIndex) { const container = document.createElement('div'); container.className = 'control-container'; const btnContainer = document.createElement('div'); btnContainer.className = 'btn-container'; const sortBtn = createControlButton('⇅', 'sort-btn', '排序'); sortBtn.dataset.action = 'sort'; sortBtn.dataset.columnIndex = columnIndex; const filterBtn = createControlButton('🔍', 'filter-btn', '筛选'); filterBtn.dataset.action = 'filter'; filterBtn.dataset.columnIndex = columnIndex; const clearFilterBtn = createControlButton('×', 'filter-btn', '清除筛选'); clearFilterBtn.dataset.action = 'clear-filter'; clearFilterBtn.dataset.columnIndex = columnIndex; clearFilterBtn.style.display = 'none'; [sortBtn, filterBtn, clearFilterBtn].forEach(btn => { btn.addEventListener('click', function (e) { e.stopPropagation(); handleControlClick(table, this); }); }); btnContainer.appendChild(sortBtn); btnContainer.appendChild(filterBtn); btnContainer.appendChild(clearFilterBtn); const filterIndicator = document.createElement('div'); filterIndicator.className = 'filter-indicator'; filterIndicator.dataset.columnIndex = columnIndex; container.appendChild(btnContainer); container.appendChild(filterIndicator); container._controlButtons = { sortBtn, filterBtn, clearFilterBtn, filterIndicator }; return container; } function refreshCalcControlButtons(table, startCol) { const controlRow = table.querySelector('.table-control-row'); if (!controlRow) return; const cells = controlRow.querySelectorAll('td[data-calc-col]'); cells.forEach((cell, i) => { const colIndex = startCol + i; cell.innerHTML = ''; cell.appendChild(createCalcControlContainer(table, colIndex)); }); } // 主初始化函数 function initialize() { console.log('表格排序筛选脚本开始初始化'); // 处理当前文档的表格 processDocument(document); // 在页面挂载“列筛选”切换按钮 try { addColumnFilterToggleButton(document); } catch (err) { console.warn('添加列筛选切换按钮失败:', err); } // 监听iframe加载 setupIframeObserver(); // 监听DOM变化 setupMutationObserver(); console.log('表格排序筛选脚本已加载完成'); } // 在页面中查找 查询/重置 按钮并插入 列筛选 切换按钮 function addColumnFilterToggleButton(doc) { // 查找包含查询和重置按钮的容器,常见为 element-ui 的操作区 const possibleSelectors = [ 'div:has(button span:contains("查询"))', ]; // 更稳妥的做法:遍历页面所有按钮,找到文本为 查询 的按钮,然后在其父容器中插入我们的按钮 const allButtons = Array.from(doc.querySelectorAll('button')); let targetContainer = null; for (const btn of allButtons) { const span = btn.querySelector('span'); if (span && span.textContent.trim() === '查询') { targetContainer = btn.parentElement; break; } } if (!targetContainer) { // 尝试查找文本为 重置 的按钮的父容器 for (const btn of allButtons) { const span = btn.querySelector('span'); if (span && span.textContent.trim() === '重置') { targetContainer = btn.parentElement; break; } } } if (!targetContainer) return; // 创建切换按钮 const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; toggleBtn.className = 'el-button el-button--mini'; toggleBtn.style.marginLeft = '8px'; const span = document.createElement('span'); span.textContent = '列筛选'; toggleBtn.appendChild(span); // 插入到 查询/重置 之后 // 如果 targetContainer 是包含多个按钮的容器,则 append 到容器末尾 targetContainer.appendChild(toggleBtn); // 点击切换显示/隐藏控制行(不清除筛选/排序状态) toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); const anyHidden = document.querySelector('table[data-controls-hidden="true"]'); if (anyHidden) { // 如果有表格处于隐藏状态,显示所有 showAllControlRows(document); toggleBtn.classList.remove('is-active'); } else { // 否则隐藏所有控制行 hideAllControlRows(document); toggleBtn.classList.add('is-active'); } }); } // ===== 记住用户手动调整的列宽 ===== // 取「原始表头」(processTable 时记录,不含计算列/控制行),用作持久化 key,避免 key 漂移 function origHeaders(table) { const st = tableData.get(generateTableId(table)); if (st && st.originalHeaders && st.originalHeaders.length) return st.originalHeaders; return ElTableAdapter.readHeaderNames(table); } function colWidthStoreKey(table) { return 'colWidths_' + makeCalcKey(location.href, origHeaders(table)); } // ===== 列宽记忆(st.colWidths = { 列名: 宽度 },只记用户调过的列)===== function loadColWidths(table) { const st = tableData.get(generateTableId(table)); if (!st || typeof GM_getValue === 'undefined') return; let v; try { v = JSON.parse(GM_getValue(colWidthStoreKey(table), '{}') || '{}'); } catch (e) { v = {}; } if (Array.isArray(v)) { // 迁移旧版本(全列宽数组)→ 按列名对象 const names = origHeaders(table).concat((st.calcColumns || []).map(d => d.name)); const obj = {}; v.forEach((w, i) => { if (w && names[i]) obj[names[i]] = w; }); v = obj; } st.colWidths = (v && typeof v === 'object' && !Array.isArray(v)) ? v : {}; } function persistColWidths(table) { const st = tableData.get(generateTableId(table)); if (!st || typeof GM_setValue === 'undefined') return; GM_setValue(colWidthStoreKey(table), JSON.stringify(st.colWidths || {})); } function saveColWidth(table, name, width) { const st = tableData.get(generateTableId(table)); if (!st) return; if (!st.colWidths) st.colWidths = {}; st.colWidths[name] = width; persistColWidths(table); } function clearColWidth(table, name) { const st = tableData.get(generateTableId(table)); if (!st || !st.colWidths) return; delete st.colWidths[name]; persistColWidths(table); } // ===== 列宽写入统一入口:inline style(优先级高于 el-table 的 width attribute, // doLayout 也压不过,从根上杜绝拉锯/抖动)===== function setColStyleWidth(col, w) { const n = parseInt(w, 10); if (col && !isNaN(n)) col.style.width = n + 'px'; } function clearColStyleWidth(col) { if (col) col.style.width = ''; } function readColRenderWidth(col) { if (!col) return 0; const r = col.getBoundingClientRect(); if (r && r.width) return Math.round(r.width); return parseInt(col.width || col.getAttribute('width') || 0, 10) || 0; } // 在某张 table 的 colgroup 中,按非 gutter 顺序取第 idx 个 col function findColByIndex(tbl, idx) { const cols = tbl && tbl.querySelectorAll('colgroup col'); if (!cols) return null; let ci = 0; for (let i = 0; i < cols.length; i++) { if (/gutter/.test(cols[i].getAttribute('name') || '')) continue; if (ci === idx) return cols[i]; ci++; } return null; } // 在 header + body 两 table 上,对该列(按列名)执行 fn(col) function eachColByName(table, name, fn) { const idx = getColIndexByName(table, name); if (idx < 0) return; const root = table.closest('.el-table'); if (!root) return; [root.querySelector('.el-table__header'), root.querySelector('.el-table__body')].forEach(tbl => { const col = findColByIndex(tbl, idx); if (col) fn(col); }); } // 读某列当前真实渲染宽(取表头 col;形式无关,拖拽松手比较用) function readColWidthByName(table, name) { const idx = getColIndexByName(table, name); if (idx < 0) return ''; const root = table.closest('.el-table'); const col = findColByIndex(root && root.querySelector('.el-table__header'), idx); const w = readColRenderWidth(col); return w ? String(w) : ''; } // 应用记忆列宽:仅对用户调过(有记忆值)且未隐藏的列,用 inline style 锁定。 // inline style 优先级高于 el-table 的 width attribute → doLayout 压不过 → 无需延迟重试、无拉锯。 // 未调过的列保持 el-table 原生自治(自适应内容/原生拖拽)。 function applyColWidths(table) { const st = tableData.get(generateTableId(table)); if (!st || !st.colWidths) return; const entries = Object.entries(st.colWidths); if (!entries.length) return; if (!table.closest('.el-table')) return; const hidden = new Set(st.hiddenCols || []); entries.forEach(([name, w]) => { if (hidden.has(name) || !w) return; eachColByName(table, name, col => setColStyleWidth(col, w)); }); syncTableWidth(table); } // 加载/刷新后【延迟】应用记忆列宽:el-table 首次渲染会多轮 doLayout,加载初期还可能重建 col 节点, // 过早设 inline 易被重建冲掉。故故意等一段(默认 3s)让它把原生列宽调完,再一次性锁定。 // (另由 setupColWidthGuard 守护 col 重建,两者配合保证最终稳定。觉得太慢可调小此常量) const COL_WIDTH_APPLY_DELAY = 3000; function applyColWidthsStable(table) { clearTimeout(table._colApplyTimer); table._colApplyTimer = setTimeout(() => { if (table.isConnected) applyColWidths(table); }, COL_WIDTH_APPLY_DELAY); } // 监听列宽拖拽:仅认 th 右边缘热区(排除控制按钮);按下临时解锁该列让 el-table 实时跟随, // 松手读最终宽并锁回(applyColWidths)+ 存盘。拖拽顺滑、松手即定,无二次跳动。 function setupColWidthObserver(table) { if (table._colWidthObsInited) return; table._colWidthObsInited = true; const root = table.closest('.el-table'); const ht = root && root.querySelector('.el-table__header'); if (!ht) return; let dragging = false; let draggingName = null; let dragStartWidth = null; ht.addEventListener('mousedown', (e) => { if (e.button !== 0) return; const th = e.target.closest && e.target.closest('th'); if (!th || /gutter/.test(th.className || '')) return; // 点到控制按钮(排序/筛选/➕/👁/X 等)不视为拖列宽 if (e.target.closest && e.target.closest('.sort-btn,.filter-btn,.control-hide-btn,.control-btn,[data-action]')) return; // 仅命中 th 右边缘热区(6px)才视为列宽拖拽;用 clientX 相对 th 计算,不受内层 .cell 影响 const r = th.getBoundingClientRect(); if (e.clientX - r.left < r.width - 6) return; const cell = th.querySelector('.cell'); const name = ((cell ? cell.textContent : th.textContent) || '').trim(); if (!name) return; dragging = true; draggingName = name; dragStartWidth = readColWidthByName(table, name); // 临时解锁该列,让 el-table 的实时拖拽生效(松手时再锁回) eachColByName(table, name, col => clearColStyleWidth(col)); }, true); const onUp = () => { if (!dragging) return; dragging = false; if (draggingName) { const w = readColWidthByName(table, draggingName); if (w && w !== dragStartWidth) saveColWidth(table, draggingName, w); // 把该列重新锁定回记忆/当前宽度(有记忆则锁记忆值,无记忆则交还 el-table 自治) applyColWidths(table); } draggingName = null; dragStartWidth = null; }; document.addEventListener('mouseup', onUp); table._colWidthUpHandler = onUp; // 窗口尺寸变化时重应用记忆列宽(el-table resize 会重算列宽);resize 不走 3s 延迟,尽快恢复 const onResize = () => { clearTimeout(table._colResizeTimer); table._colResizeTimer = setTimeout(() => { if (table.isConnected) applyColWidths(table); }, 300); }; window.addEventListener('resize', onResize); table._colResizeHandler = onResize; } // 守护:el-table 重渲染若重建了 col 节点(加载/刷新/某些操作),旧 col 上的 inline style 会随节点消失。 // 监听 colgroup 的 col 增删,去抖后重应用记忆列宽(inline 设回后 doLayout 压不过,不会循环)。 function setupColWidthGuard(table) { if (table._colWidthGuard) return; const root = table.closest('.el-table'); if (!root) return; const tbls = [root.querySelector('.el-table__header'), root.querySelector('.el-table__body')].filter(Boolean); if (!tbls.length) return; const mo = new MutationObserver((muts) => { let colChanged = false; for (const m of muts) { for (const n of m.addedNodes) { if (n.tagName === 'COL' || n.tagName === 'COLGROUP' || (n.querySelector && n.querySelector('col'))) { colChanged = true; break; } } if (colChanged) break; } if (colChanged) { clearTimeout(table._colGuardTimer); table._colGuardTimer = setTimeout(() => { if (table.isConnected) applyColWidths(table); }, 200); } }); tbls.forEach(t => mo.observe(t, { childList: true, subtree: true })); table._colWidthGuard = mo; } // 同步表头/表体 table 总宽(用真实渲染宽,含 inline style;跳过隐藏列),使水平滚动条覆盖计算列 function syncTableWidth(table) { const root = table.closest('.el-table'); if (!root) return; [root.querySelector('.el-table__header'), root.querySelector('.el-table__body')].forEach(tbl => { if (!tbl) return; let total = 0; tbl.querySelectorAll('colgroup col').forEach(c => { if (c.style.display === 'none') return; const w = readColRenderWidth(c); if (w) total += w; }); if (total > 0) tbl.style.width = total + 'px'; }); } // 隐藏文档中所有控制行(不清除筛选/排序状态) function hideAllControlRows(doc) { const tables = doc.querySelectorAll('table.table-processed'); tables.forEach(table => { const controlRow = table.querySelector('.table-control-row'); if (controlRow) controlRow.remove(); // 仅标记隐藏,避免移除 table-processed table.dataset.controlsHidden = 'true'; }); } // 在文档中为所有已标记隐藏的表格恢复控制行 function showAllControlRows(doc) { const tables = doc.querySelectorAll('table[data-controls-hidden="true"]'); tables.forEach(table => { // 移除隐藏标记以便 processDocument 重新添加 delete table.dataset.controlsHidden; // 标记为未处理,processDocument 会重新处理并添加控制行 table.classList.remove('table-processed'); }); // 触发一次处理以添加控制行(小延迟以等待 DOM 更新) setTimeout(() => processDocument(doc), 50); } // 处理文档中的所有表格 function processDocument(doc) { // 跳过已处理或显式隐藏控制行的表格 const tables = doc.querySelectorAll('table:not(.table-processed):not([data-controls-hidden="true"])'); tables.forEach((table, tableIndex) => { // 检查是否为要忽略的日期表格 if (shouldIgnoreTable(table) == false) { // console.log(`忽略表格: ${tableIndex + 1}`); return; } // 检查表格是否有数据行 const hasDataRows = checkTableHasData(table); if (hasDataRows) { console.log(`处理表格 ${tableIndex + 1}`, table); processTable(table, doc, tableIndex); } }); } // 检查是否应该"el-table__body" function shouldIgnoreTable(table) { // 类名为 "el-date-table table-processed" 的表格 const classNames = table.className || ''; return classNames.includes('el-table__body'); } // 检查表格是否有数据行 function checkTableHasData(table) { const rows = table.rows; if (rows.length === 0) return false; let dataRowCount = 0; for (let i = 0; i < rows.length; i++) { const row = rows[i]; const hasTh = row.querySelector('th'); const isHeaderRow = hasTh || row.classList.contains('header') || row.getAttribute('role') === 'rowheader' || row.parentElement.tagName === 'THEAD'; if (!isHeaderRow && row.cells.length > 0) { dataRowCount++; if (dataRowCount > 1) return true; } } return false; } // 处理单个表格 function processTable(table, doc, tableIndex) { // 标记已处理 table.classList.add('table-processed'); // 获取表格结构信息 const structure = analyzeTableStructure(table); if (structure.columns === 0) return; // 初始化表格数据存储 initTableData(table, structure.dataRows); // 记录原始表头(在加控制行/计算列之前),用作持久化 key,避免 key 漂移 const _st = tableData.get(generateTableId(table)); if (_st) _st.originalHeaders = ElTableAdapter.readHeaderNames(table); // 添加控制行 addControlRow(table, structure); // 添加行号指示器 updateRowNumbers(table); // 监听行变化:element-ui 重渲染行后自动重画行号,保证行号稳定常显 setupTableRowsObserver(table); // 加载持久化计算列(若有)并应用 loadCalcColumnsIfPersisted(table); applyCalcColumns(table); loadHiddenCols(table); applyHiddenCols(table); // 记住用户列宽:加载记忆 + 延迟应用 + 监听拖拽 + 守护 col 重建 loadColWidths(table); applyColWidthsStable(table); setupColWidthObserver(table); setupColWidthGuard(table); // 记录初始数据指纹(剔除计算列内容,避免应用计算列后误判为数据刷新) table._lastFingerprint = computeTableFingerprint(table); } // 分析表格结构 function analyzeTableStructure(table) { const result = { columns: 0, hasThead: false, hasTbody: false, headerRows: [], dataRows: [] }; const allRows = table.rows; for (let i = 0; i < allRows.length; i++) { const row = allRows[i]; const cells = row.cells; result.columns = Math.max(result.columns, cells.length); const isHeaderRow = row.parentElement.tagName === 'THEAD' || (cells.length > 0 && cells[0].tagName === 'TH') || row.classList.contains('header') || row.getAttribute('role') === 'rowheader'; if (isHeaderRow) { result.headerRows.push(row); } else if (cells.length > 0) { result.dataRows.push(row); } } result.hasThead = table.tHead !== null; result.hasTbody = table.tBodies.length > 0; return result; } // 初始化表格数据存储 function initTableData(table, dataRows) { const tableId = generateTableId(table); if (!tableData.has(tableId)) { tableData.set(tableId, { originalRows: [...dataRows], filters: new Map(), // columnIndex -> { text, algorithm } sortState: [], // [{ columnIndex, order }] 优先级从前到后 calcColumns: [], // 有序计算列定义 calcPersist: true, // 持久化开关(默认开) originalHeaders: [], // 原始表头(processTable 时记录,作持久化 key) hiddenCols: [], // 已隐藏列名(持久化) colWidths: {} // 列宽记忆 {列名:宽度}(持久化) }); } } // 生成表格唯一ID function generateTableId(table) { if (!table.dataset.sortTableId) { table.dataset.sortTableId = 'table_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } return table.dataset.sortTableId; } // 添加控制行(排序和筛选) function addControlRow(table, structure) { const colCount = structure.columns; // 创建控制行 const controlRow = document.createElement('tr'); controlRow.className = 'table-control-row'; // 设置行高 controlRow.style.height = '24px'; // 为每列创建控制单元格 for (let i = 0; i < colCount; i++) { const controlCell = document.createElement(structure.hasThead ? 'th' : 'td'); controlCell.style.padding = '2px 4px'; controlCell.style.height = '24px'; controlCell.style.verticalAlign = 'middle'; // 创建控制容器 const controlContainer = createControlContainer(table, i, colCount); controlCell.appendChild(controlContainer); controlRow.appendChild(controlCell); } // 插入控制行 insertControlRow(table, controlRow, structure); // 初始刷新UI(处理已有筛选/排序的状态) for (let i = 0; i < colCount; i++) { updateControlUI(table, i); } } // 创建控制容器 function createControlContainer(table, columnIndex, colCount) { const container = document.createElement('div'); container.className = 'control-container'; // 按钮容器 const btnContainer = document.createElement('div'); btnContainer.className = 'btn-container'; // 排序按钮(合并为单按钮:三态循环 ⇅→↓→↑→⇅) const sortBtn = createControlButton('⇅', 'sort-btn', '排序'); sortBtn.dataset.action = 'sort'; sortBtn.dataset.columnIndex = columnIndex; // 筛选按钮 - 初始为放大镜 const filterBtn = createControlButton('🔍', 'filter-btn', '筛选'); filterBtn.dataset.action = 'filter'; filterBtn.dataset.columnIndex = columnIndex; // 清除筛选按钮 const clearFilterBtn = createControlButton('×', 'filter-btn', '清除筛选'); clearFilterBtn.dataset.action = 'clear-filter'; clearFilterBtn.dataset.columnIndex = columnIndex; clearFilterBtn.style.display = 'none'; // 默认隐藏 // 添加点击事件 [sortBtn, filterBtn, clearFilterBtn].forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); handleControlClick(table, this); }); }); btnContainer.appendChild(sortBtn); btnContainer.appendChild(filterBtn); btnContainer.appendChild(clearFilterBtn); // 如果是最后一列,添加隐藏/恢复按钮 X let hideBtn = null; if (typeof colCount === 'number' && columnIndex === colCount - 1) { hideBtn = createControlButton('X', 'control-hide-btn', '隐藏筛选控制行并恢复原始表格'); hideBtn.dataset.action = 'hide-controls'; hideBtn.dataset.columnIndex = columnIndex; hideBtn.addEventListener('click', function(e) { e.stopPropagation(); restoreOriginalTable(table); }); btnContainer.appendChild(hideBtn); // 列管理(隐藏/显示)入口 const colMgrBtn = createControlButton('👁', 'control-btn', '列管理(隐藏/显示)'); colMgrBtn.dataset.action = 'col-manage'; colMgrBtn.addEventListener('click', function(e) { e.stopPropagation(); showColManagerModal(table); }); btnContainer.appendChild(colMgrBtn); } // 筛选指示器 const filterIndicator = document.createElement('div'); filterIndicator.className = 'filter-indicator'; filterIndicator.dataset.columnIndex = columnIndex; container.appendChild(btnContainer); container.appendChild(filterIndicator); // 存储按钮引用(直接挂在元素上,避免序列化 DOM) container._controlButtons = { sortBtn, filterBtn, clearFilterBtn, filterIndicator }; if (hideBtn) container._controlButtons.hideBtn = hideBtn; return container; } // 轻量撤销:检测到原始数据刷新时调用。清空筛选/排序/高亮、恢复行可见性,但保留控制行与脚本功能。 // 注意:不调用 reorderTableRows(避免再次移动框架刚渲染好的行),行顺序交给原始框架。 function resetOurChanges(table) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); // 清空筛选与排序状态 if (tableState) { tableState.filters = new Map(); tableState.sortState = []; } // 恢复所有数据行可见、清除单元格高亮 const allRows = getTableAllDataRows(table); allRows.forEach(row => { row.classList.remove('row-filtered-out'); for (let i = 0; i < row.cells.length; i++) { removeHighlight(row.cells[i]); } }); // 以当前真实行集作为新的「原始顺序」(不主动 reorder) syncOriginalRows(table); // 刷新行号 updateRowNumbers(table); // 刷新所有列的控制按钮 UI(回到无筛选/无排序态) const controlRow = table.querySelector('.table-control-row'); const colCount = controlRow ? controlRow.cells.length : 0; for (let i = 0; i < colCount; i++) updateControlUI(table, i); // 数据刷新后 el-table 已重建行,重新应用计算列 const st0 = tableData.get(tableId); if (st0 && st0.calcColumns && st0.calcColumns.length) { ElTableAdapter.removeAllCalcCols(table); applyCalcColumns(table); } // 恢复用户列宽 + 隐藏列。此时数据刷新已由 checkRefresh 防抖(300ms)确认稳定, // 立即设记忆宽(不走 applyColWidthsStable 的 3s 延迟)——否则上面的 applyCalcColumns // 刚把计算列重建为默认宽(120px),却要再等 3s 才恢复记忆宽,造成查询后计算列空窗、 // 与刷新页面表现不一致。inline 抗 doLayout,立即设也压得过后续渲染。 applyColWidths(table); applyHiddenCols(table); // 更新指纹(幂等:reset 不改文本,指纹与刷新后一致,不会重复触发) table._lastFingerprint = computeTableFingerprint(table); console.log('[表格增强] 检测到数据刷新,已自动清除筛选/排序'); } // 恢复原始表格:移除控制行、清理筛选/排序、高亮和样式 function restoreOriginalTable(table) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); // 移除弹窗(若存在) document.querySelectorAll('.filter-modal-overlay').forEach(el => el.remove()); // 清除高亮 const allRows = getTableAllDataRows(table); allRows.forEach(row => { for (let i = 0; i < row.cells.length; i++) { removeHighlight(row.cells[i]); row.classList.remove('row-filtered-out'); } }); // 清空筛选和排序状态 if (tableState) { tableState.filters = new Map(); tableState.sortState = []; // 恢复 originalRows 顺序 if (tableState.originalRows && tableState.originalRows.length > 0) { reorderTableRows(table, tableState.originalRows.filter(r => getTableAllDataRows(table).includes(r))); } } // 恢复所有原始列显示(清除隐藏效果) origHeaders(table).forEach(n => setHidden(table, n, false)); // 移除计算列(表头 th + 表体 td + colgroup col + 控制行格子) ElTableAdapter.removeAllCalcCols(table); // 移除控制行 const controlRow = table.querySelector('.table-control-row'); if (controlRow) controlRow.remove(); // 停止行号监听并清除残留行号指示器(reorderTableRows 可能重新画过) if (table._rowNumObserver) { table._rowNumObserver.disconnect(); delete table._rowNumObserver; } clearTimeout(table._rowNumTimer); getTableAllDataRows(table).forEach(row => { const fc = row.cells[0]; if (fc) { const ind = fc.querySelector('.row-num-indicator'); if (ind) ind.remove(); } }); // 标记为已隐藏控制行,避免脚本立即重新添加控制行 table.dataset.controlsHidden = 'true'; // 移除处理标记,保留 data-controls-hidden 以防止自动再次处理 table.classList.remove('table-processed'); // 刷新 UI(若页面后续再次触发脚本会重新添加控制行) console.log('已恢复原始表格样式并移除控制行'); } // 创建控制按钮 function createControlButton(text, className, title) { const button = document.createElement('button'); button.className = `control-btn ${className}`; button.textContent = text; button.title = title; button.type = 'button'; return button; } // ===== 计算列配置弹窗(P12)===== const FUNC_LIST = [ 'round(x,n)', 'abs(x)', 'floor(x)', 'ceil(x)', 'max(...)', 'min(...)', 'mod(a,b)', 'concat(...)', 'left(s,n)', 'right(s,n)', 'mid(s,start,len)', 'len(s)', 'replace(s,a,b)', 'upper(s)', 'lower(s)', 'trim(s)', 'contains(s,sub)', 'date(s)', 'today()', 'year(d)', 'month(d)', 'day(d)', 'datediff(a,b,unit)', 'dateadd(s,n,unit)', 'if(cond,a,b)' ]; function needsBracket(name) { return !/^[一-鿿㐀-䶿a-zA-Z0-9_]+$/.test(name); } function showCalcColumnModal(table) { document.querySelectorAll('.filter-modal-overlay').forEach(el => el.remove()); const tableId = generateTableId(table); const st = tableData.get(tableId); const headers = ElTableAdapter.readHeaderNames(table); const overlay = document.createElement('div'); overlay.className = 'filter-modal-overlay'; const modal = document.createElement('div'); modal.className = 'filter-modal calc-modal'; modal.innerHTML = `
计算列管理
`; overlay.appendChild(modal); document.body.appendChild(overlay); setupCalcModalEvents(overlay, table, headers); setupModalDrag(modal, modal.querySelector('.filter-modal-header')); } function setupCalcModalEvents(overlay, table, headers) { const tableId = generateTableId(table); const st = tableData.get(tableId); let editingId = null; const $ = id => overlay.querySelector('#' + id); const listEl = $('calc-list'); const colSel = $('calc-insert-col'); const funcSel = $('calc-insert-func'); const colPh = document.createElement('option'); colPh.value = ''; colPh.textContent = '插入字段'; colSel.appendChild(colPh); headers.forEach(h => { const o = document.createElement('option'); o.value = h; o.textContent = h; colSel.appendChild(o); }); const funcPh = document.createElement('option'); funcPh.value = ''; funcPh.textContent = '插入公式'; funcSel.appendChild(funcPh); FUNC_LIST.forEach(f => { const o = document.createElement('option'); o.value = f; o.textContent = f; funcSel.appendChild(o); }); function renderList() { listEl.innerHTML = ''; st.calcColumns.forEach((d, i) => { const row = document.createElement('div'); row.className = 'calc-row'; row.innerHTML = `${d.name}${d.expr}`; const mk = (txt, fn, title) => { const b = document.createElement('button'); b.textContent = txt; b.title = title || ''; b.onclick = () => fn(i); return b; }; row.appendChild(mk('↑', () => move(i, -1), '上移(先求值)')); row.appendChild(mk('↓', () => move(i, 1), '下移')); row.appendChild(mk('✎', () => edit(d), '编辑')); row.appendChild(mk('🗑', () => { st.calcColumns.splice(i, 1); renderList(); }, '删除')); listEl.appendChild(row); }); } function move(i, dir) { const j = i + dir; if (j < 0 || j >= st.calcColumns.length) return; const tmp = st.calcColumns[i]; st.calcColumns[i] = st.calcColumns[j]; st.calcColumns[j] = tmp; renderList(); } function edit(d) { editingId = d.id; $('calc-edit').style.display = 'block'; $('calc-name').value = d.name; $('calc-expr').value = d.expr; $('calc-type').value = d.resultType || 'auto'; $('calc-width').value = d.width || 120; validateLive(); } function validateLive() { const name = $('calc-name').value.trim(); const expr = $('calc-expr').value.trim(); const msg = $('calc-msg'); if (!name) { msg.innerHTML = '请填列名'; return false; } const known = headers.concat(st.calcColumns.filter(c => c.id !== editingId).map(c => c.name)); const r = ExprEngine.validate(expr, known); if (!r.ok) { msg.innerHTML = '✗ ' + r.error + ''; return false; } msg.innerHTML = '✓ 合法 · 用到列: ' + (r.usedColumns.join(', ') || '无') + ''; return true; } function insertAtCursor(text) { const ta = $('calc-expr'); const s = ta.selectionStart, e = ta.selectionEnd; ta.value = ta.value.slice(0, s) + text + ta.value.slice(e); validateLive(); ta.focus(); } $('calc-add').onclick = () => { editingId = null; $('calc-edit').style.display = 'block'; $('calc-name').value = ''; $('calc-expr').value = ''; $('calc-type').value = 'auto'; $('calc-width').value = 120; validateLive(); }; $('calc-name').oninput = validateLive; $('calc-expr').oninput = validateLive; colSel.onchange = () => { const h = colSel.value; if (!h) return; insertAtCursor(needsBracket(h) ? '[' + h + ']' : h); colSel.selectedIndex = 0; }; funcSel.onchange = () => { const f = funcSel.value; if (!f) return; insertAtCursor(f.split('(')[0] + '('); funcSel.selectedIndex = 0; }; $('calc-save').onclick = () => { if (!validateLive()) return; const def = { id: editingId || ('cc_' + Date.now() + Math.random().toString(36).slice(2, 6)), name: $('calc-name').value.trim(), expr: $('calc-expr').value.trim(), resultType: $('calc-type').value, width: parseInt($('calc-width').value) || 120 }; if (editingId) { const i = st.calcColumns.findIndex(c => c.id === editingId); if (i >= 0) st.calcColumns[i] = def; } else { st.calcColumns.push(def); } $('calc-edit').style.display = 'none'; editingId = null; renderList(); }; $('calc-cancel-edit').onclick = () => { $('calc-edit').style.display = 'none'; editingId = null; }; const close = () => { st.calcPersist = $('calc-persist').checked; ElTableAdapter.removeAllCalcCols(table); // 先移除计算列,表头恢复原始(保证持久化 key 与加载时一致) if (st.calcPersist) saveCalcColumns(table); applyCalcColumns(table); // 再重新应用显示 overlay.remove(); }; $('calc-close').onclick = close; overlay.querySelector('.filter-modal-close:not(#calc-help-btn)').onclick = close; overlay.onclick = e => { if (e.target === overlay) close(); }; $('calc-help-btn').onclick = () => showCalcHelpModal(); renderList(); } // ===== ℹ️ 帮助文档(P13)===== function showCalcHelpModal() { document.querySelectorAll('.calc-help-overlay').forEach(el => el.remove()); const overlay = document.createElement('div'); overlay.className = 'filter-modal-overlay calc-help-overlay'; const modal = document.createElement('div'); modal.className = 'filter-modal calc-help-modal'; modal.innerHTML = `
计算列使用说明

一、列名引用

计算列通过列名引用其它列的值参与计算。

• 简单列名直接写:单价 * 数量
• 列名含括号/#/空格等特殊符号时,用方括号包裹:[单价(元)] * [数量]、[#订单号]
• 点「插入列」下拉可自动插入(含符号的会自动加方括号)

二、运算符

• 算术:+ - * / %(% 为取余,如 10 % 3 = 1)
• 比较:> < >= <= = !=(= 等于,!= 不等于),结果为 是/否
• 逻辑:and(且)、or(或)、not(非),也可写 && || !
• 三元:条件 ? 真值 : 假值
• 文本拼接:& 把两边连成文本。完成率 & '%' = 85%、单价 & ' 元'
• 优先级:小括号 () 最高 → 乘除模 → 加减 → 文本拼接 & → 比较 → not → and → or → 三元

三、数学函数

• round(x, n) —— 四舍五入到 n 位小数。round(3.14159, 2) = 3.14;round(金额, 0) = 四舍五入取整
• abs(x) —— 绝对值。abs(-5) = 5
• floor(x) —— 向下取整。floor(3.9) = 3
• ceil(x) —— 向上取整。ceil(3.1) = 4
• max(a, b, ...) / min(a, b, ...) —— 最大值 / 最小值(可多个参数)
• mod(a, b) —— a 除以 b 的余数。mod(10, 3) = 1

四、字符串函数

• concat(a, b, ...) —— 拼接(也可用 & 运算符)。concat(姓, 名) 或 姓 & 名
• left(s, n) / right(s, n) —— 左/右取 n 个字符。left(订单号, 4)
• mid(s, start, len) —— 从第 start 个字符取 len 个(start 从 1 开始)。mid(身份证, 7, 8)
• len(s) —— 字符长度
• replace(s, 旧, 新) —— 替换。replace(编号, '-', '') 去掉横线
• upper(s) / lower(s) —— 转大写 / 小写
• trim(s) —— 去首尾空格
• contains(s, 子串) —— 是否包含,返回 是/否。contains(备注, '急')

五、日期函数

• year(d) / month(d) / day(d) —— 取年/月/日。year(订单日期)
• datediff(d1, d2, 单位) —— 两日期/时间差。单位:'second'(秒)、'minute'(分)、'hour'(时)、'day'(天)、'month'(月)。需含时间部分才能算秒/分/时。datediff(完工, 开工, 'day')=天数,datediff(结束, 开始, 'hour')=小时数
• dateadd(d, n, 单位) —— 日期加 n 天/月
• today() —— 今天
• 日期格式支持:2024-03-15、2024/3/15、2024.3.15

六、条件函数

• if(条件, 真值, 假值) —— if(金额 > 1000, '大单', '小单')
• 三元等价:金额 > 1000 ? '大单' : '小单'
• 可嵌套:if(分 >= 90, 'A', if(分 >= 60, 'B', 'C'))

七、链式计算

后定义的计算列可引用前面已定义的计算列。例如先建 金额 = 单价 * 数量,再建 税额 = 金额 * 0.13。用 ↑↓ 调整求值顺序(被引用的列必须排在前面)。

八、数字自动清洗

单元格里的数字若带千分位逗号或货币符号会自动去除:"¥1,234.50" → 1234.5、"12.5kg" → 12.5、"85%" → 85。比较时若两边都是数字则按数值比,否则按文本比。

九、错误处理

单个单元格计算出错时显示 #ERR: 原因 并标红,不影响其它单元格与排序/筛选。保存表达式时会先校验语法与列名,非法不放行。

十、综合示例

• 金额:单价 * 数量
• 含税金额:round(单价 * 数量 * 1.13, 2)
• 客户等级:if(金额 >= 10000, 'VIP', if(金额 >= 1000, '普通', '零散'))
• 工期天数:datediff(完工日期, 开工日期, 'day')
• 编号清洗:upper(replace(编号, ' ', ''))
• 百分比:round(完成率 * 100, 1) & '%'

`; overlay.appendChild(modal); document.body.appendChild(overlay); const close = () => overlay.remove(); modal.querySelector('.filter-modal-close').onclick = close; overlay.onclick = e => { if (e.target === overlay) close(); }; } // 插入控制行 function insertControlRow(table, controlRow, structure) { if (structure.hasThead) { const thead = table.tHead; thead.appendChild(controlRow); } else if (structure.hasTbody) { const tbody = table.tBodies[0]; if (structure.headerRows.length > 0) { const lastHeaderRow = structure.headerRows[structure.headerRows.length - 1]; if (lastHeaderRow.parentElement === tbody) { tbody.insertBefore(controlRow, lastHeaderRow.nextSibling); } else { tbody.insertBefore(controlRow, tbody.firstChild); } } else { tbody.insertBefore(controlRow, tbody.firstChild); } } else { table.insertBefore(controlRow, table.firstChild); } } // 处理控制按钮点击 function handleControlClick(table, button) { const action = button.dataset.action; const columnIndex = parseInt(button.dataset.columnIndex); const tableId = generateTableId(table); switch (action) { case 'sort': cycleSort(table, columnIndex); break; case 'filter': showFilterModal(table, columnIndex); break; case 'clear-filter': clearFilter(table, columnIndex); break; } // 刷新该表格所有列UI const colCount = (table.querySelector('.table-control-row') || {}).cells?.length || 0; for (let i = 0; i < colCount; i++) updateControlUI(table, i); } // 切换排序(支持多列) function toggleSort(table, columnIndex, order) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; const existingIndex = tableState.sortState.findIndex(s => s.columnIndex === columnIndex); if (existingIndex >= 0) { // 若已存在相同排序方向 -> 取消该列排序 if (tableState.sortState[existingIndex].order === order) { tableState.sortState.splice(existingIndex, 1); } else { // 更改为新的方向,并把此列提升为最前(最高优先级) tableState.sortState.splice(existingIndex, 1); tableState.sortState.unshift({ columnIndex, order }); } } else { // 新增排序,放在最前作为最高优先级 tableState.sortState.unshift({ columnIndex, order }); } // 执行排序或恢复原始顺序 sortRows(table); } // 单按钮三态循环排序:无→降序→升序→无(还原),再循环 function cycleSort(table, columnIndex) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; const existingIndex = tableState.sortState.findIndex(s => s.columnIndex === columnIndex); const currentOrder = existingIndex >= 0 ? tableState.sortState[existingIndex].order : null; // 推进顺序:null → desc → asc → null(还原) let nextOrder; if (currentOrder === null) nextOrder = 'desc'; else if (currentOrder === 'desc') nextOrder = 'asc'; else nextOrder = null; // asc → 还原 // 先移除该列现有排序 if (existingIndex >= 0) tableState.sortState.splice(existingIndex, 1); // 新方向放入最前(最高优先级,保持多列排序行为一致);nextOrder 为 null 即还原该列 if (nextOrder) tableState.sortState.unshift({ columnIndex, order: nextOrder }); // 执行排序或恢复原始顺序 sortRows(table); } // 同步 originalRows:应对表格异步追加/重渲染导致 originalRows 过期,从而在“恢复原始顺序”时丢行 function syncOriginalRows(table) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; const currentAll = getTableAllDataRows(table); // 当前DOM里所有数据行(含被筛隐藏的) const currentSet = new Set(currentAll); // 先移除已不存在的行引用 tableState.originalRows = (tableState.originalRows || []).filter(r => currentSet.has(r)); // 再把新出现的行追加进 originalRows(保持“出现先后”的大致顺序) const existSet = new Set(tableState.originalRows); currentAll.forEach(r => { if (!existSet.has(r)) tableState.originalRows.push(r); }); } // 根据 tableState.sortState 对可见行进行排序(支持多列优先级) function sortRows(table) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; // ✅ 关键:每次排序/恢复前都同步一次,避免 originalRows 过期导致丢行 syncOriginalRows(table); const sortState = tableState.sortState; const visibleRows = getVisibleDataRows(table); if (visibleRows.length < 2) return; if (!sortState || sortState.length === 0) { // ✅ 恢复“原始顺序”(保留当前可见行),同时把 originalRows 没记录到的可见新行也带上,避免丢行 const originalPart = (tableState.originalRows || []).filter(r => visibleRows.includes(r)); const extras = visibleRows.filter(r => !(tableState.originalRows || []).includes(r)); reorderTableRows(table, originalPart.concat(extras)); return; } // 多列比较器 visibleRows.sort((a, b) => { for (let i = 0; i < sortState.length; i++) { const { columnIndex, order } = sortState[i]; const aCell = a.cells[columnIndex]; const bCell = b.cells[columnIndex]; const aText = aCell ? aCell.textContent.trim() : ''; const bText = bCell ? bCell.textContent.trim() : ''; const comparison = aText.localeCompare(bText, 'zh-CN', { sensitivity: 'base', numeric: true }); if (comparison !== 0) return order === 'asc' ? comparison : -comparison; } return 0; }); reorderTableRows(table, visibleRows); } // 更新控制UI状态(刷新单列状态显示,基于 tableState) function updateControlUI(table, columnIndex) { const controlIndicator = table.querySelector(`.filter-indicator[data-column-index="${columnIndex}"]`); const controlCell = controlIndicator?.closest('td, th'); if (!controlCell) return; const container = controlCell.querySelector('.control-container'); if (!container) return; let buttons = container._controlButtons; if (!buttons) { buttons = { sortBtn: container.querySelector('[data-action="sort"]'), filterBtn: container.querySelector('[data-action="filter"]'), clearFilterBtn: container.querySelector('[data-action="clear-filter"]'), filterIndicator: container.querySelector('.filter-indicator') }; } const tableId = generateTableId(table); const tableState = tableData.get(tableId); const filter = tableState?.filters?.get(columnIndex); // 重置排序按钮样式(默认无排序态:⇅ 灰) if (buttons.sortBtn) { buttons.sortBtn.classList.remove('active-asc'); buttons.sortBtn.classList.remove('active-desc'); buttons.sortBtn.textContent = '⇅'; buttons.sortBtn.title = '排序(点击循环:降序→升序→还原)'; } // 根据 sortState 设置样式(如果存在) if (tableState && tableState.sortState && buttons.sortBtn) { const sortEntry = tableState.sortState.find(s => s.columnIndex === columnIndex); if (sortEntry) { if (sortEntry.order === 'asc') { buttons.sortBtn.classList.add('active-asc'); buttons.sortBtn.textContent = '↑'; } else if (sortEntry.order === 'desc') { buttons.sortBtn.classList.add('active-desc'); buttons.sortBtn.textContent = '↓'; } // 设置 title 显示优先级信息 const rank = tableState.sortState.findIndex(s => s.columnIndex === columnIndex); if (rank >= 0) { const pri = rank + 1; const dir = sortEntry.order === 'asc' ? '升序' : '降序'; buttons.sortBtn.title = `${dir}(优先级 ${pri})`; } } } // 更新筛选按钮状态 if (buttons.filterBtn && buttons.clearFilterBtn && buttons.filterIndicator) { if (filter) { buttons.filterBtn.classList.add('active'); buttons.filterBtn.textContent = '⏳'; buttons.clearFilterBtn.style.display = 'inline-block'; buttons.filterIndicator.textContent = buildFilterIndicatorText(filter); buttons.filterIndicator.style.color = '#0d6efd'; } else { buttons.filterBtn.classList.remove('active'); buttons.filterBtn.textContent = '🔍'; buttons.clearFilterBtn.style.display = 'none'; buttons.filterIndicator.textContent = ''; } } } // 获取算法显示文本 function getAlgorithmText(algorithm) { const map = { 'equals': '等于', 'notEquals': '不等于', 'startsWith': '开头是', 'notStartsWith': '开头不是', 'endsWith': '结尾是', 'notEndsWith': '结尾不是', 'contains': '包含', 'notContains': '不包含', 'isEmpty': '为空', 'isNotEmpty': '不为空', 'numGt': '大于', 'numGte': '大于等于', 'numLt': '小于', 'numLte': '小于等于', 'numEq': '等于', 'numNe': '不等于', 'numBetween': '区间', 'dateGt': '晚于', 'dateGte': '晚于≥', 'dateLt': '早于', 'dateLte': '早于≤', 'dateEq': '等于', 'dateBetween': '区间' }; return map[algorithm] || algorithm; } // 构建筛选指示器文本(双条件摘要,超长由 ellipsis 截断) function buildFilterIndicatorText(filter) { const c1Valid = isConditionValid(filter.text1, filter.algorithm1); const c2Valid = isConditionValid(filter.text2, filter.algorithm2); const part = (text, alg) => text ? `${text}(${getAlgorithmText(alg)})` : getAlgorithmText(alg); if (c1Valid && c2Valid) { const comb = filter.combiner === 'or' ? '或' : '且'; return `${part(filter.text1, filter.algorithm1)} ${comb} ${part(filter.text2, filter.algorithm2)}`; } if (c1Valid) return part(filter.text1, filter.algorithm1); if (c2Valid) return part(filter.text2, filter.algorithm2); return ''; } // 处理排序(保留为兼容性使用,内部使用 toggleSort/sortRows) function handleSort(table, columnIndex, order) { toggleSort(table, columnIndex, order); } // 获取可见的数据行 function getVisibleDataRows(table) { const allRows = Array.from(table.rows); return allRows.filter(row => { if (row.classList.contains('table-control-row')) return false; const hasTh = row.querySelector('th'); const isHeaderRow = hasTh || row.parentElement.tagName === 'THEAD' || row.classList.contains('header'); return !isHeaderRow && row.cells.length > 0 && !row.classList.contains('row-filtered-out'); }); } // 重新排序表格行(将 sortedRows 的顺序插入到控制行之后) function reorderTableRows(table, sortedRows) { const controlRow = table.querySelector('.table-control-row'); // 确保存在 tbody:优先使用第一个 tbody,若不存在则创建 let tbody = table.tBodies && table.tBodies.length > 0 ? table.tBodies[0] : null; if (!tbody) { tbody = document.createElement('tbody'); table.appendChild(tbody); } // 将表格顶层(直接子元素为 tr)的数据行统一移入 tbody,避免数据行散落到 table 根节点或 thead Array.from(table.children).forEach(child => { if (child.tagName === 'TR' && child !== controlRow) { tbody.appendChild(child); } }); const parent = tbody; // 移除现有可见行(避免重复),只移除非控制行的数据行 const visibleRows = getVisibleDataRows(table); visibleRows.forEach(row => { if (row !== controlRow) row.remove(); }); // ✅ 关键修复:在移除之后再计算 refNode,避免 refNode 指向已被 remove 的节点 let refNode; if (controlRow && controlRow.parentElement === parent) { refNode = controlRow.nextSibling; // 可能是第一条“隐藏行”或 null } else { refNode = parent.firstChild; // 可能是第一条“隐藏行”或 null } if (refNode && refNode.parentNode !== parent) { refNode = null; // 不在 parent 下则直接追加 } // 过滤并插入排序后的行(确保不包含控制行或表头行) const frag = document.createDocumentFragment(); const filtered = (sortedRows || []).filter(r => r && r.tagName === 'TR' && !r.classList.contains('table-control-row')); filtered.forEach(row => frag.appendChild(row)); // refNode 为 null 时等价于 appendChild,安全 parent.insertBefore(frag, refNode); // 刷新行号 updateRowNumbers(table); } // 各类型的筛选算法集 const FILTER_ALGO_SETS = { text: [ { v: 'contains', l: '包含' }, { v: 'notContains', l: '不包含' }, { v: 'equals', l: '等于' }, { v: 'notEquals', l: '不等于' }, { v: 'startsWith', l: '开头是' }, { v: 'notStartsWith', l: '开头不是' }, { v: 'endsWith', l: '结尾是' }, { v: 'notEndsWith', l: '结尾不是' }, { v: 'isEmpty', l: '为空' }, { v: 'isNotEmpty', l: '不为空' } ], number: [ { v: 'numGt', l: '大于 >' }, { v: 'numGte', l: '大于等于 ≥' }, { v: 'numLt', l: '小于 <' }, { v: 'numLte', l: '小于等于 ≤' }, { v: 'numEq', l: '等于 =' }, { v: 'numNe', l: '不等于 ≠' } ], date: [ { v: 'dateGt', l: '晚于' }, { v: 'dateGte', l: '晚于或等于' }, { v: 'dateLt', l: '早于' }, { v: 'dateLte', l: '早于或等于' }, { v: 'dateEq', l: '等于' } ] }; // 推断列类型:计算列 number 结果 → 数值,否则文本 function inferColType(table, columnIndex) { const tableId = generateTableId(table); const st = tableData.get(tableId); if (st && st.calcColumns && st.calcColumns.length) { const headers = ElTableAdapter.readHeaderNames(table); const origColCount = headers.length - st.calcColumns.length; const calcIdx = columnIndex - origColCount; if (calcIdx >= 0 && calcIdx < st.calcColumns.length) { const def = st.calcColumns[calcIdx]; if (def && def.resultType === 'number') return 'number'; } } return 'text'; } // 显示筛选弹窗 function showFilterModal(table, columnIndex) { // 移除现有弹窗 const existingModal = document.querySelector('.filter-modal-overlay'); if (existingModal) existingModal.remove(); // 创建弹窗 const modalOverlay = document.createElement('div'); modalOverlay.className = 'filter-modal-overlay'; const modal = document.createElement('div'); modal.className = 'filter-modal'; // 获取当前筛选状态 const tableId = generateTableId(table); const tableState = tableData.get(tableId); const currentFilter = tableState?.filters?.get(columnIndex); // 双条件:条件1/条件2 各自的算法与文本(兼容旧的单条件数据 {text, algorithm}) const cf = currentFilter || {}; const selAlg1 = cf.algorithm1 || cf.algorithm || 'contains'; const selAlg2 = cf.algorithm2 || 'contains'; // 算法选项区初始展示「条件1」的算法 const selAlg = selAlg1; // 弹窗头部 const header = document.createElement('div'); header.className = 'filter-modal-header'; header.innerHTML = ` 列筛选设置 `; // 弹窗内容 const body = document.createElement('div'); body.className = 'filter-modal-body'; body.innerHTML = `
数值/日期可比较大小
条件一
条件二
读作:条件一「比较方式 + 内容」 并且/或者 条件二「比较方式 + 内容」
`; // 弹窗底部 const footer = document.createElement('div'); footer.className = 'filter-modal-footer'; footer.innerHTML = ` `; modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); modalOverlay.appendChild(modal); // 添加到文档 document.body.appendChild(modalOverlay); // 添加事件监听 setupFilterModalEvents(modalOverlay, table, columnIndex, currentFilter); // 设置拖拽功能 setupModalDrag(modal, header); } // 设置弹窗拖拽功能 function setupModalDrag(modal, header) { header.addEventListener('mousedown', startDrag); function startDrag(e) { if (e.button !== 0) return; // 只响应左键 dragData.isDragging = true; dragData.startX = e.clientX; dragData.startY = e.clientY; // 获取当前弹窗位置 const rect = modal.getBoundingClientRect(); dragData.initialX = rect.left; dragData.initialY = rect.top; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); modal.style.userSelect = 'none'; } function onDrag(e) { if (!dragData.isDragging) return; const deltaX = e.clientX - dragData.startX; const deltaY = e.clientY - dragData.startY; modal.style.position = 'fixed'; modal.style.left = (dragData.initialX + deltaX) + 'px'; modal.style.top = (dragData.initialY + deltaY) + 'px'; } function stopDrag() { dragData.isDragging = false; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); modal.style.userSelect = ''; } } // 设置筛选弹窗事件(双条件:输入1 与 输入2,各自算法,并且/或者组合) function setupFilterModalEvents(modalOverlay, table, columnIndex, currentFilter) { const modal = modalOverlay.querySelector('.filter-modal'); const closeBtn = modal.querySelector('.filter-modal-close'); const cancelBtn = modal.querySelector('.filter-modal-btn-cancel'); const applyBtn = modal.querySelector('.filter-modal-btn-apply'); const clearBtn = modal.querySelector('.filter-modal-btn-clear'); const textInput1 = modal.querySelector('#filter-text1'); const textInput2 = modal.querySelector('#filter-text2'); const getCombiner = () => (modal.querySelector('input[name="filter-combiner"]:checked') || {}).value || 'and'; const typeSelect = modal.querySelector('#filter-col-type'); const algoSelect1 = modal.querySelector('#filter-algo1'); const algoSelect2 = modal.querySelector('#filter-algo2'); const cf = currentFilter || {}; let colType = cf.type || inferColType(table, columnIndex); typeSelect.value = colType; const inSet = alg => FILTER_ALGO_SETS[colType].some(a => a.v === alg); let cond1Alg = cf.algorithm1 || cf.algorithm; if (!cond1Alg || !inSet(cond1Alg)) cond1Alg = FILTER_ALGO_SETS[colType][0].v; let cond2Alg = cf.algorithm2; if (!cond2Alg || !inSet(cond2Alg)) cond2Alg = FILTER_ALGO_SETS[colType][0].v; closeBtn.addEventListener('click', () => modalOverlay.remove()); cancelBtn.addEventListener('click', () => modalOverlay.remove()); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) modalOverlay.remove(); }); // 填充两个条件的「比较方式」下拉(选项随数据类型变化),并各自选中当前算法 function fillAlgoSelects() { const set = FILTER_ALGO_SETS[colType]; [[algoSelect1, cond1Alg], [algoSelect2, cond2Alg]].forEach(([sel, cur]) => { sel.innerHTML = ''; set.forEach(a => { const o = document.createElement('option'); o.value = a.v; o.textContent = a.l; if (a.v === cur) o.selected = true; sel.appendChild(o); }); }); } // 为空/不为空(仅文本类型有)禁用对应输入框 function refreshInputDisabled() { const applyInputState = (input, alg) => { if (alg === 'isEmpty' || alg === 'isNotEmpty') { input.value = ''; input.disabled = true; input.placeholder = '无需输入'; } else { input.disabled = false; if (colType === 'number') input.placeholder = '输入数值'; else if (colType === 'date') input.placeholder = '输入日期,如 2024-01-01'; else input.placeholder = '输入内容'; } }; applyInputState(textInput1, cond1Alg); applyInputState(textInput2, cond2Alg); } // 类型切换:重置两条件算法为该类型第一个 typeSelect.addEventListener('change', () => { colType = typeSelect.value; cond1Alg = FILTER_ALGO_SETS[colType][0].v; cond2Alg = FILTER_ALGO_SETS[colType][0].v; fillAlgoSelects(); refreshInputDisabled(); }); // 比较方式下拉变更:更新对应条件算法 algoSelect1.addEventListener('change', () => { cond1Alg = algoSelect1.value; refreshInputDisabled(); }); algoSelect2.addEventListener('change', () => { cond2Alg = algoSelect2.value; refreshInputDisabled(); }); applyBtn.addEventListener('click', () => { const text1 = textInput1.value.trim(); const text2 = textInput2.value.trim(); const combiner = getCombiner(); applyFilter(table, columnIndex, { type: colType, text1, algorithm1: cond1Alg, text2, algorithm2: cond2Alg, combiner }); modalOverlay.remove(); }); clearBtn.addEventListener('click', () => { clearFilter(table, columnIndex); modalOverlay.remove(); }); [textInput1, textInput2].forEach(input => { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') applyBtn.click(); }); }); fillAlgoSelects(); refreshInputDisabled(); setTimeout(() => { if (!textInput1.disabled) textInput1.focus(); }, 100); } // 判断单个条件是否有效(参与匹配/高亮):为空/不为空不依赖文本,其余需文本非空 function isConditionValid(text, algorithm) { if (algorithm === 'isEmpty' || algorithm === 'isNotEmpty') return true; return !!text; } // 应用筛选(双条件;仅保存该列条件,由 applyAllFilters 统一生效) function applyFilter(table, columnIndex, cond) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; // 规范化条件对象(容错) const text1 = (cond.text1 || '').trim(); const text2 = (cond.text2 || '').trim(); const algorithm1 = cond.algorithm1 || 'contains'; const algorithm2 = cond.algorithm2 || 'contains'; const combiner = cond.combiner === 'or' ? 'or' : 'and'; const type = cond.type || 'text'; const c1Valid = isConditionValid(text1, algorithm1); const c2Valid = isConditionValid(text2, algorithm2); if (!c1Valid && !c2Valid) { // 两个条件都无效 → 清除该列筛选 tableState.filters.delete(columnIndex); } else { tableState.filters.set(columnIndex, { type, text1, algorithm1, text2, algorithm2, combiner }); } // 统一应用所有筛选 applyAllFilters(table); // 重新应用当前排序(如果有) if (tableState.sortState && tableState.sortState.length > 0) { sortRows(table); } // 刷新UI const colCount = (table.querySelector('.table-control-row') || {}).cells?.length || 0; for (let i = 0; i < colCount; i++) updateControlUI(table, i); console.log(`应用筛选: 列${columnIndex}`, tableState.filters.get(columnIndex)); } // 统一应用当前表格的所有筛选(多列“与”关系;每列内部为双条件 and/or) function applyAllFilters(table) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; // ✅ 同步一次,确保后续“清除筛选/恢复顺序”不会因为 originalRows 过期而掉行 syncOriginalRows(table); const filters = Array.from(tableState.filters.entries()); // [ [colIndex, {text1,algorithm1,text2,algorithm2,combiner}], ... ] const allDataRows = getTableAllDataRows(table); allDataRows.forEach(row => { let rowMatches = true; for (let [colIndex, filter] of filters) { const cell = row.cells[colIndex]; const cellText = cell ? (cell.textContent || '').trim() : ''; const isMatch = checkDualMatch(cellText, filter); if (!isMatch) { rowMatches = false; break; } } if (rowMatches) row.classList.remove('row-filtered-out'); else row.classList.add('row-filtered-out'); for (let i = 0; i < row.cells.length; i++) { const cell = row.cells[i]; const colFilter = tableState.filters.get(i); if (colFilter) { highlightMatch(cell, colFilter); } else { removeHighlight(cell); } } }); // 刷新行号 updateRowNumbers(table); } // 数值筛选 function checkNumberMatch(cellText, filterText, algorithm) { const cell = ExprEngine.toNum(cellText); const blank = (cellText == null || String(cellText).trim() === ''); if (algorithm === 'isEmpty') return blank || isNaN(cell); if (algorithm === 'isNotEmpty') return !blank && !isNaN(cell); if (!filterText) return true; if (algorithm === 'numBetween') { if (isNaN(cell)) return false; const parts = filterText.split(','); const lo = ExprEngine.toNum(parts[0]); const hi = ExprEngine.toNum(parts.length > 1 ? parts[1] : parts[0]); if (!isNaN(lo) && cell < lo) return false; if (!isNaN(hi) && cell > hi) return false; return true; } const f = ExprEngine.toNum(filterText); if (isNaN(cell) || isNaN(f)) return false; switch (algorithm) { case 'numGt': return cell > f; case 'numGte': return cell >= f; case 'numLt': return cell < f; case 'numLte': return cell <= f; case 'numEq': return cell === f; case 'numNe': return cell !== f; } return false; } // 日期筛选 function checkDateMatch(cellText, filterText, algorithm) { const cell = ExprEngine.parseDate(cellText); const blank = (cellText == null || String(cellText).trim() === ''); if (algorithm === 'isEmpty') return blank || !cell; if (algorithm === 'isNotEmpty') return !blank && !!cell; if (!filterText) return true; if (algorithm === 'dateBetween') { if (!cell) return false; const parts = filterText.split(','); const lo = ExprEngine.parseDate(parts[0]); const hi = ExprEngine.parseDate(parts.length > 1 ? parts[1] : parts[0]); const t = cell.getTime(); if (lo && t < lo.getTime()) return false; if (hi && t > hi.getTime()) return false; return true; } const f = ExprEngine.parseDate(filterText); if (!cell || !f) return false; const ct = cell.getTime(), ft = f.getTime(); switch (algorithm) { case 'dateGt': return ct > ft; case 'dateGte': return ct >= ft; case 'dateLt': return ct < ft; case 'dateLte': return ct <= ft; case 'dateEq': return ct === ft; } return false; } // 检查筛选匹配(按列类型分派) function checkFilterMatch(cellText, filterText, algorithm, type) { type = type || 'text'; if (type === 'number') return checkNumberMatch(cellText, filterText, algorithm); if (type === 'date') return checkDateMatch(cellText, filterText, algorithm); // 文本:对于非 空/非空 算法,若没有 filterText 则视为不限制 if ((algorithm !== 'isEmpty' && algorithm !== 'isNotEmpty') && !filterText) return true; const text = (cellText || '').toLowerCase(); const filter = (filterText || '').toLowerCase(); switch (algorithm) { case 'isEmpty': return (cellText || '').trim() === ''; case 'isNotEmpty': return (cellText || '').trim() !== ''; case 'startsWith': return text.startsWith(filter); case 'notStartsWith': return !text.startsWith(filter); case 'endsWith': return text.endsWith(filter); case 'notEndsWith': return !text.endsWith(filter); case 'equals': return text === filter; case 'notEquals': return text !== filter; case 'notContains': return !text.includes(filter); case 'contains': default: return text.includes(filter); } } // 双条件匹配:条件1 (and/or) 条件2;无效条件不参与 function checkDualMatch(cellText, filter) { if (!filter) return true; const type = filter.type || 'text'; const c1Valid = isConditionValid(filter.text1, filter.algorithm1); const c2Valid = isConditionValid(filter.text2, filter.algorithm2); const m1 = c1Valid ? checkFilterMatch(cellText, filter.text1, filter.algorithm1, type) : null; const m2 = c2Valid ? checkFilterMatch(cellText, filter.text2, filter.algorithm2, type) : null; if (m1 !== null && m2 !== null) { return filter.combiner === 'or' ? (m1 || m2) : (m1 && m2); } if (m1 !== null) return m1; if (m2 !== null) return m2; return true; // 两个条件都无效 → 不过滤 } // 高亮匹配文本(双条件;基于单元格原始 HTML 累积替换,支持同时高亮两个关键词) function highlightMatch(cell, filter) { if (!cell || !filter) return; // 保存原始 html(首次保存) if (!cell.dataset.originalHtml) { cell.dataset.originalHtml = cell.innerHTML; } const originalHTML = cell.dataset.originalHtml; // 数值/日期类型:不高亮文本片段(仅靠隐藏行表达),恢复原 html 后返回 const type = filter.type || 'text'; if (type === 'number' || type === 'date') { cell.innerHTML = originalHTML; return; } // 收集需要高亮的 {text, algorithm}(否定算法与无效条件不高亮) const noHighlightAlgorithms = new Set(['notContains', 'notEquals', 'notStartsWith', 'notEndsWith', 'isEmpty', 'isNotEmpty']); const targets = []; if (filter.text1 && !noHighlightAlgorithms.has(filter.algorithm1)) targets.push({ text: filter.text1, algorithm: filter.algorithm1 }); if (filter.text2 && !noHighlightAlgorithms.has(filter.algorithm2)) targets.push({ text: filter.text2, algorithm: filter.algorithm2 }); if (targets.length === 0) { cell.innerHTML = originalHTML; return; } // 在同一份 originalHTML 基础上依次叠加两个关键词的高亮 let html = originalHTML; for (const t of targets) { const escaped = escapeRegExp(t.text); const regexMap = { 'contains': new RegExp(`(${escaped})`, 'gi'), 'startsWith': new RegExp(`^(${escaped})`, 'gi'), 'equals': new RegExp(`^(${escaped})$`, 'gi'), 'endsWith': new RegExp(`(${escaped})$`, 'gi') }; const regex = regexMap[t.algorithm] || regexMap.contains; try { html = html.replace(regex, '$1'); } catch (err) { // 忽略单次替换失败,继续处理其余关键词 } } cell.innerHTML = html; } // 移除高亮(恢复到原始 HTML) function removeHighlight(cell) { if (!cell) return; if (cell.dataset.originalHtml) { cell.innerHTML = cell.dataset.originalHtml; delete cell.dataset.originalHtml; } } // 清除筛选(清除单列,然后重新应用其他列筛选) function clearFilter(table, columnIndex) { const tableId = generateTableId(table); const tableState = tableData.get(tableId); if (!tableState) return; // 移除筛选条件 tableState.filters.delete(columnIndex); // 重新应用所有筛选(剩余) applyAllFilters(table); // 恢复排序或重新排序 if (tableState.sortState && tableState.sortState.length > 0) { sortRows(table); } else { // 若无排序,恢复原始顺序(可见行) sortRows(table); } // 刷新UI const colCount = (table.querySelector('.table-control-row') || {}).cells?.length || 0; for (let i = 0; i < colCount; i++) updateControlUI(table, i); console.log(`清除列${columnIndex}的筛选`); } // 获取表格所有数据行 function getTableAllDataRows(table) { const allRows = Array.from(table.rows); return allRows.filter(row => { if (row.classList.contains('table-control-row')) return false; const hasTh = row.querySelector('th'); const isHeaderRow = hasTh || row.parentElement.tagName === 'THEAD' || row.classList.contains('header'); return !isHeaderRow && row.cells.length > 0; }); } // 计算表格「数据指纹」:用于检测原始数据是否被刷新。 // 设计为对本脚本自身的操作(排序改顺序 / 筛选改 class / 行号 span / 高亮包span)全部不敏感, // 只对「数据行文本内容变化」敏感:行数 + 所有数据行(剔除行号span后)的文本 排序后拼接(顺序无关)。 function computeTableFingerprint(table) { const rows = getTableAllDataRows(table); if (rows.length === 0) return '0'; const rowFps = rows.map(row => { const clone = row.cloneNode(true); clone.querySelectorAll('.row-num-indicator').forEach(el => el.remove()); clone.querySelectorAll('[data-calc-col]').forEach(el => el.remove()); // 剔除计算列单元格,防误判 return (clone.textContent || '').replace(/\s+/g, ' ').trim(); }); rowFps.sort(); let joined = rowFps.join(''); if (joined.length > 8000) joined = joined.slice(0, 8000); return rows.length + '::' + joined; } // 更新行号指示器(在可见行的第一个单元格左上角显示行号) function updateRowNumbers(table) { const visibleRows = getVisibleDataRows(table); visibleRows.forEach((row, idx) => { const firstCell = row.cells[0]; if (!firstCell) return; // 用内联 style 确保不被页面 CSS 覆盖 firstCell.style.position = 'relative'; let indicator = firstCell.querySelector('.row-num-indicator'); if (!indicator) { indicator = document.createElement('span'); indicator.className = 'row-num-indicator'; indicator.style.cssText = 'position:absolute;left:0;top:0;background:#fff;color:#000;font-size:10px;line-height:1;padding:2px 4px;border-radius:0 3px 3px 0;z-index:10;white-space:nowrap;pointer-events:none;font-family:"Microsoft YaHei","Segoe UI",sans-serif;box-shadow:1px 1px 3px rgba(0,0,0,0.15);'; firstCell.appendChild(indicator); } indicator.textContent = idx + 1; }); // 清除不可见行上的行号样式(防止恢复显示时残留) const allRows = getTableAllDataRows(table); allRows.forEach(row => { if (row.classList.contains('row-filtered-out')) { const firstCell = row.cells[0]; if (firstCell) { const ind = firstCell.querySelector('.row-num-indicator'); if (ind) ind.remove(); } } }); } // 监听已处理表格内部「行」的变化:element-ui 重渲染行后自动重画行号,保证行号稳定常显; // 同时叠加「数据刷新检测」:当原始数据变化(查询刷新)时自动撤销本脚本的筛选/排序。 function setupTableRowsObserver(table) { if (table._rowNumObserver) return; // 避免重复挂载 const observer = new MutationObserver((mutations) => { // 1) 行(tr)增删/替换 → 刷新行号(忽略我们注入的 indicator span 变化,避免循环) let rowChanged = false; for (const m of mutations) { const nodes = [...m.addedNodes, ...m.removedNodes]; for (const n of nodes) { if (n.nodeType !== 1) continue; if (n.tagName === 'TR' || (n.querySelector && n.querySelector('tr'))) { rowChanged = true; break; } } if (rowChanged) break; } if (rowChanged) { clearTimeout(table._rowNumTimer); table._rowNumTimer = setTimeout(() => { // 仅在表格仍挂载且控制行存在(未恢复原始)时刷新行号 if (table.isConnected && table.querySelector('.table-control-row')) { updateRowNumbers(table); // 计算列:el-table 重建 td 后需重新加列 + 重算(防抖 + 重入标记防循环) const st = tableData.get(generateTableId(table)); if (st && st.calcColumns && st.calcColumns.length && !table._calcRecomputing) { table._calcRecomputing = true; ElTableAdapter.removeAllCalcCols(table); applyCalcColumns(table); setTimeout(() => { delete table._calcRecomputing; }, 80); } // 重渲染后恢复用户列宽(el-table 会重置 col.width) applyColWidths(table); // 重渲染后重新隐藏已隐藏列(el-table 重建 td 会复位 display) applyHiddenCols(table); } }, 60); } // 2) 任何变化(含原地改 td 文本)都触发「数据刷新」防抖检测 scheduleRefreshCheck(table); }); observer.observe(table, { childList: true, subtree: true, characterData: true }); table._rowNumObserver = observer; } // 防抖触发「数据刷新」检测:聚合连续 DOM 变更,静止后才比对指纹 function scheduleRefreshCheck(table) { clearTimeout(table._refreshTimer); table._refreshTimer = setTimeout(() => checkRefresh(table), 300); } // 比对数据指纹:若变化(=原始数据刷新)→ 轻量撤销筛选/排序;否则记录当前指纹 function checkRefresh(table) { if (table._inReset) return; // reset 进行中,跳过(防御重入) if (!table.isConnected) return; // 表格已移除 if (!table.querySelector('.table-control-row')) return; // 控制行不在(已恢复原始),跳过 const fp = computeTableFingerprint(table); if (table._lastFingerprint && fp !== table._lastFingerprint) { table._inReset = true; try { resetOurChanges(table); // 内部会更新 _lastFingerprint } finally { table._inReset = false; } } else { table._lastFingerprint = fp; } } // 转义正则表达式特殊字符 function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // 设置iframe监听 function setupIframeObserver() { document.querySelectorAll('iframe').forEach(iframe => { setupIframe(iframe); }); const iframeObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.tagName === 'IFRAME') { setupIframe(node); } else if (node.querySelectorAll) { node.querySelectorAll('iframe').forEach(iframe => { setupIframe(iframe); }); } }); }); }); iframeObserver.observe(document.body, { childList: true, subtree: true }); } // 设置iframe处理 function setupIframe(iframe) { try { if (iframe.contentDocument) { processIframe(iframe); } else { iframe.addEventListener('load', () => { processIframe(iframe); }); } } catch (error) { console.log('无法访问iframe内容:', error.message); } } // 处理iframe内容 function processIframe(iframe) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const iframeUrl = iframe.src || iframeDoc.URL; if (!iframeUrl.includes('jlcerp.com')) { return; } processDocument(iframeDoc); const iframeObserver = new MutationObserver(() => { processDocument(iframeDoc); }); iframeObserver.observe(iframeDoc.body, { childList: true, subtree: true }); } catch (error) { console.log('处理iframe时出错:', error.message); } } // 设置MutationObserver监听DOM变化 function setupMutationObserver() { const observer = new MutationObserver((mutations) => { let shouldProcess = false; mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { shouldProcess = true; } }); if (shouldProcess) { setTimeout(() => { processDocument(document); }, 100); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); } // 启动初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { setTimeout(initialize, 1000); } })();