This commit is contained in:
2026-06-30 10:33:56 +08:00
commit 6e047dc0a5
607 changed files with 65966 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import BrandLogo from '@dukang/shared-ui/BrandLogo';
import { request } from '../lib/api';
type BenefitSummary = {
totalBalance: number;
maxRedeemAmount: number;
activeCouponCount: number;
};
type CouponItem = {
id: string;
couponNo: string;
totalAmount: number;
usedAmount: number;
balance: number;
status: string;
sourceProduct: string;
};
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatCouponNo(no: string) {
const tail = no.replace(/^BC/i, '').slice(-6);
return `NO. DK${tail}`;
}
function usagePercent(coupon: CouponItem) {
const total = Number(coupon.totalAmount);
if (total <= 0) return 0;
return Math.min(100, Math.round((Number(coupon.usedAmount) / total) * 100));
}
export default function BenefitPage() {
const navigate = useNavigate();
const listRef = useRef<HTMLElement>(null);
const [summary, setSummary] = useState<BenefitSummary | null>(null);
const [coupons, setCoupons] = useState<CouponItem[]>([]);
const [tab, setTab] = useState<'available' | 'history'>('available');
useEffect(() => {
Promise.all([
request<BenefitSummary>('USER_H5', '/benefit/summary'),
request<CouponItem[]>('USER_H5', '/benefit/coupons'),
])
.then(([s, list]) => {
setSummary(s);
setCoupons(
list.map((c) => ({
id: String(c.id),
couponNo: String(c.couponNo),
totalAmount: Number(c.totalAmount),
usedAmount: Number(c.usedAmount),
balance: Number(c.balance),
status: String(c.status),
sourceProduct: String(c.sourceProduct),
})),
);
})
.catch(() => {});
}, []);
const available = coupons.filter((c) => c.status === 'ACTIVE' && c.balance > 0);
const history = coupons.filter((c) => c.status === 'USED_UP' || c.status === 'VOID');
const visible = tab === 'available' ? available : history;
function scrollToList() {
listRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
return (
<div className="benefit-page">
<header className="benefit-header">
<button type="button" className="benefit-header-city">
<span className="material-symbols-outlined">location_on</span>
<span></span>
</button>
<h1 className="app-page-title"></h1>
<button
type="button"
className="benefit-header-btn"
aria-label="通知"
onClick={() => window.alert('preV1:消息通知即将开放')}
>
<span className="material-symbols-outlined">notifications</span>
</button>
</header>
<main className="benefit-main">
<section className="benefit-hero">
<div className="benefit-hero-top">
<div>
<p className="benefit-hero-label"></p>
<div className="benefit-hero-amount">
<span className="benefit-hero-symbol">¥</span>
<span className="benefit-hero-value">
{summary ? formatMoney(summary.totalBalance) : '--'}
</span>
</div>
</div>
<BrandLogo className="benefit-hero-logo" />
</div>
<button type="button" className="benefit-hero-link" onClick={scrollToList}>
<span></span>
<span className="material-symbols-outlined">chevron_right</span>
</button>
</section>
<section className="benefit-action">
<button type="button" className="benefit-use-btn" onClick={() => navigate('/stores')}>
<span className="material-symbols-outlined filled">storefront</span>
使
</button>
</section>
<nav className="benefit-tabs" ref={listRef}>
<button
type="button"
className={`benefit-tab${tab === 'available' ? ' active' : ''}`}
onClick={() => setTab('available')}
>
使
</button>
<button
type="button"
className={`benefit-tab${tab === 'history' ? ' active' : ''}`}
onClick={() => setTab('history')}
>
/
</button>
</nav>
{visible.length > 0 ? (
<div className="benefit-list">
{visible.map((c) => (
<article key={c.id} className="benefit-card">
<div className="benefit-card-inner">
<div className="benefit-card-value">
<span className="benefit-card-value-label"></span>
<div className="benefit-card-value-amount">
<span>¥</span>
<span>{Math.round(c.totalAmount)}</span>
</div>
<span className="benefit-card-notch" aria-hidden />
</div>
<div className="benefit-card-body">
<div className="benefit-card-main">
<div className="benefit-card-title-row">
<h3 className="benefit-card-title">{c.sourceProduct}</h3>
<span className="benefit-card-badge"></span>
</div>
<p className="benefit-card-desc">使</p>
<div className="benefit-card-progress-wrap">
<div className="benefit-card-progress-labels">
<span>使 ¥{formatMoney(c.usedAmount)}</span>
<span>使 ¥{formatMoney(c.balance)}</span>
</div>
<div className="benefit-card-progress">
<div
className="benefit-card-progress-bar"
style={{ width: `${usagePercent(c)}%` }}
/>
</div>
</div>
</div>
<div className="benefit-card-foot">
<Link to={`/benefit/${c.id}`} className="benefit-card-no">
{formatCouponNo(c.couponNo)}
</Link>
<button
type="button"
className="benefit-card-redeem"
onClick={() => navigate('/redeem')}
>
</button>
</div>
</div>
</div>
</article>
))}
</div>
) : (
<div className="benefit-empty">
{tab === 'available' ? (
<>
<span className="material-symbols-outlined">card_giftcard</span>
<p></p>
</>
) : (
<>
<span className="material-symbols-outlined">history_edu</span>
<p>使</p>
</>
)}
</div>
)}
</main>
</div>
);
}