- 新增token管理和用户信息状态,支持基于token的自动登录 - 实现登录或注册接口调用,自动根据URL参数完成身份验证 - 从后端加载交易记录、消息和待办提醒,替代本地缓存读写 - 对交易记录的增删改操作同步到数据库,确保数据一致性 - 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性 - 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库 - 添加待办提醒切换状态API调用,确保状态同步服务器 - 改进界面互动,支持文件与头像上传功能,更新管家形象 - 采用异步处理并增加错误捕获和用户提示,增强稳定性
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
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; |