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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// dudu 设置窗口 — 四分组(输入 / 通用 / 账号与时长 / 帮助),规格见 doc/frontend-design.html 4.2
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@dudu/design/components/core/Button';
|
||||
import { Badge } from '@dudu/design/components/core/Badge';
|
||||
import { HotkeyCombo } from '@dudu/design/components/core/Kbd';
|
||||
import { ProgressBar } from '@dudu/design/components/core/ProgressBar';
|
||||
import { Switch } from '@dudu/design/components/forms/Switch';
|
||||
import { SettingRow } from '@dudu/design/components/forms/SettingRow';
|
||||
import { invoke } from '../shared/tauri';
|
||||
import { setThemePref } from '../shared/theme';
|
||||
|
||||
const THEME_OPTIONS = [['system', '跟随系统'], ['light', '浅色'], ['dark', '深色']];
|
||||
|
||||
function Section({ title, children }) {
|
||||
return (
|
||||
<div style={{ padding: '4px 18px 8px' }}>
|
||||
<div style={{
|
||||
fontSize: 'var(--text-xs)', color: 'var(--text-3)', fontWeight: 'var(--weight-medium)',
|
||||
margin: '10px 0 2px', letterSpacing: 'var(--tracking-wide)',
|
||||
}}>{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Sep = () => <div style={{ height: 1, background: 'var(--border-1)' }}></div>;
|
||||
|
||||
export function SettingsApp() {
|
||||
const [settings, setSettings] = useState({
|
||||
hotkey: 'CmdOrCtrl+Shift+Space', mic: '', sound: false, autostart: true, theme: 'system',
|
||||
});
|
||||
const [mics, setMics] = useState([]);
|
||||
const [me, setMe] = useState(null); // null = 未登录
|
||||
|
||||
useEffect(() => {
|
||||
invoke('get_settings').then((s) => { if (s) { setSettings(s); setThemePref(s.theme); } });
|
||||
invoke('list_mic_devices').then((d) => d && setMics(d));
|
||||
invoke('fetch_me').then((m) => m && setMe(m));
|
||||
}, []);
|
||||
|
||||
const update = (patch) => {
|
||||
const next = { ...settings, ...patch };
|
||||
setSettings(next);
|
||||
invoke('set_settings', { settings: next });
|
||||
if (patch.theme) setThemePref(patch.theme);
|
||||
};
|
||||
|
||||
const minutes = me ? Math.floor(me.balance_seconds / 60) : 0;
|
||||
const trialUsedMin = me ? Math.floor(me.trial_used_today / 60) : 0;
|
||||
|
||||
return (
|
||||
<div style={{ fontFamily: 'var(--font-sans)', background: 'var(--bg-app)', minHeight: '100vh' }}>
|
||||
<Section title="输入">
|
||||
<SettingRow label="说话快捷键" description="按住说话,松开上屏">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
||||
<HotkeyCombo keys={['⌘', '⇧', 'Space']} />
|
||||
<Button variant="ghost" size="sm">修改</Button>
|
||||
</span>
|
||||
</SettingRow>
|
||||
<SettingRow label="麦克风">
|
||||
<select
|
||||
value={settings.mic}
|
||||
onChange={(e) => update({ mic: e.target.value })}
|
||||
style={{
|
||||
height: 'var(--control-sm)', padding: '0 10px', borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-1)', background: 'var(--surface-card)',
|
||||
color: 'var(--text-1)', fontSize: 'var(--text-sm)', fontFamily: 'var(--font-sans)',
|
||||
}}>
|
||||
<option value="">系统默认</option>
|
||||
{mics.map((m) => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow label="提示音" description="开始 / 完成时播放轻提示音">
|
||||
<Switch checked={settings.sound} onChange={(v) => update({ sound: v })} />
|
||||
</SettingRow>
|
||||
</Section>
|
||||
<Sep />
|
||||
<Section title="通用">
|
||||
<SettingRow label="开机自启">
|
||||
<Switch checked={settings.autostart} onChange={(v) => update({ autostart: v })} />
|
||||
</SettingRow>
|
||||
<SettingRow label="外观">
|
||||
<span style={{ display: 'inline-flex', background: 'var(--surface-2)', borderRadius: 'var(--radius-sm)', padding: 2, gap: 2 }}>
|
||||
{THEME_OPTIONS.map(([val, lab]) => (
|
||||
<button key={val} type="button" onClick={() => update({ theme: val })} style={{
|
||||
height: 24, padding: '0 10px', borderRadius: 4, border: 'none', cursor: 'pointer',
|
||||
fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)',
|
||||
background: settings.theme === val ? 'var(--surface-card)' : 'transparent',
|
||||
color: settings.theme === val ? 'var(--text-1)' : 'var(--text-2)',
|
||||
boxShadow: settings.theme === val ? 'var(--shadow-card)' : 'none',
|
||||
}}>{lab}</button>
|
||||
))}
|
||||
</span>
|
||||
</SettingRow>
|
||||
</Section>
|
||||
<Sep />
|
||||
<Section title="账号与时长">
|
||||
<SettingRow label="登录状态" description={me ? '微信已登录' : '登录后开始使用'}>
|
||||
{me ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 'var(--text-sm)', color: 'var(--text-1)' }}>
|
||||
{me.nickname_masked}
|
||||
<Badge tone={me.account_state === 'quota' ? 'orange' : 'green'}>
|
||||
{me.account_state === 'ok' ? '余额充足' : me.account_state === 'trial' ? '试用中' : '余额不足'}
|
||||
</Badge>
|
||||
</span>
|
||||
) : (
|
||||
<Button variant="primary" size="sm" onClick={() => invoke('open_login')}>微信登录</Button>
|
||||
)}
|
||||
</SettingRow>
|
||||
<SettingRow label="时长余额" description="时长不过期 · 每天另有 3 分钟免费试用">
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>
|
||||
{minutes} 分钟
|
||||
</span>
|
||||
<Button variant="secondary" size="sm" onClick={() => invoke('open_login')}>购买时长</Button>
|
||||
</span>
|
||||
</SettingRow>
|
||||
<div style={{ padding: '10px 0 12px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginBottom: 7 }}>
|
||||
<span>今日免费试用</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{trialUsedMin} / 3 分钟</span>
|
||||
</div>
|
||||
<ProgressBar value={trialUsedMin} max={3} />
|
||||
</div>
|
||||
</Section>
|
||||
<Sep />
|
||||
<Section title="帮助">
|
||||
<SettingRow label="反馈问题" description="遇到问题或建议,告诉我们">
|
||||
<Button variant="ghost" size="sm" onClick={() => invoke('open_feedback')}>去反馈</Button>
|
||||
</SettingRow>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '@dudu/design/styles.css';
|
||||
import { SettingsApp } from './SettingsApp';
|
||||
|
||||
createRoot(document.getElementById('root')).render(<SettingsApp />);
|
||||
Reference in New Issue
Block a user