门店账户多账号
This commit is contained in:
@@ -7,9 +7,22 @@ import { request, type Paginated } from '../lib/api';
|
||||
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
|
||||
type StoreBrief = { id: string; name: string; status: string; cityName?: string };
|
||||
|
||||
type Row = {
|
||||
id: string; phone: string; name: string; status: string; createdAt: string;
|
||||
store?: { id: string; name: string; status: string; cityName: string };
|
||||
id: string;
|
||||
phone: string;
|
||||
name: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
storeCount?: number;
|
||||
staffCount?: number;
|
||||
bankAccountName?: string | null;
|
||||
bankAccountNo?: string | null;
|
||||
bankBranch?: string | null;
|
||||
store?: StoreBrief | null;
|
||||
stores?: StoreBrief[];
|
||||
staff?: Array<{ id: string; name: string; phone: string; status: string; storeIds?: string[] }>;
|
||||
};
|
||||
|
||||
type StoreOption = { id: string; name: string };
|
||||
@@ -41,17 +54,53 @@ export default function StoreAccountsPage() {
|
||||
const columns: ColumnsType<Row> = [
|
||||
{ title: '姓名', dataIndex: 'name', width: 100 },
|
||||
{ title: '手机', dataIndex: 'phone', width: 120 },
|
||||
{ title: '门店', dataIndex: ['store', 'name'], width: 140 },
|
||||
{ title: '门店状态', dataIndex: ['store', 'status'], width: 90, render: (s) => s ? <Tag>{STORE_STATUS_LABELS[s] || s}</Tag> : '—' },
|
||||
{ title: '账号状态', dataIndex: 'status', width: 90, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
|
||||
{
|
||||
title: '绑定门店',
|
||||
width: 180,
|
||||
render: (_, row) =>
|
||||
row.stores?.length
|
||||
? row.stores.map((s) => s.name).join('、')
|
||||
: row.store?.name ?? '—',
|
||||
},
|
||||
{
|
||||
title: '门店数',
|
||||
dataIndex: 'storeCount',
|
||||
width: 70,
|
||||
render: (n, row) => n ?? row.stores?.length ?? 0,
|
||||
},
|
||||
{
|
||||
title: '子账号',
|
||||
dataIndex: 'staffCount',
|
||||
width: 70,
|
||||
render: (n) => n ?? 0,
|
||||
},
|
||||
{
|
||||
title: '收款户名',
|
||||
dataIndex: 'bankAccountName',
|
||||
width: 120,
|
||||
render: (v) => v || '—',
|
||||
},
|
||||
{
|
||||
title: '账号状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag>,
|
||||
},
|
||||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
title: '操作', width: 80,
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, row) => (
|
||||
<Button type="link" size="small" onClick={async () => {
|
||||
setDetail(await request(`/admin/store-accounts/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}>详情</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
setDetail(await request(`/admin/store-accounts/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -61,48 +110,141 @@ export default function StoreAccountsPage() {
|
||||
<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">新建门店时自动开通主账号;「新建账户」仅用于补录历史无账号门店</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
主账号可绑定多家门店;收款信息挂在主账号;「新建账户」用于补录无主账号门店
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Button type="primary" onClick={() => { void loadStores(); setCreateOpen(true); }}>新建账户</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
void loadStores();
|
||||
setCreateOpen(true);
|
||||
}}
|
||||
>
|
||||
新建账户
|
||||
</Button>
|
||||
</Space>
|
||||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="inline"
|
||||
style={{ marginBottom: 16 }}
|
||||
onFinish={(v) => {
|
||||
setFilters(v);
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="phone" label="手机"><Input allowClear /></Form.Item>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select allowClear style={{ width: 100 }} options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
<Select
|
||||
allowClear
|
||||
style={{ width: 100 }}
|
||||
options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
||||
</Form>
|
||||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 900 }}
|
||||
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||||
<Drawer title="门店账户" width={480} open={drawerOpen} onClose={() => setDrawerOpen(false)}
|
||||
extra={detail && (
|
||||
<Select defaultValue={detail.status} style={{ width: 100 }}
|
||||
options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
onChange={async (status) => {
|
||||
await request(`/admin/store-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify({ status }) });
|
||||
message.success('已更新');
|
||||
void reload();
|
||||
}} />
|
||||
)}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
className="admin-table-nowrap"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Drawer
|
||||
title="门店主账号"
|
||||
width={520}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
extra={
|
||||
detail && (
|
||||
<Select
|
||||
defaultValue={detail.status}
|
||||
style={{ width: 100 }}
|
||||
options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
onChange={async (status) => {
|
||||
await request(`/admin/store-accounts/${detail.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
message.success('已更新');
|
||||
void reload();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{detail && (
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="姓名">{detail.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机">{detail.phone}</Descriptions.Item>
|
||||
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small">
|
||||
<Descriptions.Item label="姓名">{detail.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机">{detail.phone}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款户名">{detail.bankAccountName || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="收款账号">{detail.bankAccountNo || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="开户行">{detail.bankBranch || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="绑定门店">
|
||||
{(detail.stores ?? []).map((s) => (
|
||||
<Tag key={s.id}>
|
||||
{s.name}
|
||||
{s.status ? `(${STORE_STATUS_LABELS[s.status] || s.status})` : ''}
|
||||
</Tag>
|
||||
))}
|
||||
{!detail.stores?.length ? '—' : null}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.staff?.length ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 24 }}>子账号</Typography.Title>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
dataSource={detail.staff}
|
||||
columns={[
|
||||
{ title: '姓名', dataIndex: 'name' },
|
||||
{ title: '手机', dataIndex: 'phone' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
<Modal title="新建门店账户" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={async () => {
|
||||
const v = await createForm.validateFields();
|
||||
await request('/admin/store-accounts', { method: 'POST', body: JSON.stringify(v) });
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
createForm.resetFields();
|
||||
void reload();
|
||||
}}>
|
||||
<Modal
|
||||
title="新建门店账户"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={async () => {
|
||||
const v = await createForm.validateFields();
|
||||
await request('/admin/store-accounts', { method: 'POST', body: JSON.stringify(v) });
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
createForm.resetFields();
|
||||
void reload();
|
||||
}}
|
||||
>
|
||||
<Form form={createForm} layout="vertical">
|
||||
<Form.Item name="storeId" label="门店" rules={[{ required: true }]}>
|
||||
<Select showSearch optionFilterProp="label" options={stores.map((s) => ({ value: s.id, label: s.name }))} />
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={stores.map((s) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="phone" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user