✨ feat(vscode): 代码阅读预览界面
Vite 加 code-reader.html 第二入口;代码块树(层级竖线、折叠、种类标签配色)、译文 / 源码按词性着色、块内直接编辑(防抖回写整行、Esc 回预览)、翻译下拉、解析失败提示;看板与代码阅读共用带类型的 acquireVsCodeApi 包装和 ErrorBoundary。
This commit is contained in:
1 parent
cab5ac0ebc
commit
af56985d9a
16 files changed
+775
-74
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user