DeepChat dc-ui 设计组件层:从 DcButton 到 DcToast 的组件契约、替换范围与行为等价迁移实践
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
本文以 DeepChat 仓库中 dc-ui 设计组件层的实施计划(plan.md)为主体,完整讲解该组件层的目录组织、别名注册方式、八个核心组件的契约定义、settings/chat 两侧的替换范围,以及第九轮 Action 收敛与行为等价约束。读完本文,你将掌握如何在 Vue + shadcn 技术栈中构建一层"少而全"的展示组件封装:既复用现有 shadcn 原语,又不改变任何事件、焦点与可访问性行为,并能让 Tailwind v4 正确扫描到组件内的 class。
配套文档可进一步参考:spec.md(组件契约与验收标准)、tasks.md(九轮实施任务清单)、design-system.md(设计系统总览)、accessibility/spec.md(可访问性契约)。
目录结构与别名注册
dc-ui 是 renderer 展示层组件库,位于src/dc-ui/,与src/shadcn/平级,导入路径统一走@dc-ui/*别名。实施计划给出的目标目录结构为:
src/dc-ui/ ├── index.ts # 顶层导出 └── components/ ├── button/ DcButton.vue + index.ts ├── icon-button/ DcIconButton.vue + index.ts ├── status-pill/ DcStatusPill.vue + index.ts ├── confirm-dialog/DcConfirmDialog.vue + index.ts ├── toggle-row/ DcToggleRow.vue + index.ts ├── empty/ DcEmpty.vue + index.ts ├── skeleton/ DcSkeleton.vue + index.ts └── toast/ DcToast.ts + index.ts对照当前仓库实际目录(src/dc-ui),可以看到结构在计划基础上继续演进:icon-button/在第九轮被DcButton吸收后移除,同时新增了badge/、choice-group/、dropdown-action-item/、form/、form-actions/、inline-error/、popover/、section-card/、sheet-panel/、tooltip/等组件目录,以及styles/motion.css动效令牌文件。这印证了实施计划"每个组件拥有一种稳定交互、复用优先"的原则——目录随真实调用方增长,而不是预先堆砌。
别名与构建扫描需要四处配套,缺一不可:
- Vite renderer 别名:electron.vite.config.ts 的 resolve.alias 中追加
'@dc-ui': resolve('src/dc-ui')。 - TS paths:tsconfig.app.json 与根 tsconfig.json 的
paths追加"@dc-ui/*"(与@shadcn/*的写法保持一致)。 - Tailwind v4 内容扫描:
src/renderer/src/assets/style.css的@source追加../../../dc-ui/**/*.{vue,ts,tsx,js,jsx},否则 dc-ui 组件内部使用的 Tailwind class 不会被扫描生成,样式会"消失"却不报错。 - 测试别名:vitest.config.renderer.ts 同样注册
@dc-ui,否则单测中 import dc 组件会解析失败(tasks.md 第六轮明确记录过这一修复)。
为什么必须让 Tailwind 扫描到 dc-ui
Tailwind v4 默认只扫描项目内被@source覆盖的文件。dc-ui 的组件把布局 class(如h-7 px-2.5 text-xs)硬编码在.vue模板与 cva 变体里,这些文件不在 renderer 默认扫描路径内。计划中专门强调这一点("否则 dc-ui 的 class 不会被扫描"),是这类跨目录组件库最容易踩的坑:开发时看起来一切正常,切换主题或增量构建后个别组件样式缺失。
组件契约总览
计划为每个组件定义了 props、事件、插槽与视觉基线。下面逐组件展开,并结合当前源码验证契约的实际落地情况。
DcButton:Button 的唯一收敛点
契约定义(见 DcButton.vue 与 props.ts):
- 透传 variant/size:保留
default / outline / ghost / destructive / link;secondary、lg、icon-lg不在 props 类型中显式暴露,但 cva 变体中实际存在,需要时仍可透传。 - 新增
size="xs":h-7 px-2.5 text-xs,用于收敛代码库中 20+ 处手工覆盖的小号按钮。源码中 xs 档完整为h-7 gap-1.5 rounded-md px-2.5 text-xs has-[>svg]:px-2(props.ts),带 icon 时自动收紧水平 padding。 iconSize:'3' | '3.5' | '4',默认'4',通过 computed 映射为size-3 / size-3.5 / size-4class(DcButton.vue),作用于内联 svg 尺寸。loading:内建 Spinner 替换 icon 区;icon:直接传 Iconify name,替代手写<Icon>节点。- 间距规则:icon + 文字用 flex
gap-1.5,不再叠加mr-*;替换调用方时必须移除手写的mr-1/mr-2。 - 可访问名:
label只提供aria-label可访问名,不创建 tooltip;tooltip才渲染可见提示,并在缺省 label 时作为兜底名(DcButton.vue)。 - 开发期校验:DEV 环境下 icon-only 按钮(size 命中
icon/icon-sm/icon-xs/icon-lg)若既无label也无tooltip,打印console.warn提示可访问性缺失(DcButton.vue)。
cva 基础类还包含动效与焦点样式:active:scale-[0.97]、motion-reduce:active:scale-100、focus-visible:ring-[3px],动画时长走--dc-motion-fast动效变量(props.ts),与styles/motion.css的令牌体系对应。
第九轮的关键演进:DcIconButton被DcButton完全吸收——icon / tooltip / label / loading / active / 默认插槽统一由 DcButton 承载,DcIconButton文件已删除。DcButton 的 tooltip 契约随之扩充:tooltipSide、tooltipSideOffset(默认 4)、tooltipDelayDuration(默认 0,保持原 DcButton 立即弹出的行为)、tooltipContentClass、tooltipIgnoreNonKeyboardFocus(默认 true),并由内建TooltipProvider :delay-duration="200"提供全局兜底(DcButton.vue)。
DcStatusPill:静态 Badge 与运行时状态分离
DcBadge表示静态元数据,DcStatusPill表示运行时状态,两者语义分离是设计系统里的硬约束(spec 中"Overlay and feedback contract"一节)。
- props:
status(六种语义态'neutral'|'active'|'success'|'warning'|'danger'|'disabled')、label、showDot(默认 true)、pulse(loading 态圆点animate-pulse)、size(sm=text-xs /xs=text-[11px])。 - 结构:
inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5+ 语义圆点。 - 颜色映射(DcStatusPill.vue):success=emerald、warning=amber、danger=red、active=primary、disabled/neutral=muted 系,全部带
dark:变体。
源码中一个值得注意的细节是status 别名归一化:normalize()函数把running→success、error/auth-error→danger、auth-required→warning、loading→active、offline/stopped→neutral(DcStatusPill.vue)。计划原文只列出了'running'|'loading'|'error'|'offline'别名,实际实现又为 MCP 场景补了auth-required与auth-error——tasks.md 第四轮记录了一次因缺auth-required导致pnpm run typecheck:web失败并修复的过程,说明别名集是随真实调用方增长的,而非一次定死。
典型调用方是 MCP 服务器卡片:running/loading/error/auth-required/stopped五种服务器状态经别名直接映射到语义色,不再手写bg-*类。
DcConfirmDialog:AlertDialog + 异步确认的完整封装
契约要点与源码对应(DcConfirmDialog.vue):
- props:
open(v-model)、title、description?、icon?、danger(默认 true,确认按钮走destructive)、confirmLabel?/cancelLabel?(缺省取 i18ncommon.confirm/common.cancel,见 DcConfirmDialog.vue)、busy、disabledConfirm、confirmIcon?;第三轮又追加confirmAttrs/cancelAttrs/busyDataTestid透传,用于保留调用方的data-testid等测试钩子。 - 事件:
confirm可返回 Promise,pending 期间双按钮禁用、确认按钮显示 busy Spinner;cancel在打开态变化为关闭时一并触发。 - 插槽:
default(如错误信息role="alert"行)、actions(完全自定义底部按钮组)。 - 容器:
w-[calc(100vw-2rem)] max-w-md,在移动端窄屏下仍留边距。
实现上还有一个计划未明说但源码可见的行为:焦点恢复——busy变 true 时记录document.activeElement,busy 结束后若焦点丢失到 body 且原触发元素仍在文档中,则focus({ preventScroll: true })还原(DcConfirmDialog.vue)。这正是 spec 中"Confirmation dialogs preserve ... focus restoration"验收项的落地。
替换面:settings 侧系统提示词/自定义提示词的删除确认,加上后续轮次扩展的 MemoryListView、DataSettings、OcrSettings 等 8 个原AlertDialogAsyncAction文件。
DcToggleRow:统一单行/双行两种开关行
源码:
- props:
id、icon?、label、description?、modelValue、disabled、ariaLabel?(缺省回落到 label)、labelMinWidth?。 - emit:
update:modelValue(Switch 的indeterminate值被归一为false);插槽:trailing(替换右侧区域,Switch 之前)。 - 布局规则:无 description 时单行
h-10;有 description 时切换为纵向双行(label 行 +pl-7 text-xs text-muted-foreground说明行),用一个组件统一了代码库中两种现存的开关行变体(DcToggleRow.vue)。
实际调用方如 CommonSettings、AutoCompactionSettingsSection、DisplaySettings 均迁移到DcToggleRow + DcSectionCard组合(tasks.md 第二轮)。
DcEmpty、DcSkeleton、DcToast
- DcEmpty(shadcn Empty 封装):props
icon?、title、description?;插槽default(正文)、action(主行动);容器统一border border-dashed rounded-lg居中布局。典型场景是自定义提示词列表的空态。 - DcSkeleton:props
width/height(默认100%/1rem,任意 CSS 值)、rounded?(默认md)、class透传;背景使用bg-muted/40-70渐变层级,沿用现有骨架惯例。 - DcToast(DcToast.ts):纯 TS 适配层,
DcToast.success/info/warning/error({ title, description?, code? })。code缺省时用crypto.randomUUID()生成,不新增 duration 参数——时长策略完全交给通知引擎按 kind 决定。内部仅调用@renderer-notifications/rendererNotificationPort的notifyRenderer,"不新增通知引擎、不覆盖时长策略"是写死在契约里的边界。
// src/dc-ui/components/toast/DcToast.ts 的核心逻辑(原文继承) const notify = (kind: TransientNotificationKind, options: DcToastOptions) => { notifyRenderer({ kind, code: options.code ?? crypto.randomUUID(), title: options.title, description: options.description }) } export const DcToast = { success: (options: DcToastOptions) => notify('success', options), info: (options: DcToastOptions) => notify('info', options), warning: (options: DcToastOptions) => notify('warning', options), error: (options: DcToastOptions) => notify('error', options) }第六轮中,dc-ui 的 Toast 还承担了架构清理职责:renderer 内联操作反馈机制(InlineOperationFeedback / useSurfaceFeedback / surfaceFeedbackController 等 4 个文件)被整体删除,prompt 管理链路与其余 28 个调用文件的成功/失败提示统一收敛到notifyRenderertoast,全量pnpm run test:renderer238 文件 / 1958 用例全绿(tasks.md 第六、七轮记录)。
替换范围:settings 与 chat-main 两个试点面
计划划定的首批替换范围刻意选择"高价值、模式重复"的文件,而不是全仓扫荡:
settings(提示词管理全量)
PromptSetting.vue:页面操作按钮 → DcButton(含iconSize定制)。prompt/SystemPromptSettingsSection.vue:新建/重置/删除按钮 → DcButton/DcIconButton,删除确认 → DcConfirmDialog(danger + busy),「启用」pill →DcStatusPill(active)。prompt/CustomPromptSettingsSection.vue:新增/编辑/删除 → DcButton/DcIconButton + DcConfirmDialog;状态 pill →DcStatusPill(active/disabled)加 neutral 来源标识;空态 → DcEmpty;启停 Switch 保持原样(dc 层不重复封装 Switch,复用 shadcn 原语)。prompt/PromptEditorSheet.vue/SystemPromptEditorSheet.vue:footer 按钮 → DcButton。
chat-main(高价值组件)
ChatTopBar.vue:ghost 型 icon 按钮 → DcIconButton(label + tooltip)。WindowSideBar.vue:icon 按钮 → DcIconButton。components/message/MessageToolbar.vue:5 个 Tooltip 包裹按钮 → DcIconButton(注:16px 超紧凑按钮曾被记为例外保留,第九轮 review 后以icon-xs尺寸 + 原尺寸还原的方式重新迁移)。components/mcp-config/components/McpServerCard.vue:状态区 → DcStatusPill(running/loading/error/auth-required/stopped 映射)。
后续的 Sheet/Popover 收敛同样遵循"保留既有行为"原则:DcSheetPanel appearance="plain"吸收原DcSheetDialog,McpServers 详情 Sheet 保留原 width、padding、ScrollArea、footer 与空 description 的 DOM/ARIA 结构;DcPopoverPanel收敛为DcPopover,McpIndicator 保留受控 open、trigger、header 与定位(tasks.md 第九轮、plan.md 第九轮条目)。
数据流与行为等价约束
dc-ui 的定位在 plan 中被一句话锁定:"dc-ui 是 renderer 展示层封装,不触碰 store / IPC / shared。"组件只拥有展示、可访问性与共享状态;业务操作、持久化、IPC 仍归功能模块所有(spec 的 "Purpose and ownership" 同义重申)。
在此定位下,迁移必须逐项保持行为等价,计划给出了四条硬约束:
- 原事件、修饰符与 payload 不变:
@click/@select/.stop/.prevent不得被组件吞掉、改名或重发。实现上 DcButton 设inheritAttrs: false,全部$attrs直接落到最内层Primitive元素上,data-testid等业务钩子随 attrs 自然落在原本可交互的 DOM 上。 - 原禁用条件、
type、loading、Popover/Dialog 开闭、键盘及焦点行为不变;缺失 tooltip 时只为可操作的 icon-only 控件补充,并复用既有 i18n 文案同时作为label。 - 原 tooltip 的文案、side、delay、条件和嵌套触发器结构不变;
DcButton因此保留tooltipDelayDuration默认 0 与ignoreNonKeyboardFocus默认 true 两个"兼容默认值",而非强加统一时序。 - 纯展示 icon、文字按钮、说明型 Switch/Checkbox/链接 tooltip 不为统一形式而改变交互——收敛只发生在真正有操作语义的控件上。
spec 的验收章节还要求:保留事件修饰与 payload、原生元素类型、disabled 条件、loading、overlay 状态、键盘行为、焦点、i18n key 与测试钩子;属性转发到拥有控件的元素而不吞掉业务事件;新行为必须满足 accessibility/spec.md 的可访问性契约。
验证策略与质量门
计划给出的验证策略(原文保留):
- 每一处迁移先人工对照原模板:事件、修饰符、disabled、
type、loading、测试钩子、tooltip 契约。 - 本阶段不运行 vitest / lint / fmt / typecheck;仅在要求提交时,按受影响范围统一执行。
- 提交前人工检查主窗口与设置窗口中按钮、弹窗、tooltip 的可见交互与深浅色样式。
实际执行中(tasks.md),验证逐步升级为可重复的质量门:
- 每轮结束跑
pnpm run typecheck:web,并真实捕获过类型错误(DcStatusAlias 缺auth-required、DcSheetPanelopen可选化、McpIndicator 模板 v-if/else 配对等)。 - 第六轮全量
pnpm run test:renderer:238 文件 / 1956 用例全部通过,期间修复了 vitest 别名缺失与 DcConfirmDialog teleport 所需的 AlertDialog stubs。 - 第七轮再全量验证:typecheck 与 1958 用例全绿。
- 主窗口 ChatMainApp 根加全局
TooltipProvider(delay 200 + ignore-non-keyboard-focus,与 settings App.vue 对齐)后,新代码无需再局部包裹;11 个既有局部 Provider 因 delay/ignore 配置不同而保留为显式覆盖(第八轮)。
小结:一套可复用的组件层治理路径
把 plan.md 的脉络串起来,dc-ui 组件层给出的是一套可迁移的组件层治理方法:
- 先定契约再建组件:每个组件的 props/事件/插槽/视觉基线先写在 plan 里,源码只是契约的兑现(对照 docs/features/dc-ui-components/plan.md 与 src/dc-ui 可以逐项核对)。
- 别名 + 扫描四处配套:vite alias、两个 tsconfig paths、Tailwind
@source,再加 vitest alias——任何一处漏配都会产生"能跑但样式缺失/测试挂"的隐蔽问题。 - 收敛而非新增:第九轮删除
DcIconButton、DcSheetDialog、DcPopoverPanel并入更通用组件,目录从"按计划建"变成"按调用方长",spec 明确要求"Shared APIs grow only for actual callers"。 - 行为等价作为迁移准入门槛:事件修饰符、焦点、testid、i18n key、tooltip 时序逐项保留,配合 typecheck + vitest 双质量门,让展示层重构不需要业务侧回归成本。
对想在同类 Electron + Vue + shadcn 项目中建立展示组件层的读者,这份 plan/spec/tasks 三件套与src/dc-ui源码本身就是可直接对照的样板:契约文档先行、替换范围按"高价值重复模式"切块、每轮以类型检查与组件测试收口。
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考