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