2025-06-28 14:20:17 +08:00

805 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;