Why: the adjudicated plan (D1-D4) calls for office/link previews with
loaders injected from the host, right-rail light preview alongside the
overlay viewer, and artifact deletion kept as an API capability with no
UI entry.
Evidence: previewsetup.ts injects docx (mammoth), xlsx (SheetJS, sheet
tabs, 200-row cap), pptx (pptx-preview full-fidelity; pptx2html was
dropped for a broken module entry — tXml case mismatch breaks Linux
builds — per the D2 fallback clause), and the /api/url-preview fetcher,
all via dynamic import: the main entry grows ~12KB while xlsx (430KB),
pptx (1.2MB) and jszip (98KB) load on first preview. DocViewer becomes a
FilePreview container (keeps text-family editing); Workbench gains a
preview tab with rail-preview state (store.railPreview); artifact chips
and panel rows gain a rail-preview action; the panel delete button is
removed while api.deleteArtifact and DELETE /api/artifacts/{id} remain.
tsc clean; vitest 33 passed; production build succeeds.
Impact: previews work end to end for the whole format matrix; deletion
stays plugin-reachable only.
Refs: task feature/file-preview-caps; ui fb76239; core 4d29301; backend
6d4ccfd.
86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
// 预览能力装配(裁定 D1–D4):把重解析器经 configureFilePreviewLoaders 注入 @ui。
|
||
// 全部 dynamic import——对应格式首次预览时才下载 chunk,主 bundle 不含解析器。
|
||
// 链接预览 fetcher 走服务端 /api/url-preview(SSRF 防护在服务端执行端)。
|
||
import { configureFilePreviewLoaders, configureLinkPreviewFetcher } from '@ui'
|
||
import { auth } from './auth'
|
||
import type { LinkPreviewData } from '@ui'
|
||
|
||
const fetchBuf = async (url: string | undefined): Promise<ArrayBuffer> => {
|
||
const r = await fetch(url!)
|
||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||
return r.arrayBuffer()
|
||
}
|
||
|
||
const docxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
|
||
const mammoth = await import('mammoth')
|
||
const { value } = await mammoth.convertToHtml({ arrayBuffer: await fetchBuf(source.contentUrl) })
|
||
el.innerHTML = value
|
||
}
|
||
|
||
const xlsxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
|
||
const XLSX = await import('xlsx')
|
||
const wb = XLSX.read(await fetchBuf(source.contentUrl), { type: 'array' })
|
||
el.innerHTML = ''
|
||
const tabs = document.createElement('div')
|
||
tabs.className = 'ui-fp-sheet-tabs'
|
||
const wrap = document.createElement('div')
|
||
wrap.className = 'ui-fp-sheet-wrap'
|
||
el.append(tabs, wrap)
|
||
wb.SheetNames.forEach((name, i) => {
|
||
const tab = document.createElement('button')
|
||
tab.className = `ui-fp-sheet-tab ${i === 0 ? 'on' : ''}`
|
||
tab.textContent = name
|
||
tab.onclick = () => {
|
||
tabs.querySelectorAll('.ui-fp-sheet-tab').forEach(t => t.classList.remove('on'))
|
||
tab.classList.add('on')
|
||
renderSheet(wrap, XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[name], { header: 1, defval: '' }))
|
||
}
|
||
tabs.append(tab)
|
||
if (i === 0) renderSheet(wrap, XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[name], { header: 1, defval: '' }))
|
||
})
|
||
}
|
||
|
||
function renderSheet(wrap: HTMLElement, rows: unknown[][]) {
|
||
wrap.innerHTML = ''
|
||
if (rows.length > 200) {
|
||
const note = document.createElement('div')
|
||
note.className = 'ui-fp-note'
|
||
note.textContent = `仅显示前 200 行(共 ${rows.length} 行)——完整内容请下载`
|
||
wrap.append(note)
|
||
}
|
||
const table = document.createElement('table')
|
||
table.className = 'ui-fp-sheet'
|
||
for (const row of rows.slice(0, 200)) {
|
||
const tr = document.createElement('tr')
|
||
for (const cell of row) {
|
||
const td = document.createElement('td')
|
||
td.textContent = cell == null ? '' : String(cell)
|
||
td.title = td.textContent
|
||
tr.append(td)
|
||
}
|
||
table.append(tr)
|
||
}
|
||
wrap.append(table)
|
||
}
|
||
|
||
const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
|
||
// pptx2html@0.3.4 的 module 入口引用了大小写错配的 ./txml(Linux 打包必炸),
|
||
// 按 D2 备选条款改用 pptx-preview(init(dom).preview(arrayBuffer) 全保真渲染)。
|
||
const { init } = await import('pptx-preview')
|
||
el.innerHTML = ''
|
||
const stage = document.createElement('div')
|
||
stage.className = 'ui-fp-slide-stage'
|
||
el.append(stage)
|
||
const previewer = init(stage, { width: stage.clientWidth || 900 })
|
||
await previewer.preview(await fetchBuf(source.contentUrl))
|
||
}
|
||
|
||
export function wirePreviewCapabilities() {
|
||
configureFilePreviewLoaders({ docx: docxLoader, xlsx: xlsxLoader, pptx: pptxLoader })
|
||
configureLinkPreviewFetcher(async href => {
|
||
const r = await fetch(auth.withKey(`/api/url-preview?url=${encodeURIComponent(href)}`))
|
||
if (!r.ok) return { url: href, error: `HTTP ${r.status}` }
|
||
return (await r.json()) as LinkPreviewData
|
||
})
|
||
}
|