v4.0.18版本提交
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
FINANCE_BANK_ACCOUNT_TYPE_LABELS,
|
||||
FINANCE_BANK_ACCOUNT_TYPES,
|
||||
type FinanceBankAccountDto,
|
||||
type FinanceBankAccountType,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { ADMIN_OPTIONS_PAGE_SIZE } from '../lib/constants';
|
||||
import { downloadBase64File } from '../lib/exportExcel';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminListHeader } from '../components/AdminListHeader';
|
||||
|
||||
type CityOption = { id: string; name: string };
|
||||
|
||||
type ExportResult = {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
contentBase64: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type OtherForm = {
|
||||
name?: string;
|
||||
bankAccountName: string;
|
||||
bankAccountNo: string;
|
||||
bankBranch?: string;
|
||||
remark?: string;
|
||||
};
|
||||
|
||||
const TYPE_COLORS: Record<FinanceBankAccountType, string> = {
|
||||
STORE: 'blue',
|
||||
WINERY: 'gold',
|
||||
PARTNER: 'purple',
|
||||
LOGISTICS: 'cyan',
|
||||
OTHER: 'default',
|
||||
};
|
||||
|
||||
function otherNumericId(id: string) {
|
||||
return id.startsWith('OTHER:') ? id.slice('OTHER:'.length) : id;
|
||||
}
|
||||
|
||||
export default function BankAccountsPage() {
|
||||
const [filterForm] = Form.useForm<{
|
||||
type?: FinanceBankAccountType;
|
||||
cityId?: string;
|
||||
keyword?: string;
|
||||
}>();
|
||||
const [otherForm] = Form.useForm<OtherForm>();
|
||||
const [remarkForm] = Form.useForm<{ remark?: string }>();
|
||||
const [filters, setFilters] = useState({ type: '', cityId: '', keyword: '' });
|
||||
const [cities, setCities] = useState<CityOption[]>([]);
|
||||
const [otherOpen, setOtherOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<FinanceBankAccountDto | null>(null);
|
||||
const [remarkTarget, setRemarkTarget] = useState<FinanceBankAccountDto | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [exporting, setExporting] = useState<'xlsx' | 'pdf' | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
.then((res) => setCities(res.items ?? []))
|
||||
.catch(() => setCities([]));
|
||||
}, []);
|
||||
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<FinanceBankAccountDto>(
|
||||
'/admin/finance/bank-accounts',
|
||||
() => {
|
||||
const qs = new URLSearchParams();
|
||||
if (filters.type) qs.set('type', filters.type);
|
||||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||||
if (filters.keyword) qs.set('keyword', filters.keyword);
|
||||
return qs;
|
||||
},
|
||||
[filters.type, filters.cityId, filters.keyword],
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
setEditing(null);
|
||||
otherForm.resetFields();
|
||||
setOtherOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(row: FinanceBankAccountDto) {
|
||||
setEditing(row);
|
||||
otherForm.setFieldsValue({
|
||||
name: row.ownerName === row.bankAccountName ? undefined : row.ownerName,
|
||||
bankAccountName: row.bankAccountName,
|
||||
bankAccountNo: row.bankAccountNo,
|
||||
bankBranch: row.bankBranch ?? undefined,
|
||||
remark: row.remark ?? undefined,
|
||||
});
|
||||
setOtherOpen(true);
|
||||
}
|
||||
|
||||
function openRemark(row: FinanceBankAccountDto) {
|
||||
setRemarkTarget(row);
|
||||
remarkForm.setFieldsValue({ remark: row.remark ?? undefined });
|
||||
}
|
||||
|
||||
async function saveOther() {
|
||||
const values = await otherForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await request(`/admin/finance/bank-accounts/other/${otherNumericId(editing.id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('已保存');
|
||||
} else {
|
||||
await request('/admin/finance/bank-accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('已新增');
|
||||
}
|
||||
setOtherOpen(false);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRemark() {
|
||||
if (!remarkTarget) return;
|
||||
const values = await remarkForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
await request(`/admin/finance/bank-accounts/${encodeURIComponent(remarkTarget.id)}/remark`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ remark: values.remark ?? '' }),
|
||||
});
|
||||
message.success('备注已保存');
|
||||
setRemarkTarget(null);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOther(row: FinanceBankAccountDto) {
|
||||
try {
|
||||
await request(`/admin/finance/bank-accounts/other/${otherNumericId(row.id)}`, { method: 'DELETE' });
|
||||
message.success('已删除');
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function exportFile(format: 'xlsx' | 'pdf') {
|
||||
setExporting(format);
|
||||
try {
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('format', format);
|
||||
if (filters.type) qs.set('type', filters.type);
|
||||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||||
if (filters.keyword) qs.set('keyword', filters.keyword);
|
||||
const result = await request<ExportResult>(`/admin/finance/bank-accounts/export?${qs}`);
|
||||
downloadBase64File(result.contentBase64, result.filename, result.mimeType);
|
||||
message.success(`已导出 ${result.count} 条`);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '导出失败');
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions = useMemo(
|
||||
() => FINANCE_BANK_ACCOUNT_TYPES.map((value) => ({ value, label: FINANCE_BANK_ACCOUNT_TYPE_LABELS[value] })),
|
||||
[],
|
||||
);
|
||||
|
||||
const baseColumns: ColumnsType<FinanceBankAccountDto> = [
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 88,
|
||||
render: (type: FinanceBankAccountType) => (
|
||||
<Tag color={TYPE_COLORS[type]}>{FINANCE_BANK_ACCOUNT_TYPE_LABELS[type]}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '归属', dataIndex: 'ownerName', width: 180 },
|
||||
{ title: '城市', dataIndex: 'cityName', width: 100, render: (v?: string | null) => v || '—' },
|
||||
{ title: '户名', dataIndex: 'bankAccountName', width: 140 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccountNo', width: 180 },
|
||||
{ title: '开户行', dataIndex: 'bankBranch', width: 180, render: (v?: string | null) => v || '—' },
|
||||
{
|
||||
title: '默认',
|
||||
dataIndex: 'isDefault',
|
||||
width: 72,
|
||||
render: (v: boolean | undefined, row) => (row.type === 'STORE' ? (v ? '是' : '否') : '—'),
|
||||
},
|
||||
{ title: '备注', dataIndex: 'remark', width: 200, render: (v?: string | null) => v || '—' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" onClick={() => openRemark(row)}>
|
||||
备注
|
||||
</Button>
|
||||
{row.editable ? (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => openEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确认删除账户「${row.ownerName}」?`}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void removeOther(row)}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-bank-accounts', baseColumns, {
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{settingsModal}
|
||||
<AdminListHeader
|
||||
title="银行账户"
|
||||
description="汇总门店、酒厂、合伙人、物流的有效收款账户;可登记不挂门店的其他账户。打款仍以各业务源账户为准。"
|
||||
settings={settingsButton}
|
||||
actions={
|
||||
<Button type="primary" onClick={openCreate}>
|
||||
新增账户
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={filterForm}
|
||||
layout="inline"
|
||||
style={{ marginBottom: 16 }}
|
||||
onFinish={(v) => {
|
||||
setFilters({
|
||||
type: v.type || '',
|
||||
cityId: v.cityId || '',
|
||||
keyword: v.keyword?.trim() || '',
|
||||
});
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="type" label="类型">
|
||||
<Select allowClear style={{ width: 140 }} options={typeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cityId" label="城市">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 160 }}
|
||||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="keyword" label="查找">
|
||||
<Input allowClear style={{ width: 220 }} placeholder="户名 / 账号 / 开户行 / 归属 / 备注" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
filterForm.resetFields();
|
||||
setFilters({ type: '', cityId: '', keyword: '' });
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={exporting === 'xlsx'} onClick={() => void exportFile('xlsx')}>
|
||||
导出 Excel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={exporting === 'pdf'} onClick={() => void exportFile('pdf')}>
|
||||
导出 PDF
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
className="admin-table-nowrap"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑账户' : '新增账户'}
|
||||
open={otherOpen}
|
||||
onCancel={() => setOtherOpen(false)}
|
||||
onOk={() => void saveOther()}
|
||||
confirmLoading={saving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={otherForm} layout="vertical">
|
||||
<Form.Item name="name" label="归属名称">
|
||||
<Input placeholder="选填,如供应商或内部账户名称" maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccountName" label="户名" rules={[{ required: true, message: '请填写户名' }]}>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bankAccountNo"
|
||||
label="银行账号"
|
||||
rules={[
|
||||
{ required: true, message: '请填写银行账号' },
|
||||
{ pattern: /^\d{8,32}$/, message: '银行账号须为 8~32 位数字' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={32} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankBranch" label="开户行">
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={3} maxLength={256} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="编辑备注"
|
||||
open={!!remarkTarget}
|
||||
onCancel={() => setRemarkTarget(null)}
|
||||
onOk={() => void saveRemark()}
|
||||
confirmLoading={saving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={remarkForm} layout="vertical">
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={4} maxLength={256} placeholder="仅财务目录可见,不改写来源账户" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user