Files
app_02_bill/components/VoiceInput.tsx
karlcarlo 194de6ec06 feat(api): 集成后端API,实现数据持久化和用户登录
- 新增token管理和用户信息状态,支持基于token的自动登录
- 实现登录或注册接口调用,自动根据URL参数完成身份验证
- 从后端加载交易记录、消息和待办提醒,替代本地缓存读写
- 对交易记录的增删改操作同步到数据库,确保数据一致性
- 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性
- 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库
- 添加待办提醒切换状态API调用,确保状态同步服务器
- 改进界面互动,支持文件与头像上传功能,更新管家形象
- 采用异步处理并增加错误捕获和用户提示,增强稳定性
2026-01-22 18:25:42 +08:00

116 lines
4.2 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 React, { useState, useEffect, useRef } from 'react';
interface VoiceInputProps {
onTranscript: (text: string) => void;
isProcessing: boolean;
isFullWidth?: boolean;
}
const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isFullWidth = false }) => {
const [isListening, setIsListening] = useState(false);
const recognitionRef = useRef<any>(null);
const timerRef = useRef<number | null>(null);
const isStartingRef = useRef(false);
useEffect(() => {
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognitionRef.current = new SpeechRecognition();
recognitionRef.current.continuous = false;
recognitionRef.current.lang = 'zh-CN';
recognitionRef.current.interimResults = false;
recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript;
if (transcript) onTranscript(transcript);
};
recognitionRef.current.onerror = (event: any) => {
if (event.error === 'network') {
alert("语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。");
}
console.error('Speech recognition error', event.error);
};
recognitionRef.current.onend = () => {
setIsListening(false);
isStartingRef.current = false;
};
}
}, [onTranscript]);
const startListening = (e: React.PointerEvent) => {
if (e.cancelable) e.preventDefault();
if (isProcessing || isListening || isStartingRef.current) return;
if (recognitionRef.current) {
try {
isStartingRef.current = true;
recognitionRef.current.start();
setIsListening(true);
} catch (err) {
console.error("Failed to start recognition", err);
// 如果 start 失败,立即重置状态,否则会锁死
isStartingRef.current = false;
setIsListening(false);
}
} else {
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
}
};
const stopListening = () => {
// 仅在识别引擎正在运行时尝试停止
// 不要在这里重置 isListening 和 isStartingRef,等待 onend 回调
if (isListening) {
try {
recognitionRef.current?.stop();
} catch (err) {
console.error("Failed to stop recognition", err);
}
}
};
return (
<div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}>
<button
onPointerDown={startListening}
onPointerUp={stopListening}
onPointerLeave={stopListening}
disabled={isProcessing}
className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${
isFullWidth
? 'w-full py-4 rounded-2xl font-bold text-sm tracking-widest'
: 'w-14 h-14 rounded-full'
} ${
isListening
? 'bg-rose-500 text-white shadow-rose-200'
: 'bg-indigo-600 text-white shadow-indigo-100'
} ${isProcessing ? 'opacity-50 grayscale cursor-not-allowed' : ''}`}
>
{isListening && (
<div className="absolute inset-0 rounded-2xl animate-ping bg-rose-400 opacity-20"></div>
)}
<div className="flex items-center gap-2 z-10">
<svg xmlns="http://www.w3.org/2000/svg" className={`${isFullWidth ? 'h-5 w-5' : 'h-6 w-6'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
{isFullWidth && <span>{isListening ? '松开 结束' : '按住 说话'}</span>}
</div>
</button>
{isListening && isFullWidth && (
<div className="absolute -top-16 left-0 right-0 flex justify-center animate-bounce">
<div className="bg-rose-500 text-white px-4 py-2 rounded-full text-xs font-bold shadow-xl border-2 border-white">
正在录音...
</div>
</div>
)}
</div>
);
};
export default VoiceInput;