Files
dukang/apps/admin-web/src/pages/StoreAccountsPage.tsx
T
2026-07-12 12:24:34 +08:00

256 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import {
Button, Descriptions, Drawer, Form, Input, Modal, Select, Space, Table, Tag, Typography, message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
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;
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 };
export default function StoreAccountsPage() {
const [form] = Form.useForm();
const [createForm] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
'/admin/store-accounts',
() => {
const qs = new URLSearchParams();
if (filters.phone) qs.set('phone', filters.phone);
if (filters.status) qs.set('status', filters.status);
return qs;
},
[filters],
);
const [detail, setDetail] = useState<Row | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [stores, setStores] = useState<StoreOption[]>([]);
async function loadStores() {
const res = await request<Paginated<StoreOption>>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
setStores(res.items);
}
const columns: ColumnsType<Row> = [
{ title: '姓名', dataIndex: 'name', width: 100 },
{ title: '手机', dataIndex: 'phone', width: 120 },
{
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,
render: (_, row) => (
<Button
type="link"
size="small"
onClick={async () => {
setDetail(await request(`/admin/store-accounts/${row.id}`));
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">
主账号可绑定多家门店;收款信息挂在主账号;「新建账户」用于补录无主账号门店
</Typography.Text>
</Space>
<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.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 }))}
/>
</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: 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.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();
}}
>
<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 }))}
/>
</Form.Item>
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="phone" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
</Form>
</Modal>
</div>
);
}