chore(points): align types/api with final backend contract - apps/web/src/components/common/PointsBadge/index.tsx
This commit is contained in:
@@ -1,114 +1,58 @@
|
||||
/**
|
||||
* Header 右上角的积分徽章(💎 580)
|
||||
* - 余额 <10 时橙色告警
|
||||
* - 点击弹出 Popover:余额、会员信息、充值入口、积分明细入口
|
||||
* 头部积分徽章组件
|
||||
* - 展示当前积分余额
|
||||
* - 会员标签(基于 membership 聚合信息或 subscription.plan_id 判断)
|
||||
* - 点击跳转积分中心
|
||||
*/
|
||||
import React, { useEffect } from "react"
|
||||
import { Popover, Button, Tag, Space, Typography, Badge } from "antd"
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
CrownOutlined,
|
||||
RightOutlined,
|
||||
WarningOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import React from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Tooltip, Typography } from "antd"
|
||||
import { CrownOutlined, StarFilled } from "@ant-design/icons"
|
||||
import { usePointsStore } from "@/store/pointsStore"
|
||||
import "./PointsBadge.css"
|
||||
import styles from "./PointsBadge.module.css"
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
const { Text } = Typography
|
||||
|
||||
/** 会员类型标签映射(基于 GET /points/subscription/membership 返回的 member_type 字符串) */
|
||||
const MEMBER_LABEL: Record<string, string> = {
|
||||
monthly: "月卡会员",
|
||||
quarterly: "季卡会员",
|
||||
yearly: "年卡会员",
|
||||
}
|
||||
|
||||
const MEMBER_COLOR: Record<string, string> = {
|
||||
monthly: "#fa8c16",
|
||||
quarterly: "#eb2f96",
|
||||
yearly: "#fadb14",
|
||||
}
|
||||
|
||||
const PointsBadge: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { balance, init, loading } = usePointsStore()
|
||||
const { balance, membership, subscription } = usePointsStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (!balance) init()
|
||||
}, [balance, init])
|
||||
|
||||
const bal = balance?.balance ?? 0
|
||||
const lowBalance = bal > 0 && bal < 10
|
||||
const zero = bal === 0
|
||||
const isMember = !!balance?.is_member
|
||||
const memberLabel = isMember
|
||||
? balance?.member_type === "yearly"
|
||||
? "年卡会员"
|
||||
: balance?.member_type === "quarterly"
|
||||
? "季卡会员"
|
||||
: balance?.member_type === "monthly"
|
||||
? "月卡会员"
|
||||
: "付费会员"
|
||||
: "免费会员"
|
||||
|
||||
const popContent = (
|
||||
<div className="xx-points-popover">
|
||||
<div className="xx-points-popover-top">
|
||||
<div className="xx-points-popover-balance">
|
||||
<ThunderboltOutlined className="xx-points-icon" />
|
||||
<span className="xx-points-value">{loading ? "…" : bal}</span>
|
||||
<span className="xx-points-unit">积分</span>
|
||||
</div>
|
||||
<Tag color={isMember ? "gold" : "default"} icon={<CrownOutlined />}>
|
||||
{memberLabel}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{(zero || lowBalance) && (
|
||||
<Paragraph type="warning" className="xx-points-warn">
|
||||
<WarningOutlined /> 积分不足,充值后可继续使用 AI 功能
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{balance?.member_expires_at && (
|
||||
<Text type="secondary" className="xx-points-expire">
|
||||
会员到期:{new Date(balance.member_expires_at).toLocaleDateString("zh-CN")}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="xx-points-popover-stats">
|
||||
<div>
|
||||
<Text type="secondary">累计获得</Text>
|
||||
<div className="xx-points-stat-val">+{balance?.total_earned ?? 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">累计消耗</Text>
|
||||
<div className="xx-points-stat-val">-{balance?.total_spent ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space direction="vertical" style={{ width: "100%" }} size={8}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
block
|
||||
onClick={() => navigate("/app/points")}
|
||||
>
|
||||
充值积分
|
||||
</Button>
|
||||
<Button block onClick={() => navigate("/app/points/transactions")}>
|
||||
积分明细
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
{!isMember && (
|
||||
<Button block type="link" onClick={() => navigate("/app/subscription")}>
|
||||
<CrownOutlined /> 升级会员享折扣
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
const points = membership?.points_balance ?? balance?.balance ?? 0
|
||||
// 优先用 membership.member_type 判定会员状态;否则用 subscription.plan_id
|
||||
const memberKey: string | null =
|
||||
membership?.member_type ??
|
||||
(subscription?.plan_id && subscription.plan_id !== "free" ? subscription.plan_id : null)
|
||||
const isMember = membership?.is_member ?? balance?.is_member ?? false
|
||||
const memberLabel = memberKey ? MEMBER_LABEL[memberKey] || "" : ""
|
||||
const memberColor = memberKey ? MEMBER_COLOR[memberKey] || "#fa8c16" : "#fa8c16"
|
||||
|
||||
return (
|
||||
<Popover content={popContent} placement="bottomRight" trigger="click" arrow={false}>
|
||||
<button
|
||||
type="button"
|
||||
className={`xx-points-badge ${zero ? "zero" : lowBalance ? "low" : ""}`}
|
||||
>
|
||||
<Badge dot={zero} color={zero ? "#faad14" : undefined}>
|
||||
<ThunderboltOutlined className="xx-points-badge-icon" />
|
||||
</Badge>
|
||||
<span className="xx-points-badge-val">{loading ? "…" : bal}</span>
|
||||
</button>
|
||||
</Popover>
|
||||
<div className={styles.pointsBadge} onClick={() => navigate("/points")}>
|
||||
<StarFilled className={styles.pointsIcon} />
|
||||
<Text strong className={styles.pointsText}>
|
||||
{points}
|
||||
</Text>
|
||||
{isMember && memberLabel && (
|
||||
<Tooltip title={`${memberLabel} · ${membership?.max_resolution ?? ""}`}>
|
||||
<span className={styles.memberBadge} style={{ color: memberColor }}>
|
||||
<CrownOutlined /> {memberLabel}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user