Refactor Power Management component to enhance user experience with improved power package and consumption record management. Introduce new API interfaces for power statistics and consumption records, and update UI elements for better navigation and interaction.
This commit is contained in:
46
Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts
Normal file
46
Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
// 算力包套餐类型(仅 buy-power 页面用到类型定义,须保留)
|
||||
export interface PowerPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
tokens: number; // 算力点数
|
||||
price: number; // 价格(分)
|
||||
originalPrice: number; // 原价(分)
|
||||
unitPrice: number; // 单价
|
||||
discount: number; // 折扣百分比
|
||||
isTrial: number; // 是否试用套餐
|
||||
isRecommend: number; // 是否推荐
|
||||
isHot: number; // 是否热门
|
||||
isVip: number; // 是否VIP
|
||||
features: string[]; // 功能特性
|
||||
status: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
// 获取套餐列表
|
||||
export function getTaocanList(): Promise<{ list: PowerPackage[] }> {
|
||||
return request("/v1/tokens/list", {}, "GET");
|
||||
}
|
||||
|
||||
export interface BuyPackageParams {
|
||||
/**
|
||||
* 二选一
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
* 二选一 自定义购买金额
|
||||
*/
|
||||
price?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
// 购买套餐
|
||||
export function buyPackage(params: BuyPackageParams) {
|
||||
return request("/v1/tokens/pay", params, "POST");
|
||||
}
|
||||
|
||||
// 自定义购买算力
|
||||
export function buyCustomPower(params: { amount: number }) {
|
||||
return request("/v1/power/buy-custom", params, "POST");
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// 购买算力包页面样式
|
||||
.buyPowerPage {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 80px; // 为底部按钮留空间
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
// 套餐列表
|
||||
.packageList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.packageCard {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e5e5e5;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.packageCardActive {
|
||||
border-color: #1890ff;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.packageHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.packageTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.packageName {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.packageTag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-orange {
|
||||
background: #fff7e6;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.tag-blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tag-green {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.tag-purple {
|
||||
background: #f9f0ff;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.packagePrice {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.currentPrice {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1890ff;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.originalPrice {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.packageTokens {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.packageMeta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.unitPrice,
|
||||
.discount {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.unitPriceValue,
|
||||
.discountValue {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.packageFeatures {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.featureItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.featureIcon {
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 自定义购买卡片
|
||||
.customCard {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.customHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.customIcon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.customTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.customContent {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.customLabel {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
// 安全保障卡片
|
||||
.securityCard {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.securityHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.securityIcon {
|
||||
font-size: 20px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.securityTitle {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.securityList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.securityItem {
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.footer {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.buyButton {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #d9d9d9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
264
Cunkebao/src/pages/mobile/mine/recharge/buy-power/index.tsx
Normal file
264
Cunkebao/src/pages/mobile/mine/recharge/buy-power/index.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Button, Toast, Dialog } from "antd-mobile";
|
||||
import { Input } from "antd";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
CheckCircleOutlined,
|
||||
SafetyOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getTaocanList, buyPackage } from "./api";
|
||||
import type { PowerPackage } from "./api";
|
||||
|
||||
const BuyPowerPage: React.FC = () => {
|
||||
const [packages, setPackages] = useState<PowerPackage[]>([]);
|
||||
const [selectedPackage, setSelectedPackage] = useState<PowerPackage | null>(
|
||||
null,
|
||||
);
|
||||
const [customAmount, setCustomAmount] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPackages();
|
||||
}, []);
|
||||
|
||||
const fetchPackages = async () => {
|
||||
try {
|
||||
const res = await getTaocanList();
|
||||
setPackages(res.list || []);
|
||||
} catch (error) {
|
||||
console.error("获取套餐列表失败:", error);
|
||||
Toast.show({ content: "获取套餐列表失败", position: "top" });
|
||||
}
|
||||
};
|
||||
|
||||
const getPackageTag = (pkg: PowerPackage) => {
|
||||
if (pkg.isTrial === 1) return { text: "限购一次", color: "orange" };
|
||||
if (pkg.isRecommend === 1) return { text: "推荐", color: "blue" };
|
||||
if (pkg.isHot === 1) return { text: "热门", color: "green" };
|
||||
if (pkg.isVip === 1) return { text: "VIP", color: "purple" };
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSelectPackage = (pkg: PowerPackage) => {
|
||||
setSelectedPackage(pkg);
|
||||
setCustomAmount(""); // 清空自定义金额
|
||||
};
|
||||
|
||||
const handleBuy = async () => {
|
||||
if (!selectedPackage && !customAmount) {
|
||||
Toast.show({ content: "请选择套餐或输入自定义金额", position: "top" });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
let res;
|
||||
|
||||
if (customAmount) {
|
||||
// 自定义购买
|
||||
const amount = parseFloat(customAmount);
|
||||
if (isNaN(amount) || amount < 1 || amount > 50000) {
|
||||
Toast.show({ content: "请输入1-50000之间的金额", position: "top" });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
res = await buyPackage({ price: amount });
|
||||
} else if (selectedPackage) {
|
||||
// 套餐购买
|
||||
res = await buyPackage({
|
||||
id: selectedPackage.id,
|
||||
price: selectedPackage.price,
|
||||
});
|
||||
}
|
||||
|
||||
if (res?.code_url) {
|
||||
// 显示支付二维码
|
||||
Dialog.show({
|
||||
content: (
|
||||
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
请使用微信扫码支付
|
||||
</div>
|
||||
<img
|
||||
src={res.code_url}
|
||||
alt="支付二维码"
|
||||
style={{ width: "250px", height: "250px", margin: "0 auto" }}
|
||||
/>
|
||||
<div
|
||||
style={{ marginTop: "16px", color: "#666", fontSize: "14px" }}
|
||||
>
|
||||
{selectedPackage
|
||||
? `支付金额: ¥${selectedPackage.price / 100}`
|
||||
: `支付金额: ¥${customAmount}`}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
closeOnMaskClick: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("购买失败:", error);
|
||||
Toast.show({ content: "购买失败,请重试", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="购买算力包" />}
|
||||
footer={
|
||||
<div className={style.footer}>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
className={style.buyButton}
|
||||
loading={loading}
|
||||
onClick={handleBuy}
|
||||
disabled={!selectedPackage && !customAmount}
|
||||
>
|
||||
{selectedPackage
|
||||
? `立即购买 ¥${selectedPackage.price / 100}`
|
||||
: customAmount
|
||||
? `立即购买 ¥${customAmount}`
|
||||
: "请选择套餐"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style.buyPowerPage}>
|
||||
{/* 选择套餐标题 */}
|
||||
<div className={style.sectionTitle}>选择套餐</div>
|
||||
|
||||
{/* 套餐列表 */}
|
||||
<div className={style.packageList}>
|
||||
{packages.map(pkg => {
|
||||
const tag = getPackageTag(pkg);
|
||||
const isSelected = selectedPackage?.id === pkg.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={pkg.id}
|
||||
className={`${style.packageCard} ${isSelected ? style.packageCardActive : ""}`}
|
||||
onClick={() => handleSelectPackage(pkg)}
|
||||
>
|
||||
{/* 套餐头部 */}
|
||||
<div className={style.packageHeader}>
|
||||
<div className={style.packageTitle}>
|
||||
<span className={style.packageName}>{pkg.name}</span>
|
||||
{tag && (
|
||||
<span
|
||||
className={`${style.packageTag} ${style[`tag-${tag.color}`]}`}
|
||||
>
|
||||
{tag.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.packagePrice}>
|
||||
<span className={style.currentPrice}>
|
||||
¥{pkg.price / 100}
|
||||
</span>
|
||||
{pkg.originalPrice && (
|
||||
<span className={style.originalPrice}>
|
||||
¥{pkg.originalPrice / 100}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 算力信息 */}
|
||||
<div className={style.packageTokens}>
|
||||
{pkg.tokens?.toLocaleString()} 算力点
|
||||
</div>
|
||||
|
||||
{/* 单价和优惠 */}
|
||||
<div className={style.packageMeta}>
|
||||
<div className={style.unitPrice}>
|
||||
单价
|
||||
<br />
|
||||
<span className={style.unitPriceValue}>
|
||||
¥{(pkg.unitPrice / 100).toFixed(4)}/点
|
||||
</span>
|
||||
</div>
|
||||
{pkg.discount > 0 && (
|
||||
<div className={style.discount}>
|
||||
优惠幅度
|
||||
<br />
|
||||
<span className={style.discountValue}>
|
||||
{pkg.discount}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 特性列表 */}
|
||||
{pkg.features && pkg.features.length > 0 && (
|
||||
<div className={style.packageFeatures}>
|
||||
{pkg.features.map((feature, index) => (
|
||||
<div key={index} className={style.featureItem}>
|
||||
<CheckCircleOutlined className={style.featureIcon} />
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 自定义购买 */}
|
||||
<Card className={style.customCard}>
|
||||
<div className={style.customHeader}>
|
||||
<ThunderboltOutlined className={style.customIcon} />
|
||||
<span className={style.customTitle}>自定义购买</span>
|
||||
</div>
|
||||
<div className={style.customContent}>
|
||||
<div className={style.customLabel}>自定义金额(1-50000元)</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="请输入金额"
|
||||
value={customAmount}
|
||||
onChange={e => {
|
||||
setCustomAmount(e.target.value);
|
||||
setSelectedPackage(null); // 清空套餐选择
|
||||
}}
|
||||
style={{ fontSize: 16 }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 安全保障 */}
|
||||
<Card className={style.securityCard}>
|
||||
<div className={style.securityHeader}>
|
||||
<SafetyOutlined className={style.securityIcon} />
|
||||
<span className={style.securityTitle}>安全保障</span>
|
||||
</div>
|
||||
<div className={style.securityList}>
|
||||
<div className={style.securityItem}>
|
||||
• 所有算力永久有效,无使用期限
|
||||
</div>
|
||||
<div className={style.securityItem}>
|
||||
• 支持微信支付、支付宝安全支付
|
||||
</div>
|
||||
<div className={style.securityItem}>
|
||||
• 购买后立即到账,7x24小时客服支持
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyPowerPage;
|
||||
@@ -1,35 +1,131 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
interface taocanItem {
|
||||
id: 1;
|
||||
name: "试用套餐";
|
||||
tokens: "2,800";
|
||||
price: 9800;
|
||||
originalPrice: 140;
|
||||
description: ["适合新用户体验", "包含基础AI功能", "永久有效", "客服支持"];
|
||||
sort: 1;
|
||||
isTrial: 1;
|
||||
isRecommend: 0;
|
||||
isHot: 0;
|
||||
isVip: 0;
|
||||
status: 1;
|
||||
isDel: 0;
|
||||
delTime: null;
|
||||
createTime: "2025-09-29 16:53:06";
|
||||
updateTime: "2025-09-29 16:53:06";
|
||||
discount: 30;
|
||||
unitPrice: 3.5;
|
||||
// 算力包套餐类型
|
||||
export interface PowerPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
tokens: number; // 算力点数
|
||||
price: number; // 价格(分)
|
||||
originalPrice: number; // 原价(分)
|
||||
unitPrice: number; // 单价
|
||||
discount: number; // 折扣百分比
|
||||
isTrial: number; // 是否试用套餐
|
||||
isRecommend: number; // 是否推荐
|
||||
isHot: number; // 是否热门
|
||||
isVip: number; // 是否VIP
|
||||
features: string[]; // 功能特性
|
||||
status: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
interface taocanList {
|
||||
list: taocanItem[];
|
||||
// 算力统计信息
|
||||
export interface PowerStats {
|
||||
balance: number; // 账户余额(元)
|
||||
totalPower: number; // 总算力
|
||||
todayUsed: number; // 今日使用
|
||||
monthUsed: number; // 本月使用
|
||||
remainingPower: number; // 剩余算力
|
||||
}
|
||||
// 套餐列表
|
||||
export function getTaocanList(): Promise<taocanList> {
|
||||
|
||||
// 消费记录类型
|
||||
export interface ConsumptionRecord {
|
||||
id: number;
|
||||
type: string; // AI分析、内容生成等
|
||||
status: string; // 已完成、进行中等
|
||||
amount: number; // 消费金额(元)
|
||||
power: number; // 消耗算力
|
||||
description: string; // 描述
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface OrderListParams {
|
||||
/**
|
||||
* 关键词搜索(订单号、商品名称)
|
||||
*/
|
||||
keyword?: string;
|
||||
/**
|
||||
* 每页数量(默认10)
|
||||
*/
|
||||
limit?: string;
|
||||
/**
|
||||
* 订单类型(1-算力充值)
|
||||
*/
|
||||
orderType?: string;
|
||||
/**
|
||||
* 页码
|
||||
*/
|
||||
page?: string;
|
||||
/**
|
||||
* 订单状态(0-待支付 1-已支付 2-已取消 3-已退款)
|
||||
*/
|
||||
status?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
interface OrderList {
|
||||
id?: number;
|
||||
mchId?: number;
|
||||
companyId?: number;
|
||||
userId?: number;
|
||||
orderType?: number;
|
||||
status?: number;
|
||||
goodsId?: number;
|
||||
goodsName?: string;
|
||||
goodsSpecs?: {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
};
|
||||
money?: number;
|
||||
orderNo?: string;
|
||||
ip?: string;
|
||||
nonceStr?: string;
|
||||
createTime?: string;
|
||||
payType?: number;
|
||||
payTime?: string;
|
||||
payInfo?: any;
|
||||
deleteTime?: string;
|
||||
tokens?: string;
|
||||
statusText?: string;
|
||||
orderTypeText?: string;
|
||||
payTypeText?: string;
|
||||
}
|
||||
|
||||
// 获取订单列表
|
||||
export function getOrderList(
|
||||
params: OrderListParams,
|
||||
): Promise<{ list: OrderList[]; total: number }> {
|
||||
return request("/v1/tokens/orderList", params, "GET");
|
||||
}
|
||||
|
||||
// 获取算力统计
|
||||
export function getPowerStats(): Promise<PowerStats> {
|
||||
return request("/v1/power/stats", {}, "GET");
|
||||
}
|
||||
|
||||
// 获取套餐列表
|
||||
export function getTaocanList(): Promise<{ list: PowerPackage[] }> {
|
||||
return request("/v1/tokens/list", {}, "GET");
|
||||
}
|
||||
|
||||
// 支付id和price 从套餐列表取对应的价格
|
||||
export function pay(params: { id: string; price: number }) {
|
||||
// 获取消费记录
|
||||
export function getConsumptionRecords(params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
type?: string;
|
||||
status?: string;
|
||||
}): Promise<{ list: ConsumptionRecord[]; total: number }> {
|
||||
return request("/v1/power/consumption-records", params, "GET");
|
||||
}
|
||||
|
||||
// 购买套餐
|
||||
export function buyPackage(params: { id: number; price: number }) {
|
||||
return request("/v1/tokens/pay", params, "POST");
|
||||
}
|
||||
|
||||
// 自定义购买算力
|
||||
export function buyCustomPower(params: { amount: number }) {
|
||||
return request("/v1/power/buy-custom", params, "POST");
|
||||
}
|
||||
|
||||
@@ -1,448 +1,340 @@
|
||||
.recharge-page {
|
||||
// 算力管理页面样式
|
||||
.powerPage {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
color: var(--primary-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(24, 142, 238, 0.1);
|
||||
.powerTabs {
|
||||
:global {
|
||||
.adm-tabs-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refreshBtn {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(24, 142, 238, 0.2);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.recharge-tabs {
|
||||
:global(.adm-tabs-header) {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab-active) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab-line) {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
// ==================== 概览Tab ====================
|
||||
.overviewContent {
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
.accountCards {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 12px;
|
||||
padding: 18px 0 18px 0;
|
||||
}
|
||||
|
||||
.balanceCard,
|
||||
.powerCard {
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.balanceCard {
|
||||
border: 1px solid #cbdbea;
|
||||
background: #edfcff;
|
||||
}
|
||||
|
||||
.powerCard {
|
||||
border: 1px solid #e3dbf0;
|
||||
background: #f9f0ff;
|
||||
}
|
||||
.powerCard .iconWrapper {
|
||||
background: #e9d9ff;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.balance-content {
|
||||
display: flex;
|
||||
color: #16b364;
|
||||
padding-left: 30px;
|
||||
}
|
||||
.wallet-icon {
|
||||
color: #16b364;
|
||||
font-size: 30px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.balance-info {
|
||||
margin-left: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.balance-label {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.balance-amount {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #16b364;
|
||||
line-height: 1.1;
|
||||
}
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
margin-bottom: 16px;
|
||||
.quick-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.desc-card {
|
||||
margin: 16px 0px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
}
|
||||
|
||||
.warn-card {
|
||||
margin: 16px 0;
|
||||
background: #fff2e8;
|
||||
border: 1px solid #ffbb96;
|
||||
}
|
||||
|
||||
.quick-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.quick-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.quick-btn {
|
||||
min-width: 80px;
|
||||
margin: 4px 0;
|
||||
font-size: 16px;
|
||||
.iconWrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.quick-btn-active {
|
||||
@extend .quick-btn;
|
||||
font-weight: 600;
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
.recharge-main-btn {
|
||||
margin-top: 16px;
|
||||
font-size: 18px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.desc-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.desc-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.warn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #faad14;
|
||||
font-size: 14px;
|
||||
}
|
||||
.warn-icon {
|
||||
font-size: 30px;
|
||||
color: #faad14;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.warn-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.warn-title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
.warn-text {
|
||||
color: #faad14;
|
||||
font-size: 14px;
|
||||
|
||||
.balanceCard .iconWrapper {
|
||||
background: #d6edff;
|
||||
}
|
||||
|
||||
// AI服务样式
|
||||
.ai-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
.cardIcon {
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ai-tag {
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ai-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-services {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ai-service-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.service-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.service-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.service-features {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-check {
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.usage-progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.usage-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// 版本套餐样式
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.version-icon {
|
||||
font-size: 24px;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.version-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.version-packages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.version-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.package-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.package-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.package-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.package-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.package-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.package-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-blue {
|
||||
background: #e6f7ff;
|
||||
.balanceCard .cardIcon {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tag-green {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
.powerCard .cardIcon {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.package-price {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
.textWrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.package-description {
|
||||
.cardTitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.package-features {
|
||||
.cardValue {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.balanceCard .cardValue {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.powerCard .cardValue {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
// 使用情况卡片
|
||||
.usageCard {
|
||||
padding: 20px 16px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.usageTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 14px;
|
||||
.usageStats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.usageItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usageValue {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.package-status {
|
||||
text-align: center;
|
||||
.valueGreen {
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upgrade-btn {
|
||||
border-radius: 8px;
|
||||
.valueBlue {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.valuePurple {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.usageLabel {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
// 快速操作卡片
|
||||
.actionCard {
|
||||
padding: 20px 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.actionTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.buyButton {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.recordButton {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
|
||||
&:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消费记录Tab ====================
|
||||
.recordsContent {
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.recordList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.recordItem {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.recordHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recordLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recordType {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.recordStatus {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.recordRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recordAmount {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.recordPower {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.recordDesc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.recordTime {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.emptyRecords {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -1,497 +1,290 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Card, Button, Toast, Tabs, Dialog } from "antd-mobile";
|
||||
import { Card, Button, Toast, Tabs, Tag, Picker } from "antd-mobile";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
WalletOutlined,
|
||||
WarningOutlined,
|
||||
ClockCircleOutlined,
|
||||
RobotOutlined,
|
||||
CrownOutlined,
|
||||
SyncOutlined,
|
||||
ShoppingCartOutlined,
|
||||
HistoryOutlined,
|
||||
LineChartOutlined,
|
||||
DownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getTaocanList, pay } from "./api";
|
||||
import { getPowerStats, getOrderList } from "./api";
|
||||
import type { PowerStats } from "./api";
|
||||
|
||||
// AI服务列表数据
|
||||
const aiServices = [
|
||||
{
|
||||
id: 1,
|
||||
name: "添加好友及打招呼",
|
||||
icon: "💬",
|
||||
price: 1,
|
||||
description: "AI智能添加好友并发送个性化打招呼消息",
|
||||
features: ["智能筛选目标用户", "发送个性化打招呼消息", "自动记录添加结果"],
|
||||
usage: { current: 15, total: 450 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "小室AI内容生产",
|
||||
icon: "⚡",
|
||||
price: 1,
|
||||
description: "AI智能创建朋友圈内容,智能配文与朋友圈内容",
|
||||
features: ["智能生成朋友圈文案", "AI配文智能文案", "内容智能排版优化"],
|
||||
usage: { current: 28, total: 680 },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "智能分发服务",
|
||||
icon: "📤",
|
||||
price: 1,
|
||||
description: "AI智能分发内容到多个平台",
|
||||
features: ["多平台智能分发", "内容智能优化", "分发效果分析"],
|
||||
usage: { current: 12, total: 300 },
|
||||
},
|
||||
];
|
||||
type OrderRecordView = {
|
||||
id: number;
|
||||
type: string;
|
||||
status: string;
|
||||
amount: number; // 元
|
||||
power: number;
|
||||
description: string;
|
||||
createTime: string;
|
||||
};
|
||||
|
||||
// 快捷金额选项(可选)
|
||||
const quickAmounts = [50, 100, 200, 500, 1000, 2000];
|
||||
|
||||
const Recharge: React.FC = () => {
|
||||
const PowerManagement: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
// 假设余额从后端接口获取,实际可用props或store传递
|
||||
const [balance] = useState(0);
|
||||
const [selected, setSelected] = useState<any | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("account");
|
||||
const [taocanList, setTaocanList] = useState<any[]>([]);
|
||||
const [customAmount, setCustomAmount] = useState(""); // 自定义充值金额
|
||||
const [stats, setStats] = useState<PowerStats | null>(null);
|
||||
const [records, setRecords] = useState<OrderRecordView[]>([]);
|
||||
const [filterType, setFilterType] = useState<string>("all");
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all");
|
||||
const [filterTypeVisible, setFilterTypeVisible] = useState(false);
|
||||
const [filterStatusVisible, setFilterStatusVisible] = useState(false);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: "全部类型", value: "all" },
|
||||
{ label: "AI分析", value: "ai_analysis" },
|
||||
{ label: "内容生成", value: "content_gen" },
|
||||
{ label: "数据训练", value: "data_train" },
|
||||
{ label: "智能推荐", value: "smart_rec" },
|
||||
{ label: "语音识别", value: "voice_rec" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "全部状态", value: "all" },
|
||||
{ label: "已完成", value: "completed" },
|
||||
{ label: "进行中", value: "processing" },
|
||||
{ label: "已取消", value: "cancelled" },
|
||||
];
|
||||
|
||||
// 加载套餐列表
|
||||
useEffect(() => {
|
||||
const loadTaocanList = async () => {
|
||||
try {
|
||||
const res = await getTaocanList();
|
||||
if (res.list) {
|
||||
setTaocanList(res.list);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载套餐列表失败:", error);
|
||||
Toast.show({ content: "加载套餐列表失败", position: "top" });
|
||||
}
|
||||
};
|
||||
loadTaocanList();
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
// 账户充值操作(自定义金额)
|
||||
const handleCustomRecharge = async () => {
|
||||
const amount = parseFloat(customAmount);
|
||||
if (!customAmount || isNaN(amount) || amount <= 0) {
|
||||
Toast.show({ content: "请输入有效的充值金额", position: "top" });
|
||||
return;
|
||||
}
|
||||
if (amount < 1) {
|
||||
Toast.show({ content: "充值金额不能小于1元", position: "top" });
|
||||
return;
|
||||
}
|
||||
if (amount > 100000) {
|
||||
Toast.show({ content: "单次充值金额不能超过10万元", position: "top" });
|
||||
return;
|
||||
useEffect(() => {
|
||||
if (activeTab === "records") {
|
||||
fetchRecords();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab, filterType, filterStatus]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await getPowerStats();
|
||||
setStats(res);
|
||||
} catch (error) {
|
||||
console.error("获取统计失败:", error);
|
||||
Toast.show({ content: "获取数据失败", position: "top" });
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRecords = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await pay({
|
||||
id: "", // 自定义充值不需要套餐ID
|
||||
price: Math.round(amount * 100), // 转换为分
|
||||
// 映射状态到订单状态:0待支付 1已支付 2已取消 3已退款
|
||||
const statusMap: Record<string, string | undefined> = {
|
||||
all: undefined,
|
||||
completed: "1",
|
||||
processing: "0",
|
||||
cancelled: "2",
|
||||
};
|
||||
|
||||
const res = await getOrderList({
|
||||
page: "1",
|
||||
limit: "100",
|
||||
orderType: "1",
|
||||
status: statusMap[filterStatus],
|
||||
});
|
||||
// 假设返回的是二维码链接,存储在res中
|
||||
if (res) {
|
||||
// 显示二维码弹窗
|
||||
Dialog.show({
|
||||
content: (
|
||||
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
请使用微信扫码支付
|
||||
</div>
|
||||
<img
|
||||
src={res.code_url as any}
|
||||
alt="支付二维码"
|
||||
style={{ width: "250px", height: "250px", margin: "0 auto" }}
|
||||
/>
|
||||
<div
|
||||
style={{ marginTop: "16px", color: "#666", fontSize: "14px" }}
|
||||
>
|
||||
支付金额: ¥{amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
closeOnMaskClick: true,
|
||||
});
|
||||
}
|
||||
|
||||
const list = (res.list || []).map((o: any) => ({
|
||||
id: o.id,
|
||||
type: o.orderTypeText || o.goodsName || "充值订单",
|
||||
status: o.statusText || "",
|
||||
amount: typeof o.money === "number" ? o.money / 100 : 0,
|
||||
power: Number(o.goodsSpecs?.tokens ?? o.tokens ?? 0),
|
||||
description: o.goodsName || "",
|
||||
createTime: o.createTime || "",
|
||||
}));
|
||||
setRecords(list);
|
||||
} catch (error) {
|
||||
console.error("支付失败:", error);
|
||||
Toast.show({ content: "支付失败,请重试", position: "top" });
|
||||
console.error("获取消费记录失败:", error);
|
||||
Toast.show({ content: "获取消费记录失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 版本套餐充值操作
|
||||
const handlePackageRecharge = async () => {
|
||||
if (!selected) {
|
||||
Toast.show({ content: "请选择充值套餐", position: "top" });
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await pay({
|
||||
id: selected.id,
|
||||
price: selected.price,
|
||||
});
|
||||
// 假设返回的是二维码链接,存储在res中
|
||||
if (res) {
|
||||
// 显示二维码弹窗
|
||||
Dialog.show({
|
||||
content: (
|
||||
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
请使用微信扫码支付
|
||||
</div>
|
||||
<img
|
||||
src={res.code_url as any}
|
||||
alt="支付二维码"
|
||||
style={{ width: "250px", height: "250px", margin: "0 auto" }}
|
||||
/>
|
||||
<div
|
||||
style={{ marginTop: "16px", color: "#666", fontSize: "14px" }}
|
||||
>
|
||||
支付金额: ¥{selected.price / 100}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
closeOnMaskClick: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("支付失败:", error);
|
||||
Toast.show({ content: "支付失败,请重试", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const handleRefresh = () => {
|
||||
if (loading) return;
|
||||
fetchStats();
|
||||
if (activeTab === "records") {
|
||||
fetchRecords();
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染账户充值tab内容
|
||||
const renderAccountRecharge = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<Card className={style["balance-card"]}>
|
||||
<div className={style["balance-content"]}>
|
||||
<WalletOutlined className={style["wallet-icon"]} />
|
||||
<div className={style["balance-info"]}>
|
||||
<div className={style["balance-label"]}>当前余额</div>
|
||||
<div className={style["balance-amount"]}>
|
||||
¥{balance.toFixed(2)}
|
||||
const handleBuyPower = () => {
|
||||
navigate("/recharge/buy-power");
|
||||
};
|
||||
|
||||
const handleViewRecords = () => {
|
||||
navigate("/recharge/usage-records");
|
||||
};
|
||||
|
||||
const getTypeLabel = () => {
|
||||
return (
|
||||
typeOptions.find(opt => opt.value === filterType)?.label || "全部类型"
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusLabel = () => {
|
||||
return (
|
||||
statusOptions.find(opt => opt.value === filterStatus)?.label || "全部状态"
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染概览Tab
|
||||
const renderOverview = () => (
|
||||
<div className={style.overviewContent}>
|
||||
{/* 账户信息卡片 */}
|
||||
<div className={style.accountCards}>
|
||||
<Card className={style.powerCard}>
|
||||
<div className={style.cardContent}>
|
||||
<div className={style.iconWrapper}>
|
||||
<LineChartOutlined className={style.cardIcon} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className={style["quick-card"]}>
|
||||
<div className={style["quick-title"]}>充值金额</div>
|
||||
|
||||
{/* 快捷金额选择 */}
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
||||
快捷选择
|
||||
</div>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
|
||||
{quickAmounts.map(amount => (
|
||||
<Button
|
||||
key={amount}
|
||||
color={
|
||||
customAmount === amount.toString() ? "primary" : "default"
|
||||
}
|
||||
onClick={() => setCustomAmount(amount.toString())}
|
||||
style={{ flex: "0 0 calc(33.333% - 6px)" }}
|
||||
>
|
||||
¥{amount}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义金额输入 */}
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
||||
或输入自定义金额(元)
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="请输入充值金额"
|
||||
value={customAmount}
|
||||
onChange={e => setCustomAmount(e.target.value)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px 16px",
|
||||
border: "1px solid #e5e5e5",
|
||||
borderRadius: "8px",
|
||||
fontSize: "16px",
|
||||
outline: "none",
|
||||
}}
|
||||
min="1"
|
||||
max="100000"
|
||||
step="0.01"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px",
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
• 最低充值金额:1元
|
||||
<br />
|
||||
• 单次充值上限:100,000元
|
||||
<br />• 支持微信支付
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{customAmount && parseFloat(customAmount) > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
padding: "12px",
|
||||
background: "#e6f7ff",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #91d5ff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ fontSize: "14px", color: "#333", marginBottom: "4px" }}
|
||||
>
|
||||
充值金额:
|
||||
<span
|
||||
style={{
|
||||
fontSize: "20px",
|
||||
fontWeight: "600",
|
||||
color: "#1890ff",
|
||||
}}
|
||||
>
|
||||
¥{parseFloat(customAmount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: "12px", color: "#666" }}>
|
||||
到账金额:¥{parseFloat(customAmount).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
className={style["recharge-main-btn"]}
|
||||
loading={loading}
|
||||
onClick={handleCustomRecharge}
|
||||
disabled={!customAmount || parseFloat(customAmount) <= 0}
|
||||
>
|
||||
立即充值
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card className={style["desc-card"]}>
|
||||
<div className={style["desc-title"]}>充值说明</div>
|
||||
<div className={style["desc-text"]}>
|
||||
• 充值金额实时到账,可用于购买AI服务和版本套餐
|
||||
<br />
|
||||
• 使用AI服务将从余额中扣除相应费用
|
||||
<br />• 余额支持退款,详情请联系客服
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{balance < 10 && (
|
||||
<Card className={style["warn-card"]}>
|
||||
<div className={style["warn-content"]}>
|
||||
<WarningOutlined className={style["warn-icon"]} />
|
||||
<div className={style["warn-info"]}>
|
||||
<div className={style["warn-title"]}>余额不足提醒</div>
|
||||
<div className={style["warn-text"]}>
|
||||
当前余额较低,建议及时充值以免影响服务使用
|
||||
</div>
|
||||
<div className={style.textWrapper}>
|
||||
<div className={style.cardTitle}>总算力</div>
|
||||
<div className={style.cardValue}>{stats?.totalPower || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
|
||||
// 渲染AI服务tab内容
|
||||
const renderAiServices = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<div className={style["ai-header"]}>
|
||||
<div className={style["ai-title"]}>
|
||||
<RobotOutlined className={style["ai-icon"]} />
|
||||
AI智能服务收费
|
||||
{/* 使用情况卡片 */}
|
||||
<Card className={style.usageCard}>
|
||||
<div className={style.usageTitle}>使用情况</div>
|
||||
<div className={style.usageStats}>
|
||||
<div className={style.usageItem}>
|
||||
<div className={`${style.usageValue} ${style.valueGreen}`}>
|
||||
{stats?.todayUsed || 0}
|
||||
</div>
|
||||
<div className={style.usageLabel}>今日使用</div>
|
||||
</div>
|
||||
<div className={style.usageItem}>
|
||||
<div className={`${style.usageValue} ${style.valueBlue}`}>
|
||||
{stats?.monthUsed || 0}
|
||||
</div>
|
||||
<div className={style.usageLabel}>本月使用</div>
|
||||
</div>
|
||||
<div className={style.usageItem}>
|
||||
<div className={`${style.usageValue} ${style.valuePurple}`}>
|
||||
{stats?.remainingPower || 0}
|
||||
</div>
|
||||
<div className={style.usageLabel}>剩余算力</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["ai-tag"]}>统一按次收费</div>
|
||||
</div>
|
||||
<div className={style["ai-description"]}>
|
||||
三项核心AI服务,按使用次数收费,每次1元
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className={style["ai-services"]}>
|
||||
{aiServices.map(service => (
|
||||
<Card key={service.id} className={style["ai-service-card"]}>
|
||||
<div className={style["service-header"]}>
|
||||
<div className={style["service-info"]}>
|
||||
<div className={style["service-icon"]}>{service.icon}</div>
|
||||
<div className={style["service-details"]}>
|
||||
<div className={style["service-name"]}>{service.name}</div>
|
||||
<div className={style["service-price"]}>
|
||||
¥{service.price}/次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["service-description"]}>
|
||||
{service.description}
|
||||
</div>
|
||||
<div className={style["service-features"]}>
|
||||
{service.features.map((feature, index) => (
|
||||
<div key={index} className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={style["usage-progress"]}>
|
||||
<div className={style["usage-label"]}>今日使用进度</div>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{
|
||||
width: `${(service.usage.current / service.usage.total) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className={style["usage-text"]}>
|
||||
{service.usage.current} / {service.usage.total}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染版本套餐tab内容
|
||||
const renderVersionPackages = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<div className={style["version-header"]}>
|
||||
<CrownOutlined className={style["version-icon"]} />
|
||||
<span>充值套餐</span>
|
||||
</div>
|
||||
<div className={style["version-description"]}>
|
||||
选择合适的充值套餐,享受更多优惠
|
||||
</div>
|
||||
|
||||
<div className={style["version-packages"]}>
|
||||
{taocanList.map(pkg => (
|
||||
<Card key={pkg.id} className={style["version-card"]}>
|
||||
<div className={style["package-header"]}>
|
||||
<div className={style["package-info"]}>
|
||||
<div className={style["package-icon"]}>💰</div>
|
||||
<div className={style["package-details"]}>
|
||||
<div className={style["package-name"]}>
|
||||
{pkg.name}
|
||||
{pkg.isRecommend === 1 && (
|
||||
<span
|
||||
className={`${style["package-tag"]} ${style["tag-blue"]}`}
|
||||
>
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
{pkg.isHot === 1 && (
|
||||
<span
|
||||
className={`${style["package-tag"]} ${style["tag-red"]}`}
|
||||
>
|
||||
热门
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["package-price"]}>
|
||||
¥{pkg.price / 100}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{pkg.description && (
|
||||
<div className={style["package-description"]}>
|
||||
{pkg.description}
|
||||
</div>
|
||||
)}
|
||||
<div className={style["package-features"]}>
|
||||
<div className={style["features-title"]}>套餐内容:</div>
|
||||
<div className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
包含 {pkg.tokens} Tokens
|
||||
</div>
|
||||
{pkg.originalPrice && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "#999",
|
||||
textDecoration: "line-through",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
原价: ¥{pkg.originalPrice / 100}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
color={selected?.id === pkg.id ? "primary" : "default"}
|
||||
className={
|
||||
selected?.id === pkg.id
|
||||
? style["upgrade-btn-active"]
|
||||
: style["upgrade-btn"]
|
||||
}
|
||||
onClick={() => setSelected(pkg)}
|
||||
>
|
||||
{selected?.id === pkg.id ? "已选择" : "选择套餐"}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: "16px",
|
||||
background: "#fff",
|
||||
borderTop: "1px solid #f0f0f0",
|
||||
boxShadow: "0 -2px 8px rgba(0,0,0,0.08)",
|
||||
}}
|
||||
>
|
||||
{/* 快速操作 */}
|
||||
<Card className={style.actionCard}>
|
||||
<div className={style.actionTitle}>快速操作</div>
|
||||
<div className={style.actionButtons}>
|
||||
<Button className={style.buyButton} onClick={handleBuyPower} block>
|
||||
<ShoppingCartOutlined className={style.buttonIcon} />
|
||||
购买算力包
|
||||
</Button>
|
||||
<Button
|
||||
className={style.recordButton}
|
||||
onClick={handleViewRecords}
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
loading={loading}
|
||||
onClick={handlePackageRecharge}
|
||||
>
|
||||
立即购买 ¥{selected.price / 100}
|
||||
<HistoryOutlined className={style.buttonIcon} />
|
||||
使用记录
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染消费记录Tab
|
||||
const renderRecords = () => (
|
||||
<div className={style.recordsContent}>
|
||||
{/* 筛选器 */}
|
||||
<div className={style.filters}>
|
||||
<Picker
|
||||
columns={[typeOptions]}
|
||||
visible={filterTypeVisible}
|
||||
onClose={() => setFilterTypeVisible(false)}
|
||||
value={[filterType]}
|
||||
onConfirm={value => {
|
||||
setFilterType(value[0] as string);
|
||||
setFilterTypeVisible(false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<div
|
||||
className={style.filterButton}
|
||||
onClick={() => setFilterTypeVisible(true)}
|
||||
>
|
||||
<DownOutlined className={style.filterIcon} />
|
||||
{getTypeLabel()}
|
||||
</div>
|
||||
)}
|
||||
</Picker>
|
||||
|
||||
<Picker
|
||||
columns={[statusOptions]}
|
||||
visible={filterStatusVisible}
|
||||
onClose={() => setFilterStatusVisible(false)}
|
||||
value={[filterStatus]}
|
||||
onConfirm={value => {
|
||||
setFilterStatus(value[0] as string);
|
||||
setFilterStatusVisible(false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<div
|
||||
className={style.filterButton}
|
||||
onClick={() => setFilterStatusVisible(true)}
|
||||
>
|
||||
<DownOutlined className={style.filterIcon} />
|
||||
{getStatusLabel()}
|
||||
</div>
|
||||
)}
|
||||
</Picker>
|
||||
</div>
|
||||
|
||||
{/* 消费记录列表 */}
|
||||
<div className={style.recordList}>
|
||||
{loading && records.length === 0 ? (
|
||||
<div className={style.loadingContainer}>
|
||||
<div className={style.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : records.length > 0 ? (
|
||||
records.map(record => (
|
||||
<Card key={record.id} className={style.recordItem}>
|
||||
<div className={style.recordHeader}>
|
||||
<div className={style.recordLeft}>
|
||||
<div className={style.recordType}>{record.type}</div>
|
||||
<Tag
|
||||
color={record.status === "已完成" ? "success" : "primary"}
|
||||
className={style.recordStatus}
|
||||
>
|
||||
{record.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={style.recordRight}>
|
||||
<div className={style.recordAmount}>
|
||||
-¥{record.amount.toFixed(1)}
|
||||
</div>
|
||||
<div className={style.recordPower}>{record.power} 算力</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.recordDesc}>{record.description}</div>
|
||||
<div className={style.recordTime}>{record.createTime}</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className={style.emptyRecords}>
|
||||
<div className={style.emptyIcon}>📋</div>
|
||||
<div className={style.emptyText}>暂无消费记录</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -499,33 +292,26 @@ const Recharge: React.FC = () => {
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="充值中心"
|
||||
title="算力管理"
|
||||
right={
|
||||
<div
|
||||
className={style["record-btn"]}
|
||||
onClick={() => navigate("/recharge/order")}
|
||||
>
|
||||
<ClockCircleOutlined />
|
||||
记录
|
||||
<div className={style.refreshBtn} onClick={handleRefresh}>
|
||||
<SyncOutlined spin={loading} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style["recharge-page"]}>
|
||||
<div className={style.powerPage}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className={style["recharge-tabs"]}
|
||||
className={style.powerTabs}
|
||||
>
|
||||
<Tabs.Tab title="账户充值" key="account">
|
||||
{renderAccountRecharge()}
|
||||
<Tabs.Tab title="概览" key="overview">
|
||||
{renderOverview()}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="AI服务" key="ai">
|
||||
{renderAiServices()}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="版本套餐" key="version">
|
||||
{renderVersionPackages()}
|
||||
<Tabs.Tab title="消费记录" key="records">
|
||||
{renderRecords()}
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -533,4 +319,4 @@ const Recharge: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Recharge;
|
||||
export default PowerManagement;
|
||||
|
||||
46
Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts
Normal file
46
Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import request from "@/api/request";
|
||||
|
||||
export interface TokensUseRecordParams {
|
||||
/**
|
||||
* 来源 0未知 1好友聊天 2群聊天 3群公告 4商家 5充值
|
||||
*/
|
||||
form?: string;
|
||||
/**
|
||||
* 条数
|
||||
*/
|
||||
limit?: string;
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
page?: string;
|
||||
/**
|
||||
* 类型 0减少 1增加
|
||||
*/
|
||||
type?: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface TokensUseRecordItem {
|
||||
id: number;
|
||||
companyId: number;
|
||||
userId: number;
|
||||
wechatAccountId: number;
|
||||
friendIdOrGroupId: number;
|
||||
form: number;
|
||||
type: number;
|
||||
tokens: number;
|
||||
balanceTokens: number;
|
||||
remarks: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface TokensUseRecordList {
|
||||
list: TokensUseRecordItem[];
|
||||
}
|
||||
|
||||
//算力使用明细
|
||||
export function getTokensUseRecord(
|
||||
TokensUseRecordParams,
|
||||
): Promise<TokensUseRecordList> {
|
||||
return request("/v1/kefu/tokensRecord/list", TokensUseRecordParams, "GET");
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
.page {
|
||||
padding: 12px 12px 16px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f7f8fa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 18px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(90deg, #ebf3ff 0%, #fff1ff 100%);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summaryItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summaryNumber {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, #1677ff 0%, #722ed1 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.summaryLabel {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.itemTitle {
|
||||
font-size: 14px;
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.itemDesc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.itemFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.power {
|
||||
color: #722ed1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 20px 0;
|
||||
}
|
||||
225
Cunkebao/src/pages/mobile/mine/recharge/usage-records/index.tsx
Normal file
225
Cunkebao/src/pages/mobile/mine/recharge/usage-records/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Picker, Card, Tag, Toast } from "antd-mobile";
|
||||
import { Pagination } from "antd";
|
||||
import { DownOutline } from "antd-mobile-icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
import { getTokensUseRecord } from "./api";
|
||||
|
||||
interface UsageRecordItem {
|
||||
id: number | string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
power?: number;
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
typeLabel?: string;
|
||||
}
|
||||
|
||||
const UsageRecords: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [records, setRecords] = useState<UsageRecordItem[]>([]);
|
||||
const [type, setType] = useState<string>("all");
|
||||
const [status, setStatus] = useState<string>("all");
|
||||
const [typeVisible, setTypeVisible] = useState(false);
|
||||
const [statusVisible, setStatusVisible] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: "全部类型", value: "all" },
|
||||
{ label: "AI助手对话", value: "chat" },
|
||||
{ label: "智能群发", value: "broadcast" },
|
||||
{ label: "内容生成", value: "content" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "全部状态", value: "all" },
|
||||
{ label: "已完成", value: "completed" },
|
||||
{ label: "进行中", value: "processing" },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [type, status, page]);
|
||||
|
||||
const fetchRecords = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 前端与接口枚举的简易映射:仅映射已知来源,其它为 undefined(即不过滤)
|
||||
const formMap: Record<string, number | undefined> = {
|
||||
all: undefined, // 全部
|
||||
chat: 1, // 好友/群聊天
|
||||
broadcast: 3, // 群公告/群发
|
||||
content: 4, // 商家/内容生成
|
||||
};
|
||||
|
||||
const res: any = await getTokensUseRecord({
|
||||
page: String(page),
|
||||
limit: String(pageSize),
|
||||
form: formMap[type]?.toString(),
|
||||
// 接口的 type 为 0减少/1增加,与当前“状态”筛选无直接对应,暂不传
|
||||
});
|
||||
|
||||
const formLabelMap: Record<number, string> = {
|
||||
0: "未知",
|
||||
1: "好友聊天",
|
||||
2: "群聊天",
|
||||
3: "群公告",
|
||||
4: "商家",
|
||||
5: "充值",
|
||||
};
|
||||
|
||||
const rawList: any[] = res?.list || [];
|
||||
const list: UsageRecordItem[] = rawList.map((item: any, idx: number) => ({
|
||||
id: item.id ?? idx,
|
||||
title: item.remarks || "使用记录",
|
||||
description: "",
|
||||
power: item.tokens ?? 0,
|
||||
status: "已完成",
|
||||
createTime: item.createTime || "",
|
||||
typeLabel: formLabelMap[item.form as number] || "",
|
||||
}));
|
||||
setRecords(list);
|
||||
|
||||
const possibleTotal =
|
||||
(res && (res.total || res.count || res.totalCount)) || 0;
|
||||
setTotal(
|
||||
typeof possibleTotal === "number" && possibleTotal > 0
|
||||
? possibleTotal
|
||||
: page * pageSize + (rawList.length === pageSize ? 1 : 0),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Toast.show({ content: "获取使用记录失败", position: "top" });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = () =>
|
||||
typeOptions.find(o => o.value === type)?.label || "全部类型";
|
||||
const getStatusLabel = () =>
|
||||
statusOptions.find(o => o.value === status)?.label || "全部状态";
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon title="使用记录" />
|
||||
<div className={style.filters}>
|
||||
<Picker
|
||||
columns={[typeOptions]}
|
||||
visible={typeVisible}
|
||||
onClose={() => setTypeVisible(false)}
|
||||
value={[type]}
|
||||
onConfirm={val => {
|
||||
setType(val[0] as string);
|
||||
setTypeVisible(false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<div
|
||||
className={style.filterButton}
|
||||
onClick={() => setTypeVisible(true)}
|
||||
>
|
||||
<span className={style.filterIcon}>
|
||||
<DownOutline />
|
||||
</span>
|
||||
{getTypeLabel()}
|
||||
</div>
|
||||
)}
|
||||
</Picker>
|
||||
|
||||
<Picker
|
||||
columns={[statusOptions]}
|
||||
visible={statusVisible}
|
||||
onClose={() => setStatusVisible(false)}
|
||||
value={[status]}
|
||||
onConfirm={val => {
|
||||
setStatus(val[0] as string);
|
||||
setStatusVisible(false);
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<div
|
||||
className={style.filterButton}
|
||||
onClick={() => setStatusVisible(true)}
|
||||
>
|
||||
<span className={style.filterIcon}>
|
||||
<DownOutline />
|
||||
</span>
|
||||
{getStatusLabel()}
|
||||
</div>
|
||||
)}
|
||||
</Picker>
|
||||
</div>
|
||||
|
||||
<div className={style.summary}>
|
||||
<div className={style.summaryItem}>
|
||||
<div className={style.summaryNumber}>{records.length}</div>
|
||||
<div className={style.summaryLabel}>记录总数</div>
|
||||
</div>
|
||||
<div className={style.summaryItem}>
|
||||
<div className={style.summaryNumber}>
|
||||
{records.reduce(
|
||||
(acc, cur) => acc + (Number(cur.power) || 0),
|
||||
0,
|
||||
)}
|
||||
</div>
|
||||
<div className={style.summaryLabel}>总消耗算力</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<div className="pagination-container">
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showSizeChanger={false}
|
||||
onChange={newPage => {
|
||||
setPage(newPage);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style.page}>
|
||||
<div className={style.list}>
|
||||
{loading && records.length === 0 ? (
|
||||
<div className={style.loading}>加载中...</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className={style.empty}>暂无使用记录</div>
|
||||
) : (
|
||||
records.map(item => (
|
||||
<Card key={item.id} className={style.item}>
|
||||
<div className={style.itemHeader}>
|
||||
<div className={style.itemTitle}>
|
||||
{item.title || item.typeLabel || "使用任务"}
|
||||
</div>
|
||||
<Tag color={item.status === "已完成" ? "success" : "primary"}>
|
||||
{item.status}
|
||||
</Tag>
|
||||
</div>
|
||||
{item.description ? (
|
||||
<div className={style.itemDesc}>{item.description}</div>
|
||||
) : null}
|
||||
<div className={style.itemFooter}>
|
||||
<div className={style.power}>消耗 {item.power ?? 0} 算力</div>
|
||||
<div className={style.time}>{item.createTime}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsageRecords;
|
||||
@@ -9,6 +9,8 @@ import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index";
|
||||
import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index";
|
||||
import Recharge from "@/pages/mobile/mine/recharge/index";
|
||||
import RechargeOrder from "@/pages/mobile/mine/recharge/order/index";
|
||||
import BuyPower from "@/pages/mobile/mine/recharge/buy-power";
|
||||
import UsageRecords from "@/pages/mobile/mine/recharge/usage-records";
|
||||
import Setting from "@/pages/mobile/mine/setting/index";
|
||||
import SecuritySetting from "@/pages/mobile/mine/setting/SecuritySetting";
|
||||
import About from "@/pages/mobile/mine/setting/About";
|
||||
@@ -74,6 +76,16 @@ const routes = [
|
||||
element: <RechargeOrder />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/recharge/buy-power",
|
||||
element: <BuyPower />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/recharge/usage-records",
|
||||
element: <UsageRecords />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <Setting />,
|
||||
|
||||
Reference in New Issue
Block a user