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

This commit is contained in:
mars 2025-06-28 20:33:38 +08:00
parent 240dd5d2a4
commit deb2900acc
25 changed files with 4900 additions and 373 deletions

15
.expo/README.md Normal file
View File

@ -0,0 +1,15 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

8
.expo/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"hostType": "lan",
"lanType": "ip",
"dev": true,
"minify": false,
"urlRandomness": null,
"https": false
}

46
App.tsx
View File

@ -1,19 +1,41 @@
import React from 'react';
import { Provider } from 'react-redux';
import { StatusBar } from 'react-native';
import { store } from '@/store';
import AppNavigator from '@/navigation/AppNavigator';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import './src/styles/global.css';
// 导入页面组件
import HomeScreen from './src/screens/HomeScreen';
import CallScreen from './src/screens/CallScreen';
import DocumentScreen from './src/screens/DocumentScreen';
import SettingsScreen from './src/screens/SettingsScreen';
// 导入移动端导航组件
import MobileNavigation from './src/components/MobileNavigation.web';
const App: React.FC = () => {
return (
<Provider store={store}>
<StatusBar
barStyle="dark-content"
backgroundColor="#fff"
translucent={false}
/>
<AppNavigator />
</Provider>
<ConfigProvider locale={zhCN}>
<Router
future={{
v7_startTransition: true,
v7_relativeSplatPath: true
}}
>
<div className="app-container">
<div className="app-content">
<Routes>
<Route path="/" element={<Navigate to="/home" replace />} />
<Route path="/home" element={<HomeScreen />} />
<Route path="/call" element={<CallScreen />} />
<Route path="/documents" element={<DocumentScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
</Routes>
</div>
<MobileNavigation />
</div>
</Router>
</ConfigProvider>
);
};

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",

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",

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 { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';
import { Layout, Menu, Typography, ConfigProvider } from 'antd';
import {
DashboardOutlined,
PhoneOutlined,
FileTextOutlined,
CalendarOutlined
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('/');
}
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider
breakpoint="lg"
collapsedWidth="0"
style={{
background: '#001529',
}}
>
<div style={{
height: 32,
margin: 16,
background: 'rgba(255,255,255,.2)',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 'bold'
}}>
Twilio管理系统
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[selectedKey]}
onClick={handleMenuClick}
items={[
const menuItems = [
{
key: '1',
key: 'dashboard',
icon: <DashboardOutlined />,
label: '仪表板',
},
{
key: '2',
key: 'calls',
icon: <PhoneOutlined />,
label: '通话管理',
label: '通话记录',
},
{
key: '3',
key: 'documents',
icon: <FileTextOutlined />,
label: '文档翻译',
},
{
key: '4',
key: 'appointments',
icon: <CalendarOutlined />,
label: '预约管理',
},
]}
/>
</Sider>
<Layout>
<Header style={{
padding: 0,
background: '#fff',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
}}>
{
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
collapsible
collapsed={collapsed}
onCollapse={setCollapsed}
theme="dark"
width={250}
>
<div style={{
padding: '0 24px',
height: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '18px',
fontWeight: 'bold'
}}>
Twilio翻译服务管理后台
{collapsed ? 'T' : 'Twilio管理后台'}
</div>
</Header>
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
<div style={{
padding: 24,
<Menu
theme="dark"
defaultSelectedKeys={['dashboard']}
mode="inline"
items={menuItems}
onClick={handleMenuClick}
/>
</Sider>
<Layout>
<Header style={{
padding: '0 24px',
background: '#fff',
minHeight: 360,
borderRadius: 8
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #f0f0f0'
}}>
<Title level={4} style={{ margin: 0 }}>
Twilio翻译服务管理系统
</Title>
</Header>
<Content style={{
margin: '0',
background: '#f0f2f5',
minHeight: 'calc(100vh - 64px)'
}}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<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>
</div>
</Content>
</Layout>
</Layout>

View File

@ -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>

View File

@ -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;

View File

@ -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;

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="元"
@ -58,13 +234,112 @@ const Dashboard: React.FC = () => {
</Col>
</Row>
<div style={{ marginTop: '24px' }}>
<Card title="系统状态">
<p> </p>
<p> 线</p>
<p> </p>
{/* 第二行统计 */}
<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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

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;
}

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 = {}
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
try {
const { method = 'GET', headers = {}, body, params } = options;
const url = this.buildURL(endpoint, params);
const requestHeaders = {
...this.defaultHeaders,
...headers,
};
try {
const response = await fetch(url, config);
const data = await response.json();
const requestInit: RequestInit = {
method,
headers: requestHeaders,
};
if (!response.ok) {
throw new Error(data.message || '请求失败');
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;

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('数据库连接成功');
}
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;

View File

@ -4,8 +4,38 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twilio 翻译服务管理后台</title>
<meta name="description" content="Twilio 翻译服务管理后台系统" />
<title>翻译通 - 移动端</title>
<meta name="description" content="专业的翻译服务平台" />
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
#root {
height: 100vh;
width: 100vw;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.app-content {
flex: 1;
overflow-y: auto;
padding-bottom: 60px;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@ -11,8 +11,7 @@ const navItems: NavItem[] = [
{ path: '/mobile/home', label: '首页', icon: '🏠' },
{ path: '/mobile/call', label: '通话', icon: '📞' },
{ path: '/mobile/documents', label: '文档', icon: '📄' },
{ path: '/mobile/appointments', label: '预约', icon: '📅' },
{ path: '/mobile/settings', label: '设置', icon: '⚙️' },
{ path: '/mobile/settings', label: '我的', icon: '👤' },
];
const MobileNavigation: FC = () => {

View File

@ -10,10 +10,8 @@ interface NavItem {
const navItems: NavItem[] = [
{ path: '/mobile/home', label: '首页', icon: '🏠' },
{ path: '/mobile/call', label: '通话', icon: '📞' },
{ path: '/mobile/video-call', label: '视频', icon: '📹' },
{ path: '/mobile/documents', label: '文档', icon: '📄' },
{ path: '/mobile/appointments', label: '预约', icon: '📅' },
{ path: '/mobile/settings', label: '设置', icon: '⚙️' },
{ path: '/mobile/settings', label: '我的', icon: '👤' },
];
const MobileNavigation: FC = () => {

View File

@ -1,15 +1,16 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '../App.tsx';
import './styles/global.css';
// 创建根元素
const root = createRoot(
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
// 渲染应用
root.render(
<StrictMode>
<React.StrictMode>
<App />
</StrictMode>
</React.StrictMode>
);

View File

@ -8,23 +8,17 @@ import { View, Text, StyleSheet } from 'react-native';
import HomeScreen from '@/screens/HomeScreen';
import CallScreen from '@/screens/CallScreen';
import DocumentScreen from '@/screens/DocumentScreen';
import AppointmentScreen from '@/screens/AppointmentScreen';
import SettingsScreen from '@/screens/SettingsScreen';
// 导航类型定义
export type RootStackParamList = {
MainTabs: undefined;
Call: {
mode: 'ai' | 'human' | 'video' | 'sign';
sourceLanguage: string;
targetLanguage: string;
};
};
export type TabParamList = {
Home: undefined;
Call: undefined;
Documents: undefined;
Appointments: undefined;
Settings: undefined;
};
@ -37,12 +31,12 @@ const TabIcon: React.FC<{ name: string; focused: boolean }> = ({ name, focused }
switch (iconName) {
case 'home':
return '🏠';
case 'call':
return '📞';
case 'documents':
return '📄';
case 'appointments':
return '📅';
case 'settings':
return '⚙️';
return '👤';
default:
return '❓';
}
@ -79,6 +73,13 @@ const TabNavigator: React.FC = () => {
tabBarLabel: '首页',
}}
/>
<Tab.Screen
name="Call"
component={CallScreen}
options={{
tabBarLabel: '通话',
}}
/>
<Tab.Screen
name="Documents"
component={DocumentScreen}
@ -86,18 +87,11 @@ const TabNavigator: React.FC = () => {
tabBarLabel: '文档',
}}
/>
<Tab.Screen
name="Appointments"
component={AppointmentScreen}
options={{
tabBarLabel: '预约',
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarLabel: '设置',
tabBarLabel: '我的',
}}
/>
</Tab.Navigator>
@ -118,14 +112,6 @@ const AppNavigator: React.FC = () => {
name="MainTabs"
component={TabNavigator}
/>
<Stack.Screen
name="Call"
component={CallScreen}
options={{
presentation: 'fullScreenModal',
gestureEnabled: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);

View File

@ -7,7 +7,6 @@ import { useAuth } from '@/store';
import HomeScreen from '@/screens/HomeScreen.web';
import CallScreen from '@/screens/CallScreen.web';
import DocumentScreen from '@/screens/DocumentScreen.web';
import AppointmentScreen from '@/screens/AppointmentScreen.web';
import SettingsScreen from '@/screens/SettingsScreen.web';
import MobileNavigation from '@/components/MobileNavigation.web';
@ -101,9 +100,7 @@ const AppRoutes = () => {
<Route path="/home" element={<HomeScreen />} />
<Route path="/call" element={<CallScreen />} />
<Route path="/documents" element={<DocumentScreen />} />
<Route path="/appointments" element={<AppointmentScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/video-call" element={<VideoCallPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</MobileLayout>

View File

@ -8,7 +8,7 @@ export default defineConfig({
alias: {
'@': path.resolve(__dirname, './src'),
// React Native Web 别名配置
'react-native$': 'react-native-web',
'react-native': 'react-native-web',
'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter/RCTDeviceEventEmitter',
'react-native/Libraries/vendor/emitter/EventEmitter$': 'react-native-web/dist/vendor/react-native/emitter/EventEmitter',
'react-native/Libraries/EventEmitter/NativeEventEmitter$': 'react-native-web/dist/vendor/react-native/NativeEventEmitter',
@ -26,8 +26,9 @@ export default defineConfig({
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
},
server: {
port: 3000,
port: 5173,
host: true,
open: true
},
build: {
outDir: 'dist',