import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Badge, Button, Descriptions, Drawer, Image, Input, Modal, Space, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { StoreInfoChangeFieldDiff, StoreInfoChangeRequestDto, StoreInfoChangeStatus, StorePackageAuditSummaryDto, StorePackageChangeRequestDto, StorePackageChangeStatus, } from '@dukang/shared-types'; import { STORE_INFO_CHANGE_STATUS_LABELS, STORE_INFO_CHANGEABLE_FIELD_LABELS, } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { notifyPackageAuditChanged } from '../lib/admin-events'; import { fmtTime } from '../lib/constants'; import StorePackageAuditPanel from '../components/StorePackageAuditPanel'; const HQ_PACKAGE_STATUS_LABELS: Record = { PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回', }; function fmtFieldValue(field: string, v: unknown): string { if (v == null || String(v).trim() === '') return '(空)'; if (field === 'avgPrice' || field === 'latitude' || field === 'longitude') { return String(v); } if (field === 'envPhotoUrls' && Array.isArray(v)) { return `${v.length} 张`; } if (field === 'coverUrl') return '见对比图'; return String(v); } function InfoChangeImageDiff({ field, live, proposed, imageSize = 72, }: { field: string; live: unknown; proposed: unknown; imageSize?: number; }) { const liveUrls = field === 'coverUrl' ? [String(live || '').trim()].filter(Boolean) : Array.isArray(live) ? live.map((u) => String(u || '').trim()).filter(Boolean) : []; const proposedUrls = field === 'coverUrl' ? [String(proposed || '').trim()].filter(Boolean) : Array.isArray(proposed) ? proposed.map((u) => String(u || '').trim()).filter(Boolean) : []; return (
变更前 {liveUrls.length ? ( {liveUrls.map((url) => ( ))} ) : ( (空) )}
变更后 {proposedUrls.length ? ( {proposedUrls.map((url) => ( ))} ) : ( (空) )}
); } function InfoChangeAuditPanel({ initialRequestId, }: { initialRequestId?: string | null; }) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [status, setStatus] = useState('PENDING'); const [pendingCount, setPendingCount] = useState(0); const [detailOpen, setDetailOpen] = useState(false); const [detailLoading, setDetailLoading] = useState(false); const [detail, setDetail] = useState<(StoreInfoChangeRequestDto & { diffs?: StoreInfoChangeFieldDiff[] }) | null>(null); const [rejectOpen, setRejectOpen] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [activeId, setActiveId] = useState(null); const [infoFullscreen, setInfoFullscreen] = useState(false); async function reload(nextPage = page, nextStatus = status) { setLoading(true); try { const qs = new URLSearchParams({ page: String(nextPage), pageSize: '20' }); if (nextStatus) qs.set('status', nextStatus); const [data, summary] = await Promise.all([ request<{ items: StoreInfoChangeRequestDto[]; total: number; page?: number }>( `/admin/store-info-change-requests?${qs}`, ), request<{ pendingCount: number }>('/admin/store-info-change-requests/summary'), ]); setItems(data.items); setTotal(data.total); setPage(data.page ?? nextPage); setPendingCount(summary.pendingCount ?? 0); } catch (e) { message.error(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); } } useEffect(() => { void reload(1, status); }, [status]); useEffect(() => { if (initialRequestId) void openDetail(initialRequestId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function openDetail(id: string) { setDetailOpen(true); setDetailLoading(true); setDetail(null); try { const data = await request( `/admin/store-info-change-requests/${id}`, ); setDetail(data); } catch (e) { message.error(e instanceof Error ? e.message : '加载详情失败'); setDetailOpen(false); } finally { setDetailLoading(false); } } async function audit(id: string, action: 'APPROVE' | 'REJECT', reason?: string) { try { await request(`/admin/store-info-change-requests/${id}/audit`, { method: 'PUT', body: JSON.stringify( action === 'REJECT' ? { action, rejectReason: reason } : { action }, ), }); message.success(action === 'APPROVE' ? '已通过' : '已驳回'); setDetailOpen(false); notifyPackageAuditChanged(); void reload(page, status); } catch (e) { message.error(e instanceof Error ? e.message : '操作失败'); } } const columns: ColumnsType = [ { title: '门店', dataIndex: 'storeName', render: (_, row) => row.storeName || row.storeId }, { title: '状态', dataIndex: 'status', render: (v: StoreInfoChangeStatus) => ( {STORE_INFO_CHANGE_STATUS_LABELS[v] ?? v} ), }, { title: '变更字段', render: (_, row) => row.changedFields?.length ? row.changedFields.map((f) => ( {STORE_INFO_CHANGEABLE_FIELD_LABELS[f as keyof typeof STORE_INFO_CHANGEABLE_FIELD_LABELS] ?? f} )) : '—', }, { title: '提交方', render: (_, row) => row.submitterType === 'PARTNER' ? '合伙人' : row.submitterType === 'SHOP' ? '门店' : '总部', }, { title: '提交时间', dataIndex: 'createdAt', render: (v) => fmtTime(String(v)) }, { title: '操作', render: (_, row) => ( {row.status === 'PENDING' ? ( <> ) : ( row.rejectReason || null )} ), }, ]; return (
{(['PENDING', 'APPROVED', 'REJECTED', ''] as const).map((s) => ( ))} void reload(p, status), }} /> { setDetailOpen(false); setInfoFullscreen(false); }} extra={ {detail?.status === 'PENDING' ? ( <> ) : null} } > {detailLoading ? ( 加载中… ) : detail ? ( <> {STORE_INFO_CHANGE_STATUS_LABELS[detail.status]} 提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : detail.submitterType === 'SHOP' ? '门店' : '总部'} · {fmtTime(detail.createdAt)} {detail.rejectReason ? ( 驳回原因:{detail.rejectReason} ) : null} {detail.diffs && detail.diffs.length ? ( {detail.diffs.map((d) => ( {d.field === 'coverUrl' || d.field === 'envPhotoUrls' ? ( ) : ( {fmtFieldValue(d.field, d.live)} {fmtFieldValue(d.field, d.proposed)} )} ))} ) : ( 无变更字段明细 )} ) : null} setRejectOpen(false)} onOk={() => { if (!activeId) return; if (!rejectReason.trim()) { message.warning('请填写驳回原因'); return; } void audit(activeId, 'REJECT', rejectReason.trim()); setRejectOpen(false); }} > setRejectReason(e.target.value)} /> ); } export default function StorePackageAuditsPage() { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [status, setStatus] = useState('PENDING'); const [pendingCount, setPendingCount] = useState(0); const [rejectOpen, setRejectOpen] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [activeId, setActiveId] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [activeRequestId, setActiveRequestId] = useState(null); const [drawerTitle, setDrawerTitle] = useState('套餐变更详情'); const [packageFullscreen, setPackageFullscreen] = useState(false); async function reload(nextPage = page, nextStatus = status) { setLoading(true); try { const qs = new URLSearchParams({ page: String(nextPage), pageSize: '20', }); if (nextStatus) qs.set('status', nextStatus); const [data, summary] = await Promise.all([ request>(`/admin/store-package-audits?${qs}`), request('/admin/store-package-audits/summary'), ]); setItems(data.items); setTotal(data.total); setPage(data.page); setPendingCount(summary.pendingCount ?? 0); } catch (e) { message.error(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); } } useEffect(() => { void reload(1, status); }, [status]); const [searchParams] = useSearchParams(); const initialTab = searchParams.get('tab') === 'info' ? 'info' : 'package'; const [activeTab, setActiveTab] = useState(initialTab); const infoRequestId = searchParams.get('infoRequestId'); useEffect(() => { const rid = searchParams.get('requestId'); if (rid) { setActiveRequestId(rid); setDetailOpen(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function openDetail(id: string) { setActiveRequestId(id); setDrawerTitle('套餐变更详情'); setDetailOpen(true); } async function audit(id: string, action: 'APPROVE' | 'REJECT', reason?: string) { try { await request(`/admin/store-package-audits/${id}/audit`, { method: 'PUT', body: JSON.stringify( action === 'REJECT' ? { action, rejectReason: reason } : { action }, ), }); message.success(action === 'APPROVE' ? '已通过' : '已驳回'); setDetailOpen(false); notifyPackageAuditChanged(); void reload(page, status); } catch (e) { message.error(e instanceof Error ? e.message : '操作失败'); } } const columns: ColumnsType = [ { title: '门店', dataIndex: 'storeName', render: (_, row) => row.storeName || row.storeId }, { title: '状态', dataIndex: 'status', render: (v: StorePackageChangeRequestDto['status']) => ( {HQ_PACKAGE_STATUS_LABELS[v] ?? v} ), }, { title: '套餐数', render: (_, row) => row.packages?.length ?? 0, }, { title: '提交方', render: (_, row) => (row.submitterType === 'PARTNER' ? '合伙人' : '门店'), }, { title: '提交时间', dataIndex: 'createdAt', render: (v) => fmtTime(String(v)) }, { title: '操作', render: (_, row) => ( {row.status === 'PENDING' ? ( <> ) : ( row.rejectReason || null )} ), }, ]; return (
审核通知 {(['PENDING', 'APPROVED', 'REJECTED', ''] as const).map((s) => ( ))}
void reload(p, status), }} /> ), }, { key: 'info', label: '信息变更', children: , }, ]} /> { setDetailOpen(false); setPackageFullscreen(false); }} destroyOnClose extra={ } > {activeRequestId ? ( { setDetailOpen(false); void reload(page, status); }} onDetailLoaded={(d) => { if (d) setDrawerTitle(`${d.storeName || d.storeId} · 套餐变更`); }} /> ) : null} setRejectOpen(false)} onOk={() => { if (!activeId) return; if (!rejectReason.trim()) { message.warning('请填写驳回原因'); return; } void audit(activeId, 'REJECT', rejectReason.trim()); setRejectOpen(false); }} > setRejectReason(e.target.value)} /> ); }