fix(workbench): complete profile and channel flows

This commit is contained in:
2026-07-24 14:46:38 +08:00
parent d7eb20a85f
commit f430566976
21 changed files with 563 additions and 151 deletions

View File

@@ -33,8 +33,6 @@ import ReactMarkdown from 'react-markdown'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
import DOMPurify from 'dompurify'
import mammoth from 'mammoth'
import { readSheet as readXlsxSheet } from 'read-excel-file/browser'
import type { ApiSession } from '../../api/client'
import {
createDocumentFolder,
@@ -79,6 +77,8 @@ type UploadItem = {
error?: string
}
type PersistedDocumentDraft = Pick<DocumentTab, 'key' | 'documentId' | 'parentId' | 'name' | 'extension' | 'mimeType' | 'markdown' | 'revision' | 'status' | 'mode'>
export function ProjectDocuments({
session,
projectId,
@@ -167,17 +167,20 @@ export function ProjectDocuments({
void refreshTree().then(async (items) => {
if (cancelled) return
const stored = readStoredTabs(projectId)
const drafts = readStoredDrafts(projectId)
const draftByDocumentID = new Map(drafts.filter((draft) => draft.documentId).map((draft) => [draft.documentId!, draft]))
const restored: DocumentTab[] = []
for (const id of stored) {
const item = items.find((document) => document.id === id && document.kind === 'file')
if (!item) continue
try {
const detail = await getDocument(session, projectId, id)
if (!cancelled) restored.push(tabFromDocument(detail))
if (!cancelled) restored.push(restoreDocumentDraft(tabFromDocument(detail), draftByDocumentID.get(id)))
} catch {
// Missing tabs are discarded.
}
}
restored.push(...drafts.filter((draft) => !draft.documentId))
if (cancelled) return
if (restored.length > 0) {
setTabs(restored)
@@ -201,6 +204,8 @@ export function ProjectDocuments({
useEffect(() => {
const ids = tabs.flatMap((tab) => tab.documentId ? [tab.documentId] : [])
localStorage.setItem(storageKey(projectId), JSON.stringify(ids))
const drafts = tabs.filter((tab) => tab.status === 'dirty' || tab.status === 'saving' || tab.status === 'error')
localStorage.setItem(draftStorageKey(projectId), JSON.stringify(drafts))
}, [projectId, tabs])
useEffect(() => {
@@ -689,10 +694,12 @@ function SmartPreview({ session, projectId, tab }: { session: ApiSession; projec
if (isText(extension, tab.mimeType)) {
setText(await blob.text())
} else if (extension === '.docx') {
const result = await mammoth.convertToHtml({ arrayBuffer: await blob.arrayBuffer() })
const mammoth = await import('mammoth')
const result = await mammoth.default.convertToHtml({ arrayBuffer: await blob.arrayBuffer() })
if (active) setHtml(DOMPurify.sanitize(result.value))
} else if (extension === '.xlsx') {
const parsed = await readXlsxSheet(blob)
const { readSheet } = await import('read-excel-file/browser')
const parsed = await readSheet(blob)
if (active) setRows(parsed as unknown[][])
} else {
const previewBlob = extension === '.svg'
@@ -793,6 +800,10 @@ function storageKey(projectId: string) {
return `senlin:documents:tabs:${projectId}`
}
function draftStorageKey(projectId: string) {
return `senlin:documents:drafts:${projectId}`
}
function readStoredTabs(projectId: string): string[] {
try {
const value: unknown = JSON.parse(localStorage.getItem(storageKey(projectId)) ?? '[]')
@@ -802,6 +813,32 @@ function readStoredTabs(projectId: string): string[] {
}
}
function readStoredDrafts(projectId: string): PersistedDocumentDraft[] {
try {
const value: unknown = JSON.parse(localStorage.getItem(draftStorageKey(projectId)) ?? '[]')
if (!Array.isArray(value)) return []
return value.filter(isPersistedDocumentDraft)
} catch {
return []
}
}
function isPersistedDocumentDraft(value: unknown): value is PersistedDocumentDraft {
if (!value || typeof value !== 'object') return false
const draft = value as Partial<PersistedDocumentDraft>
return typeof draft.key === 'string' && typeof draft.name === 'string' && typeof draft.extension === 'string' &&
typeof draft.mimeType === 'string' && typeof draft.markdown === 'string' && typeof draft.revision === 'number' &&
(draft.status === 'dirty' || draft.status === 'saving' || draft.status === 'error') &&
(draft.mode === 'edit' || draft.mode === 'preview') &&
(draft.documentId === undefined || typeof draft.documentId === 'string') &&
(draft.parentId === undefined || typeof draft.parentId === 'string')
}
function restoreDocumentDraft(tab: DocumentTab, draft?: PersistedDocumentDraft): DocumentTab {
if (!draft) return tab
return { ...tab, markdown: draft.markdown, revision: draft.revision, status: draft.status === 'saving' ? 'dirty' : draft.status, mode: draft.mode }
}
function saveLabel(status: SaveStatus) {
return { clean: '已保存', dirty: '未保存', saving: '保存中', saved: '已保存', error: '保存失败' }[status]
}