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;
|