chore(points): align types/api with final backend contract - apps/web/src/pages/points/Center.tsx
This commit is contained in:
@@ -1,309 +1,154 @@
|
||||
/**
|
||||
* 积分中心主页(/app/points)
|
||||
* 展示余额、会员信息、本月统计、快捷入口
|
||||
* 积分中心首页
|
||||
* 展示:
|
||||
* - 当前积分余额 + 会员状态(来自 balance + membership)
|
||||
* - 每日免费额度(来自 dailyUsage)
|
||||
* - 快捷入口(充值 / 消费记录 / 订阅)
|
||||
*/
|
||||
import React, { useEffect } from "react"
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Statistic,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Progress,
|
||||
List,
|
||||
Avatar,
|
||||
Empty,
|
||||
} from "antd"
|
||||
import { Card, Button, Space, Statistic, Tag, Progress, Alert } from "antd"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
CrownFilled,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
CrownOutlined,
|
||||
HistoryOutlined,
|
||||
WalletOutlined,
|
||||
FileTextOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusCircleOutlined,
|
||||
GiftOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import PageHead from "@/components/layout/PageHead"
|
||||
import { usePointsStore } from "@/store/pointsStore"
|
||||
import "./Points.css"
|
||||
import styles from "./Points.css"
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const SOURCE_NAME: Record<string, string> = {
|
||||
recharge: "充值",
|
||||
task_reward: "任务奖励",
|
||||
ai_voice: "AI 配音",
|
||||
ai_digital_human: "AI 数字人",
|
||||
ai_video: "智能混剪",
|
||||
voice_clone_train: "声音克隆训练",
|
||||
voice_clone_synth: "声音克隆合成",
|
||||
douyin_extract: "抖音提取",
|
||||
ai_rewrite: "AI 改写",
|
||||
ai_title: "AI 标题",
|
||||
ai_cover: "AI 封面",
|
||||
subscription_bonus: "会员赠送",
|
||||
admin_adjust: "管理员调整",
|
||||
refund: "失败退还",
|
||||
const MEMBER_LABEL: Record<string, string> = {
|
||||
monthly: "月卡会员",
|
||||
quarterly: "季卡会员",
|
||||
yearly: "年卡会员",
|
||||
}
|
||||
|
||||
const PointsCenter: React.FC = () => {
|
||||
const PointsCenterPage: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { balance, subscription, init, loading } = usePointsStore()
|
||||
const { balance, dailyUsage, membership, subscription, rules, init } = usePointsStore()
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [init])
|
||||
|
||||
const bal = balance?.balance ?? 0
|
||||
const earned = balance?.total_earned ?? 0
|
||||
const spent = balance?.total_spent ?? 0
|
||||
const isMember = !!balance?.is_member
|
||||
const freeUsed = balance?.free_clips_used ?? 0
|
||||
const freeLimit = balance?.free_clips_limit ?? 2
|
||||
const freeRemain = balance?.free_clips_remaining ?? (isMember ? 0 : 2)
|
||||
|
||||
// 近 5 条流水 mock(实际从 transactions 页加载)
|
||||
const recentTx = [
|
||||
{ type: "spend", source: "ai_voice", amount: 1, time: "今天 10:30" },
|
||||
{ type: "spend", source: "ai_video", amount: 3, time: "今天 09:15" },
|
||||
{ type: "earn", source: "task_reward", amount: 20, time: "昨天" },
|
||||
]
|
||||
const isMember = membership?.is_member ?? balance?.is_member ?? false
|
||||
const memberKey =
|
||||
membership?.member_type ??
|
||||
(subscription?.plan_id && subscription.plan_id !== "free" ? subscription.plan_id : null)
|
||||
const memberLabel = memberKey ? MEMBER_LABEL[memberKey] || "" : ""
|
||||
const freeLimit = dailyUsage?.free_clips_limit ?? 0
|
||||
const freeUsed = dailyUsage?.free_clips_used ?? 0
|
||||
const freeRemain = dailyUsage?.free_clips_remaining ?? 0
|
||||
const freePercent = freeLimit > 0 ? Math.round((freeUsed / freeLimit) * 100) : 0
|
||||
const freeMultiplier = rules?.free_user_multiplier ?? 1.15
|
||||
|
||||
return (
|
||||
<div className="xx-points-page">
|
||||
<PageHead
|
||||
title="积分中心"
|
||||
description="管理积分余额、查看流水、充值使用"
|
||||
<div className={styles.pointsPage}>
|
||||
<h2 className={styles.pageTitle}>积分中心</h2>
|
||||
|
||||
actions={
|
||||
<Space>
|
||||
<Button icon={<FileTextOutlined />} onClick={() => navigate("/app/points/rules")}>
|
||||
积分规则
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<WalletOutlined />}
|
||||
onClick={() => navigate("/app/subscription")}
|
||||
>
|
||||
充值积分
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 顶部大卡 */}
|
||||
<Card className="xx-points-hero" bordered={false}>
|
||||
<Row gutter={24} align="middle">
|
||||
<Col xs={24} md={14}>
|
||||
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
||||
<Text type="secondary" style={{ color: "rgba(255,255,255,0.8)" }}>
|
||||
当前可用积分
|
||||
</Text>
|
||||
<div className="xx-points-hero-balance">
|
||||
<ThunderboltOutlined />
|
||||
<span>{loading ? "…" : bal.toLocaleString()}</span>
|
||||
</div>
|
||||
<Space size={8} wrap>
|
||||
{isMember ? (
|
||||
<Tag color="gold" icon={<CrownFilled />} style={{ padding: "4px 10px" }}>
|
||||
{subscription?.member_type === "yearly"
|
||||
? "年卡"
|
||||
: subscription?.member_type === "quarterly"
|
||||
? "季卡"
|
||||
: "月卡"}
|
||||
会员
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag
|
||||
style={{
|
||||
padding: "4px 10px",
|
||||
background: "rgba(255,255,255,0.2)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
免费会员
|
||||
</Tag>
|
||||
)}
|
||||
{balance?.member_expires_at && (
|
||||
<Text style={{ color: "rgba(255,255,255,0.85)", fontSize: 12 }}>
|
||||
到期 {new Date(balance.member_expires_at).toLocaleDateString("zh-CN")}
|
||||
</Text>
|
||||
)}
|
||||
{!isMember && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => navigate("/app/subscription")}
|
||||
style={{ background: "#fff", color: "#7c3aed", borderColor: "#fff" }}
|
||||
>
|
||||
<CrownFilled /> 升级会员享 8 折
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} md={10}>
|
||||
{/* 今日免费额度 */}
|
||||
{!isMember && freeLimit > 0 && (
|
||||
<div className="xx-points-free-quota">
|
||||
<div className="xx-points-free-title">
|
||||
<InfoCircleOutlined /> 今日免费混剪
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((freeUsed / freeLimit) * 100)}
|
||||
strokeColor={{ "0%": "#f59e0b", "100%": "#ef4444" }}
|
||||
format={() => `${freeUsed}/${freeLimit} 条`}
|
||||
/>
|
||||
<Text style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
|
||||
剩余 {freeRemain} 条免费混剪,超出部分消耗积分
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
{/* 余额卡片 */}
|
||||
<Card className={styles.balanceCard}>
|
||||
<div className={styles.balanceMain}>
|
||||
<Statistic
|
||||
title="当前积分"
|
||||
value={membership?.points_balance ?? balance?.balance ?? 0}
|
||||
prefix={<ThunderboltOutlined style={{ color: "#faad14" }} />}
|
||||
valueStyle={{ color: "#faad14", fontSize: 36, fontWeight: 700 }}
|
||||
/>
|
||||
{isMember && memberLabel && (
|
||||
<Tag color="gold" icon={<CrownOutlined />} className={styles.memberTag}>
|
||||
{memberLabel}
|
||||
{membership?.max_resolution ? ` · ${membership.max_resolution}` : ""}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusCircleOutlined />}
|
||||
onClick={() => navigate("/points/recharge")}
|
||||
>
|
||||
立即充值
|
||||
</Button>
|
||||
<Button icon={<HistoryOutlined />} onClick={() => navigate("/points/transactions")}>
|
||||
消费记录
|
||||
</Button>
|
||||
<Button icon={<CrownOutlined />} onClick={() => navigate("/subscription")}>
|
||||
{isMember ? "管理订阅" : "开通会员"}
|
||||
</Button>
|
||||
</Space>
|
||||
{balance?.member_expires_at && isMember && (
|
||||
<div className={styles.expireTip}>
|
||||
会员到期时间:{new Date(balance.member_expires_at).toLocaleDateString("zh-CN")}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 统计 */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="累计获得"
|
||||
value={earned}
|
||||
prefix={<ArrowUpOutlined style={{ color: "#10b981" }} />}
|
||||
valueStyle={{ color: "#10b981" }}
|
||||
{/* 每日免费额度卡片 */}
|
||||
<Card title="🎁 每日免费额度" className={styles.freeCard}>
|
||||
{!isMember && freeLimit > 0 ? (
|
||||
<>
|
||||
<Progress
|
||||
percent={freePercent}
|
||||
strokeColor={{ "0%": "#52c41a", "100%": "#faad14" }}
|
||||
format={() => `${freeUsed}/${freeLimit}`}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="累计消耗"
|
||||
value={spent}
|
||||
prefix={<ArrowDownOutlined style={{ color: "#ef4444" }} />}
|
||||
valueStyle={{ color: "#ef4444" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="可用积分"
|
||||
value={bal}
|
||||
prefix={<ThunderboltOutlined style={{ color: "#8b5cf6" }} />}
|
||||
valueStyle={{ color: "#8b5cf6" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={isMember ? "会员等级" : "今日免费剩余"}
|
||||
value={isMember ? (subscription?.member_type_name ?? "付费会员") : `${freeRemain} 条`}
|
||||
prefix={<CrownFilled style={{ color: "#f59e0b" }} />}
|
||||
valueStyle={{ color: "#f59e0b" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className={styles.freeTip}>
|
||||
<GiftOutlined /> 今日还剩 <b>{freeRemain}</b> 次免费生成额度
|
||||
{dailyUsage?.reset_at && (
|
||||
<span className={styles.resetTime}>
|
||||
(
|
||||
{new Date(dailyUsage.reset_at).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}{" "}
|
||||
重置)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : isMember ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="会员享有更高免费额度"
|
||||
description={`每日免费 ${subscription ? "(见会员权益)" : ""}生成次数,积分消耗享折扣`}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="暂无可用免费额度"
|
||||
description="充值或开通会员解锁更多权益"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 快捷入口 & 最近流水 */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 8 }}>
|
||||
<Col xs={24} md={14}>
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<HistoryOutlined />
|
||||
最近流水
|
||||
</Space>
|
||||
}
|
||||
extra={<a onClick={() => navigate("/app/points/transactions")}>查看全部 →</a>}
|
||||
>
|
||||
{recentTx.length === 0 ? (
|
||||
<Empty description="暂无积分流水" />
|
||||
) : (
|
||||
<List
|
||||
dataSource={recentTx}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
size="small"
|
||||
style={{
|
||||
background: item.type === "earn" ? "#d1fae5" : "#fee2e2",
|
||||
color: item.type === "earn" ? "#059669" : "#dc2626",
|
||||
}}
|
||||
icon={item.type === "earn" ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
||||
/>
|
||||
}
|
||||
title={SOURCE_NAME[item.source] ?? item.source}
|
||||
description={item.time}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
color: item.type === "earn" ? "#10b981" : "#ef4444",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{item.type === "earn" ? "+" : "-"}
|
||||
{item.amount}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={10}>
|
||||
<Card title="快捷操作">
|
||||
<Space direction="vertical" style={{ width: "100%" }} size={10}>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
type="primary"
|
||||
icon={<WalletOutlined />}
|
||||
onClick={() => navigate("/app/subscription")}
|
||||
>
|
||||
充值积分
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
icon={<HistoryOutlined />}
|
||||
onClick={() => navigate("/app/points/transactions")}
|
||||
>
|
||||
积分明细
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
icon={<CrownFilled />}
|
||||
onClick={() => navigate("/app/subscription")}
|
||||
>
|
||||
{isMember ? "续费/升级会员" : "升级付费会员"}
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => navigate("/app/points/rules")}
|
||||
>
|
||||
积分消耗规则
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* 快捷提示 */}
|
||||
{!isMember && rules && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`非会员积分消耗倍率 ×${freeMultiplier},开通会员可享折扣`}
|
||||
action={
|
||||
<Button size="small" type="primary" onClick={() => navigate("/subscription")}>
|
||||
开通会员
|
||||
</Button>
|
||||
}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 累计统计 */}
|
||||
<Card title="📊 累计统计" className={styles.statsCard}>
|
||||
<div className={styles.statsGrid}>
|
||||
<Statistic title="累计获得" value={balance?.total_earned ?? 0} suffix="积分" />
|
||||
<Statistic title="累计消耗" value={balance?.total_spent ?? 0} suffix="积分" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PointsCenter
|
||||
export const Component = PointsCenter
|
||||
export default PointsCenterPage
|
||||
|
||||
Reference in New Issue
Block a user