门店审核功能

This commit is contained in:
2026-07-14 15:25:17 +08:00
parent d61a43cc8c
commit e0f5977dd9
13 changed files with 479 additions and 40 deletions
+125 -4
View File
@@ -20,7 +20,7 @@ import {
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
import { ADMIN_OPTIONS_PAGE_SIZE, STORE_AUDIT_STATUS_LABELS, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
import {
validateStoreCreateStep1,
validateStoreCreateStep3,
@@ -42,6 +42,8 @@ type StoreRow = {
name: string;
phone: string;
status: string;
auditStatus?: string;
rejectReason?: string | null;
cityName: string;
district: string;
address: string;
@@ -73,6 +75,7 @@ export default function StoresPage() {
const qs = new URLSearchParams();
if (filters.name) qs.set('name', filters.name);
if (filters.status) qs.set('status', filters.status);
if (filters.auditStatus) qs.set('auditStatus', filters.auditStatus);
if (filters.phone) qs.set('phone', filters.phone);
return qs;
},
@@ -80,6 +83,9 @@ export default function StoresPage() {
);
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [rejectOpen, setRejectOpen] = useState(false);
const [rejectReason, setRejectReason] = useState('');
const [auditing, setAuditing] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [createStep, setCreateStep] = useState(0);
const [createError, setCreateError] = useState('');
@@ -228,9 +234,26 @@ export default function StoresPage() {
{ title: '城市', dataIndex: 'cityName', width: 80 },
{ title: '电话', dataIndex: 'phone', width: 120 },
{
title: '状态', dataIndex: 'status', width: 90,
title: '营业状态', dataIndex: 'status', width: 90,
render: (s) => <Tag>{STORE_STATUS_LABELS[s] || s}</Tag>,
},
{
title: '审核', dataIndex: 'auditStatus', width: 100,
render: (s, row) => {
const status = s || 'APPROVED';
const color = status === 'PENDING' ? 'orange' : status === 'REJECTED' ? 'red' : 'green';
return (
<Space direction="vertical" size={0}>
<Tag color={color}>{STORE_AUDIT_STATUS_LABELS[status] || status}</Tag>
{status === 'REJECTED' && row.rejectReason ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }} ellipsis>
{row.rejectReason}
</Typography.Text>
) : null}
</Space>
);
},
},
{ title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 120 },
{ title: '介绍', dataIndex: 'intro', width: 160, ellipsis: true, render: (v) => v || '—' },
{ title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' },
@@ -268,9 +291,17 @@ export default function StoresPage() {
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="name" label="名称"><Input allowClear /></Form.Item>
<Form.Item name="phone" label="电话"><Input allowClear /></Form.Item>
<Form.Item name="status" label="状态">
<Form.Item name="status" label="营业状态">
<Select allowClear style={{ width: 100 }} placeholder="全部" options={Object.entries(STORE_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="auditStatus" label="审核">
<Select
allowClear
style={{ width: 110 }}
placeholder="全部"
options={Object.entries(STORE_AUDIT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item><Button type="primary" htmlType="submit"></Button></Form.Item>
<Form.Item><Button onClick={() => { form.resetFields(); setFilters({}); setPage(1); }}></Button></Form.Item>
</Form>
@@ -278,7 +309,41 @@ export default function StoresPage() {
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
<Drawer title="门店详情" width={600} open={drawerOpen} onClose={() => setDrawerOpen(false)}
extra={detail && (
<Space>
<Space wrap>
{String(detail.auditStatus || '') === 'PENDING' || String(detail.auditStatus || '') === 'REJECTED' ? (
<>
<Button
type="primary"
loading={auditing}
onClick={async () => {
setAuditing(true);
try {
const updated = await request<Record<string, unknown>>(`/admin/stores/${detail.id}/audit`, {
method: 'PUT',
body: JSON.stringify({ approved: true, remark: '审核通过' }),
});
message.success('已通过审核,合伙人可开门营业');
setDetail({ ...detail, ...updated, auditStatus: 'APPROVED', rejectReason: null });
void reload();
} finally {
setAuditing(false);
}
}}
>
</Button>
<Button
danger
loading={auditing}
onClick={() => {
setRejectReason('');
setRejectOpen(true);
}}
>
</Button>
</>
) : null}
<Select defaultValue={String(detail.status)} style={{ width: 120 }}
options={Object.entries(STORE_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
onChange={async (status) => {
@@ -304,6 +369,17 @@ export default function StoresPage() {
<>
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
<Descriptions.Item label="审核状态">
<Tag color={
String(detail.auditStatus) === 'PENDING' ? 'orange'
: String(detail.auditStatus) === 'REJECTED' ? 'red' : 'green'
}>
{STORE_AUDIT_STATUS_LABELS[String(detail.auditStatus || 'APPROVED')] || String(detail.auditStatus)}
</Tag>
</Descriptions.Item>
{String(detail.auditStatus) === 'REJECTED' ? (
<Descriptions.Item label="驳回原因">{String(detail.rejectReason || '—')}</Descriptions.Item>
) : null}
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
<Descriptions.Item label="核销结算比例">
{detail.settlementRate != null ? `${Math.round(Number(detail.settlementRate) * 100)}%` : '60%'}
@@ -319,6 +395,17 @@ export default function StoresPage() {
<Image src={String(detail.coverUrl)} width={120} />
</Descriptions.Item>
) : null}
{Array.isArray(detail.audits) && (detail.audits as Array<Record<string, unknown>>).length > 0 ? (
<Descriptions.Item label="审核记录">
<Space direction="vertical" size={4} style={{ width: '100%' }}>
{(detail.audits as Array<Record<string, unknown>>).map((a) => (
<Typography.Text key={String(a.id)} style={{ fontSize: 12 }}>
{fmtTime(String(a.createdAt))} · {String(a.status)} · {String(a.remark || '—')}
</Typography.Text>
))}
</Space>
</Descriptions.Item>
) : null}
</Descriptions>
<Form form={editForm} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
@@ -449,6 +536,40 @@ export default function StoresPage() {
</div>
</Form>
</Modal>
<Modal
title="驳回门店审核"
open={rejectOpen}
okText="确认驳回"
okButtonProps={{ danger: true, loading: auditing, disabled: !rejectReason.trim() }}
onCancel={() => setRejectOpen(false)}
onOk={async () => {
if (!detail || !rejectReason.trim()) return;
setAuditing(true);
try {
const updated = await request<Record<string, unknown>>(`/admin/stores/${detail.id}/audit`, {
method: 'PUT',
body: JSON.stringify({ approved: false, remark: rejectReason.trim() }),
});
message.success('已驳回,原因已同步合伙人端');
setDetail({ ...detail, ...updated, auditStatus: 'REJECTED', rejectReason: rejectReason.trim() });
setRejectOpen(false);
void reload();
} finally {
setAuditing(false);
}
}}
>
<Typography.Paragraph type="secondary"></Typography.Paragraph>
<Input.TextArea
rows={4}
maxLength={200}
showCount
placeholder="请填写驳回原因"
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
/>
</Modal>
</div>
);
}