Remove Dexie database implementation and related initialization logic. Update Recharge component to fetch and display subscription packages dynamically, enhancing user experience with improved payment handling and UI updates.

This commit is contained in:
超级老白兔
2025-10-27 17:07:10 +08:00
parent d01c91e6ee
commit 364a52b553
6 changed files with 177 additions and 399 deletions

View File

@@ -10,7 +10,6 @@
"antd-mobile-icons": "^0.3.0",
"axios": "^1.6.7",
"dayjs": "^1.11.13",
"dexie": "^4.2.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"react": "^18.2.0",

View File

@@ -7,38 +7,14 @@ import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import App from "./App";
import "./styles/global.scss";
import { db } from "@/utils/db"; // 引入数据库实例
// 设置dayjs为中文
dayjs.locale("zh-cn");
// 数据库初始化
async function initializeApp() {
try {
// 确保数据库已打开
await db.open();
console.log("数据库初始化成功");
// 调试模式:清理所有数据
console.log("调试模式:开始清理数据库数据...");
await db.kfUsers.clear();
await db.weChatGroup.clear();
await db.contracts.clear();
await db.newContractList.clear();
console.log("数据库数据清理完成");
} catch (error) {
console.error("数据库初始化失败:", error);
// 可以选择显示错误提示或使用降级方案
}
// 渲染应用
const root = createRoot(document.getElementById("root")!);
root.render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>,
);
}
// 启动应用
initializeApp();
// 渲染应用
const root = createRoot(document.getElementById("root")!);
root.render(
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>,
);

View File

@@ -0,0 +1,35 @@
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;
}
interface taocanList {
list: taocanItem[];
}
// 套餐列表
export function getTaocanList(): Promise<taocanList> {
return request("/v1/tokens/list", {}, "GET");
}
// 支付id和price 从套餐列表取对应的价格
export function pay(params: { id: string; price: number }) {
return request("/v1/tokens/pay", params, "POST");
}

View File

@@ -123,10 +123,18 @@
margin: 4px 0;
font-size: 16px;
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;

View File

@@ -1,7 +1,6 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card, Button, Toast, Tabs } from "antd-mobile";
import { useUserStore } from "@/store/module/user";
import { Card, Button, Toast, Tabs, Dialog } from "antd-mobile";
import style from "./index.module.scss";
import {
WalletOutlined,
@@ -12,36 +11,7 @@ import {
} from "@ant-design/icons";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
const quickAmounts = [50, 100, 200, 500, 1000];
// AI服务套餐数据
const aiServicePackages = [
{
id: 1,
name: "入门套餐",
tag: "推荐",
tagColor: "blue",
description: "适合个人用户体验AI服务",
usage: "可使用AI服务约110次",
price: 100,
originalPrice: 110,
gift: 10,
actualAmount: 110,
},
{
id: 2,
name: "标准套餐",
tag: "热门",
tagColor: "green",
description: "适合小团队日常使用",
usage: "可使用AI服务约580次",
price: 500,
originalPrice: 580,
gift: 80,
actualAmount: 580,
},
];
import { getTaocanList, pay } from "./api";
// AI服务列表数据
const aiServices = [
@@ -120,25 +90,77 @@ const versionPackages = [
const Recharge: React.FC = () => {
const navigate = useNavigate();
const { user } = useUserStore();
// 假设余额从后端接口获取实际可用props或store传递
const [balance, setBalance] = useState(0);
const [selected, setSelected] = useState<number | null>(null);
const [balance] = useState(0);
const [selected, setSelected] = useState<any | null>(null);
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState("account");
const [taocanList, setTaocanList] = useState<any[]>([]);
// 加载套餐列表
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();
}, []);
// 充值操作
const handleRecharge = async () => {
if (!selected) {
Toast.show({ content: "请选择充值金额", position: "top" });
Toast.show({ content: "请选择充值套餐", position: "top" });
return;
}
setLoading(true);
setTimeout(() => {
setBalance(b => b + selected);
Toast.show({ content: `充值成功,已到账¥${selected}` });
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);
}, 1200);
}
};
// 渲染账户充值tab内容
@@ -156,23 +178,80 @@ const Recharge: React.FC = () => {
</div>
</Card>
<Card className={style["quick-card"]}>
<div className={style["quick-title"]}></div>
<div className={style["quick-title"]}></div>
<div className={style["quick-list"]}>
{quickAmounts.map(amt => (
{taocanList.map(item => (
<Button
key={amt}
color={selected === amt ? "primary" : "default"}
key={item.id}
color={selected?.id === item.id ? "primary" : "default"}
className={
selected === amt
selected?.id === item.id
? style["quick-btn-active"]
: style["quick-btn"]
}
onClick={() => setSelected(amt)}
onClick={() => setSelected(item)}
>
{amt}
<div>
<div>{item.price / 100}</div>
{item.discount && (
<div style={{ fontSize: "12px", color: "#999" }}>
{item.discount}
</div>
)}
</div>
</Button>
))}
</div>
{selected && (
<div
style={{
marginBottom: "12px",
padding: "12px",
background: "#f5f5f5",
borderRadius: "8px",
}}
>
<div style={{ marginBottom: "6px" }}>
<span style={{ fontWeight: "500" }}>{selected.name}</span>
{selected.isRecommend === 1 && (
<span
style={{
marginLeft: "8px",
fontSize: "12px",
color: "#1890ff",
}}
>
</span>
)}
{selected.isHot === 1 && (
<span
style={{
marginLeft: "8px",
fontSize: "12px",
color: "#ff4d4f",
}}
>
</span>
)}
</div>
<div style={{ fontSize: "14px", color: "#666" }}>
{selected.tokens} Tokens
</div>
{selected.originalPrice && (
<div
style={{
fontSize: "12px",
color: "#999",
textDecoration: "line-through",
}}
>
: ¥{selected.originalPrice / 100}
</div>
)}
</div>
)}
<Button
block
color="primary"

View File

@@ -1,319 +0,0 @@
/**
* 数据库工具类 - 使用serverId作为主键的优化架构
*
* 架构设计:
* 1. 使用serverId作为数据库主键直接对应接口返回的id字段
* 2. 保留原始的id字段用于存储接口数据的完整性
* 3. 简化数据处理逻辑避免ID映射的复杂性
*
* 优势:
* - 直接使用服务器ID作为主键避免ID冲突
* - 保持数据的一致性和可追溯性
* - 简化查询逻辑,提高性能
* - 支持重复数据检测和去重
*
* 使用方法:
* - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
* - 查询数据:使用 findById(id) 根据原始ID查询或 findByPrimaryKey(serverId) 根据主键查询
* - 批量查询:使用 findByIds([id1, id2, ...]) 根据原始ID批量查询
* - 内部操作serverId作为主键用于数据库内部管理
*
* 示例:
* const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
* const serverId = await service.createWithServerId(serverData); // 存储返回serverId
* const data = await service.findById(1001); // 根据原始ID查询用户友好
* const dataByPK = await service.findByPrimaryKey(serverId); // 根据主键查询(内部使用)
*/
import Dexie, { Table } from "dexie";
import {
KfUserListData,
weChatGroup,
ContractData,
MessageListData,
} from "@/pages/pc/ckbox/data";
// 数据类型定义使用serverId作为主键
export interface KfUserWithServerId extends Omit<KfUserListData, "id"> {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
}
// 新联系人列表数据接口
export interface NewContactListData {
serverId: number | string; // 服务器ID作为主键
id?: number; // 接口数据的原始ID字段
groupName: string;
contacts: ContractData[];
weChatGroup: weChatGroup[];
}
// 数据库类
class CunkebaoDatabase extends Dexie {
kfUsers!: Table<KfUserWithServerId>;
weChatGroup!: Table<weChatGroup>;
contracts!: Table<ContractData>;
newContractList!: Table<NewContactListData>;
messageList!: Table<MessageListData>;
constructor() {
super("CunkebaoDatabase");
// 版本1使用serverId作为主键的架构
this.version(1).stores({
kfUsers:
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
weChatGroup:
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, unreadCount, notice, selfDisplyName",
contracts:
"serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
newContractList: "serverId, id, groupName, contacts",
messageList:
"serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, unreadCount, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
});
}
}
// 创建数据库实例
export const db = new CunkebaoDatabase();
// 简单的数据库操作类
export class DatabaseService<T> {
constructor(private table: Table<T>) {}
// 基础 CRUD 操作 - 使用serverId作为主键
async create(data: Omit<T, "serverId">): Promise<string | number> {
return await this.table.add(data as T);
}
// 创建数据(直接使用接口数据)
// 接口数据的id字段直接作为serverId主键原id字段保留
async createWithServerId(data: any): Promise<string | number> {
const dataToInsert = {
...data,
serverId: data.id, // 使用接口的id作为serverId主键
};
return await this.table.add(dataToInsert as T);
}
// 根据原始ID查询用户友好的查询方法
async findById(id: string | number): Promise<T | undefined> {
return await this.table.where("id").equals(id).first();
}
// 根据serverId查询内部主键查询
async findByPrimaryKey(serverId: string | number): Promise<T | undefined> {
return await this.table.get(serverId);
}
async findAll(): Promise<T[]> {
return await this.table.toArray();
}
async update(serverId: string | number, data: Partial<T>): Promise<number> {
return await this.table.update(serverId, data as any);
}
async updateMany(
dataList: { serverId: string | number; data: Partial<T> }[],
): Promise<number> {
return await this.table.bulkUpdate(
dataList.map(item => ({
key: item.serverId,
changes: item.data as any,
})),
);
}
async createMany(
dataList: Omit<T, "serverId">[],
): Promise<(string | number)[]> {
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
}
// 批量创建数据(直接使用接口数据)
// 接口数据的id字段直接作为serverId主键
async createManyWithServerId(dataList: any[]): Promise<(string | number)[]> {
// 检查是否存在重复的serverId
const serverIds = dataList.map(item => item.id);
const existingData = await this.table
.where("serverId")
.anyOf(serverIds)
.toArray();
const existingServerIds = new Set(
existingData.map((item: any) => item.serverId),
);
// 过滤掉已存在的数据
const newData = dataList.filter(item => !existingServerIds.has(item.id));
if (newData.length === 0) {
console.log("所有数据都已存在,跳过插入");
return [];
}
const processedData = newData.map(item => ({
...item,
serverId: item.id, // 使用接口的id作为serverId主键
}));
console.log(
`插入 ${processedData.length} 条新数据,跳过 ${dataList.length - newData.length} 条重复数据`,
);
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
}
async delete(serverId: string | number): Promise<void> {
await this.table.delete(serverId);
}
async clear(): Promise<void> {
await this.table.clear();
}
// 条件查询
async findWhere(field: keyof T, value: any): Promise<T[]> {
return await this.table
.where(field as string)
.equals(value)
.toArray();
}
// 根据服务器ID查询兼容性方法
async findByServerId(serverId: any): Promise<T | undefined> {
return await this.table.get(serverId);
}
// 根据原始ID批量查询
async findByIds(ids: (string | number)[]): Promise<T[]> {
return await this.table.where("id").anyOf(ids).toArray();
}
// 多值查询IN 查询)
async findWhereIn(field: keyof T, values: any[]): Promise<T[]> {
return await this.table
.where(field as string)
.anyOf(values)
.toArray();
}
// 范围查询
async findWhereBetween(field: keyof T, min: any, max: any): Promise<T[]> {
return await this.table
.where(field as string)
.between(min, max)
.toArray();
}
// 模糊查询(以指定字符串开头)
async findWhereStartsWith(field: keyof T, prefix: string): Promise<T[]> {
return await this.table
.where(field as string)
.startsWith(prefix)
.toArray();
}
// 不等于查询
async findWhereNot(field: keyof T, value: any): Promise<T[]> {
return await this.table
.where(field as string)
.notEqual(value)
.toArray();
}
// 大于查询
async findWhereGreaterThan(field: keyof T, value: any): Promise<T[]> {
return await this.table
.where(field as string)
.above(value)
.toArray();
}
// 小于查询
async findWhereLessThan(field: keyof T, value: any): Promise<T[]> {
return await this.table
.where(field as string)
.below(value)
.toArray();
}
// 复合条件查询
async findWhereMultiple(
conditions: {
field: keyof T;
operator: "equals" | "above" | "below" | "startsWith";
value: any;
}[],
): Promise<T[]> {
let collection = this.table.toCollection();
for (const condition of conditions) {
const { field, operator, value } = condition;
collection = collection.and(item => {
const fieldValue = (item as any)[field];
switch (operator) {
case "equals":
return fieldValue === value;
case "above":
return fieldValue > value;
case "below":
return fieldValue < value;
case "startsWith":
return (
typeof fieldValue === "string" && fieldValue.startsWith(value)
);
default:
return true;
}
});
}
return await collection.toArray();
}
// 分页查询
async findWithPagination(
page: number = 1,
limit: number = 10,
): Promise<{ data: T[]; total: number; page: number; limit: number }> {
const offset = (page - 1) * limit;
const total = await this.table.count();
const data = await this.table.offset(offset).limit(limit).toArray();
return { data, total, page, limit };
}
// 排序查询
async findAllSorted(
field: keyof T,
direction: "asc" | "desc" = "asc",
): Promise<T[]> {
const collection = this.table.orderBy(field as string);
return direction === "desc"
? await collection.reverse().toArray()
: await collection.toArray();
}
// 统计
async count(): Promise<number> {
return await this.table.count();
}
// 条件统计
async countWhere(field: keyof T, value: any): Promise<number> {
return await this.table
.where(field as string)
.equals(value)
.count();
}
}
// 创建各表的服务实例
export const kfUserService = new DatabaseService(db.kfUsers);
export const weChatGroupService = new DatabaseService(db.weChatGroup);
export const contractService = new DatabaseService(db.contracts);
export const newContactListService = new DatabaseService(db.newContractList);
export const messageListService = new DatabaseService(db.messageList);
// 默认导出数据库实例
export default db;