// ==UserScript==
// @name ET 表格预览器(拖入 .et 文件直接查看)
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description 点右下角按钮打开预览页,把 WPS 表格文件(.et/.ett,兼容 .xls/.xlsx/.xlsm/.csv)拖进去就能看;也可直接在任意网页拖入。支持多工作表切换、查找过滤、自动换行、单元格详情、复制、导出 CSV、一键转存 XLSX,大表格渐进加载不卡顿。
// @author xizi
// @match *://*/*
// @match file:///*
// @require https://cdn.bootcdn.net/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant unsafeWindow
// @run-at document-start
// @noframes
// @license MIT
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjMTYyMTFhIiBzdHJva2U9IiM1YWQwN2EiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTYgNy41aDEyTTYgMTJoMTJNNiAxNi41aDEyTTEwLjUgNy41djkiIHN0cm9rZT0iIzVhZDA3YSIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==
// ==/UserScript==
/*
* 功能一览
* 1. 把 .et(WPS 表格)文件拖到网页任意位置,松手即预览;也可以点右下角悬浮按钮 / 油猴菜单选择文件。
* 2. .et 有两种常见形态:老版本是 OLE2 复合文档(BIFF8),新版本是 ZIP 包(等同 xlsx)。脚本按文件头自动识别,
* 两种都能直接解析,解析工作由 SheetJS 完成(已在 @require 里内置)。
* 3. 预览界面:多工作表标签、查找过滤(命中高亮)、自动换行、单元格详情、复制、导出 CSV、转存 XLSX。
* 4. 大表格渐进渲染:滚动到底部自动追加,导出始终是完整数据。
*
* 说明:脚本只接管拖入的表格文件,其它文件的拖放会原样交还给网页,尽量不影响目标站点自身的拖拽功能。
*/
(function () {
'use strict';
/* ============================================================
* 0. 常量与全局状态
* ============================================================ */
const VERSION = '1.1.0';
const ROOT_ID = 'et-preview-root';
const BALL_ID = 'et-preview-ball';
const BANNER_ID = 'et-preview-banner';
const TOAST_ID = 'et-preview-toast';
// 支持的文件后缀:前两个是脚本的主场(WPS 表格),后面几个是顺带支持
const EXT_ET = ['et', 'ett'];
const EXT_OTHER = ['xls', 'xlsx', 'xlsm', 'xlsb', 'csv'];
// 解析库地址:即使 @require 失败,运行期也会按顺序再试一遍
const CDN_LIST = [
'https://cdn.bootcdn.net/ajax/libs/xlsx/0.18.5/xlsx.full.min.js',
'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js',
'https://fastly.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js',
'https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js',
'https://cdn.staticfile.org/xlsx/0.18.5/xlsx.full.min.js'
];
const CHUNK_ROWS = 150; // 每次追加渲染的行数
const RENDER_STEP = 20000; // 提高渲染上限时的步长
const COVER_LIMIT = 500000; // 合并区域覆盖点统计上限(防御异常文件)
const state = {
wb: null, // SheetJS 工作簿对象
fileName: '',
fileSize: 0,
sheetIndex: 0,
models: new Map(), // 工作表名 -> 渲染模型
filter: '',
visibleRows: [],
rendered: 0,
renderCap: RENDER_STEP,
selected: null, // { r, c }
xlsxPromise: null
};
const refs = {}; // DOM 引用集合
let xlsxLib = null; // 解析库缓存
let dragDepth = 0;
let toastTimer = 0;
/* ============================================================
* 1. 小工具
* ============================================================ */
function getXLSX() {
if (xlsxLib) return xlsxLib;
let lib = null;
try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.XLSX) lib = unsafeWindow.XLSX; } catch (e) { /* 忽略 */ }
if (!lib) { try { if (typeof XLSX !== 'undefined' && XLSX) lib = XLSX; } catch (e) { /* 忽略 */ } }
if (!lib && window.XLSX) lib = window.XLSX;
if (lib) xlsxLib = lib;
return lib;
}
/** 确保解析库就绪:@require 已加载则直接命中缓存,否则按 CDN 列表兜底加载 */
function ensureXLSX() {
const ready = getXLSX();
if (ready) return Promise.resolve(ready);
if (state.xlsxPromise) return state.xlsxPromise;
state.xlsxPromise = new Promise(function (resolve, reject) {
let i = 0;
const next = function () {
if (i >= CDN_LIST.length) {
reject(new Error('解析库加载失败(XLSX),请检查网络或脚本里的 @require 地址'));
return;
}
const url = CDN_LIST[i++];
const s = document.createElement('script');
s.src = url;
s.async = true;
s.onload = function () { getXLSX() ? resolve(getXLSX()) : next(); };
s.onerror = function () { try { s.remove(); } catch (e) { /* 忽略 */ } next(); };
(document.head || document.documentElement).appendChild(s);
};
next();
}).catch(function (err) {
state.xlsxPromise = null;
throw err;
});
return state.xlsxPromise;
}
function extOf(name) {
const i = String(name || '').lastIndexOf('.');
return i < 0 ? '' : String(name).slice(i + 1).toLowerCase();
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (ch) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch];
});
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function fmtInt(n) {
return Number(n || 0).toLocaleString('zh-CN');
}
function fmtSize(n) {
if (n == null) return '';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
let v = Number(n);
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return (i === 0 ? v : v.toFixed(v < 10 ? 2 : 1)) + ' ' + units[i];
}
/** 估算显示宽度:ASCII 记 1,中文等宽字符记 2 */
function visualUnits(text) {
let u = 0;
for (let i = 0; i < text.length; i++) {
u += text.charCodeAt(i) < 0x2e80 ? 1 : 2;
}
return u;
}
function firstLine(text) {
const s = String(text);
const i = s.search(/\r|\n/);
return i < 0 ? s : s.slice(0, i);
}
function toast(msg, type) {
let box = document.getElementById(TOAST_ID);
if (!box) {
box = document.createElement('div');
box.id = TOAST_ID;
(document.body || document.documentElement).appendChild(box);
}
box.className = 'et-toast et-show' + (type ? ' et-toast-' + type : '');
box.innerHTML = escapeHtml(msg).replace(/\n/g, '
');
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { box.classList.remove('et-show'); }, type === 'error' ? 6000 : 3200);
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
(document.body || document.documentElement).appendChild(a);
a.click();
setTimeout(function () {
try { a.remove(); } catch (e) { /* 忽略 */ }
URL.revokeObjectURL(url);
}, 4000);
}
function baseName(name) {
return String(name || 'workbook').replace(/\.[^.]+$/, '');
}
function copyText(text, okMsg) {
const done = function () { toast(okMsg || '已复制到剪贴板'); };
try {
if (typeof GM_setClipboard === 'function') { GM_setClipboard(text, 'text'); done(); return; }
} catch (e) { /* 继续尝试下面两种方案 */ }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text, done); });
return;
}
fallbackCopy(text, done);
}
function fallbackCopy(text, done) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
(document.body || document.documentElement).appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
try { ta.remove(); } catch (e) { /* 忽略 */ }
if (ok && done) done(); else toast('复制失败,请手动选择后复制', 'error');
}
/* ============================================================
* 2. 样式
* ============================================================ */
const CSS = [
'#' + ROOT_ID + '{position:fixed;inset:0;z-index:2147483600;display:none;align-items:center;justify-content:center;',
'background:rgba(6,8,12,.55);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);',
'font:13px/1.55 -apple-system,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;color:#e9edf5;}',
'#' + ROOT_ID + '.et-show{display:flex;animation:et-fade .16s ease-out;}',
'@keyframes et-fade{from{opacity:0}to{opacity:1}}',
'#' + ROOT_ID + ' *{box-sizing:border-box;}',
'#' + ROOT_ID + ' .et-panel{display:flex;flex-direction:column;width:min(1520px,96vw);height:min(920px,92vh);overflow:hidden;',
'background:linear-gradient(180deg,#171b24,#11141b);border:1px solid rgba(255,255,255,.1);border-radius:14px;',
'box-shadow:0 26px 90px rgba(0,0,0,.62);}',
'#' + ROOT_ID + '.et-full .et-panel{width:100vw;height:100vh;border-radius:0;border:0;}',
'#' + ROOT_ID + ' .et-head{display:flex;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);',
'background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,0));}',
'#' + ROOT_ID + ' .et-badge{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:34px;height:24px;border-radius:6px;',
'background:rgba(90,208,122,.14);border:1px solid rgba(90,208,122,.45);color:#7ee29a;font-size:11px;font-weight:700;letter-spacing:.5px;}',
'#' + ROOT_ID + ' .et-fname{font-size:14px;font-weight:600;max-width:52vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'#' + ROOT_ID + ' .et-fmeta{color:#8d97a8;font-size:12px;white-space:nowrap;}',
'#' + ROOT_ID + ' .et-grow{flex:1 1 auto;}',
'#' + ROOT_ID + ' .et-btn{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:#dbe3f0;',
'border-radius:8px;padding:5px 10px;font-size:12px;line-height:1.5;cursor:pointer;white-space:nowrap;transition:.15s;}',
'#' + ROOT_ID + ' .et-btn:hover{background:rgba(90,208,122,.16);border-color:rgba(90,208,122,.5);color:#c9ffd9;}',
'#' + ROOT_ID + ' .et-btn:active{transform:translateY(1px);}',
'#' + ROOT_ID + ' .et-close{width:30px;padding:0;height:28px;font-size:14px;}',
'#' + ROOT_ID + ' .et-tabs{display:flex;gap:6px;align-items:flex-end;padding:8px 14px 0;overflow-x:auto;overflow-y:hidden;}',
'#' + ROOT_ID + ' .et-tabs:empty{display:none;}',
'#' + ROOT_ID + ' .et-tab{flex:0 0 auto;border:1px solid rgba(255,255,255,.1);border-bottom:none;background:rgba(255,255,255,.04);',
'color:#a9b4c5;border-radius:8px 8px 0 0;padding:6px 12px;font-size:12px;cursor:pointer;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'#' + ROOT_ID + ' .et-tab:hover{color:#e9edf5;}',
'#' + ROOT_ID + ' .et-tab.et-on{background:rgba(90,208,122,.16);border-color:rgba(90,208,122,.5);color:#c9ffd9;font-weight:600;}',
'#' + ROOT_ID + ' .et-toolbar{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid rgba(255,255,255,.08);flex-wrap:wrap;}',
'#' + ROOT_ID + ' .et-search{width:min(320px,42vw);background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.14);border-radius:8px;',
'color:#e9edf5;padding:5px 10px;font-size:12px;outline:none;}',
'#' + ROOT_ID + ' .et-search:focus{border-color:rgba(90,208,122,.6);}',
'#' + ROOT_ID + ' .et-filterinfo{color:#f7c66b;font-size:12px;}',
'#' + ROOT_ID + ' .et-chk{display:inline-flex;align-items:center;gap:5px;color:#b8c2d2;font-size:12px;cursor:pointer;user-select:none;}',
'#' + ROOT_ID + ' .et-chk input{accent-color:#5ad07a;}',
'#' + ROOT_ID + ' .et-body{flex:1 1 auto;display:flex;min-height:0;position:relative;}',
'#' + ROOT_ID + ' .et-tablewrap{flex:1 1 auto;overflow:auto;background:#0d1016;position:relative;}',
'#' + ROOT_ID + ' .et-tablewrap::-webkit-scrollbar{width:12px;height:12px;}',
'#' + ROOT_ID + ' .et-tablewrap::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:8px;border:3px solid #0d1016;}',
'#' + ROOT_ID + ' .et-tablewrap::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.28);}',
'#' + ROOT_ID + ' .et-welcome{flex:1 1 auto;display:none;flex-direction:column;align-items:center;justify-content:center;gap:12px;',
'text-align:center;padding:40px 24px;background:#0d1016;}',
'#' + ROOT_ID + '.et-empty .et-welcome{display:flex;}',
'#' + ROOT_ID + '.et-empty .et-tablewrap,#' + ROOT_ID + '.et-empty .et-side{display:none;}',
'#' + ROOT_ID + ' .et-w-icon{font-size:44px;line-height:1;color:#5ad07a;opacity:.9;}',
'#' + ROOT_ID + ' .et-w-title{font-size:20px;font-weight:600;color:#e7eef8;}',
'#' + ROOT_ID + ' .et-w-sub{font-size:13px;color:#8d97a8;}',
'#' + ROOT_ID + ' .et-w-sub b{color:#a8b6c9;font-weight:600;}',
'#' + ROOT_ID + ' .et-w-btn{padding:8px 18px;font-size:13px;margin-top:2px;}',
'#' + ROOT_ID + ' .et-w-hint{font-size:12px;color:#6f7f95;}',
'#' + ROOT_ID + ' .et-table{border-collapse:separate;border-spacing:0;table-layout:fixed;min-width:100%;font-size:12.5px;}',
'#' + ROOT_ID + ' .et-table th,#' + ROOT_ID + ' .et-table td{border-right:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);',
'padding:5px 8px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'#' + ROOT_ID + ' .et-table thead th{position:sticky;top:0;z-index:3;background:#1b2130;color:#93a0b5;font-weight:600;text-align:center;',
'font-size:11.5px;border-bottom:1px solid rgba(255,255,255,.12);}',
'#' + ROOT_ID + ' .et-table .et-rh{position:sticky;left:0;z-index:2;background:#161b26;color:#7c8798;text-align:right;font-weight:400;',
'font-size:11.5px;min-width:52px;font-variant-numeric:tabular-nums;}',
'#' + ROOT_ID + ' .et-table thead .et-rh{z-index:4;background:#1b2130;}',
'#' + ROOT_ID + ' .et-table tbody tr:nth-child(even) td{background:rgba(255,255,255,.022);}',
'#' + ROOT_ID + ' .et-table tbody tr:hover td{background:rgba(90,208,122,.08);}',
'#' + ROOT_ID + ' .et-table td{cursor:cell;}',
'#' + ROOT_ID + ' .et-num{text-align:right;color:#cfe6ff;font-variant-numeric:tabular-nums;}',
'#' + ROOT_ID + ' .et-date{text-align:right;color:#8fd0ff;}',
'#' + ROOT_ID + ' .et-bool{text-align:center;color:#b39ddb;}',
'#' + ROOT_ID + ' .et-err{text-align:center;color:#ff8a8a;}',
'#' + ROOT_ID + ' .et-merge{background:rgba(90,208,122,.05);}',
'#' + ROOT_ID + ' .et-cell.et-sel{outline:2px solid #5ad07a;outline-offset:-2px;background:rgba(90,208,122,.14)!important;}',
'#' + ROOT_ID + ' mark{background:rgba(247,198,107,.32);color:#ffe6b0;border-radius:2px;padding:0 1px;}',
'#' + ROOT_ID + '.et-wrap-on .et-table th,#' + ROOT_ID + '.et-wrap-on .et-table td{white-space:pre-wrap;overflow-wrap:anywhere;}',
'#' + ROOT_ID + ' .et-merge-tag{color:#7ee29a;font-size:11px;}',
'#' + ROOT_ID + ' .et-side{flex:0 0 300px;border-left:1px solid rgba(255,255,255,.08);background:#141922;padding:12px;overflow:auto;}',
'#' + ROOT_ID + ' .et-side-empty{color:#7c8798;font-size:12px;line-height:1.8;}',
'#' + ROOT_ID + ' .et-side h4{margin:0 0 8px;font-size:12px;color:#93a0b5;font-weight:600;}',
'#' + ROOT_ID + ' .et-kv{display:grid;grid-template-columns:64px 1fr;gap:4px 8px;font-size:12px;margin-bottom:10px;}',
'#' + ROOT_ID + ' .et-kv b{color:#7c8798;font-weight:400;}',
'#' + ROOT_ID + ' .et-sideval{background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.09);border-radius:8px;padding:8px;',
'font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere;max-height:44vh;overflow:auto;color:#dfe7f3;}',
'#' + ROOT_ID + ' .et-foot{display:flex;align-items:center;gap:10px;padding:6px 14px;border-top:1px solid rgba(255,255,255,.08);',
'color:#8d97a8;font-size:12px;background:#12161e;flex-wrap:wrap;}',
'#' + ROOT_ID + ' .et-foot b{color:#a8b6c9;font-weight:600;}',
'#' + ROOT_ID + ' .et-loading{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;gap:14px;',
'background:rgba(8,10,15,.72);z-index:9;font-size:13px;color:#c9d3e2;}',
'#' + ROOT_ID + ' .et-loading.et-show{display:flex;}',
'#' + ROOT_ID + ' .et-spin{width:34px;height:34px;border-radius:50%;border:3px solid rgba(90,208,122,.25);border-top-color:#5ad07a;animation:et-spin .8s linear infinite;}',
'@keyframes et-spin{to{transform:rotate(360deg)}}',
'#' + ROOT_ID + ' .et-help{max-width:560px;width:min(560px,92vw);height:auto;padding:22px 24px;line-height:1.9;font-size:13px;}',
'#' + ROOT_ID + ' .et-help h3{margin:0 0 12px;font-size:15px;color:#c9ffd9;}',
'#' + ROOT_ID + ' .et-help code{background:rgba(255,255,255,.08);border-radius:4px;padding:1px 5px;font-size:12px;}',
'#' + ROOT_ID + ' .et-help ul{margin:6px 0 12px;padding-left:20px;color:#b8c2d2;}',
'#' + BANNER_ID + '{position:fixed;left:50%;top:16px;transform:translate(-50%,-24px);z-index:2147483600;opacity:0;pointer-events:none;',
'display:flex;align-items:center;gap:10px;padding:10px 18px;border-radius:12px;background:linear-gradient(180deg,#1d2534,#141922);',
'border:1px solid rgba(90,208,122,.55);box-shadow:0 12px 40px rgba(0,0,0,.5);color:#d9f7e2;',
'font:13px/1.4 -apple-system,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;transition:opacity .16s,transform .16s;}',
'#' + BANNER_ID + '.et-show{opacity:1;transform:translate(-50%,0);}',
'#' + BANNER_ID + ' .et-dot{width:9px;height:9px;border-radius:50%;background:#5ad07a;box-shadow:0 0 0 4px rgba(90,208,122,.18);}',
'#' + BALL_ID + '{position:fixed;right:18px;bottom:18px;z-index:2147483590;width:42px;height:42px;border-radius:50%;cursor:pointer;',
'display:none;align-items:center;justify-content:center;background:linear-gradient(180deg,#1d2534,#12161e);',
'border:1px solid rgba(90,208,122,.5);box-shadow:0 10px 26px rgba(0,0,0,.45);color:#7ee29a;font-size:18px;user-select:none;transition:.16s;}',
'#' + BALL_ID + '.et-show{display:flex;}',
'#' + BALL_ID + ':hover{transform:translateY(-2px);border-color:#5ad07a;color:#c9ffd9;}',
'#' + TOAST_ID + '{position:fixed;left:50%;bottom:34px;transform:translate(-50%,20px);z-index:2147483610;max-width:min(520px,88vw);',
'padding:10px 16px;border-radius:10px;background:rgba(20,25,34,.96);border:1px solid rgba(90,208,122,.42);color:#dbe6f2;',
'font:13px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",system-ui,sans-serif;box-shadow:0 14px 40px rgba(0,0,0,.5);',
'opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;}',
'#' + TOAST_ID + '.et-show{opacity:1;transform:translate(-50%,0);}',
'#' + TOAST_ID + '.et-toast-error{border-color:rgba(255,120,120,.55);color:#ffdada;}'
].join('');
function injectStyles() {
try {
if (typeof GM_addStyle === 'function') { GM_addStyle(CSS); return; }
} catch (e) { /* 忽略,走下面的兜底方案 */ }
const style = document.createElement('style');
style.textContent = CSS;
(document.head || document.documentElement).appendChild(style);
}
/* ============================================================
* 3. 界面构建
* ============================================================ */
/** 拖拽提示条(懒创建:脚本在 document-start 就运行,那时可能还没有 body) */
function ensureBanner() {
if (refs.banner || !document.body) return refs.banner;
const banner = document.createElement('div');
banner.id = BANNER_ID;
banner.innerHTML = '松手即可预览表格文件';
document.body.appendChild(banner);
refs.banner = banner;
return banner;
}
/** 右下角悬浮按钮(默认显示,可在油猴菜单里隐藏) */
function ensureBall() {
if (refs.ball || !document.body) return refs.ball;
const ball = document.createElement('div');
ball.id = BALL_ID;
ball.title = '打开 ET 预览页(拖入或选择 .et 文件)';
ball.textContent = '▦';
ball.addEventListener('click', openPreviewPage);
document.body.appendChild(ball);
refs.ball = ball;
return ball;
}
/** 界面只建一次;拖放事件可能在界面建好之前就发生,所以这里做成“按需创建” */
function ensureUI() {
if (refs.root || !document.body) return;
buildUI();
updateBallVisibility();
}
function buildUI() {
if (refs.root) return;
ensureBanner();
ensureBall();
// 主面板
const root = document.createElement('div');
root.id = ROOT_ID;
root.innerHTML = [
'
.et 拖进去(或点「选择文件…」)。.et,松手就会弹出预览面板。.et / .ett(WPS 表格),顺带支持 .xls / .xlsx / .xlsm / .csv。file:// 页面上使用时,需要在扩展详情里开启「允许访问文件网址」。