feat: multi-module iteration
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import TabMainHeader from '../components/TabMainHeader';
|
||||
import AppImage from '@dukang/shared-ui/AppImage';
|
||||
import { request, type UserProfile } from '../lib/api';
|
||||
import { useUserSession } from '../contexts/UserSessionContext';
|
||||
import ContactCustomerSheet from '../components/ContactCustomerSheet';
|
||||
|
||||
const DEFAULT_AVATAR =
|
||||
'https://lh3.googleusercontent.com/aida-public/AB6AXuAz_9Pnpk_Md4sEU6PXkeybus8oLZO9e-3pOpLuSwBX0jm_Z0JCfX1w2oZxz1VZayTh0PKUPjwjSuxJVX410fjtWFGR_f55f-nWppXWUweHRnEC7WyIWEqx4AyVHt-k02OhyaSGQfvY5cHG5IuRe9EqdcHy47gBQ82_cxGgX-DrKV4oYcwLoNRynAV0_xv2p1GOhisnQVulHwZcQClUJcP8q4nTY0Y3DR1w4ioa0DYTHePE43mLDJptjZcQqS7V8LihJdn4ze6fvQA';
|
||||
|
||||
const ORDER_SHORTCUTS = [
|
||||
{ tab: 'pending_pay', icon: 'payments', label: '待付款' },
|
||||
{ tab: 'paid', icon: 'package_2', label: '已付款' },
|
||||
{ tab: 'completed', icon: 'task_alt', label: '已完成' },
|
||||
] as const;
|
||||
|
||||
const SERVICES = [
|
||||
{ icon: 'location_on', label: '地址管理', to: '/addresses' },
|
||||
{ icon: 'assignment_return', label: '售后工单', to: '/after-sale/list' },
|
||||
{ icon: 'receipt_long', label: '我的发票', to: '/invoices' },
|
||||
{ icon: 'storefront', label: '可用门店', to: '/stores' },
|
||||
{ icon: 'headset_mic', label: '联系客服', badge: '在线中', action: 'cs' as const },
|
||||
{ icon: 'info', label: '关于我们', action: 'about' as const },
|
||||
] as const;
|
||||
|
||||
function formatMoney(amount: number) {
|
||||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
export default function MinePage() {
|
||||
const navigate = useNavigate();
|
||||
const { profile: sessionProfile, refreshProfile, resetSession } = useUserSession();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(sessionProfile);
|
||||
const [benefitBalance, setBenefitBalance] = useState(0);
|
||||
const [orderCounts, setOrderCounts] = useState<Record<string, number>>({});
|
||||
const [toast, setToast] = useState('');
|
||||
const [showCs, setShowCs] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
request<UserProfile>('USER_H5', '/auth/me'),
|
||||
request<Array<Record<string, unknown>>>('USER_H5', '/benefit/coupons'),
|
||||
...ORDER_SHORTCUTS.map((s) =>
|
||||
request<{ total: number }>('USER_H5', `/trade/orders?tab=${s.tab}&pageSize=1`),
|
||||
),
|
||||
])
|
||||
.then(([me, coupons, ...totals]) => {
|
||||
setProfile(me);
|
||||
void refreshProfile();
|
||||
const balance = coupons.reduce((sum, c) => {
|
||||
if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0);
|
||||
return sum;
|
||||
}, 0);
|
||||
setBenefitBalance(balance);
|
||||
const counts: Record<string, number> = {};
|
||||
ORDER_SHORTCUTS.forEach((s, i) => {
|
||||
counts[s.tab] = totals[i]?.total ?? 0;
|
||||
});
|
||||
setOrderCounts(counts);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [refreshProfile]);
|
||||
|
||||
function showToast(msg: string) {
|
||||
setToast(msg);
|
||||
window.setTimeout(() => setToast(''), 2200);
|
||||
}
|
||||
|
||||
function handleService(item: (typeof SERVICES)[number]) {
|
||||
if ('to' in item && item.to) return;
|
||||
if (item.action === 'cs') setShowCs(true);
|
||||
if (item.action === 'about') showToast('杜康好客 · 传承千年酒文化');
|
||||
}
|
||||
|
||||
function logout() {
|
||||
void resetSession().then(() => navigate('/'));
|
||||
}
|
||||
|
||||
const nickname = profile?.nickname || '用户';
|
||||
const avatar = profile?.avatarUrl || DEFAULT_AVATAR;
|
||||
const hasWechat = !!profile?.hasWechat;
|
||||
|
||||
return (
|
||||
<div className="mine-page">
|
||||
<TabMainHeader title="我的" />
|
||||
<header className="mine-header">
|
||||
<div className="mine-header-texture" aria-hidden />
|
||||
<div className="mine-profile">
|
||||
<div className="mine-avatar-wrap">
|
||||
<AppImage src={avatar} alt="" wrapperClassName="mine-avatar app-image--fill" />
|
||||
{hasWechat && (
|
||||
<span className="mine-wechat-badge" title="已绑定微信">
|
||||
<span className="material-symbols-outlined">chat</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mine-profile-info">
|
||||
<h1 className="mine-profile-name">{nickname}</h1>
|
||||
<div className="mine-profile-meta">
|
||||
<span className="mine-member-tag">{hasWechat ? '微信会员' : '好客会员'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mine-header-glow" aria-hidden />
|
||||
</header>
|
||||
|
||||
<main className="mine-main">
|
||||
<section className="mine-card">
|
||||
<div className="mine-card-head">
|
||||
<h2 className="mine-card-title">
|
||||
<span className="material-symbols-outlined mine-card-title-icon">account_balance_wallet</span>
|
||||
我的资产
|
||||
</h2>
|
||||
<Link to="/benefit" className="mine-card-link">
|
||||
查看明细
|
||||
<span className="material-symbols-outlined">chevron_right</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mine-asset-panel">
|
||||
<div className="mine-asset-notch" aria-hidden />
|
||||
<div className="mine-asset-notch-line" aria-hidden />
|
||||
<div>
|
||||
<p className="mine-asset-label">好客权益余额</p>
|
||||
<div className="mine-asset-amount">
|
||||
<span className="mine-asset-currency">¥</span>
|
||||
<span className="mine-asset-value">{formatMoney(benefitBalance)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/redeem" className="mine-asset-cta">
|
||||
去使用
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mine-card">
|
||||
<div className="mine-card-head mine-card-head-orders">
|
||||
<h2 className="mine-card-title">我的订单</h2>
|
||||
<Link to="/orders" className="mine-card-link">
|
||||
全部订单
|
||||
<span className="material-symbols-outlined">chevron_right</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mine-order-grid">
|
||||
{ORDER_SHORTCUTS.map((item) => {
|
||||
const count = orderCounts[item.tab] ?? 0;
|
||||
return (
|
||||
<Link key={item.tab} to={`/orders?tab=${item.tab}`} className="mine-order-item">
|
||||
<div className="mine-order-icon-wrap">
|
||||
<span className="material-symbols-outlined mine-order-icon">{item.icon}</span>
|
||||
{count > 0 && (
|
||||
<span className="mine-order-badge">{count > 99 ? '99+' : count}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="mine-order-label">{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mine-card mine-services">
|
||||
{SERVICES.map((item, index) => {
|
||||
const inner = (
|
||||
<>
|
||||
<div className="mine-service-left">
|
||||
<span className="mine-service-icon-wrap">
|
||||
<span className="material-symbols-outlined">{item.icon}</span>
|
||||
</span>
|
||||
<span className="mine-service-label">{item.label}</span>
|
||||
</div>
|
||||
<div className="mine-service-right">
|
||||
{'badge' in item && item.badge && (
|
||||
<span className="mine-service-badge">{item.badge}</span>
|
||||
)}
|
||||
<span className="material-symbols-outlined mine-service-chevron">chevron_right</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
if ('to' in item && item.to) {
|
||||
return (
|
||||
<Link key={item.label} to={item.to} className="mine-service-item">
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
className="mine-service-item"
|
||||
onClick={() => handleService(item)}
|
||||
>
|
||||
{inner}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<div className="mine-footer">
|
||||
<p className="mine-version">杜康好客 V2.4.0</p>
|
||||
<button type="button" className="mine-logout" onClick={logout}>
|
||||
退出当前账号
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{toast && <div className="mine-toast">{toast}</div>}
|
||||
|
||||
{showCs && <ContactCustomerSheet onClose={() => setShowCs(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user