- 新增App主组件,实现聊天、账本、统计三大功能模块切换 - 支持语音及文本输入,自动解析记账信息并生成交易记录 - 交易记录支持编辑、删除、提醒等操作,数据本地持久化 - 管家性格预设和自定义设定,包含头像上传和语气调整 - 会话展示支持关联交易显示,样式区分收入与支出 - 账本页添加日期分组交易列表及饼图统计展示 - 消息列表实现渐变背景和交互动画,提升用户体验 - 支持图片上传并同步发送给AI进行解析记账 - 底部导航实现三标签切换并同步显示对应内容 - 全局事务编辑弹窗支持直接调用和删除操作 - 增加环境变量示例和.gitignore规则,完善工程配置
104 lines
3.7 KiB
TypeScript
104 lines
3.7 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);
|
|
|
|
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);
|
|
setIsListening(false);
|
|
};
|
|
|
|
recognitionRef.current.onerror = (event: any) => {
|
|
console.error('Speech recognition error', event.error);
|
|
setIsListening(false);
|
|
};
|
|
|
|
recognitionRef.current.onend = () => {
|
|
setIsListening(false);
|
|
};
|
|
}
|
|
}, [onTranscript]);
|
|
|
|
const startListening = (e: React.MouseEvent | React.TouchEvent) => {
|
|
e.preventDefault();
|
|
if (isProcessing || isListening) return;
|
|
|
|
if (recognitionRef.current) {
|
|
try {
|
|
recognitionRef.current.start();
|
|
setIsListening(true);
|
|
} catch (e) {
|
|
console.error("Failed to start recognition", e);
|
|
}
|
|
} else {
|
|
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
|
|
}
|
|
};
|
|
|
|
const stopListening = () => {
|
|
if (isListening) {
|
|
recognitionRef.current?.stop();
|
|
setIsListening(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}>
|
|
<button
|
|
onMouseDown={startListening}
|
|
onMouseUp={stopListening}
|
|
onMouseLeave={stopListening}
|
|
onTouchStart={startListening}
|
|
onTouchEnd={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; |