Files
dukang/apps/h5-shop/src/pages/BankAccountsPage.tsx
T
jacy 4bdb09068c feat(store): v4.0.18 门店多收款账户与子账号继承二维码
C 端门店列表拼接省市区县地址;门店多银行账户与默认打款账户;子账号独立 sa_ 关联码及统计维度;同步 v4.0.18 开发文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 15:56:16 +08:00

253 lines
9.0 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 { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { StoreBankAccountDto } from '@dukang/shared-types';
import { request } from '../lib/api';
import { useStoreSession } from '../contexts/StoreSessionContext';
import { useStorePageView } from '../lib/usePageView';
type FormState = {
id?: string;
bankAccountName: string;
bankAccountNo: string;
bankBranch: string;
};
const EMPTY_FORM: FormState = { bankAccountName: '', bankAccountNo: '', bankBranch: '' };
export default function BankAccountsPage() {
useStorePageView('store_bank_accounts_view');
const navigate = useNavigate();
const { store } = useStoreSession();
const isPrimary = !!store?.isPrimary;
const [items, setItems] = useState<StoreBankAccountDto[]>([]);
const [loading, setLoading] = useState(true);
const [msg, setMsg] = useState('');
const [form, setForm] = useState<FormState>(EMPTY_FORM);
const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof FormState, string>>>({});
const [submitting, setSubmitting] = useState(false);
const load = useCallback(async () => {
try {
const res = await request<StoreBankAccountDto[]>('SHOP_H5', '/shop/store/bank-accounts');
setItems(res ?? []);
} catch (e) {
setMsg(e instanceof Error ? e.message : '加载失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
function startCreate() {
setForm(EMPTY_FORM);
setFieldErrors({});
}
function startEdit(item: StoreBankAccountDto) {
setForm({
id: item.id,
bankAccountName: item.bankAccountName,
bankAccountNo: item.bankAccountNo,
bankBranch: item.bankBranch ?? '',
});
setFieldErrors({});
}
async function submit() {
const name = form.bankAccountName.trim();
const no = form.bankAccountNo.replace(/\s+/g, '');
const branch = form.bankBranch.trim();
const next: Partial<Record<keyof FormState, string>> = {};
if (!name) next.bankAccountName = '请填写收款人';
if (!/^\d{8,32}$/.test(no)) next.bankAccountNo = '请填写 832 位数字账号';
setFieldErrors(next);
if (Object.keys(next).length) return;
setSubmitting(true);
setMsg('');
try {
const body = JSON.stringify({ bankAccountName: name, bankAccountNo: no, bankBranch: branch });
if (form.id) {
await request('SHOP_H5', `/shop/store/bank-accounts/${form.id}`, {
method: 'PUT',
body,
});
} else {
await request('SHOP_H5', '/shop/store/bank-accounts', { method: 'POST', body });
}
setForm(EMPTY_FORM);
await load();
} catch (e) {
setMsg(e instanceof Error ? e.message : '保存失败');
} finally {
setSubmitting(false);
}
}
async function setDefault(id: string) {
setMsg('');
try {
await request('SHOP_H5', `/shop/store/bank-accounts/${id}/default`, { method: 'POST' });
await load();
} catch (e) {
setMsg(e instanceof Error ? e.message : '设置失败');
}
}
async function remove(id: string) {
setMsg('');
try {
await request('SHOP_H5', `/shop/store/bank-accounts/${id}`, { method: 'DELETE' });
await load();
} catch (e) {
setMsg(e instanceof Error ? e.message : '删除失败');
}
}
const editing = !!form.id;
return (
<div className="shop-records-page">
<header className="shop-records-header" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button
type="button"
className="shop-withdraw-back"
onClick={() => navigate(-1)}
aria-label="返回"
>
<span className="material-symbols-outlined" style={{ fontSize: 22 }}>arrow_back</span>
</button>
<h1 className="app-page-title" style={{ margin: 0 }}>收款账户</h1>
</header>
<div className="shop-records-main">
<p
className="shop-records-summary-note"
style={{ marginBottom: 12 }}
>
用于总部打款,可维护多个账户;标记「默认」的账户用于提现打款,切换即时生效、无需重启。
</p>
{loading ? (
<p className="shop-records-empty">加载中…</p>
) : items.length === 0 ? (
<p className="shop-records-empty">暂无收款账户{isPrimary ? ',请新增' : ''}</p>
) : (
<div className="shop-records-list">
{items.map((it) => (
<article key={it.id} className="shop-record-card">
<div className="shop-record-card-top">
<div>
<div className="shop-record-order">
<span className="shop-record-time" style={{ margin: 0 }}>收款人</span>
<span>{it.bankAccountName}</span>
{it.isDefault ? (
<span className="shop-record-badge paid">默认</span>
) : null}
</div>
<p className="shop-record-time">账号: {it.bankAccountNo}</p>
{it.bankBranch ? <p className="shop-record-time">开户行: {it.bankBranch}</p> : null}
</div>
</div>
{isPrimary ? (
<div className="shop-record-footer" style={{ display: 'flex', gap: 12 }}>
<button type="button" className="shop-records-chip" onClick={() => startEdit(it)}>
编辑
</button>
{!it.isDefault ? (
<button type="button" className="shop-records-chip" onClick={() => void setDefault(it.id)}>
设为默认
</button>
) : null}
{!it.isDefault ? (
<button type="button" className="shop-records-chip" onClick={() => void remove(it.id)}>
删除
</button>
) : null}
</div>
) : null}
</article>
))}
</div>
)}
{isPrimary ? (
<section className="shop-records-summary" style={{ marginTop: 16 }}>
<h3 className="shop-records-list-title">
{editing ? '编辑账户' : '新增账户'}
</h3>
<div className="shop-mine-info-card">
<label className="shop-record-amount-label">
收款人 <span style={{ color: 'red' }}>*</span>
</label>
<input
className="shop-withdraw-input"
value={form.bankAccountName}
maxLength={64}
placeholder="户名 / 收款人姓名"
onChange={(e) => setForm({ ...form, bankAccountName: e.target.value })}
/>
{fieldErrors.bankAccountName ? (
<p className="shop-withdraw-msg" role="alert">{fieldErrors.bankAccountName}</p>
) : null}
<label className="shop-record-amount-label" style={{ marginTop: 12 }}>
银行账号 <span style={{ color: 'red' }}>*</span>
</label>
<input
className="shop-withdraw-input"
inputMode="numeric"
value={form.bankAccountNo}
maxLength={32}
placeholder="请输入银行账号"
onChange={(e) => setForm({ ...form, bankAccountNo: e.target.value.replace(/[^\d]/g, '') })}
/>
{fieldErrors.bankAccountNo ? (
<p className="shop-withdraw-msg" role="alert">{fieldErrors.bankAccountNo}</p>
) : null}
<label className="shop-record-amount-label" style={{ marginTop: 12 }}>
开户行名称
</label>
<input
className="shop-withdraw-input"
value={form.bankBranch}
maxLength={128}
placeholder="如 中国工商银行郑州分行"
onChange={(e) => setForm({ ...form, bankBranch: e.target.value })}
/>
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<button
type="button"
className="shop-withdraw-btn"
disabled={submitting}
onClick={() => void submit()}
>
{submitting ? '保存中…' : '保存'}
</button>
{editing ? (
<button
type="button"
className="shop-records-chip"
onClick={startCreate}
>
取消编辑
</button>
) : null}
</div>
</div>
</section>
) : (
<p className="shop-records-empty">仅主账号可维护收款账户</p>
)}
{msg ? <p className="shop-withdraw-msg">{msg}</p> : null}
</div>
</div>
);
}