// ==UserScript==
// @name 表格排序与筛选增强(支持iframe)
// @namespace http://tampermonkey.net/
// @version 3.16
// @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
// ==/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: 400px !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 && (expr[j + 1] === quote || expr[j + 1] === '\\')) { 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;
}
function buildRegex(pat, flags) {
try { return new RegExp(toStr(pat), toStr(flags || '')); }
catch (e) { throw new Error('无效正则表达式: ' + toStr(pat) + (e && e.message ? '(' + e.message + ')' : '')); }
}
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)),
regexReplace: (s, pat, repl, flags) => toStr(s).replace(buildRegex(pat, toStr(flags || '') || 'g'), toStr(repl)), // 默认 'g' 全部替换
regexMatch: (s, pat, flags) => buildRegex(pat, toStr(flags || '')).test(toStr(s)),
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();
}
};
// 小写键索引:函数名大小写宽容查找(RegexMatch / REGEXMATCH 均可用)
const FUNCS_LC = {};
Object.keys(FUNCS).forEach(k => { if (!(k.toLowerCase() in FUNCS_LC)) FUNCS_LC[k.toLowerCase()] = FUNCS[k]; });
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': {
const fn = FUNCS[node.name] || FUNCS_LC[String(node.name).toLowerCase()]; // 函数名大小写宽容
if (!fn) throw new Error('未知函数: ' + node.name);
return fn.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, calcId) {
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);
if (calcId) th.setAttribute('data-calc-col-id', calcId);
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; setColStyleWidth(col, w); col.setAttribute('data-calc-col', '');
if (calcId) col.setAttribute('data-calc-col-id', calcId);
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);
if (calcId) td.setAttribute('data-calc-col-id', calcId);
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);
if (calcId) cell.setAttribute('data-calc-col-id', calcId);
controlRow.appendChild(cell);
}
const bcg = bodyTable.querySelector('colgroup');
if (bcg) {
const col = document.createElement('col');
col.width = w; setColStyleWidth(col, w); col.setAttribute('data-calc-col', '');
if (calcId) col.setAttribute('data-calc-col-id', calcId);
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('|'); }
// ===== 列标识与存储 key 纯逻辑(仅服务隐藏列/计算列) =====
const COL_WIDTH_MIN = 30;
const COL_WIDTH_MAX = 5000;
function normalizeColWidth(value) {
if (value === null || value === undefined || value === '') return null;
const width = Math.round(Number(value));
return Number.isFinite(width) && width >= COL_WIDTH_MIN && width <= COL_WIDTH_MAX ? width : null;
}
function originalColumnId(index, name) {
return 'o:' + index + ':' + encodeURIComponent(String(name || ''));
}
function calcColumnId(id) { return 'c:' + encodeURIComponent(String(id || '')); }
function makeColumnDescriptorsFromState(headers, calcColumns) {
const descriptors = (headers || []).map((name, index) => ({
id: originalColumnId(index, name), index, name: String(name || ''), kind: 'original'
}));
(calcColumns || []).forEach((column, calcIndex) => {
descriptors.push({
id: calcColumnId(column.id), index: descriptors.length,
name: String(column.name || ''), kind: 'calc', calcIndex
});
});
return descriptors;
}
function canonicalPageUrl(value) {
let url;
try { url = new URL(String(value)); } catch (e) { return String(value || '').split('?')[0]; }
let route = '';
const hash = (url.hash || '').slice(1);
if (/^!?\//.test(hash)) route = hash.split('?')[0].replace(/\/$/, '');
const path = url.pathname.replace(/\/$/, '') || '/';
return url.origin + path + (route ? '#' + route : '');
}
function MakeTableCalcStore(table, headers) {
if (typeof GM_getValue === 'undefined') return null;
const key = makeScopedTableStoreKey('calcCols', table, 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();
// 迁移旧版 exact URL + 表头 key;同结构多表的旧值只能安全归给首表。
if ((!saved || !Array.isArray(saved) || !saved.length) && !/^schema:(?!0$)/.test(getTableScopeKey(table))) {
const legacyRaw = findLegacyScopedStoreValue('calcCols', table, headers);
if (legacyRaw) {
try {
const legacy = JSON.parse(legacyRaw);
if (Array.isArray(legacy) && legacy.length) { saved = legacy; if (store) store.save(legacy); }
} catch (e) {}
}
}
// 迁移:旧版本曾把配置存到"含计算列名后缀"的污染 key。若原始 key 无值,
// 扫描污染 key,取 id 时间戳最新的迁移过来,并删除污染 key。
if ((!saved || !Array.isArray(saved) || !saved.length) && typeof GM_listValues !== 'undefined') {
const baseKey = 'calcCols_' + makeCalcKey(getTablePageUrl(table), 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 左右内边距/余量
}
// 计算列自适应:按内容设宽
function autoFitCalcColumns(table) {
const st = tableData.get(generateTableId(table));
if (!st || !st.calcColumns || !st.calcColumns.length) return;
const baseIndex = origHeaders(table).length;
st.calcColumns.forEach((def, calcIndex) => {
const w = measureCalcColWidth(table, def.name);
if (w) eachColByIndex(table, baseIndex + calcIndex, 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, def.id));
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);
// 计算列内容自适应宽度
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 makeScopedTableStoreKey('colHidden', table, origHeaders(table));
}
function loadHiddenCols(table) {
const st = tableData.get(generateTableId(table));
if (!st || typeof GM_getValue === 'undefined') return;
try {
let raw = GM_getValue(colHiddenStoreKey(table), undefined);
if ((raw === undefined || raw === null || raw === '') && !/^schema:(?!0$)/.test(getTableScopeKey(table))) {
raw = findLegacyScopedStoreValue('colHidden', table, origHeaders(table));
}
const value = JSON.parse(raw || '[]');
if (Array.isArray(value)) {
const columns = getColumnDescriptors(table);
const validIds = new Set(columns.map(column => column.id));
const consumedNames = new Set();
let migrated = false;
st.hiddenCols = value.map(item => {
if (validIds.has(item)) return item;
const column = columns.find(candidate => candidate.name === item && !consumedNames.has(candidate.name));
if (column) { consumedNames.add(column.name); migrated = true; return column.id; }
migrated = true;
return null;
}).filter(id => {
if (!id) return false;
const column = columns.find(candidate => candidate.id === id);
const allowed = column && !(column.kind === 'original' && column.index === origHeaders(table).length - 1);
if (!allowed) migrated = true;
return allowed;
});
if ((migrated || raw !== GM_getValue(colHiddenStoreKey(table), undefined)) && typeof GM_setValue !== 'undefined') saveHiddenCols(table);
}
} 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 宽度
function setHidden(table, columnRef, hidden) {
const idx = typeof columnRef === 'number' ? columnRef : getColIndexByName(table, columnRef);
if (idx < 0) return;
if (hidden && idx === origHeaders(table).length - 1) return; // 工具入口所在原始末列必须可见
const root = table.closest('.el-table');
if (!root) return;
const disp = hidden ? 'none' : '';
eachColByIndex(table, idx, 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 columns = getColumnDescriptors(table);
const byId = new Map(columns.map(column => [column.id, column]));
columns.forEach(column => setHidden(table, column.index, false));
(st.hiddenCols || []).forEach(id => {
const column = byId.get(id);
if (column) setHidden(table, column.index, true);
});
syncTableWidth(table);
}
// 列管理对话框(隐藏/显示)
function showColManagerModal(table) {
document.querySelectorAll('.filter-modal-overlay').forEach(el => el.remove());
const st = tableData.get(generateTableId(table));
const columns = getColumnDescriptors(table);
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 = '';
columns.forEach(column => {
const n = column.name;
const isHidden = hiddenSet.has(column.id);
const row = document.createElement('div');
row.className = 'calc-row';
row.innerHTML = `${n}${isHidden ? '已隐藏' : '显示中'}`;
const btn = document.createElement('button');
const protectedToolColumn = column.kind === 'original' && column.index === origHeaders(table).length - 1;
btn.textContent = protectedToolColumn ? '工具列' : (isHidden ? '显示' : '隐藏');
btn.disabled = protectedToolColumn;
btn.title = protectedToolColumn ? '列管理入口位于此列,不能隐藏' : '';
btn.onclick = () => {
if (protectedToolColumn) return;
if (hiddenSet.has(column.id)) { hiddenSet.delete(column.id); setHidden(table, column.index, false); }
else { hiddenSet.add(column.id); setHidden(table, column.index, true); }
st.hiddenCols = Array.from(hiddenSet);
saveHiddenCols(table);
syncTableWidth(table);
render();
};
row.appendChild(btn);
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); };
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');
}
});
}
// ===== 表格作用域 key(同结构多表/iframe 隔离) =====
// 取「原始表头」(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 getTableOwnerDocument(table) { return table.ownerDocument || document; }
function getTableOwnerWindow(table) { return getTableOwnerDocument(table).defaultView || window; }
function getTablePageUrl(table) {
try { return getTableOwnerWindow(table).location.href; }
catch (e) { return location.href; }
}
function getColumnDescriptors(table) {
const st = tableData.get(generateTableId(table));
return makeColumnDescriptorsFromState(origHeaders(table), (st && st.calcColumns) || []);
}
function readUnmodifiedHeaders(table) {
const ht = ElTableAdapter.getHeaderTable(table);
if (!ht) return [];
const trs = ht.querySelectorAll('thead tr');
const tr = trs.length ? trs[trs.length - 1] : null;
return tr ? Array.from(tr.children)
.filter(th => !/gutter/.test(th.className || '') && !th.hasAttribute('data-calc-col'))
.map(th => (th.textContent || '').trim()) : [];
}
function stableContextToken(table) {
const root = table.closest('.el-table');
let node = root && root.parentElement;
let depth = 0;
while (node && node !== getTableOwnerDocument(table).body && depth++ < 8) {
for (const attr of ['data-table-key', 'data-key', 'data-testid', 'aria-label', 'name', 'id']) {
const value = node.getAttribute && node.getAttribute(attr);
if (value && !/^(?:el-table|table|pane)?[_-]?\d+$/.test(value) && !/^__BVID__\d+$/.test(value)) {
return node.tagName.toLowerCase() + ':' + attr + ':' + value;
}
}
const vueKey = node.__vue__ && node.__vue__.$vnode && node.__vue__.$vnode.key;
if (vueKey !== undefined && vueKey !== null && String(vueKey) !== '') {
return node.tagName.toLowerCase() + ':vue-key:' + String(vueKey);
}
node = node.parentElement;
}
return '';
}
function getTableScopeKey(table) {
const state = tableData.get(generateTableId(table));
if (state && state.tableScopeKey) return state.tableScopeKey;
const root = table.closest('.el-table');
const explicit = [
root && root.getAttribute('data-table-key'), root && root.getAttribute('data-key'),
root && root.id, table.getAttribute('data-table-key'), table.id
].find(value => value && !/^el-table_\d+$/.test(value));
if (explicit) {
const scope = 'id:' + encodeURIComponent(explicit);
if (state) state.tableScopeKey = scope;
return scope;
}
const doc = getTableOwnerDocument(table);
const fingerprint = JSON.stringify(origHeaders(table));
const context = stableContextToken(table);
let ordinal = 0;
const bodies = doc.querySelectorAll('table.el-table__body');
for (const body of bodies) {
if (!isMainElTableBody(body)) continue;
if (body === table) break;
if (JSON.stringify(readUnmodifiedHeaders(body)) === fingerprint && stableContextToken(body) === context) ordinal++;
}
const scope = context ? 'context:' + encodeURIComponent(context) + ':' + ordinal : 'schema:' + ordinal;
if (state) state.tableScopeKey = scope;
return scope;
}
function makeScopedTableStoreKey(prefix, table, headers) {
return prefix + '_v2_' + canonicalPageUrl(getTablePageUrl(table)) + '::' +
encodeURIComponent(JSON.stringify(headers || [])) + '::' + getTableScopeKey(table);
}
function findLegacyScopedStoreValue(prefix, table, headers) {
const exactKey = prefix + '_' + makeCalcKey(getTablePageUrl(table), headers);
const exact = GM_getValue(exactKey, undefined);
if (exact !== undefined && exact !== null && exact !== '') return exact;
if (typeof GM_listValues === 'undefined') return undefined;
const keyPrefix = prefix + '_';
const suffix = '::' + headers.join('|');
const page = canonicalPageUrl(getTablePageUrl(table));
const key = GM_listValues().find(candidate => {
if (!candidate.startsWith(keyPrefix) || candidate.startsWith(prefix + '_v') || !candidate.endsWith(suffix)) return false;
const oldUrl = candidate.slice(keyPrefix.length, candidate.length - suffix.length);
return canonicalPageUrl(oldUrl) === page;
});
return key ? GM_getValue(key, undefined) : undefined;
}
// ===== 列宽 DOM 读写(仅服务计算列设宽与表总宽同步) =====
function setColStyleWidth(col, width) {
const normalized = normalizeColWidth(width);
if (!col || normalized === null) return;
col.style.width = normalized + 'px';
col._tableEnhancerWidthManaged = true;
}
function readColLogicalWidth(col) {
if (!col) return 0;
const inline = normalizeColWidth(parseFloat(col.style.width || ''));
if (inline !== null) return inline;
const attr = normalizeColWidth(col.getAttribute('width') || col.width);
if (attr !== null) return attr;
const rect = col.getBoundingClientRect && col.getBoundingClientRect();
return rect && rect.width ? Math.round(rect.width) : 0;
}
function getMainColTables(table) {
const root = table.closest('.el-table');
return {
root,
header: root && (root.querySelector('.el-table__header-wrapper > .el-table__header') || root.querySelector('.el-table__header')),
body: root && (root.querySelector('.el-table__body-wrapper > .el-table__body') || root.querySelector('.el-table__body')),
footer: root && (root.querySelector('.el-table__footer-wrapper > .el-table__footer') || root.querySelector('.el-table__footer'))
};
}
function dataCols(tbl) {
return tbl ? Array.from(tbl.querySelectorAll('colgroup col')).filter(col => !/gutter/.test(col.getAttribute('name') || '')) : [];
}
function findColByIndex(tbl, index) { return dataCols(tbl)[index] || null; }
function eachColByIndex(table, index, fn) {
const refs = getMainColTables(table);
if (!refs.root) return;
const primary = findColByIndex(refs.header, index) || findColByIndex(refs.body, index);
const columnName = primary && primary.getAttribute('name');
const found = new Set();
if (columnName) {
refs.root.querySelectorAll('colgroup col').forEach(col => {
if (col.getAttribute('name') === columnName) found.add(col);
});
} else {
[refs.header, refs.body, refs.footer].forEach(tbl => {
const col = findColByIndex(tbl, index);
if (col) found.add(col);
});
}
found.forEach(fn);
}
// el-table 重建 colgroup/数据行后,恢复隐藏列并重新同步表总宽
function restoreColumnLayout(table) {
if (!table.isConnected) return;
applyHiddenCols(table);
syncTableWidth(table);
}
// root 级布局守护:el-table 重建 colgroup/header 后恢复隐藏列与表总宽;事件均取 table.ownerDocument,兼容 iframe。
function setupLayoutGuard(table) {
const refs = getMainColTables(table);
if (!refs.root) return;
if (table._layoutGuard) return;
const oldGuard = refs.root._tableEnhancerLayoutGuard;
if (oldGuard && oldGuard.table !== table) teardownLayoutGuard(oldGuard.table);
const doc = getTableOwnerDocument(table);
const win = getTableOwnerWindow(table);
const guard = { table, root: refs.root, doc, win, retryTimers: [] };
table._layoutGuard = guard;
refs.root._tableEnhancerLayoutGuard = guard;
const scheduleRestore = delay => {
clearTimeout(table._layoutGuardTimer);
table._layoutGuardTimer = setTimeout(() => restoreColumnLayout(table), delay);
};
// 首帧立即恢复;短重试只兜底 Vue 首次布局
restoreColumnLayout(table);
guard.retryTimers = [80, 350, 1200].map(delay => setTimeout(() => restoreColumnLayout(table), delay));
guard.onResize = () => {
if (!table.isConnected) { teardownDetachedTable(table); return; }
scheduleRestore(120);
};
guard.onPageHide = event => { if (!event.persisted) teardownDetachedTable(table); };
guard.onPageShow = event => { if (event.persisted && table.isConnected) restoreColumnLayout(table); };
win.addEventListener('resize', guard.onResize);
win.addEventListener('pagehide', guard.onPageHide);
win.addEventListener('pageshow', guard.onPageShow);
const Observer = win.MutationObserver || MutationObserver;
guard.observer = new Observer(mutations => {
if (!table.isConnected) { teardownDetachedTable(table); return; }
const colTreeChanged = mutations.some(mutation => {
const nodes = [...mutation.addedNodes, ...mutation.removedNodes];
return nodes.some(node => node.nodeType === 1 &&
(node.tagName === 'COL' || node.tagName === 'COLGROUP' || node.tagName === 'TABLE' ||
(node.querySelector && node.querySelector('col, colgroup, table'))));
});
if (colTreeChanged) scheduleRestore(60);
});
guard.observer.observe(guard.root, { childList: true, subtree: true });
if (typeof win.ResizeObserver === 'function') {
guard.resizeObserver = new win.ResizeObserver(() => guard.onResize());
guard.resizeObserver.observe(guard.root);
}
}
function teardownLayoutGuard(table) {
const guard = table._layoutGuard;
clearTimeout(table._layoutGuardTimer);
delete table._layoutGuardTimer;
if (!guard) return;
(guard.retryTimers || []).forEach(id => clearTimeout(id));
guard.win.removeEventListener('resize', guard.onResize);
guard.win.removeEventListener('pagehide', guard.onPageHide);
guard.win.removeEventListener('pageshow', guard.onPageShow);
if (guard.observer) guard.observer.disconnect();
if (guard.resizeObserver) guard.resizeObserver.disconnect();
if (guard.root._tableEnhancerLayoutGuard === guard) delete guard.root._tableEnhancerLayoutGuard;
delete table._layoutGuard;
}
function setManagedTableWidth(tbl, width) {
const normalized = Math.round(Number(width));
if (!tbl || !Number.isFinite(normalized) || normalized <= 0 || normalized > 1000000) return;
if (!Object.prototype.hasOwnProperty.call(tbl, '_tableEnhancerOriginalWidth')) tbl._tableEnhancerOriginalWidth = tbl.style.width || '';
tbl._tableEnhancerWidthManaged = true;
tbl.style.width = normalized + 'px';
}
function restoreManagedTableWidths(table) {
const root = table.closest('.el-table');
if (!root) return;
root.querySelectorAll('table, .el-table__fixed, .el-table__fixed-right').forEach(element => {
if (!element._tableEnhancerWidthManaged) return;
element.style.width = element._tableEnhancerOriginalWidth || '';
delete element._tableEnhancerOriginalWidth;
delete element._tableEnhancerWidthManaged;
});
}
function sumLogicalCols(tbl, includeGutter) {
if (!tbl) return 0;
let total = 0;
tbl.querySelectorAll('colgroup col').forEach(col => {
const gutter = /gutter/.test(col.getAttribute('name') || '');
if ((!includeGutter && gutter) || col.style.display === 'none') return;
total += readColLogicalWidth(col);
});
return total;
}
function shouldManageTableWidth(table) {
const st = tableData.get(generateTableId(table));
return Boolean(st && (
(st.calcColumns && st.calcColumns.length) ||
(st.hiddenCols && st.hiddenCols.length)
));
}
// 只用 inline/width attribute 的逻辑宽求总宽,避免把旧 table.style.width 分配出的额外空间再次算回。
function syncTableWidth(table) {
const refs = getMainColTables(table);
if (!refs.root) return;
if (!shouldManageTableWidth(table)) { restoreManagedTableWidths(table); return; }
const headerData = sumLogicalCols(refs.header, false);
const bodyData = sumLogicalCols(refs.body, false);
const dataWidth = Math.max(headerData, bodyData);
if (dataWidth > 0) {
const headerWithGutter = sumLogicalCols(refs.header, true);
setManagedTableWidth(refs.header, Math.max(dataWidth, headerWithGutter));
setManagedTableWidth(refs.body, dataWidth);
if (refs.footer) setManagedTableWidth(refs.footer, dataWidth);
} else restoreManagedTableWidths(table);
const auxiliary = refs.root.querySelectorAll('.el-table__fixed table, .el-table__fixed-right table');
auxiliary.forEach(tbl => {
const width = sumLogicalCols(tbl, true);
if (width > 0) setManagedTableWidth(tbl, width);
});
refs.root.querySelectorAll('.el-table__fixed, .el-table__fixed-right').forEach(wrapper => {
const widths = Array.from(wrapper.querySelectorAll('table')).map(tbl => sumLogicalCols(tbl, true));
const width = widths.length ? Math.max(...widths) : 0;
if (width > 0) setManagedTableWidth(wrapper, width);
});
}
// 隐藏文档中所有控制行(不清除筛选/排序状态)
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 => {
// 重新处理前先移除旧计算列,避免把注入列误记为原始表头或重复追加控制格。
ElTableAdapter.removeAllCalcCols(table);
// 移除隐藏标记以便 processDocument 重新添加
delete table.dataset.controlsHidden;
// 标记为未处理,processDocument 会重新处理并添加控制行
table.classList.remove('table-processed');
});
// 触发一次处理以添加控制行(小延迟以等待 DOM 更新)
setTimeout(() => processDocument(doc), 50);
}
// 处理文档中的所有表格
function processDocument(doc) {
// KeepAlive/tab 缓存可能先 detach、稍后再插回同一张已处理表;恢复挂在 DOM 上的状态与监听。
doc.querySelectorAll('table.table-processed.el-table__body').forEach(table => {
if (!isMainElTableBody(table) || table._layoutGuard) return;
const tableId = table.dataset.sortTableId;
if (table._tableEnhancerDetachedState) {
tableData.set(tableId, table._tableEnhancerDetachedState);
delete table._tableEnhancerDetachedState;
setupTableRowsObserver(table);
setupLayoutGuard(table);
} else if (!tableData.has(tableId)) {
// 无可恢复状态的半处理 DOM:清掉注入物,再走正常初始化。
teardownLayoutGuard(table);
restoreManagedTableWidths(table);
ElTableAdapter.removeAllCalcCols(table);
const controlRow = table.querySelector('.table-control-row');
if (controlRow) controlRow.remove();
table.classList.remove('table-processed');
}
});
// 跳过已处理或显式隐藏控制行的表格
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);
}
});
}
function isMainElTableBody(table) {
if (!table || !(table.className || '').includes('el-table__body')) return false;
const root = table.closest && table.closest('.el-table');
if (!root) return false;
const mainBody = root.querySelector('.el-table__body-wrapper > table.el-table__body');
return mainBody ? mainBody === table : Boolean(table.parentElement && table.parentElement.classList.contains('el-table__body-wrapper'));
}
// 只处理主 body table;fixed-left/right 克隆由主表列宽控制器同步,不独立注入功能。
function shouldIgnoreTable(table) {
return isMainElTableBody(table);
}
// 检查表格是否有数据行
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');
// 同一个 body table 重新启用控制行时,先清理上次注入的计算列,再按稳定状态重建。
const existingState = tableData.get(generateTableId(table));
if (existingState && table.querySelector('[data-calc-col]')) ElTableAdapter.removeAllCalcCols(table);
// 获取表格结构信息
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 || !_st.originalHeaders.length)) _st.originalHeaders = readUnmodifiedHeaders(table);
// 添加控制行
addControlRow(table, structure);
// 添加行号指示器
updateRowNumbers(table);
// 监听行变化:element-ui 重渲染行后自动重画行号,保证行号稳定常显
setupTableRowsObserver(table);
// 先加载持久化状态,再应用计算列/隐藏列
loadCalcColumnsIfPersisted(table);
loadHiddenCols(table);
applyCalcColumns(table);
applyHiddenCols(table);
// 布局守护:el-table 重建 colgroup 后恢复隐藏列与表总宽
setupLayoutGuard(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: [] // 已隐藏列 ID(持久化,旧列名数组会自动迁移)
});
}
}
// 生成表格唯一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);
}
// 数据刷新稳定后统一恢复隐藏列与表总宽;root 守护会继续覆盖后续 colgroup 替换。
restoreColumnLayout(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)));
}
}
// 恢复所有原始列显示(清除隐藏效果)
getColumnDescriptors(table).filter(column => column.kind === 'original')
.forEach(column => setHidden(table, column.index, false));
// 停止布局守护并交还当前表格的总宽控制。
teardownLayoutGuard(table);
restoreManagedTableWidths(table);
// 移除计算列(表头 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)', 'regexReplace(s,pat,repl,flags?)', 'regexMatch(s,pat,flags?)',
'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); // 再重新应用显示
restoreColumnLayout(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(备注, '急')
• regexReplace(s, 正则, 替换串, 标志?) —— 正则替换,默认全部替换。regexReplace(编号, '\\D', '') 只留数字;regexReplace(电话, '(\\d{3})\\d{4}(\\d{4})', '$1****$2') 打码。替换串支持 $1 捕获组
• regexMatch(s, 正则, 标志?) —— 是否匹配正则,返回 是/否。regexMatch(手机, '^1[3-9]\\d{9}$') 校验手机号。标志 'i' 忽略大小写;regexReplace 传 'i' 时只替换第一处,'gi' 全部替换
• 正则中的反斜杠直接单写:'\\d+' 匹配数字、'\\s' 匹配空白,无需双写
五、日期函数
• 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': '不包含',
'regexMatch': '符合正则',
'isEmpty': '为空',
'isNotEmpty': '不为空',
'numGt': '大于', 'numGte': '大于等于', 'numLt': '小于', 'numLte': '小于等于',
'numEq': '等于', 'numNe': '不等于', 'numBetween': '区间',
'dateGt': '晚于', 'dateGte': '晚于≥', 'dateLt': '早于', 'dateLte': '早于≤',
'dateEq': '等于', 'dateBetween': '区间',
'lenEq': '长度=', 'lenGt': '长度>', 'lenLt': '长度<'
};
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: 'regexMatch', 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: '不为空' },
{ v: 'lenEq', l: '文本长度等于' }, { v: 'lenGt', l: '文本长度大于' }, { v: 'lenLt', 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 if (alg === 'regexMatch') input.placeholder = '输入正则表达式,如 ^[A-Z]\\d+$';
else if (alg === 'lenEq' || alg === 'lenGt' || alg === 'lenLt') input.placeholder = '输入长度(数字)';
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();
// 正则合法性预检:非法正则不应用(否则整列都会被筛没)
for (const [label, alg, text] of [['条件一', cond1Alg, text1], ['条件二', cond2Alg, text2]]) {
if (alg === 'regexMatch' && text) {
try { new RegExp(text); }
catch (e) { alert(label + '的正则表达式无效: ' + (e && e.message ? e.message : e)); return; }
}
}
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 'lenEq':
return (cellText || '').length === parseInt(filterText, 10);
case 'lenGt':
return (cellText || '').length > parseInt(filterText, 10);
case 'lenLt':
return (cellText || '').length < parseInt(filterText, 10);
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 'regexMatch':
// 正则匹配:对原文匹配(不转小写,大小写由表达式自行控制);非法正则视为不匹配
try { return new RegExp(filterText).test(cellText || ''); }
catch (e) { return false; }
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', 'lenEq', 'lenGt', 'lenLt']);
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) {
let regex;
if (t.algorithm === 'regexMatch') {
// 正则原文直接作为模式(不转义);外层包一组,$1 即整体匹配
try { regex = new RegExp('(' + t.text + ')', 'g'); }
catch (e) { continue; }
} else {
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')
};
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);
}
// 重渲染后统一恢复隐藏列与表总宽。
restoreColumnLayout(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) {
if (!iframe._tableEnhancerLoadBound) {
iframe.addEventListener('load', () => processIframe(iframe));
iframe._tableEnhancerLoadBound = true;
}
try {
if (iframe.contentDocument) {
processIframe(iframe);
}
} catch (error) {
console.log('无法访问iframe内容:', error.message);
}
}
function cleanupRemovedTableControllers(mutations) {
mutations.forEach(mutation => mutation.removedNodes.forEach(node => {
if (node.nodeType !== 1) return;
const removedTables = [];
if (node.matches && node.matches('table.el-table__body')) removedTables.push(node);
if (node.querySelectorAll) removedTables.push(...node.querySelectorAll('table.el-table__body'));
removedTables.forEach(table => {
const guard = table._layoutGuard;
if (!guard) return;
if (!table.isConnected) teardownDetachedTable(table);
else if (table.closest('.el-table') !== guard.root) {
teardownLayoutGuard(table);
setupLayoutGuard(table);
}
});
}));
}
function teardownDetachedTable(table) {
teardownLayoutGuard(table);
if (table._rowNumObserver) { table._rowNumObserver.disconnect(); delete table._rowNumObserver; }
['_rowNumTimer', '_refreshTimer', '_calcRecomputing'].forEach(name => {
clearTimeout(table[name]);
delete table[name];
});
const tableId = table.dataset && table.dataset.sortTableId;
if (tableId && tableData.has(tableId)) {
table._tableEnhancerDetachedState = tableData.get(tableId);
tableData.delete(tableId);
}
}
// 处理iframe内容
function processIframe(iframe) {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const iframeUrl = iframe.src || iframeDoc.URL;
if (!iframeUrl.includes('jlcerp.com') || !iframeDoc.body) {
return;
}
if (iframe._tableEnhancerObservedDocument === iframeDoc) return;
if (iframe._tableEnhancerDocumentObserver) iframe._tableEnhancerDocumentObserver.disconnect();
iframe._tableEnhancerObservedDocument = iframeDoc;
processDocument(iframeDoc);
const iframeObserver = new MutationObserver((mutations) => {
cleanupRemovedTableControllers(mutations);
processDocument(iframeDoc);
});
iframeObserver.observe(iframeDoc.body, {
childList: true,
subtree: true
});
iframe._tableEnhancerDocumentObserver = iframeObserver;
} catch (error) {
console.log('处理iframe时出错:', error.message);
}
}
// 设置MutationObserver监听DOM变化
function setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
cleanupRemovedTableControllers(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);
}
})();