Files
dudu/web/index.html
T
wangjia 208d0cda12 feat(design): design/ 真相源 + 防漂移静态闸(#25)
真相源整固:
- design/index.html 登记簿:token 色板(var() 实时渲染)/ 组件登记卡 /
  图标与跨端映射 / guidelines / ui_kits 单一入口,check-ds 强制登记
- design/icons.js 图标单源(Lucide 线条)+ icon-map.json 跨端映射表
  (web/iOS/Android 三列,iOS 12 个 SF Symbol 全部收编登记)
- components/core/Icon.jsx 单源图标渲染器;MicBar/RecognitionOverlay 接入
- tokens 新增 --brand-wechat(-press)(微信官方绿)与 --overlay-wave
- fonts.css 弃 Google Fonts CDN,Outfit 可变字体自托管
  (assets/outfit-latin.woff2,40KB;中国网络下 CDN 不可用会卡字体)
- design/serve.mjs 零依赖预览服务器(登记簿与原型评审入口)

codegen 强化(export-tokens.mjs):
- 产物直写消费位置:ios/dudu/Shared/DuduTheme.swift、
  android/.../design/DuduTheme.kt——删除 design-pipeline/generated/
  中间拷贝层(此前 app 内是手工拷贝,--check 守不到真实消费文件已漂移)
- 新增 web/tokens.css 产物(custom props 透传 + Outfit data URI 内嵌)
- tokens 文件遍历排序,产物确定性

存量收编:
- desktop:三个窗口内联 SVG → Icon 组件;tray 裸 rgba → token+opacity
- Android:LoginScreen 微信绿常量 → DuduPalette.brandWechat(Press),
  Color.White → textOnAccent
- 官网 web/index.html:接入 tokens.css,34 处硬编码 hex 全部 var() 化;
  深色板块(metrics/cta/footer)改挂 data-theme="dark" 复用 dark 令牌;
  移除 Google Fonts CDN
- design 包内 8 处裸色:微信绿 token 化、装饰色 ds-ignore 白名单、
  同值色改 var() 引用

防漂移闸(零依赖 Node):
- design-pipeline/check-ds.mjs:真相源自检 7 道(dark⊆light / var 链 /
  裸色∈tokens 值集 / 组件登记 / icons 唯一 / icon-map 双向同集 / 禁 emoji)
- design-pipeline/check-code.mjs:代码侧单源(desktop 禁字面色+内联 path /
  iOS 禁字面色+SF Symbol 同集 / Android 禁 Color(0x / web 禁字面色),
  ds-ignore 行级豁免制
- .githooks/pre-commit(启用:git config core.hooksPath .githooks)
- CI design job 扩展为三道闸全绿
- 反向验证通过:注入裸色/孤儿 token/未登记组件/未登记 symbol 均被拦截

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:05:22 +08:00

594 lines
28 KiB
HTML
Raw 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 — 按住说话,文字上屏</title>
<meta name="description" content="dudu 语音输入法:大模型识别,流式实时上屏。Mac / Windows / iPhone / iPad / Android。每天免费 3 分钟。">
<link rel="stylesheet" href="tokens.css">
<style>
/* ────────────────────────────────────────────────────────────────
令牌层由 tokens.css 提供(design/tokens/ 经 export-tokens.mjs 生成,
含 Outfit 字体内嵌,禁外部 CDN)。本页只允许 var(--*),禁字面色值
check-code 闸拦截)。深色板块用 data-theme="dark" 复用 dark 令牌。
品牌硬规则:单一品牌蓝 · 无渐变 · 无 emoji · 波形动画仅录音中
──────────────────────────────────────────────────────────────── */
:root {
/* 页面级别名 → 设计系统 token(仅别名,禁止写字面色值) */
--bg: var(--bg-app);
--card: var(--surface-card);
--sans: var(--font-sans);
--latin: var(--font-sans);
--mono: var(--font-mono);
}
* { box-sizing: border-box; margin: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text-1);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent-soft); color: var(--accent-text); }
a { color: inherit; text-decoration: none; }
.latin { font-family: var(--latin); }
/* ── 结构网格:1px 边线是主要分隔语言 ── */
.frame { max-width: 1120px; margin: 0 auto; padding: 0 32px; }
@media (max-width: 720px) { .frame { padding: 0 20px; } }
/* ── 导航 ── */
nav {
position: sticky; top: 0; z-index: 50;
background: color-mix(in srgb, var(--bg) 88%, transparent);
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border-1);
}
.nav-inner {
display: flex; align-items: center; justify-content: space-between;
height: 60px;
}
.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--text-1); }
.brand svg { color: var(--accent); }
.brand .name { font-family: var(--latin); font-weight: 600; font-size: 19px; letter-spacing: .02em; }
.nav-links { display: flex; align-items: center; gap: 28px; font-size: 14px; color: var(--text-2); }
.nav-links a:hover { color: var(--text-1); }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
border: 1px solid transparent; border-radius: 6px; cursor: pointer;
font-family: var(--sans); font-weight: 500; white-space: nowrap;
transition: background 120ms var(--ease-out), border-color 120ms var(--ease-out);
}
.btn-sm { height: 32px; padding: 0 14px; font-size: 13px; }
.btn-lg { height: 48px; padding: 0 26px; font-size: 16px; border-radius: 999px; }
.btn-primary { background: var(--accent); color: var(--text-on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { background: var(--accent-press); }
.btn-secondary { background: var(--card); border-color: var(--border-2); color: var(--text-1); }
.btn-secondary:hover { background: var(--surface-2); }
.btn-ghost { background: transparent; color: var(--accent-text); }
.btn-ghost:hover { background: var(--accent-soft); }
/* ── Hero ── */
.hero { border-bottom: 1px solid var(--border-1); overflow: hidden; }
.hero-grid {
display: grid; grid-template-columns: 6fr 5fr; gap: 48px;
padding: 96px 0 110px; align-items: center;
}
@media (max-width: 920px) {
.hero-grid { grid-template-columns: 1fr; padding: 64px 0 72px; gap: 56px; }
}
.hero h1 {
font-size: clamp(48px, 7.2vw, 84px);
line-height: 1.12; letter-spacing: -0.015em;
font-weight: 700;
}
.hero h1 .accent-word { color: var(--accent); }
.hero .sub {
margin-top: 22px; font-size: 18px; color: var(--text-2);
display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
}
.hero .sub .dot { color: var(--text-3); }
.hero-cta { margin-top: 38px; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.hero-note { font-size: 13px; color: var(--text-3); }
.hero-note .num { font-family: var(--latin); }
/* ── 演示剧场 ── */
.stage { position: relative; }
.demo-window {
background: var(--card); border: 1px solid var(--border-1);
border-radius: 14px; box-shadow: var(--shadow-window); overflow: hidden;
}
.demo-titlebar {
display: flex; align-items: center; gap: 7px;
padding: 11px 14px; border-bottom: 1px solid var(--border-1); background: var(--surface-2);
}
.demo-titlebar i { width: 11px; height: 11px; border-radius: 50%; background: var(--border-2); }
.demo-titlebar span { margin-left: 8px; font-size: 12px; color: var(--text-3); }
.demo-doc { padding: 22px 24px 30px; min-height: 150px; font-size: 16px; line-height: 1.9; }
.demo-doc .placeholder { color: var(--text-3); }
.caret {
display: inline-block; width: 2px; height: 1.15em; vertical-align: -0.18em;
background: var(--accent); animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.overlay {
position: absolute; left: 50%; transform: translateX(-50%);
bottom: -34px; width: min(340px, 86%);
background: var(--overlay-bg); color: var(--overlay-text);
border-radius: 14px; padding: 14px 18px;
box-shadow: var(--shadow-overlay);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
opacity: 0; translate: 0 4px;
transition: opacity 200ms var(--ease-out), translate 200ms var(--ease-out);
pointer-events: none;
}
.overlay.show { opacity: 1; translate: 0 0; }
.wave { display: inline-flex; align-items: center; gap: 3px; height: 24px; color: var(--overlay-wave); }
.wave i { display: block; width: 3px; border-radius: 2px; background: currentColor; }
.recording .wave i { animation: wavebounce 0.9s cubic-bezier(0.45, 0, 0.2, 1) infinite alternate; }
@keyframes wavebounce { from { transform: scaleY(0.35); } to { transform: scaleY(1); } }
.overlay .ovl-status {
display: flex; align-items: center; gap: 8px;
color: var(--overlay-text-2); font-size: 15px; margin-top: 8px;
}
.overlay .ovl-text { font-size: 15px; line-height: 1.6; margin-top: 8px; min-height: 24px; }
.overlay .ovl-text .partial { color: var(--overlay-text-2); }
.overlay .ovl-hint { font-size: 11px; color: var(--overlay-text-3); margin-top: 8px; text-align: right; }
.micbar {
margin-top: 64px; width: 100%; height: 56px; border: none; cursor: pointer;
border-radius: 999px; background: var(--accent); color: var(--text-on-accent);
font-family: var(--sans); font-size: 16px; font-weight: 600;
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
user-select: none; -webkit-user-select: none; touch-action: none;
transition: background 120ms var(--ease-out);
}
.micbar:active, .micbar.recording { background: var(--positive); }
.micbar:hover:not(.recording) { background: var(--accent-hover); }
.stage-tip { margin-top: 12px; text-align: center; font-size: 13px; color: var(--text-3); }
kbd {
font-family: var(--mono); font-size: 12px; color: var(--text-2);
background: var(--surface-2); border: 1px solid var(--border-2); border-bottom-width: 2px;
border-radius: 4px; padding: 1px 7px;
}
/* ── 指标带(墨色) ── */
.metrics { background: var(--gray-950); color: var(--text-1); border-bottom: 1px solid var(--border-1); }
.metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.metric {
padding: 44px 32px; text-align: center;
border-left: 1px solid var(--border-1);
}
.metric:first-child { border-left: none; }
.metric .num {
font-family: var(--latin); font-weight: 700;
font-size: clamp(34px, 4.4vw, 52px); line-height: 1.1;
}
.metric .num small { font-size: 0.45em; font-weight: 600; color: var(--text-2); margin-left: 2px; }
.metric .label { margin-top: 8px; font-size: 14px; color: var(--text-2); }
@media (max-width: 720px) {
.metrics-grid { grid-template-columns: 1fr; }
.metric { border-left: none; border-top: 1px solid var(--border-1); padding: 28px 20px; }
.metric:first-child { border-top: none; }
}
/* ── 章节通用 ── */
section { border-bottom: 1px solid var(--border-1); }
.section-head { padding: 88px 0 0; }
.kicker {
font-size: 13px; font-weight: 600; color: var(--accent-text);
letter-spacing: .12em;
}
.section-head h2 {
margin-top: 12px; font-size: clamp(30px, 4vw, 44px);
line-height: 1.25; letter-spacing: -0.01em; font-weight: 700; max-width: 18em;
}
.reveal { opacity: 0; translate: 0 14px; transition: opacity 320ms var(--ease-out), translate 320ms var(--ease-out); }
.reveal.in { opacity: 1; translate: 0 0; }
/* ── 特性 ── */
.features {
display: grid; grid-template-columns: repeat(3, 1fr);
margin: 56px 0 96px; border: 1px solid var(--border-1); border-radius: 14px;
background: var(--card); overflow: hidden;
}
.feature { padding: 36px 32px 40px; border-left: 1px solid var(--border-1); }
.feature:first-child { border-left: none; }
.feature .icon {
width: 44px; height: 44px; border-radius: 10px;
background: var(--accent-soft); color: var(--accent-text);
display: inline-flex; align-items: center; justify-content: center;
}
.feature h3 { margin-top: 18px; font-size: 19px; font-weight: 600; }
.feature p { margin-top: 8px; font-size: 14.5px; color: var(--text-2); }
@media (max-width: 820px) {
.features { grid-template-columns: 1fr; }
.feature { border-left: none; border-top: 1px solid var(--border-1); }
.feature:first-child { border-top: none; }
}
/* ── 三步 ── */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; margin: 64px 0 96px; }
.step .no {
font-family: var(--latin); font-weight: 700; font-size: 72px; line-height: 1;
color: var(--surface-3);
}
.step h3 { margin-top: 14px; font-size: 19px; font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.step p { margin-top: 6px; font-size: 14.5px; color: var(--text-2); max-width: 26em; }
@media (max-width: 820px) { .steps { grid-template-columns: 1fr; gap: 40px; } }
/* ── 平台 ── */
.platforms { display: flex; flex-wrap: wrap; gap: 12px; margin: 48px 0 96px; }
.platform {
display: inline-flex; align-items: center; gap: 9px;
padding: 12px 22px; border: 1px solid var(--border-1); border-radius: 999px;
background: var(--card); font-size: 15px; font-weight: 500; box-shadow: var(--shadow-card);
}
.platform svg { color: var(--text-2); }
.platform .soon { font-size: 12px; color: var(--text-3); }
/* ── 定价 ── */
.pricing { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 56px 0 28px; align-items: stretch; }
.price-card {
background: var(--card); border: 1px solid var(--border-1); border-radius: 14px;
padding: 30px 28px 28px; display: flex; flex-direction: column; box-shadow: var(--shadow-card);
position: relative;
}
.price-card.hot { border: 1.5px solid var(--accent); }
.price-card .tag {
position: absolute; top: -11px; left: 24px;
font-size: 12px; font-weight: 600; padding: 2px 12px; border-radius: 999px;
background: var(--warning-soft); color: var(--warning);
}
.price-card .minutes { font-size: 17px; font-weight: 600; }
.price-card .minutes .num { font-family: var(--latin); font-size: 22px; }
.price-card .price { margin-top: 14px; font-family: var(--latin); font-weight: 700; font-size: 56px; line-height: 1; }
.price-card .price .cur { font-size: 24px; font-weight: 600; vertical-align: 14px; margin-right: 2px; }
.price-card .unit { margin-top: 8px; font-size: 13px; color: var(--text-3); }
.price-card .btn { margin-top: 26px; width: 100%; }
.pricing-note { text-align: center; font-size: 14px; color: var(--text-2); padding-bottom: 96px; }
.pricing-note .num { font-family: var(--latin); }
@media (max-width: 820px) { .pricing { grid-template-columns: 1fr; } }
/* ── 尾部 CTA(墨色) ── */
.cta { background: var(--gray-950); color: var(--text-1); border-bottom: 1px solid var(--border-1); }
.cta-inner { padding: 110px 0; text-align: center; }
.cta .logo-big { color: var(--accent); }
.cta h2 { margin-top: 26px; font-size: clamp(34px, 5vw, 56px); font-weight: 700; letter-spacing: -0.01em; }
.cta p { margin-top: 14px; color: var(--text-2); font-size: 16px; }
.cta .btn { margin-top: 36px; }
/* ── 页脚 ── */
footer { background: var(--gray-950); color: var(--text-2); }
.footer-inner {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;
padding: 28px 0; font-size: 13px;
}
.footer-inner .brand { color: var(--text-1); }
.footer-inner .brand svg { color: var(--accent); }
.footer-links { display: flex; gap: 22px; }
.footer-links a:hover { color: var(--text-1); }
</style>
</head>
<body>
<!-- ════════ 导航 ════════ -->
<nav>
<div class="frame nav-inner">
<a class="brand" href="#">
<svg width="26" height="26" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"/><path d="M17 20.5v7"/><path d="M24 16.5v15"/><path d="M31 20.5v7"/></svg>
<span class="name">dudu</span>
</a>
<div class="nav-links">
<a href="#features">特性</a>
<a href="#how">怎么用</a>
<a href="#pricing">价格</a>
<a class="btn btn-sm btn-secondary" href="#download">下载</a>
</div>
</div>
</nav>
<!-- ════════ Hero ════════ -->
<header class="hero">
<div class="frame hero-grid">
<div>
<h1>按住说话<br><span class="accent-word">文字上屏</span></h1>
<div class="sub">
<span>大模型识别</span><span class="dot">·</span>
<span>流式实时</span><span class="dot">·</span>
<span>全平台输入法</span>
</div>
<div class="hero-cta">
<a class="btn btn-lg btn-primary" href="#download">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
免费下载
</a>
<span class="hero-note">每天免费 <span class="num">3</span> 分钟 · 微信登录即用</span>
</div>
</div>
<!-- 演示剧场:按住下方按钮(或长按空格)真实体验流式上屏 -->
<div class="stage" id="stage">
<div class="demo-window">
<div class="demo-titlebar"><i></i><i></i><i></i><span>备忘录 — 周报草稿</span></div>
<div class="demo-doc">
<span id="doc-text"></span><span class="placeholder" id="doc-placeholder">光标在这里 — 在任意应用按住快捷键即可语音输入</span><span class="caret"></span>
</div>
<div class="overlay" id="overlay">
<span class="wave" id="wave"></span>
<div class="ovl-status" id="ovl-status">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
<span>聆听中…</span>
</div>
<div class="ovl-text" id="ovl-text" hidden></div>
<div class="ovl-hint">松开 ⌘⇧Space 完成输入</div>
</div>
</div>
<button class="micbar" id="micbar" type="button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
<span id="micbar-label">按住试一试</span>
</button>
<div class="stage-tip">就像在你电脑上按住 <kbd></kbd><kbd></kbd><kbd>Space</kbd> 一样</div>
</div>
</div>
</header>
<!-- ════════ 指标带 ════════ -->
<div class="metrics" data-theme="dark">
<div class="frame metrics-grid">
<div class="metric reveal">
<div class="num">&lt;500<small>ms</small></div>
<div class="label">按下到首字出现</div>
</div>
<div class="metric reveal" style="transition-delay:60ms">
<div class="num">5<small></small></div>
<div class="label">Mac · Windows · iPhone · iPad · Android</div>
</div>
<div class="metric reveal" style="transition-delay:120ms">
<div class="num">3<small>分钟/天</small></div>
<div class="label">免费试用 · 不需要先付费</div>
</div>
</div>
</div>
<!-- ════════ 特性 ════════ -->
<section id="features">
<div class="frame">
<div class="section-head reveal">
<div class="kicker">为什么是 dudu</div>
<h2>识别得准,上屏得快<br>用起来像没有这层软件</h2>
</div>
<div class="features reveal">
<div class="feature">
<span class="icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10v3"/><path d="M6 6v11"/><path d="M10 3v18"/><path d="M14 8v7"/><path d="M18 5v13"/><path d="M22 10v3"/></svg></span>
<h3>大模型识别</h3>
<p>云端大模型听写,中英混说、专有名词、自动标点都不在话下,说完即定稿</p>
</div>
<div class="feature">
<span class="icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg></span>
<h3>流式实时上屏</h3>
<p>说话的同时文字就出现,不等你说完;松开按键,整句注入焦点输入框</p>
</div>
<div class="feature">
<span class="icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg></span>
<h3>哪里都能用</h3>
<p>桌面端快捷键全局可用,手机上是真正的系统键盘——微信、文档、代码编辑器,焦点在哪写到哪</p>
</div>
</div>
</div>
</section>
<!-- ════════ 怎么用 ════════ -->
<section id="how">
<div class="frame">
<div class="section-head reveal">
<div class="kicker">怎么用</div>
<h2>三个动作,零学习成本</h2>
</div>
<div class="steps">
<div class="step reveal">
<div class="no">01</div>
<h3>按住 <kbd></kbd><kbd></kbd><kbd>Space</kbd></h3>
<p>桌面端任意输入框旁按住快捷键;手机上按住键盘的大麦克风</p>
</div>
<div class="step reveal" style="transition-delay:60ms">
<div class="no">02</div>
<h3>说话</h3>
<p>识别结果实时浮现,说错了上滑取消,不打断你的思路</p>
</div>
<div class="step reveal" style="transition-delay:120ms">
<div class="no">03</div>
<h3>松开,上屏</h3>
<p>整句文字直接写进光标处,标点已排好,接着说下一句就行</p>
</div>
</div>
</div>
</section>
<!-- ════════ 平台 ════════ -->
<section id="download">
<div class="frame">
<div class="section-head reveal">
<div class="kicker">全平台</div>
<h2>你的每一台设备</h2>
</div>
<div class="platforms reveal">
<span class="platform"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>Mac</span>
<span class="platform"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>Windows</span>
<span class="platform"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/></svg>iPhone<span class="soon">键盘扩展</span></span>
<span class="platform"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="22" x="3" y="1" rx="2"/><path d="M12 19h.01"/></svg>iPad</span>
<span class="platform"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/></svg>Android<span class="soon">系统输入法</span></span>
</div>
</div>
</section>
<!-- ════════ 价格 ════════ -->
<section id="pricing">
<div class="frame">
<div class="section-head reveal">
<div class="kicker">价格</div>
<h2>按时长买,永不过期<br>不订阅、不扣月费</h2>
</div>
<div class="pricing reveal">
<div class="price-card">
<div class="minutes"><span class="num">100</span> 分钟</div>
<div class="price"><span class="cur">¥</span>9</div>
<div class="unit">约 ¥0.09 / 分钟</div>
<a class="btn btn-lg btn-secondary" href="#download">选这档</a>
</div>
<div class="price-card hot">
<span class="tag">最多人选 · 省 13%</span>
<div class="minutes"><span class="num">500</span> 分钟</div>
<div class="price"><span class="cur">¥</span>39</div>
<div class="unit">约 ¥0.078 / 分钟</div>
<a class="btn btn-lg btn-primary" href="#download">选这档</a>
</div>
<div class="price-card">
<span class="tag">省 28%</span>
<div class="minutes"><span class="num">2000</span> 分钟</div>
<div class="price"><span class="cur">¥</span>129</div>
<div class="unit">约 ¥0.065 / 分钟</div>
<a class="btn btn-lg btn-secondary" href="#download">选这档</a>
</div>
</div>
<div class="pricing-note">每天另有 <span class="num">3</span> 分钟免费试用 · 微信支付,到账即用 · 时长永不过期</div>
</div>
</section>
<!-- ════════ 尾部 CTA ════════ -->
<div class="cta" data-theme="dark">
<div class="frame cta-inner reveal">
<svg class="logo-big" width="64" height="64" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"/><path d="M17 20.5v7"/><path d="M24 16.5v15"/><path d="M31 20.5v7"/></svg>
<h2>从下一句话开始<br>用说的</h2>
<p>下载 dudu,微信扫码登录,每天免费 3 分钟</p>
<a class="btn btn-lg btn-primary" href="#download">免费下载 dudu</a>
</div>
</div>
<!-- ════════ 页脚 ════════ -->
<footer data-theme="dark">
<div class="frame footer-inner">
<a class="brand" href="#">
<svg width="20" height="20" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"/><path d="M17 20.5v7"/><path d="M24 16.5v15"/><path d="M31 20.5v7"/></svg>
<span class="name latin" style="font-size:15px;font-weight:600">dudu</span>
</a>
<div class="footer-links">
<a href="#">用户协议</a>
<a href="#">隐私政策</a>
<a href="#">联系我们</a>
</div>
<div>© 2026 dudu · 沪ICP备XXXXXXXX号</div>
</div>
</footer>
<script>
/* ── 波形条 ── */
const WAVE_PATTERN = [0.3, 0.55, 0.85, 0.45, 0.7, 1, 0.4, 0.6, 0.25, 0.8, 0.5, 0.35];
const wave = document.getElementById('wave');
WAVE_PATTERN.forEach((p, i) => {
const bar = document.createElement('i');
bar.style.height = Math.max(4, Math.round(p * 24)) + 'px';
bar.style.animationDelay = (i % 6) * 0.07 + 's';
wave.appendChild(bar);
});
/* ── 演示剧场:按住 = 流式识别,松开 = 上屏 ── */
const SENTENCES = [
'帮我把这份周报整理一下,重点突出本周的进展和下周的计划。',
'好的,我马上把文件发给你,大概十分钟之内。',
'明天上午十点的会改到下午三点,记得同步给项目组。',
];
const stage = document.getElementById('stage');
const overlay = document.getElementById('overlay');
const ovlStatus = document.getElementById('ovl-status');
const ovlText = document.getElementById('ovl-text');
const docText = document.getElementById('doc-text');
const docPlaceholder = document.getElementById('doc-placeholder');
const micbar = document.getElementById('micbar');
const micbarLabel = document.getElementById('micbar-label');
let timer = null, idx = 0, finalText = '', partialText = '', recording = false;
function render() {
const has = finalText || partialText;
ovlStatus.hidden = !!has;
ovlText.hidden = !has;
ovlText.innerHTML = '';
ovlText.append(finalText);
if (partialText) {
const s = document.createElement('span');
s.className = 'partial';
s.textContent = partialText;
ovlText.appendChild(s);
}
}
function start() {
if (recording) return;
recording = true;
finalText = ''; partialText = '';
render();
stage.classList.add('recording');
micbar.classList.add('recording');
micbarLabel.textContent = '松开完成输入';
overlay.classList.add('show');
const sentence = SENTENCES[idx % SENTENCES.length];
let i = 0;
timer = setInterval(() => {
i += 1;
if (i > sentence.length) { clearInterval(timer); return; }
const seen = sentence.slice(0, i);
const lastPunct = Math.max(seen.lastIndexOf(''), seen.lastIndexOf('。'));
finalText = sentence.slice(0, lastPunct + 1);
partialText = sentence.slice(lastPunct + 1, i);
render();
}, 75);
}
function stop() {
if (!recording) return;
recording = false;
clearInterval(timer);
stage.classList.remove('recording');
micbar.classList.remove('recording');
micbarLabel.textContent = '按住试一试';
overlay.classList.remove('show');
const committed = finalText + partialText;
if (committed) {
idx += 1;
docPlaceholder.hidden = true;
// 模拟注入:整句快速"打"进文档
let j = 0;
const base = docText.textContent;
const typeTimer = setInterval(() => {
j += 3;
docText.textContent = base + committed.slice(0, j);
if (j >= committed.length) clearInterval(typeTimer);
}, 16);
}
finalText = ''; partialText = '';
}
micbar.addEventListener('pointerdown', (e) => { e.preventDefault(); start(); });
window.addEventListener('pointerup', stop);
micbar.addEventListener('pointercancel', stop);
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !e.repeat && e.target === document.body) { e.preventDefault(); start(); }
});
window.addEventListener('keyup', (e) => { if (e.code === 'Space') stop(); });
window.addEventListener('blur', stop);
/* ── 滚动浮现 ── */
const io = new IntersectionObserver((entries) => {
entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add('in'); io.unobserve(en.target); } });
}, { threshold: 0.15 });
document.querySelectorAll('.reveal').forEach((el) => io.observe(el));
</script>
</body>
</html>