// ==UserScript==
// @name 腾讯文档-隐藏列内容查看器
// @namespace https://docs.qq.com/
// @version 0.4.48
// @description 在腾讯文档表格页显示列实际内容,并在原网页展开/还原这些列
// @author codex
// @match https://docs.qq.com/sheet/*
// @grant GM_setClipboard
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const PANEL_ID = 'tm-hidden-col-panel';
const STYLE_ID = 'tm-hidden-col-style';
const LAUNCHER_ID = 'tm-hidden-col-launcher';
const IMAGE_VIEWER_ID = 'tm-hidden-col-image-viewer';
const DEFAULT_TARGET_COLS = [25, 26, 27, 28, 29, 30]; // Z~AE (0-based)
const PREVIEW_IMAGE_WIDTH = 320;
const QUICK_SCAN_MAX_ROWS = 240;
const QUICK_SCAN_MAX_CELLS = 12000;
const DEEP_SCAN_MAX_ROWS = 1800;
const DEEP_SCAN_MAX_CELLS = 120000;
const SELECTION_RENDER_DEBOUNCE_MS = 90;
const COL_HEADER_RETRY_MAX_ATTEMPTS = 10;
const COL_HEADER_RETRY_INTERVAL_MS = 180;
const COL_ORDER_STORAGE_PREFIX = '__TM_HIDDEN_COL_ORDER__';
const PAGE_WIN = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
const DRIVE_URL_CACHE = new Map();
let SHEET_WEBPACK_REQUIRE = null;
let OPS_FETCH_CHAIN = Promise.resolve();
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getSheetWebpackRequire() {
if (typeof SHEET_WEBPACK_REQUIRE === 'function') return SHEET_WEBPACK_REQUIRE;
const chunk = PAGE_WIN.webpackChunk_tencent_sheet;
if (!Array.isArray(chunk) || typeof chunk.push !== 'function') return null;
let req = null;
try {
const marker = `tm_probe_${Date.now()}_${Math.random()}`;
chunk.push([[marker], {}, (r) => { req = r; }]);
} catch (_) {
return null;
}
if (typeof req !== 'function') return null;
SHEET_WEBPACK_REQUIRE = req;
return SHEET_WEBPACK_REQUIRE;
}
function runOpsFetchSerial(task) {
const run = OPS_FETCH_CHAIN.then(task, task);
OPS_FETCH_CHAIN = run.catch(() => { });
return run;
}
async function withTimeout(task, timeoutMs, onTimeoutValue) {
const ms = Math.max(1, Number(timeoutMs) || 1);
let timer = null;
try {
return await Promise.race([
Promise.resolve().then(task),
new Promise((resolve) => {
timer = setTimeout(() => resolve(onTimeoutValue), ms);
})
]);
} finally {
if (timer) clearTimeout(timer);
}
}
async function waitForRevisionRuntime(app, timeoutMs = 4000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const fcs = app?.formulaCalcService;
let calc = null;
try {
calc = fcs?.cellHistoryCalculator;
} catch (_) {
calc = null;
}
if (
fcs &&
calc &&
typeof fcs.startServiceCheck === 'function' &&
typeof calc._cTC === 'function' &&
typeof calc._cTd === 'function' &&
typeof calc.clearCache === 'function'
) {
return { fcs, calc };
}
await sleep(80);
}
return null;
}
function getColumnOrderStorageKey(app) {
const padId = String(PAGE_WIN?.docInfo?.padInfo?.padId || '').trim();
let sheetId = '';
try {
sheetId = String(app?.view?.getActiveCell?.()?.sheetId || '').trim();
} catch (_) { }
if (!sheetId) {
try {
const u = new URL(String(location.href || ''));
sheetId = String(u.searchParams.get('tab') || '').trim();
} catch (_) { }
}
return `${COL_ORDER_STORAGE_PREFIX}:${padId || location.pathname}:${sheetId || 'default'}`;
}
function loadColumnOrder(key) {
if (!key) return [];
try {
const raw = localStorage.getItem(key);
if (!raw) return [];
const arr = JSON.parse(raw);
if (!Array.isArray(arr)) return [];
const out = [];
const seen = new Set();
for (const x of arr) {
const n = Number(x);
if (!Number.isInteger(n) || seen.has(n)) continue;
seen.add(n);
out.push(n);
}
return out;
} catch (_) {
return [];
}
}
function saveColumnOrder(key, cols) {
if (!key) return;
try {
const out = [];
const seen = new Set();
for (const x of Array.from(cols || [])) {
const n = Number(x);
if (!Number.isInteger(n) || n < 0 || seen.has(n)) continue;
seen.add(n);
out.push(n);
}
localStorage.setItem(key, JSON.stringify(out));
} catch (_) { }
}
function waitForSpreadsheetApp(timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const started = Date.now();
const timer = setInterval(() => {
const app = PAGE_WIN.SpreadsheetApp;
const sheet = app?.workbook?.activeSheet;
if (sheet && typeof sheet.getCellDataAtPosition === 'function') {
clearInterval(timer);
resolve({ app, sheet });
return;
}
if (Date.now() - started > timeoutMs) {
clearInterval(timer);
reject(new Error('SpreadsheetApp not ready'));
}
}, 500);
});
}
function colToLetter(col0) {
let n = col0 + 1;
let s = '';
while (n > 0) {
const m = (n - 1) % 26;
s = String.fromCharCode(65 + m) + s;
n = Math.floor((n - 1) / 26);
}
return s;
}
function textOfCell(cell) {
if (!cell) return '';
const fv = cell.formattedValue;
if (fv && typeof fv === 'object' && fv.value != null) return String(fv.value);
if (typeof fv === 'string' || typeof fv === 'number' || typeof fv === 'boolean') return String(fv);
const v = cell.value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if (v && typeof v === 'object') {
if (v.value != null) return String(v.value);
if (v.text != null) return String(v.text);
if (v.formulaResult != null) return String(v.formulaResult);
}
return '';
}
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('\"', '"')
.replaceAll("'", ''');
}
function getTokXsrf() {
const m = document.cookie.match(/(?:^|;\s*)TOK=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
function parseDriveObjKey(driveUrl) {
const m = String(driveUrl || '').match(/^drive:\/\/([^/]+)\//);
return m ? m[1] : '';
}
async function resolveDriveUrl(driveUrl) {
const key = String(driveUrl || '').trim();
if (!key) return '';
if (DRIVE_URL_CACHE.has(key)) return DRIVE_URL_CACHE.get(key);
const objKey = parseDriveObjKey(key);
const fileId = PAGE_WIN?.docInfo?.padInfo?.padId || '';
const xsrf = getTokXsrf();
if (!objKey || !fileId || !xsrf) {
DRIVE_URL_CACHE.set(key, key);
return key;
}
try {
const resp = await fetch(`/v2/drive/fileInfo?xsrf=${encodeURIComponent(xsrf)}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ file_id: fileId, xsrf, obj_key: objKey })
});
const json = await resp.json();
const thumb = json?.result?.fileMeta?.thumbnailUrl || '';
const raw = thumb ? thumb.replace(/&imageMogr2[\s\S]*$/, '') : '';
const finalUrl = raw || key;
DRIVE_URL_CACHE.set(key, finalUrl);
return finalUrl;
} catch (_) {
DRIVE_URL_CACHE.set(key, key);
return key;
}
}
async function resolveUrls(urls) {
if (!Array.isArray(urls) || !urls.length) return [];
const out = [];
for (const u of urls) {
if (!u) continue;
if (/^drive:\/\//i.test(u)) {
out.push(await resolveDriveUrl(u));
} else {
out.push(u);
}
}
return [...new Set(out.filter(Boolean))];
}
function isPreviewableImageUrl(url) {
if (typeof url !== 'string') return false;
const u = url.trim();
if (!u) return false;
if (/^https?:\/\/docimg\d*\.docs\.qq\.com\/image\//i.test(u)) return true;
if (/^https?:\/\/drive\.docs\.qq\.com\//i.test(u)) return true;
if (/\.(png|jpe?g|gif|webp|bmp|svg)([?#].*)?$/i.test(u)) return true;
return false;
}
function valueHtml(item) {
if (Array.isArray(item.resolvedUrls) && item.resolvedUrls.length) {
const imageUrls = item.resolvedUrls.filter(isPreviewableImageUrl);
if (imageUrls.length) {
return `
${imageUrls
.map((u) => {
const s = escapeHtml(u);
return `
`;
})
.join('')}
`;
}
return item.resolvedUrls
.map((u) => `${escapeHtml(u)}`)
.join('
');
}
return escapeHtml(item.value || '');
}
function actionTypeLabel(actionType) {
const n = Number(actionType);
if (n === 1) return '新增';
if (n === 2) return '删除';
if (n === 3) return '修改';
return Number.isFinite(n) ? `类型${n}` : '未知';
}
function formatLocalTime(ts) {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) return '';
try {
return new Date(n).toLocaleString('zh-CN', { hour12: false });
} catch (_) {
return '';
}
}
function formatFullDateTime(ts) {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) return '';
const d = new Date(n);
const pad2 = (x) => String(x).padStart(2, '0');
const y = d.getFullYear();
const m = pad2(d.getMonth() + 1);
const day = pad2(d.getDate());
const h = pad2(d.getHours());
const min = pad2(d.getMinutes());
const sec = pad2(d.getSeconds());
return `${y}/${m}/${day} ${h}:${min}:${sec}`;
}
function cellDeltaText(delta) {
if (delta == null) return '';
if (typeof delta === 'string' || typeof delta === 'number' || typeof delta === 'boolean') return String(delta);
if (typeof delta !== 'object') return '';
const fv = delta.formattedValue;
if (fv && typeof fv === 'object' && fv.value != null) return String(fv.value);
if (typeof fv === 'string' || typeof fv === 'number' || typeof fv === 'boolean') return String(fv);
const v = delta.value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if (v && typeof v === 'object') {
if (v.value != null) return String(v.value);
if (v.text != null) return String(v.text);
if (v.formulaResult != null) return String(v.formulaResult);
}
return '';
}
function opDetailText(item) {
const before = formatDetailValue(item?.beforeValue);
const after = formatDetailValue(item?.afterValue);
if (before || after) return `${before || '(空)'} -> ${after || '(空)'}`;
return '';
}
function tryFormatExcelSerialDate(text) {
const s = String(text || '').trim();
if (!/^-?\d+(?:\.\d+)?$/.test(s)) return '';
const n = Number(s);
// 仅转换疑似 Excel 序列日期,避免把价格/数量误判成日期。
if (!Number.isFinite(n) || n < 20000 || n > 80000) return '';
const dayMs = 24 * 60 * 60 * 1000;
const excelEpochMs = Date.UTC(1899, 11, 30);
const ts = excelEpochMs + Math.round(n * dayMs);
const dateText = formatLocalTime(ts);
return dateText || '';
}
function formatDetailValue(v) {
const raw = String(v == null ? '' : v).trim();
if (!raw) return '';
const dateText = tryFormatExcelSerialDate(raw);
return dateText || raw;
}
function isNoiseDetailValue(v) {
const text = String(v || '').trim();
if (!text) return true;
return (
/^ClearFormat$/i.test(text) ||
/^ConvertCellTextToNumber$/i.test(text) ||
/^SetCellStyle$/i.test(text) ||
/^SetCellFormat$/i.test(text)
);
}
function usefulDetailText(item) {
const beforeRaw = String(item?.beforeValue || '').trim();
const afterRaw = String(item?.afterValue || '').trim();
const before = formatDetailValue(beforeRaw);
const after = formatDetailValue(afterRaw);
const candidates = [];
if (after && !isNoiseDetailValue(afterRaw)) candidates.push(after);
if (before && !isNoiseDetailValue(beforeRaw) && before !== after) candidates.push(before);
if (!candidates.length) return '';
return [...new Set(candidates)].join(' -> ');
}
function displayDetailText(item) {
return opDetailText(item);
}
function normalizeDetailText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function dedupeOperationRecords(records) {
const out = [];
const seen = new Set();
for (const item of records || []) {
const detail = normalizeDetailText(displayDetailText(item) || opDetailText(item) || '(空)');
const key = [
Number.isFinite(item?.colIndex0) ? item.colIndex0 : '-',
Number.isFinite(item?.actionType) ? item.actionType : '-',
String(item?.authorId || item?.authorName || '-'),
detail
].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push(item);
}
return out;
}
function shouldHideNoiseDetailRecord(item) {
const rawText = [
String(item?.beforeValue || ''),
String(item?.afterValue || ''),
opDetailText(item)
].join(' ');
return /ClearFormat|ConvertCellTextToNumber/i.test(rawText);
}
function mutationActionType(typeName) {
const t = String(typeName || '').toLowerCase();
if (/insert|add|create/.test(t)) return 1;
if (/delete|remove|clear/.test(t)) return 2;
return 3;
}
function mutationTypeName(typeMap, type) {
const n = Number(type);
if (!Number.isFinite(n)) return '类型?';
if (typeMap && typeof typeMap === 'object') {
for (const [k, v] of Object.entries(typeMap)) {
if (Number(v) === n) return k;
}
}
return `类型${n}`;
}
function isRowInRange(row0, startRow, endRow) {
const sr = Number(startRow);
const er = Number(endRow);
if (!Number.isFinite(sr) || !Number.isFinite(er)) return false;
if (er > sr) return row0 >= sr && row0 < er;
return row0 === sr;
}
function parseMutationRowCells(mutation, row0, currentSheetId) {
const cells = [];
if (!mutation || typeof mutation !== 'object') return cells;
const expectedSheetId = String(currentSheetId || '').trim();
const g = mutation.gridRangeData || {};
const mutationSheetId = String(
g?.sheetId != null
? g.sheetId
: (typeof mutation.getTargetSheetId === 'function' ? mutation.getTargetSheetId() : '')
).trim();
if (expectedSheetId && mutationSheetId && mutationSheetId !== expectedSheetId) return cells;
if (Array.isArray(mutation.cellAtPositions)) {
for (const at of mutation.cellAtPositions) {
const r = Number(at?.rowIndex);
if (r !== row0) continue;
const c = Number(at?.colIndex);
cells.push({
colIndex0: Number.isFinite(c) ? c : -1,
value: textOfCell(at?.cell)
});
}
}
if (cells.length) return cells;
const sr = Number(g.startRowIndex);
const sc = Number(g.startColIndex);
if (
mutation.cell &&
Number.isFinite(sr) &&
isRowInRange(row0, g.startRowIndex, g.endRowIndex)
) {
cells.push({
colIndex0: Number.isFinite(sc) ? sc : -1,
value: textOfCell(mutation.cell)
});
}
return cells;
}
function fetchCurrentRowOperationRecordsFromLocalQueue(app, row0, sheetId) {
try {
const queue = app?.formulaCalcService?._BLZ?.mutationData?._BBk?.queue;
if (!Array.isArray(queue) || !queue.length) {
return { ok: true, records: [], source: 'localQueue', warning: '未命中本地变更缓存' };
}
const out = [];
const seen = new Set();
for (const item of queue) {
const startTime = Number(item?.startTime || 0);
const mutations = Array.isArray(item?.mutations) ? item.mutations : [];
for (const mut of mutations) {
const rowCells = parseMutationRowCells(mut, row0, sheetId);
if (!rowCells.length) continue;
for (const rc of rowCells) {
const colIndex0 = Number.isFinite(rc?.colIndex0) ? rc.colIndex0 : -1;
const detail = String(rc?.value || '').trim();
const authorId = String(mut?.cell?.author || item?.author || '');
const key = [row0, colIndex0, startTime, Number(mut?.type || 0), authorId, detail].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push({
rowIndex0: row0,
colIndex0,
modifiedDateTime: startTime,
actionType: 3,
authorId,
authorName: authorId || '-',
beforeValue: '',
afterValue: detail || '本地变更'
});
}
}
}
out.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
if (!out.length) {
return { ok: true, records: [], source: 'localQueue', warning: '未命中本地变更缓存' };
}
return { ok: true, records: out, exactCount: out.length, totalCount: out.length, source: 'localQueue' };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
async function fetchCurrentRowOperationRecordsByChanges({
requireFn, axios, domain, padId, xsrf, row0, latestRev, sheetId
}) {
try {
const revUtils = requireFn(473546);
if (!revUtils?.tq || !revUtils?.oB) {
return { ok: false, error: '变更流解析器不可用' };
}
const mutationMap = requireFn(633935)?.D || {};
let cursor = Number(latestRev);
if (!Number.isFinite(cursor) || cursor <= 0) {
try {
const { data } = await axios.get(`//${domain}/dop-api/get/history`, {
params: { padId, page: 1, _r: Math.floor(Math.random() * 10000), xsrf }
});
cursor = Number(data?.history?.items?.[0]?.i || 0);
} catch (_) {
cursor = 0;
}
}
if (!Number.isFinite(cursor) || cursor <= 0) {
return { ok: false, error: '变更流无可用修订号' };
}
const out = [];
const seen = new Set();
const windowSize = 50;
const maxWindows = 8;
const maxRecords = 120;
const maxDurationMs = 12000;
const startedAt = Date.now();
let to = cursor;
for (
let w = 0;
w < maxWindows &&
to > 0 &&
out.length < maxRecords &&
(Date.now() - startedAt) < maxDurationMs;
w += 1
) {
const from = Math.max(1, to - windowSize + 1);
let result = null;
for (let attempt = 0; attempt < 2 && !result; attempt += 1) {
try {
const { data } = await axios.get(`//${domain}/dop-api/get/changes`, {
params: { from, to, xsrf, padId, needs_author_info: true }
});
result = data?.result || null;
} catch (_) {
if (attempt < 1) await sleep(120);
}
}
const authors = result?.authors || {};
const changes = Array.isArray(result?.changes) ? result.changes : [];
for (const it of changes) {
const d = it?.data || {};
if (!d?.changeset) continue;
let mutations = [];
try {
const rev = revUtils.tq(String(d.changeset || ''));
mutations = revUtils.oB(rev) || [];
} catch (_) {
continue;
}
for (const mut of mutations) {
const rowCells = parseMutationRowCells(mut, row0, sheetId);
if (!rowCells.length) continue;
const typeName = mutationTypeName(mutationMap, mut?.type);
const authorId = String(d?.author || '');
const authorName = String(
authors?.[authorId]?.Name ||
authors?.[authorId]?.name ||
authorId ||
'-'
);
const byCol = new Map();
for (const c of rowCells) {
const key = Number.isFinite(c?.colIndex0) ? c.colIndex0 : -1;
if (!byCol.has(key)) byCol.set(key, []);
byCol.get(key).push(String(c?.value || '').trim());
}
for (const [colIndex0, values] of byCol.entries()) {
const uniqValues = [...new Set(values.filter((x) => x !== ''))];
const detail = uniqValues.length ? uniqValues.join(' | ') : typeName;
const key = [
row0, colIndex0, Number(d?.time || 0), Number(mut?.type || 0), authorId, detail
].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push({
rowIndex0: row0,
colIndex0,
modifiedDateTime: Number(d?.time || 0),
actionType: mutationActionType(typeName),
authorId,
authorName,
beforeValue: '',
afterValue: detail
});
}
}
}
if (from <= 1) break;
to = from - 1;
}
out.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
return {
ok: true,
records: out,
exactCount: out.length,
totalCount: out.length,
source: 'changes'
};
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
function buildOpsUnavailableResult(reason) {
return {
ok: true,
records: [],
exactCount: 0,
totalCount: 0,
source: 'unavailable',
warning: String(reason || '修订接口暂不可用')
};
}
function uniqueValues(values) {
const out = [];
const seen = new Set();
for (const v of values || []) {
const key = `${typeof v}:${String(v)}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(v);
}
return out;
}
function buildSheetIdCandidates(app, sheet) {
const workbook = app?.workbook;
const raw = [
workbook?.activeSheetId,
workbook?.activeSheet?.sheetId,
workbook?.activeSheet?.sheetIdKey,
sheet?.sheetId,
sheet?.sheetIdKey,
sheet?.model?.sheetId,
sheet?.model?.sheetIdKey,
app?.view?.getActiveCell?.()?.sheetId
];
const expanded = [];
for (const v of raw) {
if (v == null || v === '') continue;
expanded.push(v);
const n = Number(v);
if (Number.isFinite(n)) expanded.push(n);
const s = String(v).trim();
if (s) expanded.push(s);
}
return uniqueValues(expanded);
}
function buildFilterArgVariants(baseArgs, row0, colCount, sheetIdCandidates) {
const rowRanges = [
[row0, row0 + 1], // Tencent 常见区间是 [start, end)
[row0, row0]
];
const colRanges = [
[0, Math.max(0, colCount)],
[0, Math.max(0, colCount - 1)]
];
const out = [];
for (const sid of sheetIdCandidates) {
for (const [sr, er] of rowRanges) {
for (const [sc, ec] of colRanges) {
out.push({
...baseArgs,
filterRangeSheetId: sid,
filterRangeStartRowIndex: sr,
filterRangeStartColIndex: sc,
filterRangeEndRowIndex: er,
filterRangeEndColIndex: ec
});
}
}
}
return out;
}
function isOpsUnavailableError(errorText) {
const t = String(errorText || '');
if (!t) return false;
return (
/_cTC\(10020\)/.test(t) ||
/_cTd\(10040\)/.test(t) ||
/修订服务未就绪/.test(t) ||
/历史接口不可用/.test(t)
);
}
async function fetchCurrentRowOperationRecords(app, sheet, row0) {
try {
const requireFn = getSheetWebpackRequire();
const runtime = await waitForRevisionRuntime(app, 4500);
const fcs = runtime?.fcs;
const calc = runtime?.calc;
if (typeof requireFn !== 'function' || !fcs || !calc) {
return { ok: false, error: '修订服务未就绪' };
}
const axiosMod = requireFn(430327);
const axios = axiosMod?.default || axiosMod;
const cfgMod = requireFn(841569);
const globalCfgMod = requireFn(535512);
const xsrfMod = requireFn(281194);
if (!axios?.get) return { ok: false, error: '历史接口不可用' };
const domain = cfgMod?.Z?.getConfig?.('runtime.domainName') || location.host;
const padId = globalCfgMod?.q?.getConfig?.('globalPadId') || PAGE_WIN?.docInfo?.padInfo?.padId || '';
const xsrf = xsrfMod?.f?.() || getTokXsrf();
if (!padId || !xsrf) return { ok: false, error: '缺少文档身份信息' };
let latestRev = null;
try {
const { data } = await axios.get(`//${domain}/dop-api/get/history`, {
params: { padId, page: 1, _r: Math.floor(Math.random() * 10000), xsrf }
});
const rev = Number(data?.history?.items?.[0]?.i);
if (Number.isFinite(rev) && rev > 0) latestRev = rev;
} catch (_) { }
const workbook = app?.workbook;
const sheetIdCandidates = buildSheetIdCandidates(app, sheet);
const sheetId = sheetIdCandidates[0];
const colCount = sheet.getColCount?.() || 1;
if (sheetId == null || sheetId === '') return { ok: false, error: '工作表上下文不可用' };
const tryChangesFallback = async () => fetchCurrentRowOperationRecordsByChanges({
requireFn, axios, domain, padId, xsrf, row0, latestRev, sheetId
});
const tryLocalQueueFallback = async () => fetchCurrentRowOperationRecordsFromLocalQueue(app, row0, sheetId);
let preferredFilterShape = null;
const createBaseFilterArgs = () => ({
// 线上实测:使用 0 作为起始时间更稳定,避免 _cTC(10020)
recordStartTime: 0,
recordEndTime: Date.now(),
filterLimit: 300
});
const runFilterQuery = async () => {
const baseFilterArgs = createBaseFilterArgs();
const argsVariants = [];
const seenVariant = new Set();
const pushArgs = (args) => {
if (!args || typeof args !== 'object') return;
const key = [
String(args.filterRangeSheetId),
args.filterRangeStartRowIndex,
args.filterRangeEndRowIndex,
args.filterRangeStartColIndex,
args.filterRangeEndColIndex
].join('|');
if (seenVariant.has(key)) return;
seenVariant.add(key);
argsVariants.push(args);
};
if (preferredFilterShape) {
pushArgs({ ...baseFilterArgs, ...preferredFilterShape });
}
for (const args of buildFilterArgVariants(baseFilterArgs, row0, colCount, sheetIdCandidates)) {
pushArgs(args);
}
let lastError = null;
let lastArgs = null;
for (const args of argsVariants) {
lastArgs = args;
const step1 = await calc._cTC(args);
if (!step1?.isError) {
preferredFilterShape = {
filterRangeSheetId: args.filterRangeSheetId,
filterRangeStartRowIndex: args.filterRangeStartRowIndex,
filterRangeStartColIndex: args.filterRangeStartColIndex,
filterRangeEndRowIndex: args.filterRangeEndRowIndex,
filterRangeEndColIndex: args.filterRangeEndColIndex
};
return { ok: true, step1, args };
}
lastError = step1;
const code = Number(step1?.code);
// 参数类错误可继续尝试其它组合;其它错误先保留,仍允许后续组合兜底。
if (code !== 10020 && code !== 10040) {
continue;
}
}
return { ok: false, error: lastError || { isError: true, code: 'UNKNOWN' }, args: lastArgs };
};
const resetAndPrime = async () => {
await calc.clearCache();
if (typeof calc._cTD === 'function') {
const primeRev = Number.isFinite(Number(latestRev)) && Number(latestRev) > 0 ? Number(latestRev) : 1;
try { await calc._cTD(primeRev); } catch (_) { }
}
};
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
const maxLoops = 16;
const cellHistoryStartedAt = Date.now();
const cellHistoryBudgetMs = 9000;
const seenTrace = new Set();
const all = [];
let lastSignature = '';
let invalidTraceRetry = 0;
let filterRuntimeRetry = 0;
const constructResult = async (traceVersion, step1Data, activeSheetHint) => {
const key = step1Data?.currentSearchGridRange?.sheetIdKey;
const activeSheetCandidates = uniqueValues([
activeSheetHint,
sheetId,
Number.isFinite(Number(key)) ? Number(key) : null
]);
let lastError = null;
for (const sid of activeSheetCandidates) {
let s2 = null;
try {
s2 = await calc._cTd({ traceVersion, activeSheetId: sid });
} catch (_) {
s2 = { isError: true, code: 'EXCEPTION' };
}
if (!s2?.isError) return s2;
lastError = s2;
if (Number(s2?.code) !== 10040) break;
}
return lastError || { isError: true, code: 'UNKNOWN' };
};
let activeSheetIdForTd = sheetId;
for (let i = 0; i < maxLoops; i += 1) {
if ((Date.now() - cellHistoryStartedAt) > cellHistoryBudgetMs) break;
let filterRun = await runFilterQuery();
let step1 = filterRun?.step1;
if (!filterRun?.ok || step1?.isError) {
const code = Number(filterRun?.error?.code || step1?.code);
if (code === 10020 && filterRuntimeRetry < 3) {
filterRuntimeRetry += 1;
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
await sleep(80 * filterRuntimeRetry);
continue;
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
if (code === 10020) {
return buildOpsUnavailableResult('修订接口暂不可用(_cTC:10020)');
}
return { ok: false, error: `_cTC(${code || ''})` };
}
activeSheetIdForTd = filterRun?.args?.filterRangeSheetId ?? activeSheetIdForTd;
filterRuntimeRetry = 0;
let traceVersion = Number(step1?.data?.traceVersion);
const filterCount = Number(step1?.data?.filterCount || 0);
if (!Number.isFinite(traceVersion) || traceVersion <= 0) {
if (filterCount > 0 && invalidTraceRetry < 3) {
invalidTraceRetry += 1;
await resetAndPrime();
await sleep(80 * invalidTraceRetry);
continue;
}
if (step1?.shouldContinueFilter && i < maxLoops - 1) {
await sleep(40);
continue;
}
break;
}
invalidTraceRetry = 0;
if (seenTrace.has(traceVersion)) {
if (step1?.shouldContinueFilter && i < maxLoops - 1) {
await sleep(30);
continue;
}
break;
}
seenTrace.add(traceVersion);
let step2 = await constructResult(traceVersion, step1?.data, activeSheetIdForTd);
let retryTd10040 = 0;
while (step2?.isError && Number(step2?.code) === 10040 && retryTd10040 < 4) {
retryTd10040 += 1;
await resetAndPrime();
await sleep(50 * retryTd10040);
filterRun = await runFilterQuery();
step1 = filterRun?.step1;
if (!filterRun?.ok || step1?.isError) {
const code = Number(filterRun?.error?.code || step1?.code);
if (code === 10020 && filterRuntimeRetry < 3) {
filterRuntimeRetry += 1;
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
await sleep(80 * filterRuntimeRetry);
continue;
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
if (code === 10020) {
return buildOpsUnavailableResult('修订接口暂不可用(_cTC:10020)');
}
return { ok: false, error: `_cTC(${code || ''})` };
}
activeSheetIdForTd = filterRun?.args?.filterRangeSheetId ?? activeSheetIdForTd;
filterRuntimeRetry = 0;
traceVersion = Number(step1?.data?.traceVersion);
if (!Number.isFinite(traceVersion) || traceVersion <= 0) {
step2 = { isError: true, code: 10040 };
break;
}
if (seenTrace.has(traceVersion)) {
step2 = { isError: true, code: 10040 };
break;
}
seenTrace.add(traceVersion);
step2 = await constructResult(traceVersion, step1?.data, activeSheetIdForTd);
}
if (step2?.isError) {
// 10040 多见于 trace 已失效;尝试继续下一轮拉取,不立即中断。
if (Number(step2?.code) === 10040) {
if (all.length) break;
if (i < maxLoops - 1) {
await sleep(60);
continue;
}
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
return { ok: false, error: `_cTd(${step2?.code || ''})` };
}
const list = Array.isArray(step2?.data?.cellHistoryList) ? step2.data.cellHistoryList : [];
const authorMap = step2?.data?.authorMap || {};
for (const x of list) {
const authorId = String(x?.author || '');
all.push({
rowIndex0: Number(x?.rowIndex),
colIndex0: Number(x?.colIndex),
modifiedDateTime: Number(x?.modifiedDateTime),
actionType: Number(x?.actionType),
authorId,
authorName: authorMap?.[authorId]?.name || authorId,
beforeValue: cellDeltaText(x?.beforeCellDelta),
afterValue: cellDeltaText(x?.afterCellDelta)
});
}
const sig = `${traceVersion}|${list.length}|${step1?.shouldContinueFilter ? 1 : 0}`;
if (sig === lastSignature) break;
lastSignature = sig;
if (!step1?.shouldContinueFilter) break;
}
const uniq = [];
const seen = new Set();
for (const x of all) {
const key = [
x.rowIndex0,
x.colIndex0,
x.modifiedDateTime,
x.actionType,
x.authorId,
x.beforeValue,
x.afterValue
].join('|');
if (seen.has(key)) continue;
seen.add(key);
uniq.push(x);
}
uniq.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
const exact = uniq.filter((x) => x.rowIndex0 === row0);
if (!exact.length) {
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
return { ok: true, records: [], exactCount: 0, totalCount: uniq.length, source: 'cellHistory' };
}
return { ok: true, records: exact, exactCount: exact.length, totalCount: uniq.length, source: 'cellHistory' };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
function extractCellUrls(cell) {
const urls = [];
const seen = new Set();
const pushUrl = (u) => {
if (typeof u !== 'string') return;
const t = u.trim();
if (!t) return;
if (!/^(https?:\/\/|drive:\/\/)/i.test(t)) return;
if (seen.has(t)) return;
seen.add(t);
urls.push(t);
};
const collectFromString = (raw) => {
if (typeof raw !== 'string') return;
const s = raw.replace(/\\\//g, '/');
const re = /(drive:\/\/[^\s"'\\\],]+|https?:\/\/[^\s"'\\\],]+)/gi;
let m = null;
while ((m = re.exec(s)) !== null) {
if (m[1]) pushUrl(m[1]);
}
};
const collectDeep = (node, depth = 0) => {
if (node == null || depth > 6) return;
if (typeof node === 'string') {
collectFromString(node);
return;
}
if (Array.isArray(node)) {
for (const x of node) collectDeep(x, depth + 1);
return;
}
if (typeof node === 'object') {
for (const v of Object.values(node)) collectDeep(v, depth + 1);
}
};
// Rich text link segments
if (cell && cell.value && typeof cell.value === 'object') {
if (Array.isArray(cell.value.r)) {
for (const seg of cell.value.r) {
pushUrl(seg?.hyperlink?.url);
}
}
// Fallback for plain hyperlink object
pushUrl(cell.value?.hyperlink?.url);
}
// Image-like cell payload used by Tencent Docs (e.g. AA~AC screenshots)
const ext = cell?.extendedValue;
if (typeof ext === 'string' && ext.trim()) {
// Try decode up to 2 layers for encoded JSON string payloads.
let node = ext;
for (let i = 0; i < 2; i += 1) {
if (typeof node !== 'string') break;
const t = node.trim();
if (!t) break;
if (!/^[\[{"]/.test(t)) break;
try {
node = JSON.parse(t);
} catch (_) {
break;
}
}
collectDeep(node);
}
// Last fallback: deep scan known cell payload sections.
collectDeep(cell?.value);
collectDeep(cell?.formattedValue);
return urls;
}
function isColHidden(sheet, c) {
try {
if (typeof sheet.isColHiddenByUser === 'function' && sheet.isColHiddenByUser(c)) return true;
} catch (_) { }
try {
if (typeof sheet.isColHidden === 'function' && sheet.isColHidden(c)) return true;
} catch (_) { }
try {
if (typeof sheet.getColWidthWithDefault === 'function') {
const w = sheet.getColWidthWithDefault(c);
if (typeof w === 'number' && w <= 0) return true;
}
} catch (_) { }
try {
if (typeof sheet.getColProperty === 'function') {
const p = sheet.getColProperty(c);
if (p && (p.hidden === true || p.isHidden === true)) return true;
}
} catch (_) { }
return false;
}
function getHeaderText(sheet, col) {
// 腾讯文档常见表头在第2行(0-based=1),同时兼容第1行(0-based=0)
const rowCandidates = [1, 0];
for (const r of rowCandidates) {
const cell = sheet.getCellDataAtPosition(r, col, { isIgnoreCustomSheetView: true });
const t = textOfCell(cell).trim();
if (t) return t;
}
return '';
}
function getTrackedColumns(sheet, selectedCols = DEFAULT_TARGET_COLS) {
const colCount = sheet.getColCount?.() || 0;
const out = [];
const seen = new Set();
for (const c of Array.from(selectedCols || [])) {
if (!Number.isInteger(c) || c < 0 || c >= colCount) continue;
if (seen.has(c)) continue;
seen.add(c);
out.push(c);
}
return out;
}
function detectMaskedColumns(sheet, options = {}) {
const maxScanRows = Number(options?.maxScanRows);
const maxScanCells = Number(options?.maxScanCells);
const includeFallbackSignals = options?.includeFallbackSignals !== false;
const colCount = sheet.getColCount?.() || 0;
const rowCount = sheet.getRowCount?.() || 0;
if (!colCount || rowCount <= 2) return [];
const endByRows = Math.min(rowCount, Number.isFinite(maxScanRows) ? Math.max(3, Math.trunc(maxScanRows)) : rowCount);
const rowBudget = Math.max(1, Math.ceil((Number.isFinite(maxScanCells) ? maxScanCells : Infinity) / Math.max(1, colCount)));
const endRow = Math.min(endByRows, 2 + rowBudget);
const starCols = new Set();
const fallbackCols = new Set();
for (let r0 = 2; r0 < endRow; r0 += 1) {
for (let c0 = 0; c0 < colCount; c0 += 1) {
if (starCols.has(c0)) continue;
const cell = sheet.getCellDataAtPosition(r0, c0, { isIgnoreCustomSheetView: true });
const text = textOfCell(cell).trim();
const hasStar = /^[**]{2,}$/.test(text);
if (hasStar) {
starCols.add(c0);
continue;
}
if (!includeFallbackSignals || fallbackCols.has(c0)) continue;
// 找不到“*列”时的兜底:通过截图/隐藏对象线索识别可能列
const urls = extractCellUrls(cell);
const ext = String(cell?.extendedValue || '');
const hasDrive = urls.some((u) => /^drive:\/\//i.test(u));
const hasHiddenImage = /docimg\d*\.docs\.qq\.com\/image\/|drive:\/\/|drive\.docs\.qq\.com/i.test(ext);
const hasUrlButMaskedText = urls.length > 0 && text === '';
if (hasDrive || hasHiddenImage || hasUrlButMaskedText) fallbackCols.add(c0);
}
}
return [...(starCols.size ? starCols : fallbackCols)].sort((a, b) => a - b);
}
function detectAppealBlockColumns(sheet) {
const colCount = sheet.getColCount?.() || 0;
const normHeader = (col) => getHeaderText(sheet, col).replace(/\s+/g, '');
let start = -1;
for (let c = 0; c < colCount; c += 1) {
if (normHeader(c).includes('申诉或调整反馈')) {
start = c;
break;
}
}
if (start < 0) return [];
const out = [start];
const wanted = ['截图1', '截图2', '截图3', '截图', '申诉结果', '责任业务'];
for (let c = start + 1; c < Math.min(colCount, start + 12); c += 1) {
const h = normHeader(c);
if (!h) continue;
if (wanted.some((k) => h.includes(k))) out.push(c);
if (h.includes('责任业务')) break;
}
return [...new Set(out)].sort((a, b) => a - b);
}
function textVisualWidth(text) {
const s = String(text || '');
let w = 0;
for (const ch of s) {
w += /[\u0000-\u00ff]/.test(ch) ? 1 : 2;
}
return w;
}
function estimatePanelMetrics(rows) {
let maxColWidth = textVisualWidth('列');
for (const item of rows || []) {
maxColWidth = Math.max(maxColWidth, textVisualWidth(String(item.colLabel || item.col || '')));
}
const colPx = Math.min(120, Math.max(46, 16 + maxColWidth * 9));
// “值”列宽度与图片预览宽度保持一致
const valuePx = PREVIEW_IMAGE_WIDTH;
const panelPx = Math.max(280, Math.min(window.innerWidth - 24, colPx + valuePx + 36));
return { colPx, valuePx, panelPx };
}
function getSelectionRect() {
const selectors = ['.single-selection', '.select-selection-border'];
for (const sel of selectors) {
const list = document.querySelectorAll(sel);
for (const el of list) {
const rect = el.getBoundingClientRect();
if (rect.width <= 1 || rect.height <= 1) continue;
if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
if (rect.right < 0 || rect.left > window.innerWidth) continue;
return rect;
}
}
return null;
}
function placePanelNearSelection(panel) {
if (!panel || panel.classList.contains('hidden')) return false;
const selectionRect = getSelectionRect();
if (!selectionRect) {
panel.style.left = '';
panel.style.top = '';
panel.style.right = '16px';
panel.style.bottom = '16px';
return false;
}
const panelRect = panel.getBoundingClientRect();
const margin = 8;
const viewportW = window.innerWidth;
const viewportH = window.innerHeight;
const maxLeft = Math.max(margin, viewportW - panelRect.width - margin);
const maxTop = Math.max(margin, viewportH - panelRect.height - margin);
// 默认放在当前格右方
const rightLeft = selectionRect.right + margin;
const rightTop = Math.min(Math.max(margin, selectionRect.top), maxTop);
const canPlaceRight = rightLeft + panelRect.width <= viewportW - margin;
let left = 0;
let top = 0;
if (canPlaceRight) {
left = Math.min(Math.max(margin, rightLeft), maxLeft);
top = rightTop;
} else {
// 右侧放不下时,自动改到当前格上方
left = Math.min(Math.max(margin, selectionRect.left), maxLeft);
const aboveTop = selectionRect.top - panelRect.height - margin;
top = aboveTop >= margin ? aboveTop : margin;
}
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
return true;
}
function closeImageViewer(viewer) {
if (!viewer) return;
const img = viewer.querySelector('.tm-img-viewer-img');
if (img) img.removeAttribute('src');
viewer.classList.add('hidden');
if (Object.prototype.hasOwnProperty.call(viewer.dataset, 'prevOverflow')) {
document.documentElement.style.overflow = viewer.dataset.prevOverflow || '';
delete viewer.dataset.prevOverflow;
}
}
function ensureImageViewer() {
const existing = document.getElementById(IMAGE_VIEWER_ID);
if (existing) return existing;
const viewer = document.createElement('div');
viewer.id = IMAGE_VIEWER_ID;
viewer.className = 'hidden';
viewer.innerHTML = `
`;
viewer.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
if (target.dataset.close === '1') closeImageViewer(viewer);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !viewer.classList.contains('hidden')) {
closeImageViewer(viewer);
}
});
document.body.appendChild(viewer);
return viewer;
}
function openImageViewer(src) {
const url = String(src || '').trim();
if (!url) return;
const viewer = ensureImageViewer();
const img = viewer.querySelector('.tm-img-viewer-img');
if (!img) return;
if (viewer.classList.contains('hidden')) {
viewer.dataset.prevOverflow = document.documentElement.style.overflow || '';
document.documentElement.style.overflow = 'hidden';
}
img.setAttribute('src', url);
viewer.classList.remove('hidden');
}
function createPanel() {
if (document.getElementById(PANEL_ID)) return document.getElementById(PANEL_ID);
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${PANEL_ID} {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 999999;
width: 360px;
max-height: 60vh;
background: #ffffff;
color: #0f172a;
border: 1px solid #d9e2ec;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
display: flex;
flex-direction: column;
overflow: hidden;
}
#${PANEL_ID}.hidden { display: none; }
#${PANEL_ID} .hd {
flex: 0 0 auto;
padding: 10px 12px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
#${PANEL_ID} .title { font-weight: 600; }
#${PANEL_ID} .ctrl { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
#${PANEL_ID} input[type="number"] {
width: 84px; padding: 4px 6px; border-radius: 6px;
border: 1px solid #cbd5e1; background: #ffffff; color: #0f172a;
}
#${PANEL_ID} button {
padding: 4px 8px; border-radius: 6px; border: 1px solid #cbd5e1;
background: #ffffff; color: #0f172a; cursor: pointer;
}
#${PANEL_ID} button:hover { background: #f1f5f9; }
#${PANEL_ID} .bd {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
padding: 8px 10px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
scrollbar-width: none;
-ms-overflow-style: none;
}
#${PANEL_ID}:hover .bd { scrollbar-width: thin; }
#${PANEL_ID} .bd::-webkit-scrollbar { width: 0; height: 0; }
#${PANEL_ID}:hover .bd::-webkit-scrollbar { width: 8px; height: 0; }
#${PANEL_ID}:hover .bd::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
#${PANEL_ID}:hover .bd::-webkit-scrollbar-track { background: transparent; }
#${PANEL_ID} .tabbar {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
#${PANEL_ID} .tab-btn {
padding: 4px 8px;
}
#${PANEL_ID} .tab-btn.active {
background: #e2e8f0;
border-color: #94a3b8;
font-weight: 600;
}
#${PANEL_ID} .tab-pane { display: none; }
#${PANEL_ID} .tab-pane.active { display: block; }
#${PANEL_ID} .tm-main-table { min-width: 0; width: 100%; border-collapse: collapse; table-layout: auto; }
#${PANEL_ID} .tm-main-table th, #${PANEL_ID} .tm-main-table td { border: 1px solid #e2e8f0; padding: 4px 6px; vertical-align: top; word-break: break-word; }
#${PANEL_ID} .tm-main-table th { background: #f8fafc; font-weight: 600; }
#${PANEL_ID} .tm-main-table th:nth-child(1), #${PANEL_ID} .tm-main-table td:nth-child(1) {
width: var(--tm-col-letter-width, 56px);
white-space: nowrap;
word-break: normal;
}
#${PANEL_ID} .tm-main-table th:nth-child(2), #${PANEL_ID} .tm-main-table td:nth-child(2) {
width: var(--tm-col-value-width, 320px);
max-width: var(--tm-col-value-width, 320px);
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
#${PANEL_ID} .tm-op-note {
margin: 0 0 6px;
color: #64748b;
font-size: 11px;
line-height: 1.35;
min-height: 1.35em;
}
#${PANEL_ID} #tm-tab-ops > table {
width: 100%;
max-width: 100%;
table-layout: fixed;
}
#${PANEL_ID} .tm-history-table {
min-width: 0;
width: 100%;
max-width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
#${PANEL_ID} .tm-history-table th, #${PANEL_ID} .tm-history-table td {
border: 1px solid #e2e8f0;
padding: 4px 6px;
vertical-align: top;
word-break: break-word;
overflow-wrap: anywhere;
}
#${PANEL_ID} .tm-history-table th {
background: #f8fafc;
font-weight: 600;
}
#${PANEL_ID} .tm-history-table th:nth-child(1), #${PANEL_ID} .tm-history-table td:nth-child(1) {
width: 19ch;
min-width: 19ch;
white-space: nowrap;
word-break: normal;
overflow-wrap: normal;
font-variant-numeric: tabular-nums;
}
#${PANEL_ID} .tm-history-table th:nth-child(2), #${PANEL_ID} .tm-history-table td:nth-child(2) { white-space: normal; }
#${PANEL_ID} .tm-history-table th:nth-child(3), #${PANEL_ID} .tm-history-table td:nth-child(3) { width: 6ch; white-space: nowrap; }
#${PANEL_ID} .tm-history-table th:nth-child(4), #${PANEL_ID} .tm-history-table td:nth-child(4) { width: 12ch; white-space: normal; }
#${PANEL_ID} .tm-history-table .tm-meta-row td { background: #ffffff; }
#${PANEL_ID} .tm-history-table .tm-author-other {
font-weight: 700;
color: #b42318;
}
#${PANEL_ID} .tm-history-table .tm-detail-row td {
background: #f8fafc;
font-size: 11px;
border-top: none;
}
#${PANEL_ID} .tm-history-table .tm-detail-text {
display: block;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
#${PANEL_ID} .col-picker {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
max-width: 100%;
min-width: 0;
}
#${PANEL_ID} .col-summary-btn {
display: inline-flex;
align-items: center;
gap: 4px;
width: auto;
min-width: 0;
max-width: none;
overflow: hidden;
white-space: nowrap;
}
#${PANEL_ID} .col-summary-btn::after {
content: '▾';
color: #64748b;
font-size: 12px;
transition: transform 0.16s ease;
transform-origin: 50% 50%;
flex: none;
}
#${PANEL_ID} .col-picker.open .col-summary-btn::after { transform: rotate(180deg); }
#${PANEL_ID} .col-pop {
position: fixed;
top: 0;
left: 0;
right: auto;
z-index: 12;
width: 320px;
max-width: calc(100vw - 16px);
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
overflow: hidden;
}
#${PANEL_ID} .col-pop[hidden] { display: none; }
#${PANEL_ID} .col-list {
max-height: min(260px, 36vh);
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
scrollbar-width: none;
-ms-overflow-style: none;
display: grid;
grid-template-columns: 1fr;
gap: 4px 8px;
padding: 8px;
border-bottom: 1px solid #e2e8f0;
}
#${PANEL_ID} .col-list:hover { scrollbar-width: thin; }
#${PANEL_ID} .col-list::-webkit-scrollbar { width: 0; height: 0; }
#${PANEL_ID} .col-list:hover::-webkit-scrollbar { width: 8px; height: 0; }
#${PANEL_ID} .col-list:hover::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
#${PANEL_ID} .col-list label {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
cursor: grab;
padding: 2px 4px;
border-radius: 6px;
}
#${PANEL_ID} .col-list label.dragging { opacity: 0.55; }
#${PANEL_ID} .col-list label.drag-over {
background: #f8fafc;
outline: 1px dashed #94a3b8;
}
#${PANEL_ID} .col-list label.drag-over-before { box-shadow: inset 0 2px 0 #3b82f6; }
#${PANEL_ID} .col-list label.drag-over-after { box-shadow: inset 0 -2px 0 #3b82f6; }
#${PANEL_ID} .col-list .drag-handle {
color: #94a3b8;
font-size: 12px;
line-height: 1;
user-select: none;
flex: none;
}
#${PANEL_ID} .col-list .txt {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#${PANEL_ID} .col-actions {
display: flex;
gap: 6px;
padding: 8px;
background: #f8fafc;
}
#${PANEL_ID} .col-actions button { padding: 3px 8px; }
#${PANEL_ID} .tm-preview-wrap {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
#${PANEL_ID} .tm-preview-wrap > * { flex: 0 0 auto; }
#${PANEL_ID} .tm-img-link {
display: inline-flex;
flex-direction: row;
align-items: flex-start;
width: auto;
max-width: none;
border: 1px solid #d9e2ec;
border-radius: 6px;
overflow: hidden;
text-decoration: none;
background: #ffffff;
}
#${PANEL_ID} .tm-img-link-btn {
padding: 0;
cursor: zoom-in;
text-align: left;
font: inherit;
color: inherit;
}
#${PANEL_ID} .tm-img-preview {
display: block;
width: auto;
height: 20px;
max-width: none;
max-height: 20px;
object-fit: contain;
background: #f8fafc;
}
#${PANEL_ID} .tm-img-caption {
display: block;
width: 100%;
box-sizing: border-box;
padding: 4px 6px;
font-size: 11px;
line-height: 1.35;
color: #475569;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#${IMAGE_VIEWER_ID} {
position: fixed;
inset: 0;
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
}
#${IMAGE_VIEWER_ID}.hidden { display: none; }
#${IMAGE_VIEWER_ID} .tm-img-viewer-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.72);
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-img {
position: relative;
max-width: 92vw;
max-height: 92vh;
object-fit: contain;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.35);
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-close {
position: fixed;
top: 14px;
right: 14px;
z-index: 1;
width: 32px;
height: 32px;
line-height: 30px;
text-align: center;
border-radius: 16px;
border: 1px solid rgba(226, 232, 240, 0.9);
background: rgba(15, 23, 42, 0.85);
color: #ffffff;
font-size: 20px;
cursor: pointer;
}
#${LAUNCHER_ID} {
position: fixed;
right: 16px;
bottom: 72px;
z-index: 2147483647;
width: 36px;
height: 36px;
border-radius: 18px;
border: 1px solid #cbd5e1;
background: #ffffff;
color: #0f172a;
font: 12px/36px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-align: center;
cursor: pointer;
user-select: none;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
}
#${PANEL_ID}:not(.hidden) ~ #${LAUNCHER_ID} { display: none; }
`;
document.head.appendChild(style);
}
const panel = document.createElement('section');
panel.id = PANEL_ID;
panel.innerHTML = `
`;
document.body.appendChild(panel);
if (!document.getElementById(LAUNCHER_ID)) {
const launcher = document.createElement('div');
launcher.id = LAUNCHER_ID;
launcher.title = '显示/隐藏 隐藏列内容查看器';
launcher.textContent = '显';
launcher.addEventListener('click', () => {
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
requestAnimationFrame(() => placePanelNearSelection(panel));
}
});
document.body.appendChild(launcher);
}
return panel;
}
function safeJsonStringify(v) {
try { return JSON.stringify(v, null, 2); } catch (_) { return String(v); }
}
function tryExtractRowFromSelectionPayload(payload, maxRow) {
const seen = new WeakSet();
const weightedRows = [];
function pushRow(v, weight) {
if (typeof v !== 'number') return;
if (!Number.isFinite(v)) return;
const row = Math.trunc(v);
if (row < 0 || row >= maxRow) return;
weightedRows.push({ row, weight });
}
function walk(node, depth) {
if (!node || depth > 8) return;
if (typeof node !== 'object') return;
if (seen.has(node)) return;
seen.add(node);
const keys = Object.keys(node);
// 这些字段更可能是当前选中行。
const strongKeys = ['row', 'rowIndex', 'startRow', 'startRowIndex', 'r'];
for (const k of strongKeys) pushRow(node[k], 4);
// 这些字段通常是范围终点,权重低一些。
const weakKeys = ['endRow', 'endRowIndex'];
for (const k of weakKeys) pushRow(node[k], 2);
for (const k of keys) {
const v = node[k];
if (typeof v === 'number') {
if (/row/i.test(k)) pushRow(v, 1);
} else if (v && typeof v === 'object') {
walk(v, depth + 1);
}
}
}
walk(payload, 0);
if (!weightedRows.length) return null;
const score = new Map();
for (const { row, weight } of weightedRows) {
score.set(row, (score.get(row) || 0) + weight);
}
let bestRow = null;
let bestScore = -1;
for (const [row, s] of score.entries()) {
if (s > bestScore) {
bestScore = s;
bestRow = row;
}
}
return bestRow;
}
function parseA1Row0(raw, maxRow) {
if (typeof raw !== 'string') return null;
const text = raw.trim();
if (!text || text.length > 40) return null;
// A1 或 A1:B8 这类名称框格式
const m = text.match(/^\$?[A-Za-z]{1,4}\$?(\d{1,7})(?::\$?[A-Za-z]{1,4}\$?(\d{1,7}))?$/);
if (!m) return null;
const row0 = Number(m[1]) - 1;
if (!Number.isFinite(row0) || row0 < 0 || row0 >= maxRow) return null;
return row0;
}
function tryExtractRowFromDom(maxRow) {
const checked = new Set();
const candidates = [];
if (document.activeElement) candidates.push(document.activeElement);
candidates.push(...document.querySelectorAll('input, [role="textbox"], textarea'));
for (const el of candidates) {
if (!el || checked.has(el)) continue;
checked.add(el);
const value =
(typeof el.value === 'string' ? el.value : '') ||
(typeof el.textContent === 'string' ? el.textContent : '');
const row0 = parseA1Row0(value, maxRow);
if (typeof row0 === 'number') return row0;
}
return null;
}
function tryExtractRowFromView(app, maxRow) {
const pickRow = (v) => {
if (typeof v !== 'number' || !Number.isFinite(v)) return null;
const row = Math.trunc(v);
if (row < 0 || row >= maxRow) return null;
return row;
};
try {
const row = pickRow(app?.view?.getActiveCell?.()?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
try {
const sel = app?.view?.getSelection?.();
const row =
pickRow(sel?.activeCell?.rowIndex) ??
pickRow(sel?.rangeSelections?.[0]?.startRowIndex) ??
pickRow(sel?.rangeSelections?.[0]?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
try {
const ranges = app?.view?.getSelectionRanges?.();
const row =
pickRow(ranges?.[0]?.startRowIndex) ??
pickRow(ranges?.[0]?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
return null;
}
function install() {
const panel = createPanel();
panel.classList.add('hidden');
ensureImageViewer();
const panelBody = panel.querySelector('.bd');
const rowInputBoot = panel.querySelector('#tm-row-input');
const btnRefreshBoot = panel.querySelector('#tm-refresh');
const btnToggleBoot = panel.querySelector('#tm-toggle');
const btnDetectBoot = panel.querySelector('#tm-col-detect');
const btnColAllBoot = panel.querySelector('#tm-col-all');
const btnColClearBoot = panel.querySelector('#tm-col-clear');
let initialized = false;
let doRefresh = () => {
tryInit();
};
let doDetectCols = () => {
tryInit();
};
let doSelectAllCols = () => { };
let doClearCols = () => { };
let doToggle = () => {
panel.classList.add('hidden');
};
let doReposition = () => {
placePanelNearSelection(panel);
};
let repositionRaf = 0;
const scheduleReposition = () => {
if (panel.classList.contains('hidden')) return;
if (repositionRaf) return;
repositionRaf = requestAnimationFrame(() => {
repositionRaf = 0;
doReposition();
});
};
btnRefreshBoot?.addEventListener('click', () => doRefresh());
btnToggleBoot?.addEventListener('click', () => doToggle());
btnDetectBoot?.addEventListener('click', () => doDetectCols());
btnColAllBoot?.addEventListener('click', () => doSelectAllCols());
btnColClearBoot?.addEventListener('click', () => doClearCols());
rowInputBoot?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') doRefresh();
});
window.addEventListener('resize', () => scheduleReposition(), { passive: true });
// 避免面板内部滚动触发全局重定位,导致滚动偶发失效
window.addEventListener('scroll', () => scheduleReposition(), { passive: true });
if (typeof ResizeObserver === 'function') {
const resizeObserver = new ResizeObserver(() => scheduleReposition());
resizeObserver.observe(panel);
if (panelBody) resizeObserver.observe(panelBody);
}
panel.addEventListener('wheel', (e) => {
if (panel.classList.contains('hidden')) return;
const target = e.target;
if (!(target instanceof Element)) return;
const bdEl = panel.querySelector('.bd');
const listEl = panel.querySelector('#tm-col-list');
if (!bdEl) return;
let scrollEl = null;
if (listEl && listEl.contains(target) && listEl.scrollHeight > listEl.clientHeight + 1) {
scrollEl = listEl;
} else {
scrollEl = bdEl;
}
const max = scrollEl.scrollHeight - scrollEl.clientHeight;
if (max <= 0) return;
const delta = Number(e.deltaY) || 0;
if (delta === 0) return;
const atTop = scrollEl.scrollTop <= 0;
const atBottom = scrollEl.scrollTop >= max - 1;
const shouldHandle = (delta < 0 && !atTop) || (delta > 0 && !atBottom);
if (!shouldHandle) return;
scrollEl.scrollTop += delta;
e.preventDefault();
e.stopPropagation();
}, { capture: true, passive: false });
panel.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
const trigger = target.closest('.tm-img-link[data-full-src]');
if (!trigger || !panel.contains(trigger)) return;
const src = String(trigger.getAttribute('data-full-src') || '').trim();
if (!src) return;
e.preventDefault();
openImageViewer(src);
});
const initDataLayer = ({ app, sheet }) => {
if (initialized) return;
initialized = true;
const rowInput = panel.querySelector('#tm-row-input');
const tbody = panel.querySelector('#tm-tbody');
const opTbody = panel.querySelector('#tm-op-tbody');
const opNote = panel.querySelector('#tm-op-note');
const colPicker = panel.querySelector('#tm-col-picker');
const colPop = panel.querySelector('#tm-col-pop');
const colList = panel.querySelector('#tm-col-list');
const colSummary = panel.querySelector('#tm-col-summary');
const btnDetect = panel.querySelector('#tm-col-detect');
const tabColsBtn = panel.querySelector('#tm-tab-cols-btn');
const tabOpsBtn = panel.querySelector('#tm-tab-ops-btn');
const tabCols = panel.querySelector('#tm-tab-cols');
const tabOps = panel.querySelector('#tm-tab-ops');
let currentRow1 = 2;
let pendingOpsRow0 = 1;
let candidateCols = [];
let selectedCols = new Set();
let draggingCol = null;
let activeTab = 'cols';
let opRenderSeq = 0;
let valueRenderSeq = 0;
let selectionRenderTimer = 0;
let pendingSelectionRow1 = null;
let refreshOpsForCurrentRow = () => { };
let colHeaderRetryTimer = 0;
let tryScheduleColHeaderRetry = () => { };
const colOrderStorageKey = getColumnOrderStorageKey(app);
panel.style.setProperty('--tm-preview-width', `${PREVIEW_IMAGE_WIDTH}px`);
const maxRowForInit = sheet.getRowCount?.() || 1;
const initialRow0 =
tryExtractRowFromView(app, maxRowForInit) ??
tryExtractRowFromDom(maxRowForInit);
if (typeof initialRow0 === 'number') currentRow1 = initialRow0 + 1;
const switchTab = (tab) => {
activeTab = tab === 'ops' ? 'ops' : 'cols';
if (tabColsBtn) tabColsBtn.classList.toggle('active', activeTab === 'cols');
if (tabOpsBtn) tabOpsBtn.classList.toggle('active', activeTab === 'ops');
if (tabCols) tabCols.classList.toggle('active', activeTab === 'cols');
if (tabOps) tabOps.classList.toggle('active', activeTab === 'ops');
if (activeTab === 'ops') refreshOpsForCurrentRow();
};
tabColsBtn?.addEventListener('click', () => switchTab('cols'));
tabOpsBtn?.addEventListener('click', () => switchTab('ops'));
switchTab('cols');
const resolveInitialRow1 = async () => {
const started = Date.now();
while (Date.now() - started < 2200) {
const row0 =
tryExtractRowFromView(app, maxRowForInit) ??
tryExtractRowFromDom(maxRowForInit);
if (typeof row0 === 'number') return row0 + 1;
await new Promise((r) => setTimeout(r, 120));
}
return currentRow1;
};
const placeColPickerPop = () => {
if (!colPop || colPop.hidden || !colSummary) return;
const margin = 8;
const anchor = colSummary.getBoundingClientRect();
const rect = colPop.getBoundingClientRect();
const popW = Math.min(rect.width || 320, Math.max(120, window.innerWidth - margin * 2));
let left = anchor.left;
if (left + popW > window.innerWidth - margin) left = window.innerWidth - margin - popW;
if (left < margin) left = margin;
let top = anchor.bottom + 6;
const popH = rect.height || 260;
if (top + popH > window.innerHeight - margin) {
top = Math.max(margin, anchor.top - popH - 6);
}
colPop.style.left = `${Math.round(left)}px`;
colPop.style.top = `${Math.round(top)}px`;
};
const setColPickerOpen = (open) => {
const shouldOpen = !!open;
if (colPicker) colPicker.classList.toggle('open', shouldOpen);
if (colPop) colPop.hidden = !shouldOpen;
if (colSummary) colSummary.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false');
if (shouldOpen) {
requestAnimationFrame(placeColPickerPop);
tryScheduleColHeaderRetry();
}
};
const toggleColPicker = () => {
const isOpen = colPicker?.classList.contains('open');
setColPickerOpen(!isOpen);
};
colSummary?.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggleColPicker();
});
document.addEventListener('mousedown', (e) => {
const target = e.target;
if (!(target instanceof Node)) return;
if (!colPicker?.contains(target)) setColPickerOpen(false);
});
window.addEventListener('resize', () => {
if (colPicker?.classList.contains('open')) placeColPickerPop();
}, { passive: true });
window.addEventListener('scroll', () => {
if (colPicker?.classList.contains('open')) placeColPickerPop();
}, { passive: true });
const getOrderedSelectedCols = () => {
const ordered = [];
const used = new Set();
for (const c of candidateCols) {
if (!selectedCols.has(c)) continue;
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
for (const c of selectedCols) {
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
return getTrackedColumns(sheet, ordered);
};
const applySavedOrder = (cols) => {
const normalized = getTrackedColumns(sheet, cols);
const saved = loadColumnOrder(colOrderStorageKey);
if (!saved.length) return normalized;
const set = new Set(normalized);
const ordered = [];
for (const c of saved) {
if (!set.has(c)) continue;
ordered.push(c);
set.delete(c);
}
for (const c of normalized) {
if (!set.has(c)) continue;
ordered.push(c);
set.delete(c);
}
return ordered;
};
const persistCandidateOrder = () => {
saveColumnOrder(colOrderStorageKey, candidateCols);
};
const reorderCandidateCols = (fromCol, toCol, placeAfter = false) => {
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol === toCol) return;
const fromIdx = candidateCols.indexOf(fromCol);
const toIdx = candidateCols.indexOf(toCol);
if (fromIdx < 0 || toIdx < 0) return;
const next = candidateCols.slice();
const [moved] = next.splice(fromIdx, 1);
let insertIdx = next.indexOf(toCol);
if (insertIdx < 0) insertIdx = next.length;
else if (placeAfter) insertIdx += 1;
next.splice(insertIdx, 0, moved);
candidateCols = next;
persistCandidateOrder();
};
const updateColSummary = () => {
const letters = getOrderedSelectedCols().map(colToLetter);
colSummary.textContent = '隐藏列';
colSummary.title = letters.length ? `已选: ${letters.join(',')}` : '隐藏列';
};
const renderColList = () => {
colList.innerHTML = '';
if (!candidateCols.length) {
const empty = document.createElement('div');
empty.textContent = '未识别到可选列';
colList.appendChild(empty);
updateColSummary();
return;
}
const clearDragVisuals = () => {
colList.querySelectorAll('label.dragging, label.drag-over, label.drag-over-before, label.drag-over-after').forEach((el) => {
el.classList.remove('dragging', 'drag-over', 'drag-over-before', 'drag-over-after');
delete el.dataset.dropPos;
});
};
for (const c of candidateCols) {
const label = document.createElement('label');
label.draggable = true;
label.dataset.col = String(c);
label.title = '拖拽调整显示顺序';
label.addEventListener('dragstart', (e) => {
draggingCol = c;
label.classList.add('dragging');
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(c));
}
});
label.addEventListener('dragover', (e) => {
if (draggingCol == null || draggingCol === c) return;
e.preventDefault();
const rect = label.getBoundingClientRect();
const placeAfter = e.clientY >= rect.top + rect.height / 2;
clearDragVisuals();
label.classList.add('drag-over');
label.classList.add(placeAfter ? 'drag-over-after' : 'drag-over-before');
label.dataset.dropPos = placeAfter ? 'after' : 'before';
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
});
label.addEventListener('dragleave', () => {
label.classList.remove('drag-over', 'drag-over-before', 'drag-over-after');
delete label.dataset.dropPos;
});
label.addEventListener('drop', (e) => {
e.preventDefault();
const placeAfter = label.dataset.dropPos === 'after';
label.classList.remove('drag-over', 'drag-over-before', 'drag-over-after');
delete label.dataset.dropPos;
const from = draggingCol;
draggingCol = null;
if (!Number.isInteger(from) || from === c) return;
reorderCandidateCols(from, c, placeAfter);
renderColList();
doRefresh();
});
label.addEventListener('dragend', () => {
draggingCol = null;
clearDragVisuals();
});
const handle = document.createElement('span');
handle.className = 'drag-handle';
handle.textContent = '⋮⋮';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.draggable = false;
cb.checked = selectedCols.has(c);
cb.addEventListener('change', () => {
if (cb.checked) selectedCols.add(c);
else selectedCols.delete(c);
updateColSummary();
doRefresh();
});
const txt = document.createElement('span');
txt.className = 'txt';
const h = getHeaderText(sheet, c);
txt.textContent = `${colToLetter(c)}${h ? ` (${h})` : ''}`;
label.appendChild(handle);
label.appendChild(cb);
label.appendChild(txt);
colList.appendChild(label);
}
updateColSummary();
};
const clearColHeaderRetry = () => {
if (!colHeaderRetryTimer) return;
clearTimeout(colHeaderRetryTimer);
colHeaderRetryTimer = 0;
};
const getHeaderSignature = () => (
candidateCols.map((c) => `${c}:${getHeaderText(sheet, c)}`).join('|')
);
const hasMissingHeaders = () => candidateCols.some((c) => !getHeaderText(sheet, c));
const scheduleColHeaderRetry = () => {
clearColHeaderRetry();
if (!candidateCols.length) return;
if (!hasMissingHeaders()) return;
let attempts = 0;
let lastSig = getHeaderSignature();
const tick = () => {
colHeaderRetryTimer = 0;
attempts += 1;
const sig = getHeaderSignature();
if (sig !== lastSig) {
lastSig = sig;
renderColList();
}
if (!hasMissingHeaders()) return;
if (attempts >= COL_HEADER_RETRY_MAX_ATTEMPTS) return;
colHeaderRetryTimer = setTimeout(tick, COL_HEADER_RETRY_INTERVAL_MS);
};
colHeaderRetryTimer = setTimeout(tick, COL_HEADER_RETRY_INTERVAL_MS);
};
tryScheduleColHeaderRetry = scheduleColHeaderRetry;
const detectAndApplyCols = (mode = 'quick') => {
const appealCols = detectAppealBlockColumns(sheet);
if (appealCols.length) {
candidateCols = appealCols;
} else {
if (mode === 'deep') {
const cols = detectMaskedColumns(sheet, {
maxScanRows: DEEP_SCAN_MAX_ROWS,
maxScanCells: DEEP_SCAN_MAX_CELLS,
includeFallbackSignals: true
});
candidateCols = cols.length ? cols : getTrackedColumns(sheet, DEFAULT_TARGET_COLS);
} else {
const quickCols = detectMaskedColumns(sheet, {
maxScanRows: QUICK_SCAN_MAX_ROWS,
maxScanCells: QUICK_SCAN_MAX_CELLS,
includeFallbackSignals: false
});
candidateCols = quickCols.length ? quickCols : getTrackedColumns(sheet, DEFAULT_TARGET_COLS);
}
}
candidateCols = applySavedOrder(candidateCols);
const candidateSet = new Set(candidateCols);
if (!selectedCols.size) selectedCols = new Set(candidateCols);
selectedCols = new Set([...selectedCols].filter((c) => candidateSet.has(c)));
if (!selectedCols.size && candidateCols.length) selectedCols = new Set(candidateCols);
persistCandidateOrder();
renderColList();
scheduleColHeaderRetry();
};
const renderCurrentRowOps = async (row0) => {
pendingOpsRow0 = Math.max(0, Number(row0) || 0);
const seq = ++opRenderSeq;
if (opNote) opNote.textContent = '正在读取当前行修订记录...';
if (opTbody) opTbody.innerHTML = '| 正在读取... |
';
const result = await runOpsFetchSerial(() => withTimeout(
() => fetchCurrentRowOperationRecords(app, sheet, row0),
30000,
{ ok: false, error: '修订读取超时' }
));
if (seq !== opRenderSeq) return;
if (!opTbody) return;
if (!result?.ok) {
const errMsg = String(result?.error || '未知错误');
if (isOpsUnavailableError(errMsg)) {
if (opNote) opNote.textContent = '当前行记录暂不可用,请稍后重试';
opTbody.innerHTML = '| 当前行暂无修订记录 |
';
return;
}
if (opNote) opNote.textContent = `读取失败: ${errMsg}`;
opTbody.innerHTML = '| 未获取到当前行修订记录 |
';
return;
}
const sortedRecords = (Array.isArray(result.records) ? result.records : [])
.slice()
.sort((a, b) => (Number(b?.modifiedDateTime) || 0) - (Number(a?.modifiedDateTime) || 0));
const dedupedRecords = dedupeOperationRecords(sortedRecords);
const dedupedCount = Math.max(0, sortedRecords.length - dedupedRecords.length);
const records = dedupedRecords.filter((item) => !shouldHideNoiseDetailRecord(item));
const hiddenNoiseCount = Math.max(0, dedupedRecords.length - records.length);
if (!records.length) {
if (opNote) {
const warning = String(result?.warning || '').trim();
const hiddenTag = hiddenNoiseCount > 0 ? `(已隐藏 ${hiddenNoiseCount} 条格式操作)` : '';
opNote.textContent = `${warning || '当前行暂无修订记录'}${hiddenTag}`;
}
opTbody.innerHTML = '| 当前行暂无修订记录 |
';
return;
}
if (opNote) {
const sourceTag =
result.source === 'changes' ? '(变更流)'
: result.source === 'localQueue' ? '(本地缓存)'
: '';
const dedupeTag = dedupedCount > 0 ? `(去重 ${dedupedCount} 条)` : '';
const hiddenTag = hiddenNoiseCount > 0 ? `(隐藏 ${hiddenNoiseCount} 条格式操作)` : '';
opNote.textContent = `当前行命中 ${records.length} 条操作记录${sourceTag}${dedupeTag}${hiddenTag}`;
}
const rowsHtml = records.map((item) => {
const hasCol = Number.isFinite(item.colIndex0) && item.colIndex0 >= 0;
const colLetter = hasCol ? colToLetter(item.colIndex0) : '-';
const colHeader = hasCol ? String(getHeaderText(sheet, item.colIndex0) || '').trim() : '';
const colText = colHeader || colLetter;
const colTitle = hasCol ? `${colLetter}${colHeader ? ` (${colHeader})` : ''}` : '-';
const detail = displayDetailText(item);
const detailText = detail || '(空)';
const authorText = String(item.authorName || item.authorId || '-').trim() || '-';
const authorClass = /^mi$/i.test(authorText) ? '' : 'tm-author-other';
return `
| ${escapeHtml(formatFullDateTime(item.modifiedDateTime) || '-')} |
${escapeHtml(colText)} |
${escapeHtml(actionTypeLabel(item.actionType))} |
${escapeHtml(authorText)} |
明细:${escapeHtml(detailText)} |
`;
}).join('');
opTbody.innerHTML = rowsHtml || '| 当前行暂无修订记录 |
';
};
refreshOpsForCurrentRow = () => renderCurrentRowOps(pendingOpsRow0);
const render = async (row1, options = {}) => {
const force = options?.force === true;
const maxRow = sheet.getRowCount?.() || 0;
const row0 = Math.max(0, Math.min(maxRow - 1, row1 - 1));
const nextRow1 = row0 + 1;
if (!force && nextRow1 === currentRow1) {
scheduleReposition();
return;
}
currentRow1 = nextRow1;
pendingOpsRow0 = row0;
rowInput.value = String(currentRow1);
const bdEl = panel.querySelector('.bd');
if (bdEl) bdEl.scrollTop = 0;
if (activeTab === 'ops') renderCurrentRowOps(row0);
const renderSeq = ++valueRenderSeq;
const trackedCols = getOrderedSelectedCols();
if (!trackedCols.length) {
if (renderSeq !== valueRenderSeq) return;
tbody.innerHTML = '| 请先在“隐藏列”中选择列 |
';
const m = estimatePanelMetrics([]);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
return;
}
const mapped = trackedCols.map((c) => {
const colName = colToLetter(c);
const header = getHeaderText(sheet, c);
const colLabel = header || colName;
const cell = sheet.getCellDataAtPosition(row0, c, { isIgnoreCustomSheetView: true });
const urls = extractCellUrls(cell);
const text = textOfCell(cell).trim();
return {
col0: c,
col: colName,
header,
colLabel,
value: text,
urls
};
});
const rowData = await Promise.all(mapped.map(async (item) => {
const resolvedUrls = await resolveUrls(item.urls);
const value = resolvedUrls.length ? resolvedUrls.join(' | ') : item.value;
return { ...item, resolvedUrls, value };
}));
if (renderSeq !== valueRenderSeq) return;
const visibleData = rowData.filter((item) => {
if (Array.isArray(item.resolvedUrls) && item.resolvedUrls.length) return true;
return String(item.value || '').trim() !== '';
});
tbody.innerHTML = '';
if (!visibleData.length) {
const tr = document.createElement('tr');
tr.innerHTML = '当前行无非空内容 | ';
tbody.appendChild(tr);
const m = estimatePanelMetrics([]);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
return;
}
for (const item of visibleData) {
const tr = document.createElement('tr');
tr.innerHTML = `${escapeHtml(item.colLabel || item.col)} | ${valueHtml(item)} | `;
tbody.appendChild(tr);
}
const m = estimatePanelMetrics(visibleData);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
};
doRefresh = () => {
const row = Number(rowInput.value) || currentRow1;
render(row, { force: true });
};
doDetectCols = () => {
if (btnDetect) btnDetect.disabled = true;
try {
detectAndApplyCols('deep');
doRefresh();
} finally {
if (btnDetect) btnDetect.disabled = false;
}
};
doSelectAllCols = () => {
selectedCols = new Set(candidateCols);
renderColList();
doRefresh();
};
doClearCols = () => {
selectedCols = new Set();
renderColList();
doRefresh();
};
doToggle = () => {
clearColHeaderRetry();
setColPickerOpen(false);
panel.classList.add('hidden');
};
doReposition = () => {
placePanelNearSelection(panel);
if (colPicker?.classList.contains('open')) placeColPickerPop();
};
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
setColPickerOpen(false);
return;
}
// Alt + H 切换面板
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'h' || e.key === 'H')) {
panel.classList.toggle('hidden');
if (panel.classList.contains('hidden')) {
clearColHeaderRetry();
setColPickerOpen(false);
}
if (!panel.classList.contains('hidden')) scheduleReposition();
}
// Alt + R 刷新当前行
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'r' || e.key === 'R')) {
doRefresh();
}
});
// 保留“点击表格行 => 面板自动显示对应行”的联动能力
try {
if (typeof app.view?.onSelectionChange === 'function') {
app.view.onSelectionChange((payload) => {
const maxRow = sheet.getRowCount?.() || 1;
const row0 =
tryExtractRowFromSelectionPayload(payload, maxRow) ??
tryExtractRowFromView(app, maxRow) ??
tryExtractRowFromDom(maxRow);
if (typeof row0 === 'number') {
pendingSelectionRow1 = row0 + 1;
if (selectionRenderTimer) return;
selectionRenderTimer = setTimeout(() => {
selectionRenderTimer = 0;
const row1 = pendingSelectionRow1;
pendingSelectionRow1 = null;
if (typeof row1 === 'number') render(row1);
}, SELECTION_RENDER_DEBOUNCE_MS);
}
else doReposition();
});
}
} catch (_) { }
detectAndApplyCols('quick');
resolveInitialRow1().then((row1) => {
currentRow1 = row1;
render(currentRow1, { force: true });
});
};
const tryInit = () => {
waitForSpreadsheetApp(5000).then(initDataLayer).catch((err) => {
console.warn('[hidden-col-viewer] retry init:', err?.message || err);
setTimeout(tryInit, 2000);
});
};
tryInit();
}
if (document.readyState === 'complete') install();
else window.addEventListener('load', install, { once: true });
})();