通话逻辑调整
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AppointmentService } from '../../services/appointmentService';
|
||||
import { BillingService } from '../../services/billingService';
|
||||
import {
|
||||
Appointment,
|
||||
Interpreter,
|
||||
CallType,
|
||||
TranslationType,
|
||||
UserAccount
|
||||
} from '../../types/billing';
|
||||
|
||||
const MobileAppointment: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const appointmentService = AppointmentService.getInstance();
|
||||
const billingService = BillingService.getInstance();
|
||||
|
||||
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [selectedTime, setSelectedTime] = useState<string>('');
|
||||
const [callType, setCallType] = useState<CallType>(CallType.VOICE);
|
||||
const [translationType, setTranslationType] = useState<TranslationType>(TranslationType.TEXT);
|
||||
const [selectedLanguages, setSelectedLanguages] = useState({
|
||||
from: 'zh-CN',
|
||||
to: 'en-US',
|
||||
});
|
||||
const [selectedInterpreter, setSelectedInterpreter] = useState<Interpreter | null>(null);
|
||||
const [availableInterpreters, setAvailableInterpreters] = useState<Interpreter[]>([]);
|
||||
const [description, setDescription] = useState('');
|
||||
const [estimatedCost, setEstimatedCost] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const languages = [
|
||||
{ code: 'zh-CN', name: '中文', flag: '🇨🇳' },
|
||||
{ code: 'en-US', name: 'English', flag: '🇺🇸' },
|
||||
{ code: 'ja-JP', name: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'ko-KR', name: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'es-ES', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr-FR', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de-DE', name: 'Deutsch', flag: '🇩🇪' },
|
||||
];
|
||||
|
||||
// 生成可选时间段
|
||||
const timeSlots = [
|
||||
'09:00', '09:30', '10:00', '10:30', '11:00', '11:30',
|
||||
'14:00', '14:30', '15:00', '15:30', '16:00', '16:30',
|
||||
'17:00', '17:30', '18:00', '18:30', '19:00', '19:30',
|
||||
'20:00', '20:30', '21:00'
|
||||
];
|
||||
|
||||
// 生成未来7天的日期选项
|
||||
const getDateOptions = () => {
|
||||
const dates = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + i);
|
||||
dates.push({
|
||||
value: date.toISOString().split('T')[0],
|
||||
label: i === 0 ? '今天' : i === 1 ? '明天' :
|
||||
`${date.getMonth() + 1}月${date.getDate()}日`,
|
||||
weekday: date.toLocaleDateString('zh-CN', { weekday: 'short' })
|
||||
});
|
||||
}
|
||||
return dates;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const account = billingService.getUserAccount();
|
||||
setUserAccount(account);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate) {
|
||||
const date = new Date(selectedDate);
|
||||
const interpreters = appointmentService.getAvailableInterpreters(
|
||||
date,
|
||||
[selectedLanguages.from, selectedLanguages.to]
|
||||
);
|
||||
setAvailableInterpreters(interpreters);
|
||||
}
|
||||
}, [selectedDate, selectedLanguages]);
|
||||
|
||||
useEffect(() => {
|
||||
// 计算预估费用
|
||||
if (callType && translationType && userAccount) {
|
||||
const interpreterRate = selectedInterpreter?.pricePerMinute;
|
||||
const baseCost = billingService.calculateCallCost(
|
||||
callType,
|
||||
translationType,
|
||||
30, // 假设30分钟
|
||||
interpreterRate
|
||||
);
|
||||
setEstimatedCost(baseCost);
|
||||
}
|
||||
}, [callType, translationType, selectedInterpreter, userAccount]);
|
||||
|
||||
useEffect(() => {
|
||||
// 根据通话类型设置默认翻译类型
|
||||
if (callType === CallType.VIDEO) {
|
||||
setTranslationType(TranslationType.SIGN_LANGUAGE);
|
||||
} else {
|
||||
setTranslationType(TranslationType.TEXT);
|
||||
}
|
||||
}, [callType]);
|
||||
|
||||
const formatCurrency = (cents: number) => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedDate || !selectedTime || !callType || !translationType) {
|
||||
alert('请完善预约信息');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userAccount || userAccount.balance < estimatedCost) {
|
||||
alert('账户余额不足,请先充值');
|
||||
navigate('/mobile/recharge');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// 构建预约数据
|
||||
const appointmentData = {
|
||||
userId: 'user_1', // 实际应该从用户状态获取
|
||||
title: `${callType === CallType.VOICE ? '语音' : '视频'}通话预约`,
|
||||
description: `${translationType === TranslationType.TEXT ? '文本翻译' :
|
||||
translationType === TranslationType.SIGN_LANGUAGE ? '手语翻译' : '人工翻译'}`,
|
||||
scheduledTime: new Date(`${selectedDate}T${selectedTime}`),
|
||||
duration: 60, // 默认60分钟
|
||||
callType,
|
||||
translationType,
|
||||
interpreterIds: selectedInterpreter ? [selectedInterpreter.id] : undefined,
|
||||
estimatedCost,
|
||||
status: 'scheduled' as const,
|
||||
};
|
||||
|
||||
// 创建预约
|
||||
const appointment = appointmentService.createAppointment(appointmentData);
|
||||
|
||||
alert('预约创建成功!');
|
||||
navigate('/mobile/home');
|
||||
} catch (error) {
|
||||
console.error('创建预约失败:', error);
|
||||
alert('创建预约失败,请重试');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
backButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '12px',
|
||||
},
|
||||
title: {
|
||||
fontSize: '20px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
callTypeSelector: {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
callTypeButton: (active: boolean) => ({
|
||||
flex: 1,
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: active ? '2px solid #1890ff' : '2px solid #f0f0f0',
|
||||
backgroundColor: active ? '#e6f7ff' : 'white',
|
||||
color: active ? '#1890ff' : '#666',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500' as const,
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center' as const,
|
||||
}),
|
||||
dateGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
dateOption: (selected: boolean) => ({
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: selected ? '2px solid #1890ff' : '2px solid #f0f0f0',
|
||||
backgroundColor: selected ? '#e6f7ff' : 'white',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center' as const,
|
||||
}),
|
||||
dateLabel: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
dateWeekday: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
},
|
||||
timeGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '8px',
|
||||
},
|
||||
timeSlot: (selected: boolean, available: boolean) => ({
|
||||
padding: '12px 8px',
|
||||
borderRadius: '8px',
|
||||
border: selected ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: selected ? '#e6f7ff' : available ? 'white' : '#f5f5f5',
|
||||
color: selected ? '#1890ff' : available ? '#333' : '#999',
|
||||
fontSize: '14px',
|
||||
textAlign: 'center' as const,
|
||||
cursor: available ? 'pointer' : 'not-allowed',
|
||||
}),
|
||||
languageRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
languageSelect: {
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: 'white',
|
||||
fontSize: '14px',
|
||||
},
|
||||
swapButton: {
|
||||
margin: '0 12px',
|
||||
padding: '8px',
|
||||
borderRadius: '20px',
|
||||
border: 'none',
|
||||
backgroundColor: '#f0f8ff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
},
|
||||
translationTypeSelector: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
translationTypeButton: (active: boolean) => ({
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: active ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: active ? '#e6f7ff' : 'white',
|
||||
color: active ? '#1890ff' : '#666',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left' as const,
|
||||
}),
|
||||
interpreterList: {
|
||||
marginTop: '16px',
|
||||
},
|
||||
interpreterItem: (selected: boolean) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: selected ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: selected ? '#e6f7ff' : 'white',
|
||||
marginBottom: '12px',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
interpreterAvatar: {
|
||||
fontSize: '32px',
|
||||
marginRight: '16px',
|
||||
},
|
||||
interpreterInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
interpreterName: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
interpreterDetails: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
interpreterRate: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#fa8c16',
|
||||
},
|
||||
descriptionInput: {
|
||||
width: '100%',
|
||||
minHeight: '80px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
fontSize: '14px',
|
||||
resize: 'vertical' as const,
|
||||
},
|
||||
costSummary: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
costRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
costLabel: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
},
|
||||
costValue: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#1890ff',
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: selectedDate && selectedTime && !isSubmitting ? '#1890ff' : '#d9d9d9',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
cursor: selectedDate && selectedTime && !isSubmitting ? 'pointer' : 'not-allowed',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 头部 */}
|
||||
<div style={styles.header}>
|
||||
<button style={styles.backButton} onClick={() => navigate(-1)}>
|
||||
←
|
||||
</button>
|
||||
<div style={styles.title}>预约通话</div>
|
||||
</div>
|
||||
|
||||
{/* 通话类型选择 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择通话类型</div>
|
||||
<div style={styles.callTypeSelector}>
|
||||
<button
|
||||
style={styles.callTypeButton(callType === CallType.VOICE)}
|
||||
onClick={() => setCallType(CallType.VOICE)}
|
||||
>
|
||||
📞 语音通话
|
||||
</button>
|
||||
<button
|
||||
style={styles.callTypeButton(callType === CallType.VIDEO)}
|
||||
onClick={() => setCallType(CallType.VIDEO)}
|
||||
>
|
||||
📹 视频通话
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日期选择 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择日期</div>
|
||||
<div style={styles.dateGrid}>
|
||||
{getDateOptions().map((date) => (
|
||||
<div
|
||||
key={date.value}
|
||||
style={styles.dateOption(selectedDate === date.value)}
|
||||
onClick={() => setSelectedDate(date.value)}
|
||||
>
|
||||
<div style={styles.dateLabel}>{date.label}</div>
|
||||
<div style={styles.dateWeekday}>{date.weekday}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 时间选择 */}
|
||||
{selectedDate && (
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择时间</div>
|
||||
<div style={styles.timeGrid}>
|
||||
{timeSlots.map((time) => {
|
||||
const isAvailable = appointmentService.isTimeSlotAvailable(
|
||||
new Date(`${selectedDate}T${time}:00`),
|
||||
30
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={time}
|
||||
style={styles.timeSlot(selectedTime === time, isAvailable)}
|
||||
onClick={() => isAvailable && setSelectedTime(time)}
|
||||
disabled={!isAvailable}
|
||||
>
|
||||
{time}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 语言选择 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择语言</div>
|
||||
<div style={styles.languageRow}>
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.from}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, from: e.target.value})}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
style={styles.swapButton}
|
||||
onClick={() => setSelectedLanguages({
|
||||
from: selectedLanguages.to,
|
||||
to: selectedLanguages.from
|
||||
})}
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.to}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, to: e.target.value})}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 翻译服务选择 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择翻译服务</div>
|
||||
<div style={styles.translationTypeSelector}>
|
||||
{callType === CallType.VOICE && (
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.TEXT)}
|
||||
onClick={() => setTranslationType(TranslationType.TEXT)}
|
||||
>
|
||||
<div>💬 实时文字翻译</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
AI自动翻译,快速准确
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{callType === CallType.VIDEO && (
|
||||
<>
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.SIGN_LANGUAGE)}
|
||||
onClick={() => setTranslationType(TranslationType.SIGN_LANGUAGE)}
|
||||
>
|
||||
<div>👋 虚拟人手语翻译</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
AI虚拟人实时手语翻译
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.HUMAN_INTERPRETER)}
|
||||
onClick={() => setTranslationType(TranslationType.HUMAN_INTERPRETER)}
|
||||
>
|
||||
<div>👨💼 专业翻译员</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||
人工翻译,专业可靠
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 翻译员选择 */}
|
||||
{translationType === TranslationType.HUMAN_INTERPRETER && (
|
||||
<div style={styles.interpreterList}>
|
||||
<div style={styles.sectionTitle}>选择翻译员</div>
|
||||
{availableInterpreters.map((interpreter) => (
|
||||
<div
|
||||
key={interpreter.id}
|
||||
style={styles.interpreterItem(selectedInterpreter?.id === interpreter.id)}
|
||||
onClick={() => setSelectedInterpreter(interpreter)}
|
||||
>
|
||||
<div style={styles.interpreterAvatar}>{interpreter.avatar}</div>
|
||||
<div style={styles.interpreterInfo}>
|
||||
<div style={styles.interpreterName}>{interpreter.name}</div>
|
||||
<div style={styles.interpreterDetails}>
|
||||
{interpreter.languages.join(', ')} | ⭐ {interpreter.rating} | {interpreter.specialties.join(', ')}
|
||||
</div>
|
||||
<div style={styles.interpreterRate}>
|
||||
¥{formatCurrency(interpreter.pricePerMinute)}/分钟
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 备注信息 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>备注信息(可选)</div>
|
||||
<textarea
|
||||
style={styles.descriptionInput}
|
||||
placeholder="请输入特殊要求或备注信息..."
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 费用预估 */}
|
||||
{estimatedCost > 0 && (
|
||||
<div style={styles.costSummary}>
|
||||
<div style={styles.costRow}>
|
||||
<span style={styles.costLabel}>预估费用(30分钟)</span>
|
||||
<span style={styles.costValue}>¥{formatCurrency(estimatedCost)}</span>
|
||||
</div>
|
||||
<div style={styles.costRow}>
|
||||
<span style={styles.costLabel}>当前余额</span>
|
||||
<span style={styles.costValue}>
|
||||
¥{userAccount ? formatCurrency(userAccount.balance) : '0.00'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<button
|
||||
style={styles.submitButton}
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedDate || !selectedTime || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '预约中...' : '确认预约'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileAppointment;
|
||||
@@ -0,0 +1,698 @@
|
||||
import { FC, useState, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { BillingService } from '../../services/billingService';
|
||||
import { AppointmentService } from '../../services/appointmentService';
|
||||
import {
|
||||
UserAccount,
|
||||
CallType,
|
||||
TranslationType,
|
||||
Interpreter,
|
||||
BillingRule
|
||||
} from '../../types/billing';
|
||||
|
||||
const MobileCall: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const billingService = BillingService.getInstance();
|
||||
const appointmentService = AppointmentService.getInstance();
|
||||
|
||||
// 从URL参数获取通话类型
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const initialCallType = urlParams.get('type') === 'video' ? CallType.VIDEO : CallType.VOICE;
|
||||
|
||||
// 状态管理
|
||||
const [callType, setCallType] = useState<CallType>(initialCallType);
|
||||
const [translationType, setTranslationType] = useState<TranslationType>(
|
||||
callType === CallType.VIDEO ? TranslationType.SIGN_LANGUAGE : TranslationType.TEXT
|
||||
);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [callDuration, setCallDuration] = useState(0);
|
||||
const [currentCost, setCurrentCost] = useState(0);
|
||||
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
||||
const [selectedInterpreter, setSelectedInterpreter] = useState<Interpreter | null>(null);
|
||||
const [availableInterpreters, setAvailableInterpreters] = useState<Interpreter[]>([]);
|
||||
const [billingRule, setBillingRule] = useState<BillingRule | null>(null);
|
||||
const [showLowBalanceWarning, setShowLowBalanceWarning] = useState(false);
|
||||
const [selectedLanguages, setSelectedLanguages] = useState({
|
||||
from: 'zh-CN',
|
||||
to: 'en-US',
|
||||
});
|
||||
|
||||
// 计费相关
|
||||
const [lastBillingMinute, setLastBillingMinute] = useState(0);
|
||||
const billingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 翻译历史
|
||||
const [translationHistory, setTranslationHistory] = useState([
|
||||
{
|
||||
time: '14:23:15',
|
||||
original: '你好,很高兴见到你',
|
||||
translated: 'Hello, nice to meet you',
|
||||
speaker: 'you'
|
||||
},
|
||||
{
|
||||
time: '14:23:18',
|
||||
original: 'Nice to meet you too!',
|
||||
translated: '我也很高兴见到你!',
|
||||
speaker: 'other'
|
||||
},
|
||||
]);
|
||||
|
||||
const languages = [
|
||||
{ code: 'zh-CN', name: '中文', flag: '🇨🇳' },
|
||||
{ code: 'en-US', name: 'English', flag: '🇺🇸' },
|
||||
{ code: 'ja-JP', name: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'ko-KR', name: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'es-ES', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr-FR', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de-DE', name: 'Deutsch', flag: '🇩🇪' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// 获取用户账户信息
|
||||
const account = billingService.getUserAccount();
|
||||
setUserAccount(account);
|
||||
|
||||
// 获取计费规则
|
||||
if (account) {
|
||||
const rule = billingService.getBillingRule(callType, translationType, account.userType);
|
||||
setBillingRule(rule);
|
||||
}
|
||||
|
||||
// 获取可用翻译员
|
||||
if (translationType === TranslationType.HUMAN_INTERPRETER) {
|
||||
const interpreters = appointmentService.getAvailableInterpreters(
|
||||
new Date(),
|
||||
[selectedLanguages.from, selectedLanguages.to]
|
||||
);
|
||||
setAvailableInterpreters(interpreters);
|
||||
}
|
||||
}, [callType, translationType, selectedLanguages]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (isConnected) {
|
||||
interval = setInterval(() => {
|
||||
setCallDuration(prev => {
|
||||
const newDuration = prev + 1;
|
||||
const currentMinute = Math.ceil(newDuration / 60);
|
||||
|
||||
// 计算当前费用
|
||||
if (billingRule && userAccount) {
|
||||
const interpreterRate = selectedInterpreter?.pricePerMinute;
|
||||
const cost = billingService.calculateCallCost(
|
||||
callType,
|
||||
translationType,
|
||||
newDuration / 60,
|
||||
interpreterRate
|
||||
);
|
||||
setCurrentCost(cost);
|
||||
|
||||
// 每分钟开始时扣费
|
||||
if (currentMinute > lastBillingMinute) {
|
||||
const minuteCost = billingService.calculateCallCost(
|
||||
callType,
|
||||
translationType,
|
||||
1,
|
||||
interpreterRate
|
||||
);
|
||||
|
||||
if (billingService.deductBalance(minuteCost)) {
|
||||
setLastBillingMinute(currentMinute);
|
||||
setUserAccount(billingService.getUserAccount());
|
||||
} else {
|
||||
// 余额不足,断开通话
|
||||
handleDisconnect();
|
||||
alert('余额不足,通话已断开');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查低余额警告
|
||||
if (billingService.shouldShowLowBalanceWarning(callType, translationType, interpreterRate)) {
|
||||
setShowLowBalanceWarning(true);
|
||||
}
|
||||
}
|
||||
|
||||
return newDuration;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isConnected, billingRule, userAccount, selectedInterpreter, lastBillingMinute]);
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const formatCost = (cents: number) => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!userAccount || !billingRule) {
|
||||
alert('账户信息或计费规则未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
const interpreterRate = selectedInterpreter?.pricePerMinute;
|
||||
const balanceCheck = billingService.checkBalance(callType, translationType, 1, interpreterRate);
|
||||
|
||||
if (!balanceCheck.sufficient) {
|
||||
alert(`余额不足,需要至少 ¥${formatCost(balanceCheck.requiredAmount)} 才能开始通话`);
|
||||
navigate('/mobile/recharge');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnected(true);
|
||||
setCallDuration(0);
|
||||
setCurrentCost(0);
|
||||
setLastBillingMinute(0);
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setIsConnected(false);
|
||||
if (billingIntervalRef.current) {
|
||||
clearInterval(billingIntervalRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallTypeChange = (newCallType: CallType) => {
|
||||
if (isConnected) {
|
||||
alert('通话进行中无法切换类型');
|
||||
return;
|
||||
}
|
||||
|
||||
setCallType(newCallType);
|
||||
// 根据通话类型设置默认翻译类型
|
||||
if (newCallType === CallType.VIDEO) {
|
||||
setTranslationType(TranslationType.SIGN_LANGUAGE);
|
||||
} else {
|
||||
setTranslationType(TranslationType.TEXT);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTranslationTypeChange = (newTranslationType: TranslationType) => {
|
||||
if (isConnected) {
|
||||
alert('通话进行中无法切换翻译类型');
|
||||
return;
|
||||
}
|
||||
setTranslationType(newTranslationType);
|
||||
};
|
||||
|
||||
const handleInterpreterSelect = (interpreter: Interpreter) => {
|
||||
if (isConnected) {
|
||||
alert('通话进行中无法切换翻译员');
|
||||
return;
|
||||
}
|
||||
setSelectedInterpreter(interpreter);
|
||||
};
|
||||
|
||||
const swapLanguages = () => {
|
||||
setSelectedLanguages({
|
||||
from: selectedLanguages.to,
|
||||
to: selectedLanguages.from,
|
||||
});
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
callTypeSelector: {
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
callTypeButton: (active: boolean) => ({
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
backgroundColor: active ? '#1890ff' : '#f0f0f0',
|
||||
color: active ? 'white' : '#666',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
statusRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
statusIndicator: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusDot: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: isConnected ? '#52c41a' : '#d9d9d9',
|
||||
marginRight: '8px',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: isConnected ? '#52c41a' : '#666',
|
||||
},
|
||||
costInfo: {
|
||||
textAlign: 'right' as const,
|
||||
},
|
||||
duration: {
|
||||
fontSize: '20px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#1890ff',
|
||||
textAlign: 'center' as const,
|
||||
marginBottom: '8px',
|
||||
},
|
||||
currentCost: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#fa8c16',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
billingInfo: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
billingTitle: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
translationTypeSelector: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: '8px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
translationTypeButton: (active: boolean) => ({
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: active ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: active ? '#e6f7ff' : 'white',
|
||||
color: active ? '#1890ff' : '#666',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left' as const,
|
||||
}),
|
||||
rateInfo: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
interpreterSelector: {
|
||||
marginTop: '12px',
|
||||
},
|
||||
interpreterItem: (selected: boolean) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: selected ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||
backgroundColor: selected ? '#e6f7ff' : 'white',
|
||||
marginBottom: '8px',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
interpreterInfo: {
|
||||
marginLeft: '12px',
|
||||
flex: 1,
|
||||
},
|
||||
interpreterName: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
},
|
||||
interpreterDetails: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
},
|
||||
interpreterRate: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#fa8c16',
|
||||
},
|
||||
languageSelector: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
languageRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
languageSelect: {
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: 'white',
|
||||
fontSize: '14px',
|
||||
},
|
||||
swapButton: {
|
||||
margin: '0 12px',
|
||||
padding: '8px',
|
||||
borderRadius: '20px',
|
||||
border: 'none',
|
||||
backgroundColor: '#f0f8ff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
},
|
||||
controlButtons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '20px',
|
||||
marginBottom: '24px',
|
||||
},
|
||||
controlButton: {
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '40px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '32px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
transition: 'transform 0.2s ease',
|
||||
},
|
||||
connectButton: {
|
||||
backgroundColor: isConnected ? '#ff4d4f' : '#52c41a',
|
||||
},
|
||||
translationContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
translationTitle: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
translationItem: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
translationTime: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
translationText: {
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.4',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
originalText: {
|
||||
color: '#333',
|
||||
fontWeight: '500' as const,
|
||||
},
|
||||
translatedText: {
|
||||
color: '#1890ff',
|
||||
},
|
||||
speakerTag: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
fontStyle: 'italic' as const,
|
||||
},
|
||||
warningModal: {
|
||||
position: 'fixed' as const,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
},
|
||||
warningContent: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
margin: '20px',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
warningTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#fa8c16',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
warningText: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
warningButtons: {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
warningButton: (primary: boolean) => ({
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
backgroundColor: primary ? '#1890ff' : '#f0f0f0',
|
||||
color: primary ? 'white' : '#666',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 通话类型选择 */}
|
||||
<div style={styles.header}>
|
||||
<div style={styles.callTypeSelector}>
|
||||
<button
|
||||
style={styles.callTypeButton(callType === CallType.VOICE)}
|
||||
onClick={() => handleCallTypeChange(CallType.VOICE)}
|
||||
disabled={isConnected}
|
||||
>
|
||||
📞 语音通话
|
||||
</button>
|
||||
<button
|
||||
style={styles.callTypeButton(callType === CallType.VIDEO)}
|
||||
onClick={() => handleCallTypeChange(CallType.VIDEO)}
|
||||
disabled={isConnected}
|
||||
>
|
||||
📹 视频通话
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={styles.statusRow}>
|
||||
<div style={styles.statusIndicator}>
|
||||
<div style={styles.statusDot}></div>
|
||||
<span style={styles.statusText}>
|
||||
{isConnected ? '通话中' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.costInfo}>
|
||||
<div style={styles.duration}>{formatDuration(callDuration)}</div>
|
||||
<div style={styles.currentCost}>¥{formatCost(currentCost)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 计费信息 */}
|
||||
<div style={styles.billingInfo}>
|
||||
<div style={styles.billingTitle}>翻译服务选择</div>
|
||||
|
||||
<div style={styles.translationTypeSelector}>
|
||||
{callType === CallType.VOICE && (
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.TEXT)}
|
||||
onClick={() => handleTranslationTypeChange(TranslationType.TEXT)}
|
||||
disabled={isConnected}
|
||||
>
|
||||
<div>💬 实时文字翻译</div>
|
||||
<div style={styles.rateInfo}>
|
||||
{billingRule && `¥${formatCost(billingRule.pricePerMinute)}/分钟`}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{callType === CallType.VIDEO && (
|
||||
<>
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.SIGN_LANGUAGE)}
|
||||
onClick={() => handleTranslationTypeChange(TranslationType.SIGN_LANGUAGE)}
|
||||
disabled={isConnected}
|
||||
>
|
||||
<div>👋 虚拟人手语翻译</div>
|
||||
<div style={styles.rateInfo}>
|
||||
{billingRule && `¥${formatCost(billingRule.pricePerMinute)}/分钟`}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
style={styles.translationTypeButton(translationType === TranslationType.HUMAN_INTERPRETER)}
|
||||
onClick={() => handleTranslationTypeChange(TranslationType.HUMAN_INTERPRETER)}
|
||||
disabled={isConnected}
|
||||
>
|
||||
<div>👨💼 真人翻译员</div>
|
||||
<div style={styles.rateInfo}>
|
||||
{billingRule && `¥${formatCost(billingRule.pricePerMinute)}/分钟 + 翻译员费用`}
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 翻译员选择 */}
|
||||
{translationType === TranslationType.HUMAN_INTERPRETER && (
|
||||
<div style={styles.interpreterSelector}>
|
||||
<div style={styles.billingTitle}>选择翻译员</div>
|
||||
{availableInterpreters.map((interpreter) => (
|
||||
<div
|
||||
key={interpreter.id}
|
||||
style={styles.interpreterItem(selectedInterpreter?.id === interpreter.id)}
|
||||
onClick={() => handleInterpreterSelect(interpreter)}
|
||||
>
|
||||
<div>{interpreter.avatar}</div>
|
||||
<div style={styles.interpreterInfo}>
|
||||
<div style={styles.interpreterName}>{interpreter.name}</div>
|
||||
<div style={styles.interpreterDetails}>
|
||||
{interpreter.languages.join(', ')} | ⭐ {interpreter.rating}
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.interpreterRate}>
|
||||
¥{formatCost(interpreter.pricePerMinute)}/分钟
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 语言选择器 */}
|
||||
<div style={styles.languageSelector}>
|
||||
<div style={styles.languageRow}>
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.from}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, from: e.target.value})}
|
||||
disabled={isConnected}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button style={styles.swapButton} onClick={swapLanguages} disabled={isConnected}>
|
||||
🔄
|
||||
</button>
|
||||
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.to}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, to: e.target.value})}
|
||||
disabled={isConnected}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 控制按钮 */}
|
||||
<div style={styles.controlButtons}>
|
||||
<button
|
||||
style={{...styles.controlButton, ...styles.connectButton}}
|
||||
onClick={isConnected ? handleDisconnect : handleConnect}
|
||||
>
|
||||
{isConnected ? '📞' : (callType === CallType.VIDEO ? '📹' : '📱')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 翻译历史 */}
|
||||
<div style={styles.translationContainer}>
|
||||
<h3 style={styles.translationTitle}>
|
||||
{translationType === TranslationType.TEXT ? '实时翻译' :
|
||||
translationType === TranslationType.SIGN_LANGUAGE ? '手语翻译' : '翻译员服务'}
|
||||
</h3>
|
||||
{translationHistory.map((item, index) => (
|
||||
<div key={index} style={styles.translationItem}>
|
||||
<div style={styles.translationTime}>{item.time}</div>
|
||||
<div style={{...styles.translationText, ...styles.originalText}}>
|
||||
{item.original}
|
||||
</div>
|
||||
<div style={{...styles.translationText, ...styles.translatedText}}>
|
||||
{item.translated}
|
||||
</div>
|
||||
<div style={styles.speakerTag}>
|
||||
{item.speaker === 'you' ? '您' : '对方'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 低余额警告 */}
|
||||
{showLowBalanceWarning && (
|
||||
<div style={styles.warningModal}>
|
||||
<div style={styles.warningContent}>
|
||||
<div style={styles.warningTitle}>⚠️ 余额不足警告</div>
|
||||
<div style={styles.warningText}>
|
||||
您的账户余额即将不足,可能无法维持通话超过5分钟。
|
||||
建议立即充值以避免通话中断。
|
||||
</div>
|
||||
<div style={styles.warningButtons}>
|
||||
<button
|
||||
style={styles.warningButton(false)}
|
||||
onClick={() => setShowLowBalanceWarning(false)}
|
||||
>
|
||||
继续通话
|
||||
</button>
|
||||
<button
|
||||
style={styles.warningButton(true)}
|
||||
onClick={() => {
|
||||
setShowLowBalanceWarning(false);
|
||||
navigate('/mobile/recharge');
|
||||
}}
|
||||
>
|
||||
立即充值
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileCall;
|
||||
@@ -0,0 +1,407 @@
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
name: string;
|
||||
size: string;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
progress: number;
|
||||
originalLanguage: string;
|
||||
targetLanguage: string;
|
||||
uploadTime: string;
|
||||
}
|
||||
|
||||
const MobileDocuments: FC = () => {
|
||||
const [documents, setDocuments] = useState<Document[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: '商业合同.pdf',
|
||||
size: '2.3 MB',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
originalLanguage: 'zh-CN',
|
||||
targetLanguage: 'en-US',
|
||||
uploadTime: '2024-01-15 14:30',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '技术文档.docx',
|
||||
size: '1.8 MB',
|
||||
status: 'processing',
|
||||
progress: 65,
|
||||
originalLanguage: 'en-US',
|
||||
targetLanguage: 'zh-CN',
|
||||
uploadTime: '2024-01-15 15:20',
|
||||
},
|
||||
]);
|
||||
|
||||
const [selectedLanguages, setSelectedLanguages] = useState({
|
||||
from: 'zh-CN',
|
||||
to: 'en-US',
|
||||
});
|
||||
|
||||
const languages = [
|
||||
{ code: 'zh-CN', name: '中文', flag: '🇨🇳' },
|
||||
{ code: 'en-US', name: 'English', flag: '🇺🇸' },
|
||||
{ code: 'ja-JP', name: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'ko-KR', name: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'es-ES', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr-FR', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de-DE', name: 'Deutsch', flag: '🇩🇪' },
|
||||
];
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
const newDocument: Document = {
|
||||
id: Date.now().toString(),
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(1)} MB`,
|
||||
status: 'uploading',
|
||||
progress: 0,
|
||||
originalLanguage: selectedLanguages.from,
|
||||
targetLanguage: selectedLanguages.to,
|
||||
uploadTime: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
|
||||
setDocuments(prev => [newDocument, ...prev]);
|
||||
|
||||
// 模拟上传进度
|
||||
simulateUpload(newDocument.id);
|
||||
}
|
||||
};
|
||||
|
||||
const simulateUpload = (docId: string) => {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 15;
|
||||
if (progress >= 100) {
|
||||
progress = 100;
|
||||
setDocuments(prev => prev.map(doc =>
|
||||
doc.id === docId
|
||||
? { ...doc, status: 'processing', progress: 0 }
|
||||
: doc
|
||||
));
|
||||
clearInterval(interval);
|
||||
simulateProcessing(docId);
|
||||
} else {
|
||||
setDocuments(prev => prev.map(doc =>
|
||||
doc.id === docId
|
||||
? { ...doc, progress: Math.floor(progress) }
|
||||
: doc
|
||||
));
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const simulateProcessing = (docId: string) => {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 10;
|
||||
if (progress >= 100) {
|
||||
progress = 100;
|
||||
setDocuments(prev => prev.map(doc =>
|
||||
doc.id === docId
|
||||
? { ...doc, status: 'completed', progress: 100 }
|
||||
: doc
|
||||
));
|
||||
clearInterval(interval);
|
||||
} else {
|
||||
setDocuments(prev => prev.map(doc =>
|
||||
doc.id === docId
|
||||
? { ...doc, progress: Math.floor(progress) }
|
||||
: doc
|
||||
));
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const getStatusText = (status: Document['status']) => {
|
||||
switch (status) {
|
||||
case 'uploading': return '上传中';
|
||||
case 'processing': return '翻译中';
|
||||
case 'completed': return '已完成';
|
||||
case 'failed': return '失败';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: Document['status']) => {
|
||||
switch (status) {
|
||||
case 'uploading': return '#1890ff';
|
||||
case 'processing': return '#fa8c16';
|
||||
case 'completed': return '#52c41a';
|
||||
case 'failed': return '#ff4d4f';
|
||||
default: return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
const swapLanguages = () => {
|
||||
setSelectedLanguages({
|
||||
from: selectedLanguages.to,
|
||||
to: selectedLanguages.from,
|
||||
});
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100%',
|
||||
},
|
||||
uploadSection: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
uploadTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
languageSelector: {
|
||||
marginBottom: '20px',
|
||||
},
|
||||
languageRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
languageSelect: {
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
backgroundColor: 'white',
|
||||
fontSize: '14px',
|
||||
},
|
||||
swapButton: {
|
||||
margin: '0 12px',
|
||||
padding: '8px',
|
||||
borderRadius: '20px',
|
||||
border: 'none',
|
||||
backgroundColor: '#f0f8ff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
},
|
||||
uploadArea: {
|
||||
border: '2px dashed #d9d9d9',
|
||||
borderRadius: '8px',
|
||||
padding: '40px 20px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#fafafa',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 0.3s ease',
|
||||
},
|
||||
uploadIcon: {
|
||||
fontSize: '48px',
|
||||
marginBottom: '12px',
|
||||
color: '#1890ff',
|
||||
},
|
||||
uploadText: {
|
||||
fontSize: '16px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
uploadSubtext: {
|
||||
fontSize: '14px',
|
||||
color: '#999',
|
||||
},
|
||||
hiddenInput: {
|
||||
display: 'none',
|
||||
},
|
||||
documentsSection: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
documentItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
documentIcon: {
|
||||
fontSize: '24px',
|
||||
marginRight: '12px',
|
||||
},
|
||||
documentInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
documentName: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
documentDetails: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
documentLanguages: {
|
||||
fontSize: '12px',
|
||||
color: '#1890ff',
|
||||
},
|
||||
documentStatus: {
|
||||
textAlign: 'right' as const,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: '12px',
|
||||
fontWeight: '500' as const,
|
||||
marginBottom: '4px',
|
||||
},
|
||||
progressBar: {
|
||||
width: '60px',
|
||||
height: '4px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
backgroundColor: '#1890ff',
|
||||
transition: 'width 0.3s ease',
|
||||
},
|
||||
actionButton: {
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
backgroundColor: '#1890ff',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
marginTop: '4px',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 上传区域 */}
|
||||
<div style={styles.uploadSection}>
|
||||
<h2 style={styles.uploadTitle}>📄 文档翻译</h2>
|
||||
|
||||
{/* 语言选择 */}
|
||||
<div style={styles.languageSelector}>
|
||||
<div style={styles.languageRow}>
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.from}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, from: e.target.value})}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button style={styles.swapButton} onClick={swapLanguages}>
|
||||
🔄
|
||||
</button>
|
||||
|
||||
<select
|
||||
style={styles.languageSelect}
|
||||
value={selectedLanguages.to}
|
||||
onChange={(e) => setSelectedLanguages({...selectedLanguages, to: e.target.value})}
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.flag} {lang.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上传区域 */}
|
||||
<label style={styles.uploadArea}>
|
||||
<input
|
||||
type="file"
|
||||
style={styles.hiddenInput}
|
||||
accept=".pdf,.doc,.docx,.txt"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<div style={styles.uploadIcon}>📁</div>
|
||||
<div style={styles.uploadText}>点击上传文档</div>
|
||||
<div style={styles.uploadSubtext}>
|
||||
支持 PDF、DOC、DOCX、TXT 格式
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 文档列表 */}
|
||||
<div style={styles.documentsSection}>
|
||||
<h3 style={styles.sectionTitle}>我的文档</h3>
|
||||
|
||||
{documents.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无文档,请上传您的第一个文档
|
||||
</div>
|
||||
) : (
|
||||
documents.map((doc) => (
|
||||
<div key={doc.id} style={styles.documentItem}>
|
||||
<div style={styles.documentIcon}>
|
||||
{doc.name.endsWith('.pdf') ? '📄' : '📝'}
|
||||
</div>
|
||||
|
||||
<div style={styles.documentInfo}>
|
||||
<div style={styles.documentName}>{doc.name}</div>
|
||||
<div style={styles.documentDetails}>
|
||||
{doc.size} • {doc.uploadTime}
|
||||
</div>
|
||||
<div style={styles.documentLanguages}>
|
||||
{languages.find(l => l.code === doc.originalLanguage)?.flag} → {' '}
|
||||
{languages.find(l => l.code === doc.targetLanguage)?.flag}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.documentStatus}>
|
||||
<div style={{
|
||||
...styles.statusText,
|
||||
color: getStatusColor(doc.status)
|
||||
}}>
|
||||
{getStatusText(doc.status)}
|
||||
</div>
|
||||
|
||||
{doc.status !== 'completed' && (
|
||||
<div style={styles.progressBar}>
|
||||
<div
|
||||
style={{
|
||||
...styles.progressFill,
|
||||
width: `${doc.progress}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{doc.status === 'completed' && (
|
||||
<button style={styles.actionButton}>
|
||||
下载
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileDocuments;
|
||||
@@ -0,0 +1,388 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BillingService } from '../../services/billingService';
|
||||
import { AppointmentService } from '../../services/appointmentService';
|
||||
import { UserType, UserAccount, Appointment } from '../../types/billing';
|
||||
|
||||
const MobileHome: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
||||
const [upcomingAppointments, setUpcomingAppointments] = useState<Appointment[]>([]);
|
||||
const billingService = BillingService.getInstance();
|
||||
const appointmentService = AppointmentService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟用户账户数据
|
||||
const mockAccount: UserAccount = {
|
||||
id: 'user_1',
|
||||
userType: UserType.INDIVIDUAL,
|
||||
balance: 5000, // 50元
|
||||
};
|
||||
|
||||
billingService.setUserAccount(mockAccount);
|
||||
setUserAccount(mockAccount);
|
||||
|
||||
// 获取即将到来的预约
|
||||
const appointments = appointmentService.getUpcomingAppointments('user_1', 3);
|
||||
setUpcomingAppointments(appointments);
|
||||
}, []);
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
id: 1,
|
||||
title: '语音通话',
|
||||
description: '发起实时语音翻译通话',
|
||||
icon: '📞',
|
||||
color: '#52c41a',
|
||||
action: () => navigate('/mobile/call?type=voice'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '视频通话',
|
||||
description: '发起视频通话和手语翻译',
|
||||
icon: '📹',
|
||||
color: '#1890ff',
|
||||
action: () => navigate('/mobile/call?type=video'),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '预约通话',
|
||||
description: '预约专业翻译员服务',
|
||||
icon: '📅',
|
||||
color: '#722ed1',
|
||||
action: () => navigate('/mobile/appointment'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '充值',
|
||||
description: '账户余额充值',
|
||||
icon: '💰',
|
||||
color: '#fa8c16',
|
||||
action: () => navigate('/mobile/recharge'),
|
||||
},
|
||||
];
|
||||
|
||||
const recentActivities = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'call',
|
||||
title: '与 John Smith 的通话',
|
||||
time: '2小时前',
|
||||
status: '已完成',
|
||||
cost: '¥15.50',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'appointment',
|
||||
title: '商务会议翻译',
|
||||
time: '明天 14:00',
|
||||
status: '已预约',
|
||||
cost: '¥180.00',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'call',
|
||||
title: '医疗咨询翻译',
|
||||
time: '2天前',
|
||||
status: '已完成',
|
||||
cost: '¥32.00',
|
||||
},
|
||||
];
|
||||
|
||||
const formatBalance = (balance: number) => {
|
||||
return (balance / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const getActivityIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'call': return '📞';
|
||||
case 'appointment': return '📅';
|
||||
case 'recharge': return '💰';
|
||||
default: return '📋';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case '已完成': return '#52c41a';
|
||||
case '已预约': return '#1890ff';
|
||||
case '进行中': return '#fa8c16';
|
||||
case '已取消': return '#ff4d4f';
|
||||
default: return '#666';
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100%',
|
||||
},
|
||||
balanceCard: {
|
||||
backgroundColor: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
color: 'white',
|
||||
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
|
||||
},
|
||||
balanceTitle: {
|
||||
fontSize: '14px',
|
||||
opacity: 0.9,
|
||||
marginBottom: '8px',
|
||||
},
|
||||
balanceAmount: {
|
||||
fontSize: '32px',
|
||||
fontWeight: '700' as const,
|
||||
marginBottom: '4px',
|
||||
},
|
||||
balanceSubtitle: {
|
||||
fontSize: '12px',
|
||||
opacity: 0.8,
|
||||
},
|
||||
userTypeTag: {
|
||||
position: 'absolute' as const,
|
||||
top: '16px',
|
||||
right: '16px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500' as const,
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
welcomeTitle: {
|
||||
fontSize: '24px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#1890ff',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
welcomeSubtitle: {
|
||||
fontSize: '16px',
|
||||
color: '#666',
|
||||
marginBottom: '0',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
marginTop: '24px',
|
||||
},
|
||||
quickActionsGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '12px',
|
||||
marginBottom: '24px',
|
||||
},
|
||||
actionCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
textAlign: 'center' as const,
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
position: 'relative' as const,
|
||||
},
|
||||
actionIcon: {
|
||||
fontSize: '32px',
|
||||
marginBottom: '8px',
|
||||
display: 'block',
|
||||
},
|
||||
actionTitle: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
actionDescription: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
lineHeight: '1.4',
|
||||
},
|
||||
appointmentPreview: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
appointmentTitle: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '12px',
|
||||
},
|
||||
appointmentItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '8px 0',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
appointmentInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
appointmentName: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
marginBottom: '2px',
|
||||
},
|
||||
appointmentTime: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
},
|
||||
appointmentStatus: {
|
||||
fontSize: '12px',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
color: '#1890ff',
|
||||
},
|
||||
activityList: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
activityItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 0',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
activityIcon: {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '20px',
|
||||
backgroundColor: '#f0f8ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: '12px',
|
||||
fontSize: '18px',
|
||||
},
|
||||
activityContent: {
|
||||
flex: 1,
|
||||
},
|
||||
activityTitle: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
activityTime: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
},
|
||||
activityRight: {
|
||||
textAlign: 'right' as const,
|
||||
},
|
||||
activityStatus: {
|
||||
fontSize: '12px',
|
||||
fontWeight: '500' as const,
|
||||
marginBottom: '4px',
|
||||
},
|
||||
activityCost: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
},
|
||||
};
|
||||
|
||||
const handleActionPress = (action: () => void) => {
|
||||
action();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 余额卡片 */}
|
||||
{userAccount && (
|
||||
<div style={styles.balanceCard}>
|
||||
<div style={styles.userTypeTag}>
|
||||
{userAccount.userType === UserType.INDIVIDUAL ? '个人用户' : '企业用户'}
|
||||
</div>
|
||||
<div style={styles.balanceTitle}>账户余额</div>
|
||||
<div style={styles.balanceAmount}>¥{formatBalance(userAccount.balance)}</div>
|
||||
<div style={styles.balanceSubtitle}>可用于通话和翻译服务</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<h2 style={styles.sectionTitle}>快捷操作</h2>
|
||||
<div style={styles.quickActionsGrid}>
|
||||
{quickActions.map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
style={styles.actionCard}
|
||||
onClick={() => handleActionPress(action.action)}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
|
||||
}}
|
||||
>
|
||||
<span style={styles.actionIcon}>{action.icon}</span>
|
||||
<div style={styles.actionTitle}>{action.title}</div>
|
||||
<div style={styles.actionDescription}>{action.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 即将到来的预约 */}
|
||||
{upcomingAppointments.length > 0 && (
|
||||
<div style={styles.appointmentPreview}>
|
||||
<div style={styles.appointmentTitle}>即将到来的预约</div>
|
||||
{upcomingAppointments.map((appointment) => (
|
||||
<div key={appointment.id} style={styles.appointmentItem}>
|
||||
<div style={styles.appointmentInfo}>
|
||||
<div style={styles.appointmentName}>{appointment.title}</div>
|
||||
<div style={styles.appointmentTime}>
|
||||
{appointment.scheduledTime.toLocaleDateString()} {appointment.scheduledTime.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.appointmentStatus}>{appointment.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 最近活动 */}
|
||||
<h2 style={styles.sectionTitle}>最近活动</h2>
|
||||
<div style={styles.activityList}>
|
||||
{recentActivities.map((activity) => (
|
||||
<div key={activity.id} style={styles.activityItem}>
|
||||
<div style={styles.activityIcon}>
|
||||
{getActivityIcon(activity.type)}
|
||||
</div>
|
||||
<div style={styles.activityContent}>
|
||||
<div style={styles.activityTitle}>{activity.title}</div>
|
||||
<div style={styles.activityTime}>{activity.time}</div>
|
||||
</div>
|
||||
<div style={styles.activityRight}>
|
||||
<div style={{...styles.activityStatus, color: getStatusColor(activity.status)}}>
|
||||
{activity.status}
|
||||
</div>
|
||||
<div style={styles.activityCost}>{activity.cost}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileHome;
|
||||
@@ -0,0 +1,497 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BillingService } from '../../services/billingService';
|
||||
import { UserAccount, UserType, RechargeRecord } from '../../types/billing';
|
||||
|
||||
const MobileRecharge: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const billingService = BillingService.getInstance();
|
||||
|
||||
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
||||
const [selectedAmount, setSelectedAmount] = useState<number>(0);
|
||||
const [customAmount, setCustomAmount] = useState<string>('');
|
||||
const [selectedPayment, setSelectedPayment] = useState<'wechat' | 'alipay' | 'card'>('wechat');
|
||||
const [rechargeHistory, setRechargeHistory] = useState<RechargeRecord[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// 预设充值金额(分)
|
||||
const presetAmounts = [
|
||||
{ value: 5000, label: '¥50', bonus: 0, popular: false },
|
||||
{ value: 10000, label: '¥100', bonus: 500, popular: true },
|
||||
{ value: 20000, label: '¥200', bonus: 1500, popular: false },
|
||||
{ value: 50000, label: '¥500', bonus: 5000, popular: false },
|
||||
{ value: 100000, label: '¥1000', bonus: 15000, popular: false },
|
||||
];
|
||||
|
||||
const paymentMethods = [
|
||||
{ id: 'wechat', name: '微信支付', icon: '💚', color: '#07c160' },
|
||||
{ id: 'alipay', name: '支付宝', icon: '💙', color: '#1677ff' },
|
||||
{ id: 'card', name: '银行卡', icon: '💳', color: '#722ed1' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const account = billingService.getUserAccount();
|
||||
setUserAccount(account);
|
||||
|
||||
// 模拟充值历史
|
||||
const mockHistory: RechargeRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
userId: account?.id || '1',
|
||||
amount: 10000,
|
||||
bonus: 500,
|
||||
paymentMethod: 'wechat',
|
||||
status: 'completed',
|
||||
createdAt: new Date(Date.now() - 86400000), // 1天前
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
userId: account?.id || '1',
|
||||
amount: 5000,
|
||||
bonus: 0,
|
||||
paymentMethod: 'alipay',
|
||||
status: 'completed',
|
||||
createdAt: new Date(Date.now() - 7 * 86400000), // 7天前
|
||||
},
|
||||
];
|
||||
setRechargeHistory(mockHistory);
|
||||
}, []);
|
||||
|
||||
const formatCurrency = (cents: number) => {
|
||||
return (cents / 100).toFixed(2);
|
||||
};
|
||||
|
||||
const handleAmountSelect = (amount: number) => {
|
||||
setSelectedAmount(amount);
|
||||
setCustomAmount('');
|
||||
};
|
||||
|
||||
const handleCustomAmountChange = (value: string) => {
|
||||
const numValue = parseFloat(value);
|
||||
if (!isNaN(numValue) && numValue > 0) {
|
||||
setSelectedAmount(Math.round(numValue * 100));
|
||||
setCustomAmount(value);
|
||||
} else {
|
||||
setSelectedAmount(0);
|
||||
setCustomAmount(value);
|
||||
}
|
||||
};
|
||||
|
||||
const getBonus = (amount: number) => {
|
||||
const preset = presetAmounts.find(p => p.value === amount);
|
||||
return preset?.bonus || 0;
|
||||
};
|
||||
|
||||
const getTotalAmount = () => {
|
||||
return selectedAmount + getBonus(selectedAmount);
|
||||
};
|
||||
|
||||
const handleRecharge = async () => {
|
||||
if (selectedAmount < 100) { // 最低1元
|
||||
alert('充值金额不能少于1元');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userAccount) {
|
||||
alert('用户账户信息未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
// 模拟支付处理
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// 执行充值
|
||||
const success = billingService.rechargeAccount(selectedAmount);
|
||||
|
||||
if (success) {
|
||||
// 更新用户账户
|
||||
setUserAccount(billingService.getUserAccount());
|
||||
|
||||
// 添加充值记录
|
||||
const newRecord: RechargeRecord = {
|
||||
id: Date.now().toString(),
|
||||
userId: userAccount.id,
|
||||
amount: selectedAmount,
|
||||
bonus: getBonus(selectedAmount),
|
||||
paymentMethod: selectedPayment,
|
||||
status: 'completed',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
setRechargeHistory(prev => [newRecord, ...prev]);
|
||||
|
||||
alert(`充值成功!到账金额:¥${formatCurrency(getTotalAmount())}`);
|
||||
setSelectedAmount(0);
|
||||
setCustomAmount('');
|
||||
} else {
|
||||
alert('充值失败,请重试');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('充值失败,请重试');
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return '#52c41a';
|
||||
case 'pending':
|
||||
return '#fa8c16';
|
||||
case 'failed':
|
||||
return '#ff4d4f';
|
||||
default:
|
||||
return '#d9d9d9';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return '成功';
|
||||
case 'pending':
|
||||
return '处理中';
|
||||
case 'failed':
|
||||
return '失败';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
padding: '16px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
backButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '12px',
|
||||
},
|
||||
title: {
|
||||
fontSize: '20px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
},
|
||||
balanceCard: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
textAlign: 'center' as const,
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
balanceLabel: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
balanceAmount: {
|
||||
fontSize: '32px',
|
||||
fontWeight: '700' as const,
|
||||
color: '#1890ff',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
userType: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
backgroundColor: '#f0f8ff',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '12px',
|
||||
display: 'inline-block',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
amountGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
amountCard: (selected: boolean, popular: boolean) => ({
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: selected ? '2px solid #1890ff' : '2px solid transparent',
|
||||
backgroundColor: selected ? '#e6f7ff' : '#fafafa',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center' as const,
|
||||
position: 'relative' as const,
|
||||
...(popular && {
|
||||
borderColor: '#fa8c16',
|
||||
backgroundColor: '#fff7e6',
|
||||
}),
|
||||
}),
|
||||
popularBadge: {
|
||||
position: 'absolute' as const,
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
backgroundColor: '#fa8c16',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: '500' as const,
|
||||
},
|
||||
amountValue: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
bonusText: {
|
||||
fontSize: '12px',
|
||||
color: '#52c41a',
|
||||
fontWeight: '500' as const,
|
||||
},
|
||||
customAmountInput: {
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #d9d9d9',
|
||||
fontSize: '16px',
|
||||
marginBottom: '16px',
|
||||
},
|
||||
paymentMethods: {
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
paymentMethod: (selected: boolean, color: string) => ({
|
||||
flex: 1,
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: selected ? `2px solid ${color}` : '2px solid #f0f0f0',
|
||||
backgroundColor: selected ? `${color}10` : 'white',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center' as const,
|
||||
}),
|
||||
paymentIcon: {
|
||||
fontSize: '24px',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
paymentName: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
},
|
||||
summaryCard: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
summaryRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: '14px',
|
||||
fontWeight: '500' as const,
|
||||
color: '#333',
|
||||
},
|
||||
totalRow: {
|
||||
borderTop: '1px solid #e8e8e8',
|
||||
paddingTop: '8px',
|
||||
marginTop: '8px',
|
||||
},
|
||||
totalValue: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#1890ff',
|
||||
},
|
||||
rechargeButton: {
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: selectedAmount > 0 && !isProcessing ? '#1890ff' : '#d9d9d9',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
cursor: selectedAmount > 0 && !isProcessing ? 'pointer' : 'not-allowed',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
historyItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '16px 0',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
historyLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
historyAmount: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
historyDate: {
|
||||
fontSize: '12px',
|
||||
color: '#999',
|
||||
},
|
||||
historyStatus: (status: string) => ({
|
||||
fontSize: '12px',
|
||||
fontWeight: '500' as const,
|
||||
color: getStatusColor(status),
|
||||
backgroundColor: `${getStatusColor(status)}15`,
|
||||
padding: '2px 8px',
|
||||
borderRadius: '8px',
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 头部 */}
|
||||
<div style={styles.header}>
|
||||
<button style={styles.backButton} onClick={() => navigate(-1)}>
|
||||
←
|
||||
</button>
|
||||
<div style={styles.title}>账户充值</div>
|
||||
</div>
|
||||
|
||||
{/* 余额显示 */}
|
||||
<div style={styles.balanceCard}>
|
||||
<div style={styles.balanceLabel}>当前余额</div>
|
||||
<div style={styles.balanceAmount}>
|
||||
¥{userAccount ? formatCurrency(userAccount.balance) : '0.00'}
|
||||
</div>
|
||||
<div style={styles.userType}>
|
||||
{userAccount?.userType === UserType.ENTERPRISE ? '企业用户' : '个人用户'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 充值金额选择 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择充值金额</div>
|
||||
<div style={styles.amountGrid}>
|
||||
{presetAmounts.map((amount) => (
|
||||
<div
|
||||
key={amount.value}
|
||||
style={styles.amountCard(selectedAmount === amount.value, amount.popular)}
|
||||
onClick={() => handleAmountSelect(amount.value)}
|
||||
>
|
||||
{amount.popular && <div style={styles.popularBadge}>推荐</div>}
|
||||
<div style={styles.amountValue}>{amount.label}</div>
|
||||
{amount.bonus > 0 && (
|
||||
<div style={styles.bonusText}>
|
||||
送¥{formatCurrency(amount.bonus)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="自定义金额(元)"
|
||||
style={styles.customAmountInput}
|
||||
value={customAmount}
|
||||
onChange={(e) => handleCustomAmountChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 支付方式 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>选择支付方式</div>
|
||||
<div style={styles.paymentMethods}>
|
||||
{paymentMethods.map((method) => (
|
||||
<div
|
||||
key={method.id}
|
||||
style={styles.paymentMethod(
|
||||
selectedPayment === method.id,
|
||||
method.color
|
||||
)}
|
||||
onClick={() => setSelectedPayment(method.id as any)}
|
||||
>
|
||||
<div style={styles.paymentIcon}>{method.icon}</div>
|
||||
<div style={styles.paymentName}>{method.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 费用明细 */}
|
||||
{selectedAmount > 0 && (
|
||||
<div style={styles.summaryCard}>
|
||||
<div style={styles.summaryRow}>
|
||||
<span style={styles.summaryLabel}>充值金额</span>
|
||||
<span style={styles.summaryValue}>¥{formatCurrency(selectedAmount)}</span>
|
||||
</div>
|
||||
{getBonus(selectedAmount) > 0 && (
|
||||
<div style={styles.summaryRow}>
|
||||
<span style={styles.summaryLabel}>赠送金额</span>
|
||||
<span style={{...styles.summaryValue, color: '#52c41a'}}>
|
||||
+¥{formatCurrency(getBonus(selectedAmount))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{...styles.summaryRow, ...styles.totalRow}}>
|
||||
<span style={styles.summaryLabel}>到账金额</span>
|
||||
<span style={styles.totalValue}>¥{formatCurrency(getTotalAmount())}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 充值按钮 */}
|
||||
<button
|
||||
style={styles.rechargeButton}
|
||||
onClick={handleRecharge}
|
||||
disabled={selectedAmount === 0 || isProcessing}
|
||||
>
|
||||
{isProcessing ? '处理中...' : `确认充值 ¥${formatCurrency(selectedAmount)}`}
|
||||
</button>
|
||||
|
||||
{/* 充值历史 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>充值记录</div>
|
||||
{rechargeHistory.map((record) => (
|
||||
<div key={record.id} style={styles.historyItem}>
|
||||
<div style={styles.historyLeft}>
|
||||
<div style={styles.historyAmount}>
|
||||
+¥{formatCurrency(record.amount + record.bonus)}
|
||||
</div>
|
||||
<div style={styles.historyDate}>
|
||||
{record.createdAt.toLocaleDateString()} {record.createdAt.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.historyStatus(record.status)}>
|
||||
{getStatusText(record.status)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileRecharge;
|
||||
@@ -0,0 +1,357 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BillingService } from '../../services/billingService';
|
||||
import { UserAccount, UserType } from '../../types/billing';
|
||||
|
||||
const MobileSettings: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const billingService = BillingService.getInstance();
|
||||
|
||||
const [userAccount, setUserAccount] = useState<UserAccount | null>(null);
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
callReminder: true,
|
||||
balanceAlert: true,
|
||||
promotions: false,
|
||||
});
|
||||
const [languageSettings, setLanguageSettings] = useState({
|
||||
interface: 'zh-CN',
|
||||
defaultSource: 'zh-CN',
|
||||
defaultTarget: 'en-US',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const account = billingService.getUserAccount();
|
||||
setUserAccount(account);
|
||||
}, []);
|
||||
|
||||
const formatCurrency = (cents: number) => {
|
||||
return `¥${(cents / 100).toFixed(2)}`;
|
||||
};
|
||||
|
||||
const getUserTypeText = (userType: UserType) => {
|
||||
return userType === UserType.INDIVIDUAL ? '个人用户' : '企业用户';
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
const confirmed = confirm('确定要退出登录吗?');
|
||||
if (confirmed) {
|
||||
// 这里应该清除用户登录状态
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
const settingsOptions = [
|
||||
{
|
||||
category: '账户信息',
|
||||
items: [
|
||||
{
|
||||
label: '个人资料',
|
||||
value: userAccount?.id || '未设置',
|
||||
icon: '👤',
|
||||
onClick: () => navigate('/mobile/profile'),
|
||||
},
|
||||
{
|
||||
label: '账户类型',
|
||||
value: userAccount ? getUserTypeText(userAccount.userType) : '未知',
|
||||
icon: '🏷️',
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
label: '账户余额',
|
||||
value: userAccount ? formatCurrency(userAccount.balance) : '¥0.00',
|
||||
icon: '💰',
|
||||
onClick: () => navigate('/mobile/recharge'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '通话设置',
|
||||
items: [
|
||||
{
|
||||
label: '默认通话类型',
|
||||
value: '语音通话',
|
||||
icon: '📞',
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
label: '音质设置',
|
||||
value: '高清',
|
||||
icon: '🎵',
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
label: '自动录音',
|
||||
value: '关闭',
|
||||
icon: '🎙️',
|
||||
onClick: () => {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '翻译设置',
|
||||
items: [
|
||||
{
|
||||
label: '默认源语言',
|
||||
value: '中文',
|
||||
icon: '🌐',
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
label: '默认目标语言',
|
||||
value: 'English',
|
||||
icon: '🌍',
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
label: '翻译历史',
|
||||
value: '查看记录',
|
||||
icon: '📝',
|
||||
onClick: () => navigate('/mobile/translation-history'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '通知设置',
|
||||
items: [
|
||||
{
|
||||
label: '通话提醒',
|
||||
value: notificationSettings.callReminder ? '开启' : '关闭',
|
||||
icon: '🔔',
|
||||
onClick: () => setNotificationSettings(prev => ({
|
||||
...prev,
|
||||
callReminder: !prev.callReminder
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: '余额提醒',
|
||||
value: notificationSettings.balanceAlert ? '开启' : '关闭',
|
||||
icon: '⚠️',
|
||||
onClick: () => setNotificationSettings(prev => ({
|
||||
...prev,
|
||||
balanceAlert: !prev.balanceAlert
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: '推广消息',
|
||||
value: notificationSettings.promotions ? '开启' : '关闭',
|
||||
icon: '📢',
|
||||
onClick: () => setNotificationSettings(prev => ({
|
||||
...prev,
|
||||
promotions: !prev.promotions
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: '其他',
|
||||
items: [
|
||||
{
|
||||
label: '帮助中心',
|
||||
value: '',
|
||||
icon: '❓',
|
||||
onClick: () => navigate('/mobile/help'),
|
||||
},
|
||||
{
|
||||
label: '意见反馈',
|
||||
value: '',
|
||||
icon: '💬',
|
||||
onClick: () => navigate('/mobile/feedback'),
|
||||
},
|
||||
{
|
||||
label: '关于我们',
|
||||
value: 'v1.0.0',
|
||||
icon: 'ℹ️',
|
||||
onClick: () => navigate('/mobile/about'),
|
||||
},
|
||||
{
|
||||
label: '退出登录',
|
||||
value: '',
|
||||
icon: '🚪',
|
||||
onClick: handleLogout,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh',
|
||||
paddingBottom: '80px',
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'white',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
backButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '12px',
|
||||
},
|
||||
title: {
|
||||
fontSize: '20px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
},
|
||||
userCard: {
|
||||
backgroundColor: 'white',
|
||||
margin: '16px',
|
||||
borderRadius: '16px',
|
||||
padding: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
avatar: {
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#1890ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
marginRight: '16px',
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
userName: {
|
||||
fontSize: '18px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '4px',
|
||||
},
|
||||
userType: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
balance: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#1890ff',
|
||||
},
|
||||
section: {
|
||||
margin: '16px',
|
||||
marginTop: '8px',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: '16px',
|
||||
fontWeight: '600' as const,
|
||||
color: '#333',
|
||||
marginBottom: '12px',
|
||||
paddingLeft: '4px',
|
||||
},
|
||||
settingsGroup: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
},
|
||||
settingItem: {
|
||||
padding: '16px 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid #f8f8f8',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s',
|
||||
},
|
||||
settingItemLast: {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
settingIcon: {
|
||||
fontSize: '20px',
|
||||
marginRight: '12px',
|
||||
width: '24px',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
settingContent: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
settingLabel: {
|
||||
fontSize: '16px',
|
||||
color: '#333',
|
||||
},
|
||||
settingValue: {
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
},
|
||||
arrow: {
|
||||
fontSize: '12px',
|
||||
color: '#ccc',
|
||||
marginLeft: '8px',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 头部 */}
|
||||
<div style={styles.header}>
|
||||
<button style={styles.backButton} onClick={() => navigate(-1)}>
|
||||
←
|
||||
</button>
|
||||
<h1 style={styles.title}>设置</h1>
|
||||
</div>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
<div style={styles.userCard}>
|
||||
<div style={styles.avatar}>
|
||||
👤
|
||||
</div>
|
||||
<div style={styles.userInfo}>
|
||||
<div style={styles.userName}>
|
||||
{userAccount?.id || '用户'}
|
||||
</div>
|
||||
<div style={styles.userType}>
|
||||
{userAccount ? getUserTypeText(userAccount.userType) : '未知类型'}
|
||||
</div>
|
||||
<div style={styles.balance}>
|
||||
余额: {userAccount ? formatCurrency(userAccount.balance) : '¥0.00'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设置选项 */}
|
||||
{settingsOptions.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex} style={styles.section}>
|
||||
<div style={styles.sectionTitle}>{section.category}</div>
|
||||
<div style={styles.settingsGroup}>
|
||||
{section.items.map((item, itemIndex) => (
|
||||
<div
|
||||
key={itemIndex}
|
||||
style={{
|
||||
...styles.settingItem,
|
||||
...(itemIndex === section.items.length - 1 ? styles.settingItemLast : {}),
|
||||
}}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div style={styles.settingIcon}>{item.icon}</div>
|
||||
<div style={styles.settingContent}>
|
||||
<div style={styles.settingLabel}>{item.label}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{item.value && (
|
||||
<div style={styles.settingValue}>{item.value}</div>
|
||||
)}
|
||||
<div style={styles.arrow}>›</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileSettings;
|
||||
Reference in New Issue
Block a user