aura-web/src/pages/PointsMallPage/components/PointsMallPageWeb.tsx

276 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Button, Card, Empty, Form, Input, Select, Space, Spin, Tag } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import type { PointsMallProduct } from '../../../api';
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
import ExchangeModal from '../components/ExchangeModal';
import ConfirmExchangeModal from '../components/ConfirmExchangeModal';
import type { ExchangeFormValues } from '../types';
interface Props {
logic: PointsMallPageLogicOutput;
}
export default function PointsMallPageWeb({ logic }: Props) {
const [exchangeForm] = Form.useForm<ExchangeFormValues>();
const {
overview,
overviewLoading,
categories,
categoryId,
q,
sort,
page,
pageSize,
productsLoading,
products,
total,
userPoints,
totalSpentUSD,
banner,
promoEntries,
exchangeModalVisible,
confirmModalVisible,
selectedProduct,
exchangeQuantity,
pendingExpiresAt,
exchangeLoading,
setCategoryId,
setQ,
setSort,
setPage,
setPageSize,
handleExchangeClick,
setExchangeModalVisible,
setConfirmModalVisible,
} = logic;
return (
<div className="page-container" style={{ maxWidth: 1400 }}>
<div className="points-mall-hero">
<div className="points-mall-header">
<div className="points-mall-title-section">
<h1 className="page-title stats-page-title"></h1>
<p className="page-subtitle stats-page-subtitle">使 API 1 = 1000 </p>
</div>
<div className="points-mall-balance-card">
{overviewLoading ? (
<Spin />
) : (
<div className="points-balance-row">
<div>
<div className="points-balance-label"></div>
<div className="points-balance-value">{userPoints.toLocaleString()}</div>
<div className="points-balance-subtext">
${typeof totalSpentUSD === 'number' ? totalSpentUSD.toFixed(2) : '--'}
</div>
</div>
<Tag
bordered={false}
style={{ margin: 0, borderRadius: 999, background: 'var(--color-brand-soft)', color: 'var(--color-brand)' }}
>
{String(overview?.me?.level || 'Lv.0')}
</Tag>
</div>
)}
</div>
</div>
</div>
<Card className="points-mall-category-card" bodyStyle={{ padding: 0 }}>
<div className="points-mall-category-body">
<div className="points-mall-category-row">
<span className="points-mall-category-label"></span>
{categories.map((c) => (
<Button
key={c.id}
size="small"
type={c.id === categoryId ? 'primary' : 'default'}
style={{ borderRadius: 999 }}
onClick={() => {
setPage(1);
setCategoryId(c.id);
}}
>
{c.name}
</Button>
))}
</div>
</div>
</Card>
<div className="points-mall-banner-section">
<div className="points-mall-banner-card">
<div className="points-mall-banner-header">
<div>
<div className="points-mall-banner-title">{banner?.title || '本期活动'}</div>
<div className="points-mall-banner-subtitle">{banner?.subtitle || 'Up to 25% Off'}</div>
</div>
<Button type="primary" style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
</Button>
</div>
<div className="points-mall-banner-footer">banner </div>
</div>
<div className="points-mall-promo-grid">
{promoEntries.slice(0, 2).map((p) => (
<div key={p.id} className="points-mall-promo-card">
<div style={{ minWidth: 0 }}>
<div className="points-mall-promo-title">{p.title}</div>
<div className="points-mall-promo-subtitle">{p.subtitle}</div>
</div>
<Button size="small" className="points-mall-exchange-btn">
</Button>
</div>
))}
{promoEntries.length < 2 && <div className="points-mall-promo-empty"></div>}
</div>
</div>
<Card className="stats-page-chart-card">
<div className="points-mall-filters-row">
<Space size={10} wrap className="points-mall-filters-left">
<Input
value={q}
onChange={(e) => {
setPage(1);
setQ(e.target.value);
}}
prefix={<SearchOutlined />}
placeholder="搜索商品"
allowClear
className="points-mall-search-input"
/>
<Select
value={sort}
className="points-mall-filter-select"
onChange={(v) => {
setPage(1);
setSort(v);
}}
options={[
{ value: 'popular', label: '热度优先' },
{ value: 'newest', label: '最新上架' },
{ value: 'price_asc', label: '积分从低到高' },
{ value: 'price_desc', label: '积分从高到低' },
]}
/>
<Select
value={pageSize}
className="points-mall-filter-select-small"
onChange={(v) => {
setPage(1);
setPageSize(v);
}}
options={[
{ value: 12, label: '每页 12' },
{ value: 24, label: '每页 24' },
{ value: 48, label: '每页 48' },
]}
/>
</Space>
<div className="points-mall-total-text"> {total.toLocaleString()} </div>
</div>
{productsLoading ? (
<Spin style={{ marginTop: 40, display: 'block' }} />
) : products.length === 0 ? (
<Empty description="暂无商品" style={{ marginTop: 60 }} />
) : (
<div className="points-mall-products-grid">
{products.map((p: PointsMallProduct) => (
<div key={p.id} className="points-mall-product-card">
<div className="points-mall-product-cover" />
<div className="points-mall-product-body">
<div className="points-mall-product-header">
<div className="points-mall-product-info">
<div className="points-mall-product-name">{p.name}</div>
<div className="points-mall-product-desc">{p.subtitle}</div>
</div>
{p.tags?.length ? (
<Tag bordered={false} className="points-mall-product-tag">
{p.tags[0]}
</Tag>
) : null}
</div>
<div className="points-mall-product-price-row">
<div>
<span className="points-mall-product-price">{Number(p.pointsPrice).toLocaleString()}</span>
<span className="points-mall-product-price-label"></span>
</div>
<Button
type="primary"
className="points-mall-exchange-btn points-mall-product-exchange-btn"
disabled={userPoints < p.pointsPrice || exchangeLoading}
onClick={() => handleExchangeClick(p)}
>
{userPoints < p.pointsPrice ? '积分不足' : '兑换'}
</Button>
</div>
<div className="points-mall-product-footer">
<span> {p.stock}</span>
<span> {p.sold}</span>
</div>
</div>
</div>
))}
</div>
)}
{!!total && (
<div className="points-mall-pagination">
<Space size={10}>
<Button disabled={page <= 1} onClick={() => setPage((v) => Math.max(1, v - 1))} className="points-mall-exchange-btn">
</Button>
<Tag bordered={false} className="points-mall-pagination-tag">
{page}
</Tag>
<Button disabled={page * pageSize >= total} onClick={() => setPage((v) => v + 1)} className="points-mall-exchange-btn">
</Button>
</Space>
</div>
)}
</Card>
<ExchangeModal
open={exchangeModalVisible}
product={selectedProduct}
quantity={exchangeQuantity}
expiresAt={pendingExpiresAt}
loading={exchangeLoading}
form={exchangeForm}
onSubmit={() => {
exchangeForm.validateFields().then((values) => logic.handleExchangeSubmit(values));
}}
onCancel={() => {
setExchangeModalVisible(false);
exchangeForm.resetFields();
logic.setPendingOrderId(null);
logic.setPendingExpiresAt(null);
logic.setSelectedProduct(null);
logic.setExchangeQuantity(1);
}}
/>
<ConfirmExchangeModal
open={confirmModalVisible}
product={selectedProduct}
userPoints={userPoints}
quantity={exchangeQuantity}
loading={exchangeLoading}
onQuantityChange={(v) => logic.setExchangeQuantity(Math.max(1, Math.floor(v || 1)))}
onConfirm={logic.handleConfirmExchange}
onCancel={() => {
if (exchangeLoading) return;
setConfirmModalVisible(false);
logic.setSelectedProduct(null);
logic.setExchangeQuantity(1);
}}
/>
</div>
);
}