277 lines
12 KiB
TypeScript
277 lines
12 KiB
TypeScript
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>
|
||
);
|
||
}
|