diff --git a/.expo/README.md b/.expo/README.md new file mode 100644 index 0000000..fd146b4 --- /dev/null +++ b/.expo/README.md @@ -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. diff --git a/.expo/settings.json b/.expo/settings.json new file mode 100644 index 0000000..92bc513 --- /dev/null +++ b/.expo/settings.json @@ -0,0 +1,8 @@ +{ + "hostType": "lan", + "lanType": "ip", + "dev": true, + "minify": false, + "urlRandomness": null, + "https": false +} diff --git a/App.tsx b/App.tsx index 4fd68b2..66052d4 100644 --- a/App.tsx +++ b/App.tsx @@ -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 ( - - - - + + +
+
+ + } /> + } /> + } /> + } /> + } /> + +
+ +
+
+
); }; diff --git a/Twilioapp-admin/package-lock.json b/Twilioapp-admin/package-lock.json index 19c319a..dbf046a 100644 --- a/Twilioapp-admin/package-lock.json +++ b/Twilioapp-admin/package-lock.json @@ -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", diff --git a/Twilioapp-admin/package.json b/Twilioapp-admin/package.json index 321f8c2..4c9aa6b 100644 --- a/Twilioapp-admin/package.json +++ b/Twilioapp-admin/package.json @@ -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", diff --git a/Twilioapp-admin/src/App.tsx b/Twilioapp-admin/src/App.tsx index 5651e68..48daf03 100644 --- a/Twilioapp-admin/src/App.tsx +++ b/Twilioapp-admin/src/App.tsx @@ -1,11 +1,15 @@ import React, { useState } from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom'; -import { Layout, Menu, ConfigProvider } from 'antd'; -import { - DashboardOutlined, - PhoneOutlined, - FileTextOutlined, - CalendarOutlined +import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom'; +import { Layout, Menu, Typography, ConfigProvider } from 'antd'; +import { + DashboardOutlined, + PhoneOutlined, + FileTextOutlined, + CalendarOutlined, + UserOutlined, + TeamOutlined, + DollarOutlined, + SettingOutlined } from '@ant-design/icons'; import zhCN from 'antd/locale/zh_CN'; import 'antd/dist/reset.css'; @@ -13,114 +17,159 @@ import './App.css'; // 导入页面组件 import Dashboard from './pages/Dashboard'; +import CallList from './pages/Calls/CallList'; import CallDetail from './pages/Calls/CallDetail'; +import DocumentList from './pages/Documents/DocumentList'; import DocumentDetail from './pages/Documents/DocumentDetail'; +import AppointmentList from './pages/Appointments/AppointmentList'; import AppointmentDetail from './pages/Appointments/AppointmentDetail'; +import UserList from './pages/Users/UserList'; +import TranslatorList from './pages/Translators/TranslatorList'; +import PaymentList from './pages/Payments/PaymentList'; +import SystemSettings from './pages/Settings/SystemSettings'; const { Header, Sider, Content } = Layout; +const { Title } = Typography; const AppContent: React.FC = () => { + const [collapsed, setCollapsed] = useState(false); const navigate = useNavigate(); - const [selectedKey, setSelectedKey] = useState('1'); - const handleMenuClick = (e: any) => { - setSelectedKey(e.key); - switch (e.key) { - case '1': - navigate('/dashboard'); + const handleMenuClick = ({ key }: { key: string }) => { + switch (key) { + case 'dashboard': + navigate('/'); break; - case '2': - navigate('/calls/1'); + case 'calls': + navigate('/calls'); break; - case '3': - navigate('/documents/1'); + case 'documents': + navigate('/documents'); break; - case '4': - navigate('/appointments/1'); + case 'appointments': + navigate('/appointments'); break; + case 'users': + navigate('/users'); + break; + case 'translators': + navigate('/translators'); + break; + case 'payments': + navigate('/payments'); + break; + case 'settings': + navigate('/settings'); + break; + default: + navigate('/'); } }; + const menuItems = [ + { + key: 'dashboard', + icon: , + label: '仪表板', + }, + { + key: 'calls', + icon: , + label: '通话记录', + }, + { + key: 'documents', + icon: , + label: '文档翻译', + }, + { + key: 'appointments', + icon: , + label: '预约管理', + }, + { + key: 'users', + icon: , + label: '用户管理', + }, + { + key: 'translators', + icon: , + label: '译员管理', + }, + { + key: 'payments', + icon: , + label: '支付记录', + }, + { + key: 'settings', + icon: , + label: '系统设置', + }, + ]; + return (
- Twilio管理系统 + {collapsed ? 'T' : 'Twilio管理后台'}
, - label: '仪表板', - }, - { - key: '2', - icon: , - label: '通话管理', - }, - { - key: '3', - icon: , - label: '文档翻译', - }, - { - key: '4', - icon: , - label: '预约管理', - }, - ]} /> +
-
- Twilio翻译服务管理后台 -
+ + Twilio翻译服务管理系统 +
- -
- - } /> - } /> - } /> - } /> - } /> - -
+ + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
diff --git a/Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx b/Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx index 6f45629..ffd5359 100644 --- a/Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx +++ b/Twilioapp-admin/src/pages/Appointments/AppointmentDetail.tsx @@ -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 = () => { 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 = () => { 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 = () => { = 90 ? '#3f8600' : '#faad14' }} + valueStyle={{ color: (appointment.qualityScore || 0) >= 90 ? '#3f8600' : '#faad14' }} prefix={} /> @@ -498,7 +492,7 @@ const AppointmentDetail: React.FC = () => { - + {getUrgencyText(appointment.urgency)} @@ -577,8 +571,8 @@ const AppointmentDetail: React.FC = () => { - 0 ? 'danger' : 'secondary'}> - ¥{appointment.refundAmount.toFixed(2)} + 0 ? 'danger' : 'secondary'}> + ¥{(appointment.refundAmount || 0).toFixed(2)} @@ -762,8 +756,8 @@ const AppointmentDetail: React.FC = () => {
-
= 90 ? '#52c41a' : appointment.qualityScore >= 70 ? '#faad14' : '#ff4d4f' }}> - {appointment.qualityScore}/100 +
= 90 ? '#52c41a' : (appointment.qualityScore || 0) >= 70 ? '#faad14' : '#ff4d4f' }}> + {appointment.qualityScore || 0}/100
系统评分
@@ -844,8 +838,9 @@ const AppointmentDetail: React.FC = () => { > diff --git a/Twilioapp-admin/src/pages/Appointments/AppointmentList.tsx b/Twilioapp-admin/src/pages/Appointments/AppointmentList.tsx new file mode 100644 index 0000000..c6e95c1 --- /dev/null +++ b/Twilioapp-admin/src/pages/Appointments/AppointmentList.tsx @@ -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([]); + const [filteredAppointments, setFilteredAppointments] = useState([]); + const [searchText, setSearchText] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [serviceTypeFilter, setServiceTypeFilter] = useState('all'); + const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null); + const [modalVisible, setModalVisible] = useState(false); + const [editingAppointment, setEditingAppointment] = useState(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 {config.text}; + }; + + const getServiceTypeTag = (type: string) => { + const typeConfig = { + voice: { color: 'blue', text: '语音翻译', icon: }, + video: { color: 'green', text: '视频翻译', icon: }, + document: { color: 'purple', text: '文档讨论', icon: } + }; + const config = typeConfig[type as keyof typeof typeConfig]; + return ( + + {config.text} + + ); + }; + + 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 = [ + { + title: '客户信息', + key: 'client', + width: 200, + render: (_, record) => ( +
+
+ } /> + {record.clientName} +
+
+ {record.clientPhone} +
+
+ ) + }, + { + title: '预约时间', + key: 'datetime', + width: 150, + render: (_, record) => ( +
+
+ + {record.appointmentDate} +
+
+ + {record.appointmentTime} ({record.duration}分钟) +
+
+ ) + }, + { + title: '服务类型', + dataIndex: 'serviceType', + key: 'serviceType', + width: 120, + render: getServiceTypeTag + }, + { + title: '语言对', + key: 'languages', + width: 150, + render: (_, record) => ( +
+ {record.sourceLanguage} + + {record.targetLanguage} +
+ ) + }, + { + 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) => ( + + + +