Files
dudu/desktop/src/login/main.jsx
T
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

161 lines
7.7 KiB
React
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.
// 登录 / 购买窗口四步流:扫码登录 → 时长包 → 支付二维码 → 完成
// 规格见 doc/frontend-design.html 4.2;二维码内容来自后端,轮询自动推进。
import { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import '@dudu/design/styles.css';
import { Button } from '@dudu/design/components/core/Button';
import { Badge } from '@dudu/design/components/core/Badge';
import { invoke } from '../shared/tauri';
function Center({ children }) {
return (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14,
padding: '26px 24px 30px', fontFamily: 'var(--font-sans)',
background: 'var(--bg-app)', minHeight: '100vh', boxSizing: 'border-box',
}}>{children}</div>
);
}
// 简易二维码占位:真实渲染由 qr 内容字符串绘制(接入 qrcode 库或后端返图)。
function QrBox({ value }) {
return (
<div style={{
width: 180, height: 180, borderRadius: 'var(--radius-md)', border: '1px solid var(--border-1)',
background: 'var(--surface-card)', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 'var(--text-xs)', color: 'var(--text-3)', padding: 12, textAlign: 'center',
wordBreak: 'break-all', overflow: 'hidden',
}}>{value || '加载中'}</div>
);
}
function LoginApp() {
const [step, setStep] = useState('login'); // login | plans | pay | done
const [qr, setQr] = useState(null);
const [packs, setPacks] = useState([]);
const [sel, setSel] = useState('pack_500');
const [order, setOrder] = useState(null);
const [me, setMe] = useState(null);
const timer = useRef(null);
// 扫码登录:创建二维码 + 轮询
useEffect(() => {
if (step !== 'login') return undefined;
let stop = false;
invoke('login_qr_create').then((r) => {
if (!r || stop) return;
setQr(r);
timer.current = setInterval(async () => {
const st = await invoke('login_qr_poll', { state: r.state });
if (st && st.status === 'confirmed') {
clearInterval(timer.current);
const m = await invoke('fetch_me');
setMe(m);
const ps = await invoke('fetch_packs');
setPacks(ps || []);
setStep('plans');
}
}, 1000);
});
return () => { stop = true; clearInterval(timer.current); };
}, [step]);
// 支付轮询
useEffect(() => {
if (step !== 'pay' || !order) return undefined;
const t = setInterval(async () => {
const st = await invoke('order_status', { orderId: order.order_id });
if (st && st.status === 'paid') {
clearInterval(t);
setMe(await invoke('fetch_me'));
setStep('done');
}
}, 1000);
return () => clearInterval(t);
}, [step, order]);
const pack = packs.find((p) => p.id === sel);
if (step === 'login') {
return (
<Center>
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>微信扫码登录</div>
<QrBox value={qr && qr.qr_url} />
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)', textAlign: 'center' }}>打开微信扫一扫确认后自动登录</div>
</Center>
);
}
if (step === 'plans') {
return (
<Center>
<div style={{ alignSelf: 'stretch', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>购买时长</span>
{me && <span style={{ display: 'inline-flex', gap: 8, alignItems: 'center', fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>
{me.nickname_masked} <Badge tone="blue">{me.account_state === 'ok' ? '余额充足' : '试用中'}</Badge>
</span>}
</div>
<div style={{ alignSelf: 'stretch', display: 'flex', flexDirection: 'column', gap: 10 }}>
{packs.map((p) => {
const active = sel === p.id;
return (
<button key={p.id} type="button" onClick={() => setSel(p.id)} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center', textAlign: 'left',
padding: '13px 16px', borderRadius: 'var(--radius-md)', cursor: 'pointer',
border: active ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
background: active ? 'var(--accent-soft)' : 'var(--surface-card)', fontFamily: 'var(--font-sans)',
}}>
<span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{p.minutes} 分钟</span>
{p.tag && <span style={{ fontSize: 10, padding: '1px 7px', borderRadius: 'var(--radius-full)', background: 'var(--warning-soft)', color: 'var(--warning)', fontWeight: 'var(--weight-medium)' }}>{p.tag}</span>}
</span>
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 3 }}>{p.unit_desc}</span>
</span>
<span style={{ fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-bold)', color: active ? 'var(--accent-text)' : 'var(--text-1)' }}>
¥{p.price_cents / 100}
</span>
</button>
);
})}
</div>
<div style={{ alignSelf: 'stretch', fontSize: 'var(--text-xs)', color: 'var(--text-3)' }}>时长不过期 · 每天另有 3 分钟免费试用</div>
<Button variant="primary" block disabled={!pack} onClick={async () => {
const o = await invoke('create_order', { packId: sel });
if (o) { setOrder(o); setStep('pay'); }
}}>微信支付 ¥{pack ? pack.price_cents / 100 : '—'}</Button>
</Center>
);
}
if (step === 'pay') {
return (
<Center>
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>微信扫码支付</div>
<div style={{ fontSize: 'var(--text-3xl)', fontWeight: 'var(--weight-bold)', color: 'var(--text-1)' }}>
¥{pack.price_cents / 100}
<span style={{ fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-regular)', color: 'var(--text-3)' }}> · {pack.minutes} 分钟时长包</span>
</div>
<QrBox value={order && order.code_url} />
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>支付完成后自动开通</div>
<Button variant="secondary" size="sm" onClick={() => setStep('plans')}>返回</Button>
</Center>
);
}
return (
<Center>
<span style={{
width: 56, height: 56, borderRadius: '50%', background: 'var(--positive-soft)', color: 'var(--positive)',
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28,
}}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
</span>
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>已到账 {pack.minutes} 分钟</div>
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)', textAlign: 'center' }}>
时长余额 {me ? Math.floor(me.balance_seconds / 60) : '—'} 分钟 · 不过期 · 现在就按住 Space 试试
</div>
<Button variant="primary" onClick={() => invoke('close_login')}>开始使用</Button>
</Center>
);
}
createRoot(document.getElementById('root')).render(<LoginApp />);