feat(store): v4.0.18 门店多收款账户与子账号继承二维码

C 端门店列表拼接省市区县地址;门店多银行账户与默认打款账户;子账号独立 sa_ 关联码及统计维度;同步 v4.0.18 开发文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 15:56:16 +08:00
parent a9c466930d
commit 4bdb09068c
26 changed files with 1193 additions and 102 deletions
+2
View File
@@ -13,6 +13,7 @@ import StatusPage from './pages/StatusPage';
import MinePage from './pages/MinePage';
import StaffPage from './pages/StaffPage';
import WithdrawPage from './pages/WithdrawPage';
import BankAccountsPage from './pages/BankAccountsPage';
import PackagesPage from './pages/PackagesPage';
export default function App() {
@@ -25,6 +26,7 @@ export default function App() {
<Route path="/select-store" element={<SelectStorePage />} />
<Route path="/staff" element={<StaffPage />} />
<Route path="/withdraw" element={<WithdrawPage />} />
<Route path="/bank-accounts" element={<BankAccountsPage />} />
<Route path="/packages" element={<PackagesPage />} />
<Route path="/redeem" element={<RedeemConfirmPage />} />
<Route path="/redeem/phone" element={<PhoneRedeemPage />} />
+252
View File
@@ -0,0 +1,252 @@
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>
);
}
+6
View File
@@ -153,6 +153,12 @@ export default function MinePage() {
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>account_balance_wallet</span>
</button>
{profile?.isPrimary ? (
<button type="button" className="shop-mine-action" onClick={() => navigate('/bank-accounts')}>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>account_balance</span>
</button>
) : null}
{profile?.isPrimary ? (
<button type="button" className="shop-mine-action" onClick={() => navigate('/packages')}>
<span className="material-symbols-outlined" style={{ fontSize: 20 }}>restaurant_menu</span>
+12
View File
@@ -1923,6 +1923,18 @@
text-align: center;
}
.shop-withdraw-input {
width: 100%;
margin-top: 6px;
height: 42px;
padding: 0 12px;
border: 1px solid var(--color-border, #e5e5e5);
border-radius: 10px;
font-size: 15px;
background: #fff;
box-sizing: border-box;
}
/* ─── 休息中核销·开张确认弹窗 ─── */
.shop-redeem-modal {
position: fixed;