import { Alert, Button, Card, Col, Descriptions, Divider, Drawer, Form, Input, InputNumber, Modal, Row, Select, Space, Statistic, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import { Copy, Eye, Plus, RefreshCw, ShieldBan, Trash2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { PageHeader, Surface } from '@/components/Page'; import { apiRequest, jsonOptions } from '@/services/api'; import type { FulfillmentMetrics, Paged, ServiceCustomerBlock, ServiceLeaderboardItem, ServiceOrder, ServiceOrderStatus, ServicePackageTemplate, ServiceReview, ServiceReviewQr, ServiceReviewTemplate, ServiceReport, ServiceSettings, } from '@/types'; const emptyPage = (): Paged => ({ items: [], total: 0, page: 1, page_size: 20 }); const statusLabels: Record = { requested: '待处理', quoted: '待确认报价', confirmed: '已确认', in_progress: '服务中', completion_pending: '待扫码确认', completed: '已完成', rejected: '已拒绝', canceled_customer: '顾客取消', canceled_technician: '技师取消', disputed: '争议中', voided: '已作废', }; const statusColors: Record = { requested: 'processing', quoted: 'warning', confirmed: 'cyan', in_progress: 'blue', completion_pending: 'purple', completed: 'success', rejected: 'error', canceled_customer: 'default', canceled_technician: 'default', disputed: 'volcano', voided: 'default', }; type TemplateFormValue = { template_name: string; admin_note?: string; sort_order?: number; name: string; category: string; description: string; price_display: string; duration_minutes?: number; additional_fee_description: string; }; export default function ServicesPage() { const [loading, setLoading] = useState(false); const [metrics, setMetrics] = useState(null); const [settings, setSettings] = useState(null); const [templates, setTemplates] = useState([]); const [orders, setOrders] = useState>(emptyPage()); const [reviews, setReviews] = useState>(emptyPage()); const [qrs, setQrs] = useState>(emptyPage()); const [leaderboard, setLeaderboard] = useState>(emptyPage()); const [customerBlocks, setCustomerBlocks] = useState>(emptyPage()); const [reports, setReports] = useState>(emptyPage()); const [reviewTemplate, setReviewTemplate] = useState(null); const [orderStatus, setOrderStatus] = useState(''); const [reviewStatus, setReviewStatus] = useState('pending'); const [query, setQuery] = useState(''); const [detail, setDetail] = useState(null); const [templateTarget, setTemplateTarget] = useState(null); const [templateOpen, setTemplateOpen] = useState(false); const [orderAction, setOrderAction] = useState<{ order: ServiceOrder; action: 'resume' | 'void' } | null>(null); const [addressTarget, setAddressTarget] = useState(null); const [reviewAction, setReviewAction] = useState<{ review: ServiceReview; action: 'reject' | 'void' } | null>(null); const [blockOpen, setBlockOpen] = useState(false); const [reportTarget, setReportTarget] = useState<{ report: ServiceReport; action: 'resolve' | 'dismiss' } | null>(null); const [settingsForm] = Form.useForm(); const [templateForm] = Form.useForm(); const [reviewTemplateForm] = Form.useForm>(); const [orderReasonForm] = Form.useForm<{ reason: string }>(); const [reviewReasonForm] = Form.useForm<{ reason: string }>(); const [addressForm] = Form.useForm<{ reason: string }>(); const [blockForm] = Form.useForm<{ customer_id: string; reason: string }>(); const [reportForm] = Form.useForm<{ reason: string }>(); const loadOrders = async (page = 1) => { const params = new URLSearchParams({ page: String(page), page_size: '20', status: orderStatus, query }); setOrders(await apiRequest>(`/directory/service-orders?${params}`)); }; const loadReviews = async (page = 1) => { const params = new URLSearchParams({ page: String(page), page_size: '20', status: reviewStatus, query }); setReviews(await apiRequest>(`/directory/reviews?${params}`)); }; const refresh = async () => { setLoading(true); try { const [metricsValue, settingsValue, templateList, qrList, rankList, activeReviewTemplate, blockList, reportList] = await Promise.all([ apiRequest('/directory/fulfillment-metrics'), apiRequest('/directory/service-settings'), apiRequest<{ items: ServicePackageTemplate[] }>('/directory/package-templates?include_archived=true'), apiRequest>('/directory/review-qrs?page=1&page_size=20'), apiRequest>('/directory/leaderboard?page=1&page_size=20'), apiRequest('/directory/review-template'), apiRequest>('/directory/customer-blocks?scope=global&active=true&page=1&page_size=20'), apiRequest>('/directory/reports?status=open&page=1&page_size=20'), ]); setMetrics(metricsValue); setSettings(settingsValue); settingsForm.setFieldsValue(settingsValue); setTemplates(templateList.items); setQrs(qrList); setLeaderboard(rankList); setCustomerBlocks(blockList); setReports(reportList); setReviewTemplate(activeReviewTemplate); reviewTemplateForm.setFieldsValue({ questions: activeReviewTemplate.questions, }); await Promise.all([loadOrders(orders.page), loadReviews(reviews.page)]); } catch (error) { message.error(error instanceof Error ? error.message : '咨询与评价数据加载失败'); } finally { setLoading(false); } }; useEffect(() => { void refresh(); }, []); const saveSettings = async (values: ServiceSettings) => { const saved = await apiRequest( '/directory/service-settings', jsonOptions('PUT', { ...values, confirm: true }), ); setSettings(saved); settingsForm.setFieldsValue(saved); message.success('咨询与评价设置已更新'); }; const openTemplate = (item?: ServicePackageTemplate) => { setTemplateTarget(item || null); setTemplateOpen(true); if (!item) { templateForm.setFieldsValue({ sort_order: 0, category: '通用服务', duration_minutes: 60, additional_fee_description: '其他费用请提前私聊沟通', }); return; } const value = item.package; templateForm.setFieldsValue({ template_name: item.name, admin_note: item.admin_note, sort_order: item.sort_order, name: value.name, category: value.category, description: value.description, price_display: value.price_display || `${value.currency} ${value.min_price}/${value.price_unit === 'per_hour' ? '小时' : '次'}`, duration_minutes: value.duration_minutes || undefined, additional_fee_description: value.travel_fee.description || '其他费用请提前私聊沟通', }); }; const saveTemplate = async (values: TemplateFormValue) => { const payload = { template_name: values.template_name, admin_note: values.admin_note, sort_order: values.sort_order, confirm: true, package: { name: values.name, category: values.category, description: values.description, tags: [], service_modes: ['at_store', 'onsite'], price_mode: 'negotiable', currency: 'CNY', price_unit: 'per_service', min_price: '0', max_price: '0', price_display: values.price_display, duration_minutes: values.duration_minutes, service_radius_km: null, included_items: '', excluded_items: '', preparation: '', out_of_range_policy: '', travel_fee: { mode: 'quoted', amount: '0', per_km: '0', description: values.additional_fee_description, }, addons: [], }, }; await apiRequest( templateTarget ? `/directory/package-templates/${templateTarget.template_id}` : '/directory/package-templates', jsonOptions(templateTarget ? 'PUT' : 'POST', payload), ); setTemplateOpen(false); templateForm.resetFields(); message.success(templateTarget ? '套餐模板已创建新版本' : '套餐模板已创建'); await refresh(); }; const templateAction = (item: ServicePackageTemplate, action: string) => { Modal.confirm({ title: action === 'archive' ? '归档套餐模板?' : `确认执行 ${action}?`, content: '模板操作不会同步、提醒或覆盖技师已发布套餐和历史服务单快照。', okText: '确认执行', onOk: async () => { await apiRequest( `/directory/package-templates/${item.template_id}/actions`, jsonOptions('POST', { action, confirm: true }), ); message.success('模板状态已更新'); await refresh(); }, }); }; const showOrder = async (order: ServiceOrder) => { setDetail(await apiRequest(`/directory/service-orders/${order.order_id}`)); }; const submitOrderAction = async ({ reason }: { reason: string }) => { if (!orderAction) return; await apiRequest( `/directory/service-orders/${orderAction.order.order_id}/actions`, jsonOptions('POST', { action: orderAction.action, reason, confirm: true }), ); setOrderAction(null); orderReasonForm.resetFields(); message.success('服务单状态已更新'); await refresh(); }; const revealAddress = async ({ reason }: { reason: string }) => { if (!addressTarget) return; const address = await apiRequest<{ kind: string; address_text: string; longitude: number; latitude: number }>( `/directory/service-orders/${addressTarget.order_id}/address`, jsonOptions('POST', { reason, confirm: true }), ); setAddressTarget(null); addressForm.resetFields(); Modal.info({ title: '精确履约地址(敏感信息)', content: ( {address.address_text} {address.latitude}, {address.longitude} 本次查看原因和管理员账号已记录审计。 ), }); }; const approveReview = async (review: ServiceReview) => { const approved = await apiRequest( `/directory/reviews/${review.review_id}/actions`, jsonOptions('POST', { action: 'approve', confirm: true }), ); if (approved.topic_publication?.status === 'published') { message.success('评价已审核通过并发布到技师公开话题'); } else if (approved.topic_publication?.status === 'failed') { message.warning(`评价已通过,但话题发布失败:${approved.topic_publication.message}`); } else { message.warning('评价已通过;配置公开评价群后可自动发布'); } await refresh(); }; const submitReviewAction = async ({ reason }: { reason: string }) => { if (!reviewAction) return; await apiRequest( `/directory/reviews/${reviewAction.review.review_id}/actions`, jsonOptions('POST', { action: reviewAction.action, reason, confirm: true }), ); setReviewAction(null); reviewReasonForm.resetFields(); message.success('评价状态已更新并重算排行'); await refresh(); }; const publishReviewTemplate = async (values: Pick) => { const published = await apiRequest( '/directory/review-template', jsonOptions('PUT', { ...values, confirm: true }), ); setReviewTemplate(published); message.success(`评价模板 V${published.version} 已发布,仅影响后续评价`); }; const blockCustomer = async ({ customer_id, reason }: { customer_id: string; reason: string }) => { await apiRequest( `/directory/customer-blocks/${customer_id}`, jsonOptions('POST', { active: true, reason, confirm: true }), ); setBlockOpen(false); blockForm.resetFields(); message.success('顾客已全局封禁'); await refresh(); }; const unblockCustomer = (item: ServiceCustomerBlock) => { Modal.confirm({ title: `解除顾客 ${item.customer_id} 的全局封禁?`, okText: '确认解除', onOk: async () => { await apiRequest( `/directory/customer-blocks/${item.customer_id}`, jsonOptions('POST', { active: false, reason: '管理员解除全局封禁', confirm: true }), ); message.success('顾客封禁已解除'); await refresh(); }, }); }; const submitReportAction = async ({ reason }: { reason: string }) => { if (!reportTarget) return; await apiRequest( `/directory/reports/${reportTarget.report.report_id}/actions`, jsonOptions('POST', { action: reportTarget.action, reason, confirm: true }), ); setReportTarget(null); reportForm.resetFields(); message.success('举报已处理'); await refresh(); }; const templateColumns: ColumnsType = [ { title: '模板', minWidth: 220, render: (_, item) => ( {item.name} {item.package.category} · V{item.version} ), }, { title: '价格 / 时长', minWidth: 220, render: (_, item) => ( {item.package.price_display || `${item.package.currency} ${item.package.min_price}`} 约 {item.package.duration_minutes || '-'} 分钟 ), }, { title: '排序', dataIndex: 'sort_order', width: 80 }, { title: '状态', dataIndex: 'status', width: 100, render: (value) => {value === 'enabled' ? '启用' : value === 'archived' ? '已归档' : '停用'} }, { title: '操作', width: 300, fixed: 'right', render: (_, item) => ( {item.status !== 'archived' ? ( ) : null} {item.status !== 'archived' ? : null} ), }, ]; const orderColumns: ColumnsType = [ { title: '服务单', dataIndex: 'order_id', width: 120, render: (value) => {value.slice(0, 8)} }, { title: '技师 / 顾客', minWidth: 220, render: (_, item) => {item.technician_name} · {item.technician_id}{item.customer_name} · {item.customer_id} }, { title: '套餐', minWidth: 180, render: (_, item) => {item.package_snapshot.name}{item.service_mode === 'at_store' ? '到店' : '上门'} · {item.distance_band} }, { title: '最终报价', width: 140, render: (_, item) => item.quote_snapshot ? `${item.quote_snapshot.currency} ${item.quote_snapshot.total_amount}` : '-' }, { title: '状态', dataIndex: 'status', width: 130, render: (value: ServiceOrderStatus) => {statusLabels[value]} }, { title: '更新时间', dataIndex: 'updated_at', width: 160, responsive: ['lg'], render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm') }, { title: '操作', width: 250, fixed: 'right', render: (_, item) => ( {item.status === 'disputed' ? : null} {item.status !== 'voided' ? : null} ), }, ]; const reviewColumns: ColumnsType = [ { title: '评价', dataIndex: 'review_id', width: 110, render: (value) => value.slice(0, 8) }, { title: '技师 / 顾客', minWidth: 210, render: (_, item) => {item.technician_name} · {item.technician_id}{item.customer_name} · {item.customer_id} }, { title: '来源', dataIndex: 'source', width: 150, render: (value) => {value === 'qr_verified' ? '完成服务单评价' : '用户主动评价'} }, { title: '评分', dataIndex: 'score', width: 90, render: (value) => value.toFixed(2) }, { title: '重复风险', width: 190, render: (_, item) => 累计 {item.customer_review_count} 次30天 {item.customer_review_count_30d} / 90天 {item.customer_review_count_90d} 1 ? 'danger' : 'secondary'}>相似内容 {item.similar_content_count} 次 }, { title: '评价内容', minWidth: 260, ellipsis: true, render: (_, item) => [ ...Object.values(item.choice_answers || {}).flat(), ...Object.values(item.text_answers || {}), ].join(';') || '-', }, { title: '公开话题', width: 110, render: (_, item) => item.topic_url ? 查看 : '-', }, { title: '状态', dataIndex: 'status', width: 100, render: (value) => {value === 'pending' ? '待审核' : value === 'approved' ? '已批准' : value === 'rejected' ? '已拒绝' : '已作废'} }, { title: '操作', width: 200, fixed: 'right', render: (_, item) => ( {item.status === 'pending' ? : null} {item.status === 'pending' ? : null} {item.status === 'approved' ? : null} ), }, ]; return ( <> } loading={loading} onClick={() => void refresh()}>刷新} /> {metrics?.scope_note} 公开评价话题
void saveSettings(values)}>
), }, { key: 'templates', label: `套餐模板 (${templates.length})`, children: ( <>
), }, { key: 'orders', label: `服务订单 (${orders.total})`, children: ( <>
setQuery(event.target.value)} onPressEnter={() => void loadOrders(1)} placeholder="订单、技师或顾客" style={{ width: 260 }} />
void loadOrders(pagination.current || 1)} scroll={{ x: 1200 }} /> ), }, { key: 'reviews', label: `评价审核 (${reviews.total})`, children: ( <>
setQuery(event.target.value)} onPressEnter={() => void loadReviews(1)} placeholder="技师、顾客或评价 ID" style={{ width: 260 }} />
void loadReviews(pagination.current || 1)} scroll={{ x: 1300 }} /> ), }, { key: 'review-template', label: `评价模板 V${reviewTemplate?.version || '-'}`, children: ( void publishReviewTemplate(values)}> 评价问题(1–8 个) {(fields, { add, remove }) => ( {fields.map((field) => ( } onClick={() => remove(field.name)} />} > {({ getFieldValue }) => { const questionType = getFieldValue(['questions', field.name, 'type']); if (questionType === 'text') { return ; } return ( <> {questionType === 'single_choice' ? ( ) : null} 选项(2–8 个) {(optionFields, optionActions) => ( {optionFields.map((optionField) => ( {questionType === 'single_choice' ? : null} )} ); }} ))} )} ), }, { key: 'ranking-risk', label: '排行与风控', children: ( <>
审核评价排行
`${item.technician_id}:${item.category}`} dataSource={leaderboard.items} pagination={false} columns={[ { title: '技师', render: (_, item) => `${item.display_name || item.technician_id}${item.username ? ` (@${item.username})` : ''}` }, { title: '分类', dataIndex: 'category' }, { title: '排行分', dataIndex: 'rank_score', render: (value) => value.toFixed(3) }, { title: '平均分', dataIndex: 'average_score', render: (value) => value.toFixed(2) }, { title: '批准评价', dataIndex: 'review_count' }, ]} /> 全局顾客黑名单
`${item.scope}:${item.customer_id}`} dataSource={customerBlocks.items} pagination={false} columns={[ { title: '顾客 ID', dataIndex: 'customer_id' }, { title: '原因', dataIndex: 'reason' }, { title: '更新时间', dataIndex: 'updated_at', render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm') }, { title: '操作', render: (_, item) => }, ]} /> 待处理举报
value.slice(0, 8) }, { title: '举报人', dataIndex: 'reporter_id' }, { title: '对象', render: (_, item) => `${item.target_type} · ${item.target_id.slice(0, 12)}` }, { title: '原因', dataIndex: 'reason' }, { title: '操作', render: (_, item) => }, ]} /> 完成评价二维码记录
value.slice(0, 8) }, { title: '服务单', dataIndex: 'order_id', render: (value) => value.slice(0, 8) }, { title: '技师 / 顾客', render: (_, item) => `${item.technician_id} / ${item.customer_id}` }, { title: '状态', dataIndex: 'status' }, { title: '过期时间', dataIndex: 'expires_at', render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm') }, ]} /> ), }, ]} /> { setTemplateOpen(false); templateForm.resetFields(); }} onOk={() => templateForm.submit()} okText="保存快照版本">
void saveTemplate(values)}>
setDetail(null)}> {detail ? ( <> 状态轨迹 {(detail.events || []).map((event, index) => ( {dayjs(String(event.created_at)).format('YYYY-MM-DD HH:mm:ss')} · {String(event.event_type)} · {String(event.reason || '')} ))} ) : null} setOrderAction(null)} onOk={() => orderReasonForm.submit()} okText="确认执行" okButtonProps={{ danger: orderAction?.action === 'void' }}>
void submitOrderAction(values)}>
setAddressTarget(null)} onOk={() => addressForm.submit()} okText="确认查看" okButtonProps={{ danger: true }}>
void revealAddress(values)}>
setReviewAction(null)} onOk={() => reviewReasonForm.submit()} okText="确认执行" okButtonProps={{ danger: true }}>
void submitReviewAction(values)}>
setBlockOpen(false)} onOk={() => blockForm.submit()} okText="确认封禁" okButtonProps={{ danger: true }}>
void blockCustomer(values)}>
setReportTarget(null)} onOk={() => reportForm.submit()} okText="确认处理">
void submitReportAction(values)}>
); }