DeepChat dc-ui 设计组件层:从 DcButton 到 DcToast 的组件契约、替换范围与行为等价迁移实践
2026/9/17 18:46:29 网站建设 项目流程

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动效令牌文件。这印证了实施计划"每个组件拥有一种稳定交互、复用优先"的原则——目录随真实调用方增长,而不是预先堆砌。

别名与构建扫描需要四处配套,缺一不可:

  1. Vite renderer 别名:electron.vite.config.ts 的 resolve.alias 中追加'@dc-ui': resolve('src/dc-ui')
  2. TS paths:tsconfig.app.json 与根 tsconfig.json 的paths追加"@dc-ui/*"(与@shadcn/*的写法保持一致)。
  3. Tailwind v4 内容扫描src/renderer/src/assets/style.css@source追加../../../dc-ui/**/*.{vue,ts,tsx,js,jsx},否则 dc-ui 组件内部使用的 Tailwind class 不会被扫描生成,样式会"消失"却不报错。
  4. 测试别名: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 / linksecondarylgicon-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 + 文字用 flexgap-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-100focus-visible:ring-[3px],动画时长走--dc-motion-fast动效变量(props.ts),与styles/motion.css的令牌体系对应。

第九轮的关键演进DcIconButtonDcButton完全吸收——icon / tooltip / label / loading / active / 默认插槽统一由 DcButton 承载,DcIconButton文件已删除。DcButton 的 tooltip 契约随之扩充:tooltipSidetooltipSideOffset(默认 4)、tooltipDelayDuration(默认 0,保持原 DcButton 立即弹出的行为)、tooltipContentClasstooltipIgnoreNonKeyboardFocus(默认 true),并由内建TooltipProvider :delay-duration="200"提供全局兜底(DcButton.vue)。

DcStatusPill:静态 Badge 与运行时状态分离

DcBadge表示静态元数据,DcStatusPill表示运行时状态,两者语义分离是设计系统里的硬约束(spec 中"Overlay and feedback contract"一节)。

  • props:status(六种语义态'neutral'|'active'|'success'|'warning'|'danger'|'disabled')、labelshowDot(默认 true)、pulse(loading 态圆点animate-pulse)、sizesm=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→successerror/auth-error→dangerauth-required→warningloading→activeoffline/stopped→neutral(DcStatusPill.vue)。计划原文只列出了'running'|'loading'|'error'|'offline'别名,实际实现又为 MCP 场景补了auth-requiredauth-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)、titledescription?icon?danger(默认 true,确认按钮走destructive)、confirmLabel?/cancelLabel?(缺省取 i18ncommon.confirm/common.cancel,见 DcConfirmDialog.vue)、busydisabledConfirmconfirmIcon?;第三轮又追加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:idicon?labeldescription?modelValuedisabledariaLabel?(缺省回落到 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 封装):propsicon?titledescription?;插槽default(正文)、action(主行动);容器统一border border-dashed rounded-lg居中布局。典型场景是自定义提示词列表的空态。
  • DcSkeleton:propswidth/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/rendererNotificationPortnotifyRenderer,"不新增通知引擎、不覆盖时长策略"是写死在契约里的边界。
// 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" 同义重申)。

在此定位下,迁移必须逐项保持行为等价,计划给出了四条硬约束:

  1. 原事件、修饰符与 payload 不变@click/@select/.stop/.prevent不得被组件吞掉、改名或重发。实现上 DcButton 设inheritAttrs: false,全部$attrs直接落到最内层Primitive元素上,data-testid等业务钩子随 attrs 自然落在原本可交互的 DOM 上。
  2. 原禁用条件、type、loading、Popover/Dialog 开闭、键盘及焦点行为不变;缺失 tooltip 时只为可操作的 icon-only 控件补充,并复用既有 i18n 文案同时作为label
  3. 原 tooltip 的文案、side、delay、条件和嵌套触发器结构不变DcButton因此保留tooltipDelayDuration默认 0 与ignoreNonKeyboardFocus默认 true 两个"兼容默认值",而非强加统一时序。
  4. 纯展示 icon、文字按钮、说明型 Switch/Checkbox/链接 tooltip 不为统一形式而改变交互——收敛只发生在真正有操作语义的控件上。

spec 的验收章节还要求:保留事件修饰与 payload、原生元素类型、disabled 条件、loading、overlay 状态、键盘行为、焦点、i18n key 与测试钩子;属性转发到拥有控件的元素而不吞掉业务事件;新行为必须满足 accessibility/spec.md 的可访问性契约。

验证策略与质量门

计划给出的验证策略(原文保留):

  1. 每一处迁移先人工对照原模板:事件、修饰符、disabled、type、loading、测试钩子、tooltip 契约。
  2. 本阶段不运行 vitest / lint / fmt / typecheck;仅在要求提交时,按受影响范围统一执行。
  3. 提交前人工检查主窗口与设置窗口中按钮、弹窗、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 组件层给出的是一套可迁移的组件层治理方法:

  1. 先定契约再建组件:每个组件的 props/事件/插槽/视觉基线先写在 plan 里,源码只是契约的兑现(对照 docs/features/dc-ui-components/plan.md 与 src/dc-ui 可以逐项核对)。
  2. 别名 + 扫描四处配套:vite alias、两个 tsconfig paths、Tailwind@source,再加 vitest alias——任何一处漏配都会产生"能跑但样式缺失/测试挂"的隐蔽问题。
  3. 收敛而非新增:第九轮删除DcIconButtonDcSheetDialogDcPopoverPanel并入更通用组件,目录从"按计划建"变成"按调用方长",spec 明确要求"Shared APIs grow only for actual callers"。
  4. 行为等价作为迁移准入门槛:事件修饰符、焦点、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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询