805 lines
24 KiB
TypeScript
Raw Normal View History

2025-06-28 14:20:17 +08:00
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;