门店审核功能
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user