1221 lines
44 KiB
JavaScript
1221 lines
44 KiB
JavaScript
const cs = (() => {
|
|
let me = null
|
|
let currentId = 0
|
|
let currentVersionId = 0
|
|
let currentTab = 'list'
|
|
let lastSavedPayload = ''
|
|
let attachPanelOpen = false
|
|
let isFullscreen = false
|
|
let univerMode = 'none'
|
|
let univerAdapter = null
|
|
let pendingEditHint = false
|
|
let actionDepth = 0
|
|
let actionHasError = false
|
|
let actionTimer = null
|
|
let actionCountdown = 0
|
|
let actionLogger = null
|
|
let dialogResolver = null
|
|
const themeKey = 'cloud_sheets_theme'
|
|
const unsavedPromptEnabled = true
|
|
|
|
const el = id => document.getElementById(id)
|
|
const txt = (id, v) => { const n = el(id); if (n) n.textContent = v == null ? '' : String(v) }
|
|
const show = (id, on) => { const n = el(id); if (n) n.style.display = on ? '' : 'none' }
|
|
const esc = s => String(s || '').replace(/[&<>"]/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[m]))
|
|
const status = (id, v) => {
|
|
const n = el(id)
|
|
if (!n) return
|
|
if (!v) {
|
|
n.style.display = 'none'
|
|
n.textContent = ''
|
|
return
|
|
}
|
|
n.style.display = ''
|
|
n.textContent = String(v)
|
|
}
|
|
const nowStr = () => {
|
|
const d = new Date()
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
const ss = String(d.getSeconds()).padStart(2, '0')
|
|
return `${hh}:${mm}:${ss}`
|
|
}
|
|
const clearActionTimer = () => {
|
|
if (actionTimer) {
|
|
clearInterval(actionTimer)
|
|
actionTimer = null
|
|
}
|
|
}
|
|
const setActionStatus = (text, cls) => {
|
|
const node = el('cs-loading-status')
|
|
if (!node) return
|
|
node.className = `cs-loading-status${cls ? ` ${cls}` : ''}`
|
|
node.textContent = String(text || '')
|
|
}
|
|
const openActionModal = (title, msg) => {
|
|
clearActionTimer()
|
|
actionCountdown = 0
|
|
actionHasError = false
|
|
txt('cs-loading-title', title || '处理中')
|
|
txt('cs-loading-msg', msg || '')
|
|
setActionStatus('运行中', '')
|
|
const logNode = el('cs-loading-logs')
|
|
if (logNode) logNode.innerHTML = ''
|
|
show('cs-loading', true)
|
|
}
|
|
const appendActionLog = (text, type) => {
|
|
const logNode = el('cs-loading-logs')
|
|
if (!logNode) return
|
|
const line = document.createElement('div')
|
|
line.className = `cs-log-line${type ? ` ${type}` : ''}`
|
|
line.textContent = `[${nowStr()}] ${String(text || '')}`
|
|
logNode.appendChild(line)
|
|
logNode.scrollTop = logNode.scrollHeight
|
|
}
|
|
const closeActionModal = () => {
|
|
clearActionTimer()
|
|
show('cs-loading', false)
|
|
}
|
|
const finishActionSuccess = () => {
|
|
if (actionHasError) return
|
|
actionCountdown = 3
|
|
setActionStatus(`成功,${actionCountdown}s 后自动关闭`, 'ok')
|
|
clearActionTimer()
|
|
actionTimer = setInterval(() => {
|
|
actionCountdown -= 1
|
|
if (actionCountdown <= 0) {
|
|
clearActionTimer()
|
|
closeActionModal()
|
|
return
|
|
}
|
|
setActionStatus(`成功,${actionCountdown}s 后自动关闭`, 'ok')
|
|
}, 1000)
|
|
}
|
|
const actionErrorText = err => {
|
|
if (!err) return '未知错误'
|
|
if (typeof err === 'string') return err
|
|
const message = String(err.message || err)
|
|
const stack = err && err.stack ? String(err.stack) : ''
|
|
return stack ? `${message}\n${stack}` : message
|
|
}
|
|
const failAction = err => {
|
|
actionHasError = true
|
|
setActionStatus('发生错误,已中断', 'err')
|
|
appendActionLog(actionErrorText(err), 'err')
|
|
}
|
|
const runAction = async (title, fn, subtitle) => {
|
|
const top = actionDepth === 0
|
|
if (top) openActionModal(title, subtitle || '')
|
|
actionDepth += 1
|
|
const logger = line => appendActionLog(line)
|
|
const prevLogger = actionLogger
|
|
actionLogger = logger
|
|
appendActionLog(`开始:${title}`)
|
|
try {
|
|
const result = await fn(logger)
|
|
appendActionLog(`完成:${title}`, 'ok')
|
|
return result
|
|
} catch (err) {
|
|
failAction(err)
|
|
throw err
|
|
} finally {
|
|
actionLogger = prevLogger
|
|
actionDepth -= 1
|
|
if (top && !actionHasError) finishActionSuccess()
|
|
}
|
|
}
|
|
const openDialog = ({ title, message, buttons }) => {
|
|
txt('cs-dialog-title', title || '提示')
|
|
txt('cs-dialog-msg', message || '')
|
|
const box = el('cs-dialog-actions')
|
|
if (box) box.innerHTML = ''
|
|
;(Array.isArray(buttons) ? buttons : []).forEach(btn => {
|
|
const b = document.createElement('button')
|
|
b.className = 'mini-btn'
|
|
b.textContent = String(btn.label || '确定')
|
|
b.onclick = () => {
|
|
show('cs-dialog', false)
|
|
if (dialogResolver) {
|
|
const r = dialogResolver
|
|
dialogResolver = null
|
|
r(btn.value)
|
|
}
|
|
}
|
|
if (box) box.appendChild(b)
|
|
})
|
|
show('cs-dialog', true)
|
|
}
|
|
const askDialog = options => new Promise(resolve => {
|
|
dialogResolver = resolve
|
|
openDialog(options || {})
|
|
})
|
|
const askConfirmModal = async (message, title) => {
|
|
const out = await askDialog({
|
|
title: title || '确认操作',
|
|
message: message || '',
|
|
buttons: [
|
|
{ label: '取消', value: false },
|
|
{ label: '确定', value: true }
|
|
]
|
|
})
|
|
return !!out
|
|
}
|
|
const showInfoModal = async (message, title) => {
|
|
await askDialog({
|
|
title: title || '提示',
|
|
message: message || '',
|
|
buttons: [{ label: '确定', value: true }]
|
|
})
|
|
}
|
|
|
|
const currentTheme = () => document.body.getAttribute('data-theme') === 'light' ? 'light' : 'dark'
|
|
const applyTheme = theme => {
|
|
const t = theme === 'light' ? 'light' : 'dark'
|
|
document.body.setAttribute('data-theme', t)
|
|
try { localStorage.setItem(themeKey, t) } catch {}
|
|
const btn = el('cs-toggle-theme')
|
|
if (btn) btn.textContent = t === 'dark' ? '🌙' : '☀'
|
|
}
|
|
|
|
const getJsonEditor = () => el('cs-fallback-json')
|
|
const makeDefaultWorkbook = () => ({
|
|
id: 'workbook-main',
|
|
name: '云表格',
|
|
appVersion: '1.0.0',
|
|
locale: 'zhCN',
|
|
styles: {},
|
|
sheetOrder: ['sheet-1'],
|
|
sheets: {
|
|
'sheet-1': {
|
|
id: 'sheet-1',
|
|
name: 'Sheet1',
|
|
tabColor: '',
|
|
hidden: 0,
|
|
rowCount: 1000,
|
|
columnCount: 30,
|
|
zoomRatio: 1,
|
|
defaultColumnWidth: 80,
|
|
defaultRowHeight: 24,
|
|
cellData: {
|
|
0: { 0: { v: '证券记录' }, 1: { v: '金额' }, 2: { v: '备注' } },
|
|
1: { 0: { v: '示例' }, 1: { v: 1000 }, 2: { v: '可编辑' } }
|
|
}
|
|
}
|
|
}
|
|
})
|
|
const toWorkbookString = v => {
|
|
try {
|
|
if (typeof v === 'string') {
|
|
const s = v.trim()
|
|
if (!s) return JSON.stringify(makeDefaultWorkbook())
|
|
JSON.parse(s)
|
|
return s
|
|
}
|
|
return JSON.stringify(v && typeof v === 'object' ? v : makeDefaultWorkbook())
|
|
} catch {
|
|
return JSON.stringify(makeDefaultWorkbook())
|
|
}
|
|
}
|
|
const previewTextFromWorkbook = workbookJson => {
|
|
try {
|
|
const text = JSON.stringify(typeof workbookJson === 'string' ? JSON.parse(workbookJson) : workbookJson || {})
|
|
.replace(/[{}[\]":,]/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
if (!text) return ''
|
|
return text.length > 260 ? `${text.slice(0, 260)}...` : text
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
const logLine = line => {
|
|
if (typeof actionLogger === 'function') actionLogger(line)
|
|
}
|
|
|
|
const loadScript = src => new Promise((resolve, reject) => {
|
|
const s = document.createElement('script')
|
|
s.src = src
|
|
s.async = true
|
|
s.onload = () => resolve()
|
|
s.onerror = () => reject(new Error(`脚本加载失败: ${src}`))
|
|
document.head.appendChild(s)
|
|
})
|
|
const loadCss = href => new Promise((resolve, reject) => {
|
|
const l = document.createElement('link')
|
|
l.rel = 'stylesheet'
|
|
l.href = href
|
|
l.onload = () => resolve()
|
|
l.onerror = () => reject(new Error(`样式加载失败: ${href}`))
|
|
document.head.appendChild(l)
|
|
})
|
|
const ensureUniverAdapter = async () => {
|
|
if (window.CloudSheetsUniverAdapter && typeof window.CloudSheetsUniverAdapter.create === 'function') return window.CloudSheetsUniverAdapter
|
|
const cssCandidates = [
|
|
'/tools/cloud_sheets/vendor/univer/univer.css?v=3',
|
|
'/tools/cloud_sheets/vendor/univer/adapter.css?v=13'
|
|
]
|
|
for (const href of cssCandidates) {
|
|
logLine(`加载样式: ${href}`)
|
|
await loadCss(href)
|
|
}
|
|
const jsCandidates = [
|
|
'/tools/cloud_sheets/vendor/univer/adapter.js?v=13'
|
|
]
|
|
for (const src of jsCandidates) {
|
|
logLine(`加载脚本: ${src}`)
|
|
await loadScript(src)
|
|
if (window.CloudSheetsUniverAdapter && typeof window.CloudSheetsUniverAdapter.create === 'function') return window.CloudSheetsUniverAdapter
|
|
}
|
|
throw new Error('CloudSheetsUniverAdapter 未注入')
|
|
}
|
|
const initSheetEngine = async () => runAction('初始化表格引擎', async log => {
|
|
const host = el('cs-univer')
|
|
if (!host) throw new Error('缺少表格容器 cs-univer')
|
|
|
|
// 如果已经初始化过了,不再重复创建,而是直接恢复显示
|
|
if (univerMode === 'univer' && univerAdapter) {
|
|
log('Univer 实例已存在,直接恢复显示')
|
|
show('cs-fallback-json', false)
|
|
return
|
|
}
|
|
|
|
const adapter = await ensureUniverAdapter()
|
|
log('准备创建 Univer 实例')
|
|
// 强制显示 host 以便 Univer 能获取正确的高度
|
|
host.style.display = 'block'
|
|
// 清空容器内旧元素防止重复挂载
|
|
host.innerHTML = ''
|
|
log(`容器尺寸: 宽=${host.clientWidth}px, 高=${host.clientHeight}px`)
|
|
|
|
univerAdapter = adapter.create({
|
|
container: host,
|
|
locale: 'zh-CN',
|
|
theme: currentTheme(),
|
|
menuFull: true
|
|
})
|
|
univerMode = 'univer'
|
|
|
|
if (!univerAdapter || typeof univerAdapter.getWorkbookJSON !== 'function' || typeof univerAdapter.setWorkbookJSON !== 'function') {
|
|
throw new Error('Univer 适配器接口不完整')
|
|
}
|
|
|
|
// 检查内部 DOM 是否生成
|
|
await new Promise(resolve => setTimeout(resolve, 600))
|
|
const appContainer = host.querySelector('.univer-app-container')
|
|
if (appContainer) {
|
|
log(`已挂载 .univer-app-container, 高度: ${appContainer.clientHeight}px`)
|
|
} else {
|
|
log(`警告: 未找到 .univer-app-container,尝试检查子节点`)
|
|
log(`当前容器内子节点数: ${host.children.length}`)
|
|
if (host.children.length > 0) {
|
|
log(`第一个子节点: ${host.children[0].className} / ${host.children[0].tagName}`)
|
|
}
|
|
throw new Error('Univer 引擎未成功挂载到页面 DOM,请检查 Univer 初始化参数或版本兼容性。')
|
|
}
|
|
|
|
show('cs-fallback-json', false)
|
|
log('Univer 表格控件创建成功')
|
|
}, '本地化资源加载与控件创建')
|
|
|
|
const hasActiveUniverRuntime = () => {
|
|
const host = el('cs-univer')
|
|
const hasMountedDom = !!(host && host.querySelector && host.querySelector('.univer-app-container'))
|
|
const hasAdapterApi = !!(univerAdapter && typeof univerAdapter.getWorkbookJSON === 'function' && typeof univerAdapter.setWorkbookJSON === 'function')
|
|
return hasAdapterApi || hasMountedDom || univerMode === 'univer'
|
|
}
|
|
|
|
const getWorkbookJson = () => {
|
|
if (hasActiveUniverRuntime() && univerAdapter && typeof univerAdapter.getWorkbookJSON === 'function') {
|
|
univerMode = 'univer'
|
|
// 强制提交编辑态后再获取 JSON,确保脏状态的准确性
|
|
if (typeof univerAdapter.commitPendingChanges === 'function') {
|
|
try { univerAdapter.commitPendingChanges() } catch {}
|
|
}
|
|
return toWorkbookString(univerAdapter.getWorkbookJSON())
|
|
}
|
|
const ta = getJsonEditor()
|
|
return toWorkbookString(ta ? ta.value : '')
|
|
}
|
|
const commitWorkbookPendingChanges = async (log) => {
|
|
if (!hasActiveUniverRuntime()) {
|
|
if (log) log('当前未检测到可用的 Univer 运行时,跳过编辑态提交')
|
|
return
|
|
}
|
|
univerMode = 'univer'
|
|
|
|
// Fallback: Dispatch global Enter key event to force exit editing mode just in case adapter method is not exposed
|
|
try {
|
|
const activeEl = document.activeElement
|
|
if (activeEl) {
|
|
if (typeof activeEl.blur === 'function') activeEl.blur()
|
|
const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true })
|
|
activeEl.dispatchEvent(enterEvent)
|
|
}
|
|
} catch {}
|
|
|
|
if (!univerAdapter || typeof univerAdapter.commitPendingChanges !== 'function') {
|
|
if (log) log('当前适配器不支持编辑态提交')
|
|
return
|
|
}
|
|
|
|
const info = await univerAdapter.commitPendingChanges()
|
|
if (!log) return
|
|
if (!info || info.ok === false) {
|
|
log(`提交表格编辑态失败: ${info && info.reason ? info.reason : '未知原因'}`)
|
|
return
|
|
}
|
|
}
|
|
const setWorkbookJson = raw => {
|
|
const content = toWorkbookString(raw)
|
|
if (hasActiveUniverRuntime() && univerAdapter && typeof univerAdapter.setWorkbookJSON === 'function') {
|
|
univerMode = 'univer'
|
|
try {
|
|
univerAdapter.setWorkbookJSON(content)
|
|
} catch (e) {
|
|
log(`设置表格数据失败: ${e.message}`)
|
|
throw e
|
|
}
|
|
return
|
|
}
|
|
const ta = getJsonEditor()
|
|
if (ta) ta.value = content
|
|
}
|
|
const stableCellSignature = cell => {
|
|
if (!cell || typeof cell !== 'object') return null
|
|
const out = {}
|
|
if (Object.prototype.hasOwnProperty.call(cell, 'v')) out.v = cell.v
|
|
if (Object.prototype.hasOwnProperty.call(cell, 'f')) out.f = cell.f
|
|
if (Object.prototype.hasOwnProperty.call(cell, 'm')) out.m = cell.m
|
|
if (Object.keys(out).length === 0) return null
|
|
if (out.v == null && (!out.f || !String(out.f).trim()) && (!out.m || !String(out.m).trim())) return null
|
|
if (typeof out.v === 'string' && !out.v.trim() && (!out.f || !String(out.f).trim()) && (!out.m || !String(out.m).trim())) return null
|
|
return out
|
|
}
|
|
const buildWorkbookContentSignature = workbookJson => {
|
|
try {
|
|
const wb = typeof workbookJson === 'string' ? JSON.parse(workbookJson) : workbookJson
|
|
if (!wb || typeof wb !== 'object' || !wb.sheets || typeof wb.sheets !== 'object') return { sheets: [] }
|
|
const sheetIds = Object.keys(wb.sheets).sort()
|
|
const sheets = []
|
|
sheetIds.forEach(sid => {
|
|
const sheet = wb.sheets[sid]
|
|
if (!sheet || typeof sheet !== 'object') return
|
|
const rows = sheet.cellData && typeof sheet.cellData === 'object' ? sheet.cellData : {}
|
|
const cells = {}
|
|
Object.keys(rows).sort((a, b) => Number(a) - Number(b)).forEach(r => {
|
|
const cols = rows[r]
|
|
if (!cols || typeof cols !== 'object') return
|
|
Object.keys(cols).sort((a, b) => Number(a) - Number(b)).forEach(c => {
|
|
const sig = stableCellSignature(cols[c])
|
|
if (sig) cells[`${r}:${c}`] = sig
|
|
})
|
|
})
|
|
sheets.push({
|
|
id: sid,
|
|
name: String(sheet.name || ''),
|
|
cells
|
|
})
|
|
})
|
|
return { sheets }
|
|
} catch {
|
|
return { sheets: [] }
|
|
}
|
|
}
|
|
const buildPayloadSignature = (title, workbookJson) => JSON.stringify({
|
|
title: String(title == null ? '' : title),
|
|
workbook: buildWorkbookContentSignature(workbookJson)
|
|
})
|
|
const markPendingEdit = () => {
|
|
pendingEditHint = true
|
|
}
|
|
const clearPendingEdit = () => {
|
|
pendingEditHint = false
|
|
}
|
|
const isEditableMutationKey = e => {
|
|
if (!e) return false
|
|
if (e.ctrlKey || e.metaKey || e.altKey) return false
|
|
const key = String(e.key || '')
|
|
if (!key) return false
|
|
if (key.length === 1) return true
|
|
return key === 'Backspace' || key === 'Delete' || key === 'Enter' || key === 'Tab'
|
|
}
|
|
const isEventInsideNode = (event, node) => {
|
|
if (!event || !node) return false
|
|
const target = event.target
|
|
return !!(target && node.contains && node.contains(target))
|
|
}
|
|
const isPotentialUniverEditEvent = event => {
|
|
const host = el('cs-univer')
|
|
if (!host) return false
|
|
if (isEventInsideNode(event, host)) return true
|
|
const activeEl = document.activeElement
|
|
return !!(activeEl && host.contains && host.contains(activeEl))
|
|
}
|
|
const buildCurrentPayload = () => buildPayloadSignature(
|
|
String((el('cs-title') && el('cs-title').value) || ''),
|
|
getWorkbookJson()
|
|
)
|
|
const hasUnsavedChanges = options => {
|
|
const opts = options || {}
|
|
if (buildCurrentPayload() !== lastSavedPayload) return true
|
|
return !!opts.includePendingEdit && pendingEditHint
|
|
}
|
|
const workbookHasVisibleContent = workbookJson => {
|
|
try {
|
|
const wb = typeof workbookJson === 'string' ? JSON.parse(workbookJson) : workbookJson
|
|
if (!wb || typeof wb !== 'object' || !wb.sheets || typeof wb.sheets !== 'object') return false
|
|
for (const sid of Object.keys(wb.sheets)) {
|
|
const sheet = wb.sheets[sid]
|
|
if (!sheet || !sheet.cellData || typeof sheet.cellData !== 'object') continue
|
|
const rows = sheet.cellData
|
|
for (const r of Object.keys(rows)) {
|
|
const cols = rows[r]
|
|
if (!cols || typeof cols !== 'object') continue
|
|
for (const c of Object.keys(cols)) {
|
|
const cell = cols[c]
|
|
if (!cell || typeof cell !== 'object' || !Object.prototype.hasOwnProperty.call(cell, 'v')) continue
|
|
const v = cell.v
|
|
if (v == null) continue
|
|
if (typeof v === 'string') {
|
|
if (v.trim()) return true
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
const shouldPromptUnsaved = () => {
|
|
if (!unsavedPromptEnabled) return false
|
|
if (!hasUnsavedChanges({ includePendingEdit: true })) return false
|
|
const title = String((el('cs-title') && el('cs-title').value) || '').trim()
|
|
const workbookJson = getWorkbookJson()
|
|
if (!title && !workbookHasVisibleContent(workbookJson)) return false
|
|
return true
|
|
}
|
|
const confirmLeaveIfUnsaved = async actionLabel => {
|
|
try {
|
|
await commitWorkbookPendingChanges()
|
|
} catch {}
|
|
if (!shouldPromptUnsaved()) return true
|
|
return askConfirmModal(`你有未保存内容,确认继续${actionLabel}吗?`, '未保存提醒')
|
|
}
|
|
|
|
const get = async url => {
|
|
const fullUrl = (() => {
|
|
const sep = url.includes('?') ? '&' : '?'
|
|
return `${url}${sep}_ts=${Date.now()}`
|
|
})()
|
|
logLine(`请求 GET ${fullUrl}`)
|
|
const r = await fetch(fullUrl, { cache: 'no-store' })
|
|
logLine(`响应状态: ${r.status}`)
|
|
const data = await r.json()
|
|
if (!r.ok || (data && data.ok === false)) throw new Error(`请求失败 ${fullUrl}: ${data && data.error ? data.error : r.status}`)
|
|
return data
|
|
}
|
|
const post = async (url, body) => {
|
|
logLine(`请求 POST ${url}`)
|
|
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })
|
|
logLine(`响应状态: ${r.status}`)
|
|
const data = await r.json().catch(() => null)
|
|
if (!r.ok || (data && data.ok === false)) {
|
|
if (r.status === 409) return { status: r.status, data }
|
|
throw new Error(`请求失败 ${url}: ${data && data.error ? data.error : r.status}`)
|
|
}
|
|
return { status: r.status, data }
|
|
}
|
|
|
|
const renderList = rows => {
|
|
const ul = el('cs-list')
|
|
if (!ul) return
|
|
ul.innerHTML = ''
|
|
const arr = Array.isArray(rows) ? rows : []
|
|
arr.forEach(r => {
|
|
const li = document.createElement('li')
|
|
li.dataset.id = String(r.id || '')
|
|
const flags = []
|
|
if (r.starred) flags.push('⭐')
|
|
if (r.pinned) flags.push('📌')
|
|
li.innerHTML = `<div class="name">${esc(r.title || '')}</div><div class="meta">${esc(String(r.updated_at || ''))} ${flags.join(' ')}</div>`
|
|
li.onclick = () => selectSheet(r.id)
|
|
if (String(currentId) === String(r.id)) li.classList.add('active')
|
|
ul.appendChild(li)
|
|
})
|
|
}
|
|
|
|
const loadListCore = async () => {
|
|
const endpoint = currentTab === 'trash' ? '/api/cloud_sheets/sheet/trash_list' : '/api/cloud_sheets/sheet/list'
|
|
const j = await get(endpoint)
|
|
renderList(Array.isArray(j.rows) ? j.rows : [])
|
|
}
|
|
const loadList = async withModal => {
|
|
if (withModal === false) return loadListCore()
|
|
return runAction('加载表格列表', async log => {
|
|
log(currentTab === 'trash' ? '当前视图: 回收站' : '当前视图: 列表')
|
|
await loadListCore()
|
|
log('列表渲染完成')
|
|
})
|
|
}
|
|
|
|
const markActive = id => {
|
|
const ul = el('cs-list')
|
|
if (!ul) return
|
|
ul.querySelectorAll('li').forEach(li => li.classList.toggle('active', String(li.dataset.id || '') === String(id || '')))
|
|
}
|
|
|
|
const renderAttachments = rows => {
|
|
const box = el('cs-attachments')
|
|
if (!box) return
|
|
box.innerHTML = ''
|
|
const list = Array.isArray(rows) ? rows : []
|
|
if (!list.length) return
|
|
list.forEach(r => {
|
|
const row = document.createElement('div')
|
|
row.className = 'cs-attach-row'
|
|
const left = document.createElement('a')
|
|
left.href = r.file_path
|
|
left.target = '_blank'
|
|
left.textContent = r.filename || ''
|
|
const right = document.createElement('button')
|
|
right.className = 'mini-btn'
|
|
right.textContent = '删除'
|
|
right.onclick = async () => {
|
|
await runAction('删除附件', async log => {
|
|
log(`附件ID: ${r.id}`)
|
|
await fetch(`/api/cloud_sheets/attachments/${encodeURIComponent(r.id)}`, { method: 'DELETE' })
|
|
await loadAttachments(currentId, false)
|
|
log('附件删除并刷新完成')
|
|
})
|
|
}
|
|
row.appendChild(left)
|
|
row.appendChild(right)
|
|
box.appendChild(row)
|
|
})
|
|
}
|
|
|
|
const loadAttachmentsCore = async sheetId => {
|
|
if (!sheetId) {
|
|
renderAttachments([])
|
|
return
|
|
}
|
|
const j = await get(`/api/cloud_sheets/attachments?sheet_id=${encodeURIComponent(sheetId)}`)
|
|
renderAttachments(j.rows || [])
|
|
}
|
|
const loadAttachments = async (sheetId, withModal) => {
|
|
if (withModal === false) return loadAttachmentsCore(sheetId)
|
|
return runAction('加载附件', async log => {
|
|
log(`表格ID: ${sheetId}`)
|
|
await loadAttachmentsCore(sheetId)
|
|
log('附件渲染完成')
|
|
})
|
|
}
|
|
|
|
const showHomeView = () => {
|
|
const hv = el('cs-home-view')
|
|
const ev = el('cs-editor-view')
|
|
if (hv) hv.style.display = 'flex'
|
|
if (ev) ev.style.display = 'none'
|
|
}
|
|
|
|
const showEditorView = () => {
|
|
const hv = el('cs-home-view')
|
|
const ev = el('cs-editor-view')
|
|
if (hv) hv.style.display = 'none'
|
|
if (ev) ev.style.display = 'flex'
|
|
}
|
|
|
|
const selectSheetCore = async id => {
|
|
const j = await get(`/api/cloud_sheets/sheet/get?id=${encodeURIComponent(id)}`)
|
|
if (j && j.row) {
|
|
currentId = j.row.id
|
|
currentVersionId = Number(j.row.version_id || 0)
|
|
if (el('cs-title')) el('cs-title').value = j.row.title || ''
|
|
setWorkbookJson(j.row.workbook_json || '')
|
|
lastSavedPayload = buildPayloadSignature(String(j.row.title || ''), toWorkbookString(j.row.workbook_json || ''))
|
|
clearPendingEdit()
|
|
markActive(currentId)
|
|
await loadAttachmentsCore(currentId)
|
|
showEditorView()
|
|
}
|
|
}
|
|
const selectSheet = async id => {
|
|
if (!id) return
|
|
return runAction('打开表格', async log => {
|
|
log(`目标ID: ${id}`)
|
|
if (!(await confirmLeaveIfUnsaved('切换表格'))) {
|
|
log('用户取消切换,保留当前编辑')
|
|
return
|
|
}
|
|
if (shouldPromptUnsaved()) log('检测到未保存改动,用户选择继续切换')
|
|
await selectSheetCore(id)
|
|
log('表格内容写入编辑器完成')
|
|
}, `ID=${id}`)
|
|
}
|
|
|
|
const saveSheetCore = async (force, log) => {
|
|
if (log) log(`运行时检测: univerMode=${univerMode}, hasRuntime=${hasActiveUniverRuntime() ? '是' : '否'}`)
|
|
// 先让原生焦点离开输入控件
|
|
if (document.activeElement && typeof document.activeElement.blur === 'function') {
|
|
try { document.activeElement.blur() } catch {}
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
// 再调用 Univer 自带的“结束编辑并保存”API,确保当前正在输入的单元格真正提交进数据模型
|
|
await commitWorkbookPendingChanges(log)
|
|
await new Promise(resolve => setTimeout(resolve, 120))
|
|
|
|
const title = String((el('cs-title') && el('cs-title').value) || '').trim() || '未命名表格'
|
|
const workbook_json = getWorkbookJson()
|
|
if (log) {
|
|
log(`获取到底层表格数据: ${typeof workbook_json === 'string' ? workbook_json.length + ' 字符' : Object.keys(workbook_json).length + ' 个键'}`)
|
|
if (typeof workbook_json === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(workbook_json)
|
|
let sheetCount = 0
|
|
if (parsed.sheets) {
|
|
for (const key in parsed.sheets) {
|
|
sheetCount++
|
|
const sheet = parsed.sheets[key]
|
|
const a1 = sheet.cellData && sheet.cellData['0'] && sheet.cellData['0']['0']
|
|
log(`[${sheet.name}] A1: ${JSON.stringify(a1)}`)
|
|
}
|
|
}
|
|
log(`共找到 ${sheetCount} 个 Sheet 快照`)
|
|
} catch(e) {
|
|
log(`解析快照失败: ${e.message}`)
|
|
}
|
|
}
|
|
}
|
|
const preview_text = previewTextFromWorkbook(workbook_json)
|
|
if (log) log(`请求保存,force=${!!force}`)
|
|
const { status: httpStatus, data } = await post('/api/cloud_sheets/sheet/save', {
|
|
id: currentId || 0,
|
|
title,
|
|
workbook_json,
|
|
preview_text,
|
|
base_version_id: currentVersionId || 0,
|
|
force: !!force
|
|
})
|
|
if (httpStatus === 409 || (data && data.conflict)) return { conflict: true, data }
|
|
if (data && data.ok) {
|
|
if (log) log(`保存成功,返回ID: ${data.id}, 版本: ${data.version_id}`)
|
|
currentId = data.id || currentId
|
|
currentVersionId = Number(data.version_id || currentVersionId || 0)
|
|
lastSavedPayload = buildPayloadSignature(title, toWorkbookString(workbook_json))
|
|
clearPendingEdit()
|
|
await loadListCore()
|
|
markActive(currentId)
|
|
await loadAttachmentsCore(currentId)
|
|
} else {
|
|
if (log) log(`保存失败,返回值异常`)
|
|
}
|
|
return { conflict: false, data }
|
|
}
|
|
const saveSheet = async force => runAction('保存表格', async log => {
|
|
log(`当前ID: ${currentId || 0}`)
|
|
const out = await saveSheetCore(force, log)
|
|
if (out.conflict) {
|
|
log('检测到版本冲突')
|
|
const go = await askConfirmModal('检测到版本冲突,是否覆盖保存?', '版本冲突')
|
|
if (!go) throw new Error('用户取消覆盖保存')
|
|
log('用户确认覆盖保存')
|
|
await saveSheetCore(true, log)
|
|
}
|
|
log('保存成功并刷新列表')
|
|
}, '写入数据库并生成版本')
|
|
|
|
const newSheet = async () => runAction('新建表格', async log => {
|
|
if (!(await confirmLeaveIfUnsaved('新建表格'))) {
|
|
log('用户取消新建,保留当前编辑')
|
|
return
|
|
}
|
|
if (shouldPromptUnsaved()) log('检测到未保存改动,用户选择继续新建')
|
|
currentId = 0
|
|
currentVersionId = 0
|
|
if (el('cs-title')) el('cs-title').value = ''
|
|
setWorkbookJson(makeDefaultWorkbook())
|
|
lastSavedPayload = buildCurrentPayload()
|
|
clearPendingEdit()
|
|
markActive(0)
|
|
renderAttachments([])
|
|
showEditorView()
|
|
log('已创建空白表格并重置状态')
|
|
})
|
|
|
|
const cloneSheet = async () => {
|
|
if (!currentId) {
|
|
await showInfoModal('请先选择一个表格', '无法克隆')
|
|
return
|
|
}
|
|
return runAction('克隆表格', async log => {
|
|
const title = String((el('cs-title') && el('cs-title').value) || '').trim() || '未命名表格'
|
|
const d = new Date()
|
|
const cloneTitle = `${title}-复制${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
const workbook_json = getWorkbookJson()
|
|
const preview_text = previewTextFromWorkbook(workbook_json)
|
|
log(`新标题: ${cloneTitle}`)
|
|
const { data } = await post('/api/cloud_sheets/sheet/save', { id: 0, title: cloneTitle, workbook_json, preview_text })
|
|
if (!data || !data.ok || !data.id) throw new Error('克隆保存失败')
|
|
await post('/api/cloud_sheets/sheet/clone_attachments', { src_id: currentId, dest_id: data.id })
|
|
currentId = data.id
|
|
currentVersionId = Number(data.version_id || 0)
|
|
if (el('cs-title')) el('cs-title').value = cloneTitle
|
|
lastSavedPayload = buildPayloadSignature(cloneTitle, toWorkbookString(workbook_json))
|
|
clearPendingEdit()
|
|
await loadListCore()
|
|
markActive(currentId)
|
|
await loadAttachmentsCore(currentId)
|
|
log('克隆完成')
|
|
})
|
|
}
|
|
|
|
const toggleStar = async () => {
|
|
if (!currentId) return
|
|
return runAction('切换加星', async log => {
|
|
const li = document.querySelector(`.cs-list li[data-id="${currentId}"]`)
|
|
const isStar = !!(li && li.textContent && li.textContent.includes('⭐'))
|
|
await post('/api/cloud_sheets/sheet/star', { id: currentId, starred: !isStar })
|
|
await loadListCore()
|
|
markActive(currentId)
|
|
log(`结果: ${!isStar ? '已加星' : '已取消加星'}`)
|
|
})
|
|
}
|
|
|
|
const togglePin = async () => {
|
|
if (!currentId) return
|
|
return runAction('切换置顶', async log => {
|
|
const li = document.querySelector(`.cs-list li[data-id="${currentId}"]`)
|
|
const isPin = !!(li && li.textContent && li.textContent.includes('📌'))
|
|
await post('/api/cloud_sheets/sheet/pin', { id: currentId, pinned: !isPin })
|
|
await loadListCore()
|
|
markActive(currentId)
|
|
log(`结果: ${!isPin ? '已置顶' : '已取消置顶'}`)
|
|
})
|
|
}
|
|
|
|
const softDelete = async () => {
|
|
if (!currentId) return
|
|
const ok = await askConfirmModal('删除后进入回收站,确认继续吗?', '确认删除')
|
|
if (!ok) return
|
|
return runAction('删除到回收站', async log => {
|
|
await post('/api/cloud_sheets/sheet/trash', { id: currentId })
|
|
currentId = 0
|
|
currentVersionId = 0
|
|
if (el('cs-title')) el('cs-title').value = ''
|
|
setWorkbookJson(makeDefaultWorkbook())
|
|
lastSavedPayload = buildCurrentPayload()
|
|
clearPendingEdit()
|
|
markActive(0)
|
|
renderAttachments([])
|
|
await loadListCore()
|
|
log('已删除并刷新列表')
|
|
})
|
|
}
|
|
|
|
const restoreOrHardDeleteFromTrash = async id => {
|
|
if (!id) return
|
|
return runAction('回收站操作', async log => {
|
|
const choice = await askDialog({
|
|
title: '回收站操作',
|
|
message: '请选择要执行的操作:',
|
|
buttons: [
|
|
{ label: '取消', value: 'cancel' },
|
|
{ label: '还原', value: 'restore' },
|
|
{ label: '彻底删除', value: 'hard' }
|
|
]
|
|
})
|
|
if (choice === 'hard') {
|
|
await post('/api/cloud_sheets/sheet/trash_delete', { id })
|
|
log('执行彻底删除')
|
|
} else if (choice === 'restore') {
|
|
await post('/api/cloud_sheets/sheet/restore', { id })
|
|
log('执行还原')
|
|
} else {
|
|
log('用户取消回收站操作')
|
|
return
|
|
}
|
|
await loadListCore()
|
|
log('回收站列表刷新完成')
|
|
})
|
|
}
|
|
|
|
const openVersions = async () => {
|
|
if (!currentId) return
|
|
return runAction('加载版本列表', async log => {
|
|
const j = await get(`/api/cloud_sheets/sheet/versions?id=${encodeURIComponent(currentId)}`)
|
|
const box = el('cs-versions-list')
|
|
if (!box) throw new Error('缺少版本列表容器')
|
|
const versions = Array.isArray(j.versions) ? j.versions : []
|
|
box.innerHTML = ''
|
|
versions.forEach(v => {
|
|
const row = document.createElement('div')
|
|
row.className = 'cs-version-row'
|
|
const l = document.createElement('div')
|
|
l.textContent = `${v.saved_at || ''} · ${v.title || ''}`
|
|
const r = document.createElement('button')
|
|
r.className = 'mini-btn'
|
|
r.textContent = '恢复到编辑器'
|
|
r.onclick = () => {
|
|
if (el('cs-title')) el('cs-title').value = v.title || ''
|
|
setWorkbookJson(v.workbook_json || '')
|
|
currentVersionId = Number(v.id || currentVersionId || 0)
|
|
clearPendingEdit()
|
|
show('cs-versions-modal', false)
|
|
}
|
|
row.appendChild(l)
|
|
row.appendChild(r)
|
|
box.appendChild(row)
|
|
})
|
|
show('cs-versions-modal', true)
|
|
log(`版本数: ${versions.length}`)
|
|
})
|
|
}
|
|
|
|
const doSearch = async () => runAction('搜索表格', async log => {
|
|
const input = el('cs-search-input')
|
|
if (!input) throw new Error('缺少搜索输入框')
|
|
const q = String(input.value || '').trim()
|
|
if (!q) {
|
|
if (el('cs-search-results')) el('cs-search-results').innerHTML = ''
|
|
log('关键字为空,已清空结果')
|
|
return
|
|
}
|
|
const j = await get(`/api/cloud_sheets/sheet/search?q=${encodeURIComponent(q)}`)
|
|
const rows = Array.isArray(j.rows) ? j.rows : []
|
|
const box = el('cs-search-results')
|
|
if (!box) throw new Error('缺少搜索结果容器')
|
|
box.innerHTML = ''
|
|
rows.forEach(r => {
|
|
const item = document.createElement('div')
|
|
item.className = 'cs-search-item'
|
|
item.innerHTML = `<div>${esc(r.title || '')}</div><div>${esc((r.preview_text || '').slice(0, 120))}</div>`
|
|
item.onclick = async () => {
|
|
show('cs-search-layer', false)
|
|
await selectSheet(r.id)
|
|
}
|
|
box.appendChild(item)
|
|
})
|
|
log(`命中条数: ${rows.length}`)
|
|
})
|
|
|
|
const attachFile = async file => {
|
|
if (!currentId) {
|
|
await showInfoModal('请先保存表格再上传附件', '无法上传')
|
|
return
|
|
}
|
|
return runAction('上传附件', async log => {
|
|
log(`文件名: ${file.name || ''}`)
|
|
const r = await fetch(`/api/cloud_sheets/attachment?sheet_id=${encodeURIComponent(currentId)}&filename=${encodeURIComponent(file.name)}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
body: file
|
|
})
|
|
const j = await r.json()
|
|
if (!r.ok || !j || j.ok === false) throw new Error(`上传失败: ${j && j.error ? j.error : r.status}`)
|
|
await loadAttachmentsCore(currentId)
|
|
log('上传完成并刷新附件列表')
|
|
})
|
|
}
|
|
|
|
const ensureAuth = async () => {
|
|
let authOk = false
|
|
try {
|
|
const j = await get('/api/cloud_sheets/me')
|
|
if (j && j.ok) {
|
|
me = j.user || null
|
|
authOk = true
|
|
}
|
|
} catch (e) {
|
|
console.warn('Auth check failed', e)
|
|
}
|
|
|
|
if (authOk) {
|
|
show('cs-login', false)
|
|
show('cs-app', true)
|
|
showHomeView()
|
|
try {
|
|
await initSheetEngine()
|
|
} catch (err) {
|
|
// 如果引擎加载失败,不退出登录,保留界面以便查看错误日志
|
|
}
|
|
try {
|
|
await loadList()
|
|
} catch {}
|
|
return
|
|
}
|
|
|
|
show('cs-app', false)
|
|
show('cs-login', true)
|
|
}
|
|
|
|
const initLoginBackground = () => {
|
|
const loginContainer = el('cs-login')
|
|
if (!loginContainer) return
|
|
|
|
// 获取背景图片目录
|
|
const bgImages = [
|
|
'/home-bg/blue-sky-bf240d20/1.jpg',
|
|
'/home-bg/blue-sky-bf240d20/2.jpg',
|
|
'/home-bg/business-minimal-434facc4/1.jpg',
|
|
'/home-bg/business-minimal-434facc4/2.jpg',
|
|
'/home-bg/colorful-abstract-9b5b4d8c/1.jpg',
|
|
'/home-bg/colorful-abstract-9b5b4d8c/2.jpg',
|
|
'/home-bg/gradient-3d-9d6b3a3c/1.jpg',
|
|
'/home-bg/gradient-3d-9d6b3a3c/2.jpg',
|
|
'/home-bg/nature-landscape-8c0d5a7f/1.jpg',
|
|
'/home-bg/nature-landscape-8c0d5a7f/2.jpg',
|
|
'/home-bg/tech-futuristic-7a3b8c9d/1.jpg',
|
|
'/home-bg/tech-futuristic-7a3b8c9d/2.jpg',
|
|
'/home-bg/warm-minimal-5e4c3b2a/1.jpg',
|
|
'/home-bg/warm-minimal-5e4c3b2a/2.jpg'
|
|
]
|
|
|
|
// 随机选择一张背景图片
|
|
const randomIndex = Math.floor(Math.random() * bgImages.length)
|
|
const selectedBg = bgImages[randomIndex]
|
|
|
|
// 设置背景样式
|
|
loginContainer.style.background = `url('${selectedBg}') center/cover no-repeat`
|
|
loginContainer.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3)'
|
|
loginContainer.style.border = 'none'
|
|
loginContainer.style.backdropFilter = 'blur(10px)'
|
|
loginContainer.style.color = 'white'
|
|
|
|
// 设置输入框样式
|
|
const inputs = loginContainer.querySelectorAll('.modal-input')
|
|
inputs.forEach(input => {
|
|
input.style.background = 'rgba(255, 255, 255, 0.9)'
|
|
input.style.border = '1px solid rgba(255, 255, 255, 0.3)'
|
|
input.style.color = '#333'
|
|
})
|
|
|
|
// 设置按钮样式
|
|
const loginBtn = el('cs-login-btn')
|
|
if (loginBtn) {
|
|
loginBtn.style.background = 'rgba(24, 144, 255, 0.9)'
|
|
loginBtn.style.border = 'none'
|
|
loginBtn.style.color = 'white'
|
|
}
|
|
}
|
|
|
|
const login = async () => {
|
|
const username = String((el('cs-username') && el('cs-username').value) || '').trim()
|
|
const password = String((el('cs-password') && el('cs-password').value) || '')
|
|
if (!username || !password) {
|
|
status('cs-login-status', '请输入用户名和密码')
|
|
return
|
|
}
|
|
try {
|
|
const loginBtn = el('cs-login-btn')
|
|
if (loginBtn) loginBtn.disabled = true
|
|
|
|
await runAction('登录云表格', async log => {
|
|
log(`用户名: ${username}`)
|
|
const { data } = await post('/api/cloud_sheets/login', { username, password })
|
|
if (!data || !data.ok) throw new Error('登录失败')
|
|
status('cs-login-status', '')
|
|
await ensureAuth()
|
|
})
|
|
} catch {
|
|
status('cs-login-status', '登录失败')
|
|
show('cs-login-status', true)
|
|
} finally {
|
|
const loginBtn = el('cs-login-btn')
|
|
if (loginBtn) loginBtn.disabled = false
|
|
}
|
|
}
|
|
|
|
const logout = async () => {
|
|
try {
|
|
await runAction('退出登录', async () => {
|
|
await post('/api/cloud_sheets/logout', {})
|
|
me = null
|
|
show('cs-app', false)
|
|
show('cs-login', true)
|
|
})
|
|
} catch {}
|
|
}
|
|
|
|
const toggleTrashTab = async () => {
|
|
currentTab = currentTab === 'trash' ? 'list' : 'trash'
|
|
txt('cs-trash-tab', currentTab === 'trash' ? '返回列表' : '回收站')
|
|
await loadList()
|
|
}
|
|
|
|
const toggleAttachPanel = () => {
|
|
attachPanelOpen = !attachPanelOpen
|
|
show('cs-attach-panel', attachPanelOpen)
|
|
}
|
|
|
|
const toggleFullscreen = () => {
|
|
isFullscreen = !isFullscreen
|
|
document.body.classList.toggle('cs-fullscreen', isFullscreen)
|
|
txt('cs-fullscreen', isFullscreen ? '缩屏' : '全屏')
|
|
}
|
|
|
|
const bind = () => {
|
|
const tbtn = el('cs-toggle-theme')
|
|
if (tbtn) tbtn.onclick = () => applyTheme(currentTheme() === 'dark' ? 'light' : 'dark')
|
|
const loginBtn = el('cs-login-btn')
|
|
if (loginBtn) loginBtn.onclick = login
|
|
;(async function tryAutoFill() {
|
|
try {
|
|
const token = localStorage.getItem('toolbox_creds_token') || ''
|
|
const url = '/api/cloud_sheets/auth/local_creds' + (token ? '?token=' + encodeURIComponent(token) : '')
|
|
const r = await fetch(url, { credentials: 'same-origin', cache: 'no-store' })
|
|
const d = await r.json().catch(() => null)
|
|
if (d && d.ok && d.creds) {
|
|
if (d.creds.username) el('cs-username').value = d.creds.username
|
|
if (d.creds.password) el('cs-password').value = d.creds.password
|
|
}
|
|
} catch {}
|
|
el('cs-username').removeAttribute('readonly')
|
|
el('cs-password').removeAttribute('readonly')
|
|
})()
|
|
const pwd = el('cs-password')
|
|
if (pwd) pwd.addEventListener('keydown', e => { if (e.key === 'Enter') login() })
|
|
const logoutBtn = el('cs-logout-btn')
|
|
if (logoutBtn) logoutBtn.onclick = logout
|
|
const newBtn = el('cs-new')
|
|
if (newBtn) newBtn.onclick = () => { newSheet() }
|
|
const refreshBtn = el('cs-refresh-list')
|
|
if (refreshBtn) refreshBtn.onclick = () => { loadList() }
|
|
const saveBtn = el('cs-save')
|
|
if (saveBtn) saveBtn.onclick = () => { saveSheet(false) }
|
|
const titleInput = el('cs-title')
|
|
if (titleInput) {
|
|
titleInput.addEventListener('input', markPendingEdit)
|
|
titleInput.addEventListener('change', markPendingEdit)
|
|
titleInput.addEventListener('paste', markPendingEdit)
|
|
}
|
|
const cloneBtn = el('cs-clone')
|
|
if (cloneBtn) cloneBtn.onclick = () => { cloneSheet() }
|
|
const starBtn = el('cs-star')
|
|
if (starBtn) starBtn.onclick = () => { toggleStar() }
|
|
const pinBtn = el('cs-pin')
|
|
if (pinBtn) pinBtn.onclick = () => { togglePin() }
|
|
const delBtn = el('cs-delete')
|
|
if (delBtn) delBtn.onclick = () => { softDelete() }
|
|
const trashBtn = el('cs-trash-tab')
|
|
if (trashBtn) trashBtn.onclick = () => { toggleTrashTab() }
|
|
const fullBtn = el('cs-fullscreen')
|
|
if (fullBtn) fullBtn.onclick = toggleFullscreen
|
|
const attachToggle = el('cs-attach-toggle')
|
|
if (attachToggle) attachToggle.onclick = toggleAttachPanel
|
|
const attachBtn = el('cs-attach-btn')
|
|
if (attachBtn) attachBtn.onclick = () => { const n = el('cs-attach-file'); if (n) n.click() }
|
|
const attachInput = el('cs-attach-file')
|
|
if (attachInput) attachInput.onchange = e => { const f = e.target.files && e.target.files[0]; if (f) attachFile(f) }
|
|
const searchOpen = el('cs-search-open')
|
|
if (searchOpen) searchOpen.onclick = () => show('cs-search-layer', true)
|
|
const searchClose = el('cs-search-close')
|
|
if (searchClose) searchClose.onclick = () => show('cs-search-layer', false)
|
|
const searchInput = el('cs-search-input')
|
|
if (searchInput) searchInput.onkeydown = e => { if (e.key === 'Enter') doSearch() }
|
|
const versionsBtn = el('cs-version-btn')
|
|
if (versionsBtn) versionsBtn.onclick = () => { openVersions() }
|
|
const versionsClose = el('cs-versions-close')
|
|
if (versionsClose) versionsClose.onclick = () => show('cs-versions-modal', false)
|
|
const loadingClose = el('cs-loading-close')
|
|
if (loadingClose) loadingClose.onclick = closeActionModal
|
|
const loadingCopy = el('cs-loading-copy')
|
|
if (loadingCopy) loadingCopy.onclick = () => {
|
|
const logs = el('cs-loading-logs')
|
|
if (logs) {
|
|
const text = Array.from(logs.querySelectorAll('.cs-log-line')).map(n => n.textContent).join('\n')
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const orig = loadingCopy.textContent
|
|
loadingCopy.textContent = '已复制!'
|
|
setTimeout(() => { loadingCopy.textContent = orig }, 2000)
|
|
}).catch(async err => { await showInfoModal('复制失败: ' + err.message, '复制失败') })
|
|
}
|
|
}
|
|
const dialogClose = el('cs-dialog-close')
|
|
if (dialogClose) dialogClose.onclick = () => {
|
|
show('cs-dialog', false)
|
|
if (dialogResolver) {
|
|
const r = dialogResolver
|
|
dialogResolver = null
|
|
r(false)
|
|
}
|
|
}
|
|
|
|
// 增加重载引擎按钮,方便调试容器状态
|
|
const reloadBtn = document.createElement('button')
|
|
reloadBtn.textContent = '重载引擎'
|
|
reloadBtn.onclick = async () => {
|
|
univerMode = 'none'
|
|
if (univerAdapter && univerAdapter.dispose) {
|
|
try { univerAdapter.dispose() } catch {}
|
|
}
|
|
univerAdapter = null
|
|
await initSheetEngine()
|
|
}
|
|
const actionsBox = document.querySelector('.cs-right-header .actions')
|
|
if (actionsBox) actionsBox.appendChild(reloadBtn)
|
|
|
|
window.addEventListener('keydown', e => {
|
|
const k = String(e.key || '').toLowerCase()
|
|
if ((e.ctrlKey || e.metaKey) && k === 's') {
|
|
e.preventDefault()
|
|
saveSheet(false)
|
|
}
|
|
})
|
|
window.addEventListener('beforeinput', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('input', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('compositionend', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('paste', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('cut', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('drop', e => {
|
|
if (isPotentialUniverEditEvent(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('keydown', e => {
|
|
if (isPotentialUniverEditEvent(e) && isEditableMutationKey(e)) markPendingEdit()
|
|
}, true)
|
|
window.addEventListener('beforeunload', e => {
|
|
if (!shouldPromptUnsaved()) return
|
|
e.preventDefault()
|
|
e.returnValue = ''
|
|
})
|
|
const list = el('cs-list')
|
|
if (list) list.addEventListener('contextmenu', async e => {
|
|
const li = e.target && e.target.closest ? e.target.closest('li[data-id]') : null
|
|
if (!li) return
|
|
if (currentTab !== 'trash') return
|
|
e.preventDefault()
|
|
const id = parseInt(String(li.dataset.id || '0'), 10) || 0
|
|
await restoreOrHardDeleteFromTrash(id)
|
|
})
|
|
}
|
|
|
|
const start = async () => {
|
|
try {
|
|
const saved = localStorage.getItem(themeKey)
|
|
if (saved === 'light' || saved === 'dark') applyTheme(saved)
|
|
else applyTheme('dark')
|
|
} catch {}
|
|
bind()
|
|
initLoginBackground()
|
|
await ensureAuth()
|
|
}
|
|
|
|
return { start }
|
|
})()
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
cs.start()
|
|
})
|