import { useEffect, useState } from 'react'; import { Button, Drawer, Input, Modal, Space, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { StorePackageAuditDetailDto, StorePackageChangeRequestDto, StorePackageItemDto, StorePackageViewDto, } from '@dukang/shared-types'; import { STORE_PACKAGE_CHANGE_STATUS_LABELS } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { fmtTime } from '../lib/constants'; function packageKey(pkg: StorePackageItemDto | StorePackageViewDto, index: number) { const name = String(pkg.name ?? '').trim(); return name ? `name:${name}` : `idx:${index}`; } 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 ?? ''); 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; 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 [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 = await request>( `/admin/store-package-audits?${qs}`, ); setItems(data.items); setTotal(data.total); setPage(data.page); } catch (e) { message.error(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); } } useEffect(() => { void reload(1, status); }, [status]); 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); void reload(page, status); } catch (e) { message.error(e instanceof Error ? e.message : '操作失败'); } } const diffRows = detail ? diffPackages(detail.livePackages ?? [], detail.packages ?? []) : []; const diffColumns: ColumnsType<(typeof diffRows)[number]> = [ { title: '变更', dataIndex: 'change', width: 72, render: (v: keyof typeof CHANGE_LABELS) => { const meta = CHANGE_LABELS[v]; return {meta.text}; }, }, { title: '当前线上', render: (_, row) => row.live ? (
{row.live.name} · ¥{row.live.price}
{row.live.dishes} {row.live.usableTime ? (
可用:{row.live.usableTime}
) : null} {row.live.otherNotes ? (
备注:{row.live.otherNotes}
) : null}
) : ( '—' ), }, { title: '申请变更', render: (_, row) => row.proposed ? (
{row.proposed.name} · ¥{row.proposed.price}
{row.proposed.dishes} {row.proposed.usableTime ? (
可用:{row.proposed.usableTime}
) : null} {row.proposed.otherNotes ? (
备注:{row.proposed.otherNotes}
) : null}
) : ( '—' ), }, ]; const columns: ColumnsType = [ { title: '门店', dataIndex: 'storeName', render: (_, row) => row.storeName || row.storeId }, { title: '状态', dataIndex: 'status', render: (v: StorePackageChangeRequestDto['status']) => ( {STORE_PACKAGE_CHANGE_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 ? ( <> {STORE_PACKAGE_CHANGE_STATUS_LABELS[detail.status]} 提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : '门店'} · {fmtTime(detail.createdAt)} {detail.rejectReason ? ( 驳回原因:{detail.rejectReason} ) : null} 线上 {detail.livePackages?.length ?? 0} 条 → 申请 {detail.packages?.length ?? 0} 条
) : 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)} /> ); }