后台管理端调整

This commit is contained in:
2025-06-28 14:20:17 +08:00
parent cf40d6adeb
commit 7fcff7759d
25 changed files with 25447 additions and 0 deletions
+17369
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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"
}
}
+20
View File
@@ -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>
+231
View File
@@ -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;
}
+140
View File
@@ -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;
+21
View File
@@ -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%;
}
+15
View File
@@ -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;
+72
View File
@@ -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
+156
View File
@@ -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;
}
+220
View File
@@ -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;
+290
View File
@@ -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;
+26
View File
@@ -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"
]
}