4bdb09068c
C 端门店列表拼接省市区县地址;门店多银行账户与默认打款账户;子账号独立 sa_ 关联码及统计维度;同步 v4.0.18 开发文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
253 lines
9.0 KiB
TypeScript
253 lines
9.0 KiB
TypeScript
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 = '请填写 8~32 位数字账号';
|
||
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>
|
||
);
|
||
}
|