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>
188 lines
11 KiB
React
188 lines
11 KiB
React
// dudu 移动端 · 主 App 四屏:启用引导 / 登录 / 购买时长 / 我的
|
||
|
||
function MAppHeader({ title }) {
|
||
return (
|
||
<div style={{ textAlign: 'center', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)', padding: '4px 0 10px' }}>{title}</div>
|
||
);
|
||
}
|
||
|
||
/* ---------- ① 启用引导 ---------- */
|
||
function OnboardingScreen() {
|
||
const { Button } = window.DuduDesignSystem_2e0172;
|
||
const [step, setStep] = React.useState(0);
|
||
const steps = [
|
||
{ title: '添加键盘', desc: '设置 → 通用 → 键盘 → 添加新键盘,选择 dudu', icon: <DuMIcons.Keyboard size={26} /> },
|
||
{ title: '开启完全访问', desc: '语音识别需要联网,请允许完全访问', icon: <DuMIcons.Shield size={26} /> },
|
||
{ title: '试一试', desc: '在下面的输入框按住麦克风说一句话', icon: <DuMIcons.Mic size={26} /> },
|
||
];
|
||
return (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 18px 18px' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, padding: '18px 0 6px' }}>
|
||
<span style={{ color: 'var(--accent)' }}><DuduMLogo size={44} /></span>
|
||
<div style={{ fontSize: 'var(--text-xl)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)', whiteSpace: 'nowrap' }}>三步开启 dudu</div>
|
||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>按住说话,松开上屏</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 14 }}>
|
||
{steps.map((s, i) => {
|
||
const state = i < step ? 'done' : i === step ? 'active' : 'todo';
|
||
return (
|
||
<div key={i} style={{
|
||
display: 'flex', alignItems: 'center', gap: 12, padding: '13px 14px',
|
||
borderRadius: 'var(--radius-lg)', background: 'var(--surface-card)',
|
||
border: state === 'active' ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||
opacity: state === 'todo' ? 0.62 : 1,
|
||
transition: 'border-color var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out)',
|
||
}}>
|
||
<span style={{
|
||
width: 40, height: 40, borderRadius: 'var(--radius-md)', flex: 'none',
|
||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||
background: state === 'done' ? 'var(--positive-soft)' : 'var(--accent-soft)',
|
||
color: state === 'done' ? 'var(--positive)' : 'var(--accent-text)',
|
||
}}>{state === 'done' ? <DuMIcons.Check size={20} /> : s.icon}</span>
|
||
<span style={{ minWidth: 0 }}>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{i + 1}. {s.title}</span>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2, lineHeight: 'var(--leading-normal)' }}>{s.desc}</span>
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div style={{ marginTop: 'auto' }}>
|
||
<Button variant="primary" size="lg" block onClick={() => setStep((s) => (s + 1) % 4)}>
|
||
{step >= 3 ? '完成' : step === 2 ? '我说完了' : '去设置'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ---------- ② 登录 ---------- */
|
||
function LoginScreen() {
|
||
return (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 22px 22px' }}>
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||
<span style={{ color: 'var(--accent)' }}><DuduMLogo size={64} sw={3.2} /></span>
|
||
<div style={{ fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)', letterSpacing: 'var(--tracking-wide)' }}>dudu</div>
|
||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>大模型语音输入 · 说话即上屏</div>
|
||
</div>
|
||
<button type="button" style={{
|
||
height: 'var(--control-xl)', borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
|
||
background: '#07C160', color: '#fff', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-semibold)',
|
||
fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%',
|
||
}}>
|
||
<DuMIcons.Wechat size={19} /> 微信一键登录
|
||
</button>
|
||
<div style={{ textAlign: 'center', fontSize: 'var(--text-xs)', color: 'var(--text-3)', marginTop: 12, lineHeight: 'var(--leading-normal)' }}>
|
||
登录即同意《用户协议》与《隐私政策》
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ---------- ③ 购买时长 ---------- */
|
||
function PaywallScreen() {
|
||
const { Button } = window.DuduDesignSystem_2e0172;
|
||
const [pack, setPack] = React.useState('m');
|
||
const PACKS = [
|
||
['s', '100 分钟', 9, '约 ¥0.09 / 分钟', ''],
|
||
['m', '500 分钟', 39, '约 ¥0.078 / 分钟', '省 13%'],
|
||
['l', '2000 分钟', 129, '约 ¥0.065 / 分钟', '省 28%'],
|
||
];
|
||
const price = PACKS.find((p) => p[0] === pack)[2];
|
||
return (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 16px 16px', minHeight: 0, overflow: 'auto' }}>
|
||
<MAppHeader title="购买时长" />
|
||
|
||
{/* 余额卡 */}
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)', flex: 'none' }}>
|
||
<span>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)' }}>时长余额</span>
|
||
<span style={{ display: 'block', marginTop: 2, fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-bold)', color: 'var(--text-1)', fontFamily: 'var(--font-sans)' }}>472 <span style={{ fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-regular)', color: 'var(--text-3)' }}>分钟</span></span>
|
||
</span>
|
||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--text-3)', textAlign: 'right', lineHeight: 'var(--leading-normal)' }}>时长不过期<br />每天另有 3 分钟免费试用</span>
|
||
</div>
|
||
|
||
{/* 时长包 */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
|
||
{PACKS.map(([key, lab, p, desc, tag]) => {
|
||
const active = pack === key;
|
||
return (
|
||
<button key={key} type="button" onClick={() => setPack(key)} style={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', textAlign: 'left',
|
||
padding: '12px 14px', cursor: 'pointer', borderRadius: 'var(--radius-lg)', fontFamily: 'var(--font-sans)',
|
||
border: active ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||
background: active ? 'var(--accent-soft)' : 'var(--surface-card)',
|
||
transition: 'border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out)',
|
||
}}>
|
||
<span>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
||
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{lab}</span>
|
||
{tag && <span style={{ fontSize: 10, padding: '1px 7px', borderRadius: 'var(--radius-full)', background: 'var(--warning-soft)', color: 'var(--warning)', fontWeight: 'var(--weight-medium)', whiteSpace: 'nowrap' }}>{tag}</span>}
|
||
</span>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2 }}>{desc}</span>
|
||
</span>
|
||
<span style={{ whiteSpace: 'nowrap', fontSize: 'var(--text-xl)', fontWeight: 'var(--weight-bold)', color: active ? 'var(--accent-text)' : 'var(--text-1)' }}>¥{p}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div style={{ marginTop: 'auto', paddingTop: 14 }}>
|
||
<Button variant="primary" size="lg" block>微信支付 ¥{price}</Button>
|
||
<div style={{ textAlign: 'center', fontSize: 'var(--text-xs)', color: 'var(--text-3)', marginTop: 8 }}>支付后立即到账 · 时长不过期</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ---------- ④ 我的 ---------- */
|
||
function ProfileScreen() {
|
||
const { Badge, ProgressBar } = window.DuduDesignSystem_2e0172;
|
||
const perms = [
|
||
{ label: '麦克风权限', ok: true },
|
||
{ label: '键盘已添加', ok: true },
|
||
{ label: '完全访问', ok: false, hint: '去开启' },
|
||
];
|
||
return (
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, padding: '4px 16px 16px', minHeight: 0, overflow: 'auto' }}>
|
||
<MAppHeader title="我的" />
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 14px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)' }}>
|
||
<span style={{ width: 46, height: 46, borderRadius: '50%', background: 'var(--accent-soft)', color: 'var(--accent-text)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
|
||
<DuMIcons.User size={22} />
|
||
</span>
|
||
<span style={{ minWidth: 0, flex: 1 }}>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>wang***</span>
|
||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2 }}>时长余额 472 分钟 · 不过期</span>
|
||
</span>
|
||
<Badge tone="green">余额充足</Badge>
|
||
</div>
|
||
|
||
<div style={{ padding: '14px 14px 16px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginBottom: 8 }}>
|
||
<span>今日免费试用</span><span style={{ fontFamily: 'var(--font-mono)' }}>1 / 3 分钟</span>
|
||
</div>
|
||
<ProgressBar value={1} max={3} />
|
||
</div>
|
||
|
||
<div style={{ background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)', overflow: 'hidden' }}>
|
||
{perms.map((p, i) => (
|
||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderBottom: i < perms.length - 1 ? '1px solid var(--border-2)' : 'none', fontSize: 'var(--text-base)', color: 'var(--text-1)' }}>
|
||
<span>{p.label}</span>
|
||
{p.ok ? (
|
||
<span style={{ color: 'var(--positive)', display: 'inline-flex' }}><DuMIcons.Check size={16} /></span>
|
||
) : (
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, fontSize: 'var(--text-sm)', color: 'var(--danger)' }}>
|
||
{p.hint} <DuMIcons.ChevronRight size={14} />
|
||
</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
Object.assign(window, { OnboardingScreen, LoginScreen, PaywallScreen, ProfileScreen });
|