合伙人 H5 改造
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-16 15:04:56 +08:00
parent fd5427c7ea
commit f1dc23c54c
21 changed files with 817 additions and 614 deletions
+98 -113
View File
@@ -1,147 +1,132 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { isLoggedIn, request } from '../lib/api';
import { Link } from 'react-router-dom';
import { request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { contactSupport } from '../lib/contact';
import { isPrimaryAccount } from '../lib/partnerAccess';
import { toastError } from '../lib/toast';
export default function CenterPage() {
const navigate = useNavigate();
const { account, logout } = usePartnerSession();
const showPrimaryMenus = isPrimaryAccount(account);
const me = account as unknown as Record<string, unknown> | null;
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
type CenterPageProps = {
/** 子账号个人中心复用 */
variant?: 'primary' | 'sub';
roleLabel?: string;
};
export default function CenterPage({ variant = 'primary', roleLabel }: CenterPageProps) {
const { account, logout, refresh } = usePartnerSession();
const isPrimary = variant === 'primary' && isPrimaryAccount(account);
const [name, setName] = useState(account?.name ?? '');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
}, [navigate]);
document.title = isPrimary ? '合伙人中心' : '个人中心';
}, [isPrimary]);
const pendingBills = bills.filter((b) => String(b.status).includes('PENDING') || String(b.status).includes('CONFIRM'));
useEffect(() => {
setName(account?.name ?? '');
}, [account?.name]);
async function handleSave() {
const trimmed = name.trim();
if (!trimmed) {
toastError('请输入姓名');
return;
}
if (trimmed === account?.name) return;
setSaving(true);
try {
await request('PARTNER_H5', '/partner/me', {
method: 'PUT',
body: JSON.stringify({ name: trimmed }),
});
await refresh();
} finally {
setSaving(false);
}
}
function handleContactPartner() {
if (isPrimary) {
if (!contactSupport()) toastError('暂无客服电话');
return;
}
if (!account?.primaryPhone) {
toastError('暂无合伙人联系方式');
return;
}
window.location.href = `tel:${account.primaryPhone}`;
}
return (
<div className="page partner-center-page">
<header className="header app-page-header">
<h1 className="app-page-title"></h1>
</header>
<div className="page partner-center-page partner-home--flush-top">
<section className="partner-profile-card">
<div className="partner-profile-avatar">
<span className="material-symbols-outlined">person</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(me?.name || '合伙人')}</h2>
<span className="partner-role-badge"></span>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{account?.name || '合伙人'}</h2>
<span className="partner-role-badge">{roleLabel || (isPrimary ? '城市合伙人' : '拓店员')}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4, color: 'var(--color-subtle-gray)' }}>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>location_on</span>
<span className="body-md">{String(me?.companyName || '郑州')}</span>
</div>
<p className="label-md text-muted" style={{ marginTop: 4 }}>{String(me?.phone || '')}</p>
</div>
</section>
<section>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 20px 8px' }}>
<h3 className="headline-md"></h3>
<Link to="/center/bills" className="label-md text-primary" style={{ display: 'flex', alignItems: 'center' }}>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>chevron_right</span>
</Link>
</div>
<Link to="/center/bills" className="partner-bills-banner">
<div className="partner-bills-banner-left">
<div className="partner-bills-icon">
<span className="material-symbols-outlined">pending_actions</span>
</div>
<div>
<p className="body-md" style={{ fontWeight: 500 }}></p>
<p className="label-md text-primary"> {pendingBills.length || bills.length} </p>
</div>
</div>
<span className="material-symbols-outlined text-primary">chevron_right</span>
</Link>
<div className="partner-finance-grid">
<div className="partner-finance-card partner-finance-card--hero">
<p className="label-md" style={{ opacity: 0.8, marginBottom: 4 }}> ()</p>
<span className="amount-xl" style={{ color: '#fff', fontSize: 32 }}>
{bills.length > 0 ? Number(bills[0].totalAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 }) : '0.00'}
</span>
</div>
<div className="partner-finance-card">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<p className="headline-md">
<span className="text-primary">¥</span>
{pendingBills.reduce((s, b) => s + Number(b.totalAmount || 0), 0).toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
</p>
</div>
<div className="partner-finance-card">
<p className="label-md text-muted" style={{ marginBottom: 4 }}></p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span className="headline-md">{bills.length} </span>
<span className="material-symbols-outlined text-muted">history</span>
</div>
<span className="body-md">{account?.companyName || '—'}</span>
</div>
<p className="label-md text-muted" style={{ marginTop: 4 }}>{account?.phone || ''}</p>
</div>
</section>
<section className="partner-menu-section">
<h3 className="headline-md" style={{ marginBottom: 8 }}></h3>
<div className="partner-menu-card" style={{ padding: '16px' }}>
<label className="partner-form-label" htmlFor="center-name"></label>
<input
id="center-name"
className="partner-form-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入姓名"
maxLength={32}
/>
<label className="partner-form-label" style={{ marginTop: 16 }}></label>
<input className="partner-form-input" value={account?.phone ?? ''} readOnly disabled />
<p className="label-md text-muted" style={{ marginTop: 4 }}></p>
<button
type="button"
className="partner-btn-primary"
style={{ width: '100%', marginTop: 16 }}
disabled={saving}
onClick={() => void handleSave()}
>
{saving ? '保存中…' : '保存资料'}
</button>
</div>
</section>
<section className="partner-menu-section">
<h3 className="headline-md" style={{ marginBottom: 8 }}></h3>
<div className="partner-menu-card">
<Link to="/stores" className="partner-menu-item">
<button type="button" className="partner-menu-item" onClick={() => { if (!contactSupport()) toastError('暂无客服电话'); }}>
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">store</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
<div className="partner-menu-icon"><span className="material-symbols-outlined">support_agent</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/orders" className="partner-menu-item">
</button>
<button type="button" className="partner-menu-item" onClick={handleContactPartner}>
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">receipt_long</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
<div className="partner-menu-icon"><span className="material-symbols-outlined">call</span></div>
<span className="body-md" style={{ fontSize: 16 }}>{isPrimary ? '联系总部' : '联系合伙人'}</span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/center/settlement" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">description</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{pendingBills.length > 0 && <span className="label-md text-primary" style={{ background: 'rgba(166,29,36,0.1)', padding: '2px 8px', borderRadius: 999 }}>{pendingBills.length}</span>}
</button>
{isPrimary && (
<Link to="/center/staff" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">group</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</div>
</Link>
<Link to="/reports/weekly" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">monitoring</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/reshipments" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">assignment_return</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
<Link to="/leaderboard" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">military_tech</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
{showPrimaryMenus && (
<Link to="/center/staff" className="partner-menu-item">
<div className="partner-menu-item-left">
<div className="partner-menu-icon"><span className="material-symbols-outlined">group</span></div>
<span className="body-md" style={{ fontSize: 16 }}></span>
</div>
<span className="material-symbols-outlined text-muted">chevron_right</span>
</Link>
</Link>
)}
</div>
</section>