feat: 工作区切换保活池(网页/MD滚动/激活标签不重置) + fix: 改名输入本地草稿(可先删原名,blur回显)

This commit is contained in:
Aisland
2026-08-18 01:14:41 +08:00
parent 12cc353964
commit a8b3270670
4 changed files with 232 additions and 68 deletions
+23 -6
View File
@@ -289,6 +289,28 @@ function syncSessionScope(list: any) {
}
}
/** 管理面板改名输入:本地草稿,blur/Enter 才提交(清空时不会立刻弹回原名) */
function RenameInput(props: { initial: string; placeholder: string; onCommit: (v: string) => void }) {
const [val, setVal] = useState(props.initial)
useEffect(() => { setVal(props.initial) }, [props.initial])
const commit = () => {
const v = val
props.onCommit(v)
// 清空提交 = 删除覆盖:回显原名(与文件改名一致)
if (!v.trim()) setVal(props.initial)
}
return (
<input
className="dsh-wt_manageInput"
value={val}
placeholder={props.placeholder}
onChange={(e) => setVal(e.target.value)}
onBlur={commit}
onKeyDown={(e) => { if (e.key === 'Enter') { commit(); (e.target as HTMLInputElement).blur() } }}
/>
)
}
function WorktableSection(props: any) {
const wide = props.wide !== false
const renderProjectSlot = typeof props.renderSlot === 'function' ? props.renderSlot : null
@@ -1115,12 +1137,7 @@ function WorktableSection(props: any) {
title={t('icons.change')}
onClick={(e) => { e.stopPropagation(); openIconPick('project', id, e.currentTarget as HTMLElement) }}
>{projects.iconOverrides[id] ?? meta?.icon ?? '📦'}</span>}
<input
className="dsh-wt_manageInput"
value={display}
placeholder={t('manage.renamePh')}
onChange={(e) => renameProject(id, e.target.value)}
/>
<RenameInput initial={display} placeholder={t('manage.renamePh')} onCommit={(v) => renameProject(id, v)} />
<button type="button" className="dsh-wt_manageBtn" title={isHidden ? t('manage.show') : t('manage.hide')} onClick={() => toggleHidden(id)}>
{isHidden ? '🙈' : '👁'}
</button>
+99 -48
View File
@@ -1352,50 +1352,12 @@ function PanePicker(props: { row: PaneRow; index: number }) {
)
}
/** 分栏工作区浮层(shell.overlay 座位;订阅 splitStore 快照渲染) */
function SplitWorkspace() {
const [snap, setSnap] = useState({
active: splitStore.active,
spec: splitStore.spec,
geom: splitStore.geom,
chatW: splitStore.chatW,
topH: splitStore.topH,
paneWs: [...splitStore.paneWs],
topWs: [...splitStore.topWs],
})
type PoolItem = { spec: LayoutSpec; chatW: number; topH: number; leftW: number; paneWs: number[]; topWs: number[] }
useEffect(() => splitStore.subscribe(() => {
setSnap({
active: splitStore.active,
spec: splitStore.spec,
geom: splitStore.geom,
chatW: splitStore.chatW,
topH: splitStore.topH,
paneWs: [...splitStore.paneWs],
topWs: [...splitStore.topWs],
})
}), [])
useEffect(() => {
if (!snap.active) return
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') splitStore.close()
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [snap.active])
// 标签拖放高亮 → 重渲染
const [, setDropTick] = useState(0)
useEffect(() => {
const fn = () => setDropTick((t) => t + 1)
dropTargetListeners.add(fn)
return () => { dropTargetListeners.delete(fn) }
}, [])
if (!snap.active || !snap.spec || !snap.geom) return null
const g = snap.geom
const spec = snap.spec
/** 单个工作区渲染层(geom 为 null 时用 0 几何渲染,外层 display:none 保活,保留网页/MD 滚动/激活标签等状态) */
function WorkspaceLayer(props: { spec: LayoutSpec; geom: Geom | null; chatW: number; topH: number; leftW: number; paneWs: number[]; topWs: number[] }) {
const g = props.geom ?? { left: 0, top: 0, right: 0, bottom: 0 }
const spec = props.spec
const top = spec.top ?? []
const main = spec.main ?? []
const hasLeft = !!spec.left
@@ -1403,12 +1365,12 @@ function SplitWorkspace() {
const chatLeft = !hasLeft && spec.chatSide === 'left'
const colW = g.right - g.left
const rowH = g.bottom - g.top
const chatW = clamp(snap.chatW, spec.chatWidth.min, Math.max(spec.chatWidth.min, colW - 60))
const chatW = clamp(props.chatW, spec.chatWidth.min, Math.max(spec.chatWidth.min, colW - 60))
const topH = hasTop
? clamp(snap.topH, spec.topHeight?.min ?? 80, Math.max(spec.topHeight?.min ?? 80, rowH - BAR_H - 80))
? clamp(props.topH, spec.topHeight?.min ?? 80, Math.max(spec.topHeight?.min ?? 80, rowH - BAR_H - 80))
: 0
const leftW = hasLeft
? clamp(snap.leftW, spec.leftWidth?.min ?? 160, Math.max(spec.leftWidth?.min ?? 160, colW - 260))
? clamp(props.leftW, spec.leftWidth?.min ?? 160, Math.max(spec.leftWidth?.min ?? 160, colW - 260))
: 0
const chatFull = spec.chatFullHeight === true
const contentW = Math.max(0, colW - chatW)
@@ -1416,8 +1378,8 @@ function SplitWorkspace() {
const topRowX = hasLeft ? g.left + leftW : contentX
const topRowW = hasLeft ? Math.max(0, colW - leftW) : (chatFull ? contentW : colW)
const topItems = allocate(top, snap.topWs, topRowW)
const mainItems = allocate(main, snap.paneWs, contentW)
const topItems = allocate(top, props.topWs, topRowW)
const mainItems = allocate(main, props.paneWs, contentW)
const leftItem = spec.left ? { pane: spec.left, left: 0, width: leftW } : null
const barTop = g.top
@@ -1542,6 +1504,95 @@ function SplitWorkspace() {
)
}
/** 分栏工作区浮层(shell.overlay 座位;订阅 splitStore 快照渲染)。
* 切换项目时旧工作区不销毁:全部挂载在池中、仅当前可见(display:none 保活),
* 网页子页面/滚动位置、MD 滚动位置、激活标签等在切回时原样保留。 */
function SplitWorkspace() {
const [snap, setSnap] = useState({
active: splitStore.active,
spec: splitStore.spec,
geom: splitStore.geom,
chatW: splitStore.chatW,
topH: splitStore.topH,
leftW: splitStore.leftW,
paneWs: [...splitStore.paneWs],
topWs: [...splitStore.topWs],
})
const poolRef = useRef<Map<string, PoolItem>>(new Map())
const [, setPoolTick] = useState(0)
useEffect(() => splitStore.subscribe(() => {
const spec = splitStore.spec
if (splitStore.active && spec) {
poolRef.current.set(spec.id, {
spec,
chatW: splitStore.chatW,
topH: splitStore.topH,
leftW: splitStore.leftW,
paneWs: [...splitStore.paneWs],
topWs: [...splitStore.topWs],
})
// 保活池上限 6 个(LRU:删最老的),避免长时间使用内存膨胀
while (poolRef.current.size > 6) {
const first = poolRef.current.keys().next().value
if (first != null) poolRef.current.delete(first)
}
}
setSnap({
active: splitStore.active,
spec: splitStore.spec,
geom: splitStore.geom,
chatW: splitStore.chatW,
topH: splitStore.topH,
leftW: splitStore.leftW,
paneWs: [...splitStore.paneWs],
topWs: [...splitStore.topWs],
})
setPoolTick((t) => t + 1)
}), [])
useEffect(() => {
if (!snap.active) return
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') splitStore.close()
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [snap.active])
// 标签拖放高亮 → 重渲染
const [, setDropTick] = useState(0)
useEffect(() => {
const fn = () => setDropTick((t) => t + 1)
dropTargetListeners.add(fn)
return () => { dropTargetListeners.delete(fn) }
}, [])
const activeId = snap.active && snap.spec ? snap.spec.id : null
const entries = Array.from(poolRef.current.entries())
if (entries.length === 0) return null
return (
<>
{entries.map(([id, item]) => {
const isActive = id === activeId
return (
<div key={id} style={isActive ? undefined : { display: 'none' }}>
<WorkspaceLayer
spec={item.spec}
geom={isActive ? snap.geom : null}
chatW={isActive ? snap.chatW : item.chatW}
topH={isActive ? snap.topH : item.topH}
leftW={isActive ? snap.leftW : item.leftW}
paneWs={isActive ? snap.paneWs : item.paneWs}
topWs={isActive ? snap.topWs : item.topWs}
/>
</div>
)
})}
</>
)
}
/** 调试出口(自动化验证用;必须在 store 定义之后) */
try { (window as any).__dshWorktable = { splitStore } } catch {}