后台管理端调整
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Descriptions,
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Space,
|
||||
Modal,
|
||||
Input,
|
||||
message,
|
||||
Spin,
|
||||
Calendar,
|
||||
Badge,
|
||||
Avatar,
|
||||
Timeline,
|
||||
Tabs,
|
||||
Form,
|
||||
DatePicker,
|
||||
Select,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserOutlined,
|
||||
PhoneOutlined,
|
||||
VideoCameraOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MessageOutlined,
|
||||
LinkOutlined,
|
||||
DollarOutlined,
|
||||
TranslationOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Appointment } from '@/types';
|
||||
import { database } from '@/utils/database';
|
||||
import { api } from '@/utils/api';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
const { TabPane } = Tabs;
|
||||
const { Option } = Select;
|
||||
|
||||
interface AppointmentDetailProps {}
|
||||
|
||||
const AppointmentDetail: React.FC<AppointmentDetailProps> = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [appointment, setAppointment] = useState<Appointment | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [cancelModalVisible, setCancelModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
loadAppointmentDetails();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadAppointmentDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await database.connect();
|
||||
|
||||
// 模拟获取预约详情
|
||||
const mockAppointment: Appointment = {
|
||||
id: id!,
|
||||
userId: 'user_1',
|
||||
translatorId: 'translator_1',
|
||||
title: '商务会议翻译',
|
||||
description: '重要客户会议,需要专业的商务翻译服务,涉及合同条款和技术细节讨论。',
|
||||
type: 'human',
|
||||
sourceLanguage: 'zh-CN',
|
||||
targetLanguage: 'en-US',
|
||||
startTime: '2024-01-20T14:00:00Z',
|
||||
endTime: '2024-01-20T16:00:00Z',
|
||||
status: 'confirmed',
|
||||
cost: 200.00,
|
||||
meetingUrl: 'https://meet.example.com/room/abc123',
|
||||
notes: '客户要求准时开始,请提前5分钟进入会议室',
|
||||
reminderSent: true,
|
||||
createdAt: '2024-01-15T10:00:00Z',
|
||||
updatedAt: '2024-01-15T10:00:00Z',
|
||||
};
|
||||
|
||||
setAppointment(mockAppointment);
|
||||
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
title: mockAppointment.title,
|
||||
description: mockAppointment.description,
|
||||
type: mockAppointment.type,
|
||||
sourceLanguage: mockAppointment.sourceLanguage,
|
||||
targetLanguage: mockAppointment.targetLanguage,
|
||||
startTime: dayjs(mockAppointment.startTime),
|
||||
endTime: dayjs(mockAppointment.endTime),
|
||||
notes: mockAppointment.notes,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载预约详情失败:', error);
|
||||
message.error('加载预约详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (values: any) => {
|
||||
if (!appointment) return;
|
||||
|
||||
try {
|
||||
const updatedAppointment = {
|
||||
...appointment,
|
||||
...values,
|
||||
startTime: values.startTime.toISOString(),
|
||||
endTime: values.endTime.toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setAppointment(updatedAppointment);
|
||||
setEditModalVisible(false);
|
||||
message.success('预约信息更新成功');
|
||||
} catch (error) {
|
||||
message.error('更新预约信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
if (!appointment) return;
|
||||
|
||||
try {
|
||||
const updatedAppointment = {
|
||||
...appointment,
|
||||
status: 'cancelled' as const,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setAppointment(updatedAppointment);
|
||||
setCancelModalVisible(false);
|
||||
message.success('预约已取消');
|
||||
} catch (error) {
|
||||
message.error('取消预约失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinMeeting = () => {
|
||||
if (appointment?.meetingUrl) {
|
||||
window.open(appointment.meetingUrl, '_blank');
|
||||
} else {
|
||||
message.warning('会议链接不可用');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
scheduled: 'orange',
|
||||
confirmed: 'blue',
|
||||
cancelled: 'red',
|
||||
completed: 'green',
|
||||
};
|
||||
return colors[status as keyof typeof colors] || 'default';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
scheduled: '已安排',
|
||||
confirmed: '已确认',
|
||||
cancelled: '已取消',
|
||||
completed: '已完成',
|
||||
};
|
||||
return texts[status as keyof typeof texts] || status;
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
const icons = {
|
||||
ai: '🤖',
|
||||
human: '👤',
|
||||
video: '📹',
|
||||
sign: '🤟',
|
||||
};
|
||||
return icons[type as keyof typeof icons] || '📞';
|
||||
};
|
||||
|
||||
const getTypeText = (type: string) => {
|
||||
const texts = {
|
||||
ai: 'AI翻译',
|
||||
human: '人工翻译',
|
||||
video: '视频通话',
|
||||
sign: '手语翻译',
|
||||
};
|
||||
return texts[type as keyof typeof texts] || type;
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
return dayjs(dateTime).format('YYYY-MM-DD HH:mm');
|
||||
};
|
||||
|
||||
const getDuration = () => {
|
||||
if (!appointment) return '';
|
||||
const start = dayjs(appointment.startTime);
|
||||
const end = dayjs(appointment.endTime);
|
||||
const duration = end.diff(start, 'minute');
|
||||
const hours = Math.floor(duration / 60);
|
||||
const minutes = duration % 60;
|
||||
return hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;
|
||||
};
|
||||
|
||||
const getTimelineData = () => {
|
||||
if (!appointment) return [];
|
||||
|
||||
const timeline = [
|
||||
{
|
||||
color: 'green',
|
||||
children: (
|
||||
<div>
|
||||
<div><strong>预约创建</strong></div>
|
||||
<div>{formatDateTime(appointment.createdAt)}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (appointment.status === 'confirmed') {
|
||||
timeline.push({
|
||||
color: 'blue',
|
||||
children: (
|
||||
<div>
|
||||
<div><strong>预约确认</strong></div>
|
||||
<div>{formatDateTime(appointment.updatedAt)}</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (appointment.reminderSent) {
|
||||
timeline.push({
|
||||
color: 'orange',
|
||||
children: (
|
||||
<div>
|
||||
<div><strong>提醒已发送</strong></div>
|
||||
<div>会议前24小时</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (appointment.status === 'completed') {
|
||||
timeline.push({
|
||||
color: 'green',
|
||||
children: (
|
||||
<div>
|
||||
<div><strong>服务完成</strong></div>
|
||||
<div>{formatDateTime(appointment.endTime)}</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (appointment.status === 'cancelled') {
|
||||
timeline.push({
|
||||
color: 'red',
|
||||
children: (
|
||||
<div>
|
||||
<div><strong>预约取消</strong></div>
|
||||
<div>{formatDateTime(appointment.updatedAt)}</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return timeline;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<Spin size="large" />
|
||||
<div style={{ marginTop: '16px' }}>加载预约详情...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!appointment) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<div>预约记录不存在</div>
|
||||
<Button type="primary" onClick={() => navigate('/appointments')} style={{ marginTop: '16px' }}>
|
||||
返回预约列表
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isUpcoming = dayjs(appointment.startTime).isAfter(dayjs());
|
||||
const canEdit = appointment.status !== 'cancelled' && appointment.status !== 'completed';
|
||||
const canJoin = appointment.status === 'confirmed' && appointment.meetingUrl &&
|
||||
dayjs().isAfter(dayjs(appointment.startTime).subtract(5, 'minute')) &&
|
||||
dayjs().isBefore(dayjs(appointment.endTime));
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px' }}>
|
||||
{/* 头部导航 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/appointments')}
|
||||
style={{ marginRight: '16px' }}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Title level={2} style={{ display: 'inline-block', margin: 0 }}>
|
||||
预约详情 #{appointment.id}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{/* 快速操作按钮 */}
|
||||
<Card style={{ marginBottom: '24px' }}>
|
||||
<Space>
|
||||
{canJoin && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<VideoCameraOutlined />}
|
||||
onClick={handleJoinMeeting}
|
||||
>
|
||||
加入会议
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setEditModalVisible(true)}
|
||||
>
|
||||
编辑预约
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => setCancelModalVisible(true)}
|
||||
>
|
||||
取消预约
|
||||
</Button>
|
||||
)}
|
||||
{appointment.meetingUrl && (
|
||||
<Button
|
||||
icon={<LinkOutlined />}
|
||||
onClick={() => navigator.clipboard.writeText(appointment.meetingUrl!)}
|
||||
>
|
||||
复制会议链接
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card title="预约信息" style={{ marginBottom: '24px' }}>
|
||||
<Descriptions column={2} bordered>
|
||||
<Descriptions.Item label="预约标题" span={2}>
|
||||
<Text strong style={{ fontSize: '16px' }}>{appointment.title}</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态" span={1}>
|
||||
<Tag color={getStatusColor(appointment.status)} icon={
|
||||
appointment.status === 'confirmed' ? <CheckCircleOutlined /> :
|
||||
appointment.status === 'cancelled' ? <ExclamationCircleOutlined /> :
|
||||
<ClockCircleOutlined />
|
||||
}>
|
||||
{getStatusText(appointment.status)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="服务类型" span={1}>
|
||||
<Space>
|
||||
<span>{getTypeIcon(appointment.type)}</span>
|
||||
{getTypeText(appointment.type)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="语言对" span={1}>
|
||||
<Tag color="blue">{appointment.sourceLanguage}</Tag>
|
||||
<span style={{ margin: '0 8px' }}>→</span>
|
||||
<Tag color="green">{appointment.targetLanguage}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="费用" span={1}>
|
||||
<Space>
|
||||
<DollarOutlined />
|
||||
<Text strong>¥{appointment.cost.toFixed(2)}</Text>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间" span={1}>
|
||||
<Space>
|
||||
<CalendarOutlined />
|
||||
{formatDateTime(appointment.startTime)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间" span={1}>
|
||||
<Space>
|
||||
<CalendarOutlined />
|
||||
{formatDateTime(appointment.endTime)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="持续时间" span={1}>
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
{getDuration()}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="译员" span={1}>
|
||||
<Space>
|
||||
<Avatar size="small" icon={<UserOutlined />} />
|
||||
{appointment.translatorId || '待分配'}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
{appointment.meetingUrl && (
|
||||
<Descriptions.Item label="会议链接" span={2}>
|
||||
<Space>
|
||||
<LinkOutlined />
|
||||
<a href={appointment.meetingUrl} target="_blank" rel="noopener noreferrer">
|
||||
{appointment.meetingUrl}
|
||||
</a>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
{appointment.description && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>预约描述:</Text>
|
||||
<Paragraph style={{ marginTop: '8px' }}>
|
||||
{appointment.description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appointment.notes && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<Text strong>备注信息:</Text>
|
||||
<Paragraph style={{ marginTop: '8px' }}>
|
||||
{appointment.notes}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 详细信息标签页 */}
|
||||
<Card>
|
||||
<Tabs defaultActiveKey="timeline">
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<ClockCircleOutlined />
|
||||
时间线
|
||||
</Space>
|
||||
}
|
||||
key="timeline"
|
||||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Timeline items={getTimelineData()} />
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<MessageOutlined />
|
||||
沟通记录
|
||||
</Space>
|
||||
}
|
||||
key="communication"
|
||||
>
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#999' }}>
|
||||
暂无沟通记录
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<Space>
|
||||
<TranslationOutlined />
|
||||
服务详情
|
||||
</Space>
|
||||
}
|
||||
key="service"
|
||||
>
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Descriptions column={1}>
|
||||
<Descriptions.Item label="服务时长">
|
||||
{getDuration()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="服务费用">
|
||||
¥{appointment.cost.toFixed(2)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="付费状态">
|
||||
<Tag color="green">已支付</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提醒设置">
|
||||
{appointment.reminderSent ? (
|
||||
<Tag color="green">已发送提醒</Tag>
|
||||
) : (
|
||||
<Tag color="orange">未发送提醒</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="title"
|
||||
label="预约标题"
|
||||
rules={[{ required: true, message: '请输入预约标题' }]}
|
||||
>
|
||||
<Input placeholder="请输入预约标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="预约描述"
|
||||
>
|
||||
<TextArea rows={3} placeholder="请输入预约描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="服务类型"
|
||||
rules={[{ required: true, message: '请选择服务类型' }]}
|
||||
>
|
||||
<Select placeholder="请选择服务类型">
|
||||
<Option value="ai">AI翻译</Option>
|
||||
<Option value="human">人工翻译</Option>
|
||||
<Option value="video">视频通话</Option>
|
||||
<Option value="sign">手语翻译</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<Form.Item
|
||||
name="sourceLanguage"
|
||||
label="源语言"
|
||||
style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请选择源语言' }]}
|
||||
>
|
||||
<Select placeholder="源语言">
|
||||
<Option value="zh-CN">中文</Option>
|
||||
<Option value="en-US">英语</Option>
|
||||
<Option value="ja-JP">日语</Option>
|
||||
<Option value="ko-KR">韩语</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="targetLanguage"
|
||||
label="目标语言"
|
||||
style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请选择目标语言' }]}
|
||||
>
|
||||
<Select placeholder="目标语言">
|
||||
<Option value="zh-CN">中文</Option>
|
||||
<Option value="en-US">英语</Option>
|
||||
<Option value="ja-JP">日语</Option>
|
||||
<Option value="ko-KR">韩语</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<Form.Item
|
||||
name="startTime"
|
||||
label="开始时间"
|
||||
style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
placeholder="选择开始时间"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="endTime"
|
||||
label="结束时间"
|
||||
style={{ flex: 1 }}
|
||||
rules={[{ required: true, message: '请选择结束时间' }]}
|
||||
>
|
||||
<DatePicker
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
placeholder="选择结束时间"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="notes"
|
||||
label="备注信息"
|
||||
>
|
||||
<TextArea rows={2} placeholder="请输入备注信息" />
|
||||
</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={cancelModalVisible}
|
||||
onOk={handleCancel}
|
||||
onCancel={() => setCancelModalVisible(false)}
|
||||
okText="确认取消"
|
||||
cancelText="保持预约"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>确定要取消这个预约吗?取消后将无法恢复。</p>
|
||||
<p>如果需要重新预约,请创建新的预约。</p>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppointmentDetail;
|
||||
Reference in New Issue
Block a user