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

277 lines
12 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, 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';
interface Props {
logic: PointsMallPageLogicOutput;
}
export default function PointsMallPageH5({ logic }: Props) {
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 h5-page-container" style={{ paddingLeft: 8, paddingRight: 8 }}>
<div className="points-mall-hero h5-points-mall-hero" style={{ padding: '16px 12px' }}>
<div className="points-mall-header h5-points-mall-header" style={{ flexDirection: 'column', gap: 12 }}>
<div className="points-mall-title-section">
<h1 className="page-title stats-page-title h5-page-title" style={{ fontSize: 24, marginBottom: 6 }}></h1>
<p className="page-subtitle stats-page-subtitle h5-page-subtitle" style={{ fontSize: 13, lineHeight: 1.5 }}>
使<br /> API 1 = 1000
</p>
</div>
<div className="points-mall-balance-card h5-points-mall-balance-card" style={{ width: '100%', padding: 12 }}>
{overviewLoading ? (
<Spin />
) : (
<div className="points-balance-row h5-points-balance-row" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 8 }}>
<div style={{ width: '100%' }}>
<div className="points-balance-label" style={{ fontSize: 12 }}></div>
<div className="points-balance-value" style={{ fontSize: 28 }}>{userPoints.toLocaleString()}</div>
<div className="points-balance-subtext" style={{ fontSize: 12 }}>
${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)', fontSize: 12 }}
>
{String(overview?.me?.level || 'Lv.0')}
</Tag>
</div>
)}
</div>
</div>
</div>
<Card className="points-mall-category-card h5-points-mall-category-card" bodyStyle={{ padding: 0 }}>
<div className="points-mall-category-body">
<div className="points-mall-category-row h5-points-mall-category-row" style={{ flexWrap: 'wrap', gap: 6 }}>
<span className="points-mall-category-label" style={{ width: '100%', marginBottom: 8 }}></span>
{categories.map((c) => (
<Button
key={c.id}
size="small"
type={c.id === categoryId ? 'primary' : 'default'}
style={{ borderRadius: 999, fontSize: 12 }}
onClick={() => {
setPage(1);
setCategoryId(c.id);
}}
>
{c.name}
</Button>
))}
</div>
</div>
</Card>
<div className="points-mall-banner-section h5-points-mall-banner-section" style={{ flexDirection: 'column', gap: 12 }}>
<div className="points-mall-banner-card h5-points-mall-banner-card" style={{ width: '100%' }}>
<div className="points-mall-banner-header" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 8 }}>
<div>
<div className="points-mall-banner-title" style={{ fontSize: 16 }}>{banner?.title || '本期活动'}</div>
<div className="points-mall-banner-subtitle" style={{ fontSize: 12 }}>{banner?.subtitle || 'Up to 25% Off'}</div>
</div>
<Button type="primary" style={{ borderRadius: 10, height: 36, fontWeight: 600, width: '100%' }}>
</Button>
</div>
<div className="points-mall-banner-footer">banner </div>
</div>
<div className="points-mall-promo-grid h5-points-mall-promo-grid" style={{ gridTemplateColumns: '1fr', gap: 8 }}>
{promoEntries.slice(0, 2).map((p) => (
<div key={p.id} className="points-mall-promo-card" style={{ padding: 12 }}>
<div style={{ minWidth: 0 }}>
<div className="points-mall-promo-title" style={{ fontSize: 14 }}>{p.title}</div>
<div className="points-mall-promo-subtitle" style={{ fontSize: 12 }}>{p.subtitle}</div>
</div>
<Button size="small" className="points-mall-exchange-btn" style={{ marginTop: 8 }}>
</Button>
</div>
))}
{promoEntries.length < 2 && <div className="points-mall-promo-empty"></div>}
</div>
</div>
<Card className="stats-page-chart-card h5-stats-page-chart-card">
<div className="points-mall-filters-row h5-points-mall-filters-row" style={{ flexDirection: 'column', gap: 10 }}>
<div style={{ width: '100%' }}>
<Input
value={q}
onChange={(e) => {
setPage(1);
setQ(e.target.value);
}}
prefix={<SearchOutlined />}
placeholder="搜索商品"
allowClear
className="points-mall-search-input"
style={{ height: 36 }}
/>
</div>
<div style={{ display: 'flex', gap: 8, width: '100%' }}>
<Select
value={sort}
className="points-mall-filter-select"
style={{ flex: 1 }}
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"
style={{ width: 80 }}
onChange={(v) => {
setPage(1);
setPageSize(v);
}}
options={[
{ value: 12, label: '每页 12' },
{ value: 24, label: '每页 24' },
]}
/>
</div>
<div className="points-mall-total-text" style={{ textAlign: 'left' }}> {total.toLocaleString()} </div>
</div>
{productsLoading ? (
<Spin style={{ marginTop: 30, display: 'block' }} />
) : products.length === 0 ? (
<Empty description="暂无商品" style={{ marginTop: 40 }} />
) : (
<div className="points-mall-products-grid h5-points-mall-products-grid" style={{ gridTemplateColumns: '1fr', gap: 12 }}>
{products.map((p: PointsMallProduct) => (
<div key={p.id} className="points-mall-product-card h5-points-mall-product-card" style={{ padding: 12 }}>
<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" style={{ fontSize: 15 }}>{p.name}</div>
<div className="points-mall-product-desc" style={{ fontSize: 12 }}>{p.subtitle}</div>
</div>
{p.tags?.length ? (
<Tag bordered={false} className="points-mall-product-tag" style={{ fontSize: 11 }}>
{p.tags[0]}
</Tag>
) : null}
</div>
<div className="points-mall-product-price-row" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 8 }}>
<div>
<span className="points-mall-product-price" style={{ fontSize: 20 }}>{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)}
style={{ width: '100%' }}
>
{userPoints < p.pointsPrice ? '积分不足' : '兑换'}
</Button>
</div>
<div className="points-mall-product-footer" style={{ fontSize: 12 }}>
<span> {p.stock}</span>
<span> {p.sold}</span>
</div>
</div>
</div>
))}
</div>
)}
{!!total && (
<div className="points-mall-pagination h5-points-mall-pagination" style={{ marginTop: 16 }}>
<Space size={8}>
<Button disabled={page <= 1} onClick={() => setPage((v) => Math.max(1, v - 1))} className="points-mall-exchange-btn" size="small">
</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" size="small">
</Button>
</Space>
</div>
)}
</Card>
<ExchangeModal
open={exchangeModalVisible}
product={selectedProduct}
quantity={exchangeQuantity}
expiresAt={pendingExpiresAt}
loading={exchangeLoading}
onSubmit={logic.handleExchangeSubmit}
onCancel={() => {
setExchangeModalVisible(false);
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>
);
}