重构移动端和管理端应用,整合路由和页面组件,优化样式,添加新功能和页面,更新API配置,提升用户体验。

This commit is contained in:
2025-06-28 20:33:38 +08:00
parent 240dd5d2a4
commit deb2900acc
25 changed files with 4900 additions and 373 deletions
+1
View File
@@ -17,6 +17,7 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"antd": "^5.0.0",
"dayjs": "^1.11.13",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+1
View File
@@ -12,6 +12,7 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"antd": "^5.0.0",
"dayjs": "^1.11.13",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+128 -79
View File
@@ -1,11 +1,15 @@
import React, { useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { Layout, Menu, ConfigProvider } from 'antd';
import {
DashboardOutlined,
PhoneOutlined,
FileTextOutlined,
CalendarOutlined
import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';
import { Layout, Menu, Typography, ConfigProvider } from 'antd';
import {
DashboardOutlined,
PhoneOutlined,
FileTextOutlined,
CalendarOutlined,
UserOutlined,
TeamOutlined,
DollarOutlined,
SettingOutlined
} from '@ant-design/icons';
import zhCN from 'antd/locale/zh_CN';
import 'antd/dist/reset.css';
@@ -13,114 +17,159 @@ import './App.css';
// 导入页面组件
import Dashboard from './pages/Dashboard';
import CallList from './pages/Calls/CallList';
import CallDetail from './pages/Calls/CallDetail';
import DocumentList from './pages/Documents/DocumentList';
import DocumentDetail from './pages/Documents/DocumentDetail';
import AppointmentList from './pages/Appointments/AppointmentList';
import AppointmentDetail from './pages/Appointments/AppointmentDetail';
import UserList from './pages/Users/UserList';
import TranslatorList from './pages/Translators/TranslatorList';
import PaymentList from './pages/Payments/PaymentList';
import SystemSettings from './pages/Settings/SystemSettings';
const { Header, Sider, Content } = Layout;
const { Title } = Typography;
const AppContent: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const [selectedKey, setSelectedKey] = useState('1');
const handleMenuClick = (e: any) => {
setSelectedKey(e.key);
switch (e.key) {
case '1':
navigate('/dashboard');
const handleMenuClick = ({ key }: { key: string }) => {
switch (key) {
case 'dashboard':
navigate('/');
break;
case '2':
navigate('/calls/1');
case 'calls':
navigate('/calls');
break;
case '3':
navigate('/documents/1');
case 'documents':
navigate('/documents');
break;
case '4':
navigate('/appointments/1');
case 'appointments':
navigate('/appointments');
break;
case 'users':
navigate('/users');
break;
case 'translators':
navigate('/translators');
break;
case 'payments':
navigate('/payments');
break;
case 'settings':
navigate('/settings');
break;
default:
navigate('/');
}
};
const menuItems = [
{
key: 'dashboard',
icon: <DashboardOutlined />,
label: '仪表板',
},
{
key: 'calls',
icon: <PhoneOutlined />,
label: '通话记录',
},
{
key: 'documents',
icon: <FileTextOutlined />,
label: '文档翻译',
},
{
key: 'appointments',
icon: <CalendarOutlined />,
label: '预约管理',
},
{
key: 'users',
icon: <UserOutlined />,
label: '用户管理',
},
{
key: 'translators',
icon: <TeamOutlined />,
label: '译员管理',
},
{
key: 'payments',
icon: <DollarOutlined />,
label: '支付记录',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置',
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
breakpoint="lg"
collapsedWidth="0"
style={{
background: '#001529',
}}
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
theme="dark"
width={250}
>
<div style={{
height: 32,
margin: 16,
background: 'rgba(255,255,255,.2)',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
height: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '18px',
fontWeight: 'bold'
}}>
Twilio管理系统
{collapsed ? 'T' : 'Twilio管理后台'}
</div>
<Menu
theme="dark"
defaultSelectedKeys={['dashboard']}
mode="inline"
selectedKeys={[selectedKey]}
items={menuItems}
onClick={handleMenuClick}
items={[
{
key: '1',
icon: <DashboardOutlined />,
label: '仪表板',
},
{
key: '2',
icon: <PhoneOutlined />,
label: '通话管理',
},
{
key: '3',
icon: <FileTextOutlined />,
label: '文档翻译',
},
{
key: '4',
icon: <CalendarOutlined />,
label: '预约管理',
},
]}
/>
</Sider>
<Layout>
<Header style={{
padding: 0,
background: '#fff',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
padding: '0 24px',
background: '#fff',
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #f0f0f0'
}}>
<div style={{
padding: '0 24px',
fontSize: '18px',
fontWeight: 'bold'
}}>
Twilio翻译服务管理后台
</div>
<Title level={4} style={{ margin: 0 }}>
Twilio翻译服务管理系统
</Title>
</Header>
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
<div style={{
padding: 24,
background: '#fff',
minHeight: 360,
borderRadius: 8
}}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/calls/:id" element={<CallDetail />} />
<Route path="/documents/:id" element={<DocumentDetail />} />
<Route path="/appointments/:id" element={<AppointmentDetail />} />
</Routes>
</div>
<Content style={{
margin: '0',
background: '#f0f2f5',
minHeight: 'calc(100vh - 64px)'
}}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/calls" element={<CallList />} />
<Route path="/calls/:id" element={<CallDetail />} />
<Route path="/documents" element={<DocumentList />} />
<Route path="/documents/:id" element={<DocumentDetail />} />
<Route path="/appointments" element={<AppointmentList />} />
<Route path="/appointments/:id" element={<AppointmentDetail />} />
<Route path="/users" element={<UserList />} />
<Route path="/translators" element={<TranslatorList />} />
<Route path="/payments" element={<PaymentList />} />
<Route path="/settings" element={<SystemSettings />} />
<Route path="*" element={<Dashboard />} />
</Routes>
</Content>
</Layout>
</Layout>
@@ -18,9 +18,7 @@ import {
Form,
Alert,
DatePicker,
TimePicker,
Rate,
Divider,
Statistic,
Row,
Col,
@@ -34,19 +32,14 @@ import {
VideoCameraOutlined,
DollarOutlined,
EditOutlined,
DeleteOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
TranslationOutlined,
StarOutlined,
MessageOutlined,
SettingOutlined,
TeamOutlined,
GlobalOutlined,
AuditOutlined,
FileTextOutlined,
EnvironmentOutlined,
SwapOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import moment from 'moment';
@@ -236,11 +229,11 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
try {
const refundAmount = values.amount || appointment.cost;
const updatedAppointment = {
const updatedAppointment: Appointment = {
...appointment,
refundAmount: refundAmount,
paymentStatus: 'refunded',
status: 'cancelled',
paymentStatus: 'refunded' as const,
status: 'cancelled' as const,
updatedAt: new Date().toISOString(),
};
@@ -326,8 +319,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
const getUrgencyText = (urgency: string) => {
const texts = {
normal: '普通',
low: '低',
high: '高',
urgent: '加急',
emergency: '特急',
};
return texts[urgency as keyof typeof texts] || urgency;
};
@@ -469,9 +463,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
<Card>
<Statistic
title="质量评分"
value={appointment.qualityScore}
value={appointment.qualityScore || 0}
suffix="/100"
valueStyle={{ color: appointment.qualityScore >= 90 ? '#3f8600' : '#faad14' }}
valueStyle={{ color: (appointment.qualityScore || 0) >= 90 ? '#3f8600' : '#faad14' }}
prefix={<AuditOutlined />}
/>
</Card>
@@ -498,7 +492,7 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
</Tag>
</Descriptions.Item>
<Descriptions.Item label="紧急程度" span={1}>
<Tag color={appointment.urgency === 'emergency' ? 'red' : appointment.urgency === 'urgent' ? 'orange' : 'default'}>
<Tag color={appointment.urgency === 'urgent' ? 'orange' : appointment.urgency === 'high' ? 'red' : 'default'}>
{getUrgencyText(appointment.urgency)}
</Tag>
</Descriptions.Item>
@@ -577,8 +571,8 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
<Descriptions.Item label="退款金额" span={1}>
<Space>
<DollarOutlined />
<Text type={appointment.refundAmount > 0 ? 'danger' : 'secondary'}>
¥{appointment.refundAmount.toFixed(2)}
<Text type={(appointment.refundAmount || 0) > 0 ? 'danger' : 'secondary'}>
¥{(appointment.refundAmount || 0).toFixed(2)}
</Text>
</Space>
</Descriptions.Item>
@@ -762,8 +756,8 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
<Descriptions column={2}>
<Descriptions.Item label="质量评分">
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: appointment.qualityScore >= 90 ? '#52c41a' : appointment.qualityScore >= 70 ? '#faad14' : '#ff4d4f' }}>
{appointment.qualityScore}/100
<div style={{ fontSize: '24px', fontWeight: 'bold', color: (appointment.qualityScore || 0) >= 90 ? '#52c41a' : (appointment.qualityScore || 0) >= 70 ? '#faad14' : '#ff4d4f' }}>
{appointment.qualityScore || 0}/100
</div>
<div style={{ color: '#999', fontSize: '12px' }}></div>
</div>
@@ -844,8 +838,9 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
>
<Select>
<Option value="normal"></Option>
<Option value="low"></Option>
<Option value="high"></Option>
<Option value="urgent"></Option>
<Option value="emergency"></Option>
</Select>
</Form.Item>
@@ -0,0 +1,737 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
DatePicker,
TimePicker,
Form,
Row,
Col,
Statistic,
Tooltip,
Avatar,
Badge,
Calendar
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
UserOutlined,
CalendarOutlined,
ClockCircleOutlined,
PhoneOutlined,
VideoCameraOutlined,
CheckOutlined,
CloseOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
const { Title } = Typography;
const { Option } = Select;
const { RangePicker } = DatePicker;
interface Appointment {
id: string;
clientName: string;
clientPhone: string;
clientEmail: string;
appointmentDate: string;
appointmentTime: string;
duration: number; // 分钟
serviceType: 'voice' | 'video' | 'document';
sourceLanguage: string;
targetLanguage: string;
translator?: string;
status: 'pending' | 'confirmed' | 'in-progress' | 'completed' | 'cancelled';
notes?: string;
cost: number;
createdTime: string;
}
const AppointmentList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [filteredAppointments, setFilteredAppointments] = useState<Appointment[]>([]);
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [serviceTypeFilter, setServiceTypeFilter] = useState<string>('all');
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [editingAppointment, setEditingAppointment] = useState<Appointment | null>(null);
const [calendarVisible, setCalendarVisible] = useState(false);
const [form] = Form.useForm();
// 模拟数据
const mockAppointments: Appointment[] = [
{
id: '1',
clientName: '张先生',
clientPhone: '13800138001',
clientEmail: 'zhang@example.com',
appointmentDate: '2024-01-16',
appointmentTime: '10:00',
duration: 60,
serviceType: 'video',
sourceLanguage: '中文',
targetLanguage: '英文',
translator: '王译员',
status: 'confirmed',
notes: '商务会议翻译',
cost: 300,
createdTime: '2024-01-15 14:30:00'
},
{
id: '2',
clientName: '李女士',
clientPhone: '13800138002',
clientEmail: 'li@example.com',
appointmentDate: '2024-01-16',
appointmentTime: '14:30',
duration: 90,
serviceType: 'voice',
sourceLanguage: '英文',
targetLanguage: '中文',
translator: '李译员',
status: 'in-progress',
notes: '医疗咨询翻译',
cost: 450,
createdTime: '2024-01-15 14:25:00'
},
{
id: '3',
clientName: '王总',
clientPhone: '13800138003',
clientEmail: 'wang@example.com',
appointmentDate: '2024-01-17',
appointmentTime: '09:00',
duration: 120,
serviceType: 'video',
sourceLanguage: '中文',
targetLanguage: '日文',
translator: '张译员',
status: 'pending',
notes: '技术交流会议',
cost: 600,
createdTime: '2024-01-15 14:20:00'
},
{
id: '4',
clientName: '陈先生',
clientPhone: '13800138004',
clientEmail: 'chen@example.com',
appointmentDate: '2024-01-15',
appointmentTime: '16:00',
duration: 45,
serviceType: 'document',
sourceLanguage: '德文',
targetLanguage: '中文',
translator: '赵译员',
status: 'completed',
notes: '合同翻译讨论',
cost: 225,
createdTime: '2024-01-15 14:15:00'
},
{
id: '5',
clientName: '刘女士',
clientPhone: '13800138005',
clientEmail: 'liu@example.com',
appointmentDate: '2024-01-18',
appointmentTime: '11:00',
duration: 30,
serviceType: 'voice',
sourceLanguage: '法文',
targetLanguage: '中文',
status: 'cancelled',
notes: '客户临时取消',
cost: 0,
createdTime: '2024-01-15 14:10:00'
}
];
const fetchAppointments = async () => {
setLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
setAppointments(mockAppointments);
setFilteredAppointments(mockAppointments);
message.success('预约列表加载成功');
} catch (error) {
message.error('加载预约列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAppointments();
}, []);
useEffect(() => {
let filtered = appointments;
// 搜索过滤
if (searchText) {
filtered = filtered.filter(apt =>
apt.clientName.toLowerCase().includes(searchText.toLowerCase()) ||
apt.clientPhone.includes(searchText) ||
apt.sourceLanguage.includes(searchText) ||
apt.targetLanguage.includes(searchText) ||
(apt.translator && apt.translator.includes(searchText))
);
}
// 状态过滤
if (statusFilter !== 'all') {
filtered = filtered.filter(apt => apt.status === statusFilter);
}
// 服务类型过滤
if (serviceTypeFilter !== 'all') {
filtered = filtered.filter(apt => apt.serviceType === serviceTypeFilter);
}
// 日期范围过滤
if (dateRange) {
const [startDate, endDate] = dateRange;
filtered = filtered.filter(apt => {
const aptDate = dayjs(apt.appointmentDate);
return aptDate.isAfter(startDate.subtract(1, 'day')) &&
aptDate.isBefore(endDate.add(1, 'day'));
});
}
setFilteredAppointments(filtered);
}, [appointments, searchText, statusFilter, serviceTypeFilter, dateRange]);
const getStatusTag = (status: string) => {
const statusConfig = {
pending: { color: 'orange', text: '待确认' },
confirmed: { color: 'blue', text: '已确认' },
'in-progress': { color: 'green', text: '进行中' },
completed: { color: 'cyan', text: '已完成' },
cancelled: { color: 'red', text: '已取消' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const getServiceTypeTag = (type: string) => {
const typeConfig = {
voice: { color: 'blue', text: '语音翻译', icon: <PhoneOutlined /> },
video: { color: 'green', text: '视频翻译', icon: <VideoCameraOutlined /> },
document: { color: 'purple', text: '文档讨论', icon: <EyeOutlined /> }
};
const config = typeConfig[type as keyof typeof typeConfig];
return (
<Tag color={config.color} icon={config.icon}>
{config.text}
</Tag>
);
};
const handleStatusChange = (appointmentId: string, newStatus: string) => {
const updatedAppointments = appointments.map(apt =>
apt.id === appointmentId ? { ...apt, status: newStatus as Appointment['status'] } : apt
);
setAppointments(updatedAppointments);
message.success('状态更新成功');
};
const handleEdit = (appointment: Appointment) => {
setEditingAppointment(appointment);
form.setFieldsValue({
...appointment,
appointmentDate: dayjs(appointment.appointmentDate),
appointmentTime: dayjs(appointment.appointmentTime, 'HH:mm')
});
setModalVisible(true);
};
const handleDelete = (appointment: Appointment) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除 ${appointment.clientName} 的预约吗?`,
onOk: () => {
const newAppointments = appointments.filter(apt => apt.id !== appointment.id);
setAppointments(newAppointments);
message.success('预约删除成功');
}
});
};
const handleSave = async (values: any) => {
try {
const appointmentData = {
...values,
appointmentDate: values.appointmentDate.format('YYYY-MM-DD'),
appointmentTime: values.appointmentTime.format('HH:mm'),
};
if (editingAppointment) {
// 更新预约
const updatedAppointments = appointments.map(apt =>
apt.id === editingAppointment.id ? { ...apt, ...appointmentData } : apt
);
setAppointments(updatedAppointments);
message.success('预约更新成功');
} else {
// 新增预约
const newAppointment: Appointment = {
id: Date.now().toString(),
...appointmentData,
status: 'pending',
createdTime: new Date().toLocaleString()
};
setAppointments([...appointments, newAppointment]);
message.success('预约创建成功');
}
setModalVisible(false);
setEditingAppointment(null);
form.resetFields();
} catch (error) {
message.error('保存失败');
}
};
const columns: ColumnsType<Appointment> = [
{
title: '客户信息',
key: 'client',
width: 200,
render: (_, record) => (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<Avatar size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 8, fontWeight: 'bold' }}>{record.clientName}</span>
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{record.clientPhone}
</div>
</div>
)
},
{
title: '预约时间',
key: 'datetime',
width: 150,
render: (_, record) => (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<CalendarOutlined style={{ marginRight: 4 }} />
{record.appointmentDate}
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<ClockCircleOutlined style={{ marginRight: 4 }} />
{record.appointmentTime} ({record.duration})
</div>
</div>
)
},
{
title: '服务类型',
dataIndex: 'serviceType',
key: 'serviceType',
width: 120,
render: getServiceTypeTag
},
{
title: '语言对',
key: 'languages',
width: 150,
render: (_, record) => (
<div>
<Tag color="blue">{record.sourceLanguage}</Tag>
<span style={{ margin: '0 4px' }}></span>
<Tag color="green">{record.targetLanguage}</Tag>
</div>
)
},
{
title: '译员',
dataIndex: 'translator',
key: 'translator',
width: 100,
render: (text) => text || '-'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: getStatusTag
},
{
title: '费用(元)',
dataIndex: 'cost',
key: 'cost',
width: 100,
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<Tooltip title="查看详情">
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
/>
</Tooltip>
{record.status === 'pending' && (
<Tooltip title="确认">
<Button
size="small"
icon={<CheckOutlined />}
style={{ color: 'green' }}
onClick={() => handleStatusChange(record.id, 'confirmed')}
/>
</Tooltip>
)}
{record.status !== 'cancelled' && record.status !== 'completed' && (
<Tooltip title="取消">
<Button
size="small"
icon={<CloseOutlined />}
danger
onClick={() => handleStatusChange(record.id, 'cancelled')}
/>
</Tooltip>
)}
<Tooltip title="删除">
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
/>
</Tooltip>
</Space>
),
},
];
// 统计数据
const stats = {
total: filteredAppointments.length,
pending: filteredAppointments.filter(a => a.status === 'pending').length,
confirmed: filteredAppointments.filter(a => a.status === 'confirmed').length,
inProgress: filteredAppointments.filter(a => a.status === 'in-progress').length,
completed: filteredAppointments.filter(a => a.status === 'completed').length,
totalRevenue: filteredAppointments.filter(a => a.status === 'completed').reduce((sum, a) => sum + a.cost, 0)
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={4}>
<Card>
<Statistic
title="总预约数"
value={stats.total}
prefix={<CalendarOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="待确认"
value={stats.pending}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="已确认"
value={stats.confirmed}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="进行中"
value={stats.inProgress}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="已完成"
value={stats.completed}
valueStyle={{ color: '#13c2c2' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="总收入"
value={stats.totalRevenue}
precision={2}
prefix="¥"
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
</Row>
<Card>
{/* 搜索和筛选 */}
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={6}>
<Input
placeholder="搜索客户、电话、语言..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={4}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="pending"></Option>
<Option value="confirmed"></Option>
<Option value="in-progress"></Option>
<Option value="completed"></Option>
<Option value="cancelled"></Option>
</Select>
</Col>
<Col span={4}>
<Select
value={serviceTypeFilter}
onChange={setServiceTypeFilter}
style={{ width: '100%' }}
placeholder="服务类型"
>
<Option value="all"></Option>
<Option value="voice"></Option>
<Option value="video"></Option>
<Option value="document"></Option>
</Select>
</Col>
<Col span={6}>
<RangePicker
style={{ width: '100%' }}
value={dateRange}
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
placeholder={['开始日期', '结束日期']}
/>
</Col>
<Col span={4}>
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingAppointment(null);
form.resetFields();
setModalVisible(true);
}}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={fetchAppointments}
loading={loading}
/>
</Space>
</Col>
</Row>
{/* 预约列表表格 */}
<Table
columns={columns}
dataSource={filteredAppointments}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredAppointments.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
{/* 预约编辑弹窗 */}
<Modal
title={editingAppointment ? '编辑预约' : '新增预约'}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setEditingAppointment(null);
form.resetFields();
}}
onOk={() => form.submit()}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSave}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="clientName"
label="客户姓名"
rules={[{ required: true, message: '请输入客户姓名' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="clientPhone"
label="联系电话"
rules={[{ required: true, message: '请输入联系电话' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="appointmentDate"
label="预约日期"
rules={[{ required: true, message: '请选择预约日期' }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="appointmentTime"
label="预约时间"
rules={[{ required: true, message: '请选择预约时间' }]}
>
<TimePicker format="HH:mm" style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="duration"
label="时长(分钟)"
rules={[{ required: true, message: '请输入时长' }]}
>
<Input type="number" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="serviceType"
label="服务类型"
rules={[{ required: true, message: '请选择服务类型' }]}
>
<Select>
<Option value="voice"></Option>
<Option value="video"></Option>
<Option value="document"></Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="cost"
label="费用(元)"
rules={[{ required: true, message: '请输入费用' }]}
>
<Input type="number" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="sourceLanguage"
label="源语言"
rules={[{ required: true, message: '请选择源语言' }]}
>
<Select>
<Option value="中文"></Option>
<Option value="英文"></Option>
<Option value="日文"></Option>
<Option value="韩文"></Option>
<Option value="法文"></Option>
<Option value="德文"></Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="targetLanguage"
label="目标语言"
rules={[{ required: true, message: '请选择目标语言' }]}
>
<Select>
<Option value="中文"></Option>
<Option value="英文"></Option>
<Option value="日文"></Option>
<Option value="韩文"></Option>
<Option value="法文"></Option>
<Option value="德文"></Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="notes"
label="备注"
>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AppointmentList;
@@ -0,0 +1,470 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
DatePicker,
Row,
Col,
Statistic
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
ReloadOutlined,
PhoneOutlined,
ClockCircleOutlined,
UserOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Title } = Typography;
const { RangePicker } = DatePicker;
const { Option } = Select;
interface CallRecord {
id: string;
caller: string;
callee: string;
startTime: string;
endTime: string;
duration: string;
status: 'completed' | 'ongoing' | 'failed' | 'missed';
type: 'voice' | 'video';
language: string;
translator?: string;
quality: number;
cost: number;
}
const CallList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [calls, setCalls] = useState<CallRecord[]>([]);
const [filteredCalls, setFilteredCalls] = useState<CallRecord[]>([]);
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [selectedCall, setSelectedCall] = useState<CallRecord | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
// 模拟数据
const mockCalls: CallRecord[] = [
{
id: '1',
caller: '张三 (+86 138****1234)',
callee: '李四 (+1 555****5678)',
startTime: '2024-01-15 14:30:00',
endTime: '2024-01-15 14:45:30',
duration: '15:30',
status: 'completed',
type: 'video',
language: '中文-英文',
translator: '王译员',
quality: 4.8,
cost: 45.50
},
{
id: '2',
caller: '李四 (+1 555****5678)',
callee: '王五 (+86 139****5678)',
startTime: '2024-01-15 14:25:00',
endTime: '',
duration: '08:45',
status: 'ongoing',
type: 'voice',
language: '英文-中文',
translator: '赵译员',
quality: 0,
cost: 0
},
{
id: '3',
caller: '王五 (+86 139****5678)',
callee: '赵六 (+81 90****1234)',
startTime: '2024-01-15 14:20:00',
endTime: '2024-01-15 14:42:10',
duration: '22:10',
status: 'completed',
type: 'video',
language: '中文-日文',
translator: '孙译员',
quality: 4.9,
cost: 66.30
},
{
id: '4',
caller: '赵六 (+81 90****1234)',
callee: '孙七 (+86 137****9876)',
startTime: '2024-01-15 14:15:00',
endTime: '2024-01-15 14:20:15',
duration: '05:15',
status: 'failed',
type: 'voice',
language: '日文-中文',
translator: '',
quality: 0,
cost: 0
},
{
id: '5',
caller: '孙七 (+86 137****9876)',
callee: '周八 (+49 30****5678)',
startTime: '2024-01-15 14:10:00',
endTime: '',
duration: '00:00',
status: 'missed',
type: 'voice',
language: '中文-德文',
translator: '',
quality: 0,
cost: 0
}
];
const fetchCalls = async () => {
setLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
setCalls(mockCalls);
setFilteredCalls(mockCalls);
message.success('通话记录加载成功');
} catch (error) {
message.error('加载通话记录失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCalls();
}, []);
useEffect(() => {
let filtered = calls;
// 搜索过滤
if (searchText) {
filtered = filtered.filter(call =>
call.caller.toLowerCase().includes(searchText.toLowerCase()) ||
call.callee.toLowerCase().includes(searchText.toLowerCase()) ||
call.language.includes(searchText) ||
(call.translator && call.translator.includes(searchText))
);
}
// 状态过滤
if (statusFilter !== 'all') {
filtered = filtered.filter(call => call.status === statusFilter);
}
// 类型过滤
if (typeFilter !== 'all') {
filtered = filtered.filter(call => call.type === typeFilter);
}
setFilteredCalls(filtered);
}, [calls, searchText, statusFilter, typeFilter]);
const getStatusTag = (status: string) => {
const statusConfig = {
completed: { color: 'green', text: '已完成' },
ongoing: { color: 'blue', text: '进行中' },
failed: { color: 'red', text: '失败' },
missed: { color: 'orange', text: '未接听' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const getTypeTag = (type: string) => {
return type === 'video' ?
<Tag color="purple"></Tag> :
<Tag color="cyan"></Tag>;
};
const columns: ColumnsType<CallRecord> = [
{
title: '通话ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '呼叫方',
dataIndex: 'caller',
key: 'caller',
width: 200,
render: (text) => (
<div>
<UserOutlined style={{ marginRight: 8 }} />
{text}
</div>
)
},
{
title: '接听方',
dataIndex: 'callee',
key: 'callee',
width: 200,
render: (text) => (
<div>
<UserOutlined style={{ marginRight: 8 }} />
{text}
</div>
)
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 160,
},
{
title: '通话时长',
dataIndex: 'duration',
key: 'duration',
width: 100,
render: (text) => (
<div>
<ClockCircleOutlined style={{ marginRight: 4 }} />
{text}
</div>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: getStatusTag
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
render: getTypeTag
},
{
title: '语言',
dataIndex: 'language',
key: 'language',
width: 120,
},
{
title: '译员',
dataIndex: 'translator',
key: 'translator',
width: 100,
render: (text) => text || '-'
},
{
title: '评分',
dataIndex: 'quality',
key: 'quality',
width: 80,
render: (score) => score > 0 ? `${score}/5` : '-'
},
{
title: '费用(元)',
dataIndex: 'cost',
key: 'cost',
width: 100,
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
},
{
title: '操作',
key: 'action',
width: 100,
render: (_, record) => (
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
onClick={() => {
setSelectedCall(record);
setDetailModalVisible(true);
}}
>
</Button>
),
},
];
// 统计数据
const stats = {
total: filteredCalls.length,
completed: filteredCalls.filter(c => c.status === 'completed').length,
ongoing: filteredCalls.filter(c => c.status === 'ongoing').length,
failed: filteredCalls.filter(c => c.status === 'failed').length,
totalRevenue: filteredCalls.reduce((sum, c) => sum + c.cost, 0)
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={6}>
<Card>
<Statistic
title="总通话数"
value={stats.total}
prefix={<PhoneOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="已完成"
value={stats.completed}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="进行中"
value={stats.ongoing}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="总收入"
value={stats.totalRevenue}
precision={2}
prefix="¥"
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
</Row>
<Card>
{/* 搜索和筛选 */}
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={6}>
<Input
placeholder="搜索呼叫方、接听方、译员..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={4}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="completed"></Option>
<Option value="ongoing"></Option>
<Option value="failed"></Option>
<Option value="missed"></Option>
</Select>
</Col>
<Col span={4}>
<Select
value={typeFilter}
onChange={setTypeFilter}
style={{ width: '100%' }}
placeholder="类型筛选"
>
<Option value="all"></Option>
<Option value="voice"></Option>
<Option value="video"></Option>
</Select>
</Col>
<Col span={6}>
<RangePicker style={{ width: '100%' }} />
</Col>
<Col span={4}>
<Space>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={fetchCalls}
loading={loading}
>
</Button>
</Space>
</Col>
</Row>
{/* 通话记录表格 */}
<Table
columns={columns}
dataSource={filteredCalls}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredCalls.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
{/* 详情弹窗 */}
<Modal
title="通话详情"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
</Button>
]}
width={600}
>
{selectedCall && (
<div>
<Row gutter={16}>
<Col span={12}>
<p><strong>ID:</strong> {selectedCall.id}</p>
<p><strong>:</strong> {selectedCall.caller}</p>
<p><strong>:</strong> {selectedCall.callee}</p>
<p><strong>:</strong> {selectedCall.startTime}</p>
<p><strong>:</strong> {selectedCall.endTime || '进行中'}</p>
<p><strong>:</strong> {selectedCall.duration}</p>
</Col>
<Col span={12}>
<p><strong>:</strong> {getStatusTag(selectedCall.status)}</p>
<p><strong>:</strong> {getTypeTag(selectedCall.type)}</p>
<p><strong>:</strong> {selectedCall.language}</p>
<p><strong>:</strong> {selectedCall.translator || '无'}</p>
<p><strong>:</strong> {selectedCall.quality > 0 ? `${selectedCall.quality}/5` : '未评分'}</p>
<p><strong>:</strong> {selectedCall.cost > 0 ? `¥${selectedCall.cost.toFixed(2)}` : '免费'}</p>
</Col>
</Row>
</div>
)}
</Modal>
</div>
);
};
export default CallList;
+297 -22
View File
@@ -1,55 +1,231 @@
import React from 'react';
import { Card, Row, Col, Statistic, Typography } from 'antd';
import React, { useState, useEffect } from 'react';
import {
Card,
Row,
Col,
Statistic,
Typography,
Table,
Tag,
Progress,
Spin,
message,
Space,
Button
} from 'antd';
import {
PhoneOutlined,
FileTextOutlined,
CalendarOutlined,
DollarOutlined
DollarOutlined,
UserOutlined,
VideoCameraOutlined,
ReloadOutlined,
TrophyOutlined
} from '@ant-design/icons';
const { Title } = Typography;
const { Title, Text } = Typography;
interface DashboardData {
totalCalls: number;
totalDocuments: number;
totalAppointments: number;
totalRevenue: number;
activeUsers: number;
videoCalls: number;
recentCalls: Array<{
id: string;
caller: string;
duration: string;
status: 'completed' | 'ongoing' | 'failed';
time: string;
}>;
systemStatus: {
api: 'online' | 'offline';
database: 'online' | 'offline';
twilio: 'online' | 'offline';
};
}
const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<DashboardData>({
totalCalls: 0,
totalDocuments: 0,
totalAppointments: 0,
totalRevenue: 0,
activeUsers: 0,
videoCalls: 0,
recentCalls: [],
systemStatus: {
api: 'online',
database: 'online',
twilio: 'online'
}
});
const fetchDashboardData = async () => {
try {
setLoading(true);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const mockData: DashboardData = {
totalCalls: 1128,
totalDocuments: 892,
totalAppointments: 456,
totalRevenue: 25680,
activeUsers: 89,
videoCalls: 234,
recentCalls: [
{
id: '1',
caller: '张三',
duration: '15:30',
status: 'completed',
time: '2024-01-15 14:30'
},
{
id: '2',
caller: '李四',
duration: '08:45',
status: 'ongoing',
time: '2024-01-15 14:25'
},
{
id: '3',
caller: '王五',
duration: '22:10',
status: 'completed',
time: '2024-01-15 14:20'
},
{
id: '4',
caller: '赵六',
duration: '05:15',
status: 'failed',
time: '2024-01-15 14:15'
}
],
systemStatus: {
api: 'online',
database: 'online',
twilio: 'online'
}
};
setData(mockData);
message.success('仪表板数据加载成功');
} catch (error) {
console.error('获取仪表板数据失败:', error);
message.error('获取仪表板数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
}, []);
const recentCallsColumns = [
{
title: '呼叫者',
dataIndex: 'caller',
key: 'caller',
},
{
title: '通话时长',
dataIndex: 'duration',
key: 'duration',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const statusConfig = {
completed: { color: 'green', text: '已完成' },
ongoing: { color: 'blue', text: '进行中' },
failed: { color: 'red', text: '失败' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color}>{config.text}</Tag>;
}
},
{
title: '时间',
dataIndex: 'time',
key: 'time',
}
];
if (loading) {
return (
<div style={{
padding: '24px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px'
}}>
<Spin size="large" tip="加载仪表板数据中..." />
</div>
);
}
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<Title level={2} style={{ margin: 0 }}></Title>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={fetchDashboardData}
loading={loading}
>
</Button>
</div>
<Row gutter={16}>
<Col span={6}>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="总通话数"
value={1128}
value={data.totalCalls}
prefix={<PhoneOutlined />}
valueStyle={{ color: '#3f8600' }}
/>
</Card>
</Col>
<Col span={6}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="文档翻译"
value={892}
value={data.totalDocuments}
prefix={<FileTextOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="预约服务"
value={456}
value={data.totalAppointments}
prefix={<CalendarOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="总收入"
value={25680}
value={data.totalRevenue}
prefix={<DollarOutlined />}
valueStyle={{ color: '#cf1322' }}
suffix="元"
@@ -57,14 +233,113 @@ const Dashboard: React.FC = () => {
</Card>
</Col>
</Row>
<div style={{ marginTop: '24px' }}>
<Card title="系统状态">
<p> </p>
<p> 线</p>
<p> </p>
</Card>
</div>
{/* 第二行统计 */}
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="活跃用户"
value={data.activeUsers}
prefix={<UserOutlined />}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="视频通话"
value={data.videoCalls}
prefix={<VideoCameraOutlined />}
valueStyle={{ color: '#eb2f96' }}
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="成功率"
value={94.5}
prefix={<TrophyOutlined />}
valueStyle={{ color: '#52c41a' }}
suffix="%"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<div style={{ textAlign: 'center' }}>
<Text type="secondary"></Text>
<Progress
type="circle"
percent={75}
size={80}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
/>
</div>
</Card>
</Col>
</Row>
<Row gutter={16}>
{/* 最近通话记录 */}
<Col xs={24} lg={16}>
<Card title="最近通话记录" style={{ marginBottom: '24px' }}>
<Table
columns={recentCallsColumns}
dataSource={data.recentCalls}
pagination={false}
size="small"
rowKey="id"
/>
</Card>
</Col>
{/* 系统状态 */}
<Col xs={24} lg={8}>
<Card title="系统状态" style={{ marginBottom: '24px' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text>API服务</Text>
<Tag color={data.systemStatus.api === 'online' ? 'green' : 'red'}>
{data.systemStatus.api === 'online' ? '在线' : '离线'}
</Tag>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text></Text>
<Tag color={data.systemStatus.database === 'online' ? 'green' : 'red'}>
{data.systemStatus.database === 'online' ? '在线' : '离线'}
</Tag>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text>Twilio服务</Text>
<Tag color={data.systemStatus.twilio === 'online' ? 'green' : 'red'}>
{data.systemStatus.twilio === 'online' ? '在线' : '离线'}
</Tag>
</div>
</Space>
</Card>
{/* 快速操作 */}
<Card title="快速操作">
<Space direction="vertical" style={{ width: '100%' }}>
<Button type="primary" block icon={<PhoneOutlined />}>
</Button>
<Button block icon={<FileTextOutlined />}>
</Button>
<Button block icon={<CalendarOutlined />}>
</Button>
</Space>
</Card>
</Col>
</Row>
</div>
);
};
@@ -0,0 +1,404 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
Upload,
Progress,
Row,
Col,
Statistic,
Tooltip
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
DownloadOutlined,
UploadOutlined,
ReloadOutlined,
FileTextOutlined,
FilePdfOutlined,
FileWordOutlined,
FileExcelOutlined,
DeleteOutlined,
TranslationOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { UploadProps } from 'antd';
const { Title } = Typography;
const { Option } = Select;
interface Document {
id: string;
fileName: string;
fileType: string;
fileSize: number;
uploadTime: string;
status: 'pending' | 'translating' | 'completed' | 'failed';
sourceLanguage: string;
targetLanguage: string;
translator?: string;
progress: number;
downloadCount: number;
cost: number;
}
const DocumentList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [documents, setDocuments] = useState<Document[]>([]);
const [filteredDocuments, setFilteredDocuments] = useState<Document[]>([]);
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [uploadModalVisible, setUploadModalVisible] = useState(false);
// 模拟数据
const mockDocuments: Document[] = [
{
id: '1',
fileName: '商业合同.pdf',
fileType: 'pdf',
fileSize: 2048576,
uploadTime: '2024-01-15 14:30:00',
status: 'completed',
sourceLanguage: '中文',
targetLanguage: '英文',
translator: '王译员',
progress: 100,
downloadCount: 5,
cost: 128.50
},
{
id: '2',
fileName: '技术文档.docx',
fileType: 'docx',
fileSize: 1536000,
uploadTime: '2024-01-15 14:25:00',
status: 'translating',
sourceLanguage: '英文',
targetLanguage: '中文',
translator: '李译员',
progress: 65,
downloadCount: 0,
cost: 0
},
{
id: '3',
fileName: '财务报表.xlsx',
fileType: 'xlsx',
fileSize: 512000,
uploadTime: '2024-01-15 14:20:00',
status: 'completed',
sourceLanguage: '中文',
targetLanguage: '日文',
translator: '张译员',
progress: 100,
downloadCount: 12,
cost: 85.30
}
];
const fetchDocuments = async () => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setDocuments(mockDocuments);
setFilteredDocuments(mockDocuments);
message.success('文档列表加载成功');
} catch (error) {
message.error('加载文档列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDocuments();
}, []);
useEffect(() => {
let filtered = documents;
if (searchText) {
filtered = filtered.filter(doc =>
doc.fileName.toLowerCase().includes(searchText.toLowerCase()) ||
doc.sourceLanguage.includes(searchText) ||
doc.targetLanguage.includes(searchText) ||
(doc.translator && doc.translator.includes(searchText))
);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(doc => doc.status === statusFilter);
}
setFilteredDocuments(filtered);
}, [documents, searchText, statusFilter]);
const getStatusTag = (status: string) => {
const statusConfig = {
pending: { color: 'orange', text: '待处理' },
translating: { color: 'blue', text: '翻译中' },
completed: { color: 'green', text: '已完成' },
failed: { color: 'red', text: '失败' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const getFileIcon = (fileType: string) => {
const iconMap = {
pdf: <FilePdfOutlined style={{ color: '#ff4d4f' }} />,
docx: <FileWordOutlined style={{ color: '#1890ff' }} />,
xlsx: <FileExcelOutlined style={{ color: '#52c41a' }} />,
txt: <FileTextOutlined style={{ color: '#722ed1' }} />
};
return iconMap[fileType as keyof typeof iconMap] || <FileTextOutlined />;
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const handleDownload = (document: Document) => {
if (document.status !== 'completed') {
message.warning('文档尚未翻译完成,无法下载');
return;
}
message.success(`开始下载:${document.fileName}`);
};
const handleDelete = (document: Document) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除文档 "${document.fileName}" 吗?`,
onOk: () => {
const newDocuments = documents.filter(doc => doc.id !== document.id);
setDocuments(newDocuments);
message.success('文档删除成功');
}
});
};
const columns: ColumnsType<Document> = [
{
title: '文件名',
dataIndex: 'fileName',
key: 'fileName',
width: 250,
render: (text, record) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
{getFileIcon(record.fileType)}
<span style={{ marginLeft: 8 }}>{text}</span>
</div>
)
},
{
title: '文件大小',
dataIndex: 'fileSize',
key: 'fileSize',
width: 120,
render: formatFileSize
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: getStatusTag
},
{
title: '翻译进度',
dataIndex: 'progress',
key: 'progress',
width: 120,
render: (progress, record) => (
<Progress
percent={progress}
size="small"
status={record.status === 'failed' ? 'exception' : undefined}
/>
)
},
{
title: '源语言',
dataIndex: 'sourceLanguage',
key: 'sourceLanguage',
width: 100,
},
{
title: '目标语言',
dataIndex: 'targetLanguage',
key: 'targetLanguage',
width: 100,
},
{
title: '费用(元)',
dataIndex: 'cost',
key: 'cost',
width: 100,
render: (cost) => cost > 0 ? `¥${cost.toFixed(2)}` : '-'
},
{
title: '操作',
key: 'action',
width: 160,
render: (_, record) => (
<Space>
<Tooltip title="查看详情">
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
/>
</Tooltip>
<Tooltip title="下载">
<Button
size="small"
icon={<DownloadOutlined />}
disabled={record.status !== 'completed'}
onClick={() => handleDownload(record)}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
/>
</Tooltip>
</Space>
),
},
];
const stats = {
total: filteredDocuments.length,
completed: filteredDocuments.filter(d => d.status === 'completed').length,
translating: filteredDocuments.filter(d => d.status === 'translating').length,
totalRevenue: filteredDocuments.reduce((sum, d) => sum + d.cost, 0)
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={6}>
<Card>
<Statistic
title="总文档数"
value={stats.total}
prefix={<FileTextOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="已完成"
value={stats.completed}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="翻译中"
value={stats.translating}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="总收入"
value={stats.totalRevenue}
precision={2}
prefix="¥"
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
</Row>
<Card>
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={8}>
<Input
placeholder="搜索文件名、语言、译员..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={6}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="pending"></Option>
<Option value="translating"></Option>
<Option value="completed"></Option>
<Option value="failed"></Option>
</Select>
</Col>
<Col span={10}>
<Space>
<Button
type="primary"
icon={<UploadOutlined />}
onClick={() => setUploadModalVisible(true)}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={fetchDocuments}
loading={loading}
>
</Button>
</Space>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredDocuments}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredDocuments.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
</div>
);
};
export default DocumentList;
@@ -0,0 +1,525 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
Row,
Col,
Statistic,
Tooltip,
DatePicker,
Descriptions,
Divider
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
ReloadOutlined,
DollarOutlined,
CreditCardOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
DownloadOutlined,
UndoOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
const { Title } = Typography;
const { Option } = Select;
const { RangePicker } = DatePicker;
interface Payment {
id: string;
orderId: string;
userId: string;
userName: string;
amount: number;
paymentMethod: 'credit_card' | 'alipay' | 'wechat' | 'paypal';
status: 'pending' | 'completed' | 'failed' | 'refunded' | 'cancelled';
transactionId: string;
serviceType: 'voice_call' | 'video_call' | 'document_translation' | 'appointment';
serviceName: string;
createdAt: string;
completedAt?: string;
refundAmount?: number;
refundReason?: string;
currency: string;
fee: number; // 手续费
}
const PaymentList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [payments, setPayments] = useState<Payment[]>([]);
const [filteredPayments, setFilteredPayments] = useState<Payment[]>([]);
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [methodFilter, setMethodFilter] = useState<string>('all');
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
const [selectedPayment, setSelectedPayment] = useState<Payment | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
// 模拟数据
const mockPayments: Payment[] = [
{
id: '1',
orderId: 'ORD-2024-001',
userId: 'U001',
userName: '张三',
amount: 150.00,
paymentMethod: 'alipay',
status: 'completed',
transactionId: 'TXN-20240115-001',
serviceType: 'voice_call',
serviceName: '中英文语音翻译',
createdAt: '2024-01-15 14:30:00',
completedAt: '2024-01-15 14:31:00',
currency: 'CNY',
fee: 4.50
},
{
id: '2',
orderId: 'ORD-2024-002',
userId: 'U002',
userName: '李四',
amount: 280.00,
paymentMethod: 'wechat',
status: 'completed',
transactionId: 'TXN-20240115-002',
serviceType: 'document_translation',
serviceName: '商务文档翻译',
createdAt: '2024-01-15 13:45:00',
completedAt: '2024-01-15 13:46:00',
currency: 'CNY',
fee: 8.40
},
{
id: '3',
orderId: 'ORD-2024-003',
userId: 'U003',
userName: '王五',
amount: 320.00,
paymentMethod: 'credit_card',
status: 'refunded',
transactionId: 'TXN-20240115-003',
serviceType: 'video_call',
serviceName: '视频会议翻译',
createdAt: '2024-01-15 12:20:00',
completedAt: '2024-01-15 12:21:00',
refundAmount: 320.00,
refundReason: '服务质量问题',
currency: 'CNY',
fee: 9.60
},
{
id: '4',
orderId: 'ORD-2024-004',
userId: 'U004',
userName: '赵六',
amount: 450.00,
paymentMethod: 'paypal',
status: 'pending',
transactionId: 'TXN-20240115-004',
serviceType: 'appointment',
serviceName: '专业咨询预约',
createdAt: '2024-01-15 16:10:00',
currency: 'USD',
fee: 13.50
},
{
id: '5',
orderId: 'ORD-2024-005',
userId: 'U005',
userName: '孙七',
amount: 180.00,
paymentMethod: 'alipay',
status: 'failed',
transactionId: 'TXN-20240115-005',
serviceType: 'voice_call',
serviceName: '法语口译服务',
createdAt: '2024-01-15 11:30:00',
currency: 'CNY',
fee: 5.40
}
];
const fetchPayments = async () => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setPayments(mockPayments);
setFilteredPayments(mockPayments);
message.success('支付记录加载成功');
} catch (error) {
message.error('加载支付记录失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPayments();
}, []);
useEffect(() => {
let filtered = payments;
if (searchText) {
filtered = filtered.filter(payment =>
payment.orderId.toLowerCase().includes(searchText.toLowerCase()) ||
payment.userName.toLowerCase().includes(searchText.toLowerCase()) ||
payment.transactionId.toLowerCase().includes(searchText.toLowerCase()) ||
payment.serviceName.includes(searchText)
);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(payment => payment.status === statusFilter);
}
if (methodFilter !== 'all') {
filtered = filtered.filter(payment => payment.paymentMethod === methodFilter);
}
if (dateRange) {
const [start, end] = dateRange;
filtered = filtered.filter(payment => {
const paymentDate = dayjs(payment.createdAt);
return paymentDate.isAfter(start.startOf('day')) && paymentDate.isBefore(end.endOf('day'));
});
}
setFilteredPayments(filtered);
}, [payments, searchText, statusFilter, methodFilter, dateRange]);
const getStatusTag = (status: string) => {
const statusConfig = {
pending: { color: 'orange', text: '待支付', icon: <ExclamationCircleOutlined /> },
completed: { color: 'green', text: '已完成', icon: <CheckCircleOutlined /> },
failed: { color: 'red', text: '支付失败', icon: <CloseCircleOutlined /> },
refunded: { color: 'purple', text: '已退款', icon: <UndoOutlined /> },
cancelled: { color: 'gray', text: '已取消', icon: <CloseCircleOutlined /> }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
};
const getPaymentMethodTag = (method: string) => {
const methodConfig = {
credit_card: { color: 'blue', text: '信用卡' },
alipay: { color: 'green', text: '支付宝' },
wechat: { color: 'lime', text: '微信支付' },
paypal: { color: 'gold', text: 'PayPal' }
};
const config = methodConfig[method as keyof typeof methodConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const handleViewDetail = (payment: Payment) => {
setSelectedPayment(payment);
setDetailModalVisible(true);
};
const columns: ColumnsType<Payment> = [
{
title: '订单号',
dataIndex: 'orderId',
key: 'orderId',
width: 140,
render: (orderId) => (
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>
{orderId}
</span>
)
},
{
title: '用户',
dataIndex: 'userName',
key: 'userName',
width: 100
},
{
title: '金额',
key: 'amount',
width: 120,
render: (_, record) => (
<div>
<div style={{ fontWeight: 'bold' }}>
{record.currency} {record.amount.toFixed(2)}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
: {record.currency} {record.fee.toFixed(2)}
</div>
</div>
)
},
{
title: '支付方式',
dataIndex: 'paymentMethod',
key: 'paymentMethod',
width: 120,
render: getPaymentMethodTag
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: getStatusTag
},
{
title: '服务类型',
dataIndex: 'serviceName',
key: 'serviceName',
width: 150,
ellipsis: true
},
{
title: '交易时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 120,
render: (_, record) => (
<Space>
<Tooltip title="查看详情">
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
/>
</Tooltip>
<Tooltip title="下载凭证">
<Button
size="small"
icon={<DownloadOutlined />}
disabled={record.status !== 'completed'}
/>
</Tooltip>
</Space>
),
},
];
const stats = {
total: filteredPayments.length,
completed: filteredPayments.filter(p => p.status === 'completed').length,
pending: filteredPayments.filter(p => p.status === 'pending').length,
failed: filteredPayments.filter(p => p.status === 'failed').length,
refunded: filteredPayments.filter(p => p.status === 'refunded').length,
totalAmount: filteredPayments
.filter(p => p.status === 'completed')
.reduce((sum, p) => sum + p.amount, 0),
totalFee: filteredPayments
.filter(p => p.status === 'completed')
.reduce((sum, p) => sum + p.fee, 0),
refundAmount: filteredPayments
.filter(p => p.status === 'refunded')
.reduce((sum, p) => sum + (p.refundAmount || 0), 0)
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={6}>
<Card>
<Statistic
title="总交易数"
value={stats.total}
prefix={<CreditCardOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="成功交易"
value={stats.completed}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="交易总额"
value={stats.totalAmount}
precision={2}
prefix={<DollarOutlined />}
valueStyle={{ color: '#fa8c16' }}
suffix="CNY"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="手续费收入"
value={stats.totalFee}
precision={2}
valueStyle={{ color: '#722ed1' }}
suffix="CNY"
/>
</Card>
</Col>
</Row>
<Card>
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={6}>
<Input
placeholder="搜索订单号、用户、交易号..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={4}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="pending"></Option>
<Option value="completed"></Option>
<Option value="failed"></Option>
<Option value="refunded">退</Option>
<Option value="cancelled"></Option>
</Select>
</Col>
<Col span={4}>
<Select
value={methodFilter}
onChange={setMethodFilter}
style={{ width: '100%' }}
placeholder="支付方式"
>
<Option value="all"></Option>
<Option value="credit_card"></Option>
<Option value="alipay"></Option>
<Option value="wechat"></Option>
<Option value="paypal">PayPal</Option>
</Select>
</Col>
<Col span={6}>
<RangePicker
style={{ width: '100%' }}
value={dateRange}
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
placeholder={['开始日期', '结束日期']}
/>
</Col>
<Col span={4}>
<Button
icon={<ReloadOutlined />}
onClick={fetchPayments}
loading={loading}
style={{ width: '100%' }}
>
</Button>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredPayments}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredPayments.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
<Modal
title="支付详情"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
</Button>
]}
width={600}
>
{selectedPayment && (
<Descriptions column={2} bordered>
<Descriptions.Item label="订单号" span={2}>
{selectedPayment.orderId}
</Descriptions.Item>
<Descriptions.Item label="交易号" span={2}>
{selectedPayment.transactionId}
</Descriptions.Item>
<Descriptions.Item label="用户">
{selectedPayment.userName}
</Descriptions.Item>
<Descriptions.Item label="用户ID">
{selectedPayment.userId}
</Descriptions.Item>
<Descriptions.Item label="服务类型" span={2}>
{selectedPayment.serviceName}
</Descriptions.Item>
<Descriptions.Item label="支付金额">
{selectedPayment.currency} {selectedPayment.amount.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="手续费">
{selectedPayment.currency} {selectedPayment.fee.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="支付方式">
{getPaymentMethodTag(selectedPayment.paymentMethod)}
</Descriptions.Item>
<Descriptions.Item label="状态">
{getStatusTag(selectedPayment.status)}
</Descriptions.Item>
<Descriptions.Item label="创建时间" span={2}>
{dayjs(selectedPayment.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
{selectedPayment.completedAt && (
<Descriptions.Item label="完成时间" span={2}>
{dayjs(selectedPayment.completedAt).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
)}
{selectedPayment.refundAmount && (
<>
<Descriptions.Item label="退款金额">
{selectedPayment.currency} {selectedPayment.refundAmount.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="退款原因">
{selectedPayment.refundReason}
</Descriptions.Item>
</>
)}
</Descriptions>
)}
</Modal>
</div>
);
};
export default PaymentList;
@@ -0,0 +1,637 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Button,
Switch,
InputNumber,
Select,
Typography,
message,
Row,
Col,
Divider,
Tabs,
Space,
Alert,
Badge
} from 'antd';
import {
SaveOutlined,
ReloadOutlined,
SettingOutlined,
DollarOutlined,
PhoneOutlined,
SecurityScanOutlined,
NotificationOutlined,
GlobalOutlined
} from '@ant-design/icons';
const { Title, Text } = Typography;
const { Option } = Select;
const { TextArea } = Input;
interface SystemConfig {
// 基础设置
siteName: string;
siteDescription: string;
supportEmail: string;
supportPhone: string;
defaultLanguage: string;
timezone: string;
// Twilio 设置
twilioAccountSid: string;
twilioAuthToken: string;
twilioPhoneNumber: string;
twilioWebhookUrl: string;
enableVideoCall: boolean;
enableVoiceCall: boolean;
// 支付设置
enableAlipay: boolean;
enableWechatPay: boolean;
enableCreditCard: boolean;
enablePaypal: boolean;
paymentFeeRate: number;
minimumPayment: number;
// 业务设置
defaultCallDuration: number;
maxCallDuration: number;
translatorCommissionRate: number;
autoAssignTranslator: boolean;
requirePaymentUpfront: boolean;
// 通知设置
emailNotifications: boolean;
smsNotifications: boolean;
systemMaintenanceMode: boolean;
// 安全设置
enableTwoFactorAuth: boolean;
sessionTimeout: number;
maxLoginAttempts: number;
passwordMinLength: number;
}
const SystemSettings: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [config, setConfig] = useState<SystemConfig | null>(null);
const [activeTab, setActiveTab] = useState('basic');
// 模拟配置数据
const mockConfig: SystemConfig = {
siteName: 'Twilio翻译平台',
siteDescription: '专业的实时翻译服务平台',
supportEmail: 'support@twiliotranslate.com',
supportPhone: '400-123-4567',
defaultLanguage: 'zh-CN',
timezone: 'Asia/Shanghai',
twilioAccountSid: 'AC1234567890abcdef1234567890abcdef',
twilioAuthToken: '********************************',
twilioPhoneNumber: '+86-138-0013-8000',
twilioWebhookUrl: 'https://api.twiliotranslate.com/webhook',
enableVideoCall: true,
enableVoiceCall: true,
enableAlipay: true,
enableWechatPay: true,
enableCreditCard: true,
enablePaypal: false,
paymentFeeRate: 3.0,
minimumPayment: 10.0,
defaultCallDuration: 30,
maxCallDuration: 120,
translatorCommissionRate: 70.0,
autoAssignTranslator: true,
requirePaymentUpfront: true,
emailNotifications: true,
smsNotifications: false,
systemMaintenanceMode: false,
enableTwoFactorAuth: true,
sessionTimeout: 30,
maxLoginAttempts: 5,
passwordMinLength: 8
};
const fetchConfig = async () => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setConfig(mockConfig);
form.setFieldsValue(mockConfig);
message.success('配置加载成功');
} catch (error) {
message.error('加载配置失败');
} finally {
setLoading(false);
}
};
const handleSave = async (values: SystemConfig) => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1500));
setConfig(values);
message.success('配置保存成功');
} catch (error) {
message.error('保存配置失败');
} finally {
setLoading(false);
}
};
const testTwilioConnection = async () => {
message.loading('测试Twilio连接中...', 2);
await new Promise(resolve => setTimeout(resolve, 2000));
message.success('Twilio连接测试成功');
};
useEffect(() => {
fetchConfig();
}, []);
const renderBasicSettings = () => (
<Card title="基础设置" extra={<SettingOutlined />}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="网站名称"
name="siteName"
rules={[{ required: true, message: '请输入网站名称' }]}
>
<Input placeholder="请输入网站名称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="默认语言"
name="defaultLanguage"
rules={[{ required: true, message: '请选择默认语言' }]}
>
<Select placeholder="请选择默认语言">
<Option value="zh-CN"></Option>
<Option value="en-US">English</Option>
<Option value="ja-JP"></Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
label="网站描述"
name="siteDescription"
>
<TextArea rows={3} placeholder="请输入网站描述" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="客服邮箱"
name="supportEmail"
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
>
<Input placeholder="请输入客服邮箱" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="客服电话"
name="supportPhone"
>
<Input placeholder="请输入客服电话" />
</Form.Item>
</Col>
</Row>
<Form.Item
label="时区"
name="timezone"
>
<Select placeholder="请选择时区">
<Option value="Asia/Shanghai">Asia/Shanghai (UTC+8)</Option>
<Option value="America/New_York">America/New_York (UTC-5)</Option>
<Option value="Europe/London">Europe/London (UTC+0)</Option>
</Select>
</Form.Item>
</Card>
);
const renderTwilioSettings = () => (
<Card
title="Twilio配置"
extra={
<Space>
<Badge status="success" text="已连接" />
<Button size="small" onClick={testTwilioConnection}>
</Button>
</Space>
}
>
<Alert
message="Twilio配置说明"
description="请确保您的Twilio账户有足够的余额,并且已经验证了电话号码。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Account SID"
name="twilioAccountSid"
rules={[{ required: true, message: '请输入Account SID' }]}
>
<Input placeholder="请输入Twilio Account SID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Auth Token"
name="twilioAuthToken"
rules={[{ required: true, message: '请输入Auth Token' }]}
>
<Input.Password placeholder="请输入Twilio Auth Token" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="电话号码"
name="twilioPhoneNumber"
rules={[{ required: true, message: '请输入电话号码' }]}
>
<Input placeholder="请输入Twilio电话号码" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Webhook URL"
name="twilioWebhookUrl"
>
<Input placeholder="请输入Webhook URL" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="启用语音通话"
name="enableVoiceCall"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="启用视频通话"
name="enableVideoCall"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
</Row>
</Card>
);
const renderPaymentSettings = () => (
<Card title="支付配置" extra={<DollarOutlined />}>
<Row gutter={16}>
<Col span={6}>
<Form.Item
label="支付宝"
name="enableAlipay"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
label="微信支付"
name="enableWechatPay"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
label="信用卡"
name="enableCreditCard"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
label="PayPal"
name="enablePaypal"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="手续费率 (%)"
name="paymentFeeRate"
rules={[{ required: true, message: '请输入手续费率' }]}
>
<InputNumber
min={0}
max={10}
step={0.1}
precision={1}
style={{ width: '100%' }}
placeholder="请输入手续费率"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="最低支付金额"
name="minimumPayment"
rules={[{ required: true, message: '请输入最低支付金额' }]}
>
<InputNumber
min={1}
step={1}
style={{ width: '100%' }}
placeholder="请输入最低支付金额"
addonAfter="CNY"
/>
</Form.Item>
</Col>
</Row>
</Card>
);
const renderBusinessSettings = () => (
<Card title="业务配置" extra={<GlobalOutlined />}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="默认通话时长 (分钟)"
name="defaultCallDuration"
rules={[{ required: true, message: '请输入默认通话时长' }]}
>
<InputNumber
min={5}
max={180}
style={{ width: '100%' }}
placeholder="请输入默认通话时长"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="最大通话时长 (分钟)"
name="maxCallDuration"
rules={[{ required: true, message: '请输入最大通话时长' }]}
>
<InputNumber
min={10}
max={300}
style={{ width: '100%' }}
placeholder="请输入最大通话时长"
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="译员佣金比例 (%)"
name="translatorCommissionRate"
rules={[{ required: true, message: '请输入译员佣金比例' }]}
>
<InputNumber
min={50}
max={90}
step={1}
style={{ width: '100%' }}
placeholder="请输入译员佣金比例"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="自动分配译员"
name="autoAssignTranslator"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
</Row>
<Form.Item
label="要求预付费"
name="requirePaymentUpfront"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Card>
);
const renderNotificationSettings = () => (
<Card title="通知设置" extra={<NotificationOutlined />}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="邮件通知"
name="emailNotifications"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="短信通知"
name="smsNotifications"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
</Row>
<Form.Item
label="系统维护模式"
name="systemMaintenanceMode"
valuePropName="checked"
>
<Switch />
</Form.Item>
{config?.systemMaintenanceMode && (
<Alert
message="维护模式已启用"
description="系统当前处于维护模式,用户无法正常使用服务。"
type="warning"
showIcon
/>
)}
</Card>
);
const renderSecuritySettings = () => (
<Card title="安全设置" extra={<SecurityScanOutlined />}>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="启用双因素认证"
name="enableTwoFactorAuth"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="会话超时 (分钟)"
name="sessionTimeout"
rules={[{ required: true, message: '请输入会话超时时间' }]}
>
<InputNumber
min={5}
max={120}
style={{ width: '100%' }}
placeholder="请输入会话超时时间"
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="最大登录尝试次数"
name="maxLoginAttempts"
rules={[{ required: true, message: '请输入最大登录尝试次数' }]}
>
<InputNumber
min={3}
max={10}
style={{ width: '100%' }}
placeholder="请输入最大登录尝试次数"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="密码最小长度"
name="passwordMinLength"
rules={[{ required: true, message: '请输入密码最小长度' }]}
>
<InputNumber
min={6}
max={20}
style={{ width: '100%' }}
placeholder="请输入密码最小长度"
/>
</Form.Item>
</Col>
</Row>
</Card>
);
const tabItems = [
{
key: 'basic',
label: '基础设置',
children: renderBasicSettings()
},
{
key: 'twilio',
label: 'Twilio配置',
children: renderTwilioSettings()
},
{
key: 'payment',
label: '支付配置',
children: renderPaymentSettings()
},
{
key: 'business',
label: '业务配置',
children: renderBusinessSettings()
},
{
key: 'notification',
label: '通知设置',
children: renderNotificationSettings()
},
{
key: 'security',
label: '安全设置',
children: renderSecuritySettings()
}
];
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Form
form={form}
layout="vertical"
onFinish={handleSave}
initialValues={config || {}}
>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
/>
<Divider />
<Space>
<Button
type="primary"
htmlType="submit"
loading={loading}
icon={<SaveOutlined />}
size="large"
>
</Button>
<Button
onClick={fetchConfig}
loading={loading}
icon={<ReloadOutlined />}
size="large"
>
</Button>
</Space>
</Form>
</div>
);
};
export default SystemSettings;
@@ -0,0 +1,477 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
Row,
Col,
Statistic,
Tooltip,
Avatar,
Rate,
Progress,
Badge
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
EditOutlined,
ReloadOutlined,
UserOutlined,
StarOutlined,
TrophyOutlined,
GlobalOutlined,
PhoneOutlined,
VideoCameraOutlined,
FileTextOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Title } = Typography;
const { Option } = Select;
interface Translator {
id: string;
name: string;
email: string;
phone: string;
avatar?: string;
languages: string[];
specialties: string[];
rating: number;
totalCalls: number;
completedCalls: number;
totalEarnings: number;
status: 'available' | 'busy' | 'offline';
experience: number; // 年
certifications: string[];
joinDate: string;
lastActiveTime: string;
hourlyRate: number;
}
const TranslatorList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [translators, setTranslators] = useState<Translator[]>([]);
const [filteredTranslators, setFilteredTranslators] = useState<Translator[]>([]);
const [searchText, setSearchText] = useState('');
const [languageFilter, setLanguageFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
// 模拟数据
const mockTranslators: Translator[] = [
{
id: '1',
name: '王译员',
email: 'wang@translator.com',
phone: '13800138001',
languages: ['中文', '英文', '日文'],
specialties: ['商务', '技术', '医疗'],
rating: 4.8,
totalCalls: 156,
completedCalls: 152,
totalEarnings: 15600,
status: 'available',
experience: 5,
certifications: ['CATTI二级', '商务英语高级'],
joinDate: '2023-06-15',
lastActiveTime: '2024-01-15 14:45:00',
hourlyRate: 150
},
{
id: '2',
name: '李译员',
email: 'li@translator.com',
phone: '13800138002',
languages: ['中文', '英文', '法文'],
specialties: ['法律', '文学', '艺术'],
rating: 4.9,
totalCalls: 89,
completedCalls: 87,
totalEarnings: 12400,
status: 'busy',
experience: 7,
certifications: ['CATTI一级', '法语专业八级'],
joinDate: '2023-08-20',
lastActiveTime: '2024-01-15 13:15:00',
hourlyRate: 180
},
{
id: '3',
name: '张译员',
email: 'zhang@translator.com',
phone: '13800138003',
languages: ['中文', '德文', '俄文'],
specialties: ['工程', '科技', '学术'],
rating: 4.7,
totalCalls: 67,
completedCalls: 65,
totalEarnings: 8900,
status: 'available',
experience: 3,
certifications: ['德语专业八级', '俄语专业六级'],
joinDate: '2023-10-01',
lastActiveTime: '2024-01-15 16:20:00',
hourlyRate: 120
},
{
id: '4',
name: '赵译员',
email: 'zhao@translator.com',
phone: '13800138004',
languages: ['中文', '韩文'],
specialties: ['娱乐', '时尚', '旅游'],
rating: 4.6,
totalCalls: 45,
completedCalls: 43,
totalEarnings: 5400,
status: 'offline',
experience: 2,
certifications: ['韩语TOPIK6级'],
joinDate: '2023-11-15',
lastActiveTime: '2024-01-14 18:30:00',
hourlyRate: 100
},
{
id: '5',
name: '孙译员',
email: 'sun@translator.com',
phone: '13800138005',
languages: ['中文', '西班牙文', '葡萄牙文'],
specialties: ['体育', '新闻', '政治'],
rating: 4.5,
totalCalls: 78,
completedCalls: 74,
totalEarnings: 9200,
status: 'available',
experience: 4,
certifications: ['西语专业八级', 'DELE C2'],
joinDate: '2023-09-10',
lastActiveTime: '2024-01-15 15:10:00',
hourlyRate: 130
}
];
const fetchTranslators = async () => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setTranslators(mockTranslators);
setFilteredTranslators(mockTranslators);
message.success('译员列表加载成功');
} catch (error) {
message.error('加载译员列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTranslators();
}, []);
useEffect(() => {
let filtered = translators;
if (searchText) {
filtered = filtered.filter(translator =>
translator.name.toLowerCase().includes(searchText.toLowerCase()) ||
translator.email.toLowerCase().includes(searchText.toLowerCase()) ||
translator.languages.some(lang => lang.includes(searchText)) ||
translator.specialties.some(spec => spec.includes(searchText))
);
}
if (languageFilter !== 'all') {
filtered = filtered.filter(translator =>
translator.languages.includes(languageFilter)
);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(translator => translator.status === statusFilter);
}
setFilteredTranslators(filtered);
}, [translators, searchText, languageFilter, statusFilter]);
const getStatusTag = (status: string) => {
const statusConfig = {
available: { color: 'green', text: '可用' },
busy: { color: 'orange', text: '忙碌' },
offline: { color: 'red', text: '离线' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Badge status={status === 'available' ? 'success' : status === 'busy' ? 'processing' : 'error'} text={config.text} />;
};
const columns: ColumnsType<Translator> = [
{
title: '译员信息',
key: 'translatorInfo',
width: 250,
render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar
size={50}
src={record.avatar}
icon={<UserOutlined />}
style={{ marginRight: 12 }}
/>
<div>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
{record.name}
</div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: 4 }}>
{record.email}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{record.experience}
</div>
</div>
</div>
)
},
{
title: '语言能力',
dataIndex: 'languages',
key: 'languages',
width: 200,
render: (languages) => (
<div>
{languages.map((lang: string) => (
<Tag key={lang} color="blue" style={{ marginBottom: 4 }}>
{lang}
</Tag>
))}
</div>
)
},
{
title: '专业领域',
dataIndex: 'specialties',
key: 'specialties',
width: 180,
render: (specialties) => (
<div>
{specialties.map((spec: string) => (
<Tag key={spec} color="purple" style={{ marginBottom: 4 }}>
{spec}
</Tag>
))}
</div>
)
},
{
title: '评分',
dataIndex: 'rating',
key: 'rating',
width: 120,
render: (rating) => (
<div>
<Rate disabled defaultValue={rating} style={{ fontSize: '14px' }} />
<div style={{ fontSize: '12px', color: '#666' }}>
{rating}/5.0
</div>
</div>
)
},
{
title: '工作统计',
key: 'stats',
width: 150,
render: (_, record) => (
<div>
<div style={{ fontSize: '12px', marginBottom: 4 }}>
: {record.totalCalls}
</div>
<div style={{ fontSize: '12px', marginBottom: 4 }}>
: {((record.completedCalls / record.totalCalls) * 100).toFixed(1)}%
</div>
<div style={{ fontSize: '12px' }}>
: ¥{record.totalEarnings.toLocaleString()}
</div>
</div>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: getStatusTag
},
{
title: '时薪',
dataIndex: 'hourlyRate',
key: 'hourlyRate',
width: 100,
render: (rate) => `¥${rate}/小时`
},
{
title: '操作',
key: 'action',
width: 150,
render: (_, record) => (
<Space>
<Tooltip title="查看详情">
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
size="small"
icon={<EditOutlined />}
/>
</Tooltip>
<Tooltip title="分配任务">
<Button
size="small"
icon={<PhoneOutlined />}
disabled={record.status !== 'available'}
/>
</Tooltip>
</Space>
),
},
];
const stats = {
total: filteredTranslators.length,
available: filteredTranslators.filter(t => t.status === 'available').length,
busy: filteredTranslators.filter(t => t.status === 'busy').length,
averageRating: filteredTranslators.reduce((sum, t) => sum + t.rating, 0) / filteredTranslators.length || 0,
totalEarnings: filteredTranslators.reduce((sum, t) => sum + t.totalEarnings, 0)
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={6}>
<Card>
<Statistic
title="总译员数"
value={stats.total}
prefix={<UserOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="可用译员"
value={stats.available}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="平均评分"
value={stats.averageRating}
precision={1}
prefix={<StarOutlined />}
valueStyle={{ color: '#faad14' }}
suffix="/5.0"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="总收入"
value={stats.totalEarnings}
prefix="¥"
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
</Row>
<Card>
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={8}>
<Input
placeholder="搜索译员姓名、邮箱、语言、专业..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={6}>
<Select
value={languageFilter}
onChange={setLanguageFilter}
style={{ width: '100%' }}
placeholder="语言筛选"
>
<Option value="all"></Option>
<Option value="英文"></Option>
<Option value="日文"></Option>
<Option value="法文"></Option>
<Option value="德文"></Option>
<Option value="韩文"></Option>
<Option value="西班牙文">西</Option>
<Option value="俄文"></Option>
</Select>
</Col>
<Col span={6}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="available"></Option>
<Option value="busy"></Option>
<Option value="offline">线</Option>
</Select>
</Col>
<Col span={4}>
<Button
icon={<ReloadOutlined />}
onClick={fetchTranslators}
loading={loading}
style={{ width: '100%' }}
>
</Button>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredTranslators}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredTranslators.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
</div>
);
};
export default TranslatorList;
@@ -0,0 +1,654 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
Button,
Input,
Select,
Space,
Tag,
Typography,
Modal,
message,
DatePicker,
Row,
Col,
Statistic,
Tooltip,
Avatar,
Form,
Switch
} from 'antd';
import {
SearchOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
UserOutlined,
MailOutlined,
PhoneOutlined,
LockOutlined,
UnlockOutlined
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Title } = Typography;
const { Option } = Select;
const { RangePicker } = DatePicker;
interface User {
id: string;
username: string;
email: string;
phone: string;
realName: string;
role: 'admin' | 'translator' | 'customer' | 'manager';
status: 'active' | 'inactive' | 'banned';
avatar?: string;
lastLoginTime?: string;
registrationTime: string;
totalCalls: number;
totalSpent: number;
preferredLanguages: string[];
notes?: string;
}
const UserList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [filteredUsers, setFilteredUsers] = useState<User[]>([]);
const [searchText, setSearchText] = useState('');
const [roleFilter, setRoleFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [modalVisible, setModalVisible] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [form] = Form.useForm();
// 模拟数据
const mockUsers: User[] = [
{
id: '1',
username: 'admin001',
email: 'admin@example.com',
phone: '13800138001',
realName: '系统管理员',
role: 'admin',
status: 'active',
lastLoginTime: '2024-01-15 15:30:00',
registrationTime: '2023-01-01 10:00:00',
totalCalls: 0,
totalSpent: 0,
preferredLanguages: ['中文', '英文'],
notes: '系统管理员账户'
},
{
id: '2',
username: 'translator_wang',
email: 'wang@translator.com',
phone: '13800138002',
realName: '王译员',
role: 'translator',
status: 'active',
lastLoginTime: '2024-01-15 14:45:00',
registrationTime: '2023-06-15 09:30:00',
totalCalls: 156,
totalSpent: 0,
preferredLanguages: ['中文', '英文', '日文'],
notes: '资深英日翻译,5年经验'
},
{
id: '3',
username: 'customer_zhang',
email: 'zhang@customer.com',
phone: '13800138003',
realName: '张先生',
role: 'customer',
status: 'active',
lastLoginTime: '2024-01-15 16:20:00',
registrationTime: '2023-12-01 14:20:00',
totalCalls: 23,
totalSpent: 1580.50,
preferredLanguages: ['中文', '英文'],
notes: '企业客户,经常需要商务翻译'
},
{
id: '4',
username: 'translator_li',
email: 'li@translator.com',
phone: '13800138004',
realName: '李译员',
role: 'translator',
status: 'active',
lastLoginTime: '2024-01-15 13:15:00',
registrationTime: '2023-08-20 11:45:00',
totalCalls: 89,
totalSpent: 0,
preferredLanguages: ['中文', '英文', '法文'],
notes: '法语专业译员'
},
{
id: '5',
username: 'customer_li',
email: 'li_customer@example.com',
phone: '13800138005',
realName: '李女士',
role: 'customer',
status: 'inactive',
lastLoginTime: '2024-01-10 10:30:00',
registrationTime: '2023-11-15 16:00:00',
totalCalls: 8,
totalSpent: 420.00,
preferredLanguages: ['中文', '韩文'],
notes: '个人用户,偶尔使用'
},
{
id: '6',
username: 'manager001',
email: 'manager@example.com',
phone: '13800138006',
realName: '业务经理',
role: 'manager',
status: 'active',
lastLoginTime: '2024-01-15 17:00:00',
registrationTime: '2023-03-01 08:00:00',
totalCalls: 0,
totalSpent: 0,
preferredLanguages: ['中文', '英文'],
notes: '负责客户关系管理'
}
];
const fetchUsers = async () => {
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setUsers(mockUsers);
setFilteredUsers(mockUsers);
message.success('用户列表加载成功');
} catch (error) {
message.error('加载用户列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
let filtered = users;
if (searchText) {
filtered = filtered.filter(user =>
user.username.toLowerCase().includes(searchText.toLowerCase()) ||
user.email.toLowerCase().includes(searchText.toLowerCase()) ||
user.realName.includes(searchText) ||
user.phone.includes(searchText)
);
}
if (roleFilter !== 'all') {
filtered = filtered.filter(user => user.role === roleFilter);
}
if (statusFilter !== 'all') {
filtered = filtered.filter(user => user.status === statusFilter);
}
setFilteredUsers(filtered);
}, [users, searchText, roleFilter, statusFilter]);
const getRoleTag = (role: string) => {
const roleConfig = {
admin: { color: 'red', text: '管理员' },
manager: { color: 'purple', text: '经理' },
translator: { color: 'blue', text: '译员' },
customer: { color: 'green', text: '客户' }
};
const config = roleConfig[role as keyof typeof roleConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const getStatusTag = (status: string) => {
const statusConfig = {
active: { color: 'green', text: '活跃' },
inactive: { color: 'orange', text: '非活跃' },
banned: { color: 'red', text: '已禁用' }
};
const config = statusConfig[status as keyof typeof statusConfig];
return <Tag color={config.color}>{config.text}</Tag>;
};
const handleStatusToggle = (userId: string, newStatus: User['status']) => {
const updatedUsers = users.map(user =>
user.id === userId ? { ...user, status: newStatus } : user
);
setUsers(updatedUsers);
message.success('用户状态更新成功');
};
const handleEdit = (user: User) => {
setEditingUser(user);
form.setFieldsValue(user);
setModalVisible(true);
};
const handleDelete = (user: User) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除用户 "${user.realName}" 吗?此操作不可恢复。`,
onOk: () => {
const newUsers = users.filter(u => u.id !== user.id);
setUsers(newUsers);
message.success('用户删除成功');
}
});
};
const handleSave = async (values: any) => {
try {
if (editingUser) {
const updatedUsers = users.map(user =>
user.id === editingUser.id ? { ...user, ...values } : user
);
setUsers(updatedUsers);
message.success('用户信息更新成功');
} else {
const newUser: User = {
id: Date.now().toString(),
...values,
registrationTime: new Date().toLocaleString(),
totalCalls: 0,
totalSpent: 0
};
setUsers([...users, newUser]);
message.success('用户创建成功');
}
setModalVisible(false);
setEditingUser(null);
form.resetFields();
} catch (error) {
message.error('保存失败');
}
};
const columns: ColumnsType<User> = [
{
title: '用户信息',
key: 'userInfo',
width: 250,
render: (_, record) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Avatar
size={40}
src={record.avatar}
icon={<UserOutlined />}
style={{ marginRight: 12 }}
/>
<div>
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
{record.realName}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
@{record.username}
</div>
</div>
</div>
)
},
{
title: '联系方式',
key: 'contact',
width: 200,
render: (_, record) => (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
<MailOutlined style={{ marginRight: 4, color: '#666' }} />
<span style={{ fontSize: '12px' }}>{record.email}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<PhoneOutlined style={{ marginRight: 4, color: '#666' }} />
<span style={{ fontSize: '12px' }}>{record.phone}</span>
</div>
</div>
)
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
width: 100,
render: getRoleTag
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: getStatusTag
},
{
title: '统计信息',
key: 'stats',
width: 150,
render: (_, record) => (
<div>
<div style={{ fontSize: '12px', marginBottom: 4 }}>
: {record.totalCalls}
</div>
<div style={{ fontSize: '12px' }}>
: ¥{record.totalSpent.toFixed(2)}
</div>
</div>
)
},
{
title: '最后登录',
dataIndex: 'lastLoginTime',
key: 'lastLoginTime',
width: 150,
render: (time) => time || '从未登录'
},
{
title: '操作',
key: 'action',
width: 200,
render: (_, record) => (
<Space>
<Tooltip title="查看详情">
<Button
type="primary"
size="small"
icon={<EyeOutlined />}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
/>
</Tooltip>
<Tooltip title={record.status === 'active' ? '禁用' : '启用'}>
<Button
size="small"
icon={record.status === 'active' ? <LockOutlined /> : <UnlockOutlined />}
onClick={() => handleStatusToggle(
record.id,
record.status === 'active' ? 'banned' : 'active'
)}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
/>
</Tooltip>
</Space>
),
},
];
const stats = {
total: filteredUsers.length,
admin: filteredUsers.filter(u => u.role === 'admin').length,
translator: filteredUsers.filter(u => u.role === 'translator').length,
customer: filteredUsers.filter(u => u.role === 'customer').length,
active: filteredUsers.filter(u => u.status === 'active').length
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={16} style={{ marginBottom: '24px' }}>
<Col span={5}>
<Card>
<Statistic
title="总用户数"
value={stats.total}
prefix={<UserOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="管理员"
value={stats.admin}
valueStyle={{ color: '#cf1322' }}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="译员"
value={stats.translator}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="客户"
value={stats.customer}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic
title="活跃用户"
value={stats.active}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
</Row>
<Card>
<Row gutter={16} style={{ marginBottom: '16px' }}>
<Col span={6}>
<Input
placeholder="搜索用户名、邮箱、姓名、电话..."
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={4}>
<Select
value={roleFilter}
onChange={setRoleFilter}
style={{ width: '100%' }}
placeholder="角色筛选"
>
<Option value="all"></Option>
<Option value="admin"></Option>
<Option value="manager"></Option>
<Option value="translator"></Option>
<Option value="customer"></Option>
</Select>
</Col>
<Col span={4}>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: '100%' }}
placeholder="状态筛选"
>
<Option value="all"></Option>
<Option value="active"></Option>
<Option value="inactive"></Option>
<Option value="banned"></Option>
</Select>
</Col>
<Col span={6}>
<RangePicker style={{ width: '100%' }} placeholder={['注册开始日期', '注册结束日期']} />
</Col>
<Col span={4}>
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditingUser(null);
form.resetFields();
setModalVisible(true);
}}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={fetchUsers}
loading={loading}
/>
</Space>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredUsers}
loading={loading}
rowKey="id"
scroll={{ x: 1200 }}
pagination={{
total: filteredUsers.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total} 条记录`
}}
/>
</Card>
<Modal
title={editingUser ? '编辑用户' : '新增用户'}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setEditingUser(null);
form.resetFields();
}}
onOk={() => form.submit()}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSave}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="realName"
label="真实姓名"
rules={[{ required: true, message: '请输入真实姓名' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' }
]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="phone"
label="电话"
rules={[{ required: true, message: '请输入电话号码' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select>
<Option value="admin"></Option>
<Option value="manager"></Option>
<Option value="translator"></Option>
<Option value="customer"></Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select>
<Option value="active"></Option>
<Option value="inactive"></Option>
<Option value="banned"></Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="preferredLanguages"
label="偏好语言"
>
<Select mode="multiple" placeholder="选择偏好语言">
<Option value="中文"></Option>
<Option value="英文"></Option>
<Option value="日文"></Option>
<Option value="韩文"></Option>
<Option value="法文"></Option>
<Option value="德文"></Option>
<Option value="西班牙文">西</Option>
<Option value="俄文"></Option>
</Select>
</Form.Item>
<Form.Item
name="notes"
label="备注"
>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default UserList;
+166 -64
View File
@@ -1,3 +1,69 @@
// 用户相关类型
export interface User {
id: string;
username: string;
email: string;
phone: string;
fullName: string;
avatar?: string;
role: 'user' | 'translator' | 'admin';
status: 'active' | 'inactive' | 'suspended';
preferredLanguages: string[];
createdAt: string;
updatedAt: string;
lastLoginAt?: string;
totalCalls: number;
totalSpent: number;
rating: number;
verificationStatus: 'pending' | 'verified' | 'rejected';
}
// 译员相关类型
export interface Translator {
id: string;
userId: string;
fullName: string;
email: string;
phone: string;
avatar?: string;
languages: string[];
specializations: string[];
status: 'available' | 'busy' | 'offline' | 'suspended';
rating: number;
totalCalls: number;
totalEarnings: number;
hourlyRate: number;
certifications: Certification[];
workingHours: WorkingHours;
createdAt: string;
updatedAt: string;
}
export interface Certification {
id: string;
name: string;
issuer: string;
issuedAt: string;
expiresAt?: string;
documentUrl?: string;
verified: boolean;
}
export interface WorkingHours {
monday: TimeSlot[];
tuesday: TimeSlot[];
wednesday: TimeSlot[];
thursday: TimeSlot[];
friday: TimeSlot[];
saturday: TimeSlot[];
sunday: TimeSlot[];
}
export interface TimeSlot {
start: string; // HH:mm
end: string; // HH:mm
}
// 通话相关类型
export interface TranslationCall {
id: string;
@@ -5,13 +71,13 @@ export interface TranslationCall {
callId: string;
clientName: string;
clientPhone: string;
type: 'human' | 'ai';
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'refunded';
type: 'ai' | 'human';
status: 'pending' | 'connecting' | 'ongoing' | 'completed' | 'failed' | 'cancelled';
sourceLanguage: string;
targetLanguage: string;
startTime: string;
endTime?: string;
duration?: number;
duration?: number; // seconds
cost: number;
rating?: number;
feedback?: string;
@@ -21,14 +87,12 @@ export interface TranslationCall {
recordingUrl?: string;
transcription?: string;
translation?: string;
// 管理员相关字段
// 管理员字段
adminNotes?: string;
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
refundAmount: number;
qualityScore: number;
issues: string[];
createdAt?: string;
updatedAt?: string;
refundAmount?: number;
qualityScore?: number;
issues?: string[];
}
// 文档翻译相关类型
@@ -41,11 +105,11 @@ export interface DocumentTranslation {
translatedFileUrl?: string;
sourceLanguage: string;
targetLanguage: string;
status: 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed';
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
progress: number;
quality: 'basic' | 'professional' | 'premium';
urgency: 'normal' | 'urgent' | 'emergency';
estimatedTime: number;
urgency: 'low' | 'normal' | 'high' | 'urgent';
estimatedTime?: number; // minutes
actualTime?: number;
cost: number;
translatorId?: string;
@@ -54,82 +118,113 @@ export interface DocumentTranslation {
feedback?: string;
createdAt: string;
completedAt?: string;
// 管理员相关字段
// 管理员字段
adminNotes?: string;
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
refundAmount: number;
qualityScore: number;
issues: string[];
refundAmount?: number;
qualityScore?: number;
issues?: string[];
retranslationCount?: number;
clientName?: string;
clientEmail?: string;
clientPhone?: string;
updatedAt?: string;
}
// 预约相关类型
export interface Appointment {
id: string;
userId: string;
translatorId: string;
translatorId?: string;
title: string;
description: string;
type: string;
description?: string;
type: 'interpretation' | 'translation' | 'consultation';
sourceLanguage: string;
targetLanguage: string;
startTime: string;
endTime: string;
status: string;
status: 'pending' | 'confirmed' | 'in_progress' | 'completed' | 'cancelled';
cost: number;
meetingUrl?: string;
notes?: string;
reminderSent: boolean;
createdAt: string;
updatedAt?: string;
// 管理员相关字段
clientName: string;
clientEmail: string;
clientPhone: string;
translatorName: string;
translatorEmail: string;
translatorPhone: string;
updatedAt: string;
// 管理员字段
clientName?: string;
clientEmail?: string;
clientPhone?: string;
translatorName?: string;
translatorEmail?: string;
translatorPhone?: string;
adminNotes?: string;
paymentStatus: string;
refundAmount: number;
qualityScore: number;
issues: string[];
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
refundAmount?: number;
qualityScore?: number;
issues?: string[];
rating?: number;
feedback?: string;
location?: string;
urgency: string;
urgency: 'low' | 'normal' | 'high' | 'urgent';
}
// 用户类型
export interface User {
// 支付相关类型
export interface Payment {
id: string;
name: string;
email: string;
phone?: string;
role: 'client' | 'translator' | 'admin';
status: 'active' | 'inactive' | 'suspended';
userId: string;
type: 'call' | 'document' | 'appointment';
relatedId: string; // callId, documentId, or appointmentId
amount: number;
currency: 'CNY' | 'USD' | 'EUR';
status: 'pending' | 'processing' | 'completed' | 'failed' | 'refunded';
paymentMethod: 'wechat' | 'alipay' | 'credit_card' | 'bank_transfer';
transactionId?: string;
refundAmount?: number;
refundReason?: string;
createdAt: string;
updatedAt?: string;
completedAt?: string;
// 管理员字段
adminNotes?: string;
clientName?: string;
clientEmail?: string;
description?: string;
}
// 译员类型
export interface Translator {
id: string;
name: string;
email: string;
phone: string;
languages: string[];
specializations: string[];
rating: number;
hourlyRate: number;
status: 'available' | 'busy' | 'offline';
totalJobs: number;
successRate: number;
createdAt: string;
// 系统配置类型
export interface SystemConfig {
// 基本设置
siteName: string;
siteDescription: string;
supportEmail: string;
supportPhone: string;
// Twilio设置
twilioAccountSid: string;
twilioAuthToken: string;
twilioPhoneNumber: string;
// 支付设置
stripePublishableKey: string;
stripeSecretKey: string;
wechatPayMerchantId: string;
alipayAppId: string;
// 业务设置
defaultCallRate: number;
defaultDocumentRate: number;
maxCallDuration: number;
maxFileSize: number;
supportedLanguages: string[];
// 通知设置
emailNotifications: boolean;
smsNotifications: boolean;
pushNotifications: boolean;
// 安全设置
requireEmailVerification: boolean;
requirePhoneVerification: boolean;
maxLoginAttempts: number;
sessionTimeout: number;
}
// API响应类型
@@ -141,16 +236,23 @@ export interface ApiResponse<T = any> {
}
// 分页类型
export interface PaginationParams {
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
total?: number;
totalPages: number;
}
// 搜索参数类型
export interface SearchParams {
keyword?: string;
status?: string;
dateRange?: [string, string];
[key: string]: any;
// 统计数据类型
export interface DashboardStats {
totalUsers: number;
totalTranslators: number;
totalCalls: number;
totalDocuments: number;
totalRevenue: number;
activeUsers: number;
onlineTranslators: number;
ongoingCalls: number;
pendingDocuments: number;
}
+180 -105
View File
@@ -1,140 +1,226 @@
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse } from '../types';
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse, PaginatedResponse } from '../types';
// API基础URL
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
// API基础URL配置
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api';
// API请求工具类
class ApiManager {
// HTTP请求方法
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
// 请求选项
interface RequestOptions {
method?: HttpMethod;
headers?: Record<string, string>;
body?: any;
params?: Record<string, any>;
}
// API客户端类
export class ApiClient {
private baseURL: string;
private defaultHeaders: Record<string, string>;
constructor(baseURL: string = API_BASE_URL) {
constructor(baseURL = API_BASE_URL) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
// 通用请求方法
// 设置授权令牌
setAuthToken(token: string): void {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
}
// 移除授权令牌
removeAuthToken(): void {
delete this.defaultHeaders['Authorization'];
}
// 构建URL参数
private buildURL(endpoint: string, params?: Record<string, any>): string {
const url = new URL(`${this.baseURL}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
return url.toString();
}
// 发送HTTP请求
private async request<T>(
endpoint: string,
options: RequestInit = {}
endpoint: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
const data = await response.json();
const { method = 'GET', headers = {}, body, params } = options;
const url = this.buildURL(endpoint, params);
const requestHeaders = {
...this.defaultHeaders,
...headers,
};
if (!response.ok) {
throw new Error(data.message || '请求失败');
const requestInit: RequestInit = {
method,
headers: requestHeaders,
};
if (body && method !== 'GET') {
if (body instanceof FormData) {
// 对于FormData,不设置Content-Type,让浏览器自动设置
delete requestHeaders['Content-Type'];
requestInit.body = body;
} else {
requestInit.body = JSON.stringify(body);
}
}
const response = await fetch(url, requestInit);
// 检查响应状态
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
return {
success: true,
data,
message: '操作成功',
};
} catch (error) {
console.error('API请求错误:', error);
console.error('API请求失败:', error);
return {
success: false,
data: null as any,
message: error instanceof Error ? error.message : '网络错误',
error: error instanceof Error ? error.message : '未知错误',
};
}
}
// GET请求
async get<T>(endpoint: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'GET', params });
}
// POST请求
async post<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'POST', body });
}
// PUT请求
async put<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'PUT', body });
}
// PATCH请求
async patch<T>(endpoint: string, body?: any): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'PATCH', body });
}
// DELETE请求
async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
// 上传文件
async upload<T>(endpoint: string, file: File, additionalData?: Record<string, any>): Promise<ApiResponse<T>> {
const formData = new FormData();
formData.append('file', file);
if (additionalData) {
Object.entries(additionalData).forEach(([key, value]) => {
formData.append(key, String(value));
});
}
return this.request<T>(endpoint, {
method: 'POST',
body: formData
});
}
// 获取分页数据
async getPaginated<T>(
endpoint: string,
page = 1,
pageSize = 10,
params?: Record<string, any>
): Promise<ApiResponse<PaginatedResponse<T>>> {
const paginationParams = {
page,
pageSize,
...params,
};
return this.get<PaginatedResponse<T>>(endpoint, paginationParams);
}
// 通话管理API
async getCall(id: string): Promise<ApiResponse<TranslationCall>> {
return this.request<TranslationCall>(`/calls/${id}`);
return this.get<TranslationCall>(`/calls/${id}`);
}
async updateCall(id: string, data: Partial<TranslationCall>): Promise<ApiResponse<TranslationCall>> {
return this.request<TranslationCall>(`/calls/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return this.post<TranslationCall>(`/calls/${id}`, data);
}
async deleteCall(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${id}`, {
method: 'DELETE',
});
return this.delete<boolean>(`/calls/${id}`);
}
async processRefund(callId: string, amount: number, reason: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${callId}/refund`, {
method: 'POST',
body: JSON.stringify({ amount, reason }),
});
return this.post<boolean>(`/calls/${callId}/refund`, { amount, reason });
}
async addCallNote(callId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/calls/${callId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
return this.post<boolean>(`/calls/${callId}/notes`, { note });
}
// 文档翻译API
async getDocument(id: string): Promise<ApiResponse<DocumentTranslation>> {
return this.request<DocumentTranslation>(`/documents/${id}`);
return this.get<DocumentTranslation>(`/documents/${id}`);
}
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<ApiResponse<DocumentTranslation>> {
return this.request<DocumentTranslation>(`/documents/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return this.put<DocumentTranslation>(`/documents/${id}`, data);
}
async deleteDocument(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${id}`, {
method: 'DELETE',
});
return this.delete<boolean>(`/documents/${id}`);
}
async reassignTranslator(documentId: string, translatorId: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/reassign`, {
method: 'POST',
body: JSON.stringify({ translatorId }),
});
return this.post<boolean>(`/documents/${documentId}/reassign`, { translatorId });
}
async retranslateDocument(documentId: string, quality: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/retranslate`, {
method: 'POST',
body: JSON.stringify({ quality }),
});
return this.post<boolean>(`/documents/${documentId}/retranslate`, { quality });
}
async addDocumentNote(documentId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/documents/${documentId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
return this.post<boolean>(`/documents/${documentId}/notes`, { note });
}
// 预约管理API
async getAppointment(id: string): Promise<ApiResponse<Appointment>> {
return this.request<Appointment>(`/appointments/${id}`);
return this.get<Appointment>(`/appointments/${id}`);
}
async updateAppointment(id: string, data: Partial<Appointment>): Promise<ApiResponse<Appointment>> {
return this.request<Appointment>(`/appointments/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return this.put<Appointment>(`/appointments/${id}`, data);
}
async deleteAppointment(id: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${id}`, {
method: 'DELETE',
});
return this.delete<boolean>(`/appointments/${id}`);
}
async rescheduleAppointment(
@@ -142,79 +228,68 @@ class ApiManager {
newStartTime: string,
newEndTime: string
): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/reschedule`, {
method: 'POST',
body: JSON.stringify({ newStartTime, newEndTime }),
});
return this.post<boolean>(`/appointments/${appointmentId}/reschedule`, { newStartTime, newEndTime });
}
async reassignAppointmentTranslator(
appointmentId: string,
translatorId: string
): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/reassign`, {
method: 'POST',
body: JSON.stringify({ translatorId }),
});
return this.post<boolean>(`/appointments/${appointmentId}/reassign`, { translatorId });
}
async addAppointmentNote(appointmentId: string, note: string): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/appointments/${appointmentId}/notes`, {
method: 'POST',
body: JSON.stringify({ note }),
});
return this.post<boolean>(`/appointments/${appointmentId}/notes`, { note });
}
// 退款处理API
async refundPayment(paymentId: string, amount: number): Promise<ApiResponse<boolean>> {
return this.request<boolean>(`/payments/${paymentId}/refund`, {
method: 'POST',
body: JSON.stringify({ amount }),
});
return this.post<boolean>(`/payments/${paymentId}/refund`, { amount });
}
// 统计数据API
async getStatistics(): Promise<ApiResponse<any>> {
return this.request<any>('/statistics');
return this.get<any>('/statistics');
}
// 用户管理API
async getUsers(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
return this.request<any>(`/users?page=${page}&pageSize=${pageSize}`);
return this.get<any>(`/users?page=${page}&pageSize=${pageSize}`);
}
async updateUser(userId: string, data: any): Promise<ApiResponse<any>> {
return this.request<any>(`/users/${userId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return this.put<any>(`/users/${userId}`, data);
}
// 译员管理API
async getTranslators(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
return this.request<any>(`/translators?page=${page}&pageSize=${pageSize}`);
return this.get<any>(`/translators?page=${page}&pageSize=${pageSize}`);
}
async updateTranslator(translatorId: string, data: any): Promise<ApiResponse<any>> {
return this.request<any>(`/translators/${translatorId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
return this.put<any>(`/translators/${translatorId}`, data);
}
// 系统配置API
async getSystemConfig(): Promise<ApiResponse<any>> {
return this.request<any>('/config');
return this.get<any>('/config');
}
async updateSystemConfig(config: any): Promise<ApiResponse<any>> {
return this.request<any>('/config', {
method: 'PUT',
body: JSON.stringify(config),
});
return this.put<any>('/config', config);
}
}
// 导出API实例
export const api = new ApiManager();
export default api;
// 导出默认API客户端实例
export const api = new ApiClient();
// 导出常用的API方法
export const {
get,
post,
put,
patch,
delete: del,
upload,
getPaginated,
} = api;
+96 -28
View File
@@ -1,30 +1,76 @@
import { TranslationCall, DocumentTranslation, Appointment, User, Translator } from '../types';
// 模拟数据库连接
class DatabaseManager {
private isConnected: boolean = false;
// 模拟数据库连接和操作
export class Database {
private connected = false;
// 连接数据库
async connect(): Promise<void> {
if (!this.isConnected) {
// 模拟连接延迟
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
console.log('数据库连接成功');
}
// 模拟数据库连接
await new Promise(resolve => setTimeout(resolve, 100));
this.connected = true;
}
// 断开数据库连接
async disconnect(): Promise<void> {
if (this.isConnected) {
this.isConnected = false;
console.log('数据库连接已断开');
}
this.connected = false;
}
// 检查连接状态
isConnectionActive(): boolean {
return this.isConnected;
isConnected(): boolean {
return this.connected;
}
// 模拟查询操作
async query<T>(sql: string, params?: any[]): Promise<T[]> {
if (!this.connected) {
throw new Error('Database not connected');
}
// 模拟查询延迟
await new Promise(resolve => setTimeout(resolve, 50));
// 这里可以添加具体的查询逻辑
return [] as T[];
}
// 模拟插入操作
async insert<T>(table: string, data: Partial<T>): Promise<T> {
if (!this.connected) {
throw new Error('Database not connected');
}
await new Promise(resolve => setTimeout(resolve, 50));
// 模拟返回插入的数据
return {
...data,
id: `${table}_${Date.now()}`,
createdAt: new Date().toISOString(),
} as T;
}
// 模拟更新操作
async update<T>(table: string, id: string, data: Partial<T>): Promise<T> {
if (!this.connected) {
throw new Error('Database not connected');
}
await new Promise(resolve => setTimeout(resolve, 50));
// 模拟返回更新的数据
return {
...data,
id,
updatedAt: new Date().toISOString(),
} as T;
}
// 模拟删除操作
async delete(table: string, id: string): Promise<boolean> {
if (!this.connected) {
throw new Error('Database not connected');
}
await new Promise(resolve => setTimeout(resolve, 50));
return true;
}
// 通话相关操作
@@ -59,7 +105,6 @@ class DatabaseManager {
refundAmount: 0,
qualityScore: 0,
issues: [],
createdAt: new Date().toISOString(),
};
return newCall;
}
@@ -149,7 +194,7 @@ class DatabaseManager {
translatorId: data.translatorId || '',
title: data.title || '',
description: data.description || '',
type: data.type || '',
type: data.type || 'interpretation',
sourceLanguage: data.sourceLanguage || '',
targetLanguage: data.targetLanguage || '',
startTime: data.startTime || new Date().toISOString(),
@@ -158,6 +203,7 @@ class DatabaseManager {
cost: data.cost || 0,
reminderSent: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
clientName: data.clientName || '',
clientEmail: data.clientEmail || '',
clientPhone: data.clientPhone || '',
@@ -203,12 +249,21 @@ class DatabaseManager {
// 模拟创建用户
const newUser: User = {
id: `user_${Date.now()}`,
name: data.name || '',
username: data.username || '',
email: data.email || '',
phone: data.phone,
role: data.role || 'client',
phone: data.phone || '',
fullName: data.fullName || '',
avatar: data.avatar,
role: data.role || 'user',
status: 'active',
preferredLanguages: data.preferredLanguages || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
lastLoginAt: data.lastLoginAt,
totalCalls: data.totalCalls || 0,
totalSpent: data.totalSpent || 0,
rating: data.rating || 0,
verificationStatus: data.verificationStatus || 'pending',
};
return newUser;
}
@@ -243,17 +298,30 @@ class DatabaseManager {
// 模拟创建译员
const newTranslator: Translator = {
id: `translator_${Date.now()}`,
name: data.name || '',
userId: data.userId || '',
fullName: data.fullName || '',
email: data.email || '',
phone: data.phone || '',
avatar: data.avatar,
languages: data.languages || [],
specializations: data.specializations || [],
status: data.status || 'available',
rating: data.rating || 0,
totalCalls: data.totalCalls || 0,
totalEarnings: data.totalEarnings || 0,
hourlyRate: data.hourlyRate || 0,
status: 'available',
totalJobs: 0,
successRate: 0,
certifications: data.certifications || [],
workingHours: data.workingHours || {
monday: [],
tuesday: [],
wednesday: [],
thursday: [],
friday: [],
saturday: [],
sunday: [],
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
return newTranslator;
}
@@ -286,5 +354,5 @@ class DatabaseManager {
}
// 导出单例实例
export const database = new DatabaseManager();
export const database = new Database();
export default database;