805 lines
24 KiB
TypeScript
805 lines
24 KiB
TypeScript
|
|
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;
|