feat(store): 门店套餐 v3.4.10 全端实现
新增 StorePackage 与变更审核流,覆盖总部直存、合伙/门店提审、C 端展示与套餐异议工单;同步 PRD/开发文档与 REQ 索引。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { StorePackageChangeRequestDto } 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';
|
||||
|
||||
export default function StorePackageAuditsPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [items, setItems] = useState<StorePackageChangeRequestDto[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<string>('PENDING');
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [activeId, setActiveId] = useState<string | null>(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<Paginated<StorePackageChangeRequestDto>>(
|
||||
`/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 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' ? '已通过' : '已驳回');
|
||||
void reload(page, status);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败');
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<StorePackageChangeRequestDto> = [
|
||||
{ title: '门店', dataIndex: 'storeName', render: (_, row) => row.storeName || row.storeId },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (v: StorePackageChangeRequestDto['status']) => (
|
||||
<Tag>{STORE_PACKAGE_CHANGE_STATUS_LABELS[v] ?? v}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
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' ? (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => void audit(row.id, 'APPROVE')}>
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(row.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
row.rejectReason || '—'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4}>套餐变更审核</Typography.Title>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
{(['PENDING', 'APPROVED', 'REJECTED', ''] as const).map((s) => (
|
||||
<Button key={s || 'all'} type={status === s ? 'primary' : 'default'} onClick={() => setStatus(s)}>
|
||||
{s ? STORE_PACKAGE_CHANGE_STATUS_LABELS[s as keyof typeof STORE_PACKAGE_CHANGE_STATUS_LABELS] : '全部'}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => void reload(p, status),
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
title="驳回套餐变更"
|
||||
open={rejectOpen}
|
||||
onCancel={() => setRejectOpen(false)}
|
||||
onOk={() => {
|
||||
if (!activeId) return;
|
||||
if (!rejectReason.trim()) {
|
||||
message.warning('请填写驳回原因');
|
||||
return;
|
||||
}
|
||||
void audit(activeId, 'REJECT', rejectReason.trim());
|
||||
setRejectOpen(false);
|
||||
}}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={rejectReason}
|
||||
placeholder="驳回原因"
|
||||
onChange={(e) => setRejectReason(e.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import { resolveRegionBinding } from '../lib/china-region';
|
||||
import ChinaRegionCascader from '../components/ChinaRegionCascader';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
import TencentLocPickerModal from '../components/TencentLocPickerModal';
|
||||
import AdminStorePackagesSection from '../components/AdminStorePackagesSection';
|
||||
|
||||
const CREATE_STEPS = [
|
||||
{ title: '基本信息' },
|
||||
@@ -1214,6 +1215,11 @@ export default function StoresPage() {
|
||||
label: '审核材料',
|
||||
children: <StoreAuditMediaSection detail={detail} />,
|
||||
},
|
||||
{
|
||||
key: 'packages',
|
||||
label: '套餐',
|
||||
children: <AdminStorePackagesSection storeId={String(detail.id)} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -159,6 +159,7 @@ export default function TicketsPage() {
|
||||
{ value: 'RESHIPMENT', label: '破损补发' },
|
||||
{ value: 'DAMAGE_RETURN', label: '破损退货' },
|
||||
{ value: 'RETURN_REFUND', label: '退货退款' },
|
||||
{ value: 'PACKAGE_DISPUTE', label: '套餐异议' },
|
||||
{ value: 'ALERT', label: '异常' },
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user