Files
dukang/apps/h5-user/src/pages/MinePage.tsx
T
2026-07-19 22:16:55 +08:00

214 lines
8.3 KiB
TypeScript

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>
);
}