feat: 文件预览(MD渲染/TXT/PDF/图片 + 标签去重) + 预设3改左二右一对话 + 服务端MIME补pdf/markdown
This commit is contained in:
Generated
+103
@@ -10,6 +10,7 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"markdown-it": "^15.0.0",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -471,6 +472,36 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-3.0.0.tgz",
|
||||
"integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@@ -513,6 +544,78 @@
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
|
||||
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-15.0.0.tgz",
|
||||
"integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/markdown-it"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^3.0.0",
|
||||
"entities": "^8.0.0",
|
||||
"linkify-it": "^6.0.0",
|
||||
"mdurl": "^2.1.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.1.0.tgz",
|
||||
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-3.0.0.tgz",
|
||||
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz",
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "*",
|
||||
"@deepseek-ai/dsh-host-webserver": "*",
|
||||
"@deepseek-ai/dsh-client-locale": "*"
|
||||
"@deepseek-ai/dsh-client-locale": "*",
|
||||
"@deepseek-ai/dsh-host-webserver": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/cordis": {
|
||||
@@ -52,6 +52,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"markdown-it": "^15.0.0",
|
||||
"xterm": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const OVER_SIDE_PX = 80
|
||||
const PRESET_DEFS = [
|
||||
{ id: '2h', leftCount: 0, topCount: 0, contentCount: 1, chatFull: false },
|
||||
{ id: '3h', leftCount: 0, topCount: 0, contentCount: 2, chatFull: false },
|
||||
{ id: 'l2', leftCount: 1, topCount: 1, contentCount: 0, chatFull: false },
|
||||
{ id: 'l2', leftCount: 0, topCount: 1, contentCount: 1, chatFull: true },
|
||||
{ id: 't2', leftCount: 0, topCount: 1, contentCount: 1, chatFull: false },
|
||||
{ id: 'grid', leftCount: 0, topCount: 2, contentCount: 1, chatFull: false },
|
||||
{ id: 't3', leftCount: 0, topCount: 1, contentCount: 2, chatFull: true },
|
||||
@@ -153,13 +153,14 @@ function presetThumb(defId: string) {
|
||||
)
|
||||
}
|
||||
if (defId === 'l2') {
|
||||
// 左二右一:左侧上下两个内容窗,右侧整列对话(💬)
|
||||
return (
|
||||
<span className="dsh-wt_thumb dsh-wt_thumbCols">
|
||||
<span className="dsh-wt_thumbCol">{cell(false, 'a')}</span>
|
||||
<span className="dsh-wt_thumbCol">
|
||||
<span className="dsh-wt_thumbRow">{cell(false, 'a')}</span>
|
||||
<span className="dsh-wt_thumbRow">{cell(false, 'b')}</span>
|
||||
<span className="dsh-wt_thumbRow">{cell(true, 'c')}</span>
|
||||
</span>
|
||||
<span className="dsh-wt_thumbCol">{cell(true, 'c')}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ export const zh = {
|
||||
'pane.open': '打开',
|
||||
'pane.wip': '开发中(后续版本接入)',
|
||||
'pane.explorerNoCwd': '当前会话没有工作目录',
|
||||
'pane.openLater': '文件打开将在后续版本支持',
|
||||
'pane.openLater': '该类型暂不支持预览',
|
||||
'file.loading': '加载中…',
|
||||
'file.fail': '文件读取失败',
|
||||
'pane.gitNotRepo': '当前目录不是 git 仓库',
|
||||
'pane.gitClean': '工作区干净',
|
||||
'pane.jobsEmpty': '当前会话暂无后台任务',
|
||||
@@ -137,7 +139,9 @@ export const en = {
|
||||
'pane.open': 'Open',
|
||||
'pane.wip': 'Coming in a later version',
|
||||
'pane.explorerNoCwd': 'No working directory for the current session',
|
||||
'pane.openLater': 'Opening files comes in a later version',
|
||||
'pane.openLater': 'Preview not supported for this type',
|
||||
'file.loading': 'Loading…',
|
||||
'file.fail': 'Failed to read file',
|
||||
'pane.gitNotRepo': 'Not a git repository',
|
||||
'pane.gitClean': 'Working tree clean',
|
||||
'pane.jobsEmpty': 'No background jobs in the current session',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from 'xterm'
|
||||
import 'xterm/css/xterm.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
/**
|
||||
* dsh-worktable 乐高式工作区 M1:通用分栏引擎(PRD §13)。
|
||||
@@ -17,6 +18,7 @@ export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal'
|
||||
export type SplitContent =
|
||||
| { kind: 'iframe'; url: string }
|
||||
| { kind: 'builtin'; type: BuiltinType }
|
||||
| { kind: 'file'; path: string }
|
||||
|
||||
/** 一个内容标签页 */
|
||||
export type PaneTab = { id: string; title: string; content: SplitContent }
|
||||
@@ -125,6 +127,7 @@ const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
|
||||
|
||||
function tabTitleOf(content: SplitContent): string {
|
||||
if (content.kind === 'builtin') return T(BUILTIN_LABEL_KEYS[content.type])
|
||||
if (content.kind === 'file') return basenameOf(content.path)
|
||||
try {
|
||||
const u = new URL(content.url)
|
||||
return u.hostname || content.url
|
||||
@@ -133,6 +136,20 @@ function tabTitleOf(content: SplitContent): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** 取路径最后一段作为标签标题 */
|
||||
function basenameOf(p: string): string {
|
||||
const parts = String(p).replace(/[\\/]+$/, '').split(/[\\/]/)
|
||||
return parts[parts.length - 1] || String(p)
|
||||
}
|
||||
|
||||
/** 内容同一性(openTab 去重:同窗内同内容只保留一个标签,再次打开切过去) */
|
||||
function sameContent(a: SplitContent, b: SplitContent): boolean {
|
||||
if (a.kind === 'iframe' && b.kind === 'iframe') return a.url === b.url
|
||||
if (a.kind === 'file' && b.kind === 'file') return a.path === b.path
|
||||
if (a.kind === 'builtin' && b.kind === 'builtin') return a.type === b.type
|
||||
return false
|
||||
}
|
||||
|
||||
/** 分栏 UI 文案提供者(由工作台注入 locale t) */
|
||||
let uiT: ((key: string) => string) | null = null
|
||||
export function setSplitT(fn: ((key: string) => string) | null) {
|
||||
@@ -546,6 +563,9 @@ export const splitStore: SplitState = {
|
||||
if (!spec) return
|
||||
const mutate = (pane: SplitPane): SplitPane => {
|
||||
const tabs = [...(pane.tabs ?? [])]
|
||||
// 去重:同内容已有标签 → 直接激活
|
||||
const existing = tabs.findIndex((t) => sameContent(t.content, content))
|
||||
if (existing >= 0) return { ...pane, content: null, tabs, active: existing }
|
||||
const tab: PaneTab = { id: 't' + Date.now().toString(36), title: tabTitleOf(content), content }
|
||||
tabs.push(tab)
|
||||
return { ...pane, content: null, tabs, active: tabs.length - 1 }
|
||||
@@ -910,6 +930,8 @@ function ExplorerPane(props: { row: PaneRow; index: number }) {
|
||||
if (e.isDir) { toggle(e.path); return }
|
||||
if (/\.html?$/i.test(e.name)) {
|
||||
splitStore.openTab(props.row, props.index, { kind: 'iframe', url: '/api/worktable/file?path=' + encodeURIComponent(e.path) })
|
||||
} else if (/\.(md|markdown|mdown|txt|log|pdf|png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(e.name)) {
|
||||
splitStore.openTab(props.row, props.index, { kind: 'file', path: e.path })
|
||||
} else {
|
||||
setError(T('pane.openLater'))
|
||||
}
|
||||
@@ -1070,12 +1092,80 @@ function TerminalPane() {
|
||||
return <div ref={hostRef} className="dsh-wt_termHost" />
|
||||
}
|
||||
|
||||
const mdRenderer = new MarkdownIt({ linkify: true })
|
||||
|
||||
const IMAGE_EXTS = /[.](png|jpe?g|gif|webp|svg|bmp|ico)$/i
|
||||
const MD_EXTS = /[.](md|markdown|mdown)$/i
|
||||
|
||||
/** 本地文件预览:PDF 走原生 iframe、图片居中展示、MD 渲染预览、其余按纯文本 */
|
||||
function FileViewer(props: { path: string }) {
|
||||
const ext = (props.path.split('.').pop() || '').toLowerCase()
|
||||
const fileUrl = '/api/worktable/file?path=' + encodeURIComponent(props.path)
|
||||
if (ext === 'pdf') {
|
||||
return <iframe className="dsh-wt_paneFrame" src={fileUrl} title={basenameOf(props.path)} />
|
||||
}
|
||||
if (IMAGE_EXTS.test('.' + ext)) {
|
||||
return (
|
||||
<div className="dsh-wt_imgView">
|
||||
<img src={fileUrl} alt={basenameOf(props.path)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <TextViewer path={props.path} fileUrl={fileUrl} isMd={MD_EXTS.test('.' + ext)} />
|
||||
}
|
||||
|
||||
/** MD/TXT 文本预览(fetch 原文 → MD 渲染或 <pre> 等宽展示) */
|
||||
function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
||||
const [text, setText] = useState<string | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
useEffect(() => {
|
||||
let dead = false
|
||||
setText(null)
|
||||
setError('')
|
||||
fetch(props.fileUrl)
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status)
|
||||
return r.text()
|
||||
})
|
||||
.then((t) => { if (!dead) setText(t) })
|
||||
.catch((e) => { if (!dead) setError(String(e)) })
|
||||
return () => { dead = true }
|
||||
}, [props.fileUrl])
|
||||
if (error) {
|
||||
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.fail')}:{error}</span></div>
|
||||
}
|
||||
if (text == null) {
|
||||
return <div className="dsh-wt_paneWip"><span className="dsh-wt_paneWipText">{T('file.loading')}</span></div>
|
||||
}
|
||||
if (props.isMd) {
|
||||
return (
|
||||
<div className="dsh-wt_fileView">
|
||||
<div
|
||||
className="dsh-wt_md"
|
||||
dangerouslySetInnerHTML={{ __html: mdRenderer.render(text) }}
|
||||
onClick={(e: any) => {
|
||||
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')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="dsh-wt_fileView"><pre className="dsh-wt_txt">{text}</pre></div>
|
||||
}
|
||||
|
||||
/** 单个标签页的内容渲染 */
|
||||
function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
|
||||
const content = props.tab.content
|
||||
if (content.kind === 'iframe') {
|
||||
return <iframe className="dsh-wt_paneFrame" src={content.url} title={props.tab.title} />
|
||||
}
|
||||
if (content.kind === 'file') {
|
||||
return <FileViewer path={content.path} />
|
||||
}
|
||||
if (content.type === 'browser') return <BrowserPane />
|
||||
if (content.type === 'explorer') return <ExplorerPane row={props.row} index={props.index} />
|
||||
if (content.type === 'scm') return <GitPane />
|
||||
|
||||
@@ -76,6 +76,30 @@ export const css = [
|
||||
'.dsh-wt_splitClose{flex:none;display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font-size:12px;cursor:pointer}',
|
||||
'.dsh-wt_splitClose:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}',
|
||||
'.dsh-wt_pane{display:flex;flex-direction:column;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);overflow:hidden;transition:border-color .15s ease,box-shadow .15s ease,transform .15s ease}',
|
||||
// 文件预览:文本/MD 滚动容器 + MD 排版 + 图片居中
|
||||
'.dsh-wt_fileView{flex:1;min-height:0;overflow:auto;padding:12px 14px;background:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb);font-size:12.5px;line-height:1.7}',
|
||||
'.dsh-wt_txt{margin:0;font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:12px;line-height:1.65;white-space:pre-wrap;word-break:break-word}',
|
||||
'.dsh-wt_md h1,.dsh-wt_md h2,.dsh-wt_md h3,.dsh-wt_md h4{margin:14px 0 8px;line-height:1.3;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_md h1{font-size:20px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:6px}',
|
||||
'.dsh-wt_md h2{font-size:16.5px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);padding-bottom:5px}',
|
||||
'.dsh-wt_md h3{font-size:14.5px}',
|
||||
'.dsh-wt_md h4{font-size:13px}',
|
||||
'.dsh-wt_md p{margin:6px 0}',
|
||||
'.dsh-wt_md a{color:var(--dsw-alias-state-accent-primary,#4f8ef7);text-decoration:none}',
|
||||
'.dsh-wt_md a:hover{text-decoration:underline}',
|
||||
'.dsh-wt_md code{font-family:var(--dsw-font-mono,Consolas,Menlo,monospace);font-size:11.5px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));padding:1px 5px;border-radius:4px}',
|
||||
'.dsh-wt_md pre{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.04));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;padding:10px 12px;overflow:auto;margin:8px 0}',
|
||||
'.dsh-wt_md pre code{background:transparent;padding:0;font-size:12px}',
|
||||
'.dsh-wt_md blockquote{margin:8px 0;padding:2px 12px;border-left:3px solid var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_md ul,.dsh-wt_md ol{margin:6px 0;padding-left:22px}',
|
||||
'.dsh-wt_md li{margin:2px 0}',
|
||||
'.dsh-wt_md table{border-collapse:collapse;margin:8px 0;font-size:12px}',
|
||||
'.dsh-wt_md th,.dsh-wt_md td{border:1px solid var(--dsw-alias-border-l1,#262b36);padding:4px 10px;text-align:left}',
|
||||
'.dsh-wt_md th{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_md hr{border:none;border-top:1px solid var(--dsw-alias-border-l1,#262b36);margin:12px 0}',
|
||||
'.dsh-wt_md img{max-width:100%}',
|
||||
'.dsh-wt_imgView{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;overflow:auto;padding:10px;background:var(--dsw-alias-bg-base,#0b0e14)}',
|
||||
'.dsh-wt_imgView img{max-width:100%;max-height:100%;object-fit:contain}',
|
||||
'.dsh-wt_pane[data-drop-hover=true]{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);box-shadow:0 0 0 2px rgba(79,142,247,.4),var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4));transform:scale(1.012)}',
|
||||
'.dsh-wt_paneBar{flex:none;display:flex;align-items:center;height:22px;padding:0 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#262b36);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02))}',
|
||||
'.dsh-wt_paneTitle{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
|
||||
@@ -208,8 +208,8 @@ export function apply(ctx: Context) {
|
||||
const types: Record<string, string> = {
|
||||
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
||||
css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
|
||||
json: 'application/json; charset=utf-8', md: 'text/plain; charset=utf-8', txt: 'text/plain; charset=utf-8',
|
||||
svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
|
||||
json: 'application/json; charset=utf-8', md: 'text/markdown; charset=utf-8', markdown: 'text/markdown; charset=utf-8', txt: 'text/plain; charset=utf-8', log: 'text/plain; charset=utf-8',
|
||||
pdf: 'application/pdf', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon',
|
||||
}
|
||||
res.writeHead(200, { 'content-type': types[ext] ?? 'application/octet-stream', 'cache-control': 'no-store' })
|
||||
res.end(data)
|
||||
|
||||
+3
-2
@@ -335,8 +335,9 @@ type SplitPane = {
|
||||
视口内钳制;透明遮罩点击关闭),不再使用侧边栏内展开式下拉;面板内容仅「选择布局 + 填名称」
|
||||
(快捷方式表单暂移除,存量快捷方式条目仍保留展示/删除);
|
||||
- **拓扑预设六个**(2 行 × 3 列缩略图网格,聊天窗蓝色 💬 标注):
|
||||
左右两栏 / 三栏横排 / 左一右二 / 上一下二 / 井字四栏 / 左品右聊(2026-08-16 定案);
|
||||
左一右二 = 左列整高内容窗 + 右列「上内容 + 下聊天」(左列布局聊天固定右下,无 ⇄ 翻转);
|
||||
左右两栏 / 三栏横排 / 左二右一 / 上一下二 / 井字四栏 / 左品右聊(2026-08-17 更新:
|
||||
第三个预设由「左一右二」改为「左二右一」);
|
||||
左二右一 = 左侧上下两个内容窗 + 右侧聊天通高整列(chatFullHeight 几何,聊天可 ⇄ 翻转贴左);
|
||||
左品右聊 = 左侧品字形(上一个、下两个内容窗)+ 右侧聊天通高整列(chatFullHeight 几何,
|
||||
聊天可 ⇄ 翻转贴左);
|
||||
- 保存的布局以「布局条目」形式出现在项目区(布局 = 一种工作台项目),点击打开 tiling 工作区,
|
||||
|
||||
@@ -597,3 +597,21 @@
|
||||
- 打点:git tag checkpoint-2026-08-17-pre-fileviewer(已推远端,b22cd6b)。
|
||||
- 状态:工作台 UI/交互(卡片统一、图标、弹窗、删除确认、互斥、预设均衡、picker 自适应、
|
||||
分隔线居中)全部完成并验证;下一个大块 = 文件预览(MD/TXT/PDF 等自定义开发)。
|
||||
## 补记(文件预览 MD/TXT/PDF/图片 + 预设三改左二右一)
|
||||
|
||||
- 用户确认默认方案开工 + 第 3 个预设(左一右二)改为「左边两个 + 右边一个对话」。
|
||||
- 文件预览实现:
|
||||
① SplitContent 新增 kind:'file'(path);tabTitleOf 取 basename;openTab 去重
|
||||
(同内容已有标签 → 直接激活,sameContent 判定);
|
||||
② FileViewer:pdf → iframe 原生渲染(服务端 MIME application/pdf);图片 → 居中 img;
|
||||
md/markdown → markdown-it(打进 client.js,linkify 开、内嵌 HTML 关闭,链接点击新标签外开);
|
||||
其余 → 等宽 <pre> 纯文本;fetch 失败/加载中文案(file.fail/file.loading);
|
||||
③ 资源管理器点击 .md/.markdown/.mdown/.txt/.log/.pdf/图片 开预览标签(.html 行为不变,
|
||||
其他类型提示不支持);服务端 file 路由 MIME 补 pdf/markdown/log/bmp/ico;
|
||||
④ 全套暗色 MD 排版样式(标题/代码块/引用/表格/列表/分割线)。
|
||||
- 预设 l2 改造:PRESET_DEFS l2 → {top:1, content:1, chatFull:true}(左列上下两内容窗 + 右列通高聊天),
|
||||
缩略图改为左 2 右 1;PRD §13 预设说明同步更新。
|
||||
- 验证(functional-diag STEP11):openTab kind:file PRD.md → 标签名 PRD.md、.dsh-wt_md 渲染出
|
||||
PRD 开头文本;同路径再开 tabsAfterDup=1(去重);package.json → .dsh-wt_txt 展示;
|
||||
全 STEP 回归 ERROR_COUNT: 0。bundle-eval 补 atob/btoa 桩后 PASS(markdown-it 引入)。
|
||||
- 备注:客户端改动 F5 生效;服务端 pdf MIME 需手动重启 dsh web 一次后 PDF 预览才生效。
|
||||
|
||||
@@ -27,6 +27,7 @@ const sandbox = { self: null,
|
||||
addEventListener(){}, removeEventListener(){},
|
||||
},
|
||||
localStorage: { getItem(){ return null }, setItem(){}, removeItem(){} },
|
||||
atob: (s) => Buffer.from(s, 'base64').toString('binary'), btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
|
||||
CustomEvent: class { constructor(t, o){ this.type = t; this.detail = o && o.detail } },
|
||||
ResizeObserver: class { observe(){} disconnect(){} },
|
||||
MutationObserver: class { observe(){} disconnect(){} },
|
||||
|
||||
@@ -250,6 +250,28 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
})()`);
|
||||
console.log('STEP10:', JSON.stringify(step10));
|
||||
|
||||
// 文件预览:MD 渲染 / TXT 文本 / 标签去重
|
||||
const step11 = await evaluate(`(async function(){
|
||||
var out={};
|
||||
var st=window.__dshWorktable.splitStore;
|
||||
st.open({id:'t-file',title:'file',top:null,main:[{id:'f1',title:'f',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
|
||||
await new Promise(function(r){setTimeout(r,400)});
|
||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\02_process\\\\PRD.md'});
|
||||
await new Promise(function(r){setTimeout(r,1200)});
|
||||
out.mdTitle=st.spec.main[0].tabs[0].title;
|
||||
out.mdView=!!document.querySelector('.dsh-wt_md');
|
||||
out.mdText=document.querySelector('.dsh-wt_md')?String(document.querySelector('.dsh-wt_md').textContent).slice(0,50):null;
|
||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\02_process\\\\PRD.md'});
|
||||
out.tabsAfterDup=st.spec.main[0].tabs.length;
|
||||
st.openTab('main',0,{kind:'file',path:'E:\\\\AI_Workspace\\\\DeepseekHarness\\\\Projects\\\\dsh-worktable\\\\01_content\\\\package.json'});
|
||||
await new Promise(function(r){setTimeout(r,900)});
|
||||
out.txtView=!!document.querySelector('.dsh-wt_txt');
|
||||
out.tabsNow=st.spec.main[0].tabs.length;
|
||||
st.close();
|
||||
return JSON.stringify(out);
|
||||
})()`);
|
||||
console.log('STEP11:', JSON.stringify(step11));
|
||||
|
||||
const errors = events.filter((e) =>
|
||||
e.method === 'Runtime.exceptionThrown' ||
|
||||
(e.method === 'Log.entryAdded' && e.params.entry.level === 'error') ||
|
||||
|
||||
Reference in New Issue
Block a user