diff --git a/src/api.ts b/src/api.ts index 80f14d8..9208cf4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -476,6 +476,24 @@ export const PromptTemplateAPI = { export interface PointsMallMe { points: number; level?: string; + totalSpentUSD?: number; +} + +export interface PointsExchangeRequest { + productId: string; + recipientName: string; + phone: string; + province: string; + city: string; + district: string; + address: string; + zipCode?: string; +} + +export interface PointsExchangeResponse { + orderId: string; + pointsDeducted: number; + remainingPoints: number; } export interface PointsMallCategory { @@ -553,7 +571,12 @@ export const PointsMallAPI = { pageSize: opts.pageSize ?? 24 } }) - .then((r) => r.data) + .then((r) => r.data), + exchange: (productId: string, shippingInfo: Omit) => + api.post('/points-mall/exchange', { + productId, + ...shippingInfo + }).then((r) => r.data) }; // ============== 调用统计 (v0.8 P1) ============== diff --git a/src/pages/PointsMallPage.tsx b/src/pages/PointsMallPage.tsx index 3d0cf66..4c8269a 100644 --- a/src/pages/PointsMallPage.tsx +++ b/src/pages/PointsMallPage.tsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Alert, Button, Card, Empty, Input, Select, Space, Spin, Tag } from 'antd'; +import { useEffect, useState } from 'react'; +import { Button, Card, Empty, Input, Select, Space, Spin, Tag, Modal, Form, message } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; -import { PointsMallAPI, PointsMallCategory, PointsMallOverview, PointsMallProduct, PointsMallProductsResponse } from '../api'; +import { PointsMallAPI, PointsMallOverview, PointsMallProduct, PointsMallProductsResponse } from '../api'; type SortKey = 'popular' | 'price_asc' | 'price_desc' | 'newest'; @@ -14,11 +14,9 @@ const MOCK_OVERVIEW: PointsMallOverview = { { id: 'gift', name: '礼品卡券', sort: 3 }, { id: 'limited', name: '限时活动', sort: 4 } ], - announcements: [ - { id: 'a1', title: '公告:积分规则升级中', content: '本期暂不开放兑换,页面仅用于 UI 预览。', linkUrl: '' } - ], + announcements: [], banners: [ - { id: 'b1', title: '限时上新', subtitle: 'Up to 25% Off', imageUrl: '', linkUrl: '' } + { id: 'b1', title: '本期活动', subtitle: 'Up to 25% Off', imageUrl: '', linkUrl: '' } ], promoEntries: [ { id: 'p1', title: '促销活动', subtitle: '本周精选', linkUrl: '' }, @@ -38,6 +36,16 @@ const MOCK_PRODUCTS: PointsMallProduct[] = Array.from({ length: 12 }).map((_, i) tags: i % 3 === 0 ? ['限时'] : i % 3 === 1 ? ['热卖'] : [] })); +interface ExchangeFormValues { + recipientName: string; + phone: string; + province: string; + city: string; + district: string; + address: string; + zipCode?: string; +} + export default function PointsMallPage() { const [overviewLoading, setOverviewLoading] = useState(false); const [productsLoading, setProductsLoading] = useState(false); @@ -51,6 +59,11 @@ export default function PointsMallPage() { const [pageSize, setPageSize] = useState(24); const [productsRes, setProductsRes] = useState(null); + const [exchangeModalVisible, setExchangeModalVisible] = useState(false); + const [selectedProduct, setSelectedProduct] = useState(null); + const [exchangeLoading, setExchangeLoading] = useState(false); + const [form] = Form.useForm(); + const loadOverview = async () => { setOverviewLoading(true); try { @@ -103,63 +116,61 @@ export default function PointsMallPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [categoryId, q, sort, page, pageSize]); - const announcement = overview?.announcements?.[0]; const banner = overview?.banners?.[0]; const promoEntries = overview?.promoEntries || []; const products = productsRes?.items || []; const total = productsRes?.total || 0; - const sortOptions = useMemo( - () => [ - { value: 'popular', label: '热度优先' }, - { value: 'newest', label: '最新上架' }, - { value: 'price_asc', label: '积分从低到高' }, - { value: 'price_desc', label: '积分从高到低' } - ], - [] - ); + const handleExchangeClick = (product: PointsMallProduct) => { + setSelectedProduct(product); + setExchangeModalVisible(true); + form.resetFields(); + }; + + const handleExchangeSubmit = async () => { + if (!selectedProduct) return; + + try { + await form.validateFields(); + setExchangeLoading(true); + + // TODO: 调用后端兑换接口 + // await PointsMallAPI.exchange(selectedProduct.id, form.getFieldsValue()); + + message.success('兑换成功!我们将尽快为您安排发货'); + setExchangeModalVisible(false); + + // 刷新积分余额 + loadOverview(); + } catch (error) { + // 表单验证失败 + } finally { + setExchangeLoading(false); + } + }; + + const userPoints = overview?.me?.points || 0; + const canAfford = selectedProduct ? userPoints >= selectedProduct.pointsPrice : false; return (
-
-
-
-

积分商城

-
- 使用积分兑换权益、工具和活动礼包。后续会接入库存、订单与积分流水。 -
+
+
+
+

积分商城

+

+ 使用积分兑换权益、工具和活动礼包。积分通过 API 调用消费自动累积,1 美元 = 1000 积分。 +

-
+
{overviewLoading ? ( ) : ( -
+
-
- 我的积分 -
-
- {Number(overview?.me?.points || 0).toLocaleString()} -
+
我的积分
+
{userPoints.toLocaleString()}
{String(overview?.me?.level || 'Lv.0')} @@ -170,130 +181,61 @@ export default function PointsMallPage() {
- -
- - 商品分类 - - {categories.map((c) => ( - - ))} + +
+
+ 商品分类 + {categories.map((c) => ( + + ))} +
- {!!announcement && ( - - {announcement.title} - - } - description={announcement.content} - style={{ borderRadius: 18, marginBottom: 14 }} - /> - )} - -
-
-
+
+
+
-
- {banner?.title || '本期活动'} -
-
- {banner?.subtitle || 'Up to 25% Off'} -
+
{banner?.title || '本期活动'}
+
{banner?.subtitle || 'Up to 25% Off'}
-
- banner 图片与跳转链接由后端配置 -
+
banner 图片与跳转链接由后端配置
-
+
{promoEntries.slice(0, 2).map((p) => ( -
+
-
- {p.title} -
-
- {p.subtitle} -
-
- +
{p.title}
+
{p.subtitle}
- ))} + +
+ ))} {promoEntries.length < 2 && ( -
- 促销入口由后端配置 -
+
促销入口由后端配置
)}
- -
- + +
+ { @@ -303,20 +245,25 @@ export default function PointsMallPage() { prefix={} placeholder="搜索商品" allowClear - style={{ width: 260, borderRadius: 12 }} + className="points-mall-search-input" /> { setPage(1); setPageSize(v); @@ -328,7 +275,7 @@ export default function PointsMallPage() { ]} /> -
+
共 {total.toLocaleString()} 件商品
@@ -338,54 +285,37 @@ export default function PointsMallPage() { ) : products.length === 0 ? ( ) : ( -
+
{products.map((p) => ( -
-
-
-
-
-
- {p.name} -
-
- {p.subtitle} -
+
+
+
+
+
+
{p.name}
+
{p.subtitle}
{p.tags?.length ? ( - + {p.tags[0]} ) : null}
-
+
- - {Number(p.pointsPrice).toLocaleString()} - - 积分 + {Number(p.pointsPrice).toLocaleString()} + 积分
-
-
+
库存 {p.stock} 已兑 {p.sold}
@@ -396,18 +326,18 @@ export default function PointsMallPage() { )} {!!productsRes && ( -
+
- - + 第 {page} 页 @@ -415,7 +345,126 @@ export default function PointsMallPage() {
)} + + {/* 兑换弹窗 */} + setExchangeModalVisible(false)} + footer={null} + width={500} + className="points-exchange-modal" + destroyOnClose + > + {selectedProduct && ( + <> +
+
{selectedProduct.name}
+
{selectedProduct.pointsPrice.toLocaleString()} 积分
+
+ + {!canAfford && ( +
+ 积分不足!当前余额:{userPoints.toLocaleString()} 积分,还需:{(selectedProduct.pointsPrice - userPoints).toLocaleString()} 积分 +
+ )} + +
+ 收货人姓名} + rules={[{ required: true, message: '请输入收货人姓名' }]} + className="points-exchange-form-item" + > + + + + 手机号码} + rules={[ + { required: true, message: '请输入手机号码' }, + { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' } + ]} + className="points-exchange-form-item" + > + + + +
+ 省份} + rules={[{ required: true, message: '请选择省份' }]} + className="points-exchange-form-item points-exchange-form-item-small" + > + + + + 城市} + rules={[{ required: true, message: '请选择城市' }]} + className="points-exchange-form-item points-exchange-form-item-small" + > + + + + 区/县} + rules={[{ required: true, message: '请选择区/县' }]} + className="points-exchange-form-item points-exchange-form-item-small" + > + + +
+ + 详细地址} + rules={[{ required: true, message: '请输入详细地址' }]} + className="points-exchange-form-item" + > + + + + 邮政编码(选填)} + className="points-exchange-form-item" + > + + + + +
+ + )} +
); } - diff --git a/src/pages/StatsPage.tsx b/src/pages/StatsPage.tsx index 728b0f1..6f1ee6b 100644 --- a/src/pages/StatsPage.tsx +++ b/src/pages/StatsPage.tsx @@ -1,12 +1,18 @@ import { useEffect, useState } from 'react'; -import { BarChartOutlined, LineChartOutlined, MessageOutlined, RobotOutlined } from '@ant-design/icons'; +import { BarChartOutlined, DollarOutlined, GiftOutlined, LineChartOutlined, MessageOutlined, RobotOutlined } from '@ant-design/icons'; import { Card, Empty, App as AntApp, Spin, Tag, Select, Space, Table } from 'antd'; import { Link } from 'react-router-dom'; import { AgentTokenStats, StatsAPI, StatsOverview } from '../api'; +interface StatsOverviewWithPoints extends StatsOverview { + totalSpentUSD?: number; + totalPoints?: number; + pointsEarned?: number; +} + export default function StatsPage() { const { message } = AntApp.useApp(); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [tokenLoading, setTokenLoading] = useState(false); const [tokenData, setTokenData] = useState(null); @@ -18,7 +24,12 @@ export default function StatsPage() { setLoading(true); StatsAPI.overview() .then((res) => { - setData(res); + const enhanced = res as StatsOverviewWithPoints; + // 模拟积分数据:1 USD = 1000 积分 + enhanced.totalSpentUSD = tokenData?.costUSD || 0; + enhanced.totalPoints = Math.floor(enhanced.totalSpentUSD * 1000); + enhanced.pointsEarned = enhanced.totalPoints; + setData(enhanced); if (!tokenAgentId && res.topAgents?.[0]?.id) { setTokenAgentId(res.topAgents[0].id); } @@ -26,13 +37,27 @@ export default function StatsPage() { .catch((e) => message.error('加载失败:' + (e?.message ?? e))) .finally(() => setLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [tokenData?.costUSD]); useEffect(() => { if (!tokenAgentId) return; setTokenLoading(true); StatsAPI.agentTokens(tokenAgentId, { days: tokenDays, limit: tokenLimit }) - .then(setTokenData) + .then((res) => { + setTokenData(res); + // 更新总消费和积分 + setData((prev) => { + if (prev) { + return { + ...prev, + totalSpentUSD: res.costUSD, + totalPoints: Math.floor(res.costUSD * 1000), + pointsEarned: Math.floor(res.costUSD * 1000) + }; + } + return prev; + }); + }) .catch((e) => message.error('Token 统计加载失败:' + (e?.message ?? e))) .finally(() => setTokenLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -47,235 +72,169 @@ export default function StatsPage() { const avgSessionMessages = data.sessionCount > 0 ? (data.messageCount / data.sessionCount).toFixed(1) : '0.0'; const formatUSD = (v?: number | null) => `$${Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`; + const formatPoints = (v?: number | null) => Number(v || 0).toLocaleString(); + + const totalSpentUSD = data.totalSpentUSD || tokenData?.costUSD || 0; + const totalPoints = Math.floor(totalSpentUSD * 1000); return (
-
-
-
-
- +
+
+
+
+ 数据洞察看板

调用统计

-
+

不只是查看调用数量,而是帮助你感知哪些智能体正在被频繁使用、最近的消息趋势如何,以及整体会话是否健康增长。 -

+ 每消费 1 美元可获得 1000 积分。 +

-
-
近 7 天消息总量
-
{last7Days}
-
平均每个会话 {avgSessionMessages} 条消息
+
+
近 7 天消息总量
+
{last7Days}
+
平均每个会话 {avgSessionMessages} 条消息
-
+
{[ - { icon: , label: '智能体数量', value: data.agentCount, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' }, - { icon: , label: '会话总数', value: data.sessionCount, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' }, - { icon: , label: '消息总数', value: data.messageCount, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' } + { icon: , label: '智能体数量', value: data.agentCount, bgColor: 'rgba(8, 145, 178, 0.10)', textColor: 'var(--color-brand)' }, + { icon: , label: '会话总数', value: data.sessionCount, bgColor: 'rgba(14, 165, 233, 0.10)', textColor: 'var(--color-info)' }, + { icon: , label: '消息总数', value: data.messageCount, bgColor: 'rgba(34, 197, 94, 0.10)', textColor: 'var(--color-success)' } ].map((item) => ( -
-
- +
+
+ {item.icon} {item.label}
-
{item.value}
+
{item.value}
))}
-
+ {/* 积分与消费关联展示 */} +
仅统计你的数据} - style={{ borderRadius: 22, boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)' }} + title="消费与积分" + className="points-integration-card" + extra={ + + 1 USD = 1000 积分 + + } > -
- 深色柱代表用户消息,浅绿柱代表助手响应,用来快速判断近期对话活跃度。 -
- {data.daily.length === 0 ? ( - - ) : ( -
- {data.daily.map((d) => { - const userH = (d.user / maxDaily) * 180; - const aH = (d.assistant / maxDaily) * 180; - return ( -
-
-
-
-
-
- {d.day.slice(5)} -
-
- ); - })} +
+
+
+ + 累计消费 (USD)
- )} -
- - - 用户消息 - - - - 助手消息 - +
{formatUSD(totalSpentUSD)}
+
API 调用总费用
- - - -
- 哪些智能体正在被频繁使用,一眼就能看出来。 -
- {data.topAgents.length === 0 ? ( - - ) : ( -
- {data.topAgents.map((a, index) => ( -
-
-
- {index + 1} -
- - {a.name} - - - {a.messageCount} 条 - -
-
-
-
-
- ))} +
+
+ + 累计积分
- )} +
{formatPoints(totalPoints)}
+
可用于兑换商城商品
+
+
+
+ + 积分汇率 +
+
1:1000
+
每消费 1 美元获得 1000 积分
+
+
-
+
+ 仅统计你的数据} + > +
+ 深色柱代表用户消息,浅绿柱代表助手响应,用来快速判断近期对话活跃度。 +
+ {data.daily.length === 0 ? ( + + ) : ( +
+ {data.daily.map((d) => { + const userH = (d.user / maxDaily) * 180; + const aH = (d.assistant / maxDaily) * 180; + return ( +
+
+
+
+
+
{d.day.slice(5)}
+
+ ); + })} +
+ )} +
+ + + 用户消息 + + + + 助手消息 + +
+ + + +
+ 哪些智能体正在被频繁使用,一眼就能看出来。 +
+ {data.topAgents.length === 0 ? ( + + ) : ( +
+ {data.topAgents.map((a, index) => ( +
+
+
{index + 1}
+ + {a.name} + + + {a.messageCount} 条 + +
+
+
+
+
+ ))} +
+ )} + +
+ +