From 1de13200e2103733a717773dcb5dd910c8ba0783 Mon Sep 17 00:00:00 2001 From: Aisland Date: Mon, 17 Aug 2026 21:34:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=B8=E9=A9=BB=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=8F=AF=E6=8D=A2(iconOverrides=20DOM?= =?UTF-8?q?=E5=B1=82)=20+=20=E5=B8=83=E5=B1=80=E5=8D=A1=E5=8D=95=E8=A1=8C?= =?UTF-8?q?=E5=8C=96=E5=AF=B9=E9=BD=90=E5=B8=B8=E9=A9=BB=20+=20=E5=A4=B4?= =?UTF-8?q?=E9=83=A83=E6=8C=89=E9=92=AE=E7=85=A7=E6=8A=84=E5=AE=98?= =?UTF-8?q?=E6=96=B9SVG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/src/client/index.tsx | 157 +++++++++++++----- 01_content/src/client/locales.ts | 4 - 01_content/src/client/styles.ts | 7 +- .../worklogs/2026-08-16_worktable-v2.md | 22 +++ 04_test/bundle-eval.cjs | 2 +- 04_test/functional-diag.cjs | 15 ++ 6 files changed, 156 insertions(+), 51 deletions(-) diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index bcb9cbd..ed820c8 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -41,6 +41,8 @@ type ProjectsState = { hidden: string[] /** 显示名覆盖(编辑模式改名;空值删除覆盖)。 */ nameOverrides: Record + /** 图标覆盖(点图标换 emoji;含入驻插件项目 id → emoji)。 */ + iconOverrides: Record /** 本地快捷方式条目。 */ shortcuts: Shortcut[] /** 用户自建的布局条目(「+」新建工作区保存的 LayoutSpec)。 */ @@ -74,9 +76,28 @@ const PRESET_DEFS = [ { id: 't3', leftCount: 0, topCount: 1, contentCount: 2, chatFull: true }, ] as const -/** 侧栏图标备选集(emoji):布局/快捷方式的图标,点击可换(首项 🧱 为布局默认) */ +/** 侧栏图标备选集(emoji):布局/快捷方式/入驻项目的图标,点击可换(首项 🧱 为布局默认) */ const EMOJI_SET = ['🧱', '🏠', '🎓', '🚗', '✈️', '🌍', '🏥', '📚', '✏️', '⚙️', '🎨', '🎮', '🌏', '📐', '🧪', '🤖', '📦', '💬'] +/** 官方工作区头部按钮图标(自 DSH Web GUI 工作区面板取样,fill=currentColor 跟随主题) */ +const ICON_SEARCH = ( + + + + +) +const ICON_VIEW_OPTIONS = ( + + + +) +const ICON_ADD = ( + + + + +) + function buildLayout(presetId: string, name: string): LayoutSpec { const def = PRESET_DEFS.find((d) => d.id === presetId) ?? PRESET_DEFS[0] const mk = (i: number): SplitPane => ({ @@ -164,6 +185,7 @@ const DEFAULT_PROJECTS: ProjectsState = { lastUsed: {}, hidden: [], nameOverrides: {}, + iconOverrides: {}, shortcuts: [], layouts: [], } @@ -201,6 +223,7 @@ function loadProjects(): ProjectsState { lastUsed: p.lastUsed && typeof p.lastUsed === 'object' ? p.lastUsed : {}, hidden: Array.isArray(p.hidden) ? p.hidden.filter((x: unknown): x is string => typeof x === 'string') : [], nameOverrides: p.nameOverrides && typeof p.nameOverrides === 'object' ? p.nameOverrides : {}, + iconOverrides: p.iconOverrides && typeof p.iconOverrides === 'object' ? p.iconOverrides : {}, shortcuts: Array.isArray(p.shortcuts) ? p.shortcuts.filter((s: any) => s && typeof s.id === 'string' && typeof s.name === 'string' && typeof s.href === 'string') : [], @@ -287,7 +310,7 @@ function WorktableSection(props: any) { const [wsName, setWsName] = useState('') const [wsError, setWsError] = useState(false) /** 图标选择器:kind + 目标 id + 弹窗锚点坐标(fixed 定位) */ - const [iconPick, setIconPick] = useState<{ kind: 'layout' | 'shortcut'; id: string; x: number; y: number } | null>(null) + const [iconPick, setIconPick] = useState<{ kind: 'layout' | 'shortcut' | 'project'; id: string; x: number; y: number } | null>(null) const [float, setFloat] = useState(() => view.dock === 'float' && view.floatTop != null ? { top: view.floatTop } : null, ) @@ -496,6 +519,7 @@ function WorktableSection(props: any) { order: effectiveOrder, hidden: projects.hidden, nameOverrides: projects.nameOverrides, + iconOverrides: projects.iconOverrides, reportMeta, reportUsed, openSplit, @@ -627,11 +651,10 @@ function WorktableSection(props: any) { persistProjects((prev) => ({ ...prev, layouts: prev.layouts.filter((l) => l.id !== id) })) } - // ── 图标选择器(布局 / 快捷方式的侧栏 emoji 点击可换) ── - const openIconPick = (kind: 'layout' | 'shortcut', id: string, e: any) => { - e.stopPropagation() - e.preventDefault() - const r = (e.currentTarget as HTMLElement).getBoundingClientRect() + // ── 图标选择器(布局 / 快捷方式 / 入驻项目的侧栏 emoji 点击可换) ── + // anchor:DOM 锚点元素(自己卡片里的 icon 元素或事件 currentTarget) + const openIconPick = (kind: 'layout' | 'shortcut' | 'project', id: string, anchor: HTMLElement) => { + const r = anchor.getBoundingClientRect() const x = Math.min(r.right + 8, Math.max(16, window.innerWidth - 284)) const y = Math.max(MIN_TOP, Math.min(r.top - 4, window.innerHeight - 316)) setIconPick({ kind, id, x, y }) @@ -648,6 +671,48 @@ function WorktableSection(props: any) { shortcuts: prev.shortcuts.map((s) => (s.id === id ? { ...s, icon } : s)), })) } + const setProjectIcon = (id: string, icon: string) => { + persistProjects((prev) => ({ ...prev, iconOverrides: { ...prev.iconOverrides, [id]: icon } })) + } + + // 入驻项目卡片图标覆盖(DOM 层,不改动对方插件代码) + // 1) 属性同步:iconOverrides[id] -> 卡片 icon 元素 data-wt-icon(CSS 用 attr() 换显示); + // 2) 委托点击:捕获阶段拦截卡片 icon 点击 -> 打开图标选择器(阻止卡片自身打开项目)。 + useEffect(() => { + const ROOT_TO_ID = [['.ta_card', 'travelatlas'], ['.pr_card', 'planreview']] + const sync = () => { + for (const [rootSel, pid] of ROOT_TO_ID) { + const cards = document.querySelectorAll('.dsh-wt_projects ' + rootSel) + cards.forEach((card) => { + const icon = card.querySelector('.ta_cardIcon, .pr_cardIcon') + if (!icon) return + const ovr = projects.iconOverrides[pid] + if (ovr) icon.setAttribute('data-wt-icon', ovr) + else icon.removeAttribute('data-wt-icon') + }) + } + } + sync() + const mo = new MutationObserver(sync) + mo.observe(document.body, { childList: true, subtree: true }) + return () => mo.disconnect() + }, [projects.iconOverrides]) + + useEffect(() => { + const onDocClick = (e) => { + const target = e.target + const icon = target && target.closest ? target.closest('.ta_cardIcon, .pr_cardIcon') : null + if (!icon || !icon.closest('.dsh-wt_projects')) return + const card = icon.closest('.ta_card, .pr_card') + const pid = card && card.classList.contains('ta_card') ? 'travelatlas' : card && card.classList.contains('pr_card') ? 'planreview' : null + if (!pid) return + e.stopPropagation() + e.preventDefault() + openIconPick('project', pid, icon) + } + document.addEventListener('click', onDocClick, true) + return () => document.removeEventListener('click', onDocClick, true) + }, []) const query = view.query.trim() const queryLower = query.toLowerCase() @@ -730,7 +795,7 @@ function WorktableSection(props: any) { const popTop = clamp(sectionTop, MIN_TOP, Math.max(MIN_TOP, window.innerHeight - 540)) if (!wide) { - const projectIcons = registeredIds.map((id) => metas[id]?.icon ?? '📦') + const projectIcons = registeredIds.map((id) => projects.iconOverrides[id] ?? metas[id]?.icon ?? '📦') const shortcutIcons = projects.shortcuts.map((s) => s.icon) const layoutIcons = projects.layouts.map((l) => l.icon ?? '🧱') const icons = [...projectIcons, ...shortcutIcons, ...layoutIcons] @@ -787,21 +852,21 @@ function WorktableSection(props: any) { aria-label={t('menu.search')} title={t('menu.search')} onClick={() => persistView({ searchOpen: !view.searchOpen, query: view.searchOpen ? '' : view.query })} - >🔍 + >{ICON_SEARCH} + >{ICON_VIEW_OPTIONS} + >{ICON_ADD} @@ -855,7 +920,9 @@ function WorktableSection(props: any) { {EMOJI_SET.map((em) => { const cur = iconPick.kind === 'layout' ? (projects.layouts.find((l) => l.id === iconPick.id)?.icon ?? '🧱') - : (projects.shortcuts.find((s) => s.id === iconPick.id)?.icon ?? '🔗') + : iconPick.kind === 'shortcut' + ? (projects.shortcuts.find((s) => s.id === iconPick.id)?.icon ?? '🔗') + : (projects.iconOverrides[iconPick.id] ?? metas[iconPick.id]?.icon ?? '📦') return ( @@ -920,9 +988,15 @@ function WorktableSection(props: any) { role="button" tabIndex={0} title={t('icons.change')} - onClick={(e) => openIconPick('layout', id, e)} + onClick={(e) => { e.stopPropagation(); openIconPick('layout', id, e.currentTarget as HTMLElement) }} >{layout.icon ?? '🧱'} - : {meta?.icon ?? '📦'}} + : { e.stopPropagation(); openIconPick('project', id, e.currentTarget as HTMLElement) }} + >{projects.iconOverrides[id] ?? meta?.icon ?? '📦'}} openIconPick('shortcut', s.id, e)} + onClick={(e) => { e.stopPropagation(); openIconPick('shortcut', s.id, e.currentTarget as HTMLElement) }} >{s.icon} {s.name} @@ -962,33 +1036,28 @@ function WorktableSection(props: any) { {renderProjectSlot ? renderProjectSlot('sidebar.worktable.project', ownerProps) :
{t('empty')}
} - {visibleLayouts.map((l) => { - const paneCount = [...(l.top ?? []), ...l.main].length - return ( - - ) - })} + {visibleLayouts.map((l) => ( + + ))} {visibleShortcuts.length > 0 && ( @@ -1007,7 +1076,7 @@ function WorktableSection(props: any) { role="button" tabIndex={0} title={t('icons.change')} - onClick={(e) => openIconPick('shortcut', s.id, e)} + onClick={(e) => { e.stopPropagation(); e.preventDefault(); openIconPick('shortcut', s.id, e.currentTarget as HTMLElement) }} >{s.icon} {s.name} {t('shortcut.badge')} diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index 162a05a..c3be2a6 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -39,8 +39,6 @@ export const zh = { 'manage.deleteShortcut': '删除快捷方式', 'manage.reset': '恢复默认', 'shortcut.badge': '本地', - 'layout.badge': '布局', - 'layout.desc': '{n} 内容窗 · 对话', 'add.chooseLayout': '选择布局模式', 'add.layoutNamePh': '布局名称', 'add.layoutSave': '进入工作区', @@ -110,8 +108,6 @@ export const en = { 'manage.deleteShortcut': 'Delete shortcut', 'manage.reset': 'Reset defaults', 'shortcut.badge': 'Local', - 'layout.badge': 'Layout', - 'layout.desc': '{n} content panes · chat', 'add.chooseLayout': 'Choose a layout', 'add.layoutNamePh': 'Layout name', 'add.layoutSave': 'Enter workspace', diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts index 81140d5..fc51061 100644 --- a/01_content/src/client/styles.ts +++ b/01_content/src/client/styles.ts @@ -104,8 +104,6 @@ export const css = [ '.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}', // 项目卡片统一:入驻插件卡片(travelatlas/planreview)与布局卡片同款常显框(静止态可见边框+底) '.dsh-wt_projects .ta_card,.dsh-wt_projects .pr_card{border-color:var(--dsw-alias-border-l1,#262b36);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02))}', @@ -123,6 +121,11 @@ export const css = [ // 可点图标(布局卡 / 管理行 / 快捷方式上的图标,点击换 emoji) '.dsh-wt_iconPick{flex:none;display:inline-flex;align-items:center;justify-content:center;padding:1px;border:none;background:transparent;font:inherit;cursor:pointer;border-radius:4px}', '.dsh-wt_iconPick:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.08))}', + // 入驻项目卡片图标覆盖:data-wt-icon 存在时用 attr() 替换显示(原字符字号压到 0) + '.dsh-wt_projects .ta_cardIcon[data-wt-icon],.dsh-wt_projects .pr_cardIcon[data-wt-icon]{font-size:0;line-height:20px}', + '.dsh-wt_projects .ta_cardIcon[data-wt-icon]::before,.dsh-wt_projects .pr_cardIcon[data-wt-icon]::before{content:attr(data-wt-icon);font-size:15px;line-height:20px}', + // 头部按钮内的官方 SVG 图标 + '.dsh-wt_iconBtn svg{display:block}', // 窗内容:选择器 / 自定义 / 浏览器 / 占位 '.dsh-wt_panePicker{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:12px;overflow:auto}', '.dsh-wt_panePicker-row{flex-direction:row;flex-wrap:wrap}', diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md index 656a4bd..9fabcf5 100644 --- a/02_process/worklogs/2026-08-16_worktable-v2.md +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -511,3 +511,25 @@ (travelatlas 在无会话 headless 页面不自开分栏,taSplitPresent=false 属预期,改合成验证。) STEP1-5 回归不变(paneWs 均分、picker 自适应、拖标签);ERRORS_COUNT: 0。 - 备注:纯客户端改动,F5 即生效,无需重启 dsh web。 +## 补记(常驻项目图标可换 + 新建布局卡单行化 + 头部按钮对齐官方图标) + +- 用户反馈(三条): + ① 常驻项目(建筑审图/旅行 Atlas)也纳入图标可换范围(🌏📐 已含在 18 项图标集里); + ② 新建布局卡片要与常驻项目展示完全一致:标题单行,去掉第二行描述与右侧「布局」小字; + ③ 工作台右上角 3 按钮(搜索/视图选项/添加)完全照抄官方工作区头部 icon。 +- 处理: + ① 常驻项目图标(DOM 层,不改动 travelatlas/planreview 代码):projects.v1 新增 + iconOverrides 字段并传给卡片 ownerProps;effect 把 override 写到卡片 icon 元素的 + data-wt-icon 属性,CSS 用 attr() 换显示(原字符字号压 0);文档捕获阶段委托点击 + .ta_cardIcon/.pr_cardIcon → 打开图标选择器(kind: project,stopPropagation 阻止卡片 + 自身打开);管理行插件图标、rail 图标同步可换。图标选择器支持三类:layout/shortcut/project。 + ② 布局卡单行化:去掉 .dsh-wt_layoutDesc 与 .dsh-wt_layoutBadge 渲染及样式、 + locale 的 layout.desc/layout.badge 键;卡片 = 图标 + 名称 + ›,与 ta_card/pr_card 一致。 + ③ 头部按钮换官方 SVG:用 CDP 探针从运行中的 DSH Web GUI 工作区面板取样三枚图标 + (Search sessions 放大镜 / View options 滑块 / Add workspace 文档加号,fill=currentColor), + 内联为 ICON_SEARCH/ICON_VIEW_OPTIONS/ICON_ADD 常量替换原 🔍/☰/+ 文本。 +- 验证(functional-diag STEP6 扩展):layoutDesc=0、layoutBadge=0、headerSvgs=3; + ta_cardIcon 点击 → popup → 选 ✈️ → taIconAttr=✈️ + projects.v1.iconOverrides.travelatlas=✈️; + 既有图标链路(🧱→🏠)+ ta/pr 统一框 + 小字隐藏 + STEP7 互斥全部保持;ERRORS_COUNT: 0。 + bundle-eval 补 jsxs 桩后 PASS(模块级 JSX 常量引入)。 +- 备注:纯客户端改动,F5 即生效,无需重启 dsh web。 diff --git a/04_test/bundle-eval.cjs b/04_test/bundle-eval.cjs index 4666183..8e20a76 100644 --- a/04_test/bundle-eval.cjs +++ b/04_test/bundle-eval.cjs @@ -45,7 +45,7 @@ sandbox.self = sandbox; try { const fakeRequire = (name) => { const stubs = { "react": { useState: (v)=>[v,()=>{}], useEffect: ()=>{}, useLayoutEffect: ()=>{}, useMemo: (f)=>f(), useCallback: (f)=>f, useRef: (v)=>({current:v}), createElement: ()=>null, Fragment: null }, - "react/jsx-runtime": { jsx: ()=>({}) }, + "react/jsx-runtime": { jsx: ()=>({}), jsxs: ()=>({}) }, "react/jsx-dev-runtime": { jsx: ()=>({}) }, "@deepseek-ai/dsh-client-ui-slots": {}, "@deepseek-ai/dsh-client-ui-primitives": {}, diff --git a/04_test/functional-diag.cjs b/04_test/functional-diag.cjs index 5d21667..b4c4673 100644 --- a/04_test/functional-diag.cjs +++ b/04_test/functional-diag.cjs @@ -99,10 +99,25 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); out.icon1=card.querySelector('.dsh-wt_layoutIcon').textContent; try{ out.saved=JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).layouts[0].icon }catch(e){ out.saved='ERR:'+e.message } } + out.layoutDesc=document.querySelectorAll('.dsh-wt_layoutDesc').length; + out.layoutBadge=document.querySelectorAll('.dsh-wt_layoutBadge').length; + out.headerSvgs=document.querySelectorAll('.dsh-wt_actions .dsh-wt_iconBtn svg').length; var ta=document.querySelector('.dsh-wt_projects .ta_card'); if(ta){ var cs=getComputedStyle(ta); out.taBorder=cs.borderTopColor; var desc=ta.querySelector('.ta_cardDesc'); out.taDescDisplay=desc?getComputedStyle(desc).display:null; } var pr=document.querySelector('.dsh-wt_projects .pr_card'); if(pr){ var cs2=getComputedStyle(pr); out.prBorder=cs2.borderTopColor; var desc2=pr.querySelector('.pr_cardDesc'); out.prDescDisplay=desc2?getComputedStyle(desc2).display:null; } + // 常驻项目图标换选:点 ta_cardIcon → 选择器 → 选第 5 项 ✈️ → data-wt-icon + 持久化 + var taIcon=document.querySelector('.dsh-wt_projects .ta_cardIcon'); + if(taIcon){ + taIcon.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true})); + await new Promise(function(r){setTimeout(r,250)}); + out.taPopup=!!document.querySelector('.dsh-wt_iconPop'); + var cells2=document.querySelectorAll('.dsh-wt_iconCell'); + if(cells2[4]){ cells2[4].dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true})); } + await new Promise(function(r){setTimeout(r,250)}); + out.taIconAttr=taIcon.getAttribute('data-wt-icon'); + try{ out.taSaved=JSON.parse(localStorage.getItem('dsh.worktable.projects.v1')).iconOverrides.travelatlas }catch(e){ out.taSaved='ERR:'+e.message } + } return JSON.stringify(out); })()`); console.log('STEP6:', JSON.stringify(step6));