V4.0.9版本更新合伙人H5端调整
This commit is contained in:
@@ -26,6 +26,7 @@ import UsersManagePage from './pages/UsersManagePage';
|
||||
import AssocOrdersPage from './pages/AssocOrdersPage';
|
||||
import CommissionOrdersPage from './pages/CommissionOrdersPage';
|
||||
import ActivityPostersPage from './pages/ActivityPostersPage';
|
||||
import BankAccountPage from './pages/BankAccountPage';
|
||||
|
||||
function PrimaryRoutes() {
|
||||
return (
|
||||
@@ -42,6 +43,7 @@ function PrimaryRoutes() {
|
||||
<Route path="/center/settlement" element={<SettlementPage />} />
|
||||
<Route path="/center/staff" element={<StaffListPage />} />
|
||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||
<Route path="/center/bank" element={<BankAccountPage />} />
|
||||
<Route path="/center/activity-posters" element={<ActivityPostersPage />} />
|
||||
<Route path="/center/assoc" element={<Navigate to="/users" replace />} />
|
||||
<Route path="/center/assoc/users" element={<Navigate to="/users" replace />} />
|
||||
@@ -71,6 +73,7 @@ function SubAccountRoutes() {
|
||||
<Routes>
|
||||
<Route element={<SubAccountLayout navKind={navKind} />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/users" element={<UsersManagePage />} />
|
||||
{isWarehouse ? (
|
||||
<Route path="/orders" element={<OrderListPage tabRoot />} />
|
||||
) : (
|
||||
@@ -86,6 +89,7 @@ function SubAccountRoutes() {
|
||||
</>
|
||||
)}
|
||||
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
||||
<Route path="/users/:userId/orders" element={<AssocOrdersPage />} />
|
||||
{isWarehouse && <Route path="/orders/:id" element={<OrderDetailPage />} />}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -3,12 +3,14 @@ import type { PartnerNavKind } from '../lib/partnerAccess';
|
||||
|
||||
const STORE_STAFF_TABS = [
|
||||
{ to: '/', end: true, icon: 'dashboard', label: '首页' },
|
||||
{ to: '/users', icon: 'group', label: '用户管理' },
|
||||
{ to: '/stores', icon: 'store', label: '门店管理' },
|
||||
{ to: '/me', icon: 'person', label: '个人中心' },
|
||||
] as const;
|
||||
|
||||
const WAREHOUSE_STAFF_TABS = [
|
||||
{ to: '/', end: true, icon: 'dashboard', label: '首页' },
|
||||
{ to: '/users', icon: 'group', label: '用户管理' },
|
||||
{ to: '/orders', icon: 'receipt_long', label: '订单管理' },
|
||||
{ to: '/me', icon: 'person', label: '个人中心' },
|
||||
] as const;
|
||||
|
||||
@@ -94,8 +94,8 @@ export function partnerHomePath(account: PartnerMe | null | undefined): string {
|
||||
return '/';
|
||||
}
|
||||
|
||||
const STORE_STAFF_PREFIXES = ['/', '/stores', '/me', '/leaderboard', '/login'];
|
||||
const WAREHOUSE_STAFF_PREFIXES = ['/', '/orders', '/me', '/leaderboard', '/login'];
|
||||
const STORE_STAFF_PREFIXES = ['/', '/stores', '/users', '/me', '/leaderboard', '/login'];
|
||||
const WAREHOUSE_STAFF_PREFIXES = ['/', '/orders', '/users', '/me', '/leaderboard', '/login'];
|
||||
|
||||
export function isSubAccountPath(pathname: string, navKind: PartnerNavKind = 'store_staff'): boolean {
|
||||
if (pathname === '/login') return true;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { PartnerSettlementPreviewDto } from '@dukang/shared-types';
|
||||
|
||||
export function formatBillPeriod(start?: string | null, end?: string | null) {
|
||||
const s = String(start || '').slice(0, 10);
|
||||
const e = String(end || '').slice(0, 10);
|
||||
if (!s) return '—';
|
||||
if (!e || e === s) return s;
|
||||
return `${s} ~ ${e}`;
|
||||
}
|
||||
|
||||
export function formatIssueLabel(preview: Pick<PartnerSettlementPreviewDto, 'nextIssueAt' | 'cycleLabel'> | null) {
|
||||
if (!preview?.nextIssueAt) return preview?.cycleLabel || '每周一 08:00 出账';
|
||||
const ymd = new Date(preview.nextIssueAt).toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });
|
||||
if (!ymd || ymd === 'Invalid Date') return preview.cycleLabel || '每周一 08:00 出账';
|
||||
return `${ymd} 08:00 出账`;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import type { PartnerMe, UpdatePartnerBankRequest } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
|
||||
type FieldErrors = {
|
||||
bankAccountName?: string;
|
||||
bankAccountNo?: string;
|
||||
bankBranch?: string;
|
||||
};
|
||||
|
||||
export default function BankAccountPage() {
|
||||
const navigate = useNavigate();
|
||||
const { account, refresh } = usePartnerSession();
|
||||
const [bankAccountName, setBankAccountName] = useState(account?.bankAccount?.bankAccountName ?? '');
|
||||
const [bankAccountNo, setBankAccountNo] = useState(account?.bankAccount?.bankAccountNo ?? '');
|
||||
const [bankBranch, setBankBranch] = useState(account?.bankAccount?.bankBranch ?? '');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '收款账户';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setBankAccountName(account?.bankAccount?.bankAccountName ?? '');
|
||||
setBankAccountNo(account?.bankAccount?.bankAccountNo ?? '');
|
||||
setBankBranch(account?.bankAccount?.bankBranch ?? '');
|
||||
}, [account?.bankAccount]);
|
||||
|
||||
async function submit() {
|
||||
const name = bankAccountName.trim();
|
||||
const no = bankAccountNo.replace(/\s+/g, '');
|
||||
const branch = bankBranch.trim();
|
||||
const next: FieldErrors = {};
|
||||
if (!name) next.bankAccountName = '请填写收款人';
|
||||
if (!/^\d{8,32}$/.test(no)) next.bankAccountNo = '请填写 8~32 位数字账号';
|
||||
if (!branch) next.bankBranch = '请填写开户行名称';
|
||||
setFieldErrors(next);
|
||||
if (Object.keys(next).length) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const body: UpdatePartnerBankRequest = {
|
||||
bankAccountName: name,
|
||||
bankAccountNo: no,
|
||||
bankBranch: branch,
|
||||
};
|
||||
await request<PartnerMe>('PARTNER_H5', '/partner/me/bank', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
await refresh();
|
||||
toastSuccess('收款账户已保存');
|
||||
navigate('/center');
|
||||
} catch (e) {
|
||||
toastError(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="partner-page-sticky">
|
||||
<PageHeader title="收款账户" onBack={() => navigate('/center')} />
|
||||
|
||||
<section className="partner-form-card">
|
||||
<div className="partner-section-title">
|
||||
<div className="partner-section-bar" />
|
||||
<h2 className="headline-md">银行账号</h2>
|
||||
</div>
|
||||
<p className="label-md text-muted" style={{ marginBottom: 16, lineHeight: 1.5 }}>
|
||||
用于总部打款。请填写与合同一致的对公或对私账户。
|
||||
</p>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>收款人 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">person</span>
|
||||
<input
|
||||
placeholder="户名 / 收款人姓名"
|
||||
value={bankAccountName}
|
||||
maxLength={64}
|
||||
onChange={(e) => setBankAccountName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.bankAccountName && (
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.bankAccountName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>银行账号 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">credit_card</span>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
placeholder="请输入银行账号"
|
||||
value={bankAccountNo}
|
||||
maxLength={32}
|
||||
onChange={(e) => setBankAccountNo(e.target.value.replace(/[^\d]/g, ''))}
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.bankAccountNo && (
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.bankAccountNo}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="partner-field">
|
||||
<label>开户行名称 <span className="text-primary">*</span></label>
|
||||
<div className="partner-field-input">
|
||||
<span className="material-symbols-outlined">account_balance</span>
|
||||
<input
|
||||
placeholder="如 中国工商银行洛阳分行"
|
||||
value={bankBranch}
|
||||
maxLength={128}
|
||||
onChange={(e) => setBankBranch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.bankBranch && (
|
||||
<p className="partner-field-error" role="alert">{fieldErrors.bankBranch}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="partner-sticky-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
disabled={submitting}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
{submitting ? '保存中…' : '保存'}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import type { PartnerBillDetailDto, PartnerBillDto, PartnerBillItemDto } from '@dukang/shared-types';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { formatBillPeriod } from '../lib/settlement';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { usePartnerPageView } from '../lib/usePageView';
|
||||
|
||||
@@ -188,7 +189,8 @@ export default function BillsPage() {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
|
||||
<div>
|
||||
<p className="label-md text-muted" style={{ textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>SETTLEMENT PERIOD</p>
|
||||
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '月度结算账单')}</h2>
|
||||
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '周结算账单')}</h2>
|
||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>{formatBillPeriod(bill.periodStart, bill.periodEnd)}</p>
|
||||
</div>
|
||||
<span className={`partner-status-pill${isRejected ? ' partner-status-pill--closed' : isUnpaid ? ' partner-status-pill--paused' : isPaid ? ' partner-status-pill--open' : ' partner-status-pill--paused'}`}>
|
||||
{billStatusLabel(status)}
|
||||
@@ -271,7 +273,7 @@ export default function BillsPage() {
|
||||
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
||||
<div>
|
||||
<p className="body-md">{b.billNo}</p>
|
||||
<p className="label-md text-muted">{billStatusLabel(b.status)}</p>
|
||||
<p className="label-md text-muted">{formatBillPeriod(b.periodStart, b.periodEnd)} · {billStatusLabel(b.status)}</p>
|
||||
{b.status === 'REJECTED' && b.rejectReason ? (
|
||||
<p className="label-md text-primary" style={{ marginTop: 4 }}>驳回:{b.rejectReason}</p>
|
||||
) : null}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||
import type { PartnerBillDto, PartnerSettlementPreviewDto } from '@dukang/shared-types';
|
||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { request } from '../lib/api';
|
||||
@@ -8,6 +8,7 @@ import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { contactSupport } from '../lib/contact';
|
||||
import { isPrimaryAccount } from '../lib/partnerAccess';
|
||||
import { listPartnerStaff } from '../lib/staff';
|
||||
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import {
|
||||
authorizePartnerWechat,
|
||||
@@ -31,6 +32,7 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
const isPrimary = variant === 'primary' && isPrimaryAccount(account);
|
||||
const [authorizing, setAuthorizing] = useState(false);
|
||||
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
||||
const [preview, setPreview] = useState<PartnerSettlementPreviewDto | null>(null);
|
||||
const [staffCount, setStaffCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,6 +46,9 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills')
|
||||
.then(setBills)
|
||||
.catch(() => setBills([])),
|
||||
request<PartnerSettlementPreviewDto>('PARTNER_H5', '/partner/settlement/preview')
|
||||
.then(setPreview)
|
||||
.catch(() => setPreview(null)),
|
||||
listPartnerStaff()
|
||||
.then((list) => setStaffCount(list.length))
|
||||
.catch(() => setStaffCount(0)),
|
||||
@@ -188,6 +193,15 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
<p className="partner-finance-hero-label">账户余额 (元)</p>
|
||||
<p className="partner-finance-hero-amount">{fmtMoney(finance.balance)}</p>
|
||||
</div>
|
||||
<div className="partner-finance-card">
|
||||
<p className="label-md text-muted">预付款</p>
|
||||
<p className="partner-finance-value text-primary">¥ {fmtMoney(preview?.prepaymentAmount ?? 0)}</p>
|
||||
<p className="label-md text-muted" style={{ marginTop: 6, lineHeight: 1.4 }}>
|
||||
{formatBillPeriod(preview?.periodStart, preview?.periodEnd)}
|
||||
<br />
|
||||
{formatIssueLabel(preview)}
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/center/settlement" className="partner-finance-card">
|
||||
<p className="label-md text-muted">待结算</p>
|
||||
<p className="partner-finance-value text-primary">¥ {fmtMoney(finance.pendingTotal)}</p>
|
||||
@@ -219,6 +233,22 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to="/center/bank" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
<span className="material-symbols-outlined">account_balance</span>
|
||||
</div>
|
||||
<span className="body-md" style={{ fontSize: 16 }}>收款账户</span>
|
||||
</div>
|
||||
<div className="partner-menu-item-right">
|
||||
{account?.bankAccount?.bankAccountNo ? (
|
||||
<span className="label-md text-muted">{account.bankAccount.bankAccountNo.slice(-4)}</span>
|
||||
) : (
|
||||
<span className="partner-menu-status">未填写</span>
|
||||
)}
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to="/center/activity-posters" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import type { PartnerAssocStats, PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||
import type { PartnerAssocStats, PartnerLeaderboardEntry, PartnerSettlementPreviewDto } from '@dukang/shared-types';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { usePartnerPageView } from '../lib/usePageView';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
||||
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
|
||||
import {
|
||||
canAccessPartnerDashboard,
|
||||
canAccessPartnerOrders,
|
||||
@@ -154,6 +155,7 @@ export default function HomePage() {
|
||||
const [orders, setOrders] = useState<Array<Record<string, unknown>>>([]);
|
||||
const [leaderboardEntries, setLeaderboardEntries] = useState<PartnerLeaderboardEntry[]>([]);
|
||||
const [assocStats, setAssocStats] = useState<PartnerAssocStats | null>(null);
|
||||
const [preview, setPreview] = useState<PartnerSettlementPreviewDto | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = '工作台';
|
||||
@@ -203,9 +205,13 @@ export default function HomePage() {
|
||||
request<PartnerAssocStats>('PARTNER_H5', '/partner/assoc/stats', { silent: true })
|
||||
.then(setAssocStats)
|
||||
.catch(() => setAssocStats(null)),
|
||||
request<PartnerSettlementPreviewDto>('PARTNER_H5', '/partner/settlement/preview', { silent: true })
|
||||
.then(setPreview)
|
||||
.catch(() => setPreview(null)),
|
||||
);
|
||||
} else {
|
||||
setAssocStats(null);
|
||||
setPreview(null);
|
||||
}
|
||||
|
||||
tasks.push(
|
||||
@@ -245,27 +251,20 @@ export default function HomePage() {
|
||||
? Math.round(((monthNew - lastMonthNew) / lastMonthNew) * 100)
|
||||
: (monthNew > 0 ? 100 : 0);
|
||||
|
||||
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
|
||||
const revenue = orderCount * 128.45;
|
||||
const profit = revenue * 0.25;
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={loadHome} className="page partner-home partner-home--flush-top">
|
||||
<main className="partner-home-body">
|
||||
{isPrimary && (
|
||||
<>
|
||||
<section className="partner-revenue-card">
|
||||
<p className="partner-revenue-label">
|
||||
实时营业额 (CNY)
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 14 }}>info</span>
|
||||
<p className="partner-revenue-label">预付款金额 (CNY)</p>
|
||||
<div className="partner-revenue-amount">{fmtMoney(preview?.prepaymentAmount ?? 0)}</div>
|
||||
<p className="partner-revenue-label" style={{ marginTop: 8, opacity: 0.85, lineHeight: 1.5 }}>
|
||||
账期 {formatBillPeriod(preview?.periodStart, preview?.periodEnd)} · {formatIssueLabel(preview)}
|
||||
</p>
|
||||
<p className="partner-revenue-label" style={{ marginTop: 6, opacity: 0.75 }}>
|
||||
本周酒单与核销预估佣金,实际以账单为准
|
||||
</p>
|
||||
<div className="partner-revenue-amount">{fmtMoney(revenue)}</div>
|
||||
<div className="partner-revenue-grid">
|
||||
<div>
|
||||
<p className="partner-revenue-label">预计利润</p>
|
||||
<p className="headline-md" style={{ color: '#fff', marginTop: 4 }}>¥ {fmtMoney(profit)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||
<div className="partner-quick-actions">
|
||||
|
||||
@@ -2,8 +2,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import type { PartnerBillDto } from '@dukang/shared-types';
|
||||
import type { PartnerBillDto, PartnerSettlementPreviewDto } from '@dukang/shared-types';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
|
||||
|
||||
type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing' | 'rejected';
|
||||
|
||||
@@ -31,25 +32,23 @@ function billStatusClass(status: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function billPeriodLabel(periodStart: string) {
|
||||
const d = new Date(periodStart);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function billMonthLabel(year: number, month: number) {
|
||||
return `${year}年${month}月`;
|
||||
function billPeriodLabel(bill: PartnerBillDto) {
|
||||
return formatBillPeriod(bill.periodStart, bill.periodEnd);
|
||||
}
|
||||
|
||||
export default function SettlementPage() {
|
||||
const navigate = useNavigate();
|
||||
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
||||
const now = new Date();
|
||||
const [month, setMonth] = useState({ year: now.getFullYear(), month: now.getMonth() + 1 });
|
||||
const [preview, setPreview] = useState<PartnerSettlementPreviewDto | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
||||
|
||||
const loadBills = useCallback(() => {
|
||||
return request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
|
||||
return Promise.all([
|
||||
request<PartnerBillDto[]>('PARTNER_H5', '/partner/settlement/bills').then(setBills),
|
||||
request<PartnerSettlementPreviewDto>('PARTNER_H5', '/partner/settlement/preview')
|
||||
.then(setPreview)
|
||||
.catch(() => setPreview(null)),
|
||||
]).then(() => undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,22 +59,14 @@ export default function SettlementPage() {
|
||||
const summary = useMemo(() => {
|
||||
const pending = bills.filter((b) => b.status === 'AWAITING_CONFIRM');
|
||||
const settled = bills.filter((b) => b.status === 'PAID');
|
||||
const currentMonth = bills.filter((b) => {
|
||||
const d = new Date(b.periodStart);
|
||||
return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
|
||||
});
|
||||
return {
|
||||
pendingTotal: pending.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||
monthEstimate: currentMonth.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||
};
|
||||
}, [bills, now]);
|
||||
}, [bills]);
|
||||
|
||||
const filteredBills = useMemo(() => {
|
||||
return bills.filter((b) => {
|
||||
const d = new Date(b.periodStart);
|
||||
const matchMonth = d.getFullYear() === month.year && d.getMonth() + 1 === month.month;
|
||||
if (!matchMonth) return false;
|
||||
if (statusFilter === 'all') return true;
|
||||
if (statusFilter === 'pending') return b.status === 'AWAITING_CONFIRM';
|
||||
if (statusFilter === 'settled') return b.status === 'PAID';
|
||||
@@ -83,16 +74,7 @@ export default function SettlementPage() {
|
||||
if (statusFilter === 'rejected') return b.status === 'REJECTED';
|
||||
return true;
|
||||
});
|
||||
}, [bills, month, statusFilter]);
|
||||
|
||||
const monthOptions = useMemo(() => {
|
||||
const opts: Array<{ year: number; month: number }> = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
opts.push({ year: d.getFullYear(), month: d.getMonth() + 1 });
|
||||
}
|
||||
return opts;
|
||||
}, [now]);
|
||||
}, [bills, statusFilter]);
|
||||
|
||||
function openBill(bill: PartnerBillDto) {
|
||||
navigate(bill.id ? `/center/bills?id=${bill.id}` : '/center/bills');
|
||||
@@ -127,33 +109,18 @@ export default function SettlementPage() {
|
||||
</div>
|
||||
<div className="partner-settlement-summary-divider" />
|
||||
<div>
|
||||
<p className="partner-settlement-summary-sub">本月预估收益</p>
|
||||
<p className="partner-settlement-summary-sub">本周预付款</p>
|
||||
<p className="partner-settlement-summary-value partner-settlement-summary-value--accent">
|
||||
¥{fmtMoney(summary.monthEstimate)}
|
||||
¥{fmtMoney(preview?.prepaymentAmount ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="partner-settlement-filters">
|
||||
<select
|
||||
className="partner-settlement-month"
|
||||
value={`${month.year}-${month.month}`}
|
||||
onChange={(e) => {
|
||||
const [y, m] = e.target.value.split('-').map(Number);
|
||||
setMonth({ year: y, month: m });
|
||||
}}
|
||||
>
|
||||
{monthOptions.map((opt) => (
|
||||
<option key={`${opt.year}-${opt.month}`} value={`${opt.year}-${opt.month}`}>
|
||||
{billMonthLabel(opt.year, opt.month)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className="partner-settlement-status-btn">
|
||||
<span className="material-symbols-outlined">filter_list</span>
|
||||
状态
|
||||
</button>
|
||||
<p className="label-md text-muted" style={{ margin: 0, lineHeight: 1.5 }}>
|
||||
账期 {formatBillPeriod(preview?.periodStart, preview?.periodEnd)} · {formatIssueLabel(preview)}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="partner-settlement-chips">
|
||||
@@ -193,7 +160,7 @@ export default function SettlementPage() {
|
||||
</div>
|
||||
<p className="partner-settlement-item-meta">
|
||||
<span className="material-symbols-outlined">event</span>
|
||||
{billPeriodLabel(bill.periodStart)}
|
||||
{billPeriodLabel(bill)}
|
||||
<span className="partner-settlement-dot" />
|
||||
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
|
||||
</p>
|
||||
|
||||
@@ -205,7 +205,7 @@ export default function StaffCreatePage() {
|
||||
<span className="material-symbols-outlined">info</span>
|
||||
</div>
|
||||
<p className="label-md text-variant" style={{ marginTop: 0, lineHeight: 1.5 }}>
|
||||
子账号创建后默认禁用,需在子账号列表中手动启用后方可登录。
|
||||
子账号创建后默认可登录,可在子账号列表中禁用。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
import { usePartnerPageView } from '../lib/usePageView';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
import { isSubAccount } from '../lib/partnerAccess';
|
||||
|
||||
type ListRes = { items: PartnerAssocUserItem[]; total: number };
|
||||
|
||||
@@ -38,6 +40,8 @@ function fmtTime(iso?: string | null) {
|
||||
|
||||
export default function UsersManagePage() {
|
||||
usePartnerPageView('partner_users_manage_view');
|
||||
const { account } = usePartnerSession();
|
||||
const hidePoster = isSubAccount(account);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [summary, setSummary] = useState<PartnerAssocSummary | null>(null);
|
||||
@@ -63,7 +67,7 @@ export default function UsersManagePage() {
|
||||
}, [previewUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const posterId = summary?.activityPosterId;
|
||||
const posterId = hidePoster ? null : summary?.activityPosterId;
|
||||
if (!posterId) {
|
||||
setHeroUrl(null);
|
||||
setHeroLoading(false);
|
||||
@@ -88,7 +92,7 @@ export default function UsersManagePage() {
|
||||
cancelled = true;
|
||||
if (acc.url) URL.revokeObjectURL(acc.url);
|
||||
};
|
||||
}, [summary?.activityPosterId]);
|
||||
}, [hidePoster, summary?.activityPosterId]);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
const data = await request<PartnerAssocSummary>('PARTNER_H5', '/partner/assoc');
|
||||
@@ -130,7 +134,7 @@ export default function UsersManagePage() {
|
||||
}, [searchParams, navigate]);
|
||||
|
||||
async function downloadMainImage() {
|
||||
const posterId = summary?.activityPosterId;
|
||||
const posterId = hidePoster ? null : summary?.activityPosterId;
|
||||
try {
|
||||
if (isWechatEnv() && !posterId && isHttpImageUrl(summary?.qrcodeUrl)) {
|
||||
await previewSaveableImage(summary.qrcodeUrl);
|
||||
@@ -190,14 +194,14 @@ export default function UsersManagePage() {
|
||||
<p className="body-md text-muted" style={{ marginBottom: 12 }}>
|
||||
用户扫码后首次锁定,后续购酒计入关联订单
|
||||
</p>
|
||||
{summary?.activityPosterId && (previewUrl || heroUrl) ? (
|
||||
{summary?.activityPosterId && !hidePoster && (previewUrl || heroUrl) ? (
|
||||
<img
|
||||
className="partner-longpress-img"
|
||||
src={previewUrl || heroUrl || ''}
|
||||
alt="活动图"
|
||||
style={{ width: '100%', maxWidth: 360, background: '#f5f5f5' }}
|
||||
/>
|
||||
) : summary?.activityPosterId && heroLoading ? (
|
||||
) : summary?.activityPosterId && !hidePoster && heroLoading ? (
|
||||
<p className="body-md text-muted">正在生成活动图…</p>
|
||||
) : previewUrl || summary?.qrcodeUrl ? (
|
||||
<img
|
||||
@@ -209,19 +213,27 @@ export default function UsersManagePage() {
|
||||
) : (
|
||||
<p className="body-md text-muted">{loading ? '加载中…' : '关联码尚未生成'}</p>
|
||||
)}
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
已关联 {summary?.userCount ?? total} 人
|
||||
</p>
|
||||
{(() => {
|
||||
const scanCount = summary?.scanCount ?? 0;
|
||||
const userCount = summary?.userCount ?? total;
|
||||
if (scanCount <= 0 && userCount <= 0) return null;
|
||||
return (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
已扫码 {scanCount} 人 · 已关联 {userCount} 人
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadMainImage()}>
|
||||
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
|
||||
{summary?.activityPosterId && !hidePoster ? '下载活动图' : '下载二维码'}
|
||||
</button>
|
||||
{isWechatEnv() && (previewUrl || summary?.qrcodeUrl) ? (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
微信内请点「{summary?.activityPosterId ? '下载活动图' : '下载二维码'}」后长按保存
|
||||
微信内请点「{summary?.activityPosterId && !hidePoster ? '下载活动图' : '下载二维码'}」后长按保存
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{!hidePoster && (
|
||||
<Link to="/center/activity-posters" className="partner-menu-card" style={{ display: 'block', marginBottom: 20, textDecoration: 'none', color: 'inherit' }}>
|
||||
<div className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
@@ -233,6 +245,7 @@ export default function UsersManagePage() {
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<h2 className="headline-md" style={{ marginBottom: 12 }}>已关联用户</h2>
|
||||
<div className="partner-search">
|
||||
@@ -264,32 +277,40 @@ export default function UsersManagePage() {
|
||||
{!loading && items.length === 0 && <div className="empty">暂无关联用户</div>}
|
||||
{items.map((u) => (
|
||||
<div key={u.id} className="partner-store-card" style={{ marginBottom: 12 }}>
|
||||
<div className="partner-store-card-header" style={{ marginBottom: 8 }}>
|
||||
<div>
|
||||
<p className="body-md">{formatUserLabel(u)}</p>
|
||||
<p className="label-md text-muted">{u.phone || '未绑定手机'}</p>
|
||||
<p className="label-md text-muted">注册 {fmtTime(u.createdAt)}</p>
|
||||
</div>
|
||||
<div className="partner-store-card-header" style={{ marginBottom: 4 }}>
|
||||
<p className="body-md">{formatUserLabel(u)}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="label-md text-primary"
|
||||
style={{ background: 'none', border: 0, padding: 0 }}
|
||||
style={{ background: 'none', border: 0, padding: 0, flexShrink: 0 }}
|
||||
onClick={() => navigate(`/users/${u.id}/orders`)}
|
||||
>
|
||||
{u.orderCount} 单
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="label-md text-primary"
|
||||
style={{ background: 'none', border: 0, padding: 0 }}
|
||||
onClick={() => {
|
||||
setEditing(u);
|
||||
setRemarkDraft(u.partnerRemark ?? '');
|
||||
<p className="label-md text-muted">{u.phone || '未绑定手机'}</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{u.partnerRemark ? '改备注' : '添加备注'}
|
||||
</button>
|
||||
<p className="label-md text-muted">注册 {fmtTime(u.createdAt)}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="label-md text-primary"
|
||||
style={{ background: 'none', border: 0, padding: 0, flexShrink: 0 }}
|
||||
onClick={() => {
|
||||
setEditing(u);
|
||||
setRemarkDraft(u.partnerRemark ?? '');
|
||||
}}
|
||||
>
|
||||
{u.partnerRemark ? '改备注' : '添加备注'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user