576 lines
25 KiB
TypeScript
576 lines
25 KiB
TypeScript
import { useMemo, useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Descriptions,
|
||
Drawer,
|
||
Form,
|
||
Image,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Select,
|
||
Space,
|
||
Steps,
|
||
Table,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
} from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import { request, type Paginated } from '../lib/api';
|
||
import { ADMIN_OPTIONS_PAGE_SIZE, STORE_AUDIT_STATUS_LABELS, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||
import {
|
||
validateStoreCreateStep1,
|
||
validateStoreCreateStep3,
|
||
type StoreCreateForm,
|
||
} from '../lib/storeCreate';
|
||
import { useAdminList } from '../lib/useAdminList';
|
||
import { resolveRegionBinding } from '../lib/china-region';
|
||
import ChinaRegionCascader from '../components/ChinaRegionCascader';
|
||
import OssUpload from '../components/OssUpload';
|
||
|
||
const CREATE_STEPS = [
|
||
{ title: '基本信息' },
|
||
{ title: '照片上传' },
|
||
{ title: '结算资质' },
|
||
];
|
||
|
||
type StoreRow = {
|
||
id: string;
|
||
name: string;
|
||
phone: string;
|
||
status: string;
|
||
auditStatus?: string;
|
||
rejectReason?: string | null;
|
||
cityName: string;
|
||
district: string;
|
||
address: string;
|
||
intro: string | null;
|
||
coverUrl: string | null;
|
||
createdAt: string;
|
||
cityRef?: { name: string; code: string };
|
||
partner?: { companyName: string };
|
||
account?: { phone: string; name: string; status: string };
|
||
};
|
||
|
||
type PartnerOption = { id: string; companyName: string };
|
||
type CityOption = {
|
||
id: string;
|
||
name: string;
|
||
code: string;
|
||
partnerBindings?: Array<{ partnerAccountId: string; partnerCompanyName?: string }>;
|
||
};
|
||
|
||
export default function StoresPage() {
|
||
const navigate = useNavigate();
|
||
const [form] = Form.useForm();
|
||
const [editForm] = Form.useForm();
|
||
const [createForm] = Form.useForm<StoreCreateForm>();
|
||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<StoreRow>(
|
||
'/admin/stores',
|
||
() => {
|
||
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;
|
||
},
|
||
[filters],
|
||
);
|
||
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('');
|
||
const [partners, setPartners] = useState<PartnerOption[]>([]);
|
||
const [cities, setCities] = useState<CityOption[]>([]);
|
||
const [optionsLoading, setOptionsLoading] = useState(false);
|
||
|
||
const selectedPartnerId = Form.useWatch('partnerAccountId', createForm);
|
||
const selectedRegionCodes = Form.useWatch('regionCodes', createForm);
|
||
const selectedCityId = Form.useWatch('cityId', createForm);
|
||
|
||
function bindRegionSelection(codes: string[], partnerAccountId?: string) {
|
||
const binding = resolveRegionBinding(codes, cities, partnerAccountId ?? selectedPartnerId);
|
||
if (!binding) {
|
||
createForm.setFieldsValue({ regionCodes: codes, cityId: undefined });
|
||
return;
|
||
}
|
||
createForm.setFieldsValue({
|
||
regionCodes: codes,
|
||
province: binding.region.province,
|
||
city: binding.region.city,
|
||
district: binding.region.district,
|
||
districtCode: binding.region.districtCode,
|
||
cityId: binding.cityId,
|
||
});
|
||
}
|
||
|
||
const regionBindingHint = useMemo(() => {
|
||
if (!selectedRegionCodes?.length) return null;
|
||
const binding = resolveRegionBinding(selectedRegionCodes, cities, selectedPartnerId);
|
||
if (!binding) return null;
|
||
if (binding.cityId && binding.matchedCity) {
|
||
return `已匹配开城城市:${binding.matchedCity.name}(区划 ${binding.cityCode},区县 ${binding.region.districtCode})`;
|
||
}
|
||
return `区划 ${binding.cityCode} 暂未开城,请先在「开城 → 城市」添加`;
|
||
}, [selectedRegionCodes, cities, selectedPartnerId]);
|
||
|
||
async function loadOptions() {
|
||
setOptionsLoading(true);
|
||
try {
|
||
const qs = `pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`;
|
||
const [p, c] = await Promise.all([
|
||
request<Paginated<PartnerOption>>(`/admin/partners?${qs}`),
|
||
request<Paginated<CityOption>>(`/admin/cities?${qs}`),
|
||
]);
|
||
setPartners(p.items);
|
||
setCities(c.items);
|
||
if (!p.items.length) message.warning('暂无开城合伙人,请先在「开城 → 城市」详情中创建合伙人');
|
||
if (!c.items.length) message.warning('暂无开城城市,请先在「开城 → 城市」中创建');
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载合伙人/城市失败');
|
||
} finally {
|
||
setOptionsLoading(false);
|
||
}
|
||
}
|
||
|
||
function closeCreateModal() {
|
||
setCreateOpen(false);
|
||
setCreateStep(0);
|
||
setCreateError('');
|
||
createForm.resetFields();
|
||
}
|
||
|
||
function openCreateModal() {
|
||
void loadOptions();
|
||
createForm.setFieldsValue({
|
||
envPhotoUrls: ['', '', ''],
|
||
settlementRate: 60,
|
||
});
|
||
setCreateStep(0);
|
||
setCreateError('');
|
||
setCreateOpen(true);
|
||
}
|
||
|
||
async function handleCreateNext() {
|
||
const values = createForm.getFieldsValue();
|
||
if (createStep === 0) {
|
||
const msg = validateStoreCreateStep1(values);
|
||
if (msg) {
|
||
setCreateError(msg);
|
||
return;
|
||
}
|
||
try {
|
||
await createForm.validateFields(['partnerAccountId', 'regionCodes', 'cityId', 'name', 'phone', 'address']);
|
||
} catch {
|
||
return;
|
||
}
|
||
}
|
||
setCreateError('');
|
||
setCreateStep((s) => s + 1);
|
||
}
|
||
|
||
async function handleCreateSubmit() {
|
||
try {
|
||
const values = await createForm.validateFields();
|
||
const step3Msg = validateStoreCreateStep3(values);
|
||
if (step3Msg) {
|
||
setCreateError(step3Msg);
|
||
return;
|
||
}
|
||
|
||
const envPhotoUrls = (values.envPhotoUrls ?? []).map((u: string) => u?.trim()).filter(Boolean) as string[];
|
||
await request('/admin/stores', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
partnerAccountId: values.partnerAccountId,
|
||
cityId: values.cityId,
|
||
province: values.province,
|
||
city: values.city,
|
||
name: values.name.trim(),
|
||
phone: values.phone.trim(),
|
||
district: values.district.trim(),
|
||
address: values.address.trim(),
|
||
intro: values.intro?.trim() || undefined,
|
||
coverUrl: values.coverUrl?.trim() || undefined,
|
||
envPhotoUrls: envPhotoUrls.length ? envPhotoUrls : undefined,
|
||
contractUrl: values.contractUrl?.trim() || undefined,
|
||
bankAccountName: values.bankAccountName.trim(),
|
||
bankAccountNo: values.bankAccountNo.replace(/\s/g, ''),
|
||
bankBranch: values.bankBranch.trim(),
|
||
settlementRate: Number(values.settlementRate ?? 60) / 100,
|
||
}),
|
||
});
|
||
message.success('门店已创建');
|
||
closeCreateModal();
|
||
void reload();
|
||
} catch (e) {
|
||
if (e && typeof e === 'object' && 'errorFields' in e) {
|
||
const fields = e as { errorFields?: Array<{ name: string[] }> };
|
||
const first = fields.errorFields?.[0]?.name?.[0];
|
||
if (first === 'partnerAccountId' || first === 'cityId' || first === 'regionCodes' || first === 'name' || first === 'phone') {
|
||
setCreateStep(0);
|
||
}
|
||
return;
|
||
}
|
||
message.error(e instanceof Error ? e.message : '创建失败');
|
||
}
|
||
}
|
||
|
||
const columns: ColumnsType<StoreRow> = [
|
||
{
|
||
title: '封面', dataIndex: 'coverUrl', width: 72,
|
||
render: (url) => url ? <Image src={url} width={48} height={48} style={{ objectFit: 'cover', borderRadius: 4 }} /> : '—',
|
||
},
|
||
{ title: '门店名', dataIndex: 'name', width: 140 },
|
||
{ title: '城市', dataIndex: 'cityName', width: 80 },
|
||
{ title: '电话', dataIndex: 'phone', width: 120 },
|
||
{
|
||
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 || '—' },
|
||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||
{
|
||
title: '操作', width: 80,
|
||
render: (_, row) => (
|
||
<Button type="link" size="small" onClick={async () => {
|
||
const d = await request<Record<string, unknown>>(`/admin/stores/${row.id}`);
|
||
setDetail(d);
|
||
editForm.setFieldsValue({
|
||
name: d.name,
|
||
phone: d.phone,
|
||
intro: d.intro,
|
||
coverUrl: d.coverUrl,
|
||
address: d.address,
|
||
district: d.district,
|
||
settlementRate: d.settlementRate != null ? Number(d.settlementRate) * 100 : 60,
|
||
});
|
||
setDrawerOpen(true);
|
||
}}>详情</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<Space direction="vertical" size={0}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>门店</Typography.Title>
|
||
<Typography.Text type="secondary">共 {data?.total ?? 0} 家门店(含合伙人录入)</Typography.Text>
|
||
</Space>
|
||
<Button type="primary" onClick={openCreateModal}>新建门店</Button>
|
||
</Space>
|
||
<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="营业状态">
|
||
<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>
|
||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1200 }}
|
||
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 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) => {
|
||
await request(`/admin/stores/${detail.id}/status`, { method: 'PUT', body: JSON.stringify({ status }) });
|
||
message.success('状态已更新');
|
||
setDetail({ ...detail, status });
|
||
void reload();
|
||
}} />
|
||
<Button type="primary" onClick={async () => {
|
||
const v = await editForm.validateFields();
|
||
const payload = {
|
||
...v,
|
||
settlementRate: v.settlementRate != null ? Number(v.settlementRate) / 100 : undefined,
|
||
};
|
||
await request(`/admin/stores/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
|
||
message.success('已保存');
|
||
setDetail({ ...detail, ...v });
|
||
void reload();
|
||
}}>保存</Button>
|
||
</Space>
|
||
)}>
|
||
{detail && (
|
||
<>
|
||
<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%'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="核销数">{String(detail.redeemCount ?? 0)}</Descriptions.Item>
|
||
<Descriptions.Item label="操作">
|
||
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => navigate(`/logs/stores?storeId=${detail.id}`)}>
|
||
查看商户日志
|
||
</Button>
|
||
</Descriptions.Item>
|
||
{detail.coverUrl ? (
|
||
<Descriptions.Item label="封面">
|
||
<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>
|
||
<Form.Item name="phone" label="电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="coverUrl" label="封面图">
|
||
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
||
</Form.Item>
|
||
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} /></Form.Item>
|
||
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
||
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
|
||
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true }]}>
|
||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
||
</Form.Item>
|
||
</Form>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
<Modal
|
||
title="新建门店"
|
||
open={createOpen}
|
||
width={640}
|
||
onCancel={closeCreateModal}
|
||
destroyOnClose
|
||
footer={(
|
||
<Space>
|
||
{createStep > 0 && <Button onClick={() => { setCreateError(''); setCreateStep((s) => s - 1); }}>上一步</Button>}
|
||
{createStep < 2 ? (
|
||
<Button type="primary" onClick={() => void handleCreateNext()}>下一步</Button>
|
||
) : (
|
||
<Button type="primary" onClick={() => void handleCreateSubmit()}>提交</Button>
|
||
)}
|
||
</Space>
|
||
)}
|
||
>
|
||
<Steps current={createStep} items={CREATE_STEPS} style={{ marginBottom: 24 }} />
|
||
{createError && (
|
||
<Alert type="error" message={createError} showIcon style={{ marginBottom: 16 }} />
|
||
)}
|
||
<Form form={createForm} layout="vertical" preserve>
|
||
<div style={{ display: createStep === 0 ? 'block' : 'none' }}>
|
||
<Form.Item name="partnerAccountId" label="开城合伙人" rules={[{ required: true, message: '请选择开城合伙人' }]}>
|
||
<Select
|
||
showSearch
|
||
loading={optionsLoading}
|
||
optionFilterProp="label"
|
||
placeholder={optionsLoading ? '加载中…' : '请选择开城合伙人'}
|
||
options={partners.map((p) => ({ value: p.id, label: p.companyName }))}
|
||
onChange={(partnerAccountId) => {
|
||
const codes = createForm.getFieldValue('regionCodes') as string[] | undefined;
|
||
if (codes?.length === 3) bindRegionSelection(codes, partnerAccountId);
|
||
else createForm.setFieldValue('cityId', undefined);
|
||
}}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="regionCodes"
|
||
label="所在地区"
|
||
rules={[{ required: true, message: '请选择省 / 市 / 区县' }]}
|
||
extra={regionBindingHint ? (
|
||
<Typography.Text type={selectedCityId ? 'secondary' : 'warning'}>
|
||
{regionBindingHint}
|
||
</Typography.Text>
|
||
) : null}
|
||
>
|
||
<ChinaRegionCascader onChange={(codes) => bindRegionSelection(codes)} />
|
||
</Form.Item>
|
||
<Form.Item name="cityId" hidden rules={[{ required: true, message: '请选择所在地区以匹配开城城市' }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="province" hidden><Input /></Form.Item>
|
||
<Form.Item name="city" hidden><Input /></Form.Item>
|
||
<Form.Item name="district" hidden><Input /></Form.Item>
|
||
<Form.Item name="districtCode" hidden><Input /></Form.Item>
|
||
<Form.Item name="name" label="门店名称" rules={[{ required: true, message: '请填写门店名称' }]}>
|
||
<Input placeholder="请输入门店名称" />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="门店手机号(登录账号)" rules={[{ required: true, message: '请填写门店手机号' }]}>
|
||
<Input placeholder="11位手机号" />
|
||
</Form.Item>
|
||
<Form.Item name="address" label="详细地址" rules={[{ required: true, message: '请填写详细地址' }]}>
|
||
<Input.TextArea rows={2} placeholder="请输入详细门牌号" />
|
||
</Form.Item>
|
||
<Form.Item name="intro" label="门店简介">
|
||
<Input.TextArea rows={3} placeholder="选填,10~500字" showCount maxLength={500} />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: createStep === 1 ? 'block' : 'none' }}>
|
||
<Typography.Paragraph type="secondary">
|
||
preV1 照片上传为选填,可直接下一步(与合伙人端一致)。
|
||
</Typography.Paragraph>
|
||
<Form.Item name="coverUrl" label="门头照">
|
||
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
||
</Form.Item>
|
||
<Typography.Text strong>环境照片</Typography.Text>
|
||
<Form.List name="envPhotoUrls">
|
||
{(fields) => (
|
||
<div style={{ marginTop: 8 }}>
|
||
{fields.map((field, index) => (
|
||
<Form.Item key={field.key} name={field.name} label={`环境图 ${index + 1}`}>
|
||
<OssUpload bizType="STORE_ENV" mediaType="IMAGE" />
|
||
</Form.Item>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Form.List>
|
||
<Form.Item name="contractUrl" label="签约合同">
|
||
<OssUpload bizType="STORE_CONTRACT" mediaType="FILE" accept="image/*,.pdf" />
|
||
</Form.Item>
|
||
</div>
|
||
<div style={{ display: createStep === 2 ? 'block' : 'none' }}>
|
||
<Form.Item name="bankAccountName" label="户主姓名" rules={[{ required: true, message: '请填写户主姓名' }]}>
|
||
<Input placeholder="银行卡实名姓名" />
|
||
</Form.Item>
|
||
<Form.Item name="bankAccountNo" label="银行卡号" rules={[{ required: true, message: '请填写银行卡号' }]}>
|
||
<Input placeholder="16~19位银行卡号" />
|
||
</Form.Item>
|
||
<Form.Item name="bankBranch" label="开户支行" rules={[{ required: true, message: '请填写开户支行' }]}>
|
||
<Input placeholder="例如:中国工商银行洛阳分行" />
|
||
</Form.Item>
|
||
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true, message: '请填写结算比例' }]}>
|
||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
||
</Form.Item>
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="请确保银行卡信息准确,以免影响门店餐费结算。"
|
||
/>
|
||
</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>
|
||
);
|
||
}
|