// ==UserScript== // @name 腾讯/钉钉文档表格-隐藏列内容查看器 // @namespace https://docs.qq.com/ // @version 0.4.66 // @description 在腾讯文档与钉钉文档表格页显示列实际内容,并在原网页展开/还原这些列 // @author codex // @match https://docs.qq.com/sheet/* // @match https://alidocs.dingtalk.com/i/nodes/* // @match https://alidocs.dingtalk.com/spreadsheetv2/* // @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 DINGTALK_DEFAULT_TARGET_COLS = Array.from({ length: 26 }, (_, i) => i); // A~Z const DINGTALK_ROW_GROUP_SIZE = 3; const ALIDOCS_HEADER_SCAN_MAX_ROWS = 6; const ALIDOCS_HEADER_SCAN_MAX_COLS = 36; const ALIDOCS_HEADER_CACHE_TTL_MS = 8000; const ALIDOCS_HEADER_KEYWORDS = [ '截图', '违规', '类型', '链接', '店铺', '商品', '品牌', '申诉', '反馈', '结果', '业务', '责任', '处理', '状态', '时间', '日期', '备注', '渠道', '价格', 'sku', 'id', '名称', '手机号', '电话', '订单', '平台', '类目', '型号', '活动', '原因' ]; const PREVIEW_IMAGE_WIDTH = 320; const QUICK_SCAN_MAX_ROWS = 240; const QUICK_SCAN_MAX_CELLS = 12000; const DEEP_SCAN_MAX_ROWS = 6000; 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 REVEAL_VIEW_STORAGE_PREFIX = '__TM_HIDDEN_COL_REVEAL_VIEW__'; const CELL_READ_OPTIONS = { isIgnoreCustomSheetView: true }; const REVEAL_COL_WIDTH_MIN = 64; const REVEAL_COL_WIDTH_MAX = 1600; const REVEAL_ROW_COL_KEY = '__row__'; const REVEAL_DEFAULT_ROW_COL_WIDTH = 76; const REVEAL_DEFAULT_DATA_COL_WIDTH = 280; const INIT_RETRY_MAX_ATTEMPTS = 36; const ALIDOCS_SELECTION_POLL_MS = 160; const ALIDOCS_PORTAL_PATH_RE = /^\/i\/nodes\//; const PAGE_WIN = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window; const SPREADSHEET_APP_HINT_KEYS = [ 'SpreadsheetApp', 'spreadsheetApp', '__SpreadsheetApp__', '__sheetApp__', 'AliDocsSpreadsheetApp' ]; const WEBPACK_CHUNK_HINT_KEYS = [ 'webpackChunk_tencent_sheet', 'webpackChunk_tencent_docs', 'webpackChunk_alidocs', 'webpackChunk_dingtalk_docs', 'webpackChunk_dingtalk_doc' ]; const DRIVE_URL_CACHE = new Map(); const ALIDOCS_HEADER_ROW_CACHE = new WeakMap(); 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 seen = new Set(); const candidates = []; const addCandidate = (name, chunk) => { if (!name || !Array.isArray(chunk) || typeof chunk.push !== 'function') return; if (seen.has(chunk)) return; seen.add(chunk); candidates.push(chunk); }; for (const key of WEBPACK_CHUNK_HINT_KEYS) { let chunk = null; try { chunk = PAGE_WIN[key]; } catch (_) { chunk = null; } addCandidate(key, chunk); } let windowKeys = []; try { windowKeys = Object.getOwnPropertyNames(PAGE_WIN); } catch (_) { windowKeys = []; } for (const key of windowKeys) { if (!/^webpackChunk/i.test(String(key))) continue; let chunk = null; try { chunk = PAGE_WIN[key]; } catch (_) { chunk = null; } addCandidate(key, chunk); } for (const chunk of candidates) { let req = null; try { const marker = `tm_probe_${Date.now()}_${Math.random()}`; chunk.push([[marker], {}, (r) => { req = r; }]); } catch (_) { req = null; } if (typeof req === 'function') { SHEET_WEBPACK_REQUIRE = req; return SHEET_WEBPACK_REQUIRE; } } return null; } 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 getRevealViewStorageKey(app) { const base = String(getColumnOrderStorageKey(app) || '').trim(); const prefix = `${COL_ORDER_STORAGE_PREFIX}:`; const suffix = base.startsWith(prefix) ? base.slice(prefix.length) : base; return `${REVEAL_VIEW_STORAGE_PREFIX}:${suffix || location.pathname}`; } 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 normalizeRevealSortOrder(value) { return String(value || '').toLowerCase() === 'desc' ? 'desc' : 'asc'; } function normalizeRevealColWidth(value) { const n = Number(value); if (!Number.isFinite(n)) return null; return Math.max(REVEAL_COL_WIDTH_MIN, Math.min(REVEAL_COL_WIDTH_MAX, Math.round(n))); } function loadRevealViewConfig(key) { const fallback = { sortOrder: 'asc', colWidths: {} }; if (!key) return fallback; try { const raw = localStorage.getItem(key); if (!raw) return fallback; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return fallback; const outWidths = {}; const srcWidths = parsed.colWidths; if (srcWidths && typeof srcWidths === 'object') { for (const [k, v] of Object.entries(srcWidths)) { const keyText = String(k || '').trim(); if (!keyText) continue; const width = normalizeRevealColWidth(v); if (width == null) continue; outWidths[keyText] = width; } } return { sortOrder: normalizeRevealSortOrder(parsed.sortOrder), colWidths: outWidths }; } catch (_) { return fallback; } } function saveRevealViewConfig(key, config) { if (!key || !config || typeof config !== 'object') return; try { const srcWidths = config.colWidths && typeof config.colWidths === 'object' ? config.colWidths : {}; const outWidths = {}; for (const [k, v] of Object.entries(srcWidths)) { const keyText = String(k || '').trim(); if (!keyText) continue; const width = normalizeRevealColWidth(v); if (width == null) continue; outWidths[keyText] = width; } const out = { sortOrder: normalizeRevealSortOrder(config.sortOrder), colWidths: outWidths }; localStorage.setItem(key, JSON.stringify(out)); } catch (_) { } } function toFiniteInt(value) { const n = Number(value); if (!Number.isFinite(n)) return null; return Math.trunc(n); } function normalizeSheetId(value) { if (value == null) return ''; const s = String(value).trim(); return s; } function valueToText(value, depth = 0) { if (value == null || depth > 4) return ''; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (Array.isArray(value)) { for (const item of value) { const t = valueToText(item, depth + 1); if (t) return t; } return ''; } if (typeof value === 'object') { const keys = [ 'value', 'text', 'formulaResult', 'showValue', 'editValue', 'displayValue', 'formattedText', 'plainText', 'rawText', 'stringValue', 'str', 'cellValue', 'cellText', 'content', 'label' ]; for (const key of keys) { const t = valueToText(value[key], depth + 1); if (t) return t; } for (const [k, v] of Object.entries(value)) { if (keys.includes(k)) continue; const t = valueToText(v, depth + 1); if (t) return t; } } return ''; } function looksMaskedDisplayText(text) { const s = String(text || '').trim(); if (!s) return false; if (!/^[**·•xX#\s]+$/.test(s)) return false; return /[**·•xX#]/.test(s); } function pickSheetIdFromObject(obj) { if (!obj || typeof obj !== 'object') return ''; const candidates = [ obj.sheetId, obj.sheetID, obj.sheetid, obj.activeSheetId, obj.currentSheetId, obj.sheet?.sheetId, obj.sheet?.id, obj.range?.sheetId ]; for (const v of candidates) { const sid = normalizeSheetId(v); if (sid) return sid; } return ''; } function normalizeAliDocsCellPos(raw, fallbackSheetId = '') { if (!raw || typeof raw !== 'object') return null; const rowIndex = toFiniteInt(raw.rowIndex ?? raw.row ?? raw.r ?? raw.activeRowIndex); const colIndex = toFiniteInt(raw.colIndex ?? raw.col ?? raw.c ?? raw.activeColIndex); if (rowIndex == null || colIndex == null) return null; const sheetId = normalizeSheetId(pickSheetIdFromObject(raw) || fallbackSheetId); return { rowIndex, colIndex, sheetId }; } function normalizeAliDocsRange(raw, fallbackSheetId = '') { if (!raw || typeof raw !== 'object') return null; const startRowIndex = toFiniteInt(raw.startRowIndex ?? raw.rowIndex ?? raw.row ?? raw.r); const startColIndex = toFiniteInt(raw.startColIndex ?? raw.colIndex ?? raw.col ?? raw.c); if (startRowIndex == null || startColIndex == null) return null; const endRowRaw = raw.endRowIndex ?? raw.rowEndIndex ?? raw.endRow; const endColRaw = raw.endColIndex ?? raw.colEndIndex ?? raw.endCol; const endRowIndex = toFiniteInt(endRowRaw); const endColIndex = toFiniteInt(endColRaw); const safeEndRow = endRowIndex == null ? startRowIndex + 1 : Math.max(endRowIndex, startRowIndex + 1); const safeEndCol = endColIndex == null ? startColIndex + 1 : Math.max(endColIndex, startColIndex + 1); const sheetId = normalizeSheetId(pickSheetIdFromObject(raw) || fallbackSheetId); return { sheetId, startRowIndex, startColIndex, endRowIndex: safeEndRow, endColIndex: safeEndCol }; } function readHiddenFlag(node, depth = 0) { if (node == null || depth > 5) return null; if (typeof node === 'boolean') return node; if (typeof node === 'number') { if (node === 1) return true; if (node === 0) return false; return null; } if (typeof node === 'string') { const text = node.trim().toLowerCase(); if (text === 'true' || text === '1') return true; if (text === 'false' || text === '0') return false; return null; } if (Array.isArray(node)) { for (const item of node) { const v = readHiddenFlag(item, depth + 1); if (typeof v === 'boolean') return v; } return null; } if (typeof node === 'object') { const hiddenKeys = [ 'hidden', 'isHidden', 'hide', 'isHide', 'colHidden', 'columnHidden', 'hiddenByUser', 'isHiddenByUser' ]; for (const key of hiddenKeys) { if (!(key in node)) continue; const v = readHiddenFlag(node[key], depth + 1); if (typeof v === 'boolean') return v; } const widthKeys = ['width', 'colWidth', 'columnWidth']; for (const key of widthKeys) { const w = Number(node[key]); if (Number.isFinite(w) && w <= 0) return true; } for (const v of Object.values(node)) { const hidden = readHiddenFlag(v, depth + 1); if (typeof hidden === 'boolean') return hidden; } } return null; } function parseHiddenFromQueryCols(cols, targetCol) { if (!Array.isArray(cols) || !cols.length) return null; let fallback = null; const pickColIndex = (meta) => { if (!meta || typeof meta !== 'object') return null; const keys = ['colIndex', 'columnIndex', 'index', 'col', 'startColIndex']; for (const key of keys) { const idx = toFiniteInt(meta[key]); if (idx != null) return idx; } return null; }; for (const meta of cols) { const idx = pickColIndex(meta); const hidden = readHiddenFlag(meta); if (idx == null) { if (typeof hidden === 'boolean' && fallback == null) fallback = hidden; continue; } if (idx !== targetCol) continue; if (typeof hidden === 'boolean') return hidden; return false; } return fallback; } function normalizeAliDocsCell(rawCell) { if (!rawCell || typeof rawCell !== 'object') return null; const payload = rawCell?.payload && typeof rawCell.payload === 'object' ? rawCell.payload : {}; const showText = valueToText(payload.showValue); const editText = valueToText(payload.editValue); const rawText = valueToText(payload.value); const revealText = looksMaskedDisplayText(showText) && editText ? editText : (rawText || editText || showText); return { formattedValue: revealText ? { value: revealText } : '', value: { value: rawText || editText || showText || '', text: revealText || '' }, payload, extendedValue: rawCell?.extendedValue, __tmAliDocsRaw: rawCell, __tmAliDocsShowValue: showText, __tmAliDocsEditValue: editText, __tmAliDocsValue: rawText }; } function getReactFiberFromElement(el) { if (!el || typeof el !== 'object') return null; let keys = []; try { keys = Object.getOwnPropertyNames(el); } catch (_) { keys = []; } for (const key of keys) { const k = String(key || ''); if ( !k.startsWith('__reactFiber$') && !k.startsWith('__reactContainer$') && !k.startsWith('__reactInternalInstance$') ) continue; try { const fiber = el[key]; if (fiber && typeof fiber === 'object') return fiber; } catch (_) { } } return null; } function isAliDocsControllerCandidate(controller) { return !!controller && typeof controller.getActiveCell === 'function' && typeof controller.getSelections === 'function' && typeof controller.getSheetRowCount === 'function' && typeof controller.getSheetColCount === 'function' && typeof controller.getSheetCellValue === 'function'; } function extractControllerFromFiberNode(node) { if (!node || typeof node !== 'object') return null; const candidates = [ node?.memoizedProps?.props?.controller, node?.pendingProps?.props?.controller, node?.memoizedProps?.controller, node?.pendingProps?.controller, node?.stateNode?.props?.controller, node?.stateNode?.controller, node?.memoizedState?.element?.props?.controller ]; for (const controller of candidates) { if (isAliDocsControllerCandidate(controller)) return controller; } return null; } function findAliDocsControllerInDocument(doc) { if (!doc?.documentElement) return null; const seedFibers = []; const seenSeed = new Set(); const pushSeedFiber = (fiber) => { if (!fiber || typeof fiber !== 'object') return; if (seenSeed.has(fiber)) return; seenSeed.add(fiber); seedFibers.push(fiber); }; const seedElements = [ doc.getElementById('root'), doc.getElementById('app'), doc.body, doc.documentElement ].filter(Boolean); for (const el of seedElements) { const fiber = getReactFiberFromElement(el); if (fiber) pushSeedFiber(fiber); } if (!seedFibers.length) { let all = []; try { all = doc.querySelectorAll('*'); } catch (_) { all = []; } const limit = Math.min(all.length, 2400); for (let i = 0; i < limit; i += 1) { const fiber = getReactFiberFromElement(all[i]); if (fiber) pushSeedFiber(fiber); } } if (!seedFibers.length) return null; const queue = []; const seen = new Set(); const pushQueue = (fiber) => { if (!fiber || typeof fiber !== 'object') return; if (seen.has(fiber)) return; seen.add(fiber); queue.push(fiber); }; for (const fiber of seedFibers) pushQueue(fiber); const maxVisit = 50000; for (let i = 0; i < queue.length && i < maxVisit; i += 1) { const fiber = queue[i]; const controller = extractControllerFromFiberNode(fiber); if (controller) return controller; pushQueue(fiber.child); pushQueue(fiber.sibling); pushQueue(fiber.return); pushQueue(fiber.alternate); const stateNode = fiber.stateNode; if (stateNode instanceof Element) pushQueue(getReactFiberFromElement(stateNode)); } return null; } function buildAliDocsControllerContext(controller) { if (!isAliDocsControllerCandidate(controller)) return null; let cachedSheetId = ''; const resolveSheetId = () => { const activeCell = (() => { try { return controller.getActiveCell(); } catch (_) { return null; } })(); const fromActive = normalizeSheetId(activeCell?.sheetId); if (fromActive) { cachedSheetId = fromActive; return cachedSheetId; } let selections = null; try { selections = controller.getSelections(); } catch (_) { selections = null; } const list = Array.isArray(selections) ? selections : (selections ? [selections] : []); for (const item of list) { const sid = normalizeSheetId( item?.activeCell?.sheetId || item?.rangeSelections?.[0]?.sheetId || pickSheetIdFromObject(item) ); if (!sid) continue; cachedSheetId = sid; return cachedSheetId; } const fallback = normalizeSheetId( controller?.activeSheetId || controller?.currentSheetId || controller?.model?.activeSheetId || controller?.model?.currentSheetId || controller?.model?.sheetId ); if (fallback) cachedSheetId = fallback; return cachedSheetId; }; const hiddenCache = new Map(); const queryColHidden = (col0) => { const col = toFiniteInt(col0); if (col == null || col < 0) return false; const sheetId = resolveSheetId(); if (!sheetId) return false; const cacheKey = `${sheetId}:${col}`; if (hiddenCache.has(cacheKey)) return hiddenCache.get(cacheKey) === true; let hidden = null; try { const dim = controller?.dimensionController; if (dim && typeof dim.queryCols === 'function') { const cols = dim.queryCols(sheetId, col, 1); hidden = parseHiddenFromQueryCols(cols, col); } } catch (_) { hidden = null; } if (typeof hidden !== 'boolean') { try { hidden = readHiddenFlag(controller?.getShowStyle?.(sheetId, 0, col)); } catch (_) { hidden = null; } } const finalHidden = hidden === true; hiddenCache.set(cacheKey, finalHidden); return finalHidden; }; const buildSelectionSnapshot = () => { const sheetId = resolveSheetId(); let activeCell = normalizeAliDocsCellPos((() => { try { return controller.getActiveCell(); } catch (_) { return null; } })(), sheetId); let selections = null; try { selections = controller.getSelections(); } catch (_) { selections = null; } const entries = Array.isArray(selections) ? selections : (selections ? [selections] : []); const ranges = []; for (const entry of entries) { const rangeSelections = Array.isArray(entry?.rangeSelections) ? entry.rangeSelections : []; for (const r of rangeSelections) { const range = normalizeAliDocsRange(r, sheetId); if (range) ranges.push(range); } if (!activeCell) { activeCell = normalizeAliDocsCellPos(entry?.activeCell || entry, sheetId); } if (!ranges.length) { const range = normalizeAliDocsRange(entry?.rangeSelection || entry?.range, sheetId); if (range) ranges.push(range); } } if (!activeCell && ranges.length) { const r = ranges[0]; activeCell = { rowIndex: r.startRowIndex, colIndex: r.startColIndex, sheetId: normalizeSheetId(r.sheetId || sheetId) }; } if (activeCell && !ranges.length) { ranges.push({ sheetId: normalizeSheetId(activeCell.sheetId || sheetId), startRowIndex: activeCell.rowIndex, startColIndex: activeCell.colIndex, endRowIndex: activeCell.rowIndex + 1, endColIndex: activeCell.colIndex + 1 }); } return { activeCell: activeCell || { rowIndex: 0, colIndex: 0, sheetId: normalizeSheetId(sheetId) }, rangeSelections: ranges }; }; const selectionKeyOf = (payload) => { const active = payload?.activeCell || {}; const ranges = Array.isArray(payload?.rangeSelections) ? payload.rangeSelections : []; const activeKey = [ normalizeSheetId(active.sheetId), toFiniteInt(active.rowIndex) ?? '', toFiniteInt(active.colIndex) ?? '' ].join(':'); const rangeKey = ranges.map((r) => [ normalizeSheetId(r?.sheetId), toFiniteInt(r?.startRowIndex) ?? '', toFiniteInt(r?.startColIndex) ?? '', toFiniteInt(r?.endRowIndex) ?? '', toFiniteInt(r?.endColIndex) ?? '' ].join(':')).join('|'); return `${activeKey}::${rangeKey}`; }; let sheet = null; const app = { __tmProvider: 'alidocs-controller', __tmAliDocsController: controller, workbook: { get activeSheetId() { return resolveSheetId(); }, get activeSheet() { return sheet; } }, view: { getActiveCell() { return buildSelectionSnapshot().activeCell; }, getSelection() { return buildSelectionSnapshot(); }, getSelectionRanges() { return buildSelectionSnapshot().rangeSelections; }, onSelectionChange(callback) { if (typeof callback !== 'function') return () => { }; let lastKey = ''; const tick = () => { const payload = buildSelectionSnapshot(); const nextKey = selectionKeyOf(payload); if (nextKey === lastKey) return; lastKey = nextKey; try { callback(payload); } catch (_) { } }; tick(); const timer = setInterval(tick, ALIDOCS_SELECTION_POLL_MS); return () => clearInterval(timer); } } }; sheet = { __tmProvider: 'alidocs-controller', __tmAliDocsController: controller, getSheetId: resolveSheetId, getRowCount() { const sheetId = resolveSheetId(); if (!sheetId) return 0; try { const n = Number(controller.getSheetRowCount(sheetId)); return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0; } catch (_) { return 0; } }, getColCount() { const sheetId = resolveSheetId(); if (!sheetId) return 0; try { const n = Number(controller.getSheetColCount(sheetId)); return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0; } catch (_) { return 0; } }, getCellDataAtPosition(row0, col0) { const sheetId = resolveSheetId(); if (!sheetId) return null; const row = Math.max(0, toFiniteInt(row0) || 0); const col = Math.max(0, toFiniteInt(col0) || 0); try { return normalizeAliDocsCell(controller.getSheetCellValue(sheetId, row, col)); } catch (_) { return null; } }, isColHiddenByUser(col0) { return queryColHidden(col0); }, isColHidden(col0) { return queryColHidden(col0); }, getColProperty(col0) { const hidden = queryColHidden(col0); return hidden ? { hidden: true, isHidden: true } : {}; } }; return { app, sheet }; } function findAliDocsControllerContext() { const probes = []; const seen = new Set(); const pushProbe = (doc) => { if (!doc || seen.has(doc)) return; seen.add(doc); probes.push(doc); }; pushProbe(document); let iframes = []; try { iframes = document.querySelectorAll('iframe'); } catch (_) { iframes = []; } for (const frame of iframes) { try { pushProbe(frame?.contentDocument || null); } catch (_) { } } for (const doc of probes) { const controller = findAliDocsControllerInDocument(doc); const ctx = buildAliDocsControllerContext(controller); if (ctx) return ctx; } return null; } function isReadySheet(sheet) { return !!sheet && typeof sheet.getCellDataAtPosition === 'function' && typeof sheet.getRowCount === 'function' && typeof sheet.getColCount === 'function'; } function pickSpreadsheetContext(candidate) { if (!candidate || typeof candidate !== 'object') return null; const sheet = candidate?.workbook?.activeSheet; if (isReadySheet(sheet)) return { app: candidate, sheet }; return null; } function findSpreadsheetContext() { for (const key of SPREADSHEET_APP_HINT_KEYS) { let candidate = null; try { candidate = PAGE_WIN[key]; } catch (_) { candidate = null; } const ctx = pickSpreadsheetContext(candidate); if (ctx) return ctx; } const containerHints = ['__APP__', '__ALI_DOCS__', 'AliDocs', 'DingTalkDocs']; for (const name of containerHints) { let root = null; try { root = PAGE_WIN[name]; } catch (_) { root = null; } if (!root || typeof root !== 'object') continue; for (const key of SPREADSHEET_APP_HINT_KEYS) { let candidate = null; try { candidate = root[key]; } catch (_) { candidate = null; } const ctx = pickSpreadsheetContext(candidate); if (ctx) return ctx; } } let windowKeys = []; try { windowKeys = Object.getOwnPropertyNames(PAGE_WIN); } catch (_) { windowKeys = []; } for (const key of windowKeys) { if (!/(spreadsheet|sheet|workbook|grid)/i.test(String(key))) continue; let candidate = null; try { candidate = PAGE_WIN[key]; } catch (_) { candidate = null; } const ctx = pickSpreadsheetContext(candidate); if (ctx) return ctx; } const alidocsCtx = findAliDocsControllerContext(); if (alidocsCtx) return alidocsCtx; return null; } function waitForSpreadsheetApp(timeoutMs = 30000) { return new Promise((resolve, reject) => { const started = Date.now(); const timer = setInterval(() => { const context = findSpreadsheetContext(); if (context) { clearInterval(timer); resolve(context); return; } if (Date.now() - started > timeoutMs) { clearInterval(timer); reject(new Error('Spreadsheet runtime 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 letterToCol(letter) { const token = String(letter || '').trim().toUpperCase(); if (!/^[A-Z]+$/.test(token)) return -1; let n = 0; for (const ch of token) n = n * 26 + (ch.charCodeAt(0) - 64); return n - 1; } function parseColumnSelection(input, colCount) { const maxCols = Number(colCount); const limit = Number.isFinite(maxCols) ? Math.max(0, Math.trunc(maxCols)) : Infinity; const normalized = String(input || '').toUpperCase().replace(/[,、;;\s]+/g, ','); const parts = normalized.split(',').map((x) => x.trim()).filter(Boolean); const out = []; const seen = new Set(); const addCol = (col) => { if (!Number.isInteger(col) || col < 0 || col >= limit || seen.has(col)) return; seen.add(col); out.push(col); }; for (const part of parts) { const m = part.match(/^([A-Z]+)\s*[:\-~]\s*([A-Z]+)$/); if (m) { const left = letterToCol(m[1]); const right = letterToCol(m[2]); if (!Number.isInteger(left) || !Number.isInteger(right) || left < 0 || right < 0) continue; const from = Math.min(left, right); const to = Math.max(left, right); for (let c = from; c <= to; c += 1) addCol(c); continue; } addCol(letterToCol(part)); } return out; } function displayTextOfCell(cell) { if (!cell) return ''; const aliShowText = valueToText(cell?.__tmAliDocsShowValue || cell?.payload?.showValue); if (aliShowText) return aliShowText; 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.text != null) return String(v.text); if (v.formulaResult != null) return String(v.formulaResult); if (v.value != null) return String(v.value); const t = valueToText(v); if (t) return t; } const fallback = valueToText(cell?.payload || cell?.extendedValue || cell?.originData); if (fallback) return fallback; return ''; } function textOfCell(cell) { if (!cell) return ''; const aliShowText = valueToText(cell?.__tmAliDocsShowValue || cell?.payload?.showValue); const aliEditText = valueToText(cell?.__tmAliDocsEditValue || cell?.payload?.editValue); const aliRawText = valueToText(cell?.__tmAliDocsValue || cell?.payload?.value); if (looksMaskedDisplayText(aliShowText) && aliEditText) return aliEditText; if (aliRawText) return aliRawText; if (aliEditText) return aliEditText; 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); const nested = valueToText(v); if (nested) return nested; } const payloadText = valueToText(cell?.payload || cell?.extendedValue || cell?.originData); if (payloadText) return payloadText; return displayTextOfCell(cell); } function normalizeCellText(value) { return String(value == null ? '' : value) .replace(/[\u200B-\u200D\uFEFF]/g, '') .replace(/\u00A0/g, ' ') .trim(); } 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, workbook?.activeSheet?.getSheetId?.(), sheet?.sheetId, sheet?.sheetIdKey, sheet?.getSheetId?.(), 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 { if (app?.__tmProvider === 'alidocs-controller' || sheet?.__tmProvider === 'alidocs-controller') { return buildOpsUnavailableResult('当前文档暂不支持读取修订接口'); } 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); } else { collectDeep(ext); } // Last fallback: deep scan known cell payload sections. collectDeep(cell?.value); collectDeep(cell?.formattedValue); collectDeep(cell?.payload); collectDeep(cell?.__tmAliDocsRaw); 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 isRowHidden(sheet, row0) { const r = Math.max(0, Math.trunc(Number(row0) || 0)); try { if (typeof sheet.isRowHiddenByUser === 'function' && sheet.isRowHiddenByUser(r)) return true; } catch (_) { } try { if (typeof sheet.isRowHiddenByFilter === 'function' && sheet.isRowHiddenByFilter(r)) return true; } catch (_) { } try { if (typeof sheet.isRowHidden === 'function' && sheet.isRowHidden(r)) return true; } catch (_) { } try { if (typeof sheet.getRowProperty === 'function') { const p = sheet.getRowProperty(r); if (p && (p.hidden === true || p.isHidden === true)) return true; } } catch (_) { } return false; } function getCellTextAtPosition(sheet, row0, col0) { if (!sheet || !Number.isInteger(row0) || !Number.isInteger(col0) || row0 < 0 || col0 < 0) return ''; const cell = sheet.getCellDataAtPosition(row0, col0, CELL_READ_OPTIONS); if (!cell) return ''; const display = normalizeCellText(displayTextOfCell(cell)); if (display) return display; return normalizeCellText(textOfCell(cell)); } function isLikelyDataValue(text) { const s = normalizeCellText(text); if (!s) return false; if (/^https?:\/\//i.test(s)) return true; if (/^[-+]?\d+(?:[.,]\d+)?%?$/.test(s)) return true; if (/^\d{4}[\/.-]\d{1,2}[\/.-]\d{1,2}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(s)) return true; if (/^\d{1,2}:\d{2}(?::\d{2})?$/.test(s)) return true; if (/^(?:[A-Za-z]{0,3}\d{5,}|\d{5,}[A-Za-z]{0,3})$/.test(s)) return true; if (/^[\d\W_]+$/.test(s) && /\d/.test(s) && s.length >= 4) return true; return false; } function aliDocsHeaderRowPriority(row0) { if (row0 === 1) return 0; // 第2行优先 if (row0 === 2) return 1; // 其次第3行 if (row0 === 0) return 2; // 再次第1行 return 3 + Math.max(0, row0); } function detectAliDocsHeaderRow(sheet) { const rowCount = sheet.getRowCount?.() || 0; const colCount = sheet.getColCount?.() || 0; if (!rowCount || !colCount) return 0; const cache = ALIDOCS_HEADER_ROW_CACHE.get(sheet); const now = Date.now(); if ( cache && cache.rowCount === rowCount && cache.colCount === colCount && (now - cache.at) < ALIDOCS_HEADER_CACHE_TTL_MS && cache.nonEmpty >= 2 ) { return cache.row; } const maxRows = Math.min(rowCount, Math.max(1, ALIDOCS_HEADER_SCAN_MAX_ROWS)); const maxCols = Math.min(colCount, Math.max(1, ALIDOCS_HEADER_SCAN_MAX_COLS)); let best = null; for (let r0 = 0; r0 < maxRows; r0 += 1) { let nonEmpty = 0; let keywordHits = 0; let dataLike = 0; let shortText = 0; let longText = 0; for (let c0 = 0; c0 < maxCols; c0 += 1) { const text = getCellTextAtPosition(sheet, r0, c0); if (!text) continue; nonEmpty += 1; if (text.length <= 24) shortText += 1; if (text.length >= 40) longText += 1; if (isLikelyDataValue(text)) dataLike += 1; const compact = text.replace(/\s+/g, '').toLowerCase(); if (ALIDOCS_HEADER_KEYWORDS.some((kw) => compact.includes(String(kw).toLowerCase()))) { keywordHits += 1; } } let score = nonEmpty * 1.4 + keywordHits * 2.6 + shortText * 0.9 - dataLike * 1.3 - longText * 0.35; if (nonEmpty <= 1) score -= 2.5; const item = { row: r0, score, nonEmpty, keywordHits }; if (!best) { best = item; continue; } if (item.score > best.score + 0.0001) { best = item; continue; } if (Math.abs(item.score - best.score) <= 0.0001) { if (item.keywordHits > best.keywordHits) { best = item; continue; } if (item.keywordHits === best.keywordHits && aliDocsHeaderRowPriority(item.row) < aliDocsHeaderRowPriority(best.row)) { best = item; } } } const fallbackRow = rowCount > 1 ? 1 : 0; const bestRow = Number.isInteger(best?.row) ? best.row : fallbackRow; const safeRow = Math.max(0, Math.min(rowCount - 1, bestRow)); ALIDOCS_HEADER_ROW_CACHE.set(sheet, { row: safeRow, rowCount, colCount, nonEmpty: Number(best?.nonEmpty || 0), at: now }); return safeRow; } function getDataStartRow(sheet) { const rowCount = sheet.getRowCount?.() || 0; if (rowCount <= 1) return 0; if (isAliDocsRuntime(null, sheet)) { const headerRow = detectAliDocsHeaderRow(sheet); return Math.max(1, Math.min(rowCount - 1, headerRow + 1)); } return rowCount > 2 ? 2 : 1; } function getHeaderText(sheet, col) { // 腾讯默认优先第1行;钉钉自动识别表头行(第2/3行优先)。 const rowCount = sheet.getRowCount?.() || 0; if (!rowCount) return ''; const isAliDocsSheet = isAliDocsRuntime(null, sheet); let rowCandidates = []; if (isAliDocsSheet) { const preferredRow = detectAliDocsHeaderRow(sheet); rowCandidates = [ preferredRow, preferredRow - 1, preferredRow + 1, 1, 2, 0, 3, 4, 5 ]; } else { rowCandidates = rowCount > 1 ? [0, 1] : [0]; } const seen = new Set(); for (const r of rowCandidates) { if (!Number.isInteger(r) || r < 0 || r >= rowCount || seen.has(r)) continue; seen.add(r); const t = getCellTextAtPosition(sheet, r, col); 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 isAliDocsRuntime(app, sheet) { if (app?.__tmProvider === 'alidocs-controller' || sheet?.__tmProvider === 'alidocs-controller') return true; return location.hostname === 'alidocs.dingtalk.com'; } function getDefaultTargetColsByRuntime(app, sheet) { return isAliDocsRuntime(app, sheet) ? DINGTALK_DEFAULT_TARGET_COLS : DEFAULT_TARGET_COLS; } function detectHiddenColumns(sheet) { const colCount = sheet.getColCount?.() || 0; if (!colCount) return []; const out = []; for (let c = 0; c < colCount; c += 1) { if (!isColHidden(sheet, c)) continue; out.push(c); } return out; } function filterColumnsWithContent(sheet, cols, options = {}) { const normalized = getTrackedColumns(sheet, cols); if (!normalized.length) return []; const rowCount = sheet.getRowCount?.() || 0; if (rowCount <= 1) return normalized.slice(); const maxScanRows = Number(options?.maxScanRows); const maxScanCells = Number(options?.maxScanCells); 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, normalized.length)) ); const startRow = Math.max(0, Math.min(rowCount - 1, getDataStartRow(sheet))); const endRow = Math.min(endByRows, startRow + rowBudget); const out = []; for (const c of normalized) { let hasContent = false; for (let r0 = startRow; r0 < endRow; r0 += 1) { const cell = sheet.getCellDataAtPosition(r0, c, CELL_READ_OPTIONS); if (!cell) continue; const value = normalizeCellText(textOfCell(cell)); const display = normalizeCellText(displayTextOfCell(cell)); const urls = extractCellUrls(cell); if (urls.length || value || display) { hasContent = true; break; } } if (hasContent) out.push(c); } return out; } function hasAnyContentInRow(sheet, cols, row0) { const rowCount = sheet.getRowCount?.() || 0; if (!rowCount) return false; const row = Math.max(0, Math.min(rowCount - 1, Math.trunc(Number(row0) || 0))); const tracked = getTrackedColumns(sheet, cols); for (const c of tracked) { const cell = sheet.getCellDataAtPosition(row, c, CELL_READ_OPTIONS); if (!cell) continue; if (extractCellUrls(cell).length) return true; if (normalizeCellText(textOfCell(cell))) return true; if (normalizeCellText(displayTextOfCell(cell))) return true; } return false; } 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; const startRow = Math.max(0, Math.min(rowCount - 1, getDataStartRow(sheet))); if (!colCount || rowCount <= startRow) 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, startRow + rowBudget); const starCols = new Set(); const fallbackCols = new Set(); for (let r0 = startRow; r0 < endRow; r0 += 1) { for (let c0 = 0; c0 < colCount; c0 += 1) { if (starCols.has(c0)) continue; const cell = sheet.getCellDataAtPosition(r0, c0, CELL_READ_OPTIONS); const displayText = normalizeCellText(displayTextOfCell(cell)); const revealText = normalizeCellText(textOfCell(cell)); const hasStar = /^[**]+$/.test(displayText); const hasMaskedDisplay = looksMaskedDisplayText(displayText); const hasMaskedReveal = hasMaskedDisplay && revealText && revealText !== displayText; if (hasStar || hasMaskedDisplay) { starCols.add(c0); continue; } if (hasMaskedReveal) { 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 && revealText === ''; 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'); const link = viewer.querySelector('.tm-img-viewer-link'); if (link) { link.setAttribute('href', ''); link.classList.add('hidden'); } 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'); const link = viewer.querySelector('.tm-img-viewer-link'); if (!img) return; if (viewer.classList.contains('hidden')) { viewer.dataset.prevOverflow = document.documentElement.style.overflow || ''; document.documentElement.style.overflow = 'hidden'; } img.setAttribute('src', url); if (link) { link.setAttribute('href', url); link.classList.remove('hidden'); } 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; position: relative; padding: 10px 34px 10px 12px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; } #${PANEL_ID} .title { font-weight: 600; } #${PANEL_ID} .panel-close-btn { position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; line-height: 20px; padding: 0; border-radius: 11px; font-size: 16px; font-weight: 700; text-align: center; } #${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%; max-width: 100%; border-collapse: collapse; table-layout: fixed; } #${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); max-width: var(--tm-col-letter-width, 56px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; word-break: normal; overflow-wrap: 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); min-width: 0; overflow: hidden; 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; flex-wrap: wrap; gap: 6px; padding: 8px; background: #f8fafc; } #${PANEL_ID} .col-actions button { padding: 3px 8px; } #${PANEL_ID} .col-manual-modal { position: fixed; inset: 0; z-index: 2147483646; display: flex; align-items: center; justify-content: center; } #${PANEL_ID} .col-manual-modal[hidden] { display: none; } #${PANEL_ID} .col-manual-mask { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.45); } #${PANEL_ID} .col-manual-card { position: relative; z-index: 1; width: min(420px, calc(100vw - 24px)); box-sizing: border-box; background: #ffffff; border: 1px solid #d9e2ec; border-radius: 10px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.3); padding: 14px; display: grid; gap: 10px; } #${PANEL_ID} .col-manual-title { font-size: 14px; font-weight: 600; color: #0f172a; } #${PANEL_ID} .col-manual-tip { font-size: 12px; color: #64748b; } #${PANEL_ID} .col-manual-input { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 7px 10px; font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #0f172a; outline: none; } #${PANEL_ID} .col-manual-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.18); } #${PANEL_ID} .col-manual-error { min-height: 16px; font-size: 12px; color: #b42318; } #${PANEL_ID} .col-manual-actions { display: flex; justify-content: flex-end; gap: 8px; } #${PANEL_ID} .col-manual-actions button { min-width: 64px; padding: 6px 12px; } #${PANEL_ID} .tm-reveal-modal { position: fixed; inset: 0; z-index: 2147483646; display: flex; align-items: center; justify-content: center; } #${PANEL_ID} .tm-reveal-modal[hidden] { display: none; } #${PANEL_ID} .tm-reveal-mask { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.62); } #${PANEL_ID} .tm-reveal-card { position: relative; z-index: 1; width: min(96vw, 1320px); max-height: 90vh; display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 8px; padding: 12px; border-radius: 10px; border: 1px solid #cbd5e1; background: #ffffff; box-shadow: 0 20px 44px rgba(15, 23, 42, 0.35); } #${PANEL_ID} .tm-reveal-head { display: flex; flex-direction: column; gap: 2px; } #${PANEL_ID} .tm-reveal-title { font-size: 14px; font-weight: 600; color: #0f172a; } #${PANEL_ID} .tm-reveal-meta { font-size: 12px; color: #475569; min-height: 16px; } #${PANEL_ID} .tm-reveal-actions { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; } #${PANEL_ID} .tm-reveal-wrap { overflow: auto; border: 1px solid #e2e8f0; border-radius: 8px; background: #ffffff; } #${PANEL_ID} .tm-reveal-table { width: max-content; min-width: 100%; border-collapse: collapse; table-layout: auto; } #${PANEL_ID} .tm-reveal-table th, #${PANEL_ID} .tm-reveal-table td { border: 1px solid #e2e8f0; padding: 4px 6px; vertical-align: top; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; max-width: none; } #${PANEL_ID} .tm-reveal-table thead th { position: sticky; top: 0; z-index: 2; background: #f8fafc; font-weight: 600; padding-right: 12px; user-select: none; } #${PANEL_ID} .tm-reveal-table .tm-reveal-head-label { display: block; min-height: 18px; } #${PANEL_ID} .tm-reveal-table .tm-reveal-sort-btn { appearance: none; border: none; background: transparent; padding: 0; margin: 0; color: inherit; font: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; } #${PANEL_ID} .tm-reveal-table .tm-reveal-sort-indicator { color: #2563eb; font-size: 11px; line-height: 1; } #${PANEL_ID} .tm-reveal-table .tm-reveal-resize-handle { position: absolute; top: 0; right: -4px; width: 8px; height: 100%; cursor: col-resize; z-index: 3; } #${PANEL_ID} .tm-reveal-table .tm-reveal-resize-handle::after { content: ''; position: absolute; top: 18%; bottom: 18%; left: 50%; width: 1px; background: #cbd5e1; transform: translateX(-50%); } #${PANEL_ID}.reveal-resizing, #${PANEL_ID}.reveal-resizing * { cursor: col-resize !important; } #${PANEL_ID} .tm-reveal-table td.flag { white-space: nowrap; } #${PANEL_ID} .tm-reveal-table td.flag .tag { display: inline-block; margin-right: 4px; border-radius: 10px; padding: 0 6px; line-height: 18px; font-size: 11px; border: 1px solid #cbd5e1; background: #f8fafc; color: #334155; } #${PANEL_ID} .tm-reveal-table td.flag .tag.hidden { border-color: #f59e0b; background: #fffbeb; color: #92400e; } #${PANEL_ID} .tm-reveal-table td.flag .tag.masked { border-color: #2563eb; background: #eff6ff; color: #1e40af; } #${PANEL_ID} .tm-reveal-table .tm-reveal-media-link { display: inline-flex; align-items: center; justify-content: center; border: 1px solid #d9e2ec; border-radius: 6px; overflow: hidden; text-decoration: none; background: #ffffff; } #${PANEL_ID} .tm-reveal-table .tm-reveal-thumb { display: block; width: auto; height: 24px; max-height: 24px; object-fit: contain; background: #f8fafc; } #${PANEL_ID} .tm-reveal-table .tm-reveal-cell-link { color: #2563eb; text-decoration: underline; text-underline-offset: 2px; word-break: break-all; } #${PANEL_ID} .tm-reveal-table .tm-reveal-cell-text { margin-top: 2px; color: #334155; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; } #${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; } #${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-card { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; max-width: 92vw; } #${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-link { font-size: 11px; line-height: 1.2; color: #e2e8f0; text-decoration: underline; text-underline-offset: 2px; } #${IMAGE_VIEWER_ID} .tm-img-viewer-link:hover { color: #ffffff; } #${IMAGE_VIEWER_ID} .tm-img-viewer-link.hidden { display: none; } #${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 tryExtractColFromSelectionPayload(payload, maxCol) { if (!Number.isInteger(maxCol) || maxCol <= 0) return null; const seen = new WeakSet(); const weightedCols = []; function pushCol(v, weight) { if (typeof v !== 'number') return; if (!Number.isFinite(v)) return; const col = Math.trunc(v); if (col < 0 || col >= maxCol) return; weightedCols.push({ col, 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 = ['col', 'colIndex', 'startCol', 'startColIndex', 'c']; for (const k of strongKeys) pushCol(node[k], 4); const weakKeys = ['endCol', 'endColIndex']; for (const k of weakKeys) pushCol(node[k], 2); for (const k of keys) { const v = node[k]; if (typeof v === 'number') { if (/col|column/i.test(k)) pushCol(v, 1); } else if (v && typeof v === 'object') { walk(v, depth + 1); } } } walk(payload, 0); if (!weightedCols.length) return null; const score = new Map(); for (const { col, weight } of weightedCols) { score.set(col, (score.get(col) || 0) + weight); } let bestCol = null; let bestScore = -1; for (const [col, s] of score.entries()) { if (s > bestScore) { bestScore = s; bestCol = col; } } return bestCol; } 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 tryExtractColFromView(app, maxCol) { const pickCol = (v) => { if (typeof v !== 'number' || !Number.isFinite(v)) return null; const col = Math.trunc(v); if (col < 0 || col >= maxCol) return null; return col; }; try { const col = pickCol(app?.view?.getActiveCell?.()?.colIndex); if (typeof col === 'number') return col; } catch (_) { } try { const sel = app?.view?.getSelection?.(); const col = pickCol(sel?.activeCell?.colIndex) ?? pickCol(sel?.rangeSelections?.[0]?.startColIndex) ?? pickCol(sel?.rangeSelections?.[0]?.colIndex); if (typeof col === 'number') return col; } catch (_) { } try { const ranges = app?.view?.getSelectionRanges?.(); const col = pickCol(ranges?.[0]?.startColIndex) ?? pickCol(ranges?.[0]?.colIndex); if (typeof col === 'number') return col; } 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 btnRevealBoot = panel.querySelector('#tm-reveal-all'); const btnToggleBoot = panel.querySelector('#tm-toggle'); const btnDetectBoot = panel.querySelector('#tm-col-detect'); const btnColManualBoot = panel.querySelector('#tm-col-manual'); const btnColAllBoot = panel.querySelector('#tm-col-all'); const btnColClearBoot = panel.querySelector('#tm-col-clear'); let initialized = false; let doRefresh = () => { tryInit(); }; let doRevealAll = () => { tryInit(); }; let doDetectCols = () => { tryInit(); }; let doManualSelectCols = () => { }; 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()); btnRevealBoot?.addEventListener('click', () => doRevealAll()); btnToggleBoot?.addEventListener('click', () => doToggle()); btnDetectBoot?.addEventListener('click', () => doDetectCols()); btnColManualBoot?.addEventListener('click', () => doManualSelectCols()); 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 colManualModal = panel.querySelector('#tm-col-manual-modal'); const colManualInput = panel.querySelector('#tm-col-manual-input'); const colManualError = panel.querySelector('#tm-col-manual-error'); const colManualCancel = panel.querySelector('#tm-col-manual-cancel'); const colManualOk = panel.querySelector('#tm-col-manual-ok'); const btnReveal = panel.querySelector('#tm-reveal-all'); const revealModal = panel.querySelector('#tm-reveal-modal'); const revealMeta = panel.querySelector('#tm-reveal-meta'); const revealTable = panel.querySelector('#tm-reveal-table'); const revealWrap = panel.querySelector('#tm-reveal-wrap'); const revealClose = panel.querySelector('#tm-reveal-close'); const revealCopy = panel.querySelector('#tm-reveal-copy'); 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 pendingSelectionCol0 = null; let refreshOpsForCurrentRow = () => { }; let colHeaderRetryTimer = 0; let tryScheduleColHeaderRetry = () => { }; let revealLastTSV = ''; let revealSeq = 0; let revealRenderState = null; let revealSortOrder = 'asc'; let revealColWidths = {}; let revealResizeSession = null; const colOrderStorageKey = getColumnOrderStorageKey(app); const revealViewStorageKey = getRevealViewStorageKey(app); const revealSavedConfig = loadRevealViewConfig(revealViewStorageKey); revealSortOrder = normalizeRevealSortOrder(revealSavedConfig.sortOrder); revealColWidths = revealSavedConfig.colWidths || {}; 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 isManualDialogOpen = () => !!colManualModal && !colManualModal.hidden; const setManualDialogOpen = (open, preset = '') => { if (!colManualModal) return; const shouldOpen = !!open; colManualModal.hidden = !shouldOpen; if (!shouldOpen) return; if (colManualError) colManualError.textContent = ''; if (!colManualInput) return; colManualInput.value = String(preset || ''); requestAnimationFrame(() => { colManualInput.focus(); colManualInput.select(); }); }; const isRevealDialogOpen = () => !!revealModal && !revealModal.hidden; const setRevealDialogOpen = (open) => { if (!revealModal) return; const shouldOpen = !!open; if (!shouldOpen) { revealSeq += 1; stopRevealResize(false); } revealModal.hidden = !shouldOpen; }; const compactCellText = (text) => normalizeCellText(text || ''); const tsvCell = (text) => String(text == null ? '' : text).replace(/\t/g, ' ').replace(/\r?\n/g, ' '); const persistRevealView = () => { saveRevealViewConfig(revealViewStorageKey, { sortOrder: revealSortOrder, colWidths: revealColWidths }); }; const getRevealDefaultColWidth = (key) => { if (key === REVEAL_ROW_COL_KEY) return REVEAL_DEFAULT_ROW_COL_WIDTH; return REVEAL_DEFAULT_DATA_COL_WIDTH; }; const getRevealColWidth = (key) => { const stored = normalizeRevealColWidth(revealColWidths?.[key]); if (stored != null) return stored; return getRevealDefaultColWidth(key); }; const getRevealColStyle = (key) => { const width = getRevealColWidth(key); return `width:${width}px;min-width:${width}px;max-width:${width}px;`; }; const updateRevealColWidthInDom = (key, width) => { if (!revealTable) return; const normalized = normalizeRevealColWidth(width); if (normalized == null) return; const cols = revealTable.querySelectorAll('col[data-reveal-key]'); for (const col of cols) { if (!(col instanceof HTMLElement)) continue; if (String(col.dataset.revealKey || '') !== key) continue; col.style.width = `${normalized}px`; col.style.minWidth = `${normalized}px`; col.style.maxWidth = `${normalized}px`; } }; const getSortedRevealRows = (rows) => { const list = Array.isArray(rows) ? rows.slice() : []; list.sort((a, b) => { const diff = (Number(a?.row1) || 0) - (Number(b?.row1) || 0); if (diff === 0) return 0; return revealSortOrder === 'desc' ? -diff : diff; }); return list; }; const renderRevealCellHtml = (cellData) => { const item = cellData || {}; const text = compactCellText(item.text || ''); const urls = Array.isArray(item.urls) ? item.urls : []; const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls : []; const imageSet = new Set(imageUrls); const linkUrls = urls.filter((u) => !imageSet.has(u)); let html = ''; if (imageUrls.length) { const thumbs = imageUrls.map((u) => { const safe = escapeHtml(u); return ``; }).join(''); html += `
${thumbs}
`; } if (linkUrls.length) { html += linkUrls.map((u) => { const safe = escapeHtml(u); return `${safe}`; }).join('
'); } const shouldShowText = text && text !== urls.join(' | '); if (shouldShowText) html += `
${escapeHtml(text)}
`; if (html) return html; return text ? escapeHtml(text) : ''; }; const renderRevealTableFromState = (options = {}) => { if (!revealTable || !revealRenderState) return; const keepScroll = options?.keepScroll === true; const prevScrollTop = keepScroll ? (revealWrap?.scrollTop || 0) : 0; const prevScrollLeft = keepScroll ? (revealWrap?.scrollLeft || 0) : 0; const colDefs = [ { key: REVEAL_ROW_COL_KEY, label: '行号' }, ...(Array.isArray(revealRenderState.colDefs) ? revealRenderState.colDefs : []) ]; const rows = getSortedRevealRows(revealRenderState.rows); const arrow = revealSortOrder === 'desc' ? '↓' : '↑'; const colgroupHtml = `${colDefs .map((col) => ``) .join('')}`; const theadHtml = `${colDefs.map((col) => { if (col.key === REVEAL_ROW_COL_KEY) { return ``; } return `${escapeHtml(col.label)}`; }).join('')}`; const tbodyHtml = `${rows.map((row) => { const cells = (Array.isArray(row.values) ? row.values : []) .map((v) => `${renderRevealCellHtml(v)}`) .join(''); return `${row.row1}${cells}`; }).join('')}`; revealTable.innerHTML = `${colgroupHtml}${theadHtml}${tbodyHtml}`; if (keepScroll && revealWrap) { revealWrap.scrollTop = prevScrollTop; revealWrap.scrollLeft = prevScrollLeft; } const tsvHeader = colDefs.map((col) => col.label).join('\t'); const tsvRows = rows.map((row) => { return [row.row1, ...(Array.isArray(row.values) ? row.values.map((v) => v?.exportText || '') : [])] .map(tsvCell) .join('\t'); }); revealLastTSV = [tsvHeader, ...tsvRows].join('\n'); }; function stopRevealResize(shouldPersist = true) { if (!revealResizeSession) return; document.documentElement.style.userSelect = revealResizeSession.prevUserSelect || ''; panel.classList.remove('reveal-resizing'); revealResizeSession = null; if (shouldPersist) persistRevealView(); } const onRevealResizeMove = (e) => { if (!revealResizeSession) return; const delta = Number(e?.clientX || 0) - revealResizeSession.startX; const nextWidth = normalizeRevealColWidth(revealResizeSession.startWidth + delta); if (nextWidth == null) return; revealColWidths[revealResizeSession.key] = nextWidth; updateRevealColWidthInDom(revealResizeSession.key, nextWidth); }; const onRevealResizeEnd = () => stopRevealResize(true); document.addEventListener('mousemove', onRevealResizeMove, true); document.addEventListener('mouseup', onRevealResizeEnd, true); const normalizeRevealHeaderKey = (text) => ( String(text == null ? '' : text) .replace(/\s+/g, '') .toLowerCase() ); const revealHeaderBaseKey = (text) => { const normalized = normalizeRevealHeaderKey(text); if (!normalized) return ''; return normalized .replace(/[0-90-9]+/g, '') .replace(/[一二三四五六七八九十百千万两〇零]+/g, '') .replace(/[()()【】\[\]{}<>《》「」『』\-_::,.,。·•]/g, ''); }; const buildRevealColumnSet = () => { const rowCount = sheet.getRowCount?.() || 0; const colCount = sheet.getColCount?.() || 0; if (!rowCount || !colCount) { return { cols: [], hiddenSet: new Set(), maskedSet: new Set(), fallbackSet: new Set(), popupSync: false, truncatedMaskScan: false }; } const hiddenCols = detectHiddenColumns(sheet); const hiddenSet = new Set(hiddenCols); const totalCells = Math.max(1, rowCount * Math.max(1, colCount)); const maxMaskCells = Math.min(3_000_000, Math.max(totalCells, DEEP_SCAN_MAX_CELLS)); const maskedCols = detectMaskedColumns(sheet, { maxScanRows: rowCount, maxScanCells: maxMaskCells, includeFallbackSignals: true }); const maskedSet = new Set(maskedCols); const popupCols = (() => { 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 popupSync = popupCols.length > 0; // 全表列与面板“显示列”同步;若未同步则按识别结果回退。 let cols = popupSync ? popupCols.slice() : getTrackedColumns(sheet, maskedCols); if (!cols.length) { const runtimeDefaultCols = getTrackedColumns(sheet, getDefaultTargetColsByRuntime(app, sheet)); cols = runtimeDefaultCols.slice(); } // 先剔除整列无值列,避免弹窗里出现大量空列。 if (cols.length) { const scanCells = Math.max(DEEP_SCAN_MAX_CELLS, rowCount * Math.max(1, cols.length)); cols = filterColumnsWithContent(sheet, cols, { maxScanRows: rowCount, maxScanCells: scanCells }); } const fallbackSet = new Set(); // 与面板列同步时,如果某些同类列被选中但无值,自动补全同名且有值的列。 if (popupSync) { const nonEmptySelectedCols = new Set(cols); const emptyDetectedCols = popupCols.filter((c) => !nonEmptySelectedCols.has(c)); const fallbackBases = new Set(); for (const c of emptyDetectedCols) { const base = revealHeaderBaseKey(getHeaderText(sheet, c)); if (base) fallbackBases.add(base); } if (fallbackBases.size) { const baseCandidates = []; const existingSet = new Set(cols); for (let c = 0; c < colCount; c += 1) { if (existingSet.has(c)) continue; const base = revealHeaderBaseKey(getHeaderText(sheet, c)); if (!base || !fallbackBases.has(base)) continue; baseCandidates.push(c); } if (baseCandidates.length) { const candidateScanCells = Math.max(DEEP_SCAN_MAX_CELLS, rowCount * Math.max(1, baseCandidates.length)); const contentCandidates = filterColumnsWithContent(sheet, baseCandidates, { maxScanRows: rowCount, maxScanCells: candidateScanCells }); for (const c of contentCandidates) { if (existingSet.has(c)) continue; existingSet.add(c); fallbackSet.add(c); cols.push(c); } } } } const truncatedMaskScan = maxMaskCells < totalCells; return { cols, hiddenSet, maskedSet, fallbackSet, popupSync, truncatedMaskScan }; }; const buildRevealTable = async () => { const seq = ++revealSeq; const rowCount = sheet.getRowCount?.() || 0; const colCount = sheet.getColCount?.() || 0; if (!rowCount || !colCount) { return { ok: false, message: '当前表格为空,无法生成全表视图' }; } if (revealMeta) revealMeta.textContent = '正在识别隐藏列与*列...'; const { cols, hiddenSet, maskedSet, fallbackSet, popupSync, truncatedMaskScan } = buildRevealColumnSet(); if (!cols.length) { return { ok: false, message: '未识别到可显示列(已按弹窗显示列同步)' }; } const dataStartRow = getDataStartRow(sheet); const colLabels = cols.map((c) => { const h = getHeaderText(sheet, c); const tags = []; if (hiddenSet.has(c)) tags.push('隐藏'); if (maskedSet.has(c)) tags.push('*'); if (fallbackSet.has(c)) tags.push('补全'); const tag = tags.length ? `[${tags.join('+')}]` : ''; return `${tag}${colToLetter(c)}${h ? ` (${h})` : ''}`; }); const rows = []; const totalRows = Math.max(0, rowCount - dataStartRow); let hiddenRowCount = 0; let maskedRowCount = 0; const flushEvery = 40; const quickCellUrlText = (cell) => { const ext = String(cell?.extendedValue || ''); if (ext) { const m = ext.match(/https?:\/\/[^\s"'\\\],]+/i); if (m && m[0]) return m[0]; } const vHyper = String(cell?.value?.hyperlink?.url || '').trim(); if (vHyper) return vHyper; const fHyper = String(cell?.formattedValue?.hyperlink?.url || '').trim(); if (fHyper) return fHyper; return ''; }; const normalizeUrlText = (url) => { const raw = String(url == null ? '' : url).trim(); if (!raw) return ''; const trimmed = raw.replace(/[),.;]+$/, ''); if (!/^(https?:\/\/|drive:\/\/)/i.test(trimmed)) return ''; return trimmed; }; const collectTextUrls = (text) => { const out = []; const seen = new Set(); const push = (value) => { const normalized = normalizeUrlText(value); if (!normalized || seen.has(normalized)) return; seen.add(normalized); out.push(normalized); }; const source = String(text == null ? '' : text); if (!source) return out; const re = /(drive:\/\/[^\s"'\\\],]+|https?:\/\/[^\s"'\\\],]+)/gi; let m = null; while ((m = re.exec(source)) !== null) { if (m[1]) push(m[1]); } return out; }; const collectCellUrls = (cell, revealText, displayText) => { const out = []; const seen = new Set(); const push = (value) => { const normalized = normalizeUrlText(value); if (!normalized || seen.has(normalized)) return; seen.add(normalized); out.push(normalized); }; for (const u of extractCellUrls(cell)) push(u); push(quickCellUrlText(cell)); for (const u of collectTextUrls(revealText)) push(u); for (const u of collectTextUrls(displayText)) push(u); return out; }; const buildRevealCell = (cell) => { const display = compactCellText(displayTextOfCell(cell)); const reveal = compactCellText(textOfCell(cell)); const urls = collectCellUrls(cell, reveal, display); const imageUrls = urls.filter(isPreviewableImageUrl); const textSource = reveal ? 'reveal' : (display ? 'display' : 'urls'); const finalText = reveal || display || (urls.length ? urls.join(' | ') : ''); const text = compactCellText(finalText); const hasDriveUrls = urls.some((u) => /^drive:\/\//i.test(String(u))); return { text, exportText: text || (urls.length ? urls.join(' | ') : ''), urls, imageUrls, isMasked: looksMaskedDisplayText(display) && reveal && reveal !== display, textSource, hasDriveUrls }; }; const resolveRevealCellUrls = async (entry) => { const item = entry || {}; const urls = Array.isArray(item.urls) ? item.urls : []; if (!urls.length || !urls.some((u) => /^drive:\/\//i.test(String(u)))) return item; const resolvedUrls = await resolveUrls(urls); const nextImageUrls = resolvedUrls.filter(isPreviewableImageUrl); let nextText = compactCellText(item.text || ''); let nextExportText = compactCellText(item.exportText || ''); if (item.textSource === 'urls') { nextText = compactCellText(resolvedUrls.join(' | ')); nextExportText = nextText || (resolvedUrls.length ? resolvedUrls.join(' | ') : ''); } else if (!nextExportText && resolvedUrls.length) { nextExportText = resolvedUrls.join(' | '); } return { ...item, urls: resolvedUrls, imageUrls: nextImageUrls, text: nextText, exportText: nextExportText, hasDriveUrls: false }; }; for (let r0 = dataStartRow; r0 < rowCount; r0 += 1) { if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' }; if ((r0 - dataStartRow) % flushEvery === 0) { if (revealMeta) revealMeta.textContent = `正在扫描第 ${r0 - dataStartRow + 1}/${Math.max(1, totalRows)} 行...`; await sleep(0); } const rowHidden = isRowHidden(sheet, r0); if (rowHidden) hiddenRowCount += 1; let rowMasked = false; let rowHasValue = false; const values = []; for (const c of cols) { const cell = sheet.getCellDataAtPosition(r0, c, CELL_READ_OPTIONS); let entry = buildRevealCell(cell); if (entry.hasDriveUrls) { entry = await resolveRevealCellUrls(entry); if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' }; } if (!rowMasked && entry.isMasked) rowMasked = true; if (!rowHasValue && compactCellText(entry.exportText)) rowHasValue = true; values.push(entry); } if (rowMasked) maskedRowCount += 1; if (!rowHasValue && !rowHidden && !rowMasked) { continue; } rows.push({ row1: r0 + 1, rowHidden, rowMasked, values }); } if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' }; if (!rows.length) { return { ok: false, message: '已识别到目标列,但目标行无可显示内容' }; } revealRenderState = { colDefs: cols.map((c, idx) => ({ key: `c:${c}`, label: String(colLabels[idx] || colToLetter(c) || '') })), rows }; renderRevealTableFromState({ keepScroll: false }); const extraNotes = []; if (truncatedMaskScan) extraNotes.push('*列扫描已按上限截断'); if (popupSync) extraNotes.push('列已与弹窗显示列同步'); if (fallbackSet.size) extraNotes.push(`已补全 ${fallbackSet.size} 个同类有值列`); const extraNote = extraNotes.length ? `(${extraNotes.join(';')})` : ''; return { ok: true, rows: rows.length, cols: cols.length, hiddenRows: hiddenRowCount, maskedRows: maskedRowCount, meta: `已显示 ${rows.length} 行 / ${cols.length} 列;隐藏行 ${hiddenRowCount},*行 ${maskedRowCount}${extraNote}` }; }; 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 ensureCandidateCols = (cols) => { const normalized = getTrackedColumns(sheet, cols); if (!normalized.length) return; const set = new Set(candidateCols); let changed = false; for (const c of normalized) { if (set.has(c)) continue; set.add(c); candidateCols.push(c); changed = true; } if (!changed) return; candidateCols = applySavedOrder(candidateCols); }; 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 runtimeDefaultCols = getTrackedColumns(sheet, getDefaultTargetColsByRuntime(app, sheet)); const isAliDocs = isAliDocsRuntime(app, sheet); const useAliDocsManualDefault = isAliDocs && mode !== 'deep'; if (useAliDocsManualDefault) { candidateCols = runtimeDefaultCols; } else { const hiddenCols = filterColumnsWithContent(sheet, detectHiddenColumns(sheet), { maxScanRows: DEEP_SCAN_MAX_ROWS, maxScanCells: DEEP_SCAN_MAX_CELLS }); const appealCols = detectAppealBlockColumns(sheet); if (hiddenCols.length) { candidateCols = hiddenCols; } else 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 : runtimeDefaultCols; } else { const quickCols = detectMaskedColumns(sheet, { maxScanRows: QUICK_SCAN_MAX_ROWS, maxScanCells: QUICK_SCAN_MAX_CELLS, includeFallbackSignals: false }); if (quickCols.length) { candidateCols = quickCols; } else { const deepStarCols = detectMaskedColumns(sheet, { maxScanRows: DEEP_SCAN_MAX_ROWS, maxScanCells: DEEP_SCAN_MAX_CELLS, includeFallbackSignals: false }); candidateCols = deepStarCols.length ? deepStarCols : runtimeDefaultCols; } } } } if (!useAliDocsManualDefault && mode === 'deep' && candidateCols.length && runtimeDefaultCols.length) { const maxRow = sheet.getRowCount?.() || 0; const activeRow0 = tryExtractRowFromView(app, maxRow) ?? tryExtractRowFromDom(maxRow); if (typeof activeRow0 === 'number') { const hitDetected = hasAnyContentInRow(sheet, candidateCols, activeRow0); if (!hitDetected) { const hitDefault = hasAnyContentInRow(sheet, runtimeDefaultCols, activeRow0); if (hitDefault) candidateCols = runtimeDefaultCols.slice(); } } } candidateCols = useAliDocsManualDefault ? runtimeDefaultCols.slice() : 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 buildRowItemsForCols = (cols, row0) => { return cols.map((c) => { const colName = colToLetter(c); const header = getHeaderText(sheet, c); const colLabel = header || colName; const cell = sheet.getCellDataAtPosition(row0, c, CELL_READ_OPTIONS); const urls = extractCellUrls(cell); const displayValue = normalizeCellText(displayTextOfCell(cell)); const value = normalizeCellText(textOfCell(cell)); return { col0: c, col: colName, header, colLabel, displayValue, value, urls }; }); }; const resolveRowItems = async (items) => { return Promise.all((items || []).map(async (item) => { const resolvedUrls = await resolveUrls(item.urls); const value = resolvedUrls.length ? resolvedUrls.join(' | ') : (item.value || item.displayValue || ''); return { ...item, resolvedUrls, value }; })); }; const filterVisibleRowItems = (items) => { return (items || []).filter((item) => { if (Array.isArray(item?.resolvedUrls) && item.resolvedUrls.length) return true; if (normalizeCellText(item?.value || '') !== '') return true; return normalizeCellText(item?.displayValue || '') !== ''; }); }; const buildAliDocsGroupRowCandidates = (baseRow0, maxRow) => { const total = Math.max(0, Number(maxRow) || 0); if (!total) return []; const groupSize = Math.max(1, Number(DINGTALK_ROW_GROUP_SIZE) || 3); const base = Math.max(0, Math.min(total - 1, Math.trunc(Number(baseRow0) || 0))); const out = []; const seen = new Set(); const push = (row) => { const r = Math.trunc(Number(row)); if (!Number.isFinite(r) || r < 0 || r >= total || seen.has(r)) return; seen.add(r); out.push(r); }; const groupStart = Math.floor(base / groupSize) * groupSize; push(base); for (let i = 0; i < groupSize; i += 1) push(groupStart + i); for (let offset = 1; offset <= 2; offset += 1) { const prevGroupStart = groupStart - offset * groupSize; const nextGroupStart = groupStart + offset * groupSize; for (let i = 0; i < groupSize; i += 1) push(prevGroupStart + i); for (let i = 0; i < groupSize; i += 1) push(nextGroupStart + i); } return out; }; const findRowFallbackItems = (row0, excludedCols, limit = 24) => { const colCount = sheet.getColCount?.() || 0; if (!colCount) return []; const excluded = excludedCols instanceof Set ? excludedCols : new Set(excludedCols || []); const candidates = []; for (let c = 0; c < colCount; c += 1) { if (excluded.has(c)) continue; const cell = sheet.getCellDataAtPosition(row0, c, CELL_READ_OPTIONS); const display = normalizeCellText(displayTextOfCell(cell)); const value = normalizeCellText(textOfCell(cell)); const urls = extractCellUrls(cell); if (!urls.length && !value) continue; const hidden = isColHidden(sheet, c); const maskedReveal = looksMaskedDisplayText(display) && value && value !== display; const priority = maskedReveal ? 0 : (hidden ? 1 : 2); const colName = colToLetter(c); const header = getHeaderText(sheet, c); candidates.push({ col0: c, col: colName, header, colLabel: header || colName, displayValue: display, value, urls, priority }); } candidates.sort((a, b) => (a.priority - b.priority) || (a.col0 - b.col0)); return candidates.slice(0, Math.max(1, Number(limit) || 24)); }; const isHiddenContentNoticeText = (text) => { const s = normalizeCellText(text); if (!s) return false; if (/^此单元格已开启填写内容隐藏,你无法查看$/.test(s)) return true; return /填写内容隐藏|你无法查看|无法查看|不可查看|不能查看/.test(s); }; const readSelectionFormulaBarText = () => { const comboboxes = Array.from(document.querySelectorAll('[role="combobox"]')); for (const el of comboboxes) { const text = normalizeCellText( (typeof el?.value === 'string' ? el.value : '') || (typeof el?.getAttribute === 'function' ? (el.getAttribute('value') || '') : '') || (typeof el?.textContent === 'string' ? el.textContent : '') ); if (!text) continue; if (/^\d+%$/.test(text)) continue; return text; } return ''; }; const isHiddenContentCellAt = (row0, col0) => { const rowCount = sheet.getRowCount?.() || 0; const colCount = sheet.getColCount?.() || 0; if (!Number.isInteger(row0) || !Number.isInteger(col0)) return false; if (row0 < 0 || col0 < 0 || row0 >= rowCount || col0 >= colCount) return false; const cell = sheet.getCellDataAtPosition(row0, col0, CELL_READ_OPTIONS); const display = normalizeCellText(displayTextOfCell(cell)); const reveal = normalizeCellText(textOfCell(cell)); if (isHiddenContentNoticeText(display) || isHiddenContentNoticeText(reveal)) return true; if (display && looksMaskedDisplayText(display)) { if (!reveal) return true; return reveal !== display; } const formulaBarText = readSelectionFormulaBarText(); return isHiddenContentNoticeText(formulaBarText); }; 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; } let dataRow0 = row0; let rowData = await resolveRowItems(buildRowItemsForCols(trackedCols, dataRow0)); if (renderSeq !== valueRenderSeq) return; let visibleData = filterVisibleRowItems(rowData); let alignedByRowGroup = false; if (!visibleData.length && isAliDocsRuntime(app, sheet)) { const candidates = buildAliDocsGroupRowCandidates(row0, maxRow); for (const candidateRow0 of candidates) { if (candidateRow0 === dataRow0) continue; const candidateRowData = await resolveRowItems(buildRowItemsForCols(trackedCols, candidateRow0)); if (renderSeq !== valueRenderSeq) return; const candidateVisible = filterVisibleRowItems(candidateRowData); if (!candidateVisible.length) continue; dataRow0 = candidateRow0; rowData = candidateRowData; visibleData = candidateVisible; alignedByRowGroup = true; break; } } let usedFallbackCols = false; if (!visibleData.length && isAliDocsRuntime(app, sheet)) { const fallbackItems = findRowFallbackItems(dataRow0, new Set(trackedCols), 24); if (fallbackItems.length) { const fallbackData = await resolveRowItems(fallbackItems); if (renderSeq !== valueRenderSeq) return; visibleData = filterVisibleRowItems(fallbackData); usedFallbackCols = visibleData.length > 0; } } tbody.innerHTML = ''; if (!visibleData.length) { const tr = document.createElement('tr'); tr.innerHTML = `第 ${currentRow1} 行在当前显示列中无非空内容,请切换列或手动选列`; 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; } if (alignedByRowGroup && dataRow0 !== row0) { const tr = document.createElement('tr'); tr.innerHTML = `钉钉文档按每${DINGTALK_ROW_GROUP_SIZE}行取值,已对齐到第 ${dataRow0 + 1} 行`; tbody.appendChild(tr); } if (usedFallbackCols) { const tr = document.createElement('tr'); tr.innerHTML = '已自动补充当前行有值列(未命中已选列)'; tbody.appendChild(tr); } 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(); }; const getPreferredRefreshRow1 = () => { const maxRow = sheet.getRowCount?.() || 0; const manualRow = Number(rowInput.value); const manualRowValid = Number.isFinite(manualRow) && manualRow > 0; const activeEl = document.activeElement; const isTypingRowInput = !!rowInput && (activeEl === rowInput || rowInput.contains?.(activeEl)); if (isTypingRowInput && manualRowValid) return Math.trunc(manualRow); const activeRow0 = tryExtractRowFromView(app, maxRow) ?? tryExtractRowFromDom(maxRow); if (typeof activeRow0 === 'number') return activeRow0 + 1; if (manualRowValid) return Math.trunc(manualRow); return currentRow1; }; doRefresh = () => { const row = getPreferredRefreshRow1(); render(row, { force: true }); }; doDetectCols = () => { if (btnDetect) btnDetect.disabled = true; try { detectAndApplyCols('deep'); doRefresh(); } finally { if (btnDetect) btnDetect.disabled = false; } }; doRevealAll = async () => { if (btnReveal) btnReveal.disabled = true; stopRevealResize(false); revealLastTSV = ''; revealRenderState = null; setRevealDialogOpen(true); if (revealTable) revealTable.innerHTML = ''; if (revealMeta) revealMeta.textContent = '正在准备全表显示...'; try { const result = await buildRevealTable(); if (!result?.ok) { if (revealMeta) revealMeta.textContent = String(result?.message || '生成失败'); return; } if (revealMeta) revealMeta.textContent = result.meta || `已显示 ${result.rows || 0} 行`; } catch (err) { if (revealMeta) revealMeta.textContent = `生成失败: ${String(err?.message || err)}`; } finally { if (btnReveal) btnReveal.disabled = false; } }; const applyManualSelection = () => { const colCount = sheet.getColCount?.() || 0; if (!colCount) return; const raw = String(colManualInput?.value || '').trim(); const cols = parseColumnSelection(raw, colCount); if (!cols.length) { if (colManualError) colManualError.textContent = '未识别到有效列,请输入如 A,C,F 或 A:F'; if (colManualInput) { colManualInput.focus(); colManualInput.select(); } return; } ensureCandidateCols(cols); selectedCols = new Set(cols); persistCandidateOrder(); renderColList(); setManualDialogOpen(false); doRefresh(); }; doManualSelectCols = () => { const preset = getOrderedSelectedCols().map(colToLetter).join(','); setColPickerOpen(false); setManualDialogOpen(true, preset); }; doSelectAllCols = () => { selectedCols = new Set(candidateCols); renderColList(); doRefresh(); }; doClearCols = () => { selectedCols = new Set(); renderColList(); doRefresh(); }; colManualCancel?.addEventListener('click', () => setManualDialogOpen(false)); colManualOk?.addEventListener('click', () => applyManualSelection()); colManualInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); applyManualSelection(); return; } if (e.key === 'Escape') { e.preventDefault(); setManualDialogOpen(false); } }); colManualModal?.addEventListener('mousedown', (e) => { const target = e.target; if (!(target instanceof Element)) return; if (target.closest('[data-role=\"close\"]')) setManualDialogOpen(false); }); revealClose?.addEventListener('click', () => setRevealDialogOpen(false)); revealCopy?.addEventListener('click', () => { if (!revealLastTSV) return; try { GM_setClipboard(revealLastTSV); if (revealMeta) revealMeta.textContent = `${revealMeta.textContent || ''}(已复制)`; } catch (_) { } }); revealModal?.addEventListener('mousedown', (e) => { const target = e.target; if (!(target instanceof Element)) return; if (target.closest('[data-role=\"close-reveal\"]')) setRevealDialogOpen(false); }); revealTable?.addEventListener('click', (e) => { const target = e.target; if (!(target instanceof Element)) return; const sortBtn = target.closest('[data-role=\"reveal-sort-row\"]'); if (!sortBtn) return; revealSortOrder = revealSortOrder === 'desc' ? 'asc' : 'desc'; persistRevealView(); renderRevealTableFromState({ keepScroll: true }); }); revealTable?.addEventListener('mousedown', (e) => { if (e.button !== 0) return; const target = e.target; if (!(target instanceof Element)) return; const handle = target.closest('[data-role=\"reveal-col-resize\"]'); if (!handle) return; const key = String(handle.getAttribute('data-key') || '').trim(); if (!key) return; const th = handle.closest('th'); const baseWidth = normalizeRevealColWidth(th?.getBoundingClientRect?.().width || 0) || getRevealColWidth(key); revealResizeSession = { key, startX: Number(e.clientX || 0), startWidth: baseWidth, prevUserSelect: document.documentElement.style.userSelect || '' }; document.documentElement.style.userSelect = 'none'; panel.classList.add('reveal-resizing'); e.preventDefault(); e.stopPropagation(); }); doToggle = () => { clearColHeaderRetry(); setColPickerOpen(false); setManualDialogOpen(false); setRevealDialogOpen(false); panel.classList.add('hidden'); }; doReposition = () => { placePanelNearSelection(panel); if (colPicker?.classList.contains('open')) placeColPickerPop(); }; document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (isRevealDialogOpen()) { setRevealDialogOpen(false); return; } if (isManualDialogOpen()) { setManualDialogOpen(false); return; } 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); setManualDialogOpen(false); setRevealDialogOpen(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 maxCol = sheet.getColCount?.() || 0; const row0 = tryExtractRowFromSelectionPayload(payload, maxRow) ?? tryExtractRowFromView(app, maxRow) ?? tryExtractRowFromDom(maxRow); const col0 = maxCol > 0 ? ( tryExtractColFromSelectionPayload(payload, maxCol) ?? tryExtractColFromView(app, maxCol) ) : null; if (typeof row0 === 'number') { pendingSelectionRow1 = row0 + 1; pendingSelectionCol0 = typeof col0 === 'number' ? col0 : null; if (selectionRenderTimer) return; selectionRenderTimer = setTimeout(() => { selectionRenderTimer = 0; const row1 = pendingSelectionRow1; const col = pendingSelectionCol0; const force = typeof row1 === 'number' && typeof col === 'number' ? isHiddenContentCellAt(row1 - 1, col) : false; pendingSelectionRow1 = null; pendingSelectionCol0 = null; if (force) { setRevealDialogOpen(false); if (panel.classList.contains('hidden')) panel.classList.remove('hidden'); } const shouldAutoHideOnPlainSelection = !isAliDocsRuntime(app, sheet); if (!force && shouldAutoHideOnPlainSelection && !panel.classList.contains('hidden')) { clearColHeaderRetry(); setColPickerOpen(false); setManualDialogOpen(false); setRevealDialogOpen(false); panel.classList.add('hidden'); return; } const shouldRender = force || !panel.classList.contains('hidden'); if (typeof row1 === 'number' && shouldRender) render(row1, { force }); }, SELECTION_RENDER_DEBOUNCE_MS); } else { pendingSelectionCol0 = null; doReposition(); } }); } } catch (_) { } detectAndApplyCols('quick'); resolveInitialRow1().then((row1) => { currentRow1 = row1; render(currentRow1, { force: true }); }); }; let initRetryCount = 0; const tryInit = () => { waitForSpreadsheetApp(5000).then((ctx) => { initRetryCount = 0; initDataLayer(ctx); }).catch((err) => { if (initialized) return; initRetryCount += 1; if (initRetryCount >= INIT_RETRY_MAX_ATTEMPTS) { console.warn('[hidden-col-viewer] init aborted after max retries:', err?.message || err); return; } console.warn( `[hidden-col-viewer] retry init (${initRetryCount}/${INIT_RETRY_MAX_ATTEMPTS}):`, err?.message || err ); setTimeout(tryInit, 2000); }); }; tryInit(); } const isAliDocsPortalTopPage = ( location.hostname === 'alidocs.dingtalk.com' && ALIDOCS_PORTAL_PATH_RE.test(location.pathname) && PAGE_WIN.top === PAGE_WIN ); if (isAliDocsPortalTopPage) { console.info('[hidden-col-viewer] skip portal shell page, runtime will init inside spreadsheet iframe.'); return; } if (document.readyState === 'complete') install(); else window.addEventListener('load', install, { once: true }); })();