基于 design/ 设计系统的 100% 还原实现方案 · 桌面(Tauri 2)+ 移动(原生键盘扩展与主 App)
设计系统位于 design/(tokens / React 组件 / UI kits / 品牌资产),是所有界面的唯一真相源。两类终端用不同方式消费:
| 端 | 消费方式 | 保真度保证 |
|---|---|---|
| 桌面端 (Tauri 2 webview) |
直接复用:React 18 + Vite,把 design/ 作为 monorepo 内共享包(@dudu/design)引入,链入 styles.css 全套令牌,直接 import components/ 下的 Button / Switch / SettingRow / MicBar / RecognitionOverlay 等组件 |
零转译,与 ui_kits 演示像素级一致 |
| 移动端 (Swift / Kotlin 原生) |
令牌导出管线 + 组件规格对照:构建脚本解析 design/tokens/*.css,生成 iOS DuduTheme.swift(light/dark 双值 Color/CGFloat)与 Android DuduTheme.kt + values/values-night 资源;组件按本文档第三章的规格表原生实现 |
令牌单一来源自动同步;组件规格逐条对照 JSX 源码(尺寸/圆角/字号/色值/文案均给出精确值) |
design-pipeline/(Node 或 Go 实现均可),CI 中校验生成产物与 tokens/*.css 一致,防止两端漂移。
#07C160(仅登录按钮)来源 design/tokens/,此处摘录实现必需的关键值(完整以源文件为准)。
| 令牌 | Light | Dark | 用途 |
|---|---|---|---|
--bg-app | #F6F7FA | #0F1116 | 页面底色 |
--surface-card | #FFFFFF | #171A22 | 卡片 / 键帽 |
--accent | #4F6EF7 | #5C77E8 | 主操作 / 品牌(hover #3D58DB·#6E89F9,press #2F44B3·#4A63D6) |
--positive | #16A34A | #34C46A | 录音中 / 成功 |
--warning | #E8890C | #F2A33C | 超额 / 警告 |
--danger | #DC2626 | #F05B5B | 错误 / 权限异常 |
--text-1/2/3 | #1B1E26 / #5A6072 / #A6ABB8 | #ECEEF4 / #9AA1B2 / #5F6678 | 正文 / 次要 / 弱化 |
--overlay-bg | rgba(24,26,34,.95)(dark: rgba(30,33,43,.96))+ blur(12px) | 识别浮层(双模式深色玻璃) | |
--overlay-text/-2/-3 | #F2F4FA / #9AA3BD / #646E8C | 浮层 final / partial / hint | |
--focus-ring | 0 0 0 3px #C4CFFB | 0 0 0 3px rgba(92,119,232,.4) | 仅键盘聚焦显示 |
| 类别 | 关键值 |
|---|---|
| 字体族 | UI 走系统中文字体(PingFang SC / MiSans / HarmonyOS Sans / 雅黑);Outfit 仅用于 "dudu" 字标、数字与价格;mono 用于计时 / 用量数字 |
| 字号 | 12(角标/键帽)· 13(菜单/次要)· 14(桌面正文)· 15(浮层文本/移动正文)· 17(区块标题)· 20(窗口标题)· 24(页面大标题)· 32(价格大数字) |
| 字重 | 400 / 500 / 600(700 仅价格等大数字) |
| 间距 | 4px 网格(4/8/12/16/20/24/32/40/48/64) |
| 圆角 | 4(kbd)· 6(按钮/输入框/键帽)· 10(菜单)· 14(卡片/浮层)· 20(大容器)· 999(麦克风条/胶囊) |
| 控件高度 | 28 / 36(桌面紧凑/默认)· 44 / 56(移动触控/主操作与麦克风条) |
| 阴影 | card(极轻 0 1px 3px)/ menu / window / overlay(0 12px 36px)四档 + key(键帽底边 0 1px 0);dark 模式阴影加深、靠边框提层级 |
| 动效 | ease-out cubic-bezier(.2,.8,.2,1);120ms(hover/按压)· 200ms(浮层进出/开关)· 320ms(屏幕切换);浮层入场淡入 + 上移 4px;尊重 prefers-reduced-motion |
React 组件(桌面端直接用)→ 移动端原生等价规格。文案全部内置于组件,移动端照抄。
| 组件 | 关键 props / 状态 | 原生实现要点(iOS SwiftUI / Android Compose) |
|---|---|---|
| Button | variant: primary/secondary/ghost/danger · size: sm 28 / md 36 / lg 44 · block · disabled(opacity .45) | 圆角 6,字重 500;primary 填充 accent,hover/press 用 accent-hover/press;ghost 文字 accent-text、hover 底 accent-soft |
| Badge | tone: green/blue/orange/red/neutral | 胶囊 999,soft 底 + 功能色文字,12px |
| Kbd / HotkeyCombo | keys={['⌘','⇧','Space']} | 仅桌面端使用 |
| ProgressBar | value/max · warnAt=0.9(超阈值自动转 warning 色) | 试用进度(1/3 分钟);轨道 surface-3、填充 accent,≥90% 变 warning |
| Input / Switch / SettingRow | SettingRow: label + description(12px text-3) + 右侧控件;行 padding 12 0、底边 border-1、末行无边 | 设置页与主 App 列表行复用此范式 |
[0.3,.55,.85,.45,.7,1,.4,.6,.25,.8,.5,.35] × height,最小 4px(i%6)*0.07s;这是全产品唯一循环动画,仅录音中出现| state | 视觉 | 文案(内置) |
|---|---|---|
| idle | accent 填充、白字;按压 accent-press | 按住说话 |
| recording | positive 填充、白字、左侧白色 Waveform(9,18) | 松开完成 · 上滑取消 |
| disabled | surface-3 底、text-3 字、cursor 默认 | 打开 dudu App 登录 |
| quota | warning 填充、白字 | 今日试用已用完,去购买时长 |
尺寸:高 56(--control-xl)、全宽、胶囊 999、15px/600 字重、图标线条 mic 20px;touch-action:none;移动端按压可缩 0.97(桌面无缩放)。
listening(Waveform + mic 图标 + "聆听中…" 15px overlay-text-2)/ text(final 用 overlay-text 亮色,partial 用 overlay-text-2 灰色拼接其后,15px 行高 1.6)role="status" 无障碍播报desktop/
src-tauri/ Rust:快捷键 / 音频采集 / WS / 注入 / 托盘 / 窗口管理
src/ React + Vite + TypeScript
windows/
overlay/ 识别浮层窗口
settings/ 设置窗口
login/ 登录 / 购买窗口
tray/ 自绘托盘菜单窗口
shared/ 事件桥(tauri event ↔ React state)、API client
package.json 依赖 @dudu/design(指向 ../design)
| 窗口 | 视觉规格(来自 ui_kits/desktop) | Tauri 实现要点 |
|---|---|---|
| 识别浮层 | RecognitionOverlay 340px;深色玻璃双模式不变;跟随光标 / 目标窗口下方居中 | 无边框 + 透明 + 置顶 + focusable:false(不抢焦点是功能正确性前提)+ skip-taskbar;常驻隐藏、按键时 show 避免创建开销;blur 用 CSS backdrop-filter |
| 托盘菜单 | 248px 自绘菜单:surface-card 96% 不透明 + blur(20px)、圆角 10、shadow-menu;hover 整行 accent 反白(系统菜单惯例);条目:标题行(logo + "dudu 语音输入" + 已就绪 Badge)/ 启用快捷键✓ / 按住说话 ⌘⇧Space / 设置… / 时长余额 472 分钟 / 账号 wang*** 已登录 / 检查更新 / 退出 dudu | 原生 tray 菜单做不出此样式 → tray 图标点击弹自绘无边框小窗,贴托盘坐标定位,失焦自动关闭;tray 图标用 logo-mark(template image 随系统明暗) |
| 设置窗口 | 440px;四分组(12px 灰色组标题):输入=说话快捷键(HotkeyCombo + 修改)/ 麦克风下拉 / 提示音 Switch;通用=开机自启 Switch / 外观三段选(跟随系统 · 浅色 · 深色);账号与时长=登录状态(wang*** + 余额充足 Badge)/ 时长余额(472 分钟 + 购买时长 secondary 按钮)/ 今日免费试用 ProgressBar 1/3 分钟;帮助(v1.1 新增)=反馈问题行(描述"遇到问题或建议,告诉我们"+ ghost 按钮"去反馈") | SettingRow 控件 28px(--control-sm)紧凑行高;外观选择驱动根节点 data-theme 并持久化;"跟随系统"监听系统主题事件 |
| 反馈窗口 (v1.1 新增) |
420px;标题"反馈问题";多行输入框(placeholder"说说遇到的问题或建议",上限 1000 字,右下角字数提示);图片区=虚线"添加图片"框(Lucide image-plus)+ 64px 缩略图(右上角 × 删除),最多 3 张;SettingRow"附带诊断信息"(描述"版本、系统、最近错误摘要")Switch 默认开;底部主按钮"提交"(block primary);成功态=56px 绿对勾圆 +"已收到,谢谢你" | 图片支持粘贴截图 / 拖拽 / 文件选择;提交走 POST /v1/feedback multipart;详见第八章 |
| 登录 / 购买 | 400px 四步流:① 扫码登录(logo 40 + 二维码 + "打开微信扫一扫,确认后自动登录")② 选时长包(三档卡片:100 分钟 ¥9 / 500 分钟 ¥39 省 13% / 2000 分钟 ¥129 省 28%;选中 1.5px accent 边 + accent-soft 底;底注"时长不过期 · 每天另有 3 分钟免费试用")③ 扫码支付(金额 32px 大数字 + 二维码 + "支付完成后自动开通")④ 完成(56px 绿色对勾圆 + "已到账 xx 分钟" + 余额提示 + 开始使用按钮) | 二维码内容来自后端:登录 /v1/auth/qr、支付订单 code_url;两处均轮询状态自动推进步骤;窗口标题随步骤切换(dudu — 登录 / dudu — 购买时长) |
欢迎页 → 权限引导(macOS 辅助功能 + 麦克风,带系统设置深链)→ 微信扫码登录 → 试一试(示例输入框按住 ⌘⇧Space 说一句话)。Windows 无辅助功能步骤。
idle ──按下快捷键──▶ listening ──首个识别结果──▶ streaming ──松开──▶ idle
│ (浮层立即弹出"聆听中", │ final 注入焦点输入框
│ 不等首个结果,420ms 内无结果保持) │ 浮层淡出
└──松开(未出字)──▶ idle(不注入)
| 事件 | 载荷 / 行为 |
|---|---|
hotkey:down / hotkey:up | 浮层 show + listening 态 / 触发 stop 流程 |
asr:partial | {final, partial} 累积文本,浮层转 text 态 |
asr:final | {text} 句子定稿 |
asr:error | {code, message};QUOTA_EXCEEDED → 浮层显示警告文案"今日试用已用完,去购买时长"(warning 色)并可点击打开购买窗口;SESSION_LIMIT(单次 3 分钟截断)→ 已识别内容正常上屏 + 提示"单次最长 3 分钟,松开后可继续";RATE_LIMITED → "操作过于频繁,稍后再试";网络错误 → "连接中断,松开重试"。移动端键盘对应在 partial 文本条位置展示同文案 |
account:update | {balance_seconds, trial_used_today, trial_daily_limit};托盘 / 设置实时刷新(识别中由 WS usage 帧驱动) |
audio:level | 音量包络(30ms 间隔),驱动浮层波形高度(MVP 可先用固定动画) |
set_hotkey · set_mic_device / list_mic_devices · set_autostart · set_theme · open_url(更新下载页)· logout。设置持久化于本地(tauri-plugin-store),登录态 JWT 存系统钥匙串。
| 区域 | 规格 |
|---|---|
| 工具条 | 左:13px logo + "dudu 语音输入"(12px text-2);右:状态文案(见 6.2) |
| 功能键行 (非录音时) | 五键:切换键盘(flex1.3) / ,/ 。/ 退格(1.3) / 发送(1.3);键帽高 40、圆角 6、surface-card 底 + shadow-key 底边、15px;图标 Lucide(globe/backspace/enter 17px);不做 26 键,语音为主 |
| partial 文本条 (录音中替换功能键行) | min-height 40、padding 9 12、圆角 10、surface-card + border-1;final 亮 text-1 / partial 灰 text-3 拼接;15px 行高 1.5 |
| 大麦克风条 | MicBar 56px 胶囊(四态见 3.3) |
| 取消提示 (仅录音中) | 居中 12px text-3:"↑ 上滑取消"(Lucide arrow-up 11px) |
面板底色 --surface-2 + 顶部 1px border-1;跟随系统 light/dark。
| 账户态 | 工具条右侧 | MicBar |
|---|---|---|
| ok 有余额 | 余额 472 分钟 ✓(text-3,对勾 Lucide check 11px) | idle 可用 |
| trial 试用中 | 试用 · 今日剩 2 分钟(text-3) | idle 可用 |
| guest 未登录 | 未登录(text-3) | disabled:"打开 dudu App 登录",点击 deep link 跳主 App |
| quota 超额 | 今日试用已用完(warning 色) | quota:"今日试用已用完,去购买时长",点击跳主 App 购买页 |
录音中(任意可用态)→ 工具条右侧变 mono 计时 00:04(positive 色) | recording | |
textDocumentProxy.insertText / Android currentInputConnection.commitText 上屏;状态复位| 平台 | 要点 |
|---|---|
| iOS / iPadOS | UIInputViewController + SwiftUI 子视图;需"完全访问"联网;token 经 App Group 共享;键盘内录音 1A spike 已实证不可行(见下),走 deep link 降级路径;iPad 同一 target,size class 自适应(横屏麦克风条加宽)+ 悬浮键盘紧凑布局 |
| Android | InputMethodService + Compose(ComposeView in onCreateInputView);RECORD_AUDIO 权限在主 App 申请;token 经同应用内部存储共享;键盘高度按系统建议值 |
键盘扩展进程内无法录音:即使"允许完全访问"开启,AVAudioSession setActive/start 在扩展进程内直接报 CoreAudio 错误 2003329396 —— iOS 平台硬限制,与权限无关。因此 iOS 语音主路径采用预留的 deep link 降级方案:
extensionContext.open 打开 dudu://dictate(主 App 全屏听写页,深色玻璃 overlay 风格)/v1/asr/stream,partial/final 流式显示(final 亮 / partial 灰)textDocumentProxy.insertText 自动上屏并在工具条短暂显示"已上屏";过期则用户手动粘贴剪贴板兜底| 屏 | 规格与交互 |
|---|---|
| ① 启用引导 | 顶部 logo 44 + "三步开启 dudu"(20px/600)+ 副题"按住说话,松开上屏";三步卡片(添加键盘 / 开启完全访问 / 试一试),三态:done(绿对勾 + positive-soft 图标底)/ active(1.5px accent 边)/ todo(opacity .62);底部主按钮文案随步骤:"去设置" → "去设置" → "我说完了" → "完成",点击带系统设置深链推进;Android 版第二步替换为"启用输入法"(系统输入法设置深链) |
| ② 登录 | 居中 logo 64 + "dudu"(24px Outfit, tracking-wide)+ "大模型语音输入 · 说话即上屏";底部微信绿 #07C160 胶囊按钮"微信一键登录"(56px,唯一例外色),拉起微信 OpenSDK;下方 12px text-3"登录即同意《用户协议》与《隐私政策》" |
| ③ 购买时长 | 余额卡(左"时长余额 472 分钟"24px/700 大数字,右注"时长不过期 / 每天另有 3 分钟免费试用")+ 三档时长包卡片(同桌面规格,选中 accent 边 + soft 底,"省 13%/28%" warning-soft 角标)+ 底部主按钮"微信支付 ¥39"(lg block,金额随选中联动)+ 注"支付后立即到账 · 时长不过期";点击拉起微信 APP 支付,回调成功后余额卡即时刷新 |
| ④ 我的 | 账号卡(头像圈 accent-soft + wang*** + "时长余额 472 分钟 · 不过期" + 余额充足 Badge);今日免费试用卡("1 / 3 分钟" mono + ProgressBar);权限自检列表(麦克风权限 / 键盘已添加 / 完全访问,正常绿对勾,异常红色"去开启 ›"跳系统设置);权限卡下方加"反馈问题 ›"列表行(v1.1 新增,进反馈页,见第八章);低余额时 Badge 转 orange"余额不足"并显示购买入口 |
用户在客户端内直接反馈问题或建议,支持文字 + 图片,可选附带诊断信息。入口:桌面设置窗口"帮助"分组、移动端"我的"页列表行;键盘扩展内不放入口(保持键盘极简)。
| 字段 | 规格 |
|---|---|
| 文字内容 | 多行输入框,placeholder"说说遇到的问题或建议",1–1000 字,右下角实时字数(text-3 12px);为空时提交按钮 disabled |
| 图片 | 最多 3 张,单张 ≤5MB,jpg/png/webp;虚线添加框(border-2 虚线 + Lucide image-plus,hover 转 accent);已选图片 64px 圆角 6 缩略图,右上角 20px × 删除钮;桌面支持粘贴截图 / 拖拽 / 文件选择,移动端从相册选或拍照 |
| 附带诊断信息 | SettingRow + Switch,默认开;描述"版本、系统、最近错误摘要";内容=app_version、os_version、platform、device_id、最近 10 条错误日志摘要(不含识别文本) |
| 提交 | 主按钮"提交"(block primary,一屏唯一蓝主按钮);提交中转 loading 态 disabled;失败 toast"提交失败,请稍后再试"保留已填内容 |
| 成功态 | 替换表单内容:56px 绿对勾圆(positive-soft 底 + positive 对勾,同购买完成步样式)+"已收到,谢谢你"(17px/600)+ ghost 按钮"继续反馈"/ 关闭 |
POST /v1/feedback(multipart/form-data,需登录;未登录时入口处提示先登录)目的:量化响应时间(对照延迟目标)与关键链路质量,驱动体验改进。隐私底线:任何事件不携带识别文本与音频。
| 事件 | 测量 | 目标 |
|---|---|---|
asr.first_partial_ms | 按键(桌面快捷键 / 移动 MicBar 按下)→ 收到首个 partial | P95 < 500ms |
asr.release_to_commit_ms | 松开 → final 注入 / 上屏完成 | P95 < 300ms |
audio.start_ms | 按键 → 音频采集首帧 | P95 < 100ms |
ws.connect_ms / ws.reconnect | WS 建连耗时 / 重连次数 + 原因 | 监控 |
asr.session | audio_seconds、partial_count、是否 cancel、error_code | 质量监控 |
inject.fail(仅桌面) | 注入失败 + 前台应用标识(bundle id / exe 名) | 兼容性改进 |
| 漏斗事件(低频) | onboarding.step、login.success、purchase.success、kb.enabled | 转化分析 |
公共维度(每条自动附带):device_id(客户端首次启动生成的匿名 UUID,与账号无关)、platform、app_version、os_version、client_ts。
POST /v1/metrics/batch(gzip)| 端 | 实现 |
|---|---|
| 桌面 | Rust 侧统一 metrics 模块(计时起点都在 Rust:快捷键、音频、WS),webview 漏斗事件经 invoke 汇入同一队列;队列持久化于本地数据目录 |
| iOS 键盘扩展 | 事件写 App Group 共享队列,由扩展自身(活跃时)或主 App 启动时 flush |
| Android | IME Service 与主 App 共用应用内队列(同进程组),WorkManager 定期 flush |
前端每个动态数据点都有明确来源(接口详见 backend-architecture.html):
| UI 数据点 / 交互 | 接口与字段 |
|---|---|
| 桌面扫码登录二维码 + 自动登录 | POST /v1/auth/qr → qr_url;轮询 GET /v1/auth/qr/:state → JWT |
| 移动端微信一键登录 | 微信 OpenSDK code → POST /v1/auth/wechat → JWT |
| 余额 472 分钟 / 试用 1/3 分钟 / wang*** / Badge 态 | GET /v1/me:balance_seconds、trial_used_today、trial_daily_limit、nickname_masked(分钟数 = 秒数向下取整显示) |
| 时长包三档(价格 / 单价文案 / 省 x% 角标) | GET /v1/packs(服务端可配,前端不写死) |
| 支付二维码 / 拉起收银台 / "支付完成后自动开通" | POST /v1/orders → code_url 或 APP 支付参数;轮询 GET /v1/orders/:id → paid 后跳完成步 |
| 识别全流程(partial/final/计时/余额实时扣减) | WSS /v1/asr/stream:下行 partial / final / usage / error 帧 |
| 键盘 guest/quota 态判定 | 无 token → guest;WS error QUOTA_EXCEEDED 或 /v1/me 余额+试用均为 0 → quota |
| 托盘"检查更新" | GET /v1/app/latest → 版本号 + 下载地址 |
| 反馈提交(文字 + 图片 + 诊断) | POST /v1/feedback(multipart)→ feedback_id;超限错误 FEEDBACK_RATE_LIMITED |
| 打点批量上报 | POST /v1/metrics/batch(gzip JSON 数组,可匿名)→ 204 |
_adherence.oxlintrc.json,桌面端 lint 接入可静态检查 token 使用规范。