diff --git a/apps/web/src/pages/points/Packages.tsx b/apps/web/src/pages/points/Packages.tsx index a4d7e71ee..58f3d4354 100644 --- a/apps/web/src/pages/points/Packages.tsx +++ b/apps/web/src/pages/points/Packages.tsx @@ -1,174 +1,144 @@ /** - * 积分充值页(/app/points/recharge) - * 单独展示积分包,供入口直接跳转使用 - * 主 Plans 页面也有充值区,这里提供独立 URL 方便从"积分不足"弹窗跳转 + * 积分充值页面 + * 展示充值包列表 + 创建充值订单 + * + * 注意:当前后端 POST /points/recharge 返回 pay_params={}(支付通道未接入), + * 前端可完成订单创建 UI,但发起支付需等待支付通道接入。 */ import React, { useEffect, useState } from "react" -import { Card, Col, Row, Button, Tag, Typography, Space, Modal, message, Tooltip } from "antd" -import { ThunderboltOutlined, SafetyCertificateOutlined, CrownFilled } from "@ant-design/icons" -import { useNavigate } from "react-router-dom" -import PageHead from "@/components/layout/PageHead" +import { Card, Button, Tag, message, Spin, Alert } from "antd" +import { ThunderboltOutlined, CheckCircleFilled } from "@ant-design/icons" import { usePointsStore } from "@/store/pointsStore" -import { createPointsOrder, getPointsPackages } from "@/api/points" -import type { PointsPackage } from "@/api/points/types" -import "./Points.css" +import { getPointsPackages, createPointsOrder } from "@/api/points" +import { getDiscountPriceCents } from "@/api/points/types" +import type { PointsPackage, PointsPackagesResponse } from "@/api/points/types" +import styles from "./Points.css" -const { Title, Text, Paragraph } = Typography - -const PointsRecharge: React.FC = () => { - const navigate = useNavigate() - const { balance, init } = usePointsStore() - const [packages, setPackages] = useState([]) - const [buying, setBuying] = useState(null) +const PointsPackagesPage: React.FC = () => { + const { init, refreshBalance } = usePointsStore() + const [pkgResp, setPkgResp] = useState(null) + const [loading, setLoading] = useState(true) + const [ordering, setOrdering] = useState(null) + const [selectedCode, setSelectedCode] = useState(null) useEffect(() => { init() - getPointsPackages() - .then((r) => setPackages(r.packages)) - .catch(() => {}) + loadPackages() }, [init]) - const getPackPrice = (pkg: PointsPackage): number => { - const mt = balance?.member_type ?? "free" - type DiscountKey = - | "discounted_price_for_free" - | "discounted_price_for_monthly" - | "discounted_price_for_quarterly" - | "discounted_price_for_yearly" - const key = `discounted_price_for_${mt}` as DiscountKey - return pkg[key] ?? pkg.price - } - - const handleBuy = async (pkg: PointsPackage) => { + const loadPackages = async () => { try { - setBuying(pkg.id) - const order = await createPointsOrder({ package_id: pkg.id }) - Modal.info({ - title: "支付功能开发中", - icon: , - content: ( -
- - 订单已创建({order.id.slice(0, 16)}…),金额{" "} - ¥{(order.price_cents / 100).toFixed(2).replace(/\.00$/, "")}。 - 微信/支付宝支付正在接入中。 - - - 现阶段所有功能免费体验,积分仅为演示数据。 - -
- ), - okText: "知道了", - }) - } catch (e) { - const err = e as { message?: string } - message.error(err?.message ?? "下单失败") + setLoading(true) + const data = await getPointsPackages() + setPkgResp(data) + if (data.packages.length > 0) setSelectedCode(data.packages[1]?.code || data.packages[0].code) + } catch (err) { + message.error("加载充值包失败") } finally { - setBuying(null) + setLoading(false) } } + const handleRecharge = async (pkg: PointsPackage) => { + try { + setOrdering(pkg.code) + const order = await createPointsOrder({ package_id: pkg.code }) + message.success(`订单已创建:${order.id}(支付通道待接入)`) + // pay_params 目前是 {},支付通道接入后再处理跳转 + if (order.pay_params && Object.keys(order.pay_params).length > 0) { + // TODO: 支付通道接入后发起支付 + console.log("[recharge] pay_params:", order.pay_params) + } + await refreshBalance() + } catch (err) { + message.error((err as Error).message || "充值失败,请稍后再试") + } finally { + setOrdering(null) + } + } + + if (loading) + return ( +
+ +
+ ) + if (!pkgResp) return
充值包加载失败
+ + const userDiscount = pkgResp.user_discount + const hasDiscount = userDiscount != null && userDiscount < 1 + return ( -
- +

积分充值

- actions={ - - {!balance?.is_member && ( - - )} - - - } - /> - - {balance && ( - - -
- 当前可用积分 -
- {balance.balance.toLocaleString()} -
-
-
-
+ {hasDiscount && ( + )} - 选择积分包 - - {packages.map((pkg) => { - const price = getPackPrice(pkg) - const discount = price < pkg.price ? Math.round((1 - price / pkg.price) * 100) : 0 - const unit = price / pkg.points +
+ {pkgResp.packages.map((pkg) => { + const isSelected = selectedCode === pkg.code + const discountPrice = getDiscountPriceCents(pkg, userDiscount) + const isDiscounted = discountPrice < pkg.price_cents + const priceYuan = (discountPrice / 100).toFixed(2).replace(/\.00$/, "") + const originalYuan = (pkg.price_cents / 100).toFixed(2).replace(/\.00$/, "") + return ( - - 0 ? "has-discount" : ""}`} - hoverable + setSelectedCode(pkg.code)} + > + {isSelected && } +
+ + {pkg.points} + 积分 +
+
{pkg.name}
+
+ ¥ + {priceYuan} + {isDiscounted && ¥{originalYuan}} +
+ {pkg.unit_price < 0.1 && ( + + 超值 + + )} + -
- + 立即充值 + +
) })} - +
- - 积分消耗说明 -
    -
  • 智能混剪:3 积分/条(≤30s),每加 30s +1 积分
  • -
  • AI 配音 / 声音克隆合成:1 积分/分钟
  • -
  • AI 数字人:15 积分/分钟
  • -
  • 抖音链接提取 / AI 改写 / AI 标题 / AI 封面:1~2 积分/次
  • -
  • 声音克隆训练:免费
  • -
  • 免费用户每日 2 条混剪免费,其余 AI 功能消耗为会员价 ×1.15
  • -
- - - 所有规则以页面实际显示为准 - - -
+
) } -export default PointsRecharge -export const Component = PointsRecharge +export default PointsPackagesPage