// ==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);
}
})();