diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts
index 2f0d599..89b05a8 100644
--- a/01_content/src/client/locales.ts
+++ b/01_content/src/client/locales.ts
@@ -65,6 +65,10 @@ export const zh = {
'pane.openLater': '该类型暂不支持预览',
'file.loading': '加载中…',
'file.fail': '文件读取失败',
+ 'file.preview': '预览',
+ 'file.edit': '编辑',
+ 'file.save': '保存',
+ 'file.saveFail': '保存失败',
'pane.gitNotRepo': '当前目录不是 git 仓库',
'pane.gitClean': '工作区干净',
'pane.jobsEmpty': '当前会话暂无后台任务',
@@ -144,6 +148,10 @@ export const en = {
'pane.openLater': 'Preview not supported for this type',
'file.loading': 'Loading…',
'file.fail': 'Failed to read file',
+ 'file.preview': 'Preview',
+ 'file.edit': 'Edit',
+ 'file.save': 'Save',
+ 'file.saveFail': 'Save failed',
'pane.gitNotRepo': 'Not a git repository',
'pane.gitClean': 'Working tree clean',
'pane.jobsEmpty': 'No background jobs in the current session',
diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx
index 1e0a5e6..4be6edd 100644
--- a/01_content/src/client/split.tsx
+++ b/01_content/src/client/split.tsx
@@ -1121,10 +1121,14 @@ function FileViewer(props: { path: string }) {
return
等宽展示) */
+/** MD/TXT 文本预览(fetch 原文 → MD 渲染或 等宽展示);MD 支持编辑/预览自由切换并可保存回磁盘 */
function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
const [text, setText] = useState(null)
const [error, setError] = useState('')
+ const [mode, setMode] = useState<'preview' | 'edit'>('preview')
+ const [draft, setDraft] = useState('')
+ const [saving, setSaving] = useState(false)
+ const [saveFail, setSaveFail] = useState(false)
useEffect(() => {
let dead = false
setText(null)
@@ -1138,6 +1142,25 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
.catch((e) => { if (!dead) setError(String(e)) })
return () => { dead = true }
}, [props.fileUrl])
+ const enterEdit = () => { setDraft(text ?? ''); setSaveFail(false); setMode('edit') }
+ const save = async () => {
+ setSaving(true)
+ setSaveFail(false)
+ try {
+ const r = await fetch('/api/worktable/write', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ path: props.path, content: draft }),
+ })
+ if (!r.ok) throw new Error('HTTP ' + r.status)
+ setText(draft)
+ setMode('preview')
+ } catch {
+ setSaveFail(true)
+ } finally {
+ setSaving(false)
+ }
+ }
if (error) {
return {T('file.fail')}:{error}
}
@@ -1146,19 +1169,33 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
}
if (props.isMd) {
return (
-
- {
- const a = e.target && e.target.closest ? (e.target.closest('a') as HTMLAnchorElement | null) : null
- if (!a) return
- e.preventDefault()
- const href = a.getAttribute('href') || ''
- if (/^(https?:|mailto:)/i.test(href)) window.open(href, '_blank', 'noopener')
- }}
- />
-
+ <>
+
+
+
+ {mode === 'edit' && (
+
+ )}
+ {saveFail && {T('file.saveFail')}}
+
+ {mode === 'edit'
+ ?