删除门店的多银行账户列表
This commit is contained in:
@@ -1,178 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { StoreBankAccountDto } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
|
||||
type Props = { storeId: string };
|
||||
|
||||
type FormValues = { bankAccountName: string; bankAccountNo: string; bankBranch?: string };
|
||||
|
||||
export default function StoreBankAccountsPanel({ storeId }: Props) {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [items, setItems] = useState<StoreBankAccountDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request<StoreBankAccountDto[]>(`/admin/stores/${storeId}/bank-accounts`);
|
||||
setItems(res ?? []);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [storeId]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
function openCreate() {
|
||||
setEditingId(null);
|
||||
form.resetFields();
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(item: StoreBankAccountDto) {
|
||||
setEditingId(item.id);
|
||||
form.setFieldsValue({
|
||||
bankAccountName: item.bankAccountName,
|
||||
bankAccountNo: item.bankAccountNo,
|
||||
bankBranch: item.bankBranch ?? '',
|
||||
});
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const v = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
bankAccountName: v.bankAccountName.trim(),
|
||||
bankAccountNo: v.bankAccountNo.replace(/\s+/g, ''),
|
||||
bankBranch: v.bankBranch?.trim() || undefined,
|
||||
});
|
||||
if (editingId) {
|
||||
await request(`/admin/stores/${storeId}/bank-accounts/${editingId}`, { method: 'PUT', body });
|
||||
} else {
|
||||
await request(`/admin/stores/${storeId}/bank-accounts`, { method: 'POST', body });
|
||||
}
|
||||
message.success('已保存');
|
||||
setModalOpen(false);
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function setDefault(id: string) {
|
||||
try {
|
||||
await request(`/admin/stores/${storeId}/bank-accounts/${id}/default`, { method: 'POST' });
|
||||
message.success('已设为默认打款账户');
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '设置失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
try {
|
||||
await request(`/admin/stores/${storeId}/bank-accounts/${id}`, { method: 'DELETE' });
|
||||
message.success('已删除');
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<StoreBankAccountDto> = [
|
||||
{ title: '收款人', dataIndex: 'bankAccountName', width: 140 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccountNo', width: 180 },
|
||||
{ title: '开户行', dataIndex: 'bankBranch', render: (v) => v || '—' },
|
||||
{
|
||||
title: '默认',
|
||||
dataIndex: 'isDefault',
|
||||
width: 80,
|
||||
render: (v) => (v ? <Tag color="green">默认</Tag> : null),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_, row) => (
|
||||
<Space size={0} wrap>
|
||||
<Button type="link" size="small" onClick={() => openEdit(row)}>编辑</Button>
|
||||
{!row.isDefault ? (
|
||||
<Button type="link" size="small" onClick={() => void setDefault(row.id)}>设为默认</Button>
|
||||
) : null}
|
||||
{!row.isDefault ? (
|
||||
<Popconfirm
|
||||
title="确认删除该收款账户?"
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void remove(row.id)}
|
||||
>
|
||||
<Button type="link" size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#888' }}>
|
||||
每门店可维护多个收款账户,标记「默认」的账户用于核销结算与提现打款;切换即时生效、无需重启服务。
|
||||
</span>
|
||||
<Button type="primary" size="small" onClick={openCreate}>新增账户</Button>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
/>
|
||||
<Modal
|
||||
title={editingId ? '编辑收款账户' : '新增收款账户'}
|
||||
open={modalOpen}
|
||||
confirmLoading={submitting}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={() => void submit()}
|
||||
okText="保存"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="bankAccountName"
|
||||
label="收款人"
|
||||
rules={[{ required: true, message: '请填写收款人' }]}
|
||||
>
|
||||
<Input maxLength={64} placeholder="户名 / 收款人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bankAccountNo"
|
||||
label="银行账号"
|
||||
rules={[
|
||||
{ required: true, message: '请填写银行账号' },
|
||||
{ pattern: /^\d{8,32}$/, message: '请填写 8~32 位数字账号' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={32} placeholder="请输入银行账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankBranch" label="开户行名称">
|
||||
<Input maxLength={128} placeholder="如 中国工商银行郑州分行" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/finance/partner-bills', label: '合伙人账单' },
|
||||
{ key: '/finance/winery-bills', label: '酒厂账单' },
|
||||
{ key: '/finance/logistics-bills', label: '物流对账' },
|
||||
{ key: '/finance/bank-accounts', label: '银行账户' },
|
||||
{ key: '/finance/bank-accounts', label: '全部银行账户' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@@ -24,6 +25,7 @@ import { downloadBase64File } from '../lib/exportExcel';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminListHeader } from '../components/AdminListHeader';
|
||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||
|
||||
type CityOption = { id: string; name: string };
|
||||
|
||||
@@ -55,6 +57,7 @@ function otherNumericId(id: string) {
|
||||
}
|
||||
|
||||
export default function BankAccountsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [filterForm] = Form.useForm<{
|
||||
type?: FinanceBankAccountType;
|
||||
cityId?: string;
|
||||
@@ -198,17 +201,42 @@ export default function BankAccountsPage() {
|
||||
<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: 'ownerName',
|
||||
width: 180,
|
||||
render: (name: string, row) => (
|
||||
<AdminPrimaryLink
|
||||
onClick={() => {
|
||||
if (row.type === 'STORE' && row.ownerId) {
|
||||
navigate(`/stores?storeId=${encodeURIComponent(row.ownerId)}&tab=settlement`);
|
||||
return;
|
||||
}
|
||||
if (row.type === 'WINERY') {
|
||||
navigate('/system-settings?group=winery_bank');
|
||||
return;
|
||||
}
|
||||
if (row.type === 'PARTNER' && row.ownerId) {
|
||||
navigate(`/city-partners?partnerId=${encodeURIComponent(row.ownerId)}`);
|
||||
return;
|
||||
}
|
||||
if (row.type === 'LOGISTICS' && row.ownerId) {
|
||||
navigate(`/fulfillment-providers?id=${encodeURIComponent(row.ownerId)}`);
|
||||
return;
|
||||
}
|
||||
if (row.type === 'OTHER') {
|
||||
openEdit(row);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</AdminPrimaryLink>
|
||||
),
|
||||
},
|
||||
{ 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: 'remark', width: 200, render: (v?: string | null) => v || '—' },
|
||||
{
|
||||
title: '操作',
|
||||
@@ -251,8 +279,8 @@ export default function BankAccountsPage() {
|
||||
<div>
|
||||
{settingsModal}
|
||||
<AdminListHeader
|
||||
title="银行账户"
|
||||
description="汇总门店、酒厂、合伙人、物流的有效收款账户;可登记不挂门店的其他账户。打款仍以各业务源账户为准。"
|
||||
title="全部银行账户"
|
||||
description="汇总门店结算资质、酒厂、合伙人、物流的有效银行账户;可登记不挂门店的其他账户。打款仍以各业务源账户为准。"
|
||||
settings={settingsButton}
|
||||
actions={
|
||||
<Button type="primary" onClick={openCreate}>
|
||||
@@ -287,7 +315,7 @@ export default function BankAccountsPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="keyword" label="查找">
|
||||
<Input allowClear style={{ width: 220 }} placeholder="户名 / 账号 / 开户行 / 归属 / 备注" />
|
||||
<Input allowClear style={{ width: 220 }} placeholder="户名 / 账号 / 开户银行 / 归属 / 备注" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
@@ -361,7 +389,7 @@ export default function BankAccountsPage() {
|
||||
>
|
||||
<Input maxLength={32} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankBranch" label="开户行">
|
||||
<Form.Item name="bankBranch" label="开户银行">
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
@@ -131,6 +131,8 @@ export default function CityPartnersPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialCityId = searchParams.get('cityId')?.trim() || '';
|
||||
const initialPartnerId = searchParams.get('partnerId')?.trim() || '';
|
||||
const deepLinkPartnerOpenedRef = useRef(false);
|
||||
const [filterForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
@@ -213,6 +215,14 @@ export default function CityPartnersPage() {
|
||||
return d;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialPartnerId || deepLinkPartnerOpenedRef.current) return;
|
||||
deepLinkPartnerOpenedRef.current = true;
|
||||
void openPartner(initialPartnerId).catch((e) => {
|
||||
message.error(e instanceof Error ? e.message : '合伙人详情加载失败');
|
||||
});
|
||||
}, [initialPartnerId]);
|
||||
|
||||
async function savePartner() {
|
||||
if (!detail) return;
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -52,8 +53,11 @@ const SIGN_OPTIONS = [
|
||||
const DEFAULT_XFX_API_URL = 'https://beta.51xiaoju.cn/app/api/interface.do';
|
||||
|
||||
export default function FulfillmentProvidersPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialId = searchParams.get('id')?.trim() || '';
|
||||
const deepLinkOpenedRef = useRef(false);
|
||||
const [rows, setRows] = useState<FulfillmentProviderDto[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editRow, setEditRow] = useState<FulfillmentProviderDto | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
@@ -132,6 +136,13 @@ export default function FulfillmentProvidersPage() {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialId || deepLinkOpenedRef.current || loading) return;
|
||||
const row = rows.find((r) => String(r.id) === initialId);
|
||||
deepLinkOpenedRef.current = true;
|
||||
if (row) openEdit(row);
|
||||
}, [initialId, loading, rows]);
|
||||
|
||||
async function submit() {
|
||||
const v = await form.validateFields();
|
||||
const payload: Record<string, unknown> = {
|
||||
|
||||
@@ -51,7 +51,6 @@ import TencentLocPickerModal from '../components/TencentLocPickerModal';
|
||||
import AdminStorePackagesSection, {
|
||||
type AdminStorePackagesHandle,
|
||||
} from '../components/AdminStorePackagesSection';
|
||||
import StoreBankAccountsPanel from '../components/StoreBankAccountsPanel';
|
||||
import StorePackageAuditPanel, {
|
||||
auditStorePackageRequest,
|
||||
} from '../components/StorePackageAuditPanel';
|
||||
@@ -257,6 +256,7 @@ export default function StoresPage() {
|
||||
const initialPartnerId = searchParams.get('partnerId') ?? '';
|
||||
const initialAuditStatus = searchParams.get('auditStatus') ?? '';
|
||||
const initialStoreId = searchParams.get('storeId') ?? '';
|
||||
const initialTab = searchParams.get('tab') ?? '';
|
||||
const [form] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm<StoreCreateForm>();
|
||||
@@ -477,13 +477,13 @@ export default function StoresPage() {
|
||||
const row = data?.items?.find((s) => String(s.id) === initialStoreId);
|
||||
if (row) {
|
||||
deepLinkStoreOpenedRef.current = true;
|
||||
void openStoreDetail(row);
|
||||
void openStoreDetail(row, { tab: initialTab || undefined });
|
||||
} else if (data && (data.items?.length ?? 0) >= 0) {
|
||||
// 列表无该店时仍尝试直拉详情
|
||||
deepLinkStoreOpenedRef.current = true;
|
||||
void openStoreDetail({ id: initialStoreId } as StoreRow);
|
||||
void openStoreDetail({ id: initialStoreId } as StoreRow, { tab: initialTab || undefined });
|
||||
}
|
||||
}, [data, initialStoreId, loading]);
|
||||
}, [data, initialStoreId, initialTab, loading]);
|
||||
|
||||
async function saveStoreDetail() {
|
||||
if (!detail) return;
|
||||
@@ -1277,14 +1277,14 @@ export default function StoresPage() {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bankAccountNo"
|
||||
label="银行卡号"
|
||||
label="银行账号"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
const v = String(value || '').trim();
|
||||
if (!v) return;
|
||||
if (!/^\d{16,19}$/.test(v)) {
|
||||
throw new Error('银行卡号须为 16–19 位数字');
|
||||
throw new Error('银行账号须为 16–19 位数字');
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1298,14 +1298,6 @@ export default function StoresPage() {
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bank-accounts',
|
||||
label: '收款账户',
|
||||
forceRender: true,
|
||||
children: (
|
||||
<StoreBankAccountsPanel storeId={String(detail.id)} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'media',
|
||||
label: '审核材料',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -159,6 +159,9 @@ function renderField(
|
||||
|
||||
export default function SystemSettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const groupKey = searchParams.get('group')?.trim() || '';
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>(() => (groupKey ? [groupKey] : []));
|
||||
const [form] = Form.useForm<Record<string, string>>();
|
||||
const [meta, setMeta] = useState<SystemConfigFormResponse | null>(null);
|
||||
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||
@@ -327,13 +330,30 @@ export default function SystemSettingsPage() {
|
||||
|
||||
return {
|
||||
key: group.key,
|
||||
label: group.label,
|
||||
label: <span id={`sys-setting-group-${group.key}`}>{group.label}</span>,
|
||||
forceRender: true,
|
||||
children,
|
||||
};
|
||||
});
|
||||
}, [meta, mockSmsEnabled, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupKey) {
|
||||
setActiveKeys((prev) => (prev.includes(groupKey) ? prev : [...prev, groupKey]));
|
||||
}
|
||||
}, [groupKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupKey || loading) return;
|
||||
const timer = window.setTimeout(() => {
|
||||
document.getElementById(`sys-setting-group-${groupKey}`)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
}, 80);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [groupKey, loading]);
|
||||
|
||||
async function onSave() {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
@@ -432,7 +452,11 @@ export default function SystemSettingsPage() {
|
||||
|
||||
<Card loading={loading}>
|
||||
<Form form={form} layout="vertical" onValuesChange={() => setDirty(true)}>
|
||||
<Collapse defaultActiveKey={[]} items={collapseItems} />
|
||||
<Collapse
|
||||
activeKey={activeKeys}
|
||||
onChange={(keys) => setActiveKeys(Array.isArray(keys) ? keys : [keys])}
|
||||
items={collapseItems}
|
||||
/>
|
||||
</Form>
|
||||
{meta?.updatedAt ? (
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 16 }}>
|
||||
|
||||
Reference in New Issue
Block a user