impress_asr_input_rust/web/src/pages/SettingsPage.tsx
impressionyang da5d0d8ad2
Some checks failed
Build Windows GUI / build-windows (push) Has been cancelled
Build Windows GUI / release (push) Has been cancelled
feat: 打通录音核心链路,连接前后端命令
- 重构 capture.rs:添加 start_recording/stop_recording 支持真正的开始/停止控制
- 更新 AppState:引入 RecordingHandle 字段管理录音状态
- 重构 commands.rs:start_recording 立即返回,stop_recording 停止并保存文件
- 新增 recognize_file/recognize_last_recording 命令
- 前端 RecordPage:调用真实后端命令,监听 recording-stopped 事件自动识别
- 前端 FileConvertPage:连接 recognize_file 命令,支持导出识别结果
- 前端 SettingsPage:通过 save_config 持久化配置,支持配置加载

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 20:19:44 +08:00

404 lines
13 KiB
TypeScript
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.

import { useState, useEffect } from 'react'
interface Settings {
model: string
language: string
sampleRate: number
microphone: string
theme: string
autoStart: boolean
autoCheckUpdate: boolean
autoCopy: boolean
historyKeepDays: number
hotkeyRecord: string
hotkeyCopy: string
hotkeyToggle: string
modelPath?: string
}
interface SettingsPageProps {
theme: 'light' | 'dark' | 'system'
onThemeChange: (theme: 'light' | 'dark' | 'system') => void
}
interface TauriAPI {
invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
}
declare const window: Window & {
__TAURI__?: TauriAPI
}
const isTauri = () => typeof window !== 'undefined' && !!window.__TAURI__
const defaultSettings: Settings = {
model: 'sensevoice-small',
language: 'zh',
sampleRate: 16000,
microphone: '默认设备',
theme: 'dark',
autoStart: false,
autoCheckUpdate: true,
autoCopy: false,
historyKeepDays: 30,
hotkeyRecord: 'Ctrl+Shift+R',
hotkeyCopy: 'Ctrl+Shift+C',
hotkeyToggle: 'Ctrl+Shift+H'
}
export default function SettingsPage({ theme, onThemeChange }: SettingsPageProps) {
const [settings, setSettings] = useState<Settings>(defaultSettings)
const [modified, setModified] = useState(false)
const [saving, setSaving] = useState(false)
const [saveMessage, setSaveMessage] = useState<string | null>(null)
// 加载保存的配置
useEffect(() => {
if (!isTauri()) return
const loadConfig = async () => {
try {
const config = await window.__TAURI__!.invoke('get_config_cmd') as Record<string, unknown>
if (config) {
setSettings(prev => ({
...prev,
model: (config.asr as any)?.model || prev.model,
language: (config.asr as any)?.language || prev.language,
sampleRate: (config.audio as any)?.sample_rate || prev.sampleRate,
microphone: (config.audio as any)?.device || prev.microphone,
modelPath: (config.asr as any)?.model_path || prev.modelPath,
}))
}
} catch (e) {
console.error('加载配置失败:', e)
}
}
loadConfig()
}, [])
const handleChange = (key: keyof Settings, value: Settings[typeof key]) => {
setSettings(prev => ({ ...prev, [key]: value }))
setModified(true)
setSaveMessage(null)
}
const handleSave = async () => {
setSaving(true)
setSaveMessage(null)
try {
if (isTauri()) {
// 构建后端期望的配置格式
const configPayload = {
asr: {
model: settings.model,
language: settings.language,
model_path: settings.modelPath || null,
},
audio: {
sample_rate: settings.sampleRate,
channels: 1,
device: settings.microphone,
},
ui: {
theme: settings.theme,
auto_start: settings.autoStart,
auto_check_update: settings.autoCheckUpdate,
auto_copy: settings.autoCopy,
history_keep_days: settings.historyKeepDays,
},
hotkeys: {
record: settings.hotkeyRecord,
copy: settings.hotkeyCopy,
toggle: settings.hotkeyToggle,
}
}
await window.__TAURI__!.invoke('save_config', { settings: configPayload })
setSaveMessage('✓ 设置已保存')
} else {
// 开发环境:仅日志
console.log('保存设置 (开发环境):', settings)
setSaveMessage('✓ 设置已记录到控制台 (开发环境)')
}
setModified(false)
} catch (e) {
setSaveMessage(`✗ 保存失败: ${e instanceof Error ? e.message : String(e)}`)
} finally {
setSaving(false)
}
}
const handleReset = () => {
setSettings({ ...defaultSettings })
setModified(true)
setSaveMessage(null)
}
const handleSelectModel = async () => {
try {
if (!isTauri()) {
alert('仅在 Tauri 环境中可用')
return
}
const modelPath = await window.__TAURI__!.invoke('select_model_file')
if (modelPath) {
const path = modelPath as string
setSettings(prev => ({ ...prev, modelPath: path }))
setModified(true)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
setSaveMessage(`✗ 选择模型失败: ${errorMessage}`)
}
}
return (
<div className="settings-page">
<h1 className="page-title"></h1>
{saveMessage && (
<div className={`save-message ${saveMessage.startsWith('✓') ? 'success' : 'error'}`}>
{saveMessage}
</div>
)}
<div className="card">
{/* 识别模型 */}
<div className="setting-section">
<h2 className="setting-section-title"></h2>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description"> ONNX </div>
</div>
<select
className="select"
value={settings.model}
onChange={(e) => handleChange('model', e.target.value)}
>
<option value="sensevoice-small">SenseVoice Small ()</option>
<option value="sensevoice-base">SenseVoice Base</option>
<option value="paraformer">FunASR Paraformer</option>
<option value="whisper-small">Whisper Small</option>
</select>
</div>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description">使</div>
</div>
<div className="model-path-selector">
<input
type="text"
className="input"
style={{ flex: 1, marginRight: '8px' }}
value={settings.modelPath || ''}
placeholder="选择自定义模型文件..."
readOnly
/>
<button className="btn btn-secondary" onClick={handleSelectModel}>
</button>
</div>
</div>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description"></div>
</div>
<select
className="select"
value={settings.language}
onChange={(e) => handleChange('language', e.target.value)}
>
<option value="zh"></option>
<option value="en"></option>
<option value="ja"></option>
<option value="ko"></option>
<option value="yue"></option>
</select>
</div>
</div>
{/* 音频输入 */}
<div className="setting-section">
<h2 className="setting-section-title"></h2>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description"></div>
</div>
<select
className="select"
value={settings.microphone}
onChange={(e) => handleChange('microphone', e.target.value)}
>
<option value="default"></option>
<option value="builtin">Built-in Microphone</option>
</select>
</div>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description"></div>
</div>
<select
className="select"
value={settings.sampleRate}
onChange={(e) => handleChange('sampleRate', Number(e.target.value))}
>
<option value={16000}>16000 Hz</option>
<option value={44100}>44100 Hz</option>
<option value={48000}>48000 Hz</option>
</select>
</div>
</div>
{/* 快捷键 */}
<div className="setting-section">
<h2 className="setting-section-title"></h2>
<div className="setting-item">
<div className="setting-label">/</div>
<input
type="text"
className="input"
style={{ width: '150px' }}
value={settings.hotkeyRecord}
onChange={(e) => handleChange('hotkeyRecord', e.target.value)}
/>
</div>
<div className="setting-item">
<div className="setting-label"></div>
<input
type="text"
className="input"
style={{ width: '150px' }}
value={settings.hotkeyCopy}
onChange={(e) => handleChange('hotkeyCopy', e.target.value)}
/>
</div>
<div className="setting-item">
<div className="setting-label">/</div>
<input
type="text"
className="input"
style={{ width: '150px' }}
value={settings.hotkeyToggle}
onChange={(e) => handleChange('hotkeyToggle', e.target.value)}
/>
</div>
</div>
{/* 外观 */}
<div className="setting-section">
<h2 className="setting-section-title"></h2>
<div className="setting-item">
<div className="setting-label"></div>
<div className="theme-selector">
<button
className={`theme-btn ${theme === 'light' ? 'active' : ''}`}
onClick={() => onThemeChange('light')}
>
</button>
<button
className={`theme-btn ${theme === 'dark' ? 'active' : ''}`}
onClick={() => onThemeChange('dark')}
>
</button>
<button
className={`theme-btn ${theme === 'system' ? 'active' : ''}`}
onClick={() => onThemeChange('system')}
>
</button>
</div>
</div>
</div>
{/* 其他 */}
<div className="setting-section">
<h2 className="setting-section-title"></h2>
<div className="setting-item">
<div className="setting-label"></div>
<label className="switch">
<input
type="checkbox"
checked={settings.autoStart}
onChange={(e) => handleChange('autoStart', e.target.checked)}
/>
<span className="switch-slider"></span>
</label>
</div>
<div className="setting-item">
<div className="setting-label"></div>
<label className="switch">
<input
type="checkbox"
checked={settings.autoCheckUpdate}
onChange={(e) => handleChange('autoCheckUpdate', e.target.checked)}
/>
<span className="switch-slider"></span>
</label>
</div>
<div className="setting-item">
<div className="setting-label"></div>
<label className="switch">
<input
type="checkbox"
checked={settings.autoCopy}
onChange={(e) => handleChange('autoCopy', e.target.checked)}
/>
<span className="switch-slider"></span>
</label>
</div>
<div className="setting-item">
<div>
<div className="setting-label"></div>
<div className="setting-description"></div>
</div>
<input
type="number"
className="input"
style={{ width: '80px' }}
value={settings.historyKeepDays}
onChange={(e) => handleChange('historyKeepDays', Number(e.target.value))}
min={1}
max={365}
/>
</div>
</div>
</div>
<div className="settings-actions">
<button className="btn btn-secondary" onClick={handleReset}>
</button>
<button
className="btn btn-primary"
onClick={handleSave}
disabled={!modified || saving}
>
{saving ? '保存中...' : '保存设置'}
</button>
</div>
</div>
)
}