feat: + 面板新建工作区(拓扑预设 + 布局条目一等公民)
This commit is contained in:
@@ -43,6 +43,8 @@ type ProjectsState = {
|
|||||||
nameOverrides: Record<string, string>
|
nameOverrides: Record<string, string>
|
||||||
/** 本地快捷方式条目。 */
|
/** 本地快捷方式条目。 */
|
||||||
shortcuts: Shortcut[]
|
shortcuts: Shortcut[]
|
||||||
|
/** 用户自建的布局条目(「+」新建工作区保存的 LayoutSpec)。 */
|
||||||
|
layouts: LayoutSpec[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const PERSIST_KEY = 'dsh.worktable.view.v1'
|
const PERSIST_KEY = 'dsh.worktable.view.v1'
|
||||||
@@ -62,6 +64,38 @@ const OVER_BOTTOM_PX = 24
|
|||||||
const OVER_TOP_PX = 24
|
const OVER_TOP_PX = 24
|
||||||
const OVER_SIDE_PX = 80
|
const OVER_SIDE_PX = 80
|
||||||
|
|
||||||
|
/** 拓扑预设(聊天窗恒贴右,PRD §13.2 硬约束):左右/三栏/上一下二/井字 */
|
||||||
|
const PRESET_DEFS = [
|
||||||
|
{ id: '2h', topCount: 0, contentCount: 1 },
|
||||||
|
{ id: '3h', topCount: 0, contentCount: 2 },
|
||||||
|
{ id: 't2', topCount: 1, contentCount: 1 },
|
||||||
|
{ id: 'grid', topCount: 2, contentCount: 1 },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function presetTotal(def: { topCount: number; contentCount: number }): number {
|
||||||
|
return def.topCount + def.contentCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLayout(presetId: string, name: string, urls: string[]): LayoutSpec {
|
||||||
|
const def = PRESET_DEFS.find((d) => d.id === presetId) ?? PRESET_DEFS[0]
|
||||||
|
const mk = (i: number, url: string) => ({
|
||||||
|
id: 'p' + (i + 1),
|
||||||
|
title: '内容' + (i + 1),
|
||||||
|
min: 200,
|
||||||
|
content: { kind: 'iframe' as const, url },
|
||||||
|
})
|
||||||
|
const top = Array.from({ length: def.topCount }, (_, i) => mk(i, urls[i] ?? 'about:blank'))
|
||||||
|
const main = Array.from({ length: def.contentCount }, (_, i) => mk(def.topCount + i, urls[def.topCount + i] ?? 'about:blank'))
|
||||||
|
return {
|
||||||
|
id: 'layout-' + Date.now().toString(36),
|
||||||
|
title: name,
|
||||||
|
top: top.length > 0 ? top : null,
|
||||||
|
main,
|
||||||
|
chatWidth: { default: 360, min: 240, max: 600 },
|
||||||
|
topHeight: { default: 200, min: 120, max: 480 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_VIEW: ViewState = {
|
const DEFAULT_VIEW: ViewState = {
|
||||||
query: '',
|
query: '',
|
||||||
searchOpen: false,
|
searchOpen: false,
|
||||||
@@ -76,6 +110,7 @@ const DEFAULT_PROJECTS: ProjectsState = {
|
|||||||
hidden: [],
|
hidden: [],
|
||||||
nameOverrides: {},
|
nameOverrides: {},
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
|
layouts: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadView(): ViewState {
|
function loadView(): ViewState {
|
||||||
@@ -114,6 +149,9 @@ function loadProjects(): ProjectsState {
|
|||||||
shortcuts: Array.isArray(p.shortcuts)
|
shortcuts: Array.isArray(p.shortcuts)
|
||||||
? p.shortcuts.filter((s: any) => s && typeof s.id === 'string' && typeof s.name === 'string' && typeof s.href === 'string')
|
? p.shortcuts.filter((s: any) => s && typeof s.id === 'string' && typeof s.name === 'string' && typeof s.href === 'string')
|
||||||
: [],
|
: [],
|
||||||
|
layouts: Array.isArray(p.layouts)
|
||||||
|
? p.layouts.filter((l: any) => l && typeof l.id === 'string' && typeof l.title === 'string' && Array.isArray(l.main))
|
||||||
|
: [],
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_PROJECTS }
|
return { ...DEFAULT_PROJECTS }
|
||||||
@@ -167,6 +205,10 @@ function WorktableSection(props: any) {
|
|||||||
const [scIcon, setScIcon] = useState('')
|
const [scIcon, setScIcon] = useState('')
|
||||||
const [scHref, setScHref] = useState('')
|
const [scHref, setScHref] = useState('')
|
||||||
const [scError, setScError] = useState(false)
|
const [scError, setScError] = useState(false)
|
||||||
|
const [wsPreset, setWsPreset] = useState<string>('2h')
|
||||||
|
const [wsName, setWsName] = useState('')
|
||||||
|
const [wsUrls, setWsUrls] = useState<string[]>(['', '', ''])
|
||||||
|
const [wsError, setWsError] = useState(false)
|
||||||
const [float, setFloat] = useState<FloatRect | null>(() =>
|
const [float, setFloat] = useState<FloatRect | null>(() =>
|
||||||
view.dock === 'float' && view.floatTop != null ? { top: view.floatTop } : null,
|
view.dock === 'float' && view.floatTop != null ? { top: view.floatTop } : null,
|
||||||
)
|
)
|
||||||
@@ -319,12 +361,14 @@ function WorktableSection(props: any) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// ── 有效排序 ──
|
// ── 有效排序 ──
|
||||||
// 手动:持久化 order(过滤已卸载 id)→ 新注册 id 追加尾部;
|
// 手动:持久化 order(过滤已卸载 id)→ 新注册 id 与布局 id 追加尾部;
|
||||||
// 最近:有 lastUsed 的按时间降序在前,其余按手动序在后。
|
// 最近:有 lastUsed 的按时间降序在前,其余按手动序在后。
|
||||||
|
const layoutIds = useMemo(() => projects.layouts.map((l) => l.id), [projects.layouts])
|
||||||
|
const allIds = useMemo(() => [...registeredIds, ...layoutIds], [registeredIds, layoutIds])
|
||||||
const effectiveOrder = useMemo(() => {
|
const effectiveOrder = useMemo(() => {
|
||||||
const known = new Set(registeredIds)
|
const known = new Set(allIds)
|
||||||
const stored = projects.order.filter((id) => known.has(id))
|
const stored = projects.order.filter((id) => known.has(id))
|
||||||
const rest = registeredIds.filter((id) => !stored.includes(id))
|
const rest = allIds.filter((id) => !stored.includes(id))
|
||||||
let list = [...stored, ...rest]
|
let list = [...stored, ...rest]
|
||||||
if (view.orderBy === 'recent') {
|
if (view.orderBy === 'recent') {
|
||||||
const hot = list.filter((id) => projects.lastUsed[id] != null)
|
const hot = list.filter((id) => projects.lastUsed[id] != null)
|
||||||
@@ -333,7 +377,7 @@ function WorktableSection(props: any) {
|
|||||||
list = [...hot, ...cold]
|
list = [...hot, ...cold]
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}, [registeredIds, projects.order, projects.lastUsed, view.orderBy])
|
}, [allIds, projects.order, projects.lastUsed, view.orderBy])
|
||||||
|
|
||||||
const ownerProps = {
|
const ownerProps = {
|
||||||
query: view.query.trim(),
|
query: view.query.trim(),
|
||||||
@@ -466,11 +510,36 @@ function WorktableSection(props: any) {
|
|||||||
persistProjects((prev) => ({ ...prev, shortcuts: prev.shortcuts.filter((s) => s.id !== id) }))
|
persistProjects((prev) => ({ ...prev, shortcuts: prev.shortcuts.filter((s) => s.id !== id) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 布局(新建工作区) ──
|
||||||
|
const saveLayout = () => {
|
||||||
|
const name = wsName.trim()
|
||||||
|
const def = PRESET_DEFS.find((d) => d.id === wsPreset) ?? PRESET_DEFS[0]
|
||||||
|
const total = presetTotal(def)
|
||||||
|
const urls = wsUrls.slice(0, total).map((u) => u.trim())
|
||||||
|
if (!name || urls.some((u) => !/^(\/|https?:\/\/)/i.test(u))) { setWsError(true); return }
|
||||||
|
const layout = buildLayout(wsPreset, name, urls)
|
||||||
|
persistProjects((prev) => ({ ...prev, layouts: [...prev.layouts, layout] }))
|
||||||
|
setWsName(''); setWsUrls(['', '', '']); setWsError(false)
|
||||||
|
setAddOpen(false)
|
||||||
|
openSplit(layout)
|
||||||
|
reportUsed(layout.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeLayout = (id: string) => {
|
||||||
|
persistProjects((prev) => ({ ...prev, layouts: prev.layouts.filter((l) => l.id !== id) }))
|
||||||
|
}
|
||||||
|
|
||||||
const query = view.query.trim()
|
const query = view.query.trim()
|
||||||
const queryLower = query.toLowerCase()
|
const queryLower = query.toLowerCase()
|
||||||
const visibleShortcuts = projects.shortcuts.filter((s) =>
|
const visibleShortcuts = projects.shortcuts.filter((s) =>
|
||||||
!queryLower || (s.name + ' ' + s.href).toLowerCase().includes(queryLower),
|
!queryLower || (s.name + ' ' + s.href).toLowerCase().includes(queryLower),
|
||||||
)
|
)
|
||||||
|
const visibleLayouts = projects.layouts.filter((l) => {
|
||||||
|
if (projects.hidden.includes(l.id)) return false
|
||||||
|
if (!queryLower) return true
|
||||||
|
const paneTitles = [...(l.top ?? []), ...l.main].map((p) => p.title).join(' ')
|
||||||
|
return (l.title + ' ' + paneTitles).toLowerCase().includes(queryLower)
|
||||||
|
})
|
||||||
|
|
||||||
const isFloat = float != null
|
const isFloat = float != null
|
||||||
const floatStyle = isFloat
|
const floatStyle = isFloat
|
||||||
@@ -536,10 +605,12 @@ function WorktableSection(props: any) {
|
|||||||
if (!wide) {
|
if (!wide) {
|
||||||
const projectIcons = registeredIds.map((id) => metas[id]?.icon ?? '📦')
|
const projectIcons = registeredIds.map((id) => metas[id]?.icon ?? '📦')
|
||||||
const shortcutIcons = projects.shortcuts.map((s) => s.icon)
|
const shortcutIcons = projects.shortcuts.map((s) => s.icon)
|
||||||
const icons = [...projectIcons, ...shortcutIcons]
|
const layoutIcons = projects.layouts.map(() => '🧱')
|
||||||
|
const icons = [...projectIcons, ...shortcutIcons, ...layoutIcons]
|
||||||
const railNames = [
|
const railNames = [
|
||||||
...registeredIds.map((id) => projects.nameOverrides[id] ?? metas[id]?.name ?? id),
|
...registeredIds.map((id) => projects.nameOverrides[id] ?? metas[id]?.name ?? id),
|
||||||
...projects.shortcuts.map((s) => s.name),
|
...projects.shortcuts.map((s) => s.name),
|
||||||
|
...projects.layouts.map((l) => projects.nameOverrides[l.id] ?? l.title),
|
||||||
]
|
]
|
||||||
// 浮动态:fixed 定位到拖前高度(左/宽取折叠列实测几何);停靠态:文档流原位
|
// 浮动态:fixed 定位到拖前高度(左/宽取折叠列实测几何);停靠态:文档流原位
|
||||||
const railStyle = isFloat && railRect
|
const railStyle = isFloat && railRect
|
||||||
@@ -641,6 +712,40 @@ function WorktableSection(props: any) {
|
|||||||
<button type="button" className="dsh-wt_addBtn" onClick={addShortcut}>{t('add.shortcutAdd')}</button>
|
<button type="button" className="dsh-wt_addBtn" onClick={addShortcut}>{t('add.shortcutAdd')}</button>
|
||||||
</div>
|
</div>
|
||||||
{scError && <p className="dsh-wt_addError">{t('add.shortcutInvalid')}</p>}
|
{scError && <p className="dsh-wt_addError">{t('add.shortcutInvalid')}</p>}
|
||||||
|
<div className="dsh-wt_menuSep" />
|
||||||
|
<span className="dsh-wt_menuLabel">{t('add.workspaceTitle')}</span>
|
||||||
|
<p className="dsh-wt_addText">{t('add.workspaceDesc')}</p>
|
||||||
|
<div className="dsh-wt_presets">
|
||||||
|
{PRESET_DEFS.map((def) => (
|
||||||
|
<button
|
||||||
|
key={def.id}
|
||||||
|
type="button"
|
||||||
|
className="dsh-wt_preset"
|
||||||
|
data-on={wsPreset === def.id ? 'true' : 'false'}
|
||||||
|
onClick={() => { setWsPreset(def.id); setWsError(false) }}
|
||||||
|
>
|
||||||
|
{t('preset.' + def.id)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="dsh-wt_addForm">
|
||||||
|
<input type="text" placeholder={t('add.layoutNamePh')} value={wsName}
|
||||||
|
onChange={(e) => { setWsName(e.target.value); setWsError(false) }} />
|
||||||
|
{Array.from({ length: presetTotal(PRESET_DEFS.find((d) => d.id === wsPreset) ?? PRESET_DEFS[0]) }, (_, i) => (
|
||||||
|
<input
|
||||||
|
key={i}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('add.paneUrlPh')}
|
||||||
|
value={wsUrls[i] ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
setWsUrls((prev) => { const next = [...prev]; next[i] = e.target.value; return next })
|
||||||
|
setWsError(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<button type="button" className="dsh-wt_addBtn" onClick={saveLayout}>{t('add.layoutSave')}</button>
|
||||||
|
</div>
|
||||||
|
{wsError && <p className="dsh-wt_addError">{t('add.layoutInvalid')}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -665,7 +770,8 @@ function WorktableSection(props: any) {
|
|||||||
</div>
|
</div>
|
||||||
{effectiveOrder.map((id) => {
|
{effectiveOrder.map((id) => {
|
||||||
const meta = metas[id]
|
const meta = metas[id]
|
||||||
const display = projects.nameOverrides[id] ?? meta?.name ?? id
|
const layout = projects.layouts.find((l) => l.id === id)
|
||||||
|
const display = projects.nameOverrides[id] ?? layout?.title ?? meta?.name ?? id
|
||||||
const isHidden = projects.hidden.includes(id)
|
const isHidden = projects.hidden.includes(id)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -682,7 +788,7 @@ function WorktableSection(props: any) {
|
|||||||
onDragEnd={() => { dragIdRef.current = null }}
|
onDragEnd={() => { dragIdRef.current = null }}
|
||||||
>
|
>
|
||||||
<span className="dsh-wt_manageGrip" aria-hidden>≡</span>
|
<span className="dsh-wt_manageGrip" aria-hidden>≡</span>
|
||||||
<span className="dsh-wt_manageIcon" aria-hidden>{meta?.icon ?? '📦'}</span>
|
<span className="dsh-wt_manageIcon" aria-hidden>{layout ? '🧱' : (meta?.icon ?? '📦')}</span>
|
||||||
<input
|
<input
|
||||||
className="dsh-wt_manageInput"
|
className="dsh-wt_manageInput"
|
||||||
value={display}
|
value={display}
|
||||||
@@ -694,6 +800,9 @@ function WorktableSection(props: any) {
|
|||||||
<button type="button" className="dsh-wt_manageBtn" title={isHidden ? t('manage.show') : t('manage.hide')} onClick={() => toggleHidden(id)}>
|
<button type="button" className="dsh-wt_manageBtn" title={isHidden ? t('manage.show') : t('manage.hide')} onClick={() => toggleHidden(id)}>
|
||||||
{isHidden ? '🙈' : '👁'}
|
{isHidden ? '🙈' : '👁'}
|
||||||
</button>
|
</button>
|
||||||
|
{layout && (
|
||||||
|
<button type="button" className="dsh-wt_manageBtn" title={t('manage.deleteLayout')} onClick={() => removeLayout(id)}>✕</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -713,6 +822,27 @@ function WorktableSection(props: any) {
|
|||||||
{renderProjectSlot
|
{renderProjectSlot
|
||||||
? renderProjectSlot('sidebar.worktable.project', ownerProps)
|
? renderProjectSlot('sidebar.worktable.project', ownerProps)
|
||||||
: <div className="dsh-wt_empty">{t('empty')}</div>}
|
: <div className="dsh-wt_empty">{t('empty')}</div>}
|
||||||
|
{visibleLayouts.map((l) => {
|
||||||
|
const paneCount = [...(l.top ?? []), ...l.main].length
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={l.id}
|
||||||
|
type="button"
|
||||||
|
className="dsh-wt_layout"
|
||||||
|
data-on={activeSplitId === l.id ? 'true' : 'false'}
|
||||||
|
style={{ order: effectiveOrder.indexOf(l.id) + 1000 }}
|
||||||
|
onClick={() => { openSplit(l); reportUsed(l.id) }}
|
||||||
|
>
|
||||||
|
<span className="dsh-wt_layoutIcon" aria-hidden>🧱</span>
|
||||||
|
<span className="dsh-wt_layoutText">
|
||||||
|
<span className="dsh-wt_layoutName">{projects.nameOverrides[l.id] ?? l.title}</span>
|
||||||
|
<span className="dsh-wt_layoutDesc">{t('layout.desc', { n: String(paneCount) })}</span>
|
||||||
|
</span>
|
||||||
|
<span className="dsh-wt_layoutBadge">{t('layout.badge')}</span>
|
||||||
|
<span className="dsh-wt_layoutArrow" aria-hidden>›</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{visibleShortcuts.length > 0 && (
|
{visibleShortcuts.length > 0 && (
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ export const zh = {
|
|||||||
'manage.deleteShortcut': '删除快捷方式',
|
'manage.deleteShortcut': '删除快捷方式',
|
||||||
'manage.reset': '恢复默认',
|
'manage.reset': '恢复默认',
|
||||||
'shortcut.badge': '本地',
|
'shortcut.badge': '本地',
|
||||||
|
'layout.badge': '布局',
|
||||||
|
'layout.desc': '{n} 内容窗 · 对话',
|
||||||
|
'add.workspaceTitle': '新建工作区',
|
||||||
|
'add.workspaceDesc': '选一个拓扑,填好各窗内容,保存为工作台布局(其中一窗恒为对话)。',
|
||||||
|
'add.layoutNamePh': '布局名称',
|
||||||
|
'add.paneUrlPh': '内容地址(/xxx/ 或 https://)',
|
||||||
|
'add.layoutSave': '保存并打开',
|
||||||
|
'add.layoutInvalid': '请填写布局名称,且每个内容地址以 / 或 http(s) 开头',
|
||||||
|
'preset.2h': '左右两栏',
|
||||||
|
'preset.3h': '三栏横排',
|
||||||
|
'preset.t2': '上一下二',
|
||||||
|
'preset.grid': '井字四栏',
|
||||||
|
'manage.deleteLayout': '删除布局',
|
||||||
'empty': '暂无项目',
|
'empty': '暂无项目',
|
||||||
} satisfies Record<string, string>
|
} satisfies Record<string, string>
|
||||||
|
|
||||||
@@ -80,6 +93,19 @@ export const en = {
|
|||||||
'manage.deleteShortcut': 'Delete shortcut',
|
'manage.deleteShortcut': 'Delete shortcut',
|
||||||
'manage.reset': 'Reset defaults',
|
'manage.reset': 'Reset defaults',
|
||||||
'shortcut.badge': 'Local',
|
'shortcut.badge': 'Local',
|
||||||
|
'layout.badge': 'Layout',
|
||||||
|
'layout.desc': '{n} content panes · chat',
|
||||||
|
'add.workspaceTitle': 'New workspace',
|
||||||
|
'add.workspaceDesc': 'Pick a topology, fill in the pane contents, save as a worktable layout (one pane is always the chat).',
|
||||||
|
'add.layoutNamePh': 'Layout name',
|
||||||
|
'add.paneUrlPh': 'Content URL (/xxx/ or https://)',
|
||||||
|
'add.layoutSave': 'Save & open',
|
||||||
|
'add.layoutInvalid': 'Enter a layout name, and each content URL must start with / or http(s)',
|
||||||
|
'preset.2h': 'Side by side',
|
||||||
|
'preset.3h': 'Three columns',
|
||||||
|
'preset.t2': 'Top + two',
|
||||||
|
'preset.grid': '2×2 grid',
|
||||||
|
'manage.deleteLayout': 'Delete layout',
|
||||||
'empty': 'No projects yet',
|
'empty': 'No projects yet',
|
||||||
} satisfies Record<WorktableKey, string>
|
} satisfies Record<WorktableKey, string>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const css = [
|
|||||||
// 菜单分隔线
|
// 菜单分隔线
|
||||||
'.dsh-wt_menuSep{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}',
|
'.dsh-wt_menuSep{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}',
|
||||||
// 添加面板
|
// 添加面板
|
||||||
'.dsh-wt_add{max-height:340px;overflow:auto}',
|
'.dsh-wt_add{max-height:500px;overflow:auto}',
|
||||||
'.dsh-wt_addText{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2);padding:2px 8px 4px;margin:0}',
|
'.dsh-wt_addText{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2);padding:2px 8px 4px;margin:0}',
|
||||||
'.dsh-wt_addLink{color:var(--dsw-alias-state-accent-primary,#4f8ef7);text-decoration:none}',
|
'.dsh-wt_addLink{color:var(--dsw-alias-state-accent-primary,#4f8ef7);text-decoration:none}',
|
||||||
'.dsh-wt_addForm{display:flex;flex-direction:column;gap:5px;padding:4px 8px 8px}',
|
'.dsh-wt_addForm{display:flex;flex-direction:column;gap:5px;padding:4px 8px 8px}',
|
||||||
@@ -82,4 +82,20 @@ export const css = [
|
|||||||
'.dsh-wt_splitDivider{background:var(--dsw-alias-border-l2,#3a4150);cursor:col-resize;touch-action:none;box-sizing:border-box}',
|
'.dsh-wt_splitDivider{background:var(--dsw-alias-border-l2,#3a4150);cursor:col-resize;touch-action:none;box-sizing:border-box}',
|
||||||
'.dsh-wt_splitDividerH{cursor:row-resize}',
|
'.dsh-wt_splitDividerH{cursor:row-resize}',
|
||||||
'.dsh-wt_splitDivider:hover,.dsh-wt_splitDividerH:hover{background:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
'.dsh-wt_splitDivider:hover,.dsh-wt_splitDividerH:hover{background:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||||
|
// 新建工作区(拓扑预设)
|
||||||
|
'.dsh-wt_presets{display:flex;gap:5px;padding:2px 8px 4px}',
|
||||||
|
'.dsh-wt_preset{flex:1;padding:5px 4px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font:inherit;font-size:11px;line-height:15px;cursor:pointer}',
|
||||||
|
'.dsh-wt_preset:hover{border-color:var(--dsw-alias-border-l2,#3a4150)}',
|
||||||
|
'.dsh-wt_preset[data-on=true]{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||||
|
// 布局条目卡片
|
||||||
|
'.dsh-wt_layout{display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}',
|
||||||
|
'.dsh-wt_layout:hover{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||||
|
'.dsh-wt_layout[data-on=true]{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||||
|
'.dsh-wt_layout[data-on=true] .dsh-wt_layoutName{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||||
|
'.dsh-wt_layoutIcon{flex:none;font-size:13px}',
|
||||||
|
'.dsh-wt_layoutText{flex:1;min-width:0;display:flex;flex-direction:column;gap:0}',
|
||||||
|
'.dsh-wt_layoutName{font-size:12px;font-weight:600;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
|
'.dsh-wt_layoutDesc{font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary,#6e7683);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
|
'.dsh-wt_layoutBadge{flex:none;font-size:9px;line-height:14px;padding:0 5px;border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||||
|
'.dsh-wt_layoutArrow{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:14px;line-height:20px}',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
+4
-1
@@ -358,7 +358,10 @@ type SplitPane = {
|
|||||||
- **M1 布局引擎**:✅ 核心已实现(2026-08-16)——`01_content/src/client/split.tsx` 通用分栏引擎
|
- **M1 布局引擎**:✅ 核心已实现(2026-08-16)——`01_content/src/client/split.tsx` 通用分栏引擎
|
||||||
(本版布局模型 = 标题栏 + 顶部通栏行(可选) + 主行内容窗 + 右下聊天窗;聊天窗 marginLeft+marginTop
|
(本版布局模型 = 标题栏 + 顶部通栏行(可选) + 主行内容窗 + 右下聊天窗;聊天窗 marginLeft+marginTop
|
||||||
组合挤法;会话切换重锚定不关闭;chat/top/pane 三级分隔线拖拽;`dsh.worktable.split.v1` 持久化),
|
组合挤法;会话切换重锚定不关闭;chat/top/pane 三级分隔线拖拽;`dsh.worktable.split.v1` 持久化),
|
||||||
owner props 新增 `openSplit(spec)`;⬜ 待做:拓扑预设选择器与「+」面板「新建工作区」UI;
|
owner props 新增 `openSplit(spec)`;
|
||||||
|
✅ 「+」面板「新建工作区」(2026-08-16):拓扑预设选择(左右/三栏横排/上一下二/井字,聊天恒贴右)→
|
||||||
|
布局名称 + 各窗 URL → 保存并打开;布局条目进入项目区(🧱 卡片、选中态、搜索、隐藏/改名/↑↓ 排序/
|
||||||
|
删除、折叠图标框),存 `dsh.worktable.projects.v1.layouts`;
|
||||||
- **互斥规则(2026-08-16 用户反馈定案)**:同一时刻仅一个分栏工作区——
|
- **互斥规则(2026-08-16 用户反馈定案)**:同一时刻仅一个分栏工作区——
|
||||||
① 反选:同一项目卡片再点 = 关闭;② 替换:不同项目互斥(选 B 关 A);
|
① 反选:同一项目卡片再点 = 关闭;② 替换:不同项目互斥(选 B 关 A);
|
||||||
③ 实现:引擎内开前先关旧;对外广播 `dsh:split-claim` 共享协议并监听让位;
|
③ 实现:引擎内开前先关旧;对外广播 `dsh:split-claim` 共享协议并监听让位;
|
||||||
|
|||||||
@@ -269,3 +269,15 @@
|
|||||||
用户此后手动选择「最近」会写入标记并被尊重;persistView 统一写入标记。
|
用户此后手动选择「最近」会写入标记并被尊重;persistView 统一写入标记。
|
||||||
fresh 状态默认本就 manual(DEFAULTS.orderBy='manual')。
|
fresh 状态默认本就 manual(DEFAULTS.orderBy='manual')。
|
||||||
- 验证:构建 + node --check 通过;服务端实时下发(含 sortMigratedV2);待用户刷新确认。
|
- 验证:构建 + node --check 通过;服务端实时下发(含 sortMigratedV2);待用户刷新确认。
|
||||||
|
|
||||||
|
## 补记(「+」新建工作区:拓扑预设 + 布局条目)
|
||||||
|
|
||||||
|
- 完成 M1 剩余项(PRD §13.5/§13.8):
|
||||||
|
① 「+」面板新增「新建工作区」区:四个拓扑预设(左右两栏/三栏横排/上一下二/井字四栏,
|
||||||
|
聊天窗恒贴右)+ 布局名称 + 各内容窗 URL(校验 / 或 http(s) 开头)→ 保存并直接打开;
|
||||||
|
② 布局条目 = 一等公民:进入项目区(🧱 卡片 + 布局角标 + N 窗描述),参与搜索/隐藏/改名/
|
||||||
|
↑↓ 与拖拽排序/「最近」排序(打开计使用)/选中态(activeSplitId)/折叠图标框;管理条内可删除;
|
||||||
|
③ 持久化进 dsh.worktable.projects.v1.layouts(LayoutSpec 数组)。
|
||||||
|
- 词典新增 12 键(zh/en);样式新增预设按钮与布局卡片。
|
||||||
|
- 验证:构建 + node --check 通过;服务端实时下发(含 PRESET_DEFS/saveLayout/dsh-wt_layout);
|
||||||
|
待用户刷新实测。
|
||||||
|
|||||||
Reference in New Issue
Block a user