40760aa884
- 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>
471 lines
38 KiB
HTML
471 lines
38 KiB
HTML
<!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·#6E89F9,press #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 / 600(700 仅价格等大数字)</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 / overlay(0 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>;120ms(hover/按压)· 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,字重 500;primary 填充 accent,hover/press 用 accent-hover/press;ghost 文字 accent-text、hover 底 accent-soft</td></tr>
|
||
<tr><td>Badge</td><td>tone: green/blue/orange/red/neutral</td><td>胶囊 999,soft 底 + 功能色文字,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=#7C9BFF;MicBar 录音态 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;入场淡入 + 上移 4px(200ms 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-menu;hover 整行 accent 反白(系统菜单惯例);条目:标题行(logo + "dudu 语音输入" + 已就绪 Badge)/ 启用快捷键✓ / 按住说话 ⌘⇧Space / 设置… / 时长余额 472 分钟 / 账号 wang*** 已登录 / 检查更新 / 退出 dudu</td>
|
||
<td>原生 tray 菜单做不出此样式 → tray 图标点击弹自绘无边框小窗,贴托盘坐标定位,失焦自动关闭;tray 图标用 logo-mark(template 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;图标 Lucide(globe/backspace/enter 17px);<strong>不做 26 键</strong>,语音为主</td></tr>
|
||
<tr><td>partial 文本条<br>(录音中替换功能键行)</td><td>min-height 40、padding 9 12、圆角 10、surface-card + border-1;final 亮 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> → committed(final + 未定稿 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 同一 target,size class 自适应(横屏麦克风条加宽)+ 悬浮键盘紧凑布局</td></tr>
|
||
<tr><td>Android</td><td>InputMethodService + Compose(ComposeView 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_text(120s 有效)</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 图标底)/ active(1.5px accent 边)/ todo(opacity .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 张,单张 ≤5MB,jpg/png/webp;虚线添加框(border-2 虚线 + Lucide image-plus,hover 转 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 < 500ms</td></tr>
|
||
<tr><td><code class="inline">asr.release_to_commit_ms</code></td><td>松开 → final 注入 / 上屏完成</td><td>P95 < 300ms</td></tr>
|
||
<tr><td><code class="inline">audio.start_ms</code></td><td>按键 → 音频采集首帧</td><td>P95 < 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 → guest;WS 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 线条 currentColor;UI 无 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>
|