Files
dudu/doc/frontend-design.html
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

471 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dudu 前端实现设计 v1.1</title>
<style>
:root {
--accent: #4F6EF7;
--accent-text: #3D58DB;
--accent-soft: #EEF1FE;
--bg: #F6F7FA;
--card: #FFFFFF;
--text-1: #1B1E26;
--text-2: #5A6072;
--text-3: #A6ABB8;
--border-1: #E4E6EB;
--positive: #16A34A;
--warning: #E8890C;
--danger: #DC2626;
--code-bg: #1B1E26;
--code-text: #D6DCEA;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
background: var(--bg); color: var(--text-1); line-height: 1.75;
}
.container { max-width: 980px; margin: 0 auto; padding: 0 24px 80px; }
header.hero {
background: var(--accent); color: #fff; padding: 48px 24px 40px; text-align: center;
}
header.hero h1 { margin: 0 0 6px; font-size: 30px; }
header.hero p { margin: 0; opacity: .85; font-size: 15px; }
.chips { margin-top: 16px; }
.chip { display: inline-block; padding: 3px 14px; margin: 3px; background: rgba(255,255,255,.16); border-radius: 999px; font-size: 13px; }
section.card {
background: var(--card); border: 1px solid var(--border-1); border-radius: 14px;
padding: 28px 32px; margin-top: 24px; box-shadow: 0 1px 3px rgba(17,19,26,.05);
}
h2 { font-size: 21px; margin: 0 0 14px; padding-bottom: 8px; border-bottom: 2px solid var(--accent); display: inline-block; }
h3 { font-size: 16px; margin: 22px 0 8px; color: var(--accent-text); }
h4 { font-size: 14px; margin: 16px 0 6px; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13.5px; }
th, td { border: 1px solid var(--border-1); padding: 7px 11px; text-align: left; vertical-align: top; }
th { background: #F0F1F4; font-weight: 600; }
tr:nth-child(even) td { background: #FCFCFD; }
pre.code, pre.diagram {
background: var(--code-bg); color: var(--code-text); padding: 16px 18px; border-radius: 10px;
overflow-x: auto; font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.55;
}
code.inline { background: var(--accent-soft); color: var(--accent-text); padding: 1px 6px; border-radius: 4px; font-family: "SF Mono", Menlo, monospace; font-size: 12.5px; }
.callout { border-left: 4px solid var(--accent); background: var(--accent-soft); padding: 11px 16px; border-radius: 0 8px 8px 0; margin: 12px 0; font-size: 13.5px; }
.callout.warn { border-color: var(--warning); background: #FCEFD7; }
.swatch { display: inline-block; width: 13px; height: 13px; border-radius: 3px; border: 1px solid rgba(0,0,0,.12); vertical-align: -2px; margin-right: 5px; }
.tag { display: inline-block; font-size: 12px; padding: 1px 10px; border-radius: 999px; margin-right: 6px; font-weight: 600; }
.tag.green { background: #DCFCE7; color: var(--positive); }
.tag.orange { background: #FCEFD7; color: var(--warning); }
.tag.red { background: #FEE2E2; color: var(--danger); }
.tag.blue { background: var(--accent-soft); color: var(--accent-text); }
ul, ol { padding-left: 22px; }
li { margin: 3px 0; }
.toc { columns: 2; font-size: 14px; }
.toc a { color: var(--accent-text); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
.kbd { background: #F0F1F4; border: 1px solid #D2D5DD; border-bottom-width: 2px; border-radius: 4px; padding: 0 7px; font-family: "SF Mono", Menlo, monospace; font-size: 12px; }
footer { text-align: center; color: var(--text-3); font-size: 13px; margin-top: 44px; }
</style>
</head>
<body>
<header class="hero">
<h1>dudu 前端实现设计</h1>
<p>基于 design/ 设计系统的 100% 还原实现方案 · 桌面(Tauri 2)+ 移动(原生键盘扩展与主 App)</p>
<div class="chips">
<span class="chip">v1.1</span>
<span class="chip">2026-06-11</span>
<span class="chip">依据:design/ 设计系统 + doc/plan/design.html v0.2</span>
<span class="chip">v1.1 新增:反馈问题 + 客户端打点</span>
</div>
</header>
<div class="container">
<section class="card">
<h2>目录</h2>
<div class="toc">
<ol>
<li><a href="#strategy">设计系统消费策略</a></li>
<li><a href="#tokens">设计令牌总表</a></li>
<li><a href="#components">组件库与原生映射</a></li>
<li><a href="#desktop">桌面端实现(Tauri 2</a></li>
<li><a href="#desktop-events">桌面端状态机与 Rust↔Web 事件</a></li>
<li><a href="#keyboard">移动端键盘扩展</a></li>
<li><a href="#app">移动端主 App 四屏</a></li>
<li><a href="#feedback">反馈问题(v1.1 新增)</a></li>
<li><a href="#metrics">客户端打点(v1.1 新增)</a></li>
<li><a href="#api-map">UI 状态 ↔ 后端接口对照</a></li>
<li><a href="#checklist">100% 还原核查清单</a></li>
</ol>
</div>
</section>
<!-- ============ 1 ============ -->
<section class="card" id="strategy">
<h2>一、设计系统消费策略</h2>
<p>设计系统位于 <code class="inline">design/</code>tokens / React 组件 / UI kits / 品牌资产),是所有界面的唯一真相源。两类终端用不同方式消费:</p>
<table>
<tr><th style="width:22%"></th><th>消费方式</th><th style="width:28%">保真度保证</th></tr>
<tr>
<td>桌面端<br>Tauri 2 webview</td>
<td><strong>直接复用</strong>React 18 + Vite,把 <code class="inline">design/</code> 作为 monorepo 内共享包(<code class="inline">@dudu/design</code>)引入,链入 <code class="inline">styles.css</code> 全套令牌,直接 import <code class="inline">components/</code> 下的 Button / Switch / SettingRow / MicBar / RecognitionOverlay 等组件</td>
<td>零转译,与 ui_kits 演示像素级一致</td>
</tr>
<tr>
<td>移动端<br>Swift / Kotlin 原生)</td>
<td><strong>令牌导出管线 + 组件规格对照</strong>:构建脚本解析 <code class="inline">design/tokens/*.css</code>,生成 iOS <code class="inline">DuduTheme.swift</code>light/dark 双值 Color/CGFloat)与 Android <code class="inline">DuduTheme.kt</code> + <code class="inline">values/values-night</code> 资源;组件按本文档第三章的规格表原生实现</td>
<td>令牌单一来源自动同步;组件规格逐条对照 JSX 源码(尺寸/圆角/字号/色值/文案均给出精确值)</td>
</tr>
</table>
<div class="callout">
令牌导出脚本放在 <code class="inline">design-pipeline/</code>(Node 或 Go 实现均可),CI 中校验生成产物与 tokens/*.css 一致,防止两端漂移。
</div>
<h3>设计系统硬规则(全端强制)</h3>
<ul>
<li>一屏最多一个蓝色主按钮,其余操作用 secondary / ghost</li>
<li><strong>禁用渐变</strong>,纯色填充;单一品牌蓝,唯一例外色是微信品牌绿 <code class="inline">#07C160</code>(仅登录按钮)</li>
<li>识别浮层永远深色玻璃(light/dark 模式下不变)</li>
<li>图标只用 Lucide 线条(2px 圆头描边、currentColor),<strong>无 emoji、无 unicode 符号图标</strong>;例外:键帽字符 ⌘ ⇧ ⌫ ↵</li>
<li>文案:对用户称"你"、短句、微文案不带句号、提示语用"·"分隔;partial/final 等技术词不出现在 UI</li>
<li>动效三档 120/200/320ms、快入慢出;唯一循环动画是录音中的波形;无弹跳、无旋转、无视差</li>
<li>品牌名永远小写 <strong>dudu</strong>;中文写法"dudu 语音输入"或"嘟嘟语音输入"</li>
</ul>
</section>
<!-- ============ 2 ============ -->
<section class="card" id="tokens">
<h2>二、设计令牌总表</h2>
<p>来源 <code class="inline">design/tokens/</code>,此处摘录实现必需的关键值(完整以源文件为准)。</p>
<h3>2.1 颜色(语义别名)</h3>
<table>
<tr><th>令牌</th><th>Light</th><th>Dark</th><th>用途</th></tr>
<tr><td><code class="inline">--bg-app</code></td><td><span class="swatch" style="background:#F6F7FA"></span>#F6F7FA</td><td><span class="swatch" style="background:#0F1116"></span>#0F1116</td><td>页面底色</td></tr>
<tr><td><code class="inline">--surface-card</code></td><td><span class="swatch" style="background:#FFFFFF"></span>#FFFFFF</td><td><span class="swatch" style="background:#171A22"></span>#171A22</td><td>卡片 / 键帽</td></tr>
<tr><td><code class="inline">--accent</code></td><td><span class="swatch" style="background:#4F6EF7"></span>#4F6EF7</td><td><span class="swatch" style="background:#5C77E8"></span>#5C77E8</td><td>主操作 / 品牌(hover #3D58DB·#6E89F9press #2F44B3·#4A63D6</td></tr>
<tr><td><code class="inline">--positive</code></td><td><span class="swatch" style="background:#16A34A"></span>#16A34A</td><td><span class="swatch" style="background:#34C46A"></span>#34C46A</td><td>录音中 / 成功</td></tr>
<tr><td><code class="inline">--warning</code></td><td><span class="swatch" style="background:#E8890C"></span>#E8890C</td><td><span class="swatch" style="background:#F2A33C"></span>#F2A33C</td><td>超额 / 警告</td></tr>
<tr><td><code class="inline">--danger</code></td><td><span class="swatch" style="background:#DC2626"></span>#DC2626</td><td><span class="swatch" style="background:#F05B5B"></span>#F05B5B</td><td>错误 / 权限异常</td></tr>
<tr><td><code class="inline">--text-1/2/3</code></td><td>#1B1E26 / #5A6072 / #A6ABB8</td><td>#ECEEF4 / #9AA1B2 / #5F6678</td><td>正文 / 次要 / 弱化</td></tr>
<tr><td><code class="inline">--overlay-bg</code></td><td colspan="2">rgba(24,26,34,.95)dark: rgba(30,33,43,.96)+ blur(12px)</td><td>识别浮层(双模式深色玻璃)</td></tr>
<tr><td><code class="inline">--overlay-text/-2/-3</code></td><td colspan="2">#F2F4FA / #9AA3BD / #646E8C</td><td>浮层 final / partial / hint</td></tr>
<tr><td><code class="inline">--focus-ring</code></td><td>0 0 0 3px #C4CFFB</td><td>0 0 0 3px rgba(92,119,232,.4)</td><td>仅键盘聚焦显示</td></tr>
</table>
<h3>2.2 字体 / 间距 / 圆角 / 控件 / 动效</h3>
<table>
<tr><th style="width:18%">类别</th><th>关键值</th></tr>
<tr><td>字体族</td><td>UI 走系统中文字体(PingFang SC / MiSans / HarmonyOS Sans / 雅黑);<strong>Outfit</strong> 仅用于 "dudu" 字标、数字与价格;mono 用于计时 / 用量数字</td></tr>
<tr><td>字号</td><td>12(角标/键帽)· 13(菜单/次要)· 14(桌面正文)· 15(浮层文本/移动正文)· 17(区块标题)· 20(窗口标题)· 24(页面大标题)· 32(价格大数字)</td></tr>
<tr><td>字重</td><td>400 / 500 / 600700 仅价格等大数字)</td></tr>
<tr><td>间距</td><td>4px 网格(4/8/12/16/20/24/32/40/48/64</td></tr>
<tr><td>圆角</td><td>4(kbd)· 6(按钮/输入框/键帽)· 10(菜单)· 14(卡片/浮层)· 20(大容器)· 999(麦克风条/胶囊)</td></tr>
<tr><td>控件高度</td><td>28 / 36(桌面紧凑/默认)· 44 / 56(移动触控/主操作与麦克风条)</td></tr>
<tr><td>阴影</td><td>card(极轻 0 1px 3px/ menu / window / overlay0 12px 36px)四档 + key(键帽底边 0 1px 0);dark 模式阴影加深、靠边框提层级</td></tr>
<tr><td>动效</td><td>ease-out <code class="inline">cubic-bezier(.2,.8,.2,1)</code>120mshover/按压)· 200ms(浮层进出/开关)· 320ms(屏幕切换);浮层入场淡入 + 上移 4px;尊重 <code class="inline">prefers-reduced-motion</code></td></tr>
</table>
</section>
<!-- ============ 3 ============ -->
<section class="card" id="components">
<h2>三、组件库与原生映射</h2>
<p>React 组件(桌面端直接用)→ 移动端原生等价规格。文案全部内置于组件,移动端照抄。</p>
<h3>3.1 核心组件速查</h3>
<table>
<tr><th style="width:20%">组件</th><th>关键 props / 状态</th><th>原生实现要点(iOS SwiftUI / Android Compose</th></tr>
<tr><td>Button</td><td>variant: primary/secondary/ghost/danger · size: sm 28 / md 36 / lg 44 · block · disabled(opacity .45)</td><td>圆角 6,字重 500primary 填充 accenthover/press 用 accent-hover/pressghost 文字 accent-text、hover 底 accent-soft</td></tr>
<tr><td>Badge</td><td>tone: green/blue/orange/red/neutral</td><td>胶囊 999soft 底 + 功能色文字,12px</td></tr>
<tr><td>Kbd / HotkeyCombo</td><td>keys={['⌘','⇧','Space']}</td><td>仅桌面端使用</td></tr>
<tr><td>ProgressBar</td><td>value/max · warnAt=0.9(超阈值自动转 warning 色)</td><td>试用进度(1/3 分钟);轨道 surface-3、填充 accent,≥90% 变 warning</td></tr>
<tr><td>Input / Switch / SettingRow</td><td>SettingRow: label + description(12px text-3) + 右侧控件;行 padding 12 0、底边 border-1、末行无边</td><td>设置页与主 App 列表行复用此范式</td></tr>
</table>
<h3>3.2 Waveform(波形)</h3>
<ul>
<li>条宽 3px、间距 3px、圆角 2px、currentColor;高度模式固定 12 值序列 <code class="inline">[0.3,.55,.85,.45,.7,1,.4,.6,.25,.8,.5,.35]</code> × height,最小 4px</li>
<li>active 时 scaleY 0.35→1 交替动画 0.9s ease-in-out,逐条延迟 <code class="inline">(i%6)*0.07s</code>;这是<strong>全产品唯一循环动画</strong>,仅录音中出现</li>
<li>用法:浮层 bars=12 height=24 color=#7C9BFFMicBar 录音态 bars=9 height=18 白色 95%</li>
</ul>
<h3>3.3 MicBar(麦克风条,移动端核心)</h3>
<table>
<tr><th style="width:14%">state</th><th style="width:26%">视觉</th><th>文案(内置)</th></tr>
<tr><td><span class="tag blue">idle</span></td><td>accent 填充、白字;按压 accent-press</td><td>按住说话</td></tr>
<tr><td><span class="tag green">recording</span></td><td>positive 填充、白字、左侧白色 Waveform(9,18)</td><td>松开完成 · 上滑取消</td></tr>
<tr><td><span class="tag">disabled</span></td><td>surface-3 底、text-3 字、cursor 默认</td><td>打开 dudu App 登录</td></tr>
<tr><td><span class="tag orange">quota</span></td><td>warning 填充、白字</td><td>今日试用已用完,去购买时长</td></tr>
</table>
<p>尺寸:高 56--control-xl)、全宽、胶囊 999、15px/600 字重、图标线条 mic 20px<code class="inline">touch-action:none</code>;移动端按压可缩 0.97(桌面无缩放)。</p>
<h3>3.4 RecognitionOverlay(识别浮层)</h3>
<ul>
<li>宽 340、padding 14 18、圆角 14、--overlay-bg + blur(12px)、--shadow-overlay;入场淡入 + 上移 4px200ms ease-out</li>
<li>两态:<code class="inline">listening</code>Waveform + mic 图标 + "聆听中…" 15px overlay-text-2/ <code class="inline">text</code>final 用 overlay-text 亮色,partial 用 overlay-text-2 灰色拼接其后,15px 行高 1.6)</li>
<li>hint 右对齐 11px overlay-text-3"松开 ⌘⇧Space 完成输入"</li>
<li><code class="inline">role="status"</code> 无障碍播报</li>
</ul>
</section>
<!-- ============ 4 ============ -->
<section class="card" id="desktop">
<h2>四、桌面端实现(Tauri 2</h2>
<h3>4.1 技术栈与工程结构</h3>
<pre class="code">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</pre>
<h3>4.2 窗口清单与规格</h3>
<table>
<tr><th style="width:14%">窗口</th><th style="width:42%">视觉规格(来自 ui_kits/desktop</th><th>Tauri 实现要点</th></tr>
<tr>
<td>识别浮层</td>
<td>RecognitionOverlay 340px;深色玻璃双模式不变;跟随光标 / 目标窗口下方居中</td>
<td>无边框 + 透明 + 置顶 + <strong>focusable:false</strong>(不抢焦点是功能正确性前提)+ skip-taskbar;常驻隐藏、按键时 show 避免创建开销;blur 用 CSS backdrop-filter</td>
</tr>
<tr>
<td>托盘菜单</td>
<td>248px 自绘菜单:surface-card 96% 不透明 + blur(20px)、圆角 10、shadow-menuhover 整行 accent 反白(系统菜单惯例);条目:标题行(logo + "dudu 语音输入" + 已就绪 Badge)/ 启用快捷键✓ / 按住说话 ⌘⇧Space / 设置… / 时长余额 472 分钟 / 账号 wang*** 已登录 / 检查更新 / 退出 dudu</td>
<td>原生 tray 菜单做不出此样式 → tray 图标点击弹自绘无边框小窗,贴托盘坐标定位,失焦自动关闭;tray 图标用 logo-marktemplate image 随系统明暗)</td>
</tr>
<tr>
<td>设置窗口</td>
<td>440px;四分组(12px 灰色组标题):<strong>输入</strong>=说话快捷键(HotkeyCombo + 修改)/ 麦克风下拉 / 提示音 Switch;<strong>通用</strong>=开机自启 Switch / 外观三段选(跟随系统 · 浅色 · 深色);<strong>账号与时长</strong>=登录状态(wang*** + 余额充足 Badge/ 时长余额(472 分钟 + 购买时长 secondary 按钮)/ 今日免费试用 ProgressBar 1/3 分钟;<strong>帮助</strong>(v1.1 新增)=反馈问题行(描述"遇到问题或建议,告诉我们"+ ghost 按钮"去反馈"</td>
<td>SettingRow 控件 28px--control-sm)紧凑行高;外观选择驱动根节点 <code class="inline">data-theme</code> 并持久化;"跟随系统"监听系统主题事件</td>
</tr>
<tr>
<td>反馈窗口<br>v1.1 新增)</td>
<td>420px;标题"反馈问题";多行输入框(placeholder"说说遇到的问题或建议",上限 1000 字,右下角字数提示);图片区=虚线"添加图片"框(Lucide image-plus+ 64px 缩略图(右上角 × 删除),最多 3 张;SettingRow"附带诊断信息"(描述"版本、系统、最近错误摘要")Switch 默认开;底部主按钮"提交"block primary);成功态=56px 绿对勾圆 +"已收到,谢谢你"</td>
<td>图片支持粘贴截图 / 拖拽 / 文件选择;提交走 <code class="inline">POST /v1/feedback</code> multipart;详见第八章</td>
</tr>
<tr>
<td>登录 / 购买</td>
<td>400px 四步流:① 扫码登录(logo 40 + 二维码 + "打开微信扫一扫,确认后自动登录")② 选时长包(三档卡片:100 分钟 ¥9 / 500 分钟 ¥39 省 13% / 2000 分钟 ¥129 省 28%;选中 1.5px accent 边 + accent-soft 底;底注"时长不过期 · 每天另有 3 分钟免费试用")③ 扫码支付(金额 32px 大数字 + 二维码 + "支付完成后自动开通")④ 完成(56px 绿色对勾圆 + "已到账 xx 分钟" + 余额提示 + 开始使用按钮)</td>
<td>二维码内容来自后端:登录 <code class="inline">/v1/auth/qr</code>、支付订单 code_url;两处均轮询状态自动推进步骤;窗口标题随步骤切换(dudu — 登录 / dudu — 购买时长)</td>
</tr>
</table>
<h3>4.3 首次启动流程</h3>
<p>欢迎页 → 权限引导(macOS 辅助功能 + 麦克风,带系统设置深链)→ 微信扫码登录 → 试一试(示例输入框按住 <span class="kbd">⌘⇧Space</span> 说一句话)。Windows 无辅助功能步骤。</p>
</section>
<!-- ============ 5 ============ -->
<section class="card" id="desktop-events">
<h2>五、桌面端状态机与 Rust↔Web 事件</h2>
<h3>5.1 识别状态机(对照 OverlayDemo.jsx</h3>
<pre class="diagram">idle ──按下快捷键──▶ listening ──首个识别结果──▶ streaming ──松开──▶ idle
│ (浮层立即弹出"聆听中" │ final 注入焦点输入框
│ 不等首个结果,420ms 内无结果保持) │ 浮层淡出
└──松开(未出字)──▶ idle(不注入)</pre>
<ul>
<li>partial 只进浮层不注入;final 句子级注入(句读处定稿,浮层中 final 亮色 + partial 灰色拼接)</li>
<li>松开时若有未定稿 partial,与 final 一并注入(committed = final + partial</li>
<li>注入后浮层内容清空,准备下一次会话</li>
</ul>
<h3>5.2 Rust → Web 事件</h3>
<table>
<tr><th style="width:30%">事件</th><th>载荷 / 行为</th></tr>
<tr><td><code class="inline">hotkey:down</code> / <code class="inline">hotkey:up</code></td><td>浮层 show + listening 态 / 触发 stop 流程</td></tr>
<tr><td><code class="inline">asr:partial</code></td><td><code class="inline">{final, partial}</code> 累积文本,浮层转 text 态</td></tr>
<tr><td><code class="inline">asr:final</code></td><td><code class="inline">{text}</code> 句子定稿</td></tr>
<tr><td><code class="inline">asr:error</code></td><td><code class="inline">{code, message}</code>QUOTA_EXCEEDED → 浮层显示警告文案"今日试用已用完,去购买时长"(warning 色)并可点击打开购买窗口;SESSION_LIMIT(单次 3 分钟截断)→ 已识别内容正常上屏 + 提示"单次最长 3 分钟,松开后可继续"RATE_LIMITED → "操作过于频繁,稍后再试";网络错误 → "连接中断,松开重试"。移动端键盘对应在 partial 文本条位置展示同文案</td></tr>
<tr><td><code class="inline">account:update</code></td><td><code class="inline">{balance_seconds, trial_used_today, trial_daily_limit}</code>;托盘 / 设置实时刷新(识别中由 WS usage 帧驱动)</td></tr>
<tr><td><code class="inline">audio:level</code></td><td>音量包络(30ms 间隔),驱动浮层波形高度(MVP 可先用固定动画)</td></tr>
</table>
<h3>5.3 Web → Rust 命令(invoke</h3>
<p><code class="inline">set_hotkey</code> · <code class="inline">set_mic_device</code> / <code class="inline">list_mic_devices</code> · <code class="inline">set_autostart</code> · <code class="inline">set_theme</code> · <code class="inline">open_url</code>(更新下载页)· <code class="inline">logout</code>。设置持久化于本地(tauri-plugin-store),登录态 JWT 存系统钥匙串。</p>
</section>
<!-- ============ 6 ============ -->
<section class="card" id="keyboard">
<h2>六、移动端键盘扩展(iOS / Android</h2>
<h3>6.1 面板结构(自上而下,对照 KeyboardScreen.jsx</h3>
<table>
<tr><th style="width:24%">区域</th><th>规格</th></tr>
<tr><td>工具条</td><td>左:13px logo + "dudu 语音输入"12px text-2);右:状态文案(见 6.2)</td></tr>
<tr><td>功能键行<br>(非录音时)</td><td>五键:切换键盘(flex1.3) / / 。/ 退格(1.3) / 发送(1.3);键帽高 40、圆角 6、surface-card 底 + shadow-key 底边、15px;图标 Lucideglobe/backspace/enter 17px);<strong>不做 26 键</strong>,语音为主</td></tr>
<tr><td>partial 文本条<br>(录音中替换功能键行)</td><td>min-height 40、padding 9 12、圆角 10、surface-card + border-1final 亮 text-1 / partial 灰 text-3 拼接;15px 行高 1.5</td></tr>
<tr><td>大麦克风条</td><td>MicBar 56px 胶囊(四态见 3.3</td></tr>
<tr><td>取消提示<br>(仅录音中)</td><td>居中 12px text-3"↑ 上滑取消"Lucide arrow-up 11px</td></tr>
</table>
<p>面板底色 <code class="inline">--surface-2</code> + 顶部 1px border-1;跟随系统 light/dark。</p>
<h3>6.2 账户四态(工具条右侧文案 × MicBar 状态联动)</h3>
<table>
<tr><th style="width:16%">账户态</th><th style="width:32%">工具条右侧</th><th>MicBar</th></tr>
<tr><td><span class="tag green">ok 有余额</span></td><td>余额 472 分钟 ✓(text-3,对勾 Lucide check 11px</td><td>idle 可用</td></tr>
<tr><td><span class="tag blue">trial 试用中</span></td><td>试用 · 今日剩 2 分钟(text-3)</td><td>idle 可用</td></tr>
<tr><td><span class="tag">guest 未登录</span></td><td>未登录(text-3</td><td>disabled"打开 dudu App 登录",点击 deep link 跳主 App</td></tr>
<tr><td><span class="tag orange">quota 超额</span></td><td>今日试用已用完(warning 色)</td><td>quota:"今日试用已用完,去购买时长",点击跳主 App 购买页</td></tr>
<tr><td colspan="2">录音中(任意可用态)→ 工具条右侧变 mono 计时 <code class="inline">00:04</code>positive 色)</td><td>recording</td></tr>
</table>
<h3>6.3 交互流程</h3>
<ol>
<li><strong>按住</strong>麦克风条 → 进 recording:开始采集 + WS 推流;宿主输入框边框变 1.5px accent(视觉锚定目标)</li>
<li><strong>识别中</strong>partial 文本条流式刷新(final 亮 + partial 灰);<strong>partial 不进宿主输入框</strong></li>
<li><strong>松开</strong> → committedfinal + 未定稿 partial)经 iOS <code class="inline">textDocumentProxy.insertText</code> / Android <code class="inline">currentInputConnection.commitText</code> 上屏;状态复位</li>
<li><strong>上滑取消</strong>:手指上移超过阈值(48pt)松开 → 丢弃本次结果,不上屏</li>
<li>功能键:,。 直接 insertText;退格 deleteBackward;发送 = 宿主 return key</li>
</ol>
<h3>6.4 平台实现要点</h3>
<table>
<tr><th style="width:14%">平台</th><th>要点</th></tr>
<tr><td>iOS / iPadOS</td><td>UIInputViewController + SwiftUI 子视图;需"完全访问"联网;token 经 App Group 共享;键盘内录音 1A spike 已实证<strong>不可行</strong>(见下),走 deep link 降级路径;iPad 同一 targetsize class 自适应(横屏麦克风条加宽)+ 悬浮键盘紧凑布局</td></tr>
<tr><td>Android</td><td>InputMethodService + ComposeComposeView in onCreateInputView);RECORD_AUDIO 权限在主 App 申请;token 经同应用内部存储共享;键盘高度按系统建议值</td></tr>
</table>
<h3>6.5 iOS 录音主路径结论(1A spike 真机实证,14D 落地)</h3>
<p>键盘扩展进程内<strong>无法录音</strong>:即使"允许完全访问"开启,AVAudioSession setActive/start 在扩展进程内直接报 CoreAudio 错误 2003329396 —— iOS 平台硬限制,与权限无关。因此 iOS 语音主路径采用预留的 deep link 降级方案:</p>
<ol>
<li>键盘 MicBar 改为"按一下 · 去 dudu 说话",经 <code class="inline">extensionContext.open</code> 打开 <code class="inline">dudu://dictate</code>(主 App 全屏听写页,深色玻璃 overlay 风格)</li>
<li>主 App 进程录音推流:AVAudioEngine 采集 → AVAudioConverter 重采样 16kHz/16bit/mono → 100ms/3200B 分帧 → WSS <code class="inline">/v1/asr/stream</code>partial/final 流式显示(final 亮 / partial 灰)</li>
<li>松开定稿:committed 文本<strong>复制到剪贴板 + 写 App Group pending_text120s 有效)</strong>双保险,提示"已复制 · 回到原 App 粘贴"</li>
<li>用户手动切回原 App(iOS 无"自动跳回上一 App"的公开 API,键盘扩展也无法携带可跳回的 return 上下文)→ dudu 键盘 viewWillAppear 检出未过期 pending_text<code class="inline">textDocumentProxy.insertText</code> 自动上屏并在工具条短暂显示"已上屏";过期则用户手动粘贴剪贴板兜底</li>
</ol>
</section>
<!-- ============ 7 ============ -->
<section class="card" id="app">
<h2>七、移动端主 App 四屏(对照 AppScreens.jsx</h2>
<table>
<tr><th style="width:16%"></th><th>规格与交互</th></tr>
<tr>
<td>① 启用引导</td>
<td>顶部 logo 44 + "三步开启 dudu"20px/600+ 副题"按住说话,松开上屏";三步卡片(添加键盘 / 开启完全访问 / 试一试),三态:done(绿对勾 + positive-soft 图标底)/ active1.5px accent 边)/ todoopacity .62);底部主按钮文案随步骤:"去设置" → "去设置" → "我说完了" → "完成",点击带系统设置深链推进;Android 版第二步替换为"启用输入法"(系统输入法设置深链)</td>
</tr>
<tr>
<td>② 登录</td>
<td>居中 logo 64 + "dudu"24px Outfit, tracking-wide+ "大模型语音输入 · 说话即上屏";底部微信绿 <code class="inline">#07C160</code> 胶囊按钮"微信一键登录"(56px,唯一例外色),拉起微信 OpenSDK;下方 12px text-3"登录即同意《用户协议》与《隐私政策》"</td>
</tr>
<tr>
<td>③ 购买时长</td>
<td>余额卡(左"时长余额 472 分钟"24px/700 大数字,右注"时长不过期 / 每天另有 3 分钟免费试用")+ 三档时长包卡片(同桌面规格,选中 accent 边 + soft 底,"省 13%/28%" warning-soft 角标)+ 底部主按钮"微信支付 ¥39"lg block,金额随选中联动)+ 注"支付后立即到账 · 时长不过期";点击拉起微信 APP 支付,回调成功后余额卡即时刷新</td>
</tr>
<tr>
<td>④ 我的</td>
<td>账号卡(头像圈 accent-soft + wang*** + "时长余额 472 分钟 · 不过期" + 余额充足 Badge);今日免费试用卡("1 / 3 分钟" mono + ProgressBar);权限自检列表(麦克风权限 / 键盘已添加 / 完全访问,正常绿对勾,异常红色"去开启 ›"跳系统设置);权限卡下方加"反馈问题 ›"列表行(v1.1 新增,进反馈页,见第八章);低余额时 Badge 转 orange"余额不足"并显示购买入口</td>
</tr>
</table>
</section>
<!-- ============ 8 ============ -->
<section class="card" id="feedback">
<h2>八、反馈问题(v1.1 新增)</h2>
<p>用户在客户端内直接反馈问题或建议,支持文字 + 图片,可选附带诊断信息。入口:桌面设置窗口"帮助"分组、移动端"我的"页列表行;键盘扩展内不放入口(保持键盘极简)。</p>
<h3>8.1 表单规格(桌面 / 移动一致)</h3>
<table>
<tr><th style="width:22%">字段</th><th>规格</th></tr>
<tr><td>文字内容</td><td>多行输入框,placeholder"说说遇到的问题或建议",1–1000 字,右下角实时字数(text-3 12px);为空时提交按钮 disabled</td></tr>
<tr><td>图片</td><td>最多 3 张,单张 ≤5MBjpg/png/webp;虚线添加框(border-2 虚线 + Lucide image-plushover 转 accent);已选图片 64px 圆角 6 缩略图,右上角 20px × 删除钮;桌面支持粘贴截图 / 拖拽 / 文件选择,移动端从相册选或拍照</td></tr>
<tr><td>附带诊断信息</td><td>SettingRow + Switch,默认开;描述"版本、系统、最近错误摘要";内容=app_version、os_version、platform、device_id、最近 10 条错误日志摘要(不含识别文本)</td></tr>
<tr><td>提交</td><td>主按钮"提交"block primary,一屏唯一蓝主按钮);提交中转 loading 态 disabled;失败 toast"提交失败,请稍后再试"保留已填内容</td></tr>
<tr><td>成功态</td><td>替换表单内容:56px 绿对勾圆(positive-soft 底 + positive 对勾,同购买完成步样式)+"已收到,谢谢你"17px/600+ ghost 按钮"继续反馈"/ 关闭</td></tr>
</table>
<h3>8.2 交互与实现</h3>
<ul>
<li>提交走 <code class="inline">POST /v1/feedback</code>multipart/form-data,需登录;未登录时入口处提示先登录)</li>
<li>图片在客户端先压缩到长边 ≤2000px 再上传(缩短上传时间);上传进度复用 ProgressBar</li>
<li>桌面端反馈窗口独立于设置窗口(420px),关闭不影响设置;移动端为普通推入页面</li>
<li>限频由服务端控制(每用户 10 条/天),超限时 toast 提示"今天反馈次数已达上限"</li>
</ul>
</section>
<!-- ============ 9 ============ -->
<section class="card" id="metrics">
<h2>九、客户端打点(v1.1 新增)</h2>
<p>目的:量化响应时间(对照延迟目标)与关键链路质量,驱动体验改进。<strong>隐私底线:任何事件不携带识别文本与音频。</strong></p>
<h3>9.1 指标清单</h3>
<table>
<tr><th style="width:30%">事件</th><th>测量</th><th style="width:18%">目标</th></tr>
<tr><td><code class="inline">asr.first_partial_ms</code></td><td>按键(桌面快捷键 / 移动 MicBar 按下)→ 收到首个 partial</td><td>P95 &lt; 500ms</td></tr>
<tr><td><code class="inline">asr.release_to_commit_ms</code></td><td>松开 → final 注入 / 上屏完成</td><td>P95 &lt; 300ms</td></tr>
<tr><td><code class="inline">audio.start_ms</code></td><td>按键 → 音频采集首帧</td><td>P95 &lt; 100ms</td></tr>
<tr><td><code class="inline">ws.connect_ms</code> / <code class="inline">ws.reconnect</code></td><td>WS 建连耗时 / 重连次数 + 原因</td><td>监控</td></tr>
<tr><td><code class="inline">asr.session</code></td><td>audio_seconds、partial_count、是否 cancel、error_code</td><td>质量监控</td></tr>
<tr><td><code class="inline">inject.fail</code>(仅桌面)</td><td>注入失败 + 前台应用标识(bundle id / exe 名)</td><td>兼容性改进</td></tr>
<tr><td>漏斗事件(低频)</td><td><code class="inline">onboarding.step</code><code class="inline">login.success</code><code class="inline">purchase.success</code><code class="inline">kb.enabled</code></td><td>转化分析</td></tr>
</table>
<p>公共维度(每条自动附带):<code class="inline">device_id</code>(客户端首次启动生成的匿名 UUID,与账号无关)、platform、app_version、os_version、client_ts。</p>
<h3>9.2 上报机制</h3>
<ul>
<li>本地 JSONL 队列:满 <strong>20 条</strong><strong>60s</strong> 或冷启动时批量 <code class="inline">POST /v1/metrics/batch</code>gzip</li>
<li>失败指数退避重试;本地队列上限 1000 条,超出丢最旧;上报失败不影响任何功能</li>
<li>接口允许匿名(登录前的 onboarding 漏斗也要收);登录后自动带上 user_id</li>
<li>性能事件 MVP 全量上报(量小),后续可服务端下发采样率</li>
</ul>
<h3>9.3 各端实现位置</h3>
<table>
<tr><th style="width:18%"></th><th>实现</th></tr>
<tr><td>桌面</td><td>Rust 侧统一 metrics 模块(计时起点都在 Rust:快捷键、音频、WS),webview 漏斗事件经 invoke 汇入同一队列;队列持久化于本地数据目录</td></tr>
<tr><td>iOS 键盘扩展</td><td>事件写 App Group 共享队列,由扩展自身(活跃时)或主 App 启动时 flush</td></tr>
<tr><td>Android</td><td>IME Service 与主 App 共用应用内队列(同进程组),WorkManager 定期 flush</td></tr>
</table>
</section>
<!-- ============ 10 ============ -->
<section class="card" id="api-map">
<h2>十、UI 状态 ↔ 后端接口对照</h2>
<p>前端每个动态数据点都有明确来源(接口详见 <a href="backend-architecture.html">backend-architecture.html</a>):</p>
<table>
<tr><th style="width:38%">UI 数据点 / 交互</th><th>接口与字段</th></tr>
<tr><td>桌面扫码登录二维码 + 自动登录</td><td><code class="inline">POST /v1/auth/qr</code> → qr_url;轮询 <code class="inline">GET /v1/auth/qr/:state</code> → JWT</td></tr>
<tr><td>移动端微信一键登录</td><td>微信 OpenSDK code → <code class="inline">POST /v1/auth/wechat</code> → JWT</td></tr>
<tr><td>余额 472 分钟 / 试用 1/3 分钟 / wang*** / Badge 态</td><td><code class="inline">GET /v1/me</code>balance_seconds、trial_used_today、trial_daily_limit、nickname_masked(分钟数 = 秒数向下取整显示)</td></tr>
<tr><td>时长包三档(价格 / 单价文案 / 省 x% 角标)</td><td><code class="inline">GET /v1/packs</code>(服务端可配,前端不写死)</td></tr>
<tr><td>支付二维码 / 拉起收银台 / "支付完成后自动开通"</td><td><code class="inline">POST /v1/orders</code> → code_url 或 APP 支付参数;轮询 <code class="inline">GET /v1/orders/:id</code> → paid 后跳完成步</td></tr>
<tr><td>识别全流程(partial/final/计时/余额实时扣减)</td><td><code class="inline">WSS /v1/asr/stream</code>:下行 partial / final / usage / error 帧</td></tr>
<tr><td>键盘 guest/quota 态判定</td><td>无 token → guestWS error QUOTA_EXCEEDED 或 /v1/me 余额+试用均为 0 → quota</td></tr>
<tr><td>托盘"检查更新"</td><td><code class="inline">GET /v1/app/latest</code> → 版本号 + 下载地址</td></tr>
<tr><td>反馈提交(文字 + 图片 + 诊断)</td><td><code class="inline">POST /v1/feedback</code>multipart)→ feedback_id;超限错误 FEEDBACK_RATE_LIMITED</td></tr>
<tr><td>打点批量上报</td><td><code class="inline">POST /v1/metrics/batch</code>(gzip JSON 数组,可匿名)→ 204</td></tr>
</table>
</section>
<!-- ============ 11 ============ -->
<section class="card" id="checklist">
<h2>十一、100% 还原核查清单(验收标准)</h2>
<ol>
<li>所有颜色 / 字号 / 间距 / 圆角 / 阴影 / 动效值<strong>只来自令牌</strong>,无硬编码(移动端来自生成的 Theme 文件)</li>
<li>一屏一个蓝色主按钮;secondary/ghost 用于其余操作</li>
<li>全局无渐变;唯一例外色为微信绿 #07C160(仅登录按钮)</li>
<li>识别浮层在 light/dark 下均为深色玻璃(--overlay-bg + blur12)且 focusable:false</li>
<li>图标全部 Lucide 2px 线条 currentColorUI 无 emoji;键帽字符 ⌘⇧⌫↵ 仅在 Kbd/HotkeyCombo</li>
<li>文案抽样核查:用"你"、微文案无句号、"·"分隔提示、半角数字 + 空格(472 / 600 分钟、00:04)、无 partial/final 字样</li>
<li>MicBar 四态视觉与文案逐字对照 MicBar.jsx;浮层两态对照 RecognitionOverlay.jsx</li>
<li>波形是唯一循环动画且仅录音中出现;prefers-reduced-motion 下停止</li>
<li>焦点环仅键盘聚焦显示(:focus-visible</li>
<li>light/dark 切换全界面无遗漏(含键盘面板、设置、主 App);桌面"跟随系统"实时响应</li>
<li>对照 ui_kits 演示页(design/ui_kits/desktop/index.html、mobile/index.html)逐屏截图比对</li>
</ol>
<div class="callout">
自动化辅助:design 包内附 <code class="inline">_adherence.oxlintrc.json</code>,桌面端 lint 接入可静态检查 token 使用规范。
</div>
</section>
<footer>dudu 前端实现设计 v1.1 · 2026-06-11 · 配套文档:<a href="backend-architecture.html">后端与数据架构</a> · <a href="plan/design.html">MVP 总体方案 v0.2</a></footer>
</div>
</body>
</html>