✨ feat(vscode): 代码阅读预览界面

Vite 加 code-reader.html 第二入口;代码块树(层级竖线、折叠、种类标签配色)、译文 / 源码按词性着色、块内直接编辑(防抖回写整行、Esc 回预览)、翻译下拉、解析失败提示;看板与代码阅读共用带类型的 acquireVsCodeApi 包装和 ErrorBoundary。
This commit is contained in:
cruldra committed 2026-09-24 21:01:34 +08:00
1 parent cab5ac0ebc
commit af56985d9a
16 files changed
+775 -74

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import type { ReactElement } from 'react'
import type { BlockTreeState } from './BlockTree'
import type { DocumentMessage } from './types'
import type { ViewState } from './vscode'
import { TriangleAlert } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { SelectMenu } from '@/components/ui/select-menu'
import { BlockLevel } from './BlockTree'
import { loadViewState, onMessage, postMessage, saveViewState } from './vscode'
function toggled(ids: string[], id: string): string[] {
return ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id]
}
export function App(): ReactElement | null {
const [doc, setDoc] = useState<DocumentMessage | null>(null)
const [view, setView] = useState<ViewState>(loadViewState)
const [focusId, setFocusId] = useState<string | null>(null)
useEffect(() => {
const off = onMessage((msg) => {
if (msg.type === 'document')
setDoc(msg)
})
postMessage({ type: 'ready' })
return off
}, [])
useEffect(() => {
saveViewState(view)
}, [view])
const onFocused = useCallback(() => setFocusId(null), [])
const tree = useMemo((): BlockTreeState | null => doc && {
version: doc.version,
editing: new Set(view.editing),
collapsed: new Set(view.collapsed),
focusId,
onFocused,
onToggleEdit: (id) => {
setFocusId(view.editing.includes(id) ? null : id)
setView(prev => ({ ...prev, editing: toggled(prev.editing, id) }))
},
onToggleFold: id => setView(prev => ({ ...prev, collapsed: toggled(prev.collapsed, id) })),
}, [doc, view, focusId, onFocused])
if (!doc || !tree)
return null
return (
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
{doc.translators.length > 1 && (
<div className="mb-3 flex items-center justify-end gap-2">
<span className="text-xs text-[var(--vscode-descriptionForeground)]">翻译</span>
<SelectMenu
value={doc.translatorId}
options={doc.translators.map(t => ({ value: t.id, label: t.label }))}
onChange={id => postMessage({ type: 'selectTranslator', id })}
title="选择翻译方式"
className="w-40 text-left"
/>
</div>
)}
{doc.error !== undefined && (
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
<div className="min-w-0">
<div className="break-words">
解析失败:
{doc.error}
</div>
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
</div>
</div>
)}
{doc.blocks.length > 0
? <BlockLevel blocks={doc.blocks} tree={tree} />
: doc.error === undefined && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>}
</div>
)
}