import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Badge, Button, Drawer, Image, Input, Modal, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { StorePackageAuditDetailDto, StorePackageAuditSummaryDto, StorePackageChangeRequestDto, StorePackageChangeStatus, StorePackageItemDto, StorePackageViewDto, } from '@dukang/shared-types'; import { normalizeStorePackageImageUrls } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { notifyPackageAuditChanged } from '../lib/admin-events'; import { fmtTime } from '../lib/constants'; const HQ_PACKAGE_STATUS_LABELS: Record = { PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回', }; function packageKey(pkg: StorePackageItemDto | StorePackageViewDto, index: number) { const name = String(pkg.name ?? '').trim(); return name ? `name:${name}` : `idx:${index}`; } function imageSignature(pkg: StorePackageItemDto | StorePackageViewDto) { return normalizeStorePackageImageUrls(pkg).join('|'); } function diffPackages(live: StorePackageViewDto[], proposed: StorePackageItemDto[]) { const liveMap = new Map(live.map((p, i) => [packageKey(p, i), p])); const proposedMap = new Map(proposed.map((p, i) => [packageKey(p, i), p])); const keys = new Set([...liveMap.keys(), ...proposedMap.keys()]); const rows: Array<{ key: string; change: 'added' | 'removed' | 'changed' | 'unchanged'; live?: StorePackageViewDto; proposed?: StorePackageItemDto; }> = []; for (const key of keys) { const l = liveMap.get(key); const p = proposedMap.get(key); if (l && !p) { rows.push({ key, change: 'removed', live: l }); } else if (!l && p) { rows.push({ key, change: 'added', proposed: p }); } else if (l && p) { const changed = l.price !== p.price || l.dishes !== p.dishes || (l.usableTime ?? '') !== (p.usableTime ?? '') || (l.otherNotes ?? '') !== (p.otherNotes ?? '') || imageSignature(l) !== imageSignature(p); rows.push({ key, change: changed ? 'changed' : 'unchanged', live: l, proposed: p }); } } return rows; } const CHANGE_LABELS = { added: { text: '新增', color: 'green' }, removed: { text: '删除', color: 'red' }, changed: { text: '变更', color: 'orange' }, unchanged: { text: '未变', color: 'default' }, } as const; function PackageDetailCard({ title, pkg, change, }: { title?: string; pkg: StorePackageItemDto | StorePackageViewDto; change?: keyof typeof CHANGE_LABELS; }) { const images = normalizeStorePackageImageUrls(pkg); const meta = change ? CHANGE_LABELS[change] : null; return (
{title ? ( {title} ) : null} {meta ? {meta.text} : null}
{pkg.name} ¥{pkg.price}
{pkg.dishes || '—'} {pkg.usableTime ? ( 可用时间:{pkg.usableTime} ) : null} {pkg.otherNotes ? ( 其他说明:{pkg.otherNotes} ) : null} {images.length ? ( {images.map((url) => ( ))} ) : ( 无套餐图片 )}
); } 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 [detailLoading, setDetailLoading] = useState(false); const [detail, setDetail] = useState(null); 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]); // 从门店详情 / 门店列表跳转过来时,带 requestId 自动打开审核(对比)抽屉 const [searchParams] = useSearchParams(); useEffect(() => { const rid = searchParams.get('requestId'); if (rid) void openDetail(rid); // 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-package-audits/${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-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 diffRows = detail ? diffPackages(detail.livePackages ?? [], detail.packages ?? []) : []; const changeByKey = new Map(diffRows.map((row) => [row.key, row.change])); 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), }} /> setDetailOpen(false)} extra={ detail?.status === 'PENDING' ? ( ) : null } > {detailLoading ? ( 加载中… ) : detail ? ( <> {HQ_PACKAGE_STATUS_LABELS[detail.status]} 提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : '门店'} · {fmtTime(detail.createdAt)} {detail.rejectReason ? ( 驳回原因:{detail.rejectReason} ) : null} 线上已审核 {detail.livePackages?.length ?? 0} 条 · 待审核 {detail.packages?.length ?? 0} 条
线上已审核套餐 {(detail.livePackages ?? []).length ? ( (detail.livePackages ?? []).map((pkg, index) => ( )) ) : ( 暂无线上套餐 )}
待审核套餐 {(detail.packages ?? []).length ? ( (detail.packages ?? []).map((pkg, index) => ( )) ) : ( 暂无待审核套餐 )}
) : 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)} /> ); }