后台管理端调整
This commit is contained in:
Generated
+17369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "twilioapp-admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.11.56",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"antd": "^5.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.7.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/moment": "^2.13.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Twilio翻译服务后台管理系统"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Twilio翻译管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,231 @@
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.ant-layout-sider {
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
}
|
||||
|
||||
/* 内容区域样式 */
|
||||
.ant-layout-content {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.ant-card {
|
||||
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.ant-table {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.ant-btn {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-content {
|
||||
margin: 16px 8px 0;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义动画 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 音频播放器样式 */
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* 文件预览样式 */
|
||||
.file-preview {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 时间轴样式 */
|
||||
.timeline-item {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.timeline-item .timeline-time {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.timeline-item .timeline-content {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 评分样式 */
|
||||
.rating-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rating-value {
|
||||
font-weight: bold;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义样式 */
|
||||
.logo {
|
||||
width: 120px;
|
||||
height: 31px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
margin: 16px 24px 16px 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ant-layout-header {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.site-layout-background {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 卡片样式优化 */
|
||||
.ant-card-cover {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.ant-card-meta-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-card-meta-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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
|
||||
} from '@ant-design/icons';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import 'antd/dist/reset.css';
|
||||
import './App.css';
|
||||
|
||||
// 导入页面组件
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import CallDetail from './pages/Calls/CallDetail';
|
||||
import DocumentDetail from './pages/Documents/DocumentDetail';
|
||||
import AppointmentDetail from './pages/Appointments/AppointmentDetail';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [selectedKey, setSelectedKey] = useState('1');
|
||||
|
||||
const handleMenuClick = (e: any) => {
|
||||
setSelectedKey(e.key);
|
||||
switch (e.key) {
|
||||
case '1':
|
||||
navigate('/dashboard');
|
||||
break;
|
||||
case '2':
|
||||
navigate('/calls/1');
|
||||
break;
|
||||
case '3':
|
||||
navigate('/documents/1');
|
||||
break;
|
||||
case '4':
|
||||
navigate('/appointments/1');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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={[
|
||||
{
|
||||
key: '1',
|
||||
icon: <DashboardOutlined />,
|
||||
label: '仪表板',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
icon: <PhoneOutlined />,
|
||||
label: '通话管理',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '文档翻译',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
icon: <CalendarOutlined />,
|
||||
label: '预约管理',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{
|
||||
padding: 0,
|
||||
background: '#fff',
|
||||
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '0 24px',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Twilio翻译服务管理后台
|
||||
</div>
|
||||
</Header>
|
||||
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
|
||||
<div style={{
|
||||
padding: 24,
|
||||
background: '#fff',
|
||||
minHeight: 360,
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/calls/:id" element={<CallDetail />} />
|
||||
<Route path="/documents/:id" element={<DocumentDetail />} />
|
||||
<Route path="/appointments/:id" element={<AppointmentDetail />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<Router>
|
||||
<AppContent />
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,21 @@
|
||||
body {
|
||||
margin: 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;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import 'antd/dist/reset.css';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,805 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Descriptions,
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Space,
|
||||
Modal,
|
||||
Input,
|
||||
message,
|
||||
Spin,
|
||||
Timeline,
|
||||
Tabs,
|
||||
Avatar,
|
||||
Progress,
|
||||
Select,
|
||||
Form,
|
||||
Switch,
|
||||
Divider,
|
||||
Alert,
|
||||
Table,
|
||||
Rate,
|
||||
Statistic,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
DownloadOutlined,
|
||||
StarOutlined,
|
||||
PhoneOutlined,
|
||||
ClockCircleOutlined,
|
||||
DollarOutlined,
|
||||
UserOutlined,
|
||||
SoundOutlined,
|
||||
FileTextOutlined,
|
||||
TranslationOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
AuditOutlined,
|
||||
SettingOutlined,
|
||||
MessageOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { TranslationCall } from '../../types';
|
||||
import { database } from '../../utils/database';
|
||||
import { api } from '../../utils/api';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
const { TabPane } = Tabs;
|
||||
const { Option } = Select;
|
||||
|
||||
interface CallDetailProps {}
|
||||
|
||||
const CallDetail: React.FC<CallDetailProps> = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [call, setCall] = useState<TranslationCall | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [statusModalVisible, setStatusModalVisible] = useState(false);
|
||||
const [refundModalVisible, setRefundModalVisible] = useState(false);
|
||||
const [adminNoteModalVisible, setAdminNoteModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [statusForm] = Form.useForm();
|
||||
const [refundForm] = Form.useForm();
|
||||
const [noteForm] = Form.useForm();
|
||||
|
||||
// 模拟音频播放状态
|
||||
const [audioProgress, setAudioProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadCallDetails();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadCallDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await database.connect();
|
||||
|
||||
// 模拟获取通话详情(管理员视角)
|
||||
const mockCall: TranslationCall = {
|
||||
id: id!,
|
||||
userId: 'user_1',
|
||||
callId: `CA${Date.now()}`,
|
||||
clientName: '张先生',
|
||||
clientPhone: '+86 138 0013 8000',
|
||||
type: 'human',
|
||||
status: 'completed',
|
||||
sourceLanguage: 'zh-CN',
|
||||
targetLanguage: 'en-US',
|
||||
startTime: '2024-01-15T10:30:00Z',
|
||||
endTime: '2024-01-15T10:45:00Z',
|
||||
duration: 900,
|
||||
cost: 45.00,
|
||||
rating: 5,
|
||||
feedback: '翻译非常专业,沟通顺畅,非常满意!',
|
||||
translatorId: 'translator_1',
|
||||
translatorName: '李翻译',
|
||||
translatorPhone: '+86 138 0013 8001',
|
||||
recordingUrl: '/recordings/call_123456.mp3',
|
||||
transcription: '用户: 您好,我想了解一下贵公司的产品服务。\n翻译: Hello, I would like to learn about your company\'s products and services.\n客户: Thank you for your interest. Let me introduce our main products...\n翻译: 感谢您的关注。让我为您介绍我们的主要产品...',
|
||||
translation: '这是一次关于产品咨询的商务通话,客户询问了公司的主要产品和服务,我们提供了详细的介绍和说明。',
|
||||
// 管理员相关字段
|
||||
adminNotes: '通话质量良好,客户满意度高',
|
||||
paymentStatus: 'paid',
|
||||
refundAmount: 0,
|
||||
qualityScore: 95,
|
||||
issues: [],
|
||||
};
|
||||
|
||||
setCall(mockCall);
|
||||
setDuration(mockCall.duration || 0);
|
||||
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
clientName: mockCall.clientName,
|
||||
clientPhone: mockCall.clientPhone,
|
||||
translatorName: mockCall.translatorName,
|
||||
cost: mockCall.cost,
|
||||
});
|
||||
|
||||
statusForm.setFieldsValue({
|
||||
status: mockCall.status,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载通话详情失败:', error);
|
||||
message.error('加载通话详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlayPause = () => {
|
||||
setIsPlaying(!isPlaying);
|
||||
|
||||
if (!isPlaying) {
|
||||
// 模拟音频播放
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(prev => {
|
||||
const newTime = prev + 1;
|
||||
setAudioProgress((newTime / duration) * 100);
|
||||
|
||||
if (newTime >= duration) {
|
||||
clearInterval(interval);
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
setAudioProgress(0);
|
||||
}
|
||||
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (values: any) => {
|
||||
if (!call) return;
|
||||
|
||||
try {
|
||||
const updatedCall = {
|
||||
...call,
|
||||
...values,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCall(updatedCall);
|
||||
setEditModalVisible(false);
|
||||
message.success('通话信息更新成功');
|
||||
} catch (error) {
|
||||
message.error('更新通话信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (values: any) => {
|
||||
if (!call) return;
|
||||
|
||||
try {
|
||||
const updatedCall = {
|
||||
...call,
|
||||
status: values.status,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCall(updatedCall);
|
||||
setStatusModalVisible(false);
|
||||
message.success('状态更新成功');
|
||||
} catch (error) {
|
||||
message.error('更新状态失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefund = async (values: any) => {
|
||||
if (!call) return;
|
||||
|
||||
try {
|
||||
const refundAmount = values.amount || call.cost;
|
||||
|
||||
// 模拟退款API调用
|
||||
await api.refundPayment(`payment_${call.id}`, refundAmount);
|
||||
|
||||
const updatedCall = {
|
||||
...call,
|
||||
refundAmount: refundAmount,
|
||||
paymentStatus: 'refunded' as const,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCall(updatedCall);
|
||||
setRefundModalVisible(false);
|
||||
message.success('退款处理成功');
|
||||
} catch (error) {
|
||||
message.error('退款处理失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAdminNote = async (values: any) => {
|
||||
if (!call) return;
|
||||
|
||||
try {
|
||||
const updatedCall = {
|
||||
...call,
|
||||
adminNotes: values.note,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCall(updatedCall);
|
||||
setAdminNoteModalVisible(false);
|
||||
message.success('管理员备注添加成功');
|
||||
} catch (error) {
|
||||
message.error('添加备注失败');
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
active: 'blue',
|
||||
completed: 'green',
|
||||
cancelled: 'red',
|
||||
refunded: 'purple',
|
||||
};
|
||||
return colors[status as keyof typeof colors] || 'default';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '等待中',
|
||||
active: '通话中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
refunded: '已退款',
|
||||
};
|
||||
return texts[status as keyof typeof texts] || status;
|
||||
};
|
||||
|
||||
const getPaymentStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'orange',
|
||||
paid: 'green',
|
||||
refunded: 'purple',
|
||||
failed: 'red',
|
||||
};
|
||||
return colors[status as keyof typeof colors] || 'default';
|
||||
};
|
||||
|
||||
const getPaymentStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '待支付',
|
||||
paid: '已支付',
|
||||
refunded: '已退款',
|
||||
failed: '支付失败',
|
||||
};
|
||||
return texts[status as keyof typeof texts] || status;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: '16px' }}>加载通话详情...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!call) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<div>通话记录不存在</div>
|
||||
<Button type="primary" onClick={() => navigate('/calls')} style={{ marginTop: '16px' }}>
|
||||
返回通话列表
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
{/* 头部导航 */}
|
||||
<div style={{ marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/calls')}
|
||||
style={{ marginRight: '16px' }}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
|
||||
通话详情 #{call.id}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{/* 管理员操作按钮 */}
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setEditModalVisible(true)}
|
||||
>
|
||||
编辑信息
|
||||
</Button>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => setStatusModalVisible(true)}
|
||||
>
|
||||
更改状态
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DollarOutlined />}
|
||||
onClick={() => setRefundModalVisible(true)}
|
||||
disabled={call.paymentStatus !== 'paid'}
|
||||
>
|
||||
处理退款
|
||||
</Button>
|
||||
<Button
|
||||
icon={<MessageOutlined />}
|
||||
onClick={() => setAdminNoteModalVisible(true)}
|
||||
>
|
||||
添加备注
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 系统状态提醒 */}
|
||||
{call.issues && call.issues.length > 0 && (
|
||||
<Alert
|
||||
message="系统检测到问题"
|
||||
description={call.issues.join(', ')}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card title="通话信息" style={{ marginBottom: '24px' }}>
|
||||
<Descriptions column={3} bordered>
|
||||
<Descriptions.Item label="通话ID" span={1}>
|
||||
{call.callId}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态" span={1}>
|
||||
<Tag color={getStatusColor(call.status)}>
|
||||
{getStatusText(call.status)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="支付状态" span={1}>
|
||||
<Tag color={getPaymentStatusColor(call.paymentStatus)}>
|
||||
{getPaymentStatusText(call.paymentStatus)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="客户姓名" span={1}>
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
{call.clientName}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="客户电话" span={1}>
|
||||
<Space>
|
||||
<PhoneOutlined />
|
||||
{call.clientPhone}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="译员" span={1}>
|
||||
<Space>
|
||||
<Avatar size="small" icon={<UserOutlined />} />
|
||||
{call.translatorName}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间" span={1}>
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
{new Date(call.startTime).toLocaleString()}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间" span={1}>
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
{call.endTime ? new Date(call.endTime).toLocaleString() : '-'}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="通话时长" span={1}>
|
||||
<Space>
|
||||
<PhoneOutlined />
|
||||
{formatTime(call.duration || 0)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="费用" span={1}>
|
||||
<Space>
|
||||
<DollarOutlined />
|
||||
<Text strong>¥{call.cost.toFixed(2)}</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="退款金额" span={1}>
|
||||
<Space>
|
||||
<DollarOutlined />
|
||||
<Text type={call.refundAmount > 0 ? 'danger' : 'secondary'}>
|
||||
¥{call.refundAmount.toFixed(2)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="质量评分" span={1}>
|
||||
<Space>
|
||||
<AuditOutlined />
|
||||
<Text strong style={{ color: call.qualityScore >= 90 ? '#52c41a' : call.qualityScore >= 70 ? '#faad14' : '#ff4d4f' }}>
|
||||
{call.qualityScore}/100
|
||||
</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{call.adminNotes && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>管理员备注:</Text>
|
||||
<Paragraph style={{ marginTop: '8px', background: '#f6f6f6', padding: '12px', borderRadius: '6px' }}>
|
||||
{call.adminNotes}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 录音播放器 */}
|
||||
{call.recordingUrl && (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<SoundOutlined />
|
||||
录音播放
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
|
||||
onClick={handlePlayPause}
|
||||
style={{ marginRight: '16px' }}
|
||||
>
|
||||
{isPlaying ? '暂停' : '播放'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => message.success('录音下载中...')}
|
||||
>
|
||||
下载录音
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ margin: '20px 0' }}>
|
||||
<Progress
|
||||
percent={audioProgress}
|
||||
showInfo={false}
|
||||
strokeColor="#1890ff"
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
|
||||
<Text type="secondary">{formatTime(currentTime)}</Text>
|
||||
<Text type="secondary">{formatTime(duration)}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 详细内容标签页 */}
|
||||
<Card>
|
||||
<Tabs defaultActiveKey="transcription">
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<FileTextOutlined />
|
||||
转录内容
|
||||
</Space>
|
||||
}
|
||||
key="transcription"
|
||||
>
|
||||
<div style={{ minHeight: '200px' }}>
|
||||
{call.transcription ? (
|
||||
<Paragraph>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>
|
||||
{call.transcription}
|
||||
</pre>
|
||||
</Paragraph>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
|
||||
暂无转录内容
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<TranslationOutlined />
|
||||
翻译摘要
|
||||
</Space>
|
||||
}
|
||||
key="translation"
|
||||
>
|
||||
<div style={{ minHeight: '200px' }}>
|
||||
{call.translation ? (
|
||||
<Paragraph>{call.translation}</Paragraph>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: '#999', padding: '50px' }}>
|
||||
暂无翻译摘要
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<StarOutlined />
|
||||
用户评价
|
||||
</Space>
|
||||
}
|
||||
key="rating"
|
||||
>
|
||||
<div style={{ minHeight: '200px', padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<Text strong>服务评分:</Text>
|
||||
<Rate disabled value={call.rating} style={{ marginLeft: '8px' }} />
|
||||
{call.rating && (
|
||||
<Text style={{ marginLeft: '8px' }}>
|
||||
({call.rating}/5 分)
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{call.feedback && (
|
||||
<div>
|
||||
<Text strong>用户反馈:</Text>
|
||||
<Paragraph style={{ marginTop: '8px' }}>
|
||||
{call.feedback}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<AuditOutlined />
|
||||
质量分析
|
||||
</Space>
|
||||
}
|
||||
key="quality"
|
||||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="质量评分">
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={call.qualityScore}
|
||||
width={80}
|
||||
strokeColor={call.qualityScore >= 90 ? '#52c41a' : call.qualityScore >= 70 ? '#faad14' : '#ff4d4f'}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="系统检测">
|
||||
{call.issues && call.issues.length > 0 ? (
|
||||
<div>
|
||||
{call.issues.map((issue, index) => (
|
||||
<Tag key={index} color="red" style={{ marginBottom: '4px' }}>
|
||||
{issue}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Tag color="green">无异常</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
{/* 编辑信息弹窗 */}
|
||||
<Modal
|
||||
title="编辑通话信息"
|
||||
visible={editModalVisible}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleEdit}
|
||||
>
|
||||
<Form.Item
|
||||
name="clientName"
|
||||
label="客户姓名"
|
||||
rules={[{ required: true, message: '请输入客户姓名' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="clientPhone"
|
||||
label="客户电话"
|
||||
rules={[{ required: true, message: '请输入客户电话' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="translatorName"
|
||||
label="译员姓名"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="cost"
|
||||
label="费用"
|
||||
rules={[{ required: true, message: '请输入费用' }]}
|
||||
>
|
||||
<Input type="number" addonAfter="元" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setEditModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 更改状态弹窗 */}
|
||||
<Modal
|
||||
title="更改通话状态"
|
||||
visible={statusModalVisible}
|
||||
onCancel={() => setStatusModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={statusForm}
|
||||
layout="vertical"
|
||||
onFinish={handleStatusChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="status"
|
||||
label="新状态"
|
||||
rules={[{ required: true, message: '请选择状态' }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="pending">等待中</Option>
|
||||
<Option value="active">通话中</Option>
|
||||
<Option value="completed">已完成</Option>
|
||||
<Option value="cancelled">已取消</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setStatusModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
更新状态
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 退款处理弹窗 */}
|
||||
<Modal
|
||||
title="处理退款"
|
||||
visible={refundModalVisible}
|
||||
onCancel={() => setRefundModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={refundForm}
|
||||
layout="vertical"
|
||||
onFinish={handleRefund}
|
||||
initialValues={{ amount: call.cost }}
|
||||
>
|
||||
<Alert
|
||||
message="退款提醒"
|
||||
description={`原支付金额:¥${call.cost.toFixed(2)}`}
|
||||
type="info"
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="amount"
|
||||
label="退款金额"
|
||||
rules={[
|
||||
{ required: true, message: '请输入退款金额' },
|
||||
{ type: 'number', min: 0, max: call.cost, message: `退款金额不能超过¥${call.cost.toFixed(2)}` }
|
||||
]}
|
||||
>
|
||||
<Input type="number" addonAfter="元" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="reason"
|
||||
label="退款原因"
|
||||
rules={[{ required: true, message: '请输入退款原因' }]}
|
||||
>
|
||||
<TextArea rows={3} placeholder="请输入退款原因..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setRefundModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" danger htmlType="submit">
|
||||
确认退款
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 添加管理员备注弹窗 */}
|
||||
<Modal
|
||||
title="添加管理员备注"
|
||||
visible={adminNoteModalVisible}
|
||||
onCancel={() => setAdminNoteModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={noteForm}
|
||||
layout="vertical"
|
||||
onFinish={handleAddAdminNote}
|
||||
initialValues={{ note: call.adminNotes }}
|
||||
>
|
||||
<Form.Item
|
||||
name="note"
|
||||
label="备注内容"
|
||||
rules={[{ required: true, message: '请输入备注内容' }]}
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入管理员备注..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => setAdminNoteModalVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存备注
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallDetail;
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic, Typography } from 'antd';
|
||||
import {
|
||||
PhoneOutlined,
|
||||
FileTextOutlined,
|
||||
CalendarOutlined,
|
||||
DollarOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
<Title level={2}>仪表板</Title>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总通话数"
|
||||
value={1128}
|
||||
prefix={<PhoneOutlined />}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="文档翻译"
|
||||
value={892}
|
||||
prefix={<FileTextOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="预约服务"
|
||||
value={456}
|
||||
prefix={<CalendarOutlined />}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总收入"
|
||||
value={25680}
|
||||
prefix={<DollarOutlined />}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
suffix="元"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: '24px' }}>
|
||||
<Card title="系统状态">
|
||||
<p>✅ 系统运行正常</p>
|
||||
<p>✅ 所有服务在线</p>
|
||||
<p>✅ 数据库连接正常</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
// 通话相关类型
|
||||
export interface TranslationCall {
|
||||
id: string;
|
||||
userId: string;
|
||||
callId: string;
|
||||
clientName: string;
|
||||
clientPhone: string;
|
||||
type: 'human' | 'ai';
|
||||
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'refunded';
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
cost: number;
|
||||
rating?: number;
|
||||
feedback?: string;
|
||||
translatorId?: string;
|
||||
translatorName?: string;
|
||||
translatorPhone?: string;
|
||||
recordingUrl?: string;
|
||||
transcription?: string;
|
||||
translation?: string;
|
||||
// 管理员相关字段
|
||||
adminNotes?: string;
|
||||
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
||||
refundAmount: number;
|
||||
qualityScore: number;
|
||||
issues: string[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// 文档翻译相关类型
|
||||
export interface DocumentTranslation {
|
||||
id: string;
|
||||
userId: string;
|
||||
fileName: string;
|
||||
originalSize: number;
|
||||
fileUrl: string;
|
||||
translatedFileUrl?: string;
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'cancelled' | 'failed';
|
||||
progress: number;
|
||||
quality: 'basic' | 'professional' | 'premium';
|
||||
urgency: 'normal' | 'urgent' | 'emergency';
|
||||
estimatedTime: number;
|
||||
actualTime?: number;
|
||||
cost: number;
|
||||
translatorId?: string;
|
||||
translatorName?: string;
|
||||
rating?: number;
|
||||
feedback?: string;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
// 管理员相关字段
|
||||
adminNotes?: string;
|
||||
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
|
||||
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;
|
||||
title: string;
|
||||
description: string;
|
||||
type: string;
|
||||
sourceLanguage: string;
|
||||
targetLanguage: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
status: string;
|
||||
cost: number;
|
||||
meetingUrl?: string;
|
||||
notes?: string;
|
||||
reminderSent: boolean;
|
||||
createdAt: 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[];
|
||||
rating?: number;
|
||||
feedback?: string;
|
||||
location?: string;
|
||||
urgency: string;
|
||||
}
|
||||
|
||||
// 用户类型
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
role: 'client' | 'translator' | 'admin';
|
||||
status: 'active' | 'inactive' | 'suspended';
|
||||
createdAt: string;
|
||||
updatedAt?: 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;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 分页类型
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
// 搜索参数类型
|
||||
export interface SearchParams {
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
dateRange?: [string, string];
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { TranslationCall, DocumentTranslation, Appointment, ApiResponse } from '../types';
|
||||
|
||||
// API基础URL
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
// API请求工具类
|
||||
class ApiManager {
|
||||
private baseURL: string;
|
||||
|
||||
constructor(baseURL: string = API_BASE_URL) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const config: RequestInit = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || '请求失败');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
message: '操作成功',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null as any,
|
||||
message: error instanceof Error ? error.message : '网络错误',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 通话管理API
|
||||
async getCall(id: string): Promise<ApiResponse<TranslationCall>> {
|
||||
return this.request<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),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCall(id: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/calls/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
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 }),
|
||||
});
|
||||
}
|
||||
|
||||
async addCallNote(callId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/calls/${callId}/notes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ note }),
|
||||
});
|
||||
}
|
||||
|
||||
// 文档翻译API
|
||||
async getDocument(id: string): Promise<ApiResponse<DocumentTranslation>> {
|
||||
return this.request<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),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDocument(id: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/documents/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async reassignTranslator(documentId: string, translatorId: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/documents/${documentId}/reassign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ translatorId }),
|
||||
});
|
||||
}
|
||||
|
||||
async retranslateDocument(documentId: string, quality: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/documents/${documentId}/retranslate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ quality }),
|
||||
});
|
||||
}
|
||||
|
||||
async addDocumentNote(documentId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/documents/${documentId}/notes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ note }),
|
||||
});
|
||||
}
|
||||
|
||||
// 预约管理API
|
||||
async getAppointment(id: string): Promise<ApiResponse<Appointment>> {
|
||||
return this.request<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),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAppointment(id: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/appointments/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async rescheduleAppointment(
|
||||
appointmentId: string,
|
||||
newStartTime: string,
|
||||
newEndTime: string
|
||||
): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/appointments/${appointmentId}/reschedule`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ newStartTime, newEndTime }),
|
||||
});
|
||||
}
|
||||
|
||||
async reassignAppointmentTranslator(
|
||||
appointmentId: string,
|
||||
translatorId: string
|
||||
): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/appointments/${appointmentId}/reassign`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ translatorId }),
|
||||
});
|
||||
}
|
||||
|
||||
async addAppointmentNote(appointmentId: string, note: string): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/appointments/${appointmentId}/notes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ note }),
|
||||
});
|
||||
}
|
||||
|
||||
// 退款处理API
|
||||
async refundPayment(paymentId: string, amount: number): Promise<ApiResponse<boolean>> {
|
||||
return this.request<boolean>(`/payments/${paymentId}/refund`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amount }),
|
||||
});
|
||||
}
|
||||
|
||||
// 统计数据API
|
||||
async getStatistics(): Promise<ApiResponse<any>> {
|
||||
return this.request<any>('/statistics');
|
||||
}
|
||||
|
||||
// 用户管理API
|
||||
async getUsers(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
||||
return this.request<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),
|
||||
});
|
||||
}
|
||||
|
||||
// 译员管理API
|
||||
async getTranslators(page: number = 1, pageSize: number = 10): Promise<ApiResponse<any>> {
|
||||
return this.request<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),
|
||||
});
|
||||
}
|
||||
|
||||
// 系统配置API
|
||||
async getSystemConfig(): Promise<ApiResponse<any>> {
|
||||
return this.request<any>('/config');
|
||||
}
|
||||
|
||||
async updateSystemConfig(config: any): Promise<ApiResponse<any>> {
|
||||
return this.request<any>('/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出API实例
|
||||
export const api = new ApiManager();
|
||||
export default api;
|
||||
@@ -0,0 +1,290 @@
|
||||
import { TranslationCall, DocumentTranslation, Appointment, User, Translator } from '../types';
|
||||
|
||||
// 模拟数据库连接类
|
||||
class DatabaseManager {
|
||||
private isConnected: boolean = false;
|
||||
|
||||
// 连接数据库
|
||||
async connect(): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
// 模拟连接延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
this.isConnected = true;
|
||||
console.log('数据库连接成功');
|
||||
}
|
||||
}
|
||||
|
||||
// 断开数据库连接
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.isConnected) {
|
||||
this.isConnected = false;
|
||||
console.log('数据库连接已断开');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查连接状态
|
||||
isConnectionActive(): boolean {
|
||||
return this.isConnected;
|
||||
}
|
||||
|
||||
// 通话相关操作
|
||||
async getCalls(params?: any): Promise<TranslationCall[]> {
|
||||
await this.connect();
|
||||
// 模拟获取通话列表
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCallById(id: string): Promise<TranslationCall | null> {
|
||||
await this.connect();
|
||||
// 模拟获取单个通话
|
||||
return null;
|
||||
}
|
||||
|
||||
async createCall(data: Partial<TranslationCall>): Promise<TranslationCall> {
|
||||
await this.connect();
|
||||
// 模拟创建通话
|
||||
const newCall: TranslationCall = {
|
||||
id: `call_${Date.now()}`,
|
||||
userId: data.userId || '',
|
||||
callId: `CA${Date.now()}`,
|
||||
clientName: data.clientName || '',
|
||||
clientPhone: data.clientPhone || '',
|
||||
type: data.type || 'human',
|
||||
status: 'pending',
|
||||
sourceLanguage: data.sourceLanguage || '',
|
||||
targetLanguage: data.targetLanguage || '',
|
||||
startTime: new Date().toISOString(),
|
||||
cost: data.cost || 0,
|
||||
paymentStatus: 'pending',
|
||||
refundAmount: 0,
|
||||
qualityScore: 0,
|
||||
issues: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return newCall;
|
||||
}
|
||||
|
||||
async updateCall(id: string, data: Partial<TranslationCall>): Promise<TranslationCall | null> {
|
||||
await this.connect();
|
||||
// 模拟更新通话
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteCall(id: string): Promise<boolean> {
|
||||
await this.connect();
|
||||
// 模拟删除通话
|
||||
return true;
|
||||
}
|
||||
|
||||
// 文档翻译相关操作
|
||||
async getDocuments(params?: any): Promise<DocumentTranslation[]> {
|
||||
await this.connect();
|
||||
// 模拟获取文档列表
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDocumentById(id: string): Promise<DocumentTranslation | null> {
|
||||
await this.connect();
|
||||
// 模拟获取单个文档
|
||||
return null;
|
||||
}
|
||||
|
||||
async createDocument(data: Partial<DocumentTranslation>): Promise<DocumentTranslation> {
|
||||
await this.connect();
|
||||
// 模拟创建文档翻译
|
||||
const newDocument: DocumentTranslation = {
|
||||
id: `doc_${Date.now()}`,
|
||||
userId: data.userId || '',
|
||||
fileName: data.fileName || '',
|
||||
originalSize: data.originalSize || 0,
|
||||
fileUrl: data.fileUrl || '',
|
||||
sourceLanguage: data.sourceLanguage || '',
|
||||
targetLanguage: data.targetLanguage || '',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
quality: data.quality || 'basic',
|
||||
urgency: data.urgency || 'normal',
|
||||
estimatedTime: data.estimatedTime || 0,
|
||||
cost: data.cost || 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
paymentStatus: 'pending',
|
||||
refundAmount: 0,
|
||||
qualityScore: 0,
|
||||
issues: [],
|
||||
};
|
||||
return newDocument;
|
||||
}
|
||||
|
||||
async updateDocument(id: string, data: Partial<DocumentTranslation>): Promise<DocumentTranslation | null> {
|
||||
await this.connect();
|
||||
// 模拟更新文档
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteDocument(id: string): Promise<boolean> {
|
||||
await this.connect();
|
||||
// 模拟删除文档
|
||||
return true;
|
||||
}
|
||||
|
||||
// 预约相关操作
|
||||
async getAppointments(params?: any): Promise<Appointment[]> {
|
||||
await this.connect();
|
||||
// 模拟获取预约列表
|
||||
return [];
|
||||
}
|
||||
|
||||
async getAppointmentById(id: string): Promise<Appointment | null> {
|
||||
await this.connect();
|
||||
// 模拟获取单个预约
|
||||
return null;
|
||||
}
|
||||
|
||||
async createAppointment(data: Partial<Appointment>): Promise<Appointment> {
|
||||
await this.connect();
|
||||
// 模拟创建预约
|
||||
const newAppointment: Appointment = {
|
||||
id: `apt_${Date.now()}`,
|
||||
userId: data.userId || '',
|
||||
translatorId: data.translatorId || '',
|
||||
title: data.title || '',
|
||||
description: data.description || '',
|
||||
type: data.type || '',
|
||||
sourceLanguage: data.sourceLanguage || '',
|
||||
targetLanguage: data.targetLanguage || '',
|
||||
startTime: data.startTime || new Date().toISOString(),
|
||||
endTime: data.endTime || new Date().toISOString(),
|
||||
status: 'pending',
|
||||
cost: data.cost || 0,
|
||||
reminderSent: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
clientName: data.clientName || '',
|
||||
clientEmail: data.clientEmail || '',
|
||||
clientPhone: data.clientPhone || '',
|
||||
translatorName: data.translatorName || '',
|
||||
translatorEmail: data.translatorEmail || '',
|
||||
translatorPhone: data.translatorPhone || '',
|
||||
paymentStatus: 'pending',
|
||||
refundAmount: 0,
|
||||
qualityScore: 0,
|
||||
issues: [],
|
||||
urgency: data.urgency || 'normal',
|
||||
};
|
||||
return newAppointment;
|
||||
}
|
||||
|
||||
async updateAppointment(id: string, data: Partial<Appointment>): Promise<Appointment | null> {
|
||||
await this.connect();
|
||||
// 模拟更新预约
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteAppointment(id: string): Promise<boolean> {
|
||||
await this.connect();
|
||||
// 模拟删除预约
|
||||
return true;
|
||||
}
|
||||
|
||||
// 用户相关操作
|
||||
async getUsers(params?: any): Promise<User[]> {
|
||||
await this.connect();
|
||||
// 模拟获取用户列表
|
||||
return [];
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User | null> {
|
||||
await this.connect();
|
||||
// 模拟获取单个用户
|
||||
return null;
|
||||
}
|
||||
|
||||
async createUser(data: Partial<User>): Promise<User> {
|
||||
await this.connect();
|
||||
// 模拟创建用户
|
||||
const newUser: User = {
|
||||
id: `user_${Date.now()}`,
|
||||
name: data.name || '',
|
||||
email: data.email || '',
|
||||
phone: data.phone,
|
||||
role: data.role || 'client',
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return newUser;
|
||||
}
|
||||
|
||||
async updateUser(id: string, data: Partial<User>): Promise<User | null> {
|
||||
await this.connect();
|
||||
// 模拟更新用户
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<boolean> {
|
||||
await this.connect();
|
||||
// 模拟删除用户
|
||||
return true;
|
||||
}
|
||||
|
||||
// 译员相关操作
|
||||
async getTranslators(params?: any): Promise<Translator[]> {
|
||||
await this.connect();
|
||||
// 模拟获取译员列表
|
||||
return [];
|
||||
}
|
||||
|
||||
async getTranslatorById(id: string): Promise<Translator | null> {
|
||||
await this.connect();
|
||||
// 模拟获取单个译员
|
||||
return null;
|
||||
}
|
||||
|
||||
async createTranslator(data: Partial<Translator>): Promise<Translator> {
|
||||
await this.connect();
|
||||
// 模拟创建译员
|
||||
const newTranslator: Translator = {
|
||||
id: `translator_${Date.now()}`,
|
||||
name: data.name || '',
|
||||
email: data.email || '',
|
||||
phone: data.phone || '',
|
||||
languages: data.languages || [],
|
||||
specializations: data.specializations || [],
|
||||
rating: data.rating || 0,
|
||||
hourlyRate: data.hourlyRate || 0,
|
||||
status: 'available',
|
||||
totalJobs: 0,
|
||||
successRate: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return newTranslator;
|
||||
}
|
||||
|
||||
async updateTranslator(id: string, data: Partial<Translator>): Promise<Translator | null> {
|
||||
await this.connect();
|
||||
// 模拟更新译员
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteTranslator(id: string): Promise<boolean> {
|
||||
await this.connect();
|
||||
// 模拟删除译员
|
||||
return true;
|
||||
}
|
||||
|
||||
// 统计相关操作
|
||||
async getStatistics(): Promise<any> {
|
||||
await this.connect();
|
||||
// 模拟获取统计数据
|
||||
return {
|
||||
totalCalls: 0,
|
||||
totalDocuments: 0,
|
||||
totalAppointments: 0,
|
||||
totalUsers: 0,
|
||||
totalTranslators: 0,
|
||||
totalRevenue: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const database = new DatabaseManager();
|
||||
export default database;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es6"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user