From 83a614bd7587eb84b996f45d5e0ed9ddb347de61 Mon Sep 17 00:00:00 2001 From: yuj Date: Thu, 4 Dec 2025 17:51:24 +0800 Subject: [PATCH] =?UTF-8?q?feat():learning=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_PATTERNS.md | 557 ++ DOCUMENTATION_INDEX.md | 306 ++ Dockerfile | 33 + EXPLORATION_SUMMARY.md | 303 ++ EXPLORATION_SUMMARY.txt | 113 + FRONTEND_GUIDE.md | 1527 ++++++ PROJECT_ARCHITECTURE.md | 480 ++ QUICK_REFERENCE.md | 392 ++ QUICK_START.md | 566 ++ README.md | 178 + README_ENV.md | 78 + build-and-push.sh | 82 + env.example | 15 + eslint.config.js | 23 + index.html | 13 + k8s/configmap.yaml | 49 + k8s/deployment.yaml | 58 + k8s/httproute.yaml | 38 + k8s/service.yaml | 16 + nginx.conf | 42 + package-lock.json | 4769 +++++++++++++++++ package.json | 36 + public/vite.svg | 1 + src/App.css | 214 + src/App.tsx | 276 + src/assets/react.svg | 1 + src/atoms/auth.ts | 36 + src/components/AuthGuard.tsx | 90 + src/components/DynamicMenu.css | 108 + src/components/DynamicMenu.tsx | 250 + src/components/Layout.tsx | 391 ++ src/components/PagePermissionGuard.tsx | 24 + src/components/PermissionGuard.tsx | 57 + src/components/QuotaCharts.tsx | 142 + src/components/QuotaFilters.tsx | 162 + src/components/QuotaHistoryTable.tsx | 118 + src/components/QuotaStats.tsx | 82 + src/components/UserProjectQuotaPage.tsx | 87 + src/hooks/useAuth.ts | 415 ++ src/hooks/usePagePermissions.ts | 74 + src/hooks/usePermissions.ts | 46 + src/index.css | 1 + src/main.tsx | 28 + src/pages/AuditLogs.tsx | 315 ++ src/pages/Dashboard.tsx | 42 + src/pages/Finance.tsx | 47 + .../FinanceMcpAccountRechargeRecords.tsx | 544 ++ src/pages/FinanceMcpUsage.tsx | 85 + .../FinanceModelAccountRechargeRecords.tsx | 567 ++ src/pages/FinancePaymentRecords.tsx | 165 + src/pages/FinanceSandboxCosts.tsx | 79 + src/pages/FinanceTokenUsage.tsx | 92 + src/pages/FinanceTransactionLogs.tsx | 111 + src/pages/GoalfyMaxUsers.tsx | 532 ++ src/pages/InviteCodes.tsx | 954 ++++ src/pages/MCPProviderPricing.tsx | 517 ++ src/pages/MessagePush.tsx | 566 ++ src/pages/Monitoring.tsx | 83 + src/pages/NoPermission.tsx | 42 + src/pages/Operations.tsx | 51 + src/pages/Overview.tsx | 42 + src/pages/RoleManagement.tsx | 634 +++ src/pages/SystemConfigs.tsx | 283 + src/pages/SystemHealth.tsx | 27 + src/pages/TokenAnalytics.tsx | 27 + src/pages/TokenHistory.tsx | 190 + src/pages/UserFeedback.tsx | 489 ++ src/pages/UserLevelConfigs.tsx | 328 ++ src/pages/UserManagement.tsx | 585 ++ src/pages/UserProjectQuota.tsx | 202 + src/pages/VendorModelPricing.tsx | 418 ++ src/pages/VmPricing.tsx | 732 +++ src/pages/__tests__/InviteCodes.test.tsx | 109 + src/routes/DynamicRoutes.tsx | 95 + src/services/api.ts | 911 ++++ src/services/auditLogApi.ts | 21 + src/services/inviteCodeApi.ts | 59 + src/services/inviteCodeApplicationApi.ts | 69 + src/services/mcpProviderApi.ts | 63 + src/services/roleApi.ts | 118 + src/services/systemConfigApi.ts | 65 + src/services/userApi.ts | 188 + src/services/userLevelConfigApi.ts | 59 + src/types/auditLog.ts | 34 + src/types/inviteCode.ts | 56 + src/types/inviteCodeApplication.ts | 64 + src/types/mcpProvider.ts | 67 + src/types/quota.ts | 66 + src/types/systemConfig.ts | 50 + src/types/userLevelConfig.ts | 55 + src/types/userProjectQuota.ts | 25 + src/utils/storageMigration.ts | 22 + src/vite-env.d.ts | 14 + tsconfig.app.json | 29 + tsconfig.json | 7 + tsconfig.node.json | 26 + vite.config.ts | 26 + 97 files changed, 23324 insertions(+) create mode 100644 API_PATTERNS.md create mode 100644 DOCUMENTATION_INDEX.md create mode 100644 Dockerfile create mode 100644 EXPLORATION_SUMMARY.md create mode 100644 EXPLORATION_SUMMARY.txt create mode 100644 FRONTEND_GUIDE.md create mode 100644 PROJECT_ARCHITECTURE.md create mode 100644 QUICK_REFERENCE.md create mode 100644 QUICK_START.md create mode 100644 README.md create mode 100644 README_ENV.md create mode 100755 build-and-push.sh create mode 100644 env.example create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 k8s/configmap.yaml create mode 100644 k8s/deployment.yaml create mode 100644 k8s/httproute.yaml create mode 100644 k8s/service.yaml create mode 100644 nginx.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/assets/react.svg create mode 100644 src/atoms/auth.ts create mode 100644 src/components/AuthGuard.tsx create mode 100644 src/components/DynamicMenu.css create mode 100644 src/components/DynamicMenu.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/PagePermissionGuard.tsx create mode 100644 src/components/PermissionGuard.tsx create mode 100644 src/components/QuotaCharts.tsx create mode 100644 src/components/QuotaFilters.tsx create mode 100644 src/components/QuotaHistoryTable.tsx create mode 100644 src/components/QuotaStats.tsx create mode 100644 src/components/UserProjectQuotaPage.tsx create mode 100644 src/hooks/useAuth.ts create mode 100644 src/hooks/usePagePermissions.ts create mode 100644 src/hooks/usePermissions.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/pages/AuditLogs.tsx create mode 100644 src/pages/Dashboard.tsx create mode 100644 src/pages/Finance.tsx create mode 100644 src/pages/FinanceMcpAccountRechargeRecords.tsx create mode 100644 src/pages/FinanceMcpUsage.tsx create mode 100644 src/pages/FinanceModelAccountRechargeRecords.tsx create mode 100644 src/pages/FinancePaymentRecords.tsx create mode 100644 src/pages/FinanceSandboxCosts.tsx create mode 100644 src/pages/FinanceTokenUsage.tsx create mode 100644 src/pages/FinanceTransactionLogs.tsx create mode 100644 src/pages/GoalfyMaxUsers.tsx create mode 100644 src/pages/InviteCodes.tsx create mode 100644 src/pages/MCPProviderPricing.tsx create mode 100644 src/pages/MessagePush.tsx create mode 100644 src/pages/Monitoring.tsx create mode 100644 src/pages/NoPermission.tsx create mode 100644 src/pages/Operations.tsx create mode 100644 src/pages/Overview.tsx create mode 100644 src/pages/RoleManagement.tsx create mode 100644 src/pages/SystemConfigs.tsx create mode 100644 src/pages/SystemHealth.tsx create mode 100644 src/pages/TokenAnalytics.tsx create mode 100644 src/pages/TokenHistory.tsx create mode 100644 src/pages/UserFeedback.tsx create mode 100644 src/pages/UserLevelConfigs.tsx create mode 100644 src/pages/UserManagement.tsx create mode 100644 src/pages/UserProjectQuota.tsx create mode 100644 src/pages/VendorModelPricing.tsx create mode 100644 src/pages/VmPricing.tsx create mode 100644 src/pages/__tests__/InviteCodes.test.tsx create mode 100644 src/routes/DynamicRoutes.tsx create mode 100644 src/services/api.ts create mode 100644 src/services/auditLogApi.ts create mode 100644 src/services/inviteCodeApi.ts create mode 100644 src/services/inviteCodeApplicationApi.ts create mode 100644 src/services/mcpProviderApi.ts create mode 100644 src/services/roleApi.ts create mode 100644 src/services/systemConfigApi.ts create mode 100644 src/services/userApi.ts create mode 100644 src/services/userLevelConfigApi.ts create mode 100644 src/types/auditLog.ts create mode 100644 src/types/inviteCode.ts create mode 100644 src/types/inviteCodeApplication.ts create mode 100644 src/types/mcpProvider.ts create mode 100644 src/types/quota.ts create mode 100644 src/types/systemConfig.ts create mode 100644 src/types/userLevelConfig.ts create mode 100644 src/types/userProjectQuota.ts create mode 100644 src/utils/storageMigration.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/API_PATTERNS.md b/API_PATTERNS.md new file mode 100644 index 0000000..2f2e091 --- /dev/null +++ b/API_PATTERNS.md @@ -0,0 +1,557 @@ +# API 调用模式和通用实现指南 + +## 核心 API 架构 + +### ApiClient 核心类 + +**位置**:`src/services/api.ts` + +```typescript +class ApiClient { + private instance: any; + private isRefreshing = false; + private failedQueue: Array<{ + resolve: (value: any) => void; + reject: (reason: any) => void; + }> = []; + + // 特性: + // 1. 自动附加 Authorization 头 + // 2. 401 错误时自动刷新 token + // 3. Token 刷新期间的请求入队 + // 4. 自动处理 token 过期和重新登录 +} + +export const apiClient = new ApiClient(); +``` + +--- + +## API 服务层设计模式 + +### 模式 1:列表 API(用户等级配置) + +**文件**:`src/services/userLevelConfigApi.ts` + +```typescript +import { apiClient } from './api'; +import type { UserLevelConfig, UserLevelConfigListRequest, UserLevelConfigListResponse } from '../types/userLevelConfig'; + +// 模式:响应数据提取 +// - apiClient 返回完整响应对象 +// - 最后一层 API 函数负责提取数据或返回完整响应 + +export const getUserLevelConfigList = async ( + params: UserLevelConfigListRequest +): Promise => { + const response = await apiClient.get('/admin/user-level-configs', { params }); + return response.data; // 返回响应数据部分 +}; + +export const getAllUserLevelConfigs = async (): Promise => { + const response = await apiClient.get('/admin/user-level-configs/all'); + return response.data; +}; +``` + +### 模式 2:列表 API(系统配置) + +**文件**:`src/services/systemConfigApi.ts` + +```typescript +import { apiClient } from './api'; +import type { SystemConfig, SystemConfigListRequest, SystemConfigListResponse } from '../types/systemConfig'; + +// 注意:不同的 API 可能返回不同的数据结构 +// 系统配置返回完整响应对象(包含 code, message, data) + +export const getSystemConfigList = async ( + params: SystemConfigListRequest +): Promise => { + const response = await apiClient.get('/admin/system-configs', { params }); + return response; // 返回完整响应对象 +}; +``` + +### 模式 3:CRUD 操作 + +```typescript +// 创建 +export const createUserLevelConfig = async ( + data: UserLevelConfigCreateRequest +): Promise => { + const response = await apiClient.post('/admin/user-level-configs', data); + return response.data; +}; + +// 读取 +export const getUserLevelConfigById = async (id: number): Promise => { + const response = await apiClient.get(`/admin/user-level-configs/${id}`); + return response.data; +}; + +// 更新 +export const updateUserLevelConfig = async ( + id: number, + data: UserLevelConfigUpdateRequest +): Promise => { + const response = await apiClient.put(`/admin/user-level-configs/${id}`, data); + return response.data; +}; + +// 删除 +export const deleteUserLevelConfig = async (id: number): Promise => { + await apiClient.delete(`/admin/user-level-configs/${id}`); +}; + +// 特殊操作:状态更新 +export const updateUserLevelConfigStatus = async ( + id: number, + data: UserLevelConfigStatusRequest +): Promise => { + await apiClient.put(`/admin/user-level-configs/${id}/status`, data); +}; +``` + +--- + +## 类型定义模式 + +### 完整类型定义示例 + +**文件**:`src/types/userLevelConfig.ts` + +```typescript +// 1. 主体数据类型 +export interface UserLevelConfig { + id: number; + level_name: string; + level_code: string; + project_limit: number; + description: string; + sort_order: number; + status: number; // 1=启用, 0=禁用 + created_at: string; + updated_at: string; +} + +// 2. 请求类型 +export interface UserLevelConfigListRequest { + level_name?: string; + status?: number; + page?: number; + size?: number; +} + +export interface UserLevelConfigCreateRequest { + level_name: string; + level_code: string; + project_limit: number; + description?: string; + sort_order?: number; +} + +export interface UserLevelConfigUpdateRequest { + level_name: string; + project_limit: number; + description?: string; + sort_order?: number; +} + +export interface UserLevelConfigStatusRequest { + status: number; +} + +// 3. 响应类型 +export interface UserLevelConfigListResponse { + data: UserLevelConfig[]; + total: number; + page: number; + size: number; +} +``` + +--- + +## 页面组件实现模式 + +### CRUD 页面的标准结构 + +**文件**:`src/pages/UserLevelConfigs.tsx` + +```typescript +import React, { useEffect, useState } from 'react'; +import { Table, Button, Modal, Form, Input, message } from 'antd'; +import type { UserLevelConfig } from '../types/userLevelConfig'; +import { getUserLevelConfigList, createUserLevelConfig, updateUserLevelConfig, deleteUserLevelConfig } from '../services/userLevelConfigApi'; + +export default function UserLevelConfigs() { + // ==================== 状态管理 ==================== + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + + // 编辑弹窗状态 + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + + // 创建弹窗状态 + const [createOpen, setCreateOpen] = useState(false); + const [createForm] = Form.useForm(); + + // ==================== 数据加载 ==================== + const fetchList = async () => { + setLoading(true); + try { + const res = await getUserLevelConfigList({ page, size }); + // 处理响应数据:兼容数组和对象两种格式 + if (Array.isArray(res)) { + setList(res); + setTotal(res.length); + } else { + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } + } catch (e) { + console.error('获取列表失败:', e); + message.error('获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + // ==================== 创建操作 ==================== + const openCreate = () => { + createForm.resetFields(); + setCreateOpen(true); + }; + + const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createUserLevelConfig(values); + message.success('创建成功'); + setCreateOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败'); + } + }; + + // ==================== 编辑操作 ==================== + const openEdit = (config: UserLevelConfig) => { + setEditing(config); + form.setFieldsValue({ + level_name: config.level_name, + project_limit: config.project_limit, + description: config.description, + sort_order: config.sort_order, + }); + setEditOpen(true); + }; + + const submitEdit = async () => { + try { + const values = await form.validateFields(); + if (!editing) return; + await updateUserLevelConfig(editing.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); + } catch (error) { + message.error('更新失败'); + } + }; + + // ==================== 删除操作 ==================== + const handleDelete = async (config: UserLevelConfig) => { + try { + await deleteUserLevelConfig(config.id); + message.success('删除成功'); + fetchList(); + } catch (error) { + message.error('删除失败'); + } + }; + + // ==================== 渲染 ==================== + const columns = [ + // 列定义... + ]; + + return ( +
+ {/* 主表格 */} + + + {/* 创建弹窗 */} + +
+ {/* 表单字段 */} + +
+ + {/* 编辑弹窗 */} + +
+ {/* 表单字段 */} + +
+ + ); +} +``` + +--- + +## 权限和路由集成 + +### 添加新页面的完整检查清单 + +#### 1. 创建类型定义 +- [ ] `src/types/xxxConfig.ts` - 定义数据模型和请求/响应类型 + +#### 2. 创建 API 服务 +- [ ] `src/services/xxxConfigApi.ts` - 实现 CRUD API 方法 + +#### 3. 创建页面组件 +- [ ] `src/pages/XxxConfigs.tsx` - 实现 CRUD UI + +#### 4. 注册路由 +- [ ] `src/App.tsx` - 添加路由定义 + ```typescript + } /> + ``` + +#### 5. 添加菜单项 +- [ ] `src/components/DynamicMenu.tsx` - 添加菜单项定义 + ```typescript + { + key: 'system-xxx-configs', + label: 'Xxx 配置', + icon: , + path: '/xxx-configs', + } + ``` + +#### 6. 更新 Layout 子导航 +- [ ] `src/components/Layout.tsx` - 添加子导航按钮(如果需要) + ```typescript + + ``` + +--- + +## 错误处理最佳实践 + +### API 错误处理 + +```typescript +// 页面中的错误处理 +const fetchList = async () => { + try { + const res = await getUserLevelConfigList({ page, size }); + setList(res?.data ?? []); + } catch (error: any) { + // 1. 优先使用 API 返回的错误信息 + const errorMsg = error?.response?.data?.message; + + // 2. 其次使用通用错误信息 + message.error(errorMsg || '获取列表失败'); + + // 3. 记录详细错误用于调试 + console.error('获取列表失败:', error); + } +}; + +// 创建/更新时的错误处理 +const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createUserLevelConfig(values); + message.success('创建成功'); + } catch (error: any) { + // 区分表单验证错误和 API 错误 + if (error?.response?.data?.message) { + message.error(error.response.data.message); + } else { + message.error('创建失败'); + } + } +}; +``` + +--- + +## 通用 CRUD 表格列定义 + +### 标准列定义模式 + +```typescript +const columns = [ + // 基本字段列 + { + title: '名称', + dataIndex: 'level_name', + key: 'level_name', + }, + { + title: '代码', + dataIndex: 'level_code', + key: 'level_code', + }, + // 数值列(带格式化) + { + title: '数值', + dataIndex: 'value', + key: 'value', + render: (value: number) => (value === 0 ? '不限' : value), + }, + // 状态列(带标签) + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (value: number) => + value === 1 ? 启用 : 禁用, + }, + // 操作列(最后) + { + title: '操作', + key: 'action', + render: (_: any, record: T) => ( + + + + + ), + }, +]; +``` + +--- + +## 表单字段最佳实践 + +### 创建表单示例 + +```typescript +
+ {/* 必填字段 */} + + + + + {/* 代码字段(创建时必填,编辑时只读) */} + + + + + {/* 数值字段 */} + + + + + {/* 可选文本字段 */} + + + + + {/* 排序字段 */} + + + + +``` + +--- + +## 分页实现 + +### 标准分页逻辑 + +```typescript +// 状态 +const [page, setPage] = useState(1); +const [size, setSize] = useState(10); +const [total, setTotal] = useState(0); + +// 依赖项变化时重新加载 +useEffect(() => { + fetchList(); +}, [page, size]); + +// 表格分页配置 +
{ + setPage(p); + setSize(s); + }, + }} +/> +``` + +--- + +## 总结 + +### API 调用流程 + +``` +用户操作 + ↓ +页面组件(使用 API 函数) + ↓ +API 服务函数(处理参数和响应) + ↓ +ApiClient(自动处理 token 和拦截器) + ↓ +Axios(HTTP 请求) + ↓ +后端 API +``` + +### 关键要点 + +1. **分层设计**:每一层职责清晰 +2. **类型安全**:完整的 TypeScript 类型支持 +3. **错误处理**:统一的错误处理机制 +4. **权限集成**:无缝的权限管理 +5. **状态管理**:简化的本地状态管理 +6. **自动化**:自动 Token 管理和刷新 + diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md new file mode 100644 index 0000000..c73bb33 --- /dev/null +++ b/DOCUMENTATION_INDEX.md @@ -0,0 +1,306 @@ +# Goalfymax 管理后台 - 文档索引 + +## 文档导航地图 + +本项目包含全面的文档,帮助快速理解和开发。以下是各文档的用途和推荐阅读顺序。 + +--- + +## 文档列表 + +### 1. EXPLORATION_SUMMARY.md (项目探索总结) +**大小**: 303 行 | **阅读时间**: 8-10 分钟 +**用途**: 快速理解项目整体架构和现状 +**适合**: 初次接触项目的人 + +**包含内容**: +- 项目总体评估 +- 关键发现(双层路由、API 抽象、CRUD 模式、类型支持) +- 已实现的所有功能列表 +- 代码质量评估(优点和改进空间) +- 文件统计和最近开发活动 +- 技术栈详解 +- 学习路径建议 +- 改进建议(短期、中期、长期) + +**开始阅读**: 首先阅读这个文档了解项目全貌 + +--- + +### 2. PROJECT_ARCHITECTURE.md (项目架构详解) +**大小**: 480 行 | **阅读时间**: 15-20 分钟 +**用途**: 深入理解项目的架构设计 +**适合**: 想要理解系统设计的开发者 + +**包含内容**: +- 完整的目录结构说明 +- 核心架构设计(路由、菜单、API、权限) +- 关键文件详解(用户等级配置、系统配置、Layout、认证) +- API 端点规范 +- 权限系统详解 +- 页面模块划分 +- 通用 CRUD 页面模式 +- 代码架构建议 +- 开发指南(如何添加新功能) + +**使用场景**: +- 理解整体系统架构 +- 决定功能应该放在哪里 +- 理解权限管理机制 + +--- + +### 3. API_PATTERNS.md (API 调用模式) +**大小**: 557 行 | **阅读时间**: 15-20 分钟 +**用途**: 学习如何正确调用 API 和实现功能 +**适合**: 需要编写 API 服务和页面的开发者 + +**包含内容**: +- ApiClient 核心类说明 +- 3 种 API 服务设计模式(列表、CRUD、特殊操作) +- 完整的类型定义模板 +- CRUD 页面组件实现模式 +- 权限和路由集成 +- 添加新页面的完整检查清单 +- 错误处理最佳实践 +- 通用 CRUD 表格列定义 +- 表单字段最佳实践 +- 分页实现 + +**使用场景**: +- 添加新的管理功能 +- 实现 API 服务 +- 创建页面组件 +- 学习最佳实践 + +--- + +### 4. QUICK_REFERENCE.md (快速参考) +**大小**: 392 行 | **阅读时间**: 5-10 分钟 (查阅用) +**用途**: 快速查找常见信息 +**适合**: 需要快速找到某个特定信息的开发者 + +**包含内容**: +- 项目基本信息表 +- 目录速查表 +- 关键概念速览 +- 常见任务速查(添加新功能的步骤) +- 重要文件速查表 +- API 端点速查 +- 代码片段库(复制即用) +- 常见错误和解决方案 +- 性能优化建议 +- 开发工作流标准步骤 +- 有用的命令 +- 常用类名和常量 + +**使用场景**: +- 快速查找某个文件的作用 +- 复制代码模板 +- 查询常见错误的解决方案 +- 回忆某个 API 端点 + +--- + +### 5. QUICK_START.md (快速开始) +**大小**: 566 行 | **阅读时间**: 10-15 分钟 +**用途**: 快速启动项目和理解基础 +**适合**: 刚开始使用项目的开发者 + +**包含内容**: +- 环境要求 +- 项目初始化 +- 开发服务器启动 +- 构建和部署 +- 项目结构概览 +- 核心技术说明 +- 认证和权限系统 +- 路由说明 +- 常用 npm 命令 + +--- + +### 6. FRONTEND_GUIDE.md (前端开发指南) +**大小**: 1527 行 | **阅读时间**: 25-30 分钟 +**用途**: 全面的前端开发指南 +**适合**: 需要全面理解前端实现的开发者 + +**包含内容**: +- 项目概述和技术栈 +- 完整的项目结构说明 +- 核心架构和设计模式 +- 关键特性详解 +- 开发最佳实践 +- 常见模式和示例 +- 故障排除指南 +- 性能优化指南 + +--- + +## 推荐阅读路径 + +### 路径 1: 快速上手(2-3 小时) +适合需要快速理解项目基础的新人 +``` +1. EXPLORATION_SUMMARY.md (8 分钟) + ↓ +2. QUICK_START.md (12 分钟) + ↓ +3. QUICK_REFERENCE.md (10 分钟,仅浏览目录) + ↓ +4. 查看具体代码文件 + - src/App.tsx + - src/services/userLevelConfigApi.ts + - src/pages/UserLevelConfigs.tsx +``` + +### 路径 2: 深度理解(4-5 小时) +适合需要全面理解架构的开发者 +``` +1. EXPLORATION_SUMMARY.md (8 分钟) + ↓ +2. PROJECT_ARCHITECTURE.md (20 分钟) + ↓ +3. QUICK_REFERENCE.md (10 分钟) + ↓ +4. API_PATTERNS.md (20 分钟) + ↓ +5. FRONTEND_GUIDE.md (30 分钟) + ↓ +6. 研究关键代码文件 + - src/services/api.ts (ApiClient 类) + - src/components/DynamicMenu.tsx (权限过滤) + - src/components/Layout.tsx (布局结构) +``` + +### 路径 3: 开发新功能(1-2 小时) +适合需要立即开始开发的人 +``` +1. QUICK_REFERENCE.md - 常见任务速查部分 (5 分钟) + ↓ +2. API_PATTERNS.md - 相关部分 (10 分钟) + ↓ +3. 复制模板代码,开始开发 + ↓ +4. 遇到问题时查阅相关文档 +``` + +--- + +## 按主题查找文档 + +### 我需要了解... + +| 主题 | 首选文档 | 相关部分 | +|------|---------|---------| +| 项目整体架构 | PROJECT_ARCHITECTURE.md | "核心架构设计" | +| 如何添加新功能 | API_PATTERNS.md | "权限和路由集成" | +| API 调用方式 | API_PATTERNS.md | "API 服务层设计模式" | +| 权限和菜单 | PROJECT_ARCHITECTURE.md | "权限系统" | +| 如何启动项目 | QUICK_START.md | "项目初始化" | +| 某个文件的作用 | QUICK_REFERENCE.md | "重要文件速查" | +| 某个 API 端点 | QUICK_REFERENCE.md | "API 端点速查" | +| 常见错误解决 | QUICK_REFERENCE.md | "常见错误和解决方案" | +| 代码最佳实践 | API_PATTERNS.md | "最佳实践" 部分 | +| 项目文件统计 | EXPLORATION_SUMMARY.md | "文件统计" | +| 技术栈详情 | EXPLORATION_SUMMARY.md | "技术栈详解" | + +--- + +## 文档使用技巧 + +### 快速导航 +1. 使用文档内的目录跳转(通常在开头) +2. 使用编辑器的搜索功能 (Ctrl+F / Cmd+F) 查找特定内容 +3. 从 QUICK_REFERENCE.md 的表格快速定位信息 + +### 复制代码 +1. 在 API_PATTERNS.md 中找到相应的代码片段 +2. 直接复制到你的项目中 +3. 根据需要修改变量名和 API 端点 + +### 学习流程 +1. 首先阅读相关概念的说明 +2. 查看具体的代码例子 +3. 理解代码背后的原理 +4. 在自己的代码中应用 + +--- + +## 文档的版本和更新 + +- **生成时间**: 2025-10-28 +- **项目状态**: 活跃开发中 +- **最后更新**: 正在添加系统配置管理功能 + +### 何时更新文档 +- 添加新的主要功能时 +- 更改了架构或模式时 +- 添加新的最佳实践或约定时 + +--- + +## 相关资源链接 + +### 项目内文档 +- [项目根目录](.) - 所有文档都在这里 + +### 外部技术文档 +- [Ant Design 官方文档](https://ant.design/) - UI 组件库 +- [React Router 官方文档](https://reactrouter.com/) - 路由管理 +- [Jotai 官方文档](https://jotai.org/) - 状态管理 +- [TypeScript 官方文档](https://www.typescriptlang.org/) - 类型系统 +- [Axios 官方文档](https://axios-http.com/) - HTTP 客户端 +- [Vite 官方文档](https://vitejs.dev/) - 构建工具 + +--- + +## 获得帮助 + +### 如果你... + +**不知道从哪里开始** +→ 从 EXPLORATION_SUMMARY.md 开始,了解项目整体情况 + +**需要快速实现一个功能** +→ 查看 API_PATTERNS.md 的"权限和路由集成"部分 + +**遇到了 bug 或问题** +→ 查看 QUICK_REFERENCE.md 的"常见错误和解决方案"部分 + +**需要详细了解某个概念** +→ 查看 PROJECT_ARCHITECTURE.md 的相关章节 + +**需要查找某个 API 端点** +→ 查看 QUICK_REFERENCE.md 的"API 端点速查"部分 + +**想学习最佳实践** +→ 阅读 API_PATTERNS.md 和 FRONTEND_GUIDE.md + +--- + +## 文档质量反馈 + +这些文档的目标是帮助你快速上手并高效开发。如果你发现: +- 某个部分不清楚 +- 缺少某个重要概念的解释 +- 代码示例有错误 +- 可以改进的地方 + +欢迎提出改进建议。 + +--- + +## 总结 + +本项目提供的文档覆盖了从快速上手到深度理解的全部内容: + +✅ **EXPLORATION_SUMMARY.md** - 项目全貌 +✅ **PROJECT_ARCHITECTURE.md** - 架构深度解析 +✅ **API_PATTERNS.md** - 开发模式和最佳实践 +✅ **QUICK_REFERENCE.md** - 快速查阅工具 +✅ **QUICK_START.md** - 快速开始指南 +✅ **FRONTEND_GUIDE.md** - 全面的前端指南 + +根据你的需求选择合适的文档,祝开发顺利! + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2372657 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# 多阶段构建 Dockerfile +# 阶段1:构建阶段 +FROM node:20-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 复制 package.json 和 package-lock.json +COPY package*.json ./ + +# 安装依赖 +RUN npm ci + +# 复制项目文件 +COPY . . + +# 构建生产版本 +RUN npm run build + +# 阶段2:运行阶段 +FROM nginx:alpine + +# 复制构建产物到 nginx +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露端口 +EXPOSE 80 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/EXPLORATION_SUMMARY.md b/EXPLORATION_SUMMARY.md new file mode 100644 index 0000000..ad0faa6 --- /dev/null +++ b/EXPLORATION_SUMMARY.md @@ -0,0 +1,303 @@ +# Goalfymax 管理后台 - 项目探索总结 + +## 项目总体评估 + +这是一个**架构清晰、类型安全的 React 管理后台应用**,采用现代化技术栈和最佳实践。 + +--- + +## 关键发现 + +### 1. 双层路由系统 + +项目实现了巧妙的权限管理策略: +- **App.tsx**:定义所有可用路由(静态) +- **DynamicRoutes.tsx**:根据用户权限动态加载路由(备用) +- **DynamicMenu.tsx**:权限感知的菜单项过滤 + +这使得权限管理贯穿整个应用的路由、菜单和 UI 层级。 + +### 2. 完整的 API 抽象层 + +``` +3 层架构: +┌─────────────────┐ +│ UI 组件 │ (页面) +├─────────────────┤ +│ API 服务层 │ (xxxApi.ts) +├─────────────────┤ +│ ApiClient │ (api.ts 中的类) +├─────────────────┤ +│ Axios │ + 拦截器 +└─────────────────┘ +``` + +特点: +- 自动 Token 管理(请求自动附加 Authorization 头) +- 自动 Token 刷新(401 错误时) +- 请求队列机制(刷新期间的请求不会重复) + +### 3. 统一的 CRUD 模式 + +UserLevelConfigs 和 SystemConfigs 两个新增功能遵循相同的模式: +- 3 层结构(页面 + 服务 + 类型) +- 列表 + 创建 + 编辑 + 删除 + 状态切换 +- Modal 弹窗实现 +- 表格形式展现 + +这可以作为未来添加新管理功能的模板。 + +### 4. 完整的类型支持 + +每个功能都配套完整的 TypeScript 类型: +- 实体类型(如 UserLevelConfig) +- 请求类型(如 UserLevelConfigListRequest) +- 响应类型(如 UserLevelConfigListResponse) + +--- + +## 已实现的功能 + +### 核心功能 +- [x] 单点登录(SSO)集成 +- [x] 基于角色的访问控制(RBAC) +- [x] 动态菜单系统 +- [x] 权限感知路由 +- [x] 自动 Token 刷新 + +### 管理功能 +- [x] 仪表盘(Dashboard) +- [x] 系统用户管理 +- [x] 角色管理 +- [x] 配额规则管理 +- [x] 用户项目配额管理 +- [x] GoalfyMax 用户管理 +- [x] **用户等级配置**(新) +- [x] **系统配置**(新) +- [x] 用户反馈管理 +- [x] 消息推送管理 +- [x] 供应商模型价格管理 + +### 监控和分析 +- [x] Token 历史记录 +- [x] Token 分析 +- [x] 系统健康检查 + +--- + +## 代码质量评估 + +### 优点 + +| 方面 | 评价 | +|------|------| +| **架构** | 清晰的分层结构,职责分离 | +| **类型安全** | 完整的 TypeScript 支持 | +| **权限管理** | 多层次的权限检查机制 | +| **可维护性** | 统一的 CRUD 模式,便于维护 | +| **可扩展性** | 易于添加新的管理功能 | +| **错误处理** | 统一的 API 错误处理 | +| **Token 管理** | 自动化的 Token 刷新机制 | + +### 需要改进的地方 + +| 问题 | 严重程度 | 建议 | +|------|---------|------| +| 页面文件过大 | 中 | 拆分为子组件(表格、表单等) | +| CRUD 代码重复 | 中 | 提取通用 Hook 或高阶组件 | +| API 响应处理不一致 | 低 | 统一响应提取逻辑 | +| 加载状态管理 | 低 | 考虑使用更高级的状态管理 | + +--- + +## 文件统计 + +### 代码统计 +- **总 TS/TSX 文件数**:48 个 +- **总代码行数**:约 7000+ 行 +- **核心基础设施**:~1000 行 + - api.ts: 587 行 + - Layout.tsx: 298 行 + - DynamicMenu.tsx: 228 行 + +### 功能模块大小 +| 功能 | 文件数 | 行数 | +|------|--------|------| +| 用户等级配置 | 3 | 399 | +| 系统配置 | 3 | 401 | +| 认证管理 | 1+ | 500+ | +| API 客户端 | 1 | 587 | +| 布局和菜单 | 2 | 526 | + +--- + +## 最近的开发活动 + +### Git 提交历史 +``` +6cfd343 Merge branch 'feature/token-price' # 合并 token 价格功能 +09d50ea 创建用户时输入密码 +c4cefd1 创建goalfymax用户 +021bbc4 推送消息增加标题 +516fd5d token计费配置 +``` + +### 当前改动 +``` +Modified (在 Git 中): +- src/App.css +- src/App.tsx +- src/components/AuthGuard.tsx +- src/components/DynamicMenu.tsx +- src/components/Layout.tsx +- src/pages/GoalfyMaxUsers.tsx +- src/routes/DynamicRoutes.tsx +- src/services/api.ts + +Added (未追踪): +- src/components/DynamicMenu.css +- src/pages/SystemConfigs.tsx +- src/pages/UserLevelConfigs.tsx +- src/services/systemConfigApi.ts +- src/services/userLevelConfigApi.ts +- src/types/systemConfig.ts +- src/types/userLevelConfig.ts +``` + +--- + +## 核心文件快速查阅 + +### 必看文件(了解项目架构) +1. **src/App.tsx** - 应用入口和路由定义 +2. **src/services/api.ts** - API 客户端核心 +3. **src/components/Layout.tsx** - 主布局结构 +4. **src/components/DynamicMenu.tsx** - 权限感知菜单 + +### 参考模板(创建新功能) +1. **src/types/userLevelConfig.ts** - 类型定义模板 +2. **src/services/userLevelConfigApi.ts** - API 服务模板 +3. **src/pages/UserLevelConfigs.tsx** - 页面组件模板 + +### 权限和认证 +1. **src/hooks/useAuth.ts** - 认证 Hook +2. **src/hooks/usePagePermissions.ts** - 页面权限 Hook +3. **src/atoms/auth.ts** - 认证状态原子 + +--- + +## 技术栈详解 + +### 前端框架 +- **React 18.3.1** - UI 框架 +- **TypeScript 5.9.3** - 类型系统 +- **Vite 5.4.20** - 构建工具 + +### UI 和交互 +- **Ant Design 5.27.4** - UI 组件库 +- **React Router 7.9.4** - 路由管理 +- **Axios 1.12.2** - HTTP 客户端 + +### 状态管理和其他 +- **Jotai 2.15.0** - 原子状态管理 +- **Dayjs 1.11.18** - 日期处理 + +### 开发工具 +- **Vite 插件** (@vitejs/plugin-react) +- **ESLint** - 代码检查 +- **TypeScript ESLint** - 类型检查 + +--- + +## 学习路径建议 + +### 初级(快速上手) +1. 阅读 QUICK_REFERENCE.md +2. 理解 App.tsx 的路由结构 +3. 浏览 UserLevelConfigs.tsx 了解页面模式 + +### 中级(理解架构) +1. 阅读 PROJECT_ARCHITECTURE.md +2. 深入研究 ApiClient 的 token 刷新机制 +3. 分析 DynamicMenu 的权限过滤逻辑 + +### 高级(扩展能力) +1. 阅读 API_PATTERNS.md 详细模式 +2. 研究如何提取通用 CRUD Hook +3. 优化性能(拆分大文件、缓存策略等) + +--- + +## 立即可做的改进 + +### 短期(1-2 周) +1. **统一 API 响应处理** - 让所有 API 服务使用一致的响应提取方式 +2. **提取通用 CRUD Hook** - 减少页面重复代码 +3. **完善错误处理** - 添加更详细的错误信息和日志 + +### 中期(1-2 月) +1. **拆分大文件** - UserLevelConfigs 和 SystemConfigs 拆分为子组件 +2. **添加表单验证** - 增强表单验证和用户反馈 +3. **性能优化** - 添加列表虚拟化、缓存等 + +### 长期(3-6 月) +1. **重构为 Hooks** - 将更多逻辑移到自定义 Hooks +2. **添加单元测试** - 提高代码覆盖率 +3. **国际化** - 支持多语言 + +--- + +## 资源导航 + +### 项目内文档 +- **PROJECT_ARCHITECTURE.md** - 详细的项目结构和设计说明 +- **API_PATTERNS.md** - API 调用模式和实现指南 +- **QUICK_REFERENCE.md** - 快速参考和代码片段 + +### 外部资源 +- [Ant Design 官方文档](https://ant.design/) +- [React Router 官方文档](https://reactrouter.com/) +- [Jotai 官方文档](https://jotai.org/) +- [TypeScript 官方文档](https://www.typescriptlang.org/) +- [Axios 官方文档](https://axios-http.com/) + +--- + +## 常见问题解答 + +### Q: 如何添加新的管理功能? +A: 参考 UserLevelConfigs 或 SystemConfigs 的实现,按照以下步骤: +1. 创建类型定义 (src/types/) +2. 创建 API 服务 (src/services/) +3. 创建页面组件 (src/pages/) +4. 注册路由和菜单项 + +### Q: 如何处理权限检查? +A: 使用 `usePagePermissions()` Hook 获取用户权限,DynamicMenu 会自动过滤菜单项。 + +### Q: Token 过期了怎么办? +A: ApiClient 会自动处理 401 错误,尝试刷新 token,如果失败会触发重新登录。 + +### Q: 页面加载很慢怎么办? +A: 检查是否有大量 API 请求,考虑使用缓存、分页或虚拟化。 + +--- + +## 总结 + +这个项目展示了一个**成熟的 React 管理后台的标准实践**: + +✅ **架构清晰** - 分层明确,易于理解和维护 +✅ **类型安全** - 完整的 TypeScript 支持 +✅ **权限管理** - 多层次的权限检查 +✅ **可扩展** - 统一的 CRUD 模式,易于添加功能 +✅ **自动化** - Token 刷新、权限检查等自动化处理 + +适合作为企业级管理后台的参考实现和模板。 + +--- + +**文档生成时间**:2025-10-28 +**项目状态**:活跃开发中 +**最后更新**:正在添加系统配置管理功能 + diff --git a/EXPLORATION_SUMMARY.txt b/EXPLORATION_SUMMARY.txt new file mode 100644 index 0000000..f511315 --- /dev/null +++ b/EXPLORATION_SUMMARY.txt @@ -0,0 +1,113 @@ +======================================== +前端项目探索完成总结 +======================================== + +项目路径: /Users/youziba/goalfyagent/goalfymax-admin-web + +技术栈: +- React 18.3.1 +- TypeScript +- Vite (构建工具) +- React Router 7.9.4 (路由) +- Ant Design 5.27.4 (UI组件库) +- Jotai 2.15.0 (状态管理) +- Axios 1.12.2 (HTTP客户端) + +======================================== +关键发现 +======================================== + +1. 项目框架: React + TypeScript + Vite + - 标准的 React 函数组件范式 + - 完整的 TypeScript 类型支持 + - 使用 Jotai 管理全局认证状态 + +2. 路由设置: + - 文件: src/App.tsx + - 使用 React Router v7 的 Routes/Route + - 支持嵌套路由 + - 所有路由在一个文件中集中管理 + +3. 菜单配置: + - 文件: src/components/Layout.tsx + - 静态硬编码方式 + - 支持主菜单和子菜单 + - 菜单项通过权限进行条件渲染 + +4. API 调用: + - 文件: src/services/api.ts + - 使用 ApiClient 类包装 Axios + - 自动处理 Token 认证和刷新 (401 处理) + - 支持请求/响应拦截 + - 各功能模块创建专门的 API 服务文件 + +5. 表单和表格: + - 使用 Ant Design Form 进行表单管理 + - 使用 Ant Design Table 展示列表数据 + - Modal 弹窗用于创建和编辑 + - 完整的分页支持 + +6. 权限管理: + - Hook: usePagePermissions + - 从认证状态中获取用户权限页面列表 + - 菜单和路由通过权限检查进行显示 + +7. 参考实现: + - UserLevelConfigs.tsx (用户等级配置) - 283 行 + - 包含: 列表、创建、编辑、删除、状态切换 + +======================================== +已生成的文档 +======================================== + +1. FRONTEND_GUIDE.md (完整指南, 1527 行) + - 详细的项目结构介绍 + - 每个功能的完整代码示例 + - 权限管理详解 + - 创建新页面的 9 步完整流程 + + 位置: /Users/youziba/goalfyagent/goalfymax-admin-web/FRONTEND_GUIDE.md + +2. QUICK_START.md (快速开始指南) + - 简化的 5 步创建新页面流程 + - 核心文件速查表 + - 常用开发模式代码示例 + - 快速参考信息 + + 位置: /Users/youziba/goalfyagent/goalfymax-admin-web/QUICK_START.md + +======================================== +创建新的配置管理页面需要 5 个步骤 +======================================== + +1. src/types/xxx.ts - TypeScript 接口定义 +2. src/services/xxxApi.ts - API 服务函数 +3. src/pages/Xxx.tsx - React 页面组件 +4. src/App.tsx - 添加路由 +5. src/components/Layout.tsx - 添加菜单项 + +具体代码示例请查看 QUICK_START.md + +======================================== +核心文件位置速查 +======================================== + +路由配置: src/App.tsx +菜单配置: src/components/Layout.tsx +API 客户端: src/services/api.ts +权限检查: src/hooks/usePagePermissions.ts +认证管理: src/hooks/useAuth.ts +全局状态: src/atoms/auth.ts +页面列表: src/pages/ +类型定义: src/types/ + +======================================== +可以立即开始的工作 +======================================== + +1. 根据 QUICK_START.md 创建通用配置管理页面 +2. 参考用户等级配置页面的实现方式 +3. 遵循现有的代码风格和架构模式 +4. 充分利用 Ant Design 组件库 + +======================================== diff --git a/FRONTEND_GUIDE.md b/FRONTEND_GUIDE.md new file mode 100644 index 0000000..463f6f1 --- /dev/null +++ b/FRONTEND_GUIDE.md @@ -0,0 +1,1527 @@ +# GoalfyMax Admin Web 前端项目探索指南 + +## 项目概览 + +**项目路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web` + +**技术栈**: +- **框架**: React 18.3.1 +- **路由**: React Router DOM 7.9.4 +- **UI库**: Ant Design 5.27.4 +- **状态管理**: Jotai 2.15.0 +- **构建工具**: Vite 5.4.20 +- **HTTP客户端**: Axios 1.12.2 +- **语言**: TypeScript + +**项目结构**: +``` +src/ + ├── pages/ # 页面组件 + ├── components/ # 通用组件 + ├── services/ # API 服务层 + ├── types/ # TypeScript 类型定义 + ├── hooks/ # 自定义 hooks + ├── atoms/ # Jotai 状态管理 + ├── routes/ # 路由配置 + ├── assets/ # 静态资源 + ├── utils/ # 工具函数 + ├── App.tsx # 主应用入口 + └── main.tsx # 应用启动文件 +``` + +--- + +## 1. 前端项目框架 + +### 框架:React 18.3.1 + TypeScript + +**主入口文件**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/main.tsx` + +```typescript +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { Provider } from 'jotai' +import { BrowserRouter } from 'react-router-dom' +import './App.css' +import 'antd/dist/reset.css' +import App from './App.tsx' +import AuthGuard from './components/AuthGuard' + +const root = createRoot(document.getElementById('root')!) +root.render( + + + + + + + + + +) +``` + +**关键特点**: +- 使用 Jotai 进行全局状态管理 +- React Router 管理路由 +- AuthGuard 包裹整个应用,确保认证 +- Ant Design 提供 UI 组件 + +--- + +## 2. 现有配置管理页面实现 + +### 用户等级配置页面 (参考实现) + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/pages/UserLevelConfigs.tsx` + +**文件大小**: 283 行 + +**页面功能**: +- 列表展示(分页) +- 创建新配置 +- 编辑配置 +- 删除配置 +- 切换状态(启用/禁用) + +**关键代码片段**: + +```typescript +import React, { useEffect, useState } from 'react'; +import { + Table, + Button, + Modal, + Form, + Input, + InputNumber, + Tag, + Space, + Popconfirm, + message, + Row, + Col, + Card, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + CheckCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; + +export default function UserLevelConfigs() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + + // 获取列表 + const fetchList = async () => { + setLoading(true); + try { + const res = await getUserLevelConfigList({ page, size }); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } catch (e) { + message.error('获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + // 打开编辑弹窗 + const openEdit = (config: UserLevelConfig) => { + setEditing(config); + form.setFieldsValue({ + level_name: config.level_name, + project_limit: config.project_limit, + description: config.description, + sort_order: config.sort_order, + }); + setEditOpen(true); + }; + + // 提交编辑 + const submitEdit = async () => { + try { + const values = await form.validateFields(); + if (!editing) return; + await updateUserLevelConfig(editing.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); + } catch (error) { + message.error('更新失败'); + } + }; + + // 创建配置 + const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createUserLevelConfig(values); + message.success('创建成功'); + setCreateOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败'); + } + }; + + // 删除配置 + const handleDelete = async (config: UserLevelConfig) => { + try { + await deleteUserLevelConfig(config.id); + message.success('删除成功'); + fetchList(); + } catch (error) { + message.error('删除失败'); + } + }; + + // 切换状态 + const handleToggleStatus = async (config: UserLevelConfig) => { + try { + const newStatus = config.status === 1 ? 0 : 1; + await updateUserLevelConfigStatus(config.id, { status: newStatus }); + message.success(newStatus === 1 ? '已启用' : '已禁用'); + fetchList(); + } catch (error) { + message.error('状态更新失败'); + } + }; + + // 定义表格列 + const columns = [ + { + title: '等级名称', + dataIndex: 'level_name', + key: 'level_name', + }, + { + title: '等级代码', + dataIndex: 'level_code', + key: 'level_code', + }, + { + title: '项目数限制', + dataIndex: 'project_limit', + key: 'project_limit', + render: (value: number) => (value === 0 ? '不限' : value), + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + }, + { + title: '排序', + dataIndex: 'sort_order', + key: 'sort_order', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (value: number) => + value === 1 ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: any, config: UserLevelConfig) => ( + + + + handleDelete(config)}> + + + + ), + }, + ]; + + return ( +
+ +
+ +
+ +
+
{ + setPage(p); + setSize(s); + }, + }} + /> + + + + + {/* 编辑弹窗 */} + setEditOpen(false)} + > +
+ + + + + + + + + + + + + +
+ + {/* 创建弹窗 */} + setCreateOpen(false)} + > +
+ + + + + + + + + + + + + + + + +
+ + ); +} +``` + +--- + +## 3. 路由配置 + +### 路由配置方式 + +**主路由文件**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/App.tsx` + +```typescript +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import Overview from './pages/Overview'; +import Operations from './pages/Operations'; +import Monitoring from './pages/Monitoring'; +import Finance from './pages/Finance'; +import TokenHistoryPage from './pages/TokenHistory'; +import TokenAnalytics from './pages/TokenAnalytics'; +import SystemHealth from './pages/SystemHealth'; +import QuotaRulesPage from './pages/QuotaRules'; +import UserProjectQuotaWrapper from './pages/UserProjectQuota'; +import UserManagement from './pages/UserManagement'; +import RoleManagement from './pages/RoleManagement'; +import GoalfyMaxUsers from './pages/GoalfyMaxUsers'; +import UserFeedback from './pages/UserFeedback'; +import MessagePush from './pages/MessagePush'; +import VendorModelPricing from './pages/VendorModelPricing'; + +function App() { + return ( + + + {/* 默认重定向到仪表盘 */} + } /> + + {/* 仪表盘 */} + } /> + + {/* 总览页面 */} + } /> + + {/* 运营页面 - 嵌套路由 */} + } /> + } /> + } /> + } /> + + {/* 监控页面 - 嵌套路由 */} + } /> + } /> + } /> + } /> + + {/* 财务页面 */} + } /> + + {/* 系统管理页面 - 嵌套路由 */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* 404页面 */} + } /> + + + ); +} + +export default App; +``` + +**新页面的添加步骤**: +1. 在 `src/pages/` 目录下创建新页面组件 +2. 在 `App.tsx` 中导入页面组件 +3. 在 `Routes` 中添加 `` 配置 +4. 如果需要在菜单中显示,更新 `Layout.tsx` 中的导航配置 + +--- + +## 4. 菜单配置 + +### 菜单配置方式 + +**主菜单文件**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx` + +菜单配置使用静态定义的方式,在 Layout 组件中硬编码: + +```typescript +// Layout.tsx 中的导航菜单代码(简化版) + +{/* 总览 */} +{accessiblePages.includes('/overview') && ( + { + e.preventDefault(); + navigate('/overview'); + }} + > + 总览 Overview + +)} + +{/* 运营 */} +{accessiblePages.includes('/operations') && ( + { + e.preventDefault(); + navigate('/operations'); + }} + > + 运营 Operations + +)} + +{/* 系统管理 */} +{accessiblePages.includes('/system') && ( + { + e.preventDefault(); + navigate('/system'); + }} + > + 系统管理 Admin + +)} +``` + +### 子菜单配置(以系统管理为例) + +```typescript +// Layout.tsx 中的系统管理子菜单代码 + +{activeTab === 'admin' && ( + +)} +``` + +**菜单的权限控制**: + +菜单项通过 `usePagePermissions` hook 获取用户权限,使用 `accessiblePages.includes()` 来判断是否显示: + +```typescript +import { usePagePermissions } from '../hooks/usePagePermissions'; + +const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { getAccessiblePages } = usePagePermissions(); + const accessiblePages = getAccessiblePages(); + + // 根据权限显示菜单项 + if (!accessiblePages.includes('/overview')) { + // 不显示总览菜单 + } +} +``` + +--- + +## 5. API 调用的封装方式 + +### API 客户端配置 + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/services/api.ts` + +**关键特点**: +- 使用 Axios 作为 HTTP 客户端 +- 自动处理 Token 认证和刷新 +- 支持请求/响应拦截 +- 处理 401 错误自动刷新 Token + +```typescript +import axios from 'axios'; + +class ApiClient { + private instance: any; + private isRefreshing = false; + private failedQueue: Array<{ + resolve: (value: any) => void; + reject: (reason: any) => void; + }> = []; + + constructor(baseURL: string = '/api') { + this.instance = axios.create({ + baseURL, + timeout: 60000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + this.setupInterceptors(); + } + + private setupInterceptors() { + // 请求拦截器 - 添加 Authorization header + this.instance.interceptors.request.use( + config => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + error => { + return Promise.reject(error); + } + ); + + // 响应拦截器 - 处理 401 错误和 Token 刷新 + this.instance.interceptors.response.use( + (response: any) => { + return response; + }, + error => { + if (error.response?.status === 401) { + const originalRequest = error.config; + + if (originalRequest._retry) { + // 重试失败,触发重新登录 + const event = new CustomEvent('auth:relogin'); + window.dispatchEvent(event); + return Promise.reject(error); + } + + if (this.isRefreshing) { + // 如果正在刷新token,将请求加入队列 + return new Promise((resolve, reject) => { + this.failedQueue.push({ resolve, reject }); + }) + .then(token => { + originalRequest.headers.Authorization = token; + return this.instance(originalRequest); + }) + .catch(err => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + this.isRefreshing = true; + + return this.handleTokenRefresh() + .then(refreshResult => { + const { access_token, refresh_token, expires_in } = refreshResult; + this.processQueue(null, `Bearer ${access_token}`); + originalRequest.headers.Authorization = `Bearer ${access_token}`; + + // 触发 token 更新事件 + const event = new CustomEvent('auth:tokenRefreshed', { + detail: { + access_token, + refresh_token, + expires_in, + }, + }); + window.dispatchEvent(event); + + return this.instance(originalRequest); + }) + .catch(refreshError => { + this.processQueue(refreshError, null); + + // 刷新失败,重新登录 + const event = new CustomEvent('auth:relogin'); + window.dispatchEvent(event); + + return Promise.reject(refreshError); + }) + .finally(() => { + this.isRefreshing = false; + }); + } + + return Promise.reject(error); + } + ); + } + + // GET 请求 + async get(url: string, config?: any): Promise { + const response = await this.instance.get(url, config); + return response.data; + } + + // POST 请求 + async post( + url: string, + data?: any, + config?: any + ): Promise { + const response = await this.instance.post(url, data, config); + return response.data; + } + + // PUT 请求 + async put( + url: string, + data?: any, + config?: any + ): Promise { + const response = await this.instance.put(url, data, config); + return response.data; + } + + // DELETE 请求 + async delete( + url: string, + data?: any, + config?: any + ): Promise { + const response = await this.instance.delete(url, { + ...config, + data, + }); + return response.data; + } +} + +export const apiClient = new ApiClient(); +``` + +### API 服务层示例 + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/services/userLevelConfigApi.ts` + +```typescript +import { apiClient } from './api'; +import type { + UserLevelConfig, + UserLevelConfigListRequest, + UserLevelConfigListResponse, + UserLevelConfigCreateRequest, + UserLevelConfigUpdateRequest, + UserLevelConfigStatusRequest, +} from '../types/userLevelConfig'; + +// 获取用户等级配置列表 +export const getUserLevelConfigList = async ( + params: UserLevelConfigListRequest +): Promise => { + const response = await apiClient.get('/admin/user-level-configs', { params }); + return response.data; +}; + +// 获取用户等级配置详情 +export const getUserLevelConfigById = async (id: number): Promise => { + const response = await apiClient.get(`/admin/user-level-configs/${id}`); + return response.data; +}; + +// 创建用户等级配置 +export const createUserLevelConfig = async ( + data: UserLevelConfigCreateRequest +): Promise => { + const response = await apiClient.post('/admin/user-level-configs', data); + return response.data; +}; + +// 更新用户等级配置 +export const updateUserLevelConfig = async ( + id: number, + data: UserLevelConfigUpdateRequest +): Promise => { + const response = await apiClient.put(`/admin/user-level-configs/${id}`, data); + return response.data; +}; + +// 删除用户等级配置 +export const deleteUserLevelConfig = async (id: number): Promise => { + await apiClient.delete(`/admin/user-level-configs/${id}`); +}; + +// 更新用户等级配置状态 +export const updateUserLevelConfigStatus = async ( + id: number, + data: UserLevelConfigStatusRequest +): Promise => { + await apiClient.put(`/admin/user-level-configs/${id}/status`, data); +}; +``` + +**API 命名规范**: +- 列表接口: `/admin/xxx` +- 详情接口: `/admin/xxx/:id` +- 创建接口: `POST /admin/xxx` +- 更新接口: `PUT /admin/xxx/:id` +- 删除接口: `DELETE /admin/xxx/:id` +- 状态变更: `PUT /admin/xxx/:id/status` + +--- + +## 6. 表单组件和表格组件的使用方式 + +### Ant Design Form 组件使用 + +```typescript +import { Form, Input, InputNumber, Button, Modal } from 'antd'; + +// 基本表单使用 +const [form] = Form.useForm(); + +const submitForm = async () => { + try { + const values = await form.validateFields(); + // 处理表单数据 + console.log(values); + } catch (error) { + console.error('表单验证失败:', error); + } +}; + +// 在 JSX 中使用 +
+ + + + + + + + + + + + +``` + +### Ant Design Table 组件使用 + +```typescript +import { Table, Button, Space, Tag } from 'antd'; +import { EditOutlined, DeleteOutlined } from '@ant-design/icons'; + +// 定义表格列 +const columns = [ + { + title: '等级名称', + dataIndex: 'level_name', + key: 'level_name', + }, + { + title: '项目数限制', + dataIndex: 'project_limit', + key: 'project_limit', + render: (value: number) => (value === 0 ? '不限' : value), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (value: number) => + value === 1 ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: any, record: UserLevelConfig) => ( + + + + + ), + }, +]; + +// 在 JSX 中使用 +
{ + setPage(p); + setSize(s); + }, + }} +/> +``` + +### Modal 弹窗组件使用 + +```typescript +import { Modal, Button, Form } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; + +const [createOpen, setCreateOpen] = useState(false); +const [createForm] = Form.useForm(); + +const openCreate = () => { + createForm.resetFields(); + setCreateOpen(true); +}; + +const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + // 提交创建请求 + setCreateOpen(false); + } catch (error) { + message.error('创建失败'); + } +}; + +// 在 JSX 中使用 + + + setCreateOpen(false)} +> +
+ {/* 表单项 */} + +
+``` + +--- + +## 7. 类型定义 + +### TypeScript 类型定义位置 + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/types/userLevelConfig.ts` + +```typescript +export interface UserLevelConfig { + id: number; + level_name: string; + level_code: string; + project_limit: number; + description: string; + sort_order: number; + status: number; // 1-启用 0-禁用 + created_at: string; + updated_at: string; +} + +export interface UserLevelConfigListRequest { + level_name?: string; + status?: number; + page?: number; + size?: number; +} + +export interface UserLevelConfigListResponse { + data: UserLevelConfig[]; + total: number; + page: number; + size: number; +} + +export interface UserLevelConfigCreateRequest { + level_name: string; + level_code: string; + project_limit: number; + description?: string; + sort_order?: number; +} + +export interface UserLevelConfigUpdateRequest { + level_name: string; + project_limit: number; + description?: string; + sort_order?: number; +} + +export interface UserLevelConfigStatusRequest { + status: number; +} +``` + +--- + +## 8. 权限管理 + +### 权限检查 Hook + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/hooks/usePagePermissions.ts` + +```typescript +import { useAtom } from 'jotai'; +import { authStateAtom } from '../atoms/auth'; + +export const usePagePermissions = () => { + const [authState] = useAtom(authStateAtom); + + // 检查页面权限 + const hasPagePermission = (pagePath: string, action: string = 'read'): boolean => { + if (!authState.user?.pages) { + return false; + } + + const hasAccess = authState.user.pages.some(page => + page.path === pagePath && page.is_active !== false + ); + + return hasAccess; + }; + + // 获取页面操作权限 + const getPageActions = (pagePath: string): string[] => { + if (!authState.user?.pages) { + return []; + } + + const hasAccess = authState.user.pages.some(page => page.path === pagePath); + if (!hasAccess) { + return []; + } + + return ['read', 'create', 'update', 'delete']; + }; + + // 获取用户可访问的所有页面 + const getAccessiblePages = (): string[] => { + if (!authState.user?.pages) { + return []; + } + + const pages = authState.user.pages + .filter(page => page.is_active !== false) + .map(page => page.path); + + return pages; + }; + + return { + hasPagePermission, + getPageActions, + getAccessiblePages + }; +}; +``` + +**使用方式**: + +```typescript +import { usePagePermissions } from '../hooks/usePagePermissions'; + +function MyComponent() { + const { getAccessiblePages, hasPagePermission } = usePagePermissions(); + const accessiblePages = getAccessiblePages(); + + // 检查权限 + if (!hasPagePermission('/system')) { + return
无权访问
; + } + + return
有权访问
; +} +``` + +--- + +## 9. 其他关键概念 + +### 状态管理 (Jotai) + +**文件路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/atoms/auth.ts` + +Jotai 用于管理全局认证状态和用户权限信息。 + +### 国际化和本地化 + +整个项目使用中文进行开发,暂无国际化配置。 + +### 样式处理 + +- 使用 Ant Design 组件样式 +- 主样式文件: `src/App.css` +- 各组件可使用 Ant Design 的 `style` prop 进行内联样式 + +--- + +## 创建新的配置管理页面的完整步骤 + +### 第1步:创建 TypeScript 类型定义 + +创建文件 `/Users/youziba/goalfyagent/goalfymax-admin-web/src/types/systemConfig.ts`: + +```typescript +export interface SystemConfig { + id: number; + config_name: string; + config_value: string; + description: string; + status: number; // 1-启用 0-禁用 + created_at: string; + updated_at: string; +} + +export interface SystemConfigListRequest { + config_name?: string; + status?: number; + page?: number; + size?: number; +} + +export interface SystemConfigListResponse { + data: SystemConfig[]; + total: number; + page: number; + size: number; +} + +export interface SystemConfigCreateRequest { + config_name: string; + config_value: string; + description?: string; +} + +export interface SystemConfigUpdateRequest { + config_name: string; + config_value: string; + description?: string; +} +``` + +### 第2步:创建 API 服务 + +创建文件 `/Users/youziba/goalfyagent/goalfymax-admin-web/src/services/systemConfigApi.ts`: + +```typescript +import { apiClient } from './api'; +import type { + SystemConfig, + SystemConfigListRequest, + SystemConfigListResponse, + SystemConfigCreateRequest, + SystemConfigUpdateRequest, +} from '../types/systemConfig'; + +// 获取系统配置列表 +export const getSystemConfigList = async ( + params: SystemConfigListRequest +): Promise => { + const response = await apiClient.get('/admin/system-configs', { params }); + return response.data; +}; + +// 获取系统配置详情 +export const getSystemConfigById = async (id: number): Promise => { + const response = await apiClient.get(`/admin/system-configs/${id}`); + return response.data; +}; + +// 创建系统配置 +export const createSystemConfig = async ( + data: SystemConfigCreateRequest +): Promise => { + const response = await apiClient.post('/admin/system-configs', data); + return response.data; +}; + +// 更新系统配置 +export const updateSystemConfig = async ( + id: number, + data: SystemConfigUpdateRequest +): Promise => { + const response = await apiClient.put(`/admin/system-configs/${id}`, data); + return response.data; +}; + +// 删除系统配置 +export const deleteSystemConfig = async (id: number): Promise => { + await apiClient.delete(`/admin/system-configs/${id}`); +}; + +// 更新系统配置状态 +export const updateSystemConfigStatus = async ( + id: number, + status: number +): Promise => { + await apiClient.put(`/admin/system-configs/${id}/status`, { status }); +}; +``` + +### 第3步:创建页面组件 + +创建文件 `/Users/youziba/goalfyagent/goalfymax-admin-web/src/pages/SystemConfigs.tsx`: + +```typescript +import React, { useEffect, useState } from 'react'; +import { + Table, + Button, + Modal, + Form, + Input, + Tag, + Space, + Popconfirm, + message, + Row, + Col, + Card, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + CheckCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; +import type { SystemConfig } from '../types/systemConfig'; +import { + getSystemConfigList, + createSystemConfig, + updateSystemConfig, + deleteSystemConfig, + updateSystemConfigStatus, +} from '../services/systemConfigApi'; + +export default function SystemConfigs() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + + // 获取列表 + const fetchList = async () => { + setLoading(true); + try { + const res = await getSystemConfigList({ page, size }); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } catch (e) { + message.error('获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + // 打开编辑弹窗 + const openEdit = (config: SystemConfig) => { + setEditing(config); + form.setFieldsValue({ + config_name: config.config_name, + config_value: config.config_value, + description: config.description, + }); + setEditOpen(true); + }; + + // 提交编辑 + const submitEdit = async () => { + try { + const values = await form.validateFields(); + if (!editing) return; + await updateSystemConfig(editing.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); + } catch (error) { + message.error('更新失败'); + } + }; + + // 打开创建弹窗 + const openCreate = () => { + createForm.resetFields(); + setCreateOpen(true); + }; + + // 提交创建 + const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createSystemConfig(values); + message.success('创建成功'); + setCreateOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败'); + } + }; + + // 删除配置 + const handleDelete = async (config: SystemConfig) => { + try { + await deleteSystemConfig(config.id); + message.success('删除成功'); + fetchList(); + } catch (error) { + message.error('删除失败'); + } + }; + + // 切换状态 + const handleToggleStatus = async (config: SystemConfig) => { + try { + const newStatus = config.status === 1 ? 0 : 1; + await updateSystemConfigStatus(config.id, newStatus); + message.success(newStatus === 1 ? '已启用' : '已禁用'); + fetchList(); + } catch (error) { + message.error('状态更新失败'); + } + }; + + // 定义表格列 + const columns = [ + { + title: '配置名称', + dataIndex: 'config_name', + key: 'config_name', + }, + { + title: '配置值', + dataIndex: 'config_value', + key: 'config_value', + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (value: number) => + value === 1 ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: any, config: SystemConfig) => ( + + + + handleDelete(config)}> + + + + ), + }, + ]; + + return ( +
+ +
+ +
+ +
+
{ + setPage(p); + setSize(s); + }, + }} + /> + + + + + {/* 编辑弹窗 */} + setEditOpen(false)} + > +
+ + + + + + + + + + +
+ + {/* 创建弹窗 */} + setCreateOpen(false)} + > +
+ + + + + + + + + + +
+ + ); +} +``` + +### 第4步:添加路由 + +在 `/Users/youziba/goalfyagent/goalfymax-admin-web/src/App.tsx` 中添加: + +```typescript +import SystemConfigs from './pages/SystemConfigs'; + +// 在 Routes 中添加新路由 +} /> +``` + +### 第5步:添加菜单项 + +在 `/Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx` 中的系统管理子菜单中添加: + +```typescript + +``` + +--- + +## 总结 + +**核心文件清单**: + +| 文件类型 | 路径 | 说明 | +|---------|------|------| +| 类型定义 | `/src/types/*.ts` | TypeScript 接口定义 | +| API 服务 | `/src/services/*Api.ts` | API 调用封装 | +| 页面组件 | `/src/pages/*.tsx` | 页面级别组件 | +| 通用组件 | `/src/components/*.tsx` | 可复用的组件 | +| Hooks | `/src/hooks/*.ts` | 自定义 hooks | +| 状态管理 | `/src/atoms/*.ts` | Jotai 原子状态 | +| 路由配置 | `/src/App.tsx` | 路由定义 | +| 菜单配置 | `/src/components/Layout.tsx` | 菜单和导航 | + +**最佳实践**: +1. 按照现有的目录结构和命名规范创建新文件 +2. 使用 TypeScript 定义强类型 +3. 在 API 层使用 `apiClient` 进行请求 +4. 在页面中使用 Ant Design 组件 +5. 通过 `usePagePermissions` hook 检查权限 +6. 使用 Jotai 的 `useAtom` 进行状态管理 + diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md new file mode 100644 index 0000000..8cd92f7 --- /dev/null +++ b/PROJECT_ARCHITECTURE.md @@ -0,0 +1,480 @@ +# Goalfymax 管理后台项目架构概览 + +## 项目概述 + +这是一个基于 **React 18** + **Vite** + **Ant Design 5** + **React Router 7** 的现代管理后台应用。 + +- **技术栈**:React 18.3.1 + TypeScript + Vite 5.4.20 + Ant Design 5.27.4 +- **状态管理**:Jotai(原子状态管理) +- **HTTP 客户端**:Axios 1.12.2 +- **日期处理**:Dayjs 1.11.18 +- **总文件数**:48 个 TypeScript/TSX 文件 +- **代码行数**:~7000+ 行代码 + +--- + +## 目录结构 + +``` +src/ +├── App.tsx # 主应用入口,定义静态路由 +├── main.tsx # Vite 应用入口点 +├── index.css # 全局样式 +├── App.css # 应用样式 +│ +├── atoms/ # 状态管理(Jotai 原子状态) +│ └── auth.ts # 认证状态原子 +│ +├── components/ # 可复用 UI 组件 +│ ├── Layout.tsx # 主布局组件(侧边栏+主区域) +│ ├── DynamicMenu.tsx # 动态菜单组件(权限感知) +│ ├── DynamicMenu.css # 菜单样式 +│ ├── AuthGuard.tsx # 认证守卫组件 +│ ├── PermissionGuard.tsx # 权限守卫组件 +│ ├── PagePermissionGuard.tsx +│ ├── QuotaCharts.tsx # 配额图表组件 +│ ├── QuotaRulesForm.tsx # 配额规则表单 +│ ├── QuotaRulesTable.tsx # 配额规则表格 +│ ├── QuotaHistoryTable.tsx # 配额历史表格 +│ ├── QuotaFilters.tsx # 配额过滤器 +│ ├── QuotaStats.tsx # 配额统计 +│ └── UserProjectQuotaPage.tsx +│ +├── hooks/ # 自定义 React Hooks +│ ├── useAuth.ts # 认证 Hook(登录、登出、Token 管理) +│ ├── usePagePermissions.ts # 页面权限 Hook +│ └── usePermissions.ts # 操作权限 Hook +│ +├── pages/ # 页面组件 +│ ├── Dashboard.tsx # 仪表盘 +│ ├── Overview.tsx # 总览页 +│ ├── Operations.tsx # 运营页 +│ ├── Monitoring.tsx # 监控页 +│ ├── Finance.tsx # 财务页 +│ ├── SystemHealth.tsx # 系统健康 +│ ├── QuotaRules.tsx # 配额规则管理 +│ ├── UserProjectQuota.tsx # 用户项目配额管理 +│ ├── UserManagement.tsx # 系统用户管理 +│ ├── RoleManagement.tsx # 角色管理 +│ ├── GoalfyMaxUsers.tsx # GoalfyMax 用户管理 +│ ├── UserLevelConfigs.tsx # 用户等级配置管理 +│ ├── SystemConfigs.tsx # 系统配置管理 +│ ├── UserFeedback.tsx # 用户反馈管理 +│ ├── MessagePush.tsx # 消息推送管理 +│ ├── TokenHistory.tsx # Token 历史 +│ ├── TokenAnalytics.tsx # Token 分析 +│ ├── VendorModelPricing.tsx# 供应商模型价格管理 +│ └── ... +│ +├── routes/ # 路由配置 +│ └── DynamicRoutes.tsx # 动态路由配置(基于权限的动态路由) +│ +├── services/ # API 服务层 +│ ├── api.ts # 核心 API 客户端(ApiClient)+ API 服务导出 +│ ├── userLevelConfigApi.ts # 用户等级配置 API +│ ├── systemConfigApi.ts # 系统配置 API +│ ├── userApi.ts # 用户 API +│ ├── roleApi.ts # 角色 API +│ └── ... +│ +├── types/ # TypeScript 类型定义 +│ ├── userLevelConfig.ts # 用户等级配置类型 +│ ├── systemConfig.ts # 系统配置类型 +│ ├── quota.ts # 配额类型 +│ ├── userProjectQuota.ts # 用户项目配额类型 +│ └── ... +│ +├── utils/ # 工具函数 +│ └── storageMigration.ts # 存储迁移工具 +│ +└── assets/ # 静态资源 + └── react.svg +``` + +--- + +## 核心架构设计 + +### 1. 路由设计 + +**双层路由系统**: + +#### App.tsx 中的静态路由 +- 定义所有可用的路由路径 +- 组织模块化的路由结构(dashboard、operations、monitoring、system 等) + +```typescript +// 示例:系统管理路由 +} /> +} /> +} /> +} /> +} /> +``` + +#### DynamicRoutes.tsx 中的权限感知路由 +- 根据用户权限动态显示/隐藏路由 +- 使用 `usePagePermissions()` hook 获取用户权限 +- 只挂载有权限访问的路由 + +### 2. 菜单系统 + +**DynamicMenu 组件**特点: +- 权限感知的菜单项过滤 +- 支持一级和二级菜单 +- 自动同步当前路由位置 +- 详细的权限检查日志 + +核心逻辑: +```typescript +const filterAccessibleMenus = (items: MenuItem[]): MenuItem[] => { + // 1. 仪表盘始终显示 + // 2. 父级菜单权限检查(如 /system、/operations) + // 3. 单个菜单项权限检查 + // 4. 返回过滤后的菜单项 +} +``` + +### 3. API 调用模式 + +**分层结构**: + +``` +UI 组件 + ↓ +Custom Hook (useXxx) + ↓ +API Service (xxxApi.ts) + ↓ +ApiClient (api.ts) + ↓ +Axios 实例 + 拦截器 + ↓ +后端 API +``` + +#### ApiClient 类特性 +- **自动 Token 管理**:请求自动附加 Authorization 头 +- **Token 刷新机制**:401 错误时自动刷新 token +- **请求队列**:刷新 token 期间的请求入队等待 +- **错误处理**:统一处理认证失败和其他错误 + +#### API 服务示例 + +**系统配置 API**(systemConfigApi.ts): +```typescript +export const getSystemConfigList = async (params) => { + const response = await apiClient.get('/admin/system-configs', { params }); + return response; // 返回完整响应对象 +} + +export const createSystemConfig = async (data) => { + const response = await apiClient.post('/admin/system-configs', data); + return response.data; +} +``` + +**用户等级配置 API**(userLevelConfigApi.ts): +```typescript +export const getUserLevelConfigList = async (params) => { + const response = await apiClient.get('/admin/user-level-configs', { params }); + return response.data; +} +``` + +--- + +## 关键文件详解 + +### 1. 用户等级配置(UserLevelConfigs) + +**文件清单**: +- `/src/pages/UserLevelConfigs.tsx` - 页面组件(292 行) +- `/src/services/userLevelConfigApi.ts` - API 服务(60 行) +- `/src/types/userLevelConfig.ts` - 类型定义(47 行) + +**功能**: +- 列表查询(分页) +- 创建新等级配置 +- 编辑等级配置 +- 删除等级配置 +- 启用/禁用等级配置 + +**数据模型**: +```typescript +interface UserLevelConfig { + id: number; + level_name: string; // 等级名称 + level_code: string; // 等级代码 + project_limit: number; // 项目数限制 + description: string; // 描述 + sort_order: number; // 排序顺序 + status: number; // 1=启用, 0=禁用 + created_at: string; + updated_at: string; +} +``` + +### 2. 系统配置(SystemConfigs) + +**文件清单**: +- `/src/pages/SystemConfigs.tsx` - 页面组件(284 行) +- `/src/services/systemConfigApi.ts` - API 服务(66 行) +- `/src/types/systemConfig.ts` - 类型定义(51 行) + +**功能**: +- 列表查询(分页) +- 创建新系统配置 +- 编辑系统配置 +- 删除系统配置 +- 启用/禁用系统配置 + +**数据模型**: +```typescript +interface SystemConfig { + id: number; + key: string; // 唯一标识 + name: string; // 配置名称 + value: string; // 配置值 + type: string; // 配置类型(string, int, bool, json) + desc: string; // 配置描述 + status: number; // 1=启用, 0=禁用 + createdAt: string; + updatedAt: string; +} +``` + +### 3. Layout 组件(布局) + +**结构**: +``` +┌─────────────────────────────┐ +│ Header(顶部栏) │ +├─────────┬───────────────────┤ +│ │ │ +│ Sidebar │ Main Content │ +│(Menus) │ (页面内容) │ +│ │ │ +├─────────┼───────────────────┤ +│ Footer(页脚) │ +└─────────────────────────────┘ +``` + +**特性**: +- 侧边栏可折叠/展开(按钮) +- 动态菜单(DynamicMenu) +- 子导航标签(针对多级菜单页面) +- 用户认证状态显示 + 登出按钮 + +### 4. 认证流程(useAuth Hook) + +**功能**: +- SSO 单点登录 +- Token 管理(access_token + refresh_token) +- 自动 Token 刷新 +- Token 过期检测和重新登录 + +--- + +## API 端点规范 + +### 用户等级配置 +``` +GET /admin/user-level-configs # 列表(分页) +GET /admin/user-level-configs/all # 全部(不分页) +GET /admin/user-level-configs/:id # 详情 +POST /admin/user-level-configs # 创建 +PUT /admin/user-level-configs/:id # 更新 +PUT /admin/user-level-configs/:id/status # 更新状态 +DELETE /admin/user-level-configs/:id # 删除 +``` + +### 系统配置 +``` +GET /admin/system-configs # 列表(分页) +GET /admin/system-configs/all # 全部(不分页) +GET /admin/system-configs/:id # 详情 +GET /admin/system-configs/key/:key # 按 Key 查询 +POST /admin/system-configs # 创建 +PUT /admin/system-configs/:id # 更新 +PUT /admin/system-configs/:id/status # 更新状态 +DELETE /admin/system-configs/:id # 删除 +``` + +### 其他 API +``` +GET /admin/goalfymax-users # GoalfyMax 用户列表 +GET /admin/user-feedback # 用户反馈 +GET /admin/message-push/logs # 推送记录 +GET /admin/vendor-model-pricing # 供应商模型价格 +POST /admin/message-push/send # 发送消息 +``` + +--- + +## 权限系统 + +### 权限检查层级 + +1. **页面级权限**(usePagePermissions) + - 检查用户是否可以访问某个页面 + - 返回可访问的页面列表 + +2. **操作级权限**(usePermissions) + - 检查用户是否可以执行某个操作 + - 如:创建、编辑、删除、启用/禁用 + +3. **菜单级权限**(DynamicMenu) + - 根据页面权限动态显示/隐藏菜单项 + - 支持嵌套菜单的权限管理 + +### 权限存储 +- 通过 localStorage 缓存 +- 从后端 SSO API 获取用户权限信息 +- Jotai 原子状态管理 + +--- + +## 页面模块划分 + +### 系统管理(/system) +- 配额/套餐规则 +- 用户项目配额 +- 系统用户管理 +- 角色管理 +- GoalfyMax 用户管理 +- **用户等级配置** +- **系统配置** + +### 运营(/operations) +- 用户反馈管理 +- 消息推送管理 +- 供应商模型价格管理 + +### 监控(/monitoring) +- Token 历史 +- Token 分析 +- 系统健康 + +### 财务(/finance) +- 财务数据统计 + +--- + +## 通用 CRUD 页面模式 + +### UserLevelConfigs 和 SystemConfigs 采用的设计模式 + +**页面结构**: +1. 数据列表(分页表格) +2. 创建弹窗 +3. 编辑弹窗 +4. 操作列(编辑、启用/禁用、删除) + +**状态管理**: +```typescript +const [list, setList] = useState([]); +const [total, setTotal] = useState(0); +const [page, setPage] = useState(1); +const [size, setSize] = useState(10); +const [editing, setEditing] = useState(null); +const [editOpen, setEditOpen] = useState(false); +const [createOpen, setCreateOpen] = useState(false); +``` + +**操作流程**: +1. **查询列表**:`fetchList()` → API → 更新 state +2. **创建**:表单验证 → API → 刷新列表 +3. **编辑**:打开弹窗 → 表单填充 → 表单验证 → API → 刷新列表 +4. **删除**:确认弹窗 → API → 刷新列表 +5. **状态切换**:API → 刷新列表 + +--- + +## 代码架构建议 + +### 当前优点 +1. 清晰的分层结构(页面 → Hook → API → Client) +2. 类型安全(完整的 TypeScript 类型定义) +3. 权限管理系统 +4. 可复用的 CRUD 模式 +5. 自动 Token 刷新机制 + +### 需要关注的方面 +1. **页面文件大小**:UserLevelConfigs.tsx 和 SystemConfigs.tsx 都接近 300 行,可以考虑拆分 +2. **组件复用**:考虑提取通用的 CRUD 表格组件 +3. **错误处理**:某些 API 调用的错误处理需要完善 +4. **加载状态**:分页加载、动作加载等状态管理可以统一 + +--- + +## 开发指南 + +### 添加新的配置管理页面 + +**步骤**: + +1. **创建类型定义** (`src/types/xxxConfig.ts`) +```typescript +export interface XxxConfig { + id: number; + name: string; + value: string; + status: number; + created_at: string; + updated_at: string; +} + +export interface XxxConfigListRequest { + page?: number; + size?: number; +} + +export interface XxxConfigListResponse { + data: XxxConfig[]; + total: number; +} +``` + +2. **创建 API 服务** (`src/services/xxxConfigApi.ts`) +```typescript +export const getXxxConfigList = async (params) => + (await apiClient.get('/admin/xxx-configs', { params })).data; + +export const createXxxConfig = async (data) => + (await apiClient.post('/admin/xxx-configs', data)).data; +// ... 其他 CRUD 操作 +``` + +3. **创建页面组件** (`src/pages/XxxConfigs.tsx`) +- 参考 UserLevelConfigs.tsx 或 SystemConfigs.tsx 的结构 +- 实现列表、创建、编辑、删除、状态切换 + +4. **添加路由** (`src/App.tsx`) +```typescript +} /> +``` + +5. **添加菜单项** (`src/components/DynamicMenu.tsx`) +```typescript +{ + key: 'system-xxx-configs', + label: 'Xxx 配置', + icon: , + path: '/xxx-configs', +} +``` + +--- + +## 总结 + +这是一个**结构良好的 React 管理后台**,具有: +- 权限驱动的动态菜单和路由 +- 清晰的分层 API 架构 +- 完整的 TypeScript 类型支持 +- 通用的 CRUD 页面模式 +- 自动化的 Token 管理 + +最近的开发方向是**添加系统配置管理功能**(SystemConfigs 和 UserLevelConfigs),这些都是遵循统一的模式和最佳实践。 + diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..4e2dd2d --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,392 @@ +# Goalfymax 管理后台 - 快速参考指南 + +## 项目信息 + +| 项目 | 说明 | +|------|------| +| **名称** | goalfymax-admin-web | +| **框架** | React 18.3.1 + TypeScript + Vite | +| **UI 库** | Ant Design 5.27.4 | +| **路由** | React Router 7.9.4 | +| **HTTP** | Axios 1.12.2 | +| **状态管理** | Jotai 2.15.0 | +| **总文件数** | 48 个 TS/TSX 文件 | + +--- + +## 目录速查表 + +| 目录 | 用途 | 示例 | +|------|------|------| +| `src/pages/` | 页面组件 | UserLevelConfigs.tsx | +| `src/services/` | API 服务 | userLevelConfigApi.ts | +| `src/types/` | TypeScript 类型 | userLevelConfig.ts | +| `src/components/` | 可复用组件 | Layout.tsx, DynamicMenu.tsx | +| `src/hooks/` | 自定义 Hooks | useAuth.ts, usePagePermissions.ts | +| `src/atoms/` | Jotai 状态管理 | auth.ts | +| `src/routes/` | 路由配置 | DynamicRoutes.tsx | +| `src/utils/` | 工具函数 | storageMigration.ts | + +--- + +## 关键概念速览 + +### 1. 双层路由系统 + +``` +App.tsx (静态路由) + DynamicRoutes.tsx (权限感知路由) + ↓ + Layout 组件 (侧边栏 + 菜单) + ↓ + DynamicMenu (权限过滤菜单项) +``` + +### 2. API 调用流程 + +``` +页面组件 → API 服务 → ApiClient → Axios → 后端 API +``` + +### 3. 权限检查层级 + +``` +页面权限 (usePagePermissions) → 菜单权限 (DynamicMenu) → 操作权限 (usePermissions) +``` + +--- + +## 常见任务速查 + +### 添加新的配置管理页面 + +**所需文件数**:3-4 个 +**开发时间**:约 30-60 分钟 +**步骤**: + +```bash +# 1. 创建类型定义 +src/types/xxxConfig.ts + +# 2. 创建 API 服务 +src/services/xxxConfigApi.ts + +# 3. 创建页面组件 +src/pages/XxxConfigs.tsx + +# 4. 更新路由 (src/App.tsx) +} /> + +# 5. 更新菜单 (src/components/DynamicMenu.tsx) +# 6. 更新子导航 (src/components/Layout.tsx) +``` + +### 快速复制模板 + +**UserLevelConfigs 类似的 CRUD 页面** +- 类型定义:`src/types/userLevelConfig.ts` (47 行) +- API 服务:`src/services/userLevelConfigApi.ts` (60 行) +- 页面组件:`src/pages/UserLevelConfigs.tsx` (292 行) + +**SystemConfigs 类似的配置管理** +- 类型定义:`src/types/systemConfig.ts` (51 行) +- API 服务:`src/services/systemConfigApi.ts` (66 行) +- 页面组件:`src/pages/SystemConfigs.tsx` (284 行) + +--- + +## 重要文件速查 + +### 核心基础设施 + +| 文件 | 行数 | 作用 | +|------|------|------| +| `src/App.tsx` | 68 | 主应用入口,静态路由定义 | +| `src/services/api.ts` | 587 | 核心 API 客户端 + 所有 API 服务导出 | +| `src/components/Layout.tsx` | 298 | 主布局(侧边栏 + 主区域 + 子导航) | +| `src/components/DynamicMenu.tsx` | 228 | 权限感知菜单 | +| `src/hooks/useAuth.ts` | ? | 认证管理 Hook | +| `src/hooks/usePagePermissions.ts` | ? | 页面权限 Hook | + +### 配置管理示例 + +| 功能 | 页面 | 类型 | API 服务 | +|------|------|------|----------| +| 用户等级配置 | UserLevelConfigs.tsx | userLevelConfig.ts | userLevelConfigApi.ts | +| 系统配置 | SystemConfigs.tsx | systemConfig.ts | systemConfigApi.ts | +| GoalfyMax 用户 | GoalfyMaxUsers.tsx | - | api.ts (GoalfyMaxUsersApi) | + +--- + +## API 端点速查 + +### 用户等级配置 +``` +GET /admin/user-level-configs 列表(分页) +GET /admin/user-level-configs/all 全部(不分页) +GET /admin/user-level-configs/:id 详情 +POST /admin/user-level-configs 创建 +PUT /admin/user-level-configs/:id 更新 +PUT /admin/user-level-configs/:id/status 状态更新 +DELETE /admin/user-level-configs/:id 删除 +``` + +### 系统配置 +``` +GET /admin/system-configs 列表(分页) +GET /admin/system-configs/all 全部(不分页) +GET /admin/system-configs/:id 详情 +GET /admin/system-configs/key/:key 按 Key 查询 +POST /admin/system-configs 创建 +PUT /admin/system-configs/:id 更新 +PUT /admin/system-configs/:id/status 状态更新 +DELETE /admin/system-configs/:id 删除 +``` + +--- + +## 代码片段库 + +### 创建新的 API 服务函数 + +```typescript +// src/services/xxxConfigApi.ts +import { apiClient } from './api'; +import type { XxxConfig, XxxConfigListRequest } from '../types/xxxConfig'; + +export const getXxxConfigList = async (params: XxxConfigListRequest) => { + const response = await apiClient.get('/admin/xxx-configs', { params }); + return response.data; +}; + +export const createXxxConfig = async (data: any) => { + const response = await apiClient.post('/admin/xxx-configs', data); + return response.data; +}; + +export const updateXxxConfig = async (id: number, data: any) => { + const response = await apiClient.put(`/admin/xxx-configs/${id}`, data); + return response.data; +}; + +export const deleteXxxConfig = async (id: number) => { + await apiClient.delete(`/admin/xxx-configs/${id}`); +}; +``` + +### 创建新的类型定义 + +```typescript +// src/types/xxxConfig.ts +export interface XxxConfig { + id: number; + name: string; + value: string; + status: number; + created_at: string; + updated_at: string; +} + +export interface XxxConfigListRequest { + page?: number; + size?: number; +} + +export interface XxxConfigListResponse { + data: XxxConfig[]; + total: number; +} +``` + +### 创建 CRUD 页面骨架 + +```typescript +// src/pages/XxxConfigs.tsx +import React, { useEffect, useState } from 'react'; +import { Table, Button, Modal, Form, Input, message } from 'antd'; +import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import type { XxxConfig } from '../types/xxxConfig'; +import { getXxxConfigList, createXxxConfig, updateXxxConfig, deleteXxxConfig } from '../services/xxxConfigApi'; + +export default function XxxConfigs() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [editing, setEditing] = useState(null); + const [editOpen, setEditOpen] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + + const fetchList = async () => { + setLoading(true); + try { + const res = await getXxxConfigList({ page, size }); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } catch (e) { + message.error('获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + // ... 其他 handlers + + return ( +
+ +
{ setPage(p); setSize(s); } + }} /> + {/* 弹窗 */} + + ); +} +``` + +--- + +## 常见错误和解决方案 + +### 问题 1:页面显示后没有数据 +**原因**:API 端点不正确或权限不足 +**解决**: +1. 检查 API 端点 URL +2. 检查用户权限 +3. 查看浏览器控制台错误日志 + +### 问题 2:菜单项不显示 +**原因**:权限检查失败 +**解决**: +1. 检查 DynamicMenu.tsx 的权限逻辑 +2. 验证用户权限是否包含该页面路径 +3. 查看控制台的权限检查日志(带 🔍 标记) + +### 问题 3:创建/编辑后页面没有刷新 +**原因**:未调用 fetchList() +**解决**: +1. 确保在 submitCreate/submitEdit 中调用 fetchList() +2. 检查是否有错误导致代码未执行到刷新逻辑 + +### 问题 4:Token 过期导致 401 错误 +**原因**:Token 已过期,ApiClient 应该自动处理 +**解决**: +1. 检查 refreshToken 是否存在 +2. 查看浏览器控制台的 token 刷新日志 +3. 检查后端是否正确响应 token 刷新请求 + +--- + +## 性能优化建议 + +### 当前架构的瓶颈 + +1. **大型页面文件** + - UserLevelConfigs.tsx: 292 行 + - SystemConfigs.tsx: 284 行 + - 建议:拆分为表格、表单、操作等子组件 + +2. **重复的 CRUD 代码** + - 每个管理页面都有类似的列表、创建、编辑、删除逻辑 + - 建议:提取通用的 CRUD Hook 或高阶组件 + +3. **API 响应处理不一致** + - userLevelConfigApi 返回 response.data + - systemConfigApi 返回 response + - 建议:统一响应处理方式 + +--- + +## 开发工作流 + +### 开发新功能的标准步骤 + +``` +1. 分析需求,确定数据模型 + ↓ +2. 创建 TypeScript 类型定义 (src/types/) + ↓ +3. 创建 API 服务函数 (src/services/) + ↓ +4. 创建页面组件 (src/pages/) + ↓ +5. 注册路由 (src/App.tsx) + ↓ +6. 添加菜单项 (src/components/DynamicMenu.tsx) + ↓ +7. 更新子导航(如需要) (src/components/Layout.tsx) + ↓ +8. 本地测试 + ↓ +9. 提交代码 +``` + +--- + +## 有用的命令 + +```bash +# 启动开发服务器 +npm run dev + +# 构建生产版本 +npm run build + +# 代码检查 +npm run lint + +# 预览生产构建 +npm run preview +``` + +--- + +## 文档链接 + +- **项目架构概览**:PROJECT_ARCHITECTURE.md +- **API 模式详解**:API_PATTERNS.md +- **Ant Design 文档**:https://ant.design/ +- **React Router 文档**:https://reactrouter.com/ +- **Jotai 文档**:https://jotai.org/ + +--- + +## 常用类名和常量 + +### 用户等级配置 +- 页面:`/system/user-level-configs` +- 菜单 key:`system-user-level-configs` +- 类型文件:`userLevelConfig.ts` +- API 服务:`userLevelConfigApi.ts` + +### 系统配置 +- 页面:`/system/system-configs` +- 菜单 key:`system-system-configs` 或 `system-global-configs` +- 类型文件:`systemConfig.ts` +- API 服务:`systemConfigApi.ts` + +--- + +## 快速检查清单 + +创建新页面前,检查: +- [ ] 后端 API 已实现 +- [ ] 类型定义完整 +- [ ] API 服务函数齐全(CRUD 所有操作) +- [ ] 页面组件遵循 CRUD 模式 +- [ ] 路由已注册 +- [ ] 菜单项已添加 +- [ ] 权限检查已配置(如需要) +- [ ] 表单验证规则完整 +- [ ] 错误处理到位 +- [ ] 加载状态反馈清晰 + diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..841d3d5 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,566 @@ +# 前端开发快速开始指南 + +## 项目信息 + +- **技术栈**: React 18 + TypeScript + Vite + Ant Design 5 + React Router 7 +- **项目路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web` +- **状态管理**: Jotai +- **HTTP客户端**: Axios (自动处理 Token 认证和刷新) + +--- + +## 快速创建新的配置管理页面 + +以创建"通用配置"管理页面为例,需要 5 个步骤: + +### 步骤 1:创建类型定义文件 + +**文件**: `src/types/commonConfig.ts` + +```typescript +export interface CommonConfig { + id: number; + config_key: string; // 配置键 + config_value: string; // 配置值 + description: string; // 描述 + status: number; // 1=启用, 0=禁用 + created_at: string; + updated_at: string; +} + +export interface CommonConfigListRequest { + config_key?: string; + status?: number; + page?: number; + size?: number; +} + +export interface CommonConfigListResponse { + data: CommonConfig[]; + total: number; + page: number; + size: number; +} + +export interface CommonConfigCreateRequest { + config_key: string; + config_value: string; + description?: string; +} + +export interface CommonConfigUpdateRequest { + config_key: string; + config_value: string; + description?: string; +} +``` + +### 步骤 2:创建 API 服务文件 + +**文件**: `src/services/commonConfigApi.ts` + +```typescript +import { apiClient } from './api'; +import type { + CommonConfig, + CommonConfigListRequest, + CommonConfigListResponse, + CommonConfigCreateRequest, + CommonConfigUpdateRequest, +} from '../types/commonConfig'; + +export const getCommonConfigList = async ( + params: CommonConfigListRequest +): Promise => { + const response = await apiClient.get('/admin/common-configs', { params }); + return response.data; +}; + +export const getCommonConfigById = async (id: number): Promise => { + const response = await apiClient.get(`/admin/common-configs/${id}`); + return response.data; +}; + +export const createCommonConfig = async ( + data: CommonConfigCreateRequest +): Promise => { + const response = await apiClient.post('/admin/common-configs', data); + return response.data; +}; + +export const updateCommonConfig = async ( + id: number, + data: CommonConfigUpdateRequest +): Promise => { + const response = await apiClient.put(`/admin/common-configs/${id}`, data); + return response.data; +}; + +export const deleteCommonConfig = async (id: number): Promise => { + await apiClient.delete(`/admin/common-configs/${id}`); +}; + +export const updateCommonConfigStatus = async ( + id: number, + status: number +): Promise => { + await apiClient.put(`/admin/common-configs/${id}/status`, { status }); +}; +``` + +### 步骤 3:创建页面组件 + +**文件**: `src/pages/CommonConfigs.tsx` + +```typescript +import React, { useEffect, useState } from 'react'; +import { + Table, + Button, + Modal, + Form, + Input, + Tag, + Space, + Popconfirm, + message, + Row, + Col, + Card, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + CheckCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; +import type { CommonConfig } from '../types/commonConfig'; +import { + getCommonConfigList, + createCommonConfig, + updateCommonConfig, + deleteCommonConfig, + updateCommonConfigStatus, +} from '../services/commonConfigApi'; + +export default function CommonConfigs() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + + // 获取列表 + const fetchList = async () => { + setLoading(true); + try { + const res = await getCommonConfigList({ page, size }); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } catch (e) { + message.error('获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + const openEdit = (config: CommonConfig) => { + setEditing(config); + form.setFieldsValue({ + config_key: config.config_key, + config_value: config.config_value, + description: config.description, + }); + setEditOpen(true); + }; + + const submitEdit = async () => { + try { + const values = await form.validateFields(); + if (!editing) return; + await updateCommonConfig(editing.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); + } catch (error) { + message.error('更新失败'); + } + }; + + const openCreate = () => { + createForm.resetFields(); + setCreateOpen(true); + }; + + const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createCommonConfig(values); + message.success('创建成功'); + setCreateOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败'); + } + }; + + const handleDelete = async (config: CommonConfig) => { + try { + await deleteCommonConfig(config.id); + message.success('删除成功'); + fetchList(); + } catch (error) { + message.error('删除失败'); + } + }; + + const handleToggleStatus = async (config: CommonConfig) => { + try { + const newStatus = config.status === 1 ? 0 : 1; + await updateCommonConfigStatus(config.id, newStatus); + message.success(newStatus === 1 ? '已启用' : '已禁用'); + fetchList(); + } catch (error) { + message.error('状态更新失败'); + } + }; + + const columns = [ + { + title: '配置键', + dataIndex: 'config_key', + key: 'config_key', + }, + { + title: '配置值', + dataIndex: 'config_value', + key: 'config_value', + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (value: number) => + value === 1 ? 启用 : 禁用, + }, + { + title: '操作', + key: 'action', + render: (_: any, config: CommonConfig) => ( + + + + handleDelete(config)}> + + + + ), + }, + ]; + + return ( +
+ +
+ +
+ +
+
{ + setPage(p); + setSize(s); + }, + }} + /> + + + + + {/* 编辑弹窗 */} + setEditOpen(false)} + > +
+ + + + + + + + + + +
+ + {/* 创建弹窗 */} + setCreateOpen(false)} + > +
+ + + + + + + + + + +
+ + ); +} +``` + +### 步骤 4:在 App.tsx 中添加路由 + +在 `src/App.tsx` 中找到系统管理路由部分,添加: + +```typescript +import CommonConfigs from './pages/CommonConfigs'; + +// 在 Routes 中的系统管理部分添加: +} /> +``` + +### 步骤 5:在 Layout.tsx 中添加菜单项 + +在 `src/components/Layout.tsx` 中找到系统管理子菜单部分(`data-tabs="admin"`),添加: + +```typescript + +``` + +--- + +## 核心文件速查表 + +| 功能 | 文件位置 | 说明 | +|------|---------|------| +| 全局路由 | `src/App.tsx` | 所有页面路由定义 | +| 菜单导航 | `src/components/Layout.tsx` | 菜单项和导航配置 | +| API 客户端 | `src/services/api.ts` | Axios 包装,自动处理 Token | +| 权限检查 | `src/hooks/usePagePermissions.ts` | 获取用户权限信息 | +| 认证管理 | `src/hooks/useAuth.ts` | SSO 认证相关逻辑 | +| 全局状态 | `src/atoms/auth.ts` | Jotai 状态原子定义 | + +--- + +## 常用开发模式 + +### 1. 获取列表并分页 + +```typescript +const [list, setList] = useState([]); +const [page, setPage] = useState(1); +const [size, setSize] = useState(10); +const [total, setTotal] = useState(0); + +const fetchList = async () => { + const res = await getXxxList({ page, size }); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); +}; + +useEffect(() => { + fetchList(); +}, [page, size]); + +// 在 Table 中使用 +
{ + setPage(p); + setSize(s); + }, + }} +/> +``` + +### 2. 打开编辑弹窗并回显数据 + +```typescript +const [editOpen, setEditOpen] = useState(false); +const [editing, setEditing] = useState(null); +const [form] = Form.useForm(); + +const openEdit = (record: T) => { + setEditing(record); + form.setFieldsValue({ + field1: record.field1, + field2: record.field2, + }); + setEditOpen(true); +}; + +const submitEdit = async () => { + const values = await form.validateFields(); + await updateXxx(editing!.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); +}; +``` + +### 3. 权限检查 + +```typescript +import { usePagePermissions } from '../hooks/usePagePermissions'; + +function MyComponent() { + const { getAccessiblePages } = usePagePermissions(); + const accessiblePages = getAccessiblePages(); + + if (!accessiblePages.includes('/system')) { + return
无权访问
; + } + + return
内容
; +} +``` + +### 4. 调用 API + +```typescript +import { apiClient } from '../services/api'; + +// GET 请求 +const data = await apiClient.get('/admin/xxx'); + +// POST 请求 +const result = await apiClient.post('/admin/xxx', { key: 'value' }); + +// PUT 请求 +const updated = await apiClient.put('/admin/xxx/123', { key: 'new-value' }); + +// DELETE 请求 +await apiClient.delete('/admin/xxx/123'); +``` + +--- + +## Ant Design 常用组件 + +| 组件 | 用途 | 导入 | +|------|------|------| +| `Button` | 按钮 | `from 'antd'` | +| `Table` | 数据表格 | `from 'antd'` | +| `Form` | 表单 | `from 'antd'` | +| `Input` | 文本输入 | `from 'antd'` | +| `InputNumber` | 数字输入 | `from 'antd'` | +| `Select` | 下拉选择 | `from 'antd'` | +| `Modal` | 弹窗对话框 | `from 'antd'` | +| `Message` | 消息提示 | `from 'antd'` | +| `Tag` | 标签 | `from 'antd'` | +| `Card` | 卡片容器 | `from 'antd'` | +| `Space` | 间距布局 | `from 'antd'` | +| `Popconfirm` | 确认弹窗 | `from 'antd'` | + +--- + +## 开发工作流 + +1. **创建类型定义** → `src/types/xxx.ts` +2. **创建 API 服务** → `src/services/xxxApi.ts` +3. **创建页面组件** → `src/pages/Xxx.tsx` +4. **添加路由** → `src/App.tsx` +5. **添加菜单** → `src/components/Layout.tsx` +6. **测试** → `npm run dev` + +--- + +## API 命名规范 + +- 列表: `GET /admin/xxx` +- 详情: `GET /admin/xxx/:id` +- 创建: `POST /admin/xxx` +- 更新: `PUT /admin/xxx/:id` +- 删除: `DELETE /admin/xxx/:id` +- 状态: `PUT /admin/xxx/:id/status` + +--- + +## 注意事项 + +1. **Token 自动管理**: API 请求会自动附带 Token,401 错误会自动刷新 Token +2. **权限检查**: 菜单和路由需要通过 `usePagePermissions` 检查用户权限 +3. **类型安全**: 始终使用 TypeScript 定义接口,避免使用 `any` +4. **表单验证**: 使用 `Form.useForm()` 和 `validateFields()` 进行表单验证 +5. **消息提示**: 使用 `message.success()` / `message.error()` 等方法提示用户 +6. **异步处理**: 使用 `async/await` 简化异步逻辑 + +--- + +## 参考文档 + +完整的开发指南请查看: `/Users/youziba/goalfyagent/goalfymax-admin-web/FRONTEND_GUIDE.md` + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9978f16 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Goalfy Admin Web - Token 统计系统 + +基于 React + Ant Design + TypeScript + Vite 技术栈开发的 Token 使用统计前端页面。 + +## 技术栈 + +- **React 19.1.1** - 前端框架 +- **Ant Design 5.x** - UI 组件库 +- **TypeScript 5.9.3** - 类型系统 +- **Vite 7.1.7** - 构建工具 +- **Axios** - HTTP 客户端 +- **Day.js** - 日期处理 +- **ESLint** - 代码检查 + +## 功能特性 + +### 🎯 核心功能 +- **Token 使用统计** - 实时显示 Token 使用量、用户数、项目数、模型数 +- **历史数据查询** - 支持按时间范围、用户、项目等条件查询 +- **数据可视化** - 图表展示使用趋势和排行榜 +- **详细数据表格** - 支持排序、分页的详细数据展示 + +### 🎨 界面设计 +- **深色主题** - 参考 goalfy-admin 的深色设计风格 +- **响应式布局** - 支持桌面和移动端 +- **现代化 UI** - 使用 Ant Design 组件库 +- **直观操作** - 简洁的查询界面和操作流程 + +## 项目结构 + +``` +src/ +├── components/ # React 组件 +│ ├── QuotaFilters.tsx # 查询过滤器 +│ ├── QuotaStats.tsx # 统计概览 +│ ├── QuotaCharts.tsx # 图表组件 +│ └── QuotaHistoryTable.tsx # 历史数据表格 +├── services/ # API 服务 +│ └── api.ts # API 客户端 +├── types/ # TypeScript 类型 +│ └── quota.ts # 配额相关类型 +├── App.tsx # 主应用组件 +├── App.css # 全局样式 +└── main.tsx # 应用入口 +``` + +## 快速开始 + +### 1. 安装依赖 +```bash +npm install +``` + +### 2. 启动开发服务器 +```bash +npm run dev +``` + +### 3. 构建生产版本 +```bash +npm run build +``` + +## API 配置 + +在 `src/services/api.ts` 中配置后端 API 地址: + +```typescript +const API_BASE_URL = 'http://localhost:8080'; // 修改为实际的后端地址 +``` + +## 后端 API 接口 + +应用调用以下后端接口: + +- `POST /api/quotas/history` - 获取配额历史数据 +- `GET /health` - 健康检查 + +### 请求参数示例 + +```json +{ + "start_date": "2024-01-01", + "end_date": "2024-01-31", + "user_id": "user123", + "api_group": "openai", + "project_id": "project456", + "period": "daily", + "group_by": ["user_id", "model"] +} +``` + +### 响应数据格式 + +```json +{ + "success": true, + "data": [ + { + "id": 1, + "user_id": "user123", + "api_group": "openai", + "project_id": "project456", + "day": "2024-01-01", + "account": "account123", + "model": "gpt-4", + "quota_used": 1000.5, + "created_at": "2024-01-01T10:00:00Z" + } + ] +} +``` + +## 组件说明 + +### QuotaFilters +查询条件过滤器,支持: +- 时间范围选择 +- 用户ID、API组、项目ID筛选 +- 统计周期选择 +- 分组维度配置 + +### QuotaStats +统计概览卡片,显示: +- 总 Token 使用量 +- 活跃用户数 +- 项目数量 +- 模型数量 + +### QuotaCharts +数据可视化图表: +- 每日使用趋势图 +- 用户使用量排行 +- 模型使用量排行 + +### QuotaHistoryTable +详细数据表格,支持: +- 数据排序 +- 分页显示 +- 响应式布局 +- 数据导出 + +## 样式定制 + +应用使用深色主题,主要颜色变量: + +```css +--bg: #0b0c0f; /* 背景色 */ +--panel: #12141a; /* 面板色 */ +--text: #e7e9ee; /* 文字色 */ +--accent: #5f97d2; /* 强调色 */ +--border: #232634; /* 边框色 */ +``` + +## 开发说明 + +### 添加新功能 +1. 在 `src/types/` 中定义类型 +2. 在 `src/services/` 中添加 API 调用 +3. 在 `src/components/` 中创建组件 +4. 在 `App.tsx` 中集成组件 + +### 代码规范 +- 使用 TypeScript 严格模式 +- 遵循 ESLint 规则 +- 组件使用函数式写法 +- 使用 Ant Design 组件库 + +## 部署说明 + +1. 构建生产版本:`npm run build` +2. 将 `dist` 目录部署到 Web 服务器 +3. 配置后端 API 地址 +4. 确保 CORS 配置正确 + +## 许可证 + +内部使用,仅供 Goalfy 团队使用。 \ No newline at end of file diff --git a/README_ENV.md b/README_ENV.md new file mode 100644 index 0000000..70c47b5 --- /dev/null +++ b/README_ENV.md @@ -0,0 +1,78 @@ +# 环境变量配置 + +## 配置说明 + +项目支持通过环境变量配置,支持不同环境的部署需求。参考 `goalfymax-front` 项目的环境变量配置方式。 + +## 环境变量文件 + +### 开发环境 +创建 `.env.development` 文件: +```bash +VITE_API_BASE_URL=http://localhost:8084 +VITE_APP_TITLE=GoalfyMax Admin +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_DEVTOOLS=true +VITE_USE_MOCK=false +``` + +### 生产环境 +创建 `.env.production` 文件: +```bash +VITE_API_BASE_URL=https://your-production-api.com +VITE_APP_TITLE=GoalfyMax Admin +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_DEVTOOLS=false +VITE_USE_MOCK=false +``` + +### 测试环境 +创建 `.env.test` 文件: +```bash +VITE_API_BASE_URL=https://your-test-api.com +VITE_APP_TITLE=GoalfyMax Admin +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_DEVTOOLS=true +VITE_USE_MOCK=false +``` + +## 使用方法 + +### 1. 复制环境变量模板 +```bash +cp env.example .env.development +``` + +### 2. 修改配置 +编辑对应的环境变量文件,设置正确的配置值。 + +### 3. 启动项目 +```bash +# 开发环境 +npm run dev + +# 生产环境构建 +npm run build +``` + +## 环境变量说明 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `VITE_API_BASE_URL` | API 基础地址 | `http://localhost:8084` | +| `VITE_APP_TITLE` | 应用标题 | `GoalfyMax Admin` | +| `VITE_APP_VERSION` | 应用版本 | `1.0.0` | +| `VITE_ENABLE_DEVTOOLS` | 启用开发工具 | `true` | +| `VITE_USE_MOCK` | 使用 Mock 数据 | `false` | + +## 代理配置 + +开发环境下,Vite 会自动代理 `/api` 请求到 `VITE_API_BASE_URL`,无需在代码中硬编码 API 地址。 + +## 注意事项 + +1. 环境变量文件需要以 `.env` 开头 +2. 变量名必须以 `VITE_` 开头才能在客户端代码中使用 +3. 生产环境构建时,环境变量会被编译到代码中 +4. 敏感信息不要放在环境变量中,应该通过后端配置 +5. 开发环境使用代理,生产环境直接使用环境变量中的 API 地址 diff --git a/build-and-push.sh b/build-and-push.sh new file mode 100755 index 0000000..a6d7ceb --- /dev/null +++ b/build-and-push.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e # 遇到错误立即退出 + +# 配置变量 +AWS_ACCOUNT_ID="177603749739" +AWS_REGION="us-west-2" +ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com" +IMAGE_NAME="goalfy/goalfymax-admin-web" +IMAGE_TAG="${1:-latest}" # 默认使用 latest,可通过第一个参数指定版本 + +FULL_IMAGE_NAME="${ECR_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" + +echo "==========================================" +echo "构建和推送前端 Docker 镜像到 AWS ECR" +echo "==========================================" +echo "镜像名称: ${FULL_IMAGE_NAME}" +echo "==========================================" + +# 1. 登录到 AWS ECR +echo "步骤 1/4: 登录到 AWS ECR..." +aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY} + +if [ $? -ne 0 ]; then + echo "错误: ECR 登录失败,请检查 AWS 凭证配置" + exit 1 +fi + +echo "✓ ECR 登录成功" + +# 2. 构建 Docker 镜像 +echo "" +echo "步骤 2/4: 构建 Docker 镜像..." +docker build --platform linux/amd64 -t ${FULL_IMAGE_NAME} -f Dockerfile . + +if [ $? -ne 0 ]; then + echo "错误: Docker 镜像构建失败" + exit 1 +fi + +echo "✓ 镜像构建成功" + +# 3. 如果指定了版本标签,同时也打上 latest 标签 +if [ "${IMAGE_TAG}" != "latest" ]; then + echo "" + echo "步骤 3/4: 打 latest 标签..." + LATEST_IMAGE_NAME="${ECR_REGISTRY}/${IMAGE_NAME}:latest" + docker tag ${FULL_IMAGE_NAME} ${LATEST_IMAGE_NAME} + echo "✓ latest 标签已创建" +else + echo "" + echo "步骤 3/4: 跳过(已经是 latest 标签)" +fi + +# 4. 推送镜像到 ECR +echo "" +echo "步骤 4/4: 推送镜像到 ECR..." +docker push ${FULL_IMAGE_NAME} + +if [ $? -ne 0 ]; then + echo "错误: 镜像推送失败" + exit 1 +fi + +# 如果创建了 latest 标签,也推送它 +if [ "${IMAGE_TAG}" != "latest" ]; then + echo "推送 latest 标签..." + docker push ${LATEST_IMAGE_NAME} +fi + +echo "" +echo "==========================================" +echo "✓ 完成!镜像已成功推送" +echo "==========================================" +echo "镜像地址: ${FULL_IMAGE_NAME}" +if [ "${IMAGE_TAG}" != "latest" ]; then + echo " ${LATEST_IMAGE_NAME}" +fi +echo "==========================================" +echo "" +echo "下一步: 部署到 K8s 集群" +echo " kubectl apply -f k8s/" diff --git a/env.example b/env.example new file mode 100644 index 0000000..0b5d1b3 --- /dev/null +++ b/env.example @@ -0,0 +1,15 @@ +# 环境变量配置示例 +# 复制此文件为 .env.local 或 .env.development 等,并根据需要修改 + +# API 基础地址 +VITE_API_BASE_URL=http://localhost:8084 + +# 应用信息 +VITE_APP_TITLE=GoalfyMax Admin +VITE_APP_VERSION=1.0.0 + +# 开发工具 +VITE_ENABLE_DEVTOOLS=true + +# Mock 数据 +VITE_USE_MOCK=false diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b19330b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..73b08e4 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + goalfymax-admin-web + + +
+ + + diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..0dcc59e --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: goalfymax-admin-web-config + namespace: goalfyagent +data: + nginx.conf: | + server { + listen 80; + server_name _; + + # 前端静态文件根目录 + root /usr/share/nginx/html; + index index.html index.htm; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # 前端路由支持(SPA应用) + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # 健康检查接口 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 安全头设置 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 隐藏 nginx 版本 + server_tokens off; + } diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..66b04ca --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: goalfymax-admin-web + namespace: goalfyagent + labels: + app: goalfymax-admin-web +spec: + replicas: 2 + selector: + matchLabels: + app: goalfymax-admin-web + template: + metadata: + labels: + app: goalfymax-admin-web + spec: + containers: + - name: goalfymax-admin-web + image: 177603749739.dkr.ecr.us-west-2.amazonaws.com/goalfy/goalfymax-admin-web:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + protocol: TCP + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "256Mi" + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: nginx.conf + readOnly: true + volumes: + - name: nginx-config + configMap: + name: goalfymax-admin-web-config + restartPolicy: Always diff --git a/k8s/httproute.yaml b/k8s/httproute.yaml new file mode 100644 index 0000000..be39566 --- /dev/null +++ b/k8s/httproute.yaml @@ -0,0 +1,38 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: goalfymax-admin-web-route + namespace: goalfyagent + labels: + app: goalfymax-admin-web +spec: + # 指定父级 Gateway + parentRefs: + - name: ingress-gateway + namespace: istio-system + + # 配置主机名 + hostnames: + - "goalfymax-admin.goalfyai.com" + + # 路由规则 + rules: + # 前端静态文件路由 + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: goalfymax-admin-web + port: 80 + weight: 100 + + # API 路由到后端服务 + - matches: + - path: + type: PathPrefix + value: /api/ + backendRefs: + - name: goalfymax-admin + port: 8087 + weight: 100 diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..d2a94fb --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: goalfymax-admin-web + namespace: goalfyagent + labels: + app: goalfymax-admin-web +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app: goalfymax-admin-web diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..c9b6d1a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 80; + server_name _; + + # 前端静态文件根目录 + root /usr/share/nginx/html; + index index.html index.htm; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # 前端路由支持(SPA应用) + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # 健康检查接口 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 安全头设置 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 隐藏 nginx 版本 + server_tokens off; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7861f83 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4769 @@ +{ + "name": "goalfymax-admin-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "goalfymax-admin-web", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.1.0", + "antd": "^5.27.4", + "axios": "^1.12.2", + "dayjs": "^1.11.18", + "jotai": "^2.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.9.4" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^5.4.20" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz", + "integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz", + "integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/color-picker/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.1.tgz", + "integrity": "sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.0.tgz", + "integrity": "sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.3.0.tgz", + "integrity": "sha512-hfXE04CVsxI/slmWKeSh6du7sSKpbvVdVEZCa8A+2QWDlL97EsCYme2c3ZWLn1uC9FR21JoewlrhUPWO4QgO8w==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", + "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", + "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/type-utils": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", + "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", + "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "5.27.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.27.4.tgz", + "integrity": "sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.53.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.2", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/antd/node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/antd/node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/antd/node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", + "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jotai": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz", + "integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0", + "@babel/template": ">=7.0.0", + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@babel/template": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "devOptional": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.53.1.tgz", + "integrity": "sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", + "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.0", + "@typescript-eslint/parser": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce95f96 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "goalfymax-admin-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.1.0", + "antd": "^5.27.4", + "axios": "^1.12.2", + "dayjs": "^1.11.18", + "jotai": "^2.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.9.4" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^5.4.20" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..5c87033 --- /dev/null +++ b/src/App.css @@ -0,0 +1,214 @@ +/* --- Query form styles (aligned with gateway UI tone) --- */ +.form-row { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + margin-bottom: 12px; +} +.form-item { display: inline-flex; align-items: center; gap: 6px; } +.form-label { font-size: 12px; color: var(--muted); white-space: nowrap; } +.form-input, .form-select { + height: 32px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--soft); + color: var(--text); + font-size: 14px; +} +.form-select[multiple] { + height: 120px; + padding: 6px 8px; +} +.dropdown-panel { + background: var(--soft); + border: 1px solid var(--border); + border-radius: 8px; + min-width: 220px; + box-shadow: 0 8px 20px rgba(0,0,0,0.25); +} +.form-split { color: var(--muted); } +:root{ + --bg: #f5f7fb; + --panel: #ffffff; + --soft: #f1f3f7; + --text: #171a1f; + --muted: #4d5562; + --accent: #5f97d2; + --good: #2fb167; + --warn: #caa410; + --bad: #b6404a; + --border: #e5e7ef; + --shadow: 0 10px 25px rgba(10,16,25,.08); +} +:root.light{ + --bg: #f5f7fb; + --panel: #ffffff; + --soft: #f1f3f7; + --text: #171a1f; + --muted: #4d5562; + --accent: #5f97d2; + --good: #2fb167; + --warn: #caa410; + --bad: #b6404a; + --border: #e5e7ef; + --shadow: 0 10px 25px rgba(10,16,25,.08); +} +*{box-sizing:border-box; margin:0; padding:0} +html,body{height:100%; width:100%; margin:0; padding:0} +#root{width:100%; height:100%; margin:0; padding:0} +body{ + margin:0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + background:var(--bg); + color:var(--text); + display:flex; + min-height:100vh; + width:100%; + overflow:hidden; +} +a{color:inherit; text-decoration:none} +.container{display:flex; flex:1; width:100vw; height:100vh; max-width:none; margin:0; padding:0} + +.sidebar{ + border-right:1px solid var(--border); + background:linear-gradient(180deg, var(--panel), var(--soft)); + padding:20px; height:100vh; flex-shrink:0; + display:flex; flex-direction:column; + overflow: hidden; +} +.brand{ display:flex; align-items:center; gap:10px; padding:8px 6px; margin-bottom:16px; } +.brand .logo{ + width:28px; height:28px; border-radius:8px; + background: radial-gradient(120% 120% at 0% 0%, var(--accent) 0%, #9dc3e7 48%, #9394e7 100%); + box-shadow:var(--shadow); +} +.brand h1{font-size:16px; letter-spacing:.4px; margin:0; font-weight:700} + +.sidegroup{margin-top:16px} +.sidegroup h2{ font-size:11px; text-transform:uppercase; color:var(--muted); letter-spacing:.8px; margin:12px 8px; } +.nav{display:flex; flex-direction:column; gap:4px} +.nav a{ + display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:10px; + color:var(--text); border:1px solid transparent; +} +.nav a:hover{background:var(--soft)} +.nav a.active{background:var(--soft); border-color:var(--border)} + +.header{ + display:flex; align-items:center; justify-content:space-between; padding:16px 22px; + border-bottom:1px solid var(--border); background:var(--panel); z-index:6; flex-shrink:0; +} +.search{ display:flex; align-items:center; gap:10px; background:var(--soft); padding:8px 12px; border-radius:12px; border:1px solid var(--border); } +.search input{background:transparent; border:none; outline:none; color:var(--text); width:280px} +.actions{display:flex; align-items:center; gap:10px} +.btn{ padding:8px 12px; border-radius:10px; border:1px solid var(--border); background:var(--soft); color:var(--text); cursor:pointer; } +.btn.primary{background:var(--accent); border-color:transparent; color:white; box-shadow:var(--shadow)} + +.main{flex:1; display:flex; flex-direction:column; min-width:0; height:100vh; overflow:hidden; width:100%} +.subnav{ + background:var(--panel); + padding:12px 22px; border-bottom:1px solid var(--border); flex-shrink:0; + overflow-x:auto; -webkit-overflow-scrolling:touch; +} +.segment{ + display:flex; gap:8px; background:var(--soft); padding:6px; border-radius:12px; border:1px solid var(--border); width:max-content; +} +.segment button{ + border:none; background:transparent; color:var(--muted); padding:8px 12px; border-radius:8px; cursor:pointer; +} +.segment button.active{ + background:var(--panel); color:var(--text); box-shadow:var(--shadow); +} + +.content{padding:22px; display:flex; flex-direction:column; gap:16px; flex:1; overflow-y:auto; min-height:0; box-sizing:border-box} + +.panel{ + background:var(--panel); border:1px solid var(--border); border-radius:16px; padding:18px; box-shadow:var(--shadow); + width:100%; +} +.panel h2{margin:0 0 8px 0; font-size:16px} +.panel .desc{color:var(--muted); font-size:13px; margin-bottom:10px} + +.flex-2{display:grid; grid-template-columns: 1.2fr 1fr; gap:16px} +.flex-3{display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:16px} +.full{display:block; width:100%; box-sizing:border-box} + +.kpis{display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap:12px} +.kpi{ + background:var(--soft); border:1px solid var(--border); border-radius:14px; padding:14px; +} +.kpi h3{margin:0; font-size:12px; color:var(--muted)} +.kpi .num{font-size:26px; font-weight:800; margin-top:6px} +.kpi .delta{font-size:12px; margin-left:8px; padding:2px 6px; border-radius:8px; background:var(--panel); border:1px solid var(--border)} + +.table{width:100%; border-collapse:collapse; font-size:14px; min-width:800px; table-layout:fixed; box-sizing:border-box} +.table th, .table td{padding:10px 12px; border-bottom:1px solid var(--border); text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis} +.table thead th{color:var(--muted); font-weight:600} +.table-container{overflow-x:auto; width:100%; max-width:100%; -webkit-overflow-scrolling:touch; box-sizing:border-box} +.table th:nth-child(1), .table td:nth-child(1){width:8%} +.table th:nth-child(2), .table td:nth-child(2){width:15%} +.table th:nth-child(3), .table td:nth-child(3){width:12%} +.table th:nth-child(4), .table td:nth-child(4){width:15%} +.table th:nth-child(5), .table td:nth-child(5){width:15%} +.table th:nth-child(6), .table td:nth-child(6){width:15%} +.table th:nth-child(7), .table td:nth-child(7){width:20%} +.badge{font-size:12px; padding:4px 8px; border-radius:999px; background:var(--soft); border:1px solid var(--border)} +.badge.good{background:rgba(47,177,103,.12); border-color:rgba(47,177,103,.35); color:var(--good)} +.badge.warn{background:rgba(202,164,16,.12); border-color:rgba(202,164,16,.35); color:var(--warn)} +.badge.bad{ background:rgba(182,64,74,.12); border-color:rgba(182,64,74,.35); color:var(--bad)} + +.chart{ height:260px; border:1px dashed var(--border); border-radius:12px; display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:13px; } + +.footer{padding:12px 22px; border-top:1px solid var(--border); color:var(--muted)} + +.hidden{display:none} + +@media (max-width: 1200px){ + .kpis{grid-template-columns: repeat(2, minmax(0,1fr));} + .flex-3{grid-template-columns: repeat(2, minmax(0,1fr));} + .flex-2{grid-template-columns: 1fr;} + .table{font-size:13px; min-width:700px} + .table th, .table td{padding:8px 10px} + .table th:nth-child(1), .table td:nth-child(1){width:10%} + .table th:nth-child(2), .table td:nth-child(2){width:18%} + .table th:nth-child(3), .table td:nth-child(3){width:15%} + .table th:nth-child(4), .table td:nth-child(4){width:18%} + .table th:nth-child(5), .table td:nth-child(5){width:18%} + .table th:nth-child(6), .table td:nth-child(6){width:15%} + .table th:nth-child(7), .table td:nth-child(7){width:6%} +} +@media (max-width: 900px){ + .sidebar{display:none} + .sidebar.mobile-open{display:flex; position:fixed; left:0; top:0; height:100vh; width:240px; z-index:1000;} + .mobile-backdrop{position:fixed; inset:0; background:rgba(0,0,0,.35); z-index:999} + .mobile-toggle{display:inline-flex; align-items:center; gap:6px} + .kpis{grid-template-columns: 1fr;} + .flex-3{grid-template-columns: 1fr;} + .table{font-size:12px; min-width:600px} + .table th, .table td{padding:6px 8px} + .search input{width:200px} + .header{padding:12px 16px} + .content{padding:16px; overflow-x:auto} + .table th:nth-child(1), .table td:nth-child(1){width:12%} + .table th:nth-child(2), .table td:nth-child(2){width:20%} + .table th:nth-child(3), .table td:nth-child(3){width:18%} + .table th:nth-child(4), .table td:nth-child(4){width:20%} + .table th:nth-child(5), .table td:nth-child(5){width:18%} + .table th:nth-child(6), .table td:nth-child(6){width:12%} + .table th:nth-child(7), .table td:nth-child(7){width:0%; display:none} +} +@media (max-width: 600px){ + .table{min-width:500px} + .table th, .table td{padding:4px 6px; font-size:11px} + .kpi .num{font-size:20px} + .kpi h3{font-size:11px} + .table th:nth-child(1), .table td:nth-child(1){width:15%} + .table th:nth-child(2), .table td:nth-child(2){width:25%} + .table th:nth-child(3), .table td:nth-child(3){width:20%} + .table th:nth-child(4), .table td:nth-child(4){width:0%; display:none} + .table th:nth-child(5), .table td:nth-child(5){width:25%} + .table th:nth-child(6), .table td:nth-child(6){width:15%} + .table th:nth-child(7), .table td:nth-child(7){width:0%; display:none} +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..c64602e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,276 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import PagePermissionGuard from './components/PagePermissionGuard'; +import { usePagePermissions } from './hooks/usePagePermissions'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import Overview from './pages/Overview'; +import Operations from './pages/Operations'; +import Monitoring from './pages/Monitoring'; +import Finance from './pages/Finance'; +import FinanceTransactionLogs from './pages/FinanceTransactionLogs'; +import TokenHistoryPage from './pages/TokenHistory'; +import TokenAnalytics from './pages/TokenAnalytics'; +import SystemHealth from './pages/SystemHealth'; +import UserProjectQuotaWrapper from './pages/UserProjectQuota'; +import UserManagement from './pages/UserManagement'; +import RoleManagement from './pages/RoleManagement'; +import GoalfyMaxUsers from './pages/GoalfyMaxUsers'; +import UserFeedback from './pages/UserFeedback'; +import MessagePush from './pages/MessagePush'; +import VendorModelPricing from './pages/VendorModelPricing'; +import MCPProviderPricing from './pages/MCPProviderPricing'; +import VmPricing from './pages/VmPricing'; +import SystemConfigs from './pages/SystemConfigs'; +import UserLevelConfigs from './pages/UserLevelConfigs'; +import AuditLogs from './pages/AuditLogs'; +import InviteCodes from './pages/InviteCodes'; +import NoPermission from './pages/NoPermission'; +import FinanceSandboxCosts from './pages/FinanceSandboxCosts'; +import FinanceTokenUsage from './pages/FinanceTokenUsage'; +import FinanceMcpUsage from './pages/FinanceMcpUsage'; +import FinancePaymentRecords from './pages/FinancePaymentRecords'; +import FinanceMcpAccountRechargeRecords from './pages/FinanceMcpAccountRechargeRecords'; +import FinanceModelAccountRechargeRecords from './pages/FinanceModelAccountRechargeRecords'; + +function App() { + const FirstAllowedRedirect: React.FC = () => { + const { getAccessiblePages } = usePagePermissions(); + const pages = getAccessiblePages(); + const target = pages[0] || '/no-permission'; + console.log('↪️ [Router] default redirect to:', target, 'pages=', pages); + return ; + }; + return ( + + + {/* 默认落地:首个可访问页面或无权限页 */} + } /> + + {/* 仪表盘(受页面权限控制) */} + }> + + + } + /> + + {/* 总览页面(受页面权限控制) */} + }> + + + } + /> + + {/* 运营页面 - 嵌套路由(父路径受控) */} + } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + {/* 监控页面 - 嵌套路由(父路径受控) */} + } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + {/* 财务页面(父路由重定向到子页面:用户流水) */} + } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + {/* 系统管理页面 - 嵌套路由 */} + } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + {/* 无权限页面 */} + } /> + + {/* 404页面 */} + } /> + + + ); +} + +export default App; \ No newline at end of file diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/atoms/auth.ts b/src/atoms/auth.ts new file mode 100644 index 0000000..a3cc748 --- /dev/null +++ b/src/atoms/auth.ts @@ -0,0 +1,36 @@ +import { atom } from 'jotai'; + +export interface User { + sub: string; + name?: string; + email?: string; + preferred_username?: string; + permissions?: string[]; + roles?: string[]; + pages?: Array<{ + id?: number; + name?: string; + path: string; + icon?: string; + sort_order?: number; + is_active?: boolean; + }>; +} + +export interface AuthState { + user: User | null; + token: string | null; + refreshToken: string | null; + isAuthenticated: boolean; + loginUrl: string | null; +} + +export const authStateAtom = atom({ + user: null, + token: localStorage.getItem('token'), + refreshToken: localStorage.getItem('refreshToken'), + isAuthenticated: false, + loginUrl: null, +}); + +export const authLoadingAtom = atom(false); diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard.tsx new file mode 100644 index 0000000..106df02 --- /dev/null +++ b/src/components/AuthGuard.tsx @@ -0,0 +1,90 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Spin } from 'antd'; +import { useAuth } from '../hooks/useAuth'; + +interface AuthGuardProps { + children: React.ReactNode; +} + +const AuthGuard: React.FC = ({ children }) => { + const { + isAuthenticated, + initAuth, + loginUrl, + redirectToLogin, + loading, + logout, + } = useAuth(); + + const initedRef = useRef(false); + const [booting, setBooting] = useState(true); + + // 调试信息 + console.log('🔐 [AuthGuard] 状态:', { + booting, + isAuthenticated, + loading, + hasLoginUrl: !!loginUrl, + }); + + useEffect(() => { + if (initedRef.current) { + setBooting(false); + return; + } + initedRef.current = true; + (async () => { + try { + await initAuth(); + } finally { + setBooting(false); + } + })(); + }, [initAuth]); + + // 未认证时,主动触发SSO登录流程 + useEffect(() => { + if (!booting && !isAuthenticated && !loading) { + redirectToLogin(); + } + }, [booting, isAuthenticated, loading, redirectToLogin]); + + if (booting) { + return ( +
+ +
+ +
+ ); + } + + if (!isAuthenticated) { + // 未认证时显示加载,等待自动跳转SSO + return ( +
+ +
+ +
+ ); + } + + return <>{children}; +}; + +export default AuthGuard; diff --git a/src/components/DynamicMenu.css b/src/components/DynamicMenu.css new file mode 100644 index 0000000..e77ebb4 --- /dev/null +++ b/src/components/DynamicMenu.css @@ -0,0 +1,108 @@ +/* Ant Design Menu 自定义样式 */ +.dynamic-menu-wrapper { + width: 100%; +} + +.dynamic-menu-wrapper .ant-menu-inline { + border: none !important; + background: transparent; +} + +.dynamic-menu-wrapper .ant-menu-item, +.dynamic-menu-wrapper .ant-menu-submenu-title { + border-radius: 10px; + margin: 4px 0; + padding-left: 16px !important; + height: 40px; + line-height: 40px; + transition: all 0.2s; + border: 1px solid transparent; +} + +.dynamic-menu-wrapper .ant-menu-item:hover, +.dynamic-menu-wrapper .ant-menu-submenu-title:hover { + background: var(--soft) !important; +} + +.dynamic-menu-wrapper .ant-menu-item-selected { + background: var(--soft) !important; + border-color: var(--border) !important; +} + +.dynamic-menu-wrapper .ant-menu-item-selected::after { + display: none; +} + +/* 图标样式 */ +.dynamic-menu-wrapper .ant-menu-item .anticon, +.dynamic-menu-wrapper .ant-menu-submenu-title .anticon { + font-size: 16px; + color: var(--text); +} + +/* 折叠时的样式 */ +.dynamic-menu-wrapper .ant-menu-inline-collapsed { + width: 100% !important; +} + +.dynamic-menu-wrapper .ant-menu-inline-collapsed > .ant-menu-item, +.dynamic-menu-wrapper .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title { + padding: 0 16px !important; + text-align: center; + margin: 4px 0; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.dynamic-menu-wrapper .ant-menu-inline-collapsed .ant-menu-item-icon { + margin-inline-end: 0 !important; +} + +.dynamic-menu-wrapper .ant-menu-inline-collapsed > .ant-menu-item .anticon, +.dynamic-menu-wrapper .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title .anticon { + font-size: 20px; + line-height: 40px; + margin: 0 !important; +} + +/* 子菜单样式 */ +.dynamic-menu-wrapper .ant-menu-submenu-title { + color: var(--text); +} + +.dynamic-menu-wrapper .ant-menu-sub { + background: transparent !important; +} + +.dynamic-menu-wrapper .ant-menu-submenu .ant-menu-item { + padding-left: 48px !important; +} + +/* 移除默认的边框和阴影 */ +.dynamic-menu-wrapper .ant-menu-item, +.dynamic-menu-wrapper .ant-menu-submenu { + border: none; +} + +.dynamic-menu-wrapper .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { + background-color: var(--soft); +} + +/* 展开箭头样式 */ +.dynamic-menu-wrapper .ant-menu-submenu-arrow { + color: var(--muted); +} + +/* 子菜单展开时的样式 */ +.dynamic-menu-wrapper .ant-menu-submenu-open > .ant-menu-submenu-title { + color: var(--text); +} + +/* 文字超出隐藏 */ +.dynamic-menu-wrapper .ant-menu-title-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/components/DynamicMenu.tsx b/src/components/DynamicMenu.tsx new file mode 100644 index 0000000..ce10a9f --- /dev/null +++ b/src/components/DynamicMenu.tsx @@ -0,0 +1,250 @@ +import React from 'react'; +import { Menu } from 'antd'; +import { + PieChartOutlined, + TeamOutlined, + ControlOutlined, + MessageOutlined, + BellOutlined, + LineChartOutlined, + AccountBookOutlined, + SettingOutlined, + DatabaseOutlined, + UsergroupAddOutlined, + CrownOutlined +} from '@ant-design/icons'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { usePagePermissions } from '../hooks/usePagePermissions'; +import './DynamicMenu.css'; + +interface MenuItem { + key: string; + label: string; + icon?: React.ReactNode; + path?: string; + children?: MenuItem[]; +} + +interface DynamicMenuProps { + collapsed?: boolean; +} + +const DynamicMenu: React.FC = ({ collapsed = false }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { getAccessiblePages, getPageActions } = usePagePermissions(); + + const accessiblePages = getAccessiblePages(); + + console.log('🔍 [DynamicMenu] 用户可访问页面:', accessiblePages); + + // 菜单配置 - 使用数据库中的页面路径 + const menuItems: MenuItem[] = [ + { + key: 'overview', + label: '总览', + icon: , + path: '/overview', + }, + { + key: 'operations', + label: '运营', + icon: , + path: '/operations', + children: [ + { + key: 'operations-main', + label: '运营管理', + icon: , + path: '/operations', + }, + { + key: 'operations-user-feedback', + label: '用户反馈', + icon: , + path: '/user-feedback', + }, + { + key: 'operations-message-push', + label: '消息推送', + icon: , + path: '/message-push', + }, + { + key: 'operations-mcp-pricing', + label: 'MCP价格配置', + icon: , + path: '/mcp-provider-pricing', + }, + ], + }, + { + key: 'monitoring', + label: '监控', + icon: , + path: '/monitoring', + }, + { + key: 'finance', + label: '财务', + icon: , + path: '/finance', + }, + { + key: 'system', + label: '系统管理', + icon: , + path: '/system/user-management', + children: [ + { + key: 'system-quota', + label: '配额管理', + icon: , + path: '/system', + }, + { + key: 'system-goalfymax-users', + label: 'GoalfyMax用户', + icon: , + path: '/goalfymax-users', + }, + { + key: 'system-user-level-configs', + label: '用户等级管理', + icon: , + path: '/user-level-configs', + }, + ], + }, + ]; + + // 过滤可访问的菜单项 + const filterAccessibleMenus = (items: MenuItem[]): MenuItem[] => { + return items.map(item => { + // 创建新对象,避免修改原始数据 + const newItem = { ...item }; + + // 移除所有子菜单,只显示一级菜单(子导航在页面内显示) + newItem.children = undefined; + + // 父级"系统管理"权限检查 + if (newItem.key === 'system' || newItem.path === '/system') { + const hasSystemAccess = accessiblePages.includes('/system'); + if (!hasSystemAccess) { + console.log(`🚫 [DynamicMenu] 隐藏系统管理: 用户无 /system 权限`); + return null; + } + console.log(`✅ [DynamicMenu] 显示系统管理菜单项`); + return newItem; + } + + // 父级"运营"权限检查 + if (newItem.key === 'operations' || newItem.path === '/operations') { + const hasOperationsAccess = accessiblePages.includes('/operations'); + if (!hasOperationsAccess) { + console.log(`🚫 [DynamicMenu] 隐藏运营: 用户无 /operations 权限`); + return null; + } + console.log(`✅ [DynamicMenu] 显示运营菜单项`); + return newItem; + } + + // 对于有路径的项目,检查权限 + if (newItem.path && !accessiblePages.includes(newItem.path)) { + console.log(`🚫 [DynamicMenu] 隐藏菜单项: ${newItem.label} (${newItem.path}) - 用户无权限`); + return null; + } + + console.log(`✅ [DynamicMenu] 显示菜单项: ${newItem.label} (${newItem.path}) - 用户有权限`); + return newItem; + }).filter((item): item is MenuItem => item !== null); + }; + + const accessibleMenus = filterAccessibleMenus(menuItems); + + console.log('🔍 [DynamicMenu] 原始菜单项:', menuItems.map(item => ({ key: item.key, label: item.label, path: item.path }))); + console.log('🔍 [DynamicMenu] 过滤后的菜单项:', accessibleMenus.map(item => ({ key: item.key, label: item.label, path: item.path }))); + + const handleMenuClick = ({ key }: { key: string }) => { + const findMenuItem = (items: MenuItem[], targetKey: string): MenuItem | null => { + for (const item of items) { + if (item.key === targetKey) { + return item; + } + if (item.children) { + const found = findMenuItem(item.children, targetKey); + if (found) return found; + } + } + return null; + }; + + const menuItem = findMenuItem(accessibleMenus, key); + if (menuItem?.path) { + navigate(menuItem.path); + } + }; + + // 根据当前路径找到对应的菜单key + const getSelectedMenuKey = (): string => { + const currentPath = location.pathname; + + // 遍历所有菜单项,找到匹配当前路径的菜单key + const findKeyByPath = (items: MenuItem[]): string | null => { + for (const item of items) { + // 精确匹配路径 + if (item.path === currentPath) { + return item.key; + } + // 检查是否是子路径(例如 /system/quota-rules 应该选中 system) + if (item.path && currentPath.startsWith(item.path)) { + return item.key; + } + // 递归检查子菜单 + if (item.children) { + const childKey = findKeyByPath(item.children); + if (childKey) return childKey; + } + } + return null; + }; + + const key = findKeyByPath(accessibleMenus); + return key || ''; + }; + + // 转换为 Ant Design Menu 所需的格式 + const convertToMenuItems = (items: MenuItem[]): any[] => { + return items.map(item => { + if (item.children && item.children.length > 0) { + return { + key: item.key, + label: item.label, + icon: item.icon, + children: convertToMenuItems(item.children), + }; + } + return { + key: item.key, + label: item.label, + icon: item.icon, + }; + }); + }; + + const selectedKey = getSelectedMenuKey(); + + return ( +
+ +
+ ); +}; + +export default DynamicMenu; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..03bccac --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,391 @@ +import React, { useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + MenuFoldOutlined, + MenuUnfoldOutlined +} from '@ant-design/icons'; +import { useAuth } from '../hooks/useAuth'; +import DynamicMenu from './DynamicMenu'; + +const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const location = useLocation(); + const navigate = useNavigate(); + const { token, logout } = useAuth(); + const [collapsed, setCollapsed] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + + + + // 从路径获取当前激活的标签 + const getCurrentTab = () => { + const path = location.pathname; + if (path === '/dashboard' || path === '/overview') return 'overview'; + if (path.startsWith('/operations')) return 'operations'; + if (path.startsWith('/monitoring')) return 'monitoring'; + if (path === '/finance' || path.startsWith('/finance/')) return 'finance'; + if (path.startsWith('/system')) return 'admin'; + return 'overview'; + }; + + const getCurrentSubTab = () => { + const path = location.pathname; + // 运营页面子导航 + if (path.startsWith('/operations/user-feedback')) return 'user-feedback'; + if (path.startsWith('/operations/message-push')) return 'message-push'; + if (path.startsWith('/operations/vendor-model-pricing')) return 'vendor-model-pricing'; + if (path.startsWith('/operations/mcp-provider-pricing')) return 'mcp-provider-pricing'; + if (path.startsWith('/operations/vm-pricing')) return 'vm-pricing'; + if (path.startsWith('/operations/invite-codes')) return 'invite-codes'; + // 监控页面子导航 + if (path.startsWith('/monitoring/token-history')) return 'token-history'; + if (path.startsWith('/monitoring/token-analytics')) return 'token-analytics'; + if (path.startsWith('/monitoring/system-health')) return 'system-health'; + // 财务页面子导航 + if (path.startsWith('/finance/transaction-logs')) return 'transaction-logs'; + if (path.startsWith('/finance/sandbox-costs')) return 'sandbox-costs'; + if (path.startsWith('/finance/token-usage')) return 'token-usage'; + if (path.startsWith('/finance/mcp-usage')) return 'mcp-usage'; + if (path.startsWith('/finance/payment-records')) return 'payment-records'; + if (path.startsWith('/finance/mcp-account-recharge-records')) return 'mcp-account-recharge-records'; + if (path.startsWith('/finance/model-account-recharge-records')) return 'model-account-recharge-records'; + // 系统管理页面子导航 + if (path.startsWith('/system/user-project-quota')) return 'user-project-quota'; + if (path.startsWith('/system/user-management')) return 'user-management'; + if (path.startsWith('/system/role-management')) return 'role-management'; + if (path.startsWith('/system/goalfymax-users')) return 'goalfymax-users'; + if (path.startsWith('/system/user-level-configs')) return 'user-level-configs'; + if (path.startsWith('/system/system-configs')) return 'system-configs'; + if (path.startsWith('/system/audit-logs')) return 'audit-logs'; + return ''; + }; + + const activeTab = getCurrentTab(); + const activeSubTab = getCurrentSubTab(); + + const handleLogout = async () => { + await logout(); + }; + + return ( +
+ {/* 侧边栏 */} + + + {/* 主内容区 */} +
+
+
+ +
+
+
+ {token ? ( + <> + 已登录 + + + ) : null} +
+
+
+ + {/* 监控页面的子导航 */} + {activeTab === 'monitoring' && ( + + )} + + {/* 运营页面的子导航 */} + {activeTab === 'operations' && ( + + )} + + {/* 财务页面的子导航 */} + {activeTab === 'finance' && ( + + )} + + {/* 系统管理页面的子导航 */} + {activeTab === 'admin' && ( + + )} + +
+ {children} +
+ +
© 2025 Goalfy · Internal use only
+
+ {mobileOpen && ( +
setMobileOpen(false)} + /> + )} +
+ ); +}; + +export default Layout; diff --git a/src/components/PagePermissionGuard.tsx b/src/components/PagePermissionGuard.tsx new file mode 100644 index 0000000..20e94cf --- /dev/null +++ b/src/components/PagePermissionGuard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { usePagePermissions } from '../hooks/usePagePermissions'; + +interface PagePermissionGuardProps { + pagePath: string; + action?: string; + children: React.ReactNode; + fallback?: React.ReactNode; +} + +const PagePermissionGuard: React.FC = ({ + pagePath, + action = 'access', + children, + fallback = null +}) => { + const { hasPagePermission } = usePagePermissions(); + + const hasAccess = hasPagePermission(pagePath, action); + + return hasAccess ? <>{children} : <>{fallback}; +}; + +export default PagePermissionGuard; diff --git a/src/components/PermissionGuard.tsx b/src/components/PermissionGuard.tsx new file mode 100644 index 0000000..b66fba8 --- /dev/null +++ b/src/components/PermissionGuard.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { usePermissions } from '../hooks/usePermissions'; + +interface PermissionGuardProps { + permission?: string; + permissions?: string[]; + requireAll?: boolean; + role?: string; + roles?: string[]; + requireAllRoles?: boolean; + children: React.ReactNode; + fallback?: React.ReactNode; +} + +const PermissionGuard: React.FC = ({ + permission, + permissions, + requireAll = false, + role, + roles, + requireAllRoles = false, + children, + fallback = null +}) => { + const { + hasPermission, + hasAnyPermission, + hasAllPermissions, + hasRole, + hasAnyRole, + hasAllRoles + } = usePermissions(); + + let hasAccess = false; + + // 检查权限 + if (permission) { + hasAccess = hasPermission(permission); + console.log(`🔐 [PermissionGuard] 检查权限 "${permission}": ${hasAccess}`); + } else if (permissions) { + hasAccess = requireAll ? hasAllPermissions(permissions) : hasAnyPermission(permissions); + console.log(`🔐 [PermissionGuard] 检查权限组 "${permissions}": ${hasAccess}`); + } + + // 检查角色 + if (role) { + hasAccess = hasRole(role); + console.log(`👤 [PermissionGuard] 检查角色 "${role}": ${hasAccess}`); + } else if (roles) { + hasAccess = requireAllRoles ? hasAllRoles(roles) : hasAnyRole(roles); + console.log(`👤 [PermissionGuard] 检查角色组 "${roles}": ${hasAccess}`); + } + + return hasAccess ? <>{children} : <>{fallback}; +}; + +export default PermissionGuard; diff --git a/src/components/QuotaCharts.tsx b/src/components/QuotaCharts.tsx new file mode 100644 index 0000000..11469d6 --- /dev/null +++ b/src/components/QuotaCharts.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Card, Row, Col, Typography } from 'antd'; +import { QuotaHistoryItem } from '../types/quota'; +import dayjs from 'dayjs'; + +const { Title } = Typography; + +interface QuotaChartsProps { + data: QuotaHistoryItem[]; + loading?: boolean; +} + +const QuotaCharts: React.FC = ({ + data, + loading = false +}) => { + // 计算每日使用量 + const dailyUsage = React.useMemo(() => { + const dailyMap = new Map(); + data.forEach(item => { + const day = item.day; + dailyMap.set(day, (dailyMap.get(day) || 0) + item.quota_used); + }); + + return Array.from(dailyMap.entries()) + .map(([date, quota]) => ({ date, quota })) + .sort((a, b) => dayjs(a.date).unix() - dayjs(b.date).unix()); + }, [data]); + + // 计算用户使用量排行 + const topUsers = React.useMemo(() => { + const userMap = new Map(); + data.forEach(item => { + userMap.set(item.user_id, (userMap.get(item.user_id) || 0) + item.quota_used); + }); + + return Array.from(userMap.entries()) + .map(([user_id, quota_used]) => ({ user_id, quota_used })) + .sort((a, b) => b.quota_used - a.quota_used) + .slice(0, 10); + }, [data]); + + // 计算模型使用量排行 + const topModels = React.useMemo(() => { + const modelMap = new Map(); + data.forEach(item => { + modelMap.set(item.model, (modelMap.get(item.model) || 0) + item.quota_used); + }); + + return Array.from(modelMap.entries()) + .map(([model, quota_used]) => ({ model, quota_used })) + .sort((a, b) => b.quota_used - a.quota_used) + .slice(0, 10); + }, [data]); + + return ( + +
+ + 每日 Token 使用趋势 +
+
+
📈
+
图表组件占位
+
+ 数据点: {dailyUsage.length} 个 +
+
+
+
+ + + + + 用户 Token 使用排行 +
+ {topUsers.map((user, index) => ( +
+ + + {index + 1} + + {user.user_id} + + + {user.quota_used.toLocaleString()} + +
+ ))} +
+
+ + + + + 模型 Token 使用排行 +
+ {topModels.map((model, index) => ( +
+ + + {index + 1} + + {model.model} + + + {model.quota_used.toLocaleString()} + +
+ ))} +
+
+ + + ); +}; + +export default QuotaCharts; + + diff --git a/src/components/QuotaFilters.tsx b/src/components/QuotaFilters.tsx new file mode 100644 index 0000000..4107e3c --- /dev/null +++ b/src/components/QuotaFilters.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { + Card, + Form, + DatePicker, + Input, + Select, + Button, + Space, + Row, + Col, + Typography +} from 'antd'; +import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; + +const { Title } = Typography; +const { RangePicker } = DatePicker; +const { Option } = Select; + +interface QuotaFiltersProps { + onSearch: (values: any) => void; + onReset: () => void; + loading?: boolean; +} + +const QuotaFilters: React.FC = ({ + onSearch, + onReset, + loading = false +}) => { + const [form] = Form.useForm(); + + const handleSearch = (values: any) => { + const searchParams = { + ...values, + start_date: values.dateRange?.[0]?.format('YYYY-MM-DD'), + end_date: values.dateRange?.[1]?.format('YYYY-MM-DD'), + }; + delete searchParams.dateRange; + onSearch(searchParams); + }; + + const handleReset = () => { + form.resetFields(); + onReset(); + }; + + return ( + + + 查询条件 + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default QuotaFilters; + + diff --git a/src/components/QuotaHistoryTable.tsx b/src/components/QuotaHistoryTable.tsx new file mode 100644 index 0000000..64441c9 --- /dev/null +++ b/src/components/QuotaHistoryTable.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { Table, Tag, Card, Typography, Space } from 'antd'; +import { QuotaHistoryItem } from '../types/quota'; +import dayjs from 'dayjs'; + +const { Title } = Typography; + +interface QuotaHistoryTableProps { + data: QuotaHistoryItem[]; + loading?: boolean; +} + +const QuotaHistoryTable: React.FC = ({ + data, + loading = false +}) => { + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + }, + { + title: '用户ID', + dataIndex: 'user_id', + key: 'user_id', + width: 120, + }, + { + title: 'API组', + dataIndex: 'api_group', + key: 'api_group', + width: 120, + render: (apiGroup: string) => ( + {apiGroup} + ), + }, + { + title: '项目ID', + dataIndex: 'project_id', + key: 'project_id', + width: 120, + }, + { + title: '账户', + dataIndex: 'account', + key: 'account', + width: 120, + }, + { + title: '模型', + dataIndex: 'model', + key: 'model', + width: 150, + render: (model: string) => ( + {model} + ), + }, + { + title: 'Token 使用量', + dataIndex: 'quota_used', + key: 'quota_used', + width: 120, + render: (quota: number) => ( + + {quota.toLocaleString()} + + ), + sorter: (a: QuotaHistoryItem, b: QuotaHistoryItem) => a.quota_used - b.quota_used, + }, + { + title: '日期', + dataIndex: 'day', + key: 'day', + width: 120, + render: (day: string) => dayjs(day).format('YYYY-MM-DD'), + sorter: (a: QuotaHistoryItem, b: QuotaHistoryItem) => + dayjs(a.day).unix() - dayjs(b.day).unix(), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + width: 160, + render: (createdAt: string) => dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss'), + sorter: (a: QuotaHistoryItem, b: QuotaHistoryItem) => + dayjs(a.created_at).unix() - dayjs(b.created_at).unix(), + }, + ]; + + return ( + + + Token 使用历史 + +
+ `第 ${range[0]}-${range[1]} 条,共 ${total} 条记录`, + }} + scroll={{ x: 1200 }} + size="small" + /> + + ); +}; + +export default QuotaHistoryTable; + + diff --git a/src/components/QuotaStats.tsx b/src/components/QuotaStats.tsx new file mode 100644 index 0000000..76abb2b --- /dev/null +++ b/src/components/QuotaStats.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Card, Row, Col, Statistic, Typography } from 'antd'; +import { + UserOutlined, + ProjectOutlined, + ApiOutlined, + DatabaseOutlined +} from '@ant-design/icons'; + +const { Title } = Typography; + +interface QuotaStatsProps { + totalQuota: number; + totalUsers: number; + totalProjects: number; + totalModels: number; + loading?: boolean; +} + +const QuotaStats: React.FC = ({ + totalQuota, + totalUsers, + totalProjects, + totalModels, + loading = false +}) => { + return ( +
+ + Token 使用统计 + + +
+ + } + suffix="tokens" + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + ); +}; + +export default QuotaStats; + + diff --git a/src/components/UserProjectQuotaPage.tsx b/src/components/UserProjectQuotaPage.tsx new file mode 100644 index 0000000..f0f3d93 --- /dev/null +++ b/src/components/UserProjectQuotaPage.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react' +import { QuotaApiService } from '../services/api' +import type { PageResponse, UserProjectQuotaItem } from '../types/userProjectQuota' + +export default function UserProjectQuotaPage() { + const [items, setItems] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [size, setSize] = useState(10) + const [userId, setUserId] = useState('') + const [enabled, setEnabled] = useState('') + + const [form, setForm] = useState({ id:0, user_id: '', project_limit: 0, vm_limit: 0, process_limit: 0, enabled: true, description: '' }) + const [edit, setEdit] = useState(null) + + const load = async () => { + const res: PageResponse = await QuotaApiService.listUserProjectQuota({ user_id: userId, enabled, page, size }) + const list = res?.data?.data || res?.data?.items || [] + setItems(Array.isArray(list) ? list : []) + setTotal(res?.data?.total || 0) + } + + useEffect(()=>{ load() }, [page, size]) + + return ( +
+

用户项目配额

+
为用户设置项目/虚拟机/进程数量上限
+ +
+ setUserId(e.target.value)} /> + + +
+ +
+

{edit ? '编辑配额' : '创建配额'}

+
+ edit? setEdit({...edit, user_id:e.target.value}) : setForm({...form, user_id:e.target.value}) } disabled={!!edit} /> + edit? setEdit({...edit, project_limit:Number(e.target.value)}) : setForm({...form, project_limit:Number(e.target.value)}) } /> + edit? setEdit({...edit, vm_limit:Number(e.target.value)}) : setForm({...form, vm_limit:Number(e.target.value)}) } /> + edit? setEdit({...edit, process_limit:Number(e.target.value)}) : setForm({...form, process_limit:Number(e.target.value)}) } /> + + edit? setEdit({...edit, description:e.target.value}) : setForm({...form, description:e.target.value}) } /> +
+
+ {!edit && ()} + {edit && (<> + + + )} +
+
+ +
+
+
+ + + {items.map((it)=> ( + + + + + + + + + + ))} + +
IDuser_idproject_limitvm_limitprocess_limitenabled操作
{it.id}{it.user_id}{it.project_limit}{it.vm_limit}{it.process_limit}{String(it.enabled)} + + +
+
+ + + + ) +} + + diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..d8c6b85 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,415 @@ +// import { message } from 'antd'; // 暂时注释掉,未使用 +import { useAtom } from 'jotai'; +import { useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; + +import { authStateAtom, authLoadingAtom } from '../atoms/auth'; +import { SSOApiService } from '../services/api'; +import type { AuthState } from '../atoms/auth'; + +export const useAuth = () => { + const [authState, setAuthState] = useAtom(authStateAtom); + const [loading, setLoading] = useAtom(authLoadingAtom); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + + const refreshTimerRef = useRef(null); + + // 公共的token更新和定时器设置逻辑 + const updateTokenAndSetTimer = ( + tokenData: { + access_token: string; + refresh_token?: string; + expires_in?: number; + } + ) => { + const { access_token, refresh_token, expires_in } = tokenData; + + // 更新本地存储 + localStorage.setItem('token', access_token); + if (refresh_token) { + localStorage.setItem('refreshToken', refresh_token); + } + + // 更新状态 + const updatedState: Partial = { + token: access_token, + }; + if (refresh_token) { + updatedState.refreshToken = refresh_token; + } + setAuthState(prev => ({ + ...prev, + ...updatedState, + })); + + // 清除旧的定时器并设置新的定时器 + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + + if (expires_in) { + // 提前2分钟触发主动刷新 + refreshTimerRef.current = setTimeout( + refreshToken, + expires_in * 1000 - 2 * 60000 + ); + } + }; + + // 跳转到登录页面(若无URL则先请求再跳转) + const redirectToLogin = async () => { + console.log('🔄 [Redirect] 开始跳转登录流程'); + let loginUrl = authState.loginUrl; + console.log('🔍 [Redirect] 当前loginUrl:', loginUrl); + + if (!loginUrl) { + console.log('🚀 [Redirect] 无登录URL,获取新的登录URL'); + try { + const resp = await SSOApiService.initiateLogin(); + console.log('🚀 [Redirect] 登录URL响应:', { + success: resp?.success, + hasAuthUrl: !!resp?.auth_url, + authUrl: resp?.auth_url + }); + + if (resp?.auth_url) { + loginUrl = resp.auth_url; + setAuthState(prev => ({ ...prev, loginUrl })); + } + } catch (e) { + console.error('❌ [Redirect] 获取登录URL失败:', e); + } + } + + if (loginUrl && loginUrl.startsWith('http')) { + console.log('🔄 [Redirect] 跳转到SSO登录页面:', loginUrl); + window.location.href = loginUrl; + } else { + console.error('❌ [Redirect] 无效的登录URL:', loginUrl); + } + }; + + // token 刷新 + const refreshToken = async () => { + if (!authState.refreshToken) { + console.warn('No refresh token available'); + return; + } + + try { + const response = await SSOApiService.refreshToken(authState.refreshToken); + if (response?.access_token) { + updateTokenAndSetTimer({ + access_token: response.access_token, + refresh_token: response.refresh_token, + expires_in: response.expires_in, + }); + } + } catch (error) { + console.error('Token refresh failed:', error); + // 刷新失败,重新登录 + logout(); + } + }; + + // SSO登录 + const ssoLogin = async (code?: string, state?: string) => { + try { + console.log('🔐 [SSO] 开始SSO登录流程', { hasCode: !!code, hasState: !!state }); + setLoading(true); + + if (code && state) { + console.log('📞 [SSO] 处理SSO回调,调用 /api/sso/callback'); + // 有code和state,进行回调处理 + const response = await SSOApiService.callback(code, state); + console.log('📞 [SSO] 回调响应:', { + success: response?.success, + hasToken: !!response?.access_token, + hasUserInfo: !!response?.user_info + }); + + if (response?.access_token) { + console.log('🔑 [SSO] 获取到access_token,更新本地状态'); + updateTokenAndSetTimer({ + access_token: response.access_token, + refresh_token: response.refresh_token, + expires_in: response.expires_in, + }); + + // 获取用户信息 + if (response.user_info) { + console.log('👤 [SSO] 回调中包含用户信息,直接设置'); + setAuthState(prev => ({ + ...prev, + user: response.user_info || null, + isAuthenticated: true, + })); + localStorage.setItem('user', JSON.stringify(response.user_info)); + // 角色判断:若非 sys_admin,跳转无权限 + try { + const rolesRaw: any = (response as any)?.user_info?.roles || []; + const roleNames: string[] = Array.isArray(rolesRaw) + ? rolesRaw.map((r: any) => (typeof r === 'string' ? r : r?.name)).filter(Boolean) + : []; + const isSysAdmin = roleNames.includes('sys_admin'); + console.log('🔎 [SSO] 角色检测:', { roleNames, isSysAdmin }); + if (!isSysAdmin) { + console.warn('🚫 [Permission] 非 sys_admin,跳转 /no-permission'); + navigate('/no-permission', { replace: true }); + } + } catch (e) { + console.warn('⚠️ [SSO] 角色解析失败:', e); + } + } else { + console.log('👤 [SSO] 回调中无用户信息,调用 /api/sso/userinfo'); + await getCurrentUser(); + } + } + } else { + console.log('🚀 [SSO] 获取登录URL,调用 /api/sso/login'); + // 无code和state,获取登录URL + const response = await SSOApiService.initiateLogin(); + console.log('🚀 [SSO] 登录URL响应:', { + success: response?.success, + hasAuthUrl: !!response?.auth_url, + authUrl: response?.auth_url + }); + + if (response?.auth_url) { + setAuthState(prev => ({ + ...prev, + loginUrl: response.auth_url, + })); + } + } + } catch (error) { + console.error('❌ [SSO] SSO登录失败:', error); + throw error; + } finally { + setLoading(false); + } + }; + + // 获取当前用户信息 + const getCurrentUser = async () => { + try { + setLoading(true); + const response = await SSOApiService.userInfo(); + if (response?.user_info) { + setAuthState(prev => ({ + ...prev, + user: response.user_info, + isAuthenticated: true, + })); + localStorage.setItem('user', JSON.stringify(response.user_info)); + // 角色判断:若非 sys_admin,跳转无权限 + try { + const rolesRaw: any = (response as any)?.user_info?.roles || []; + const roleNames: string[] = Array.isArray(rolesRaw) + ? rolesRaw.map((r: any) => (typeof r === 'string' ? r : r?.name)).filter(Boolean) + : []; + const isSysAdmin = roleNames.includes('sys_admin'); + console.log('🔎 [Auth] 角色检测:', { roleNames, isSysAdmin }); + if (!isSysAdmin) { + console.warn('🚫 [Permission] 非 sys_admin,跳转 /no-permission'); + navigate('/no-permission', { replace: true }); + } + } catch (e) { + console.warn('⚠️ [Auth] 角色解析失败:', e); + } + } + } catch (error) { + console.error('Get current user failed:', error); + throw error; + } finally { + setLoading(false); + } + }; + + // 初始化认证状态 + const initAuth = async () => { + try { + console.log('🔐 [Auth] 开始初始化认证状态'); + setLoading(true); + + // 检查URL中的code参数 + const code = searchParams.get('code'); + const state = searchParams.get('state'); + console.log('🔍 [Auth] 检查URL参数:', { code: !!code, state: !!state }); + + if (code && state) { + console.log('📞 [Auth] 检测到SSO回调参数,开始处理回调'); + // 如果有code参数,进行单点登录 + try { + await ssoLogin(code, state); + setSearchParams({}); + console.log('✅ [Auth] SSO回调处理完成'); + } catch (error) { + console.error('❌ [Auth] SSO回调处理失败:', error); + } + return; + } + + // 检查本地存储的token + const token = localStorage.getItem('token'); + const refreshToken = localStorage.getItem('refreshToken'); + const userStr = localStorage.getItem('user'); + console.log('💾 [Auth] 检查本地存储:', { + hasToken: !!token, + hasRefreshToken: !!refreshToken, + hasUser: !!userStr + }); + + if (token && userStr) { + console.log('🔑 [Auth] 发现本地token,开始验证有效性'); + try { + const user = JSON.parse(userStr); + setAuthState(prev => ({ + ...prev, + user, + token, + refreshToken, + isAuthenticated: true, // 先标记为已认证,避免触发SSO登录 + })); + + // 在后台验证token是否有效(不阻塞用户使用) + getCurrentUser().then(() => { + console.log('✅ [Auth] 后台Token验证成功'); + }).catch(error => { + console.warn('⚠️ [Auth] 后台Token验证失败,但不影响用户使用:', error); + // 可以选择是否要处理验证失败的情况 + }); + + console.log('✅ [Auth] 本地token验证成功,用户已认证'); + return; + } catch (error) { + console.error('❌ [Auth] 用户数据解析失败:', error); + } + } + + // 如果没有code也没有有效token,请求登录URL + console.log('🚀 [Auth] 无有效认证信息,开始获取登录URL'); + await ssoLogin(); + } catch (error) { + console.error('❌ [Auth] 初始化认证失败:', error); + } finally { + setLoading(false); + } + }; + + // 登出 + const logout = async () => { + try { + console.log('🚪 [Logout] 开始登出流程'); + + // 调用后端登出接口获取登出URL + let logoutUrl = null; + if (authState.token) { + try { + console.log('📞 [Logout] 调用后端登出接口 /api/sso/logout'); + const res = await SSOApiService.logout(); + console.log('📞 [Logout] 后端登出响应:', { + success: res?.success, + message: res?.message + }); + + // 检查返回的message是否为有效URL + if (res?.message && res.message.startsWith('http')) { + logoutUrl = res.message; + console.log('✅ [Logout] 使用后端返回的登出URL:', logoutUrl); + } else { + console.warn('⚠️ [Logout] 后端返回的message不是有效URL:', res.message); + } + } catch (error) { + console.warn('⚠️ [Logout] 后端登出失败,但继续清理本地状态:', error); + } + } + + // 清理本地状态(必须) + console.log('🧹 [Logout] 清理本地状态'); + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('user'); + + // 清理定时器 + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + + // 使用后端返回的登出URL,如果没有则使用默认的 + if (logoutUrl) { + console.log('✅ [Logout] 登出完成,跳转到SSO登出页面'); + console.log('🔄 [Logout] 登出URL:', logoutUrl); + window.location.href = logoutUrl; + } else { + console.log('✅ [Logout] 登出完成,跳转到当前域名'); + // 如果没有有效的登出URL,跳转到当前域名让应用重新走认证流程 + window.location.href = window.location.origin; + } + } catch (error) { + console.error('❌ [Logout] 登出失败:', error); + // 即使登出失败,也要清理本地状态 + console.log('🧹 [Logout] 清理本地状态(异常情况)'); + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('user'); + + setAuthState({ + user: null, + token: null, + refreshToken: null, + isAuthenticated: false, + loginUrl: null, + }); + + // 跳转到登录页面 + console.log('🔄 [Logout] 异常情况,跳转到登录页面'); + await redirectToLogin(); + } + }; + + // 处理业务接口被动触发的token刷新和重新登录问题 + useEffect(() => { + const handleTokenRefreshed = (event: CustomEvent) => { + const { access_token, refresh_token, expires_in } = event.detail; + updateTokenAndSetTimer({ + access_token, + refresh_token, + expires_in, + }); + }; + + const handleRelogin = () => { + logout(); + }; + + window.addEventListener('auth:tokenRefreshed', handleTokenRefreshed as EventListener); + window.addEventListener('auth:relogin', handleRelogin); + + return () => { + window.removeEventListener('auth:tokenRefreshed', handleTokenRefreshed as EventListener); + window.removeEventListener('auth:relogin', handleRelogin); + }; + }, []); + + // 组件卸载时清理定时器 + useEffect(() => { + return () => { + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + } + }; + }, []); + + return { + ...authState, + loading, + initAuth, + redirectToLogin, + refreshToken, + getCurrentUser, + logout, + }; +}; diff --git a/src/hooks/usePagePermissions.ts b/src/hooks/usePagePermissions.ts new file mode 100644 index 0000000..09be869 --- /dev/null +++ b/src/hooks/usePagePermissions.ts @@ -0,0 +1,74 @@ +import { useAtom } from 'jotai'; +import { authStateAtom } from '../atoms/auth'; + +export const usePagePermissions = () => { + const [authState] = useAtom(authStateAtom); + + const hasPagePermission = (pagePath: string, action: string = 'read'): boolean => { + console.log(`🔐 [usePagePermissions] 检查页面权限 "${pagePath}:${action}"`); + console.log(`🔐 [usePagePermissions] 用户数据:`, authState.user); + + // 检查用户是否有页面数据 + if (!authState.user?.pages) { + console.log(`🔐 [usePagePermissions] 用户页面数据为空:`, authState.user); + return false; + } + + // 根据页面路径和激活状态检查权限 + const hasAccess = authState.user.pages.some(page => + page.path === pagePath && page.is_active !== false + ); + + console.log(`🔐 [usePagePermissions] 页面权限检查结果: ${hasAccess}, 用户可访问页面:`, + authState.user.pages.map(p => ({ path: p.path, isActive: p.is_active }))); + return hasAccess; + }; + + const hasPageAction = (pagePath: string, action: string): boolean => { + return hasPagePermission(pagePath, action); + }; + + const getPageActions = (pagePath: string): string[] => { + console.log(`🔐 [usePagePermissions] 获取页面操作权限 "${pagePath}"`); + + if (!authState.user?.pages) { + console.log(`🔐 [usePagePermissions] 用户页面数据为空`); + return []; + } + + // 检查用户是否有该页面的访问权限 + const hasAccess = authState.user.pages.some(page => page.path === pagePath); + if (!hasAccess) { + console.log(`🔐 [usePagePermissions] 用户无该页面访问权限`); + return []; + } + + // 简化版本:如果有页面访问权限,返回基本操作 + return ['read', 'create', 'update', 'delete']; + }; + + const getAccessiblePages = (): string[] => { + console.log(`🔐 [usePagePermissions] 获取用户可访问页面`); + console.log(`🔐 [usePagePermissions] 用户数据:`, authState.user); + + if (!authState.user?.pages) { + console.log(`🔐 [usePagePermissions] 用户页面数据为空`); + return []; + } + + // 只返回激活状态的页面路径 + const pages = authState.user.pages + .filter(page => page.is_active !== false) + .map(page => page.path); + console.log(`🔐 [usePagePermissions] 用户可访问页面:`, pages); + + return pages; + }; + + return { + hasPagePermission, + hasPageAction, + getPageActions, + getAccessiblePages + }; +}; diff --git a/src/hooks/usePermissions.ts b/src/hooks/usePermissions.ts new file mode 100644 index 0000000..4a125b2 --- /dev/null +++ b/src/hooks/usePermissions.ts @@ -0,0 +1,46 @@ +import { useAtom } from 'jotai'; +import { authStateAtom } from '../atoms/auth'; + +export const usePermissions = () => { + const [authState] = useAtom(authStateAtom); + + const hasPermission = (permission: string): boolean => { + if (!authState.user?.permissions) { + console.log(`🔐 [usePermissions] 用户权限数据为空:`, authState.user); + return false; + } + const hasAccess = authState.user.permissions.includes(permission); + console.log(`🔐 [usePermissions] 检查权限 "${permission}": ${hasAccess}, 用户权限:`, authState.user.permissions); + return hasAccess; + }; + + const hasAnyPermission = (permissions: string[]): boolean => { + return permissions.some(permission => hasPermission(permission)); + }; + + const hasAllPermissions = (permissions: string[]): boolean => { + return permissions.every(permission => hasPermission(permission)); + }; + + const hasRole = (roleName: string): boolean => { + if (!authState.user?.roles) return false; + return authState.user.roles.includes(roleName); + }; + + const hasAnyRole = (roleNames: string[]): boolean => { + return roleNames.some(roleName => hasRole(roleName)); + }; + + const hasAllRoles = (roleNames: string[]): boolean => { + return roleNames.every(roleName => hasRole(roleName)); + }; + + return { + hasPermission, + hasAnyPermission, + hasAllPermissions, + hasRole, + hasAnyRole, + hasAllRoles + }; +}; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..f20dad5 --- /dev/null +++ b/src/index.css @@ -0,0 +1 @@ +/* 基础样式重置 - 不干扰 App.css 的设计 */ diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..828b4b0 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,28 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { Provider } from 'jotai' +import { BrowserRouter } from 'react-router-dom' +import './App.css' +import 'antd/dist/reset.css' +import App from './App.tsx' +import AuthGuard from './components/AuthGuard' +import { migrateStorage } from './utils/storageMigration' + +// 执行存储迁移 +migrateStorage(); + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +const root = createRoot(rootElement) +root.render( + + + + + + + + + +) diff --git a/src/pages/AuditLogs.tsx b/src/pages/AuditLogs.tsx new file mode 100644 index 0000000..de4bef3 --- /dev/null +++ b/src/pages/AuditLogs.tsx @@ -0,0 +1,315 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Card, Table, Tag, Form, Select, Input, DatePicker, Button, Space, Typography, Tooltip, message, Row, Col } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import dayjs, { Dayjs } from 'dayjs'; +import { AuditLogApiService } from '../services/auditLogApi'; +import type { AuditLog, AuditLogListRequest } from '../types/auditLog'; + +const { RangePicker } = DatePicker; +const { Text } = Typography; + +const OPERATION_TYPE_LABELS: Record = { + balance_adjustment: '余额调整', + user_level_change: '用户等级修改', + user_status_change: '用户状态变更', + model_price_change: '模型价格修改', + permission_change: '权限变更', + config_change: '配置修改', +}; + +const STATUS_TAG_PROPS: Record = { + success: { color: 'green', text: '成功' }, + failed: { color: 'red', text: '失败' }, +}; + +interface FilterValues { + operation_type?: string; + operator_email?: string; + target_email?: string; + time_range?: [Dayjs, Dayjs]; +} + +const formatKey = (key: string) => key.replace(/_/g, ' '); + +const renderDetailValue = (value: any): React.ReactNode => { + if (value === null || value === undefined) { + return '-'; + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + if (typeof value === 'number') { + return value.toLocaleString(); + } + return String(value); +}; + +const renderOperationDetails = (details?: Record | null) => { + if (!details || Object.keys(details).length === 0) { + return -; + } + + return ( +
+ {Object.entries(details).map(([key, value]) => ( +
+ {formatKey(key)}: {renderDetailValue(value)} +
+ ))} +
+ ); +}; + +const AuditLogs: React.FC = () => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [logs, setLogs] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + const [filters, setFilters] = useState({}); + + const fetchLogs = useCallback(async () => { + setLoading(true); + try { + const params: AuditLogListRequest = { + ...filters, + page, + size: pageSize, + }; + const data = await AuditLogApiService.getAuditLogs(params); + setLogs(data.list ?? []); + setTotal(data.total ?? 0); + if (data.page) setPage(data.page); + if (data.size) setPageSize(data.size); + } catch (error: any) { + console.error('获取审计日志失败', error); + message.error(error?.response?.data?.message || '获取审计日志失败'); + } finally { + setLoading(false); + } + }, [filters, page, pageSize]); + + useEffect(() => { + fetchLogs(); + }, [fetchLogs]); + + const handleSearch = useCallback(() => { + form + .validateFields() + .then(values => { + const nextFilters: AuditLogListRequest = {}; + if (values.operation_type) { + nextFilters.operation_type = values.operation_type; + } + if (values.operator_email) { + nextFilters.operator_email = values.operator_email.trim(); + } + if (values.target_email) { + nextFilters.target_email = values.target_email.trim(); + } + if (values.time_range && values.time_range.length === 2) { + nextFilters.start_time = values.time_range[0].format('YYYY-MM-DD HH:mm:ss'); + nextFilters.end_time = values.time_range[1].format('YYYY-MM-DD HH:mm:ss'); + } + setPage(1); + setFilters(nextFilters); + }) + .catch(() => { + /* ignore validation errors */ + }); + }, [form]); + + const handleReset = useCallback(() => { + form.resetFields(); + setPage(1); + setPageSize(20); + setFilters({}); + }, [form]); + + const columns: ColumnsType = useMemo(() => [ + { + title: '操作时间', + dataIndex: 'operation_time', + key: 'operation_time', + width: 180, + render: (value: string) => { + const formatted = value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '-'; + return {formatted}; + }, + sorter: false, + }, + { + title: '操作类型', + dataIndex: 'operation_type', + key: 'operation_type', + width: 140, + render: (value: string) => { + const label = OPERATION_TYPE_LABELS[value] || value || '-'; + return {label}; + }, + }, + { + title: '操作对象', + dataIndex: 'target_email', + key: 'target_email', + width: 200, + render: (value: string | null, record) => ( +
+
{value || '-'}
+ {record.target_type && ( + + {record.target_type}{record.target_id ? ` #${record.target_id}` : ''} + + )} +
+ ), + }, + { + title: '操作详情', + dataIndex: 'operation_details', + key: 'operation_details', + width: 260, + render: (details: Record | null) => renderOperationDetails(details), + }, + { + title: '操作人', + dataIndex: 'operator_email', + key: 'operator_email', + width: 200, + render: (value: string, record) => ( +
+
{value || '-'}
+ ID: {record.operator_id ?? '-'} +
+ ), + }, + { + title: 'IP / UA', + dataIndex: 'ip_address', + key: 'ip_address', + width: 220, + render: (value: string | null, record) => ( +
+
{value || '-'}
+ {record.user_agent && ( + + + {record.user_agent} + + + )} +
+ ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (value: string | null, record) => { + const statusKey = value ? value.toLowerCase() : 'success'; + const tagProps = STATUS_TAG_PROPS[statusKey] || { color: 'default', text: value || '未知' }; + return ( + + {tagProps.text} + {record.error_message && ( + + + {record.error_message} + + + )} + + ); + }, + }, + ], []); + + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + rowKey="id" + loading={loading} + columns={columns} + dataSource={logs} + pagination={{ + current: page, + pageSize, + total, + showSizeChanger: true, + showTotal: totalValue => `共 ${totalValue} 条记录`, + onChange: (nextPage, nextSize) => { + setPage(nextPage); + setPageSize(nextSize || 20); + }, + }} + scroll={{ x: 1200 }} + /> + +
+ ); +}; + +export default AuditLogs; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..143f5ef --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +const Dashboard: React.FC = () => { + return ( +
+

系统总览

+
系统整体运行状态、关键指标概览
+ +
+
+

总用户数

+
1,234
+
+
+

今日活跃用户

+
567
+
+
+

总Token消耗

+
12.5M
+
+
+

系统状态

+
正常
+
+
+ +
+
+

最近7天用户活跃度

+
用户活跃度趋势图占位
+
+
+

热门模型使用排行

+
模型使用量排行占位
+
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/pages/Finance.tsx b/src/pages/Finance.tsx new file mode 100644 index 0000000..0401a7d --- /dev/null +++ b/src/pages/Finance.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +const Finance: React.FC = () => { + return ( +
+

财务管理

+
收入统计、成本分析、财务报表
+ +
+
+

本月收入

+
¥123,456
+
+
+

本月成本

+
¥78,901
+
+
+

净利润

+
¥44,555
+
+
+

利润率

+
36.1%
+
+
+ +
+
+

收入趋势

+
收入变化趋势图占位
+
+
+

成本分析

+
成本构成饼图占位
+
+
+ +
+

财务报表

+
详细财务报表占位
+
+
+ ); +}; + +export default Finance; diff --git a/src/pages/FinanceMcpAccountRechargeRecords.tsx b/src/pages/FinanceMcpAccountRechargeRecords.tsx new file mode 100644 index 0000000..9f81c97 --- /dev/null +++ b/src/pages/FinanceMcpAccountRechargeRecords.tsx @@ -0,0 +1,544 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space, Select, Modal, InputNumber, message, Popconfirm, Tabs, Drawer } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; +import dayjs from 'dayjs'; + +const { TabPane } = Tabs; + +const FinanceMcpAccountRechargeRecords: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [modalVisible, setModalVisible] = useState(false); + const [modalForm] = Form.useForm(); + const [providerAccounts, setProviderAccounts] = useState([]); + const [loadingAccounts, setLoadingAccounts] = useState(false); + const [activeTab, setActiveTab] = useState('recharge-records'); + const [balances, setBalances] = useState([]); + const [loadingBalances, setLoadingBalances] = useState(false); + const [balanceHistoryVisible, setBalanceHistoryVisible] = useState(false); + const [balanceHistory, setBalanceHistory] = useState([]); + const [loadingHistory, setLoadingHistory] = useState(false); + const [balanceModalVisible, setBalanceModalVisible] = useState(false); + const [balanceModalMode, setBalanceModalMode] = useState<'create' | 'edit'>('create'); + const [editingBalanceRecord, setEditingBalanceRecord] = useState(null); + const [balanceForm] = Form.useForm(); + const [updatingBalance, setUpdatingBalance] = useState(false); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { + page: p, + size: s, + provider: vals.provider, + account: vals.account, + start, + end, + } as any; + }; + + const fetchList = useCallback(async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listMcpAccountRechargeRecords(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取数据失败'); + } finally { + setLoading(false); + } + }, [form, message, page, size]); + + const fetchProviderAccounts = useCallback(async () => { + setLoadingAccounts(true); + try { + const res = await FinanceApiService.getMcpProviderAccounts({ status: 'active', is_used: true }); + const accounts = res?.data || res || []; + setProviderAccounts(accounts); + } catch (e: any) { + message.error('获取账号列表失败'); + } finally { + setLoadingAccounts(false); + } + }, []); + + const fetchBalances = useCallback(async () => { + setLoadingBalances(true); + try { + const res = await FinanceApiService.getMcpAccountBalances(); + const balanceList = res?.data || res || []; + setBalances(balanceList); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取余额数据失败'); + } finally { + setLoadingBalances(false); + } + }, []); + + const fetchBalanceHistory = useCallback(async (providerId: string) => { + setLoadingHistory(true); + try { + const res = await FinanceApiService.getMcpAccountBalanceHistory(providerId); + const history = res?.data || res || []; + setBalanceHistory(history); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取余额历史失败'); + } finally { + setLoadingHistory(false); + } + }, []); + + useEffect(() => { + fetchProviderAccounts(); + }, [fetchProviderAccounts]); + + useEffect(() => { + if (activeTab === 'recharge-records') { + fetchList(); + } else if (activeTab === 'balances') { + fetchBalances(); + } + }, [activeTab, fetchBalances, fetchList]); + + useEffect(() => { + if (activeTab === 'recharge-records') { + fetchList(page, size); + } + }, [activeTab, page, size, fetchList]); + + const handleAdd = () => { + modalForm.resetFields(); + modalForm.setFieldsValue({ + recharge_date: dayjs(), + }); + setModalVisible(true); + }; + + const handleDelete = async (id: string) => { + try { + await FinanceApiService.deleteMcpAccountRechargeRecord(id); + message.success('删除成功'); + fetchList(page, size); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '删除失败'); + } + }; + + const handleModalOk = async () => { + try { + const values = await modalForm.validateFields(); + const payload = { + provider_id: values.provider_id, + amount: values.amount, + recharge_date: values.recharge_date.format('YYYY-MM-DD'), + remark: values.remark || '', + }; + + await FinanceApiService.createMcpAccountRechargeRecord(payload); + message.success('创建成功'); + setModalVisible(false); + fetchList(page, size); + if (activeTab === 'balances') { + fetchBalances(); + } + } catch (e: any) { + if (e?.errorFields) { + // 表单验证错误,不显示错误消息 + return; + } + message.error(e?.response?.data?.message || e?.message || '操作失败'); + } + }; + + const handleModalCancel = () => { + setModalVisible(false); + modalForm.resetFields(); + }; + + const columns: ColumnsType = useMemo(() => [ + { title: 'ID', dataIndex: 'id', key: 'id', width: 100, ellipsis: true }, + { title: '服务商', dataIndex: 'provider', key: 'provider', width: 150 }, + { title: '账号', dataIndex: 'account', key: 'account', width: 200 }, + { + title: '充值金额', + dataIndex: 'amount', + key: 'amount', + width: 120, + render: (amount: number, record: any) => { + if (amount === null || amount === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${amount.toFixed(2)} ${currency}`; + }, + }, + { + title: '充值日期', + dataIndex: 'recharge_date', + key: 'recharge_date', + width: 120, + }, + { title: '操作人', dataIndex: 'operator_name', key: 'operator_name', width: 120 }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 200, ellipsis: true }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 }, + { + title: '操作', + key: 'actions', + fixed: 'right', + width: 120, + render: (_: any, record: any) => ( + handleDelete(record.id)} + okText="确认" + cancelText="取消" + > + + + ), + }, + ], []); + + const accountOptions = useMemo(() => { + return providerAccounts.map((acc: any) => { + const label = acc.account ? `${acc.provider} - ${acc.account}` : acc.provider; + return { + label, + value: acc.id, + provider: acc.provider, + account: acc.account, + }; + }); + }, [providerAccounts]); + + const handleViewBalanceHistory = useCallback((providerId: string) => { + setBalanceHistoryVisible(true); + fetchBalanceHistory(providerId); + }, [fetchBalanceHistory]); + + const balanceColumns: ColumnsType = useMemo(() => [ + { title: '服务商', dataIndex: 'provider', key: 'provider', width: 150 }, + { title: '账号', dataIndex: 'account', key: 'account', width: 200 }, + { + title: '当前余额', + dataIndex: 'balance', + key: 'balance', + width: 150, + render: (balance: number, record: any) => { + if (balance === null || balance === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${balance.toFixed(2)} ${currency}`; + }, + }, + { + title: '更新时间', + dataIndex: 'created_at', + key: 'created_at', + width: 180, + }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 220, ellipsis: true }, + { + title: '操作', + key: 'actions', + fixed: 'right', + width: 200, + render: (_: any, record: any) => ( + + + + + ), + }, + ], [balanceForm, handleViewBalanceHistory]); + + const balanceHistoryColumns: ColumnsType = useMemo(() => [ + { + title: '余额', + dataIndex: 'balance', + key: 'balance', + width: 150, + render: (balance: number, record: any) => { + if (balance === null || balance === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${balance.toFixed(2)} ${currency}`; + }, + }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 220, ellipsis: true }, + { title: '更新时间', dataIndex: 'created_at', key: 'created_at', width: 180 }, + ], []); + + return ( + <> + MCP账号管理} + > + + + +
fetchList(1, size)} layout="inline"> + + + + + + + + + + + + + + + +
+ +
+ r.id} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ + current: page, + pageSize: size, + total, + onChange: (p, s) => { + setPage(p); + setSize(s); + fetchList(p, s); + }, + }} + scroll={{ x: 1400 }} + /> + + + + + + + +
r.provider_id || r.id} + columns={balanceColumns} + dataSource={balances} + loading={loadingBalances} + scroll={{ x: 1000 }} + /> + + + + + { + setBalanceHistoryVisible(false); + setBalanceHistory([]); + }} + > +
r.id} + columns={balanceHistoryColumns} + dataSource={balanceHistory} + loading={loadingHistory} + pagination={false} + scroll={{ x: 800 }} + /> + + + +
+ + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + options={accountOptions} + disabled={balanceModalMode === 'edit'} + /> + + + + + + + + +
+ + ); +}; + +export default FinanceMcpAccountRechargeRecords; + diff --git a/src/pages/FinanceMcpUsage.tsx b/src/pages/FinanceMcpUsage.tsx new file mode 100644 index 0000000..512684e --- /dev/null +++ b/src/pages/FinanceMcpUsage.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; + +const FinanceMcpUsage: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { page: p, size: s, user_id: vals.user_id, project: vals.project, start, end } as any; + }; + + const fetchList = async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listMcpUsages(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchList(); }, []); + + const columns: ColumnsType = useMemo(() => ([ + { title: '用户id', dataIndex: 'user_id', key: 'user_id' }, + { title: 'API分组', dataIndex: 'api_group', key: 'api_group' }, + { title: '对话id', dataIndex: 'project_id', key: 'project_id' }, + { title: '日期', dataIndex: 'day', key: 'day' }, + { title: '小时', dataIndex: 'hour', key: 'hour' }, + { title: '服务商', dataIndex: 'provider', key: 'provider' }, + { title: '账号', dataIndex: 'account', key: 'account' }, + { title: '模型', dataIndex: 'model', key: 'model' }, + { title: '代理类型', dataIndex: 'agent_type', key: 'agent_type' }, + { title: '任务id', dataIndex: 'chat_id', key: 'chat_id' }, + { title: '花费金额', dataIndex: 'cost', key: 'cost' }, + { title: '调用次数', dataIndex: 'call_count', key: 'call_count' }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at' }, + { title: '修改时间', dataIndex: 'updated_at', key: 'updated_at' }, + ]), []); + + return ( + MCP 调用费用}> +
fetchList(1, size)} style={{ marginBottom: 12 }}> + + + + + + + + + + + + + + + + +
r.id || `${r.user_id}-${r.day}-${r.hour}-${r.model}-${r.account}`} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ current: page, pageSize: size, total, onChange: (p, s) => { setPage(p); setSize(s); fetchList(p, s); } }} + scroll={{ x: 1000 }} + /> + + ); +}; + +export default FinanceMcpUsage; + + diff --git a/src/pages/FinanceModelAccountRechargeRecords.tsx b/src/pages/FinanceModelAccountRechargeRecords.tsx new file mode 100644 index 0000000..7012ea0 --- /dev/null +++ b/src/pages/FinanceModelAccountRechargeRecords.tsx @@ -0,0 +1,567 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space, Select, Modal, InputNumber, message, Popconfirm, Tabs, Drawer, Tooltip } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; +import dayjs from 'dayjs'; + +const { TabPane } = Tabs; + +const FinanceModelAccountRechargeRecords: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [modalVisible, setModalVisible] = useState(false); + const [modalForm] = Form.useForm(); + const [modelAccounts, setModelAccounts] = useState([]); + const [loadingAccounts, setLoadingAccounts] = useState(false); + const [activeTab, setActiveTab] = useState('recharge-records'); + const [balances, setBalances] = useState([]); + const [loadingBalances, setLoadingBalances] = useState(false); + const [balanceHistoryVisible, setBalanceHistoryVisible] = useState(false); + const [balanceHistory, setBalanceHistory] = useState([]); + const [loadingHistory, setLoadingHistory] = useState(false); + const [balanceModalVisible, setBalanceModalVisible] = useState(false); + const [balanceModalMode, setBalanceModalMode] = useState<'create' | 'edit'>('create'); + const [editingBalanceRecord, setEditingBalanceRecord] = useState(null); + const [balanceForm] = Form.useForm(); + const [updatingBalance, setUpdatingBalance] = useState(false); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { + page: p, + size: s, + provider: vals.provider, + account: vals.account, + start, + end, + } as any; + }; + + const fetchList = useCallback(async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listModelAccountRechargeRecords(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取数据失败'); + } finally { + setLoading(false); + } + }, [form, page, size]); + + const fetchModelAccounts = useCallback(async () => { + setLoadingAccounts(true); + try { + const res = await FinanceApiService.getModelConfigAccounts({ enabled: true }); + const accounts = res?.data || res || []; + setModelAccounts(accounts); + } catch (e: any) { + message.error('获取模型账号列表失败'); + } finally { + setLoadingAccounts(false); + } + }, []); + + const fetchBalances = useCallback(async () => { + setLoadingBalances(true); + try { + const res = await FinanceApiService.getModelAccountBalances(); + const balanceList = res?.data || res || []; + setBalances(balanceList); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取余额数据失败'); + } finally { + setLoadingBalances(false); + } + }, []); + + const fetchBalanceHistory = useCallback(async (account: string) => { + setLoadingHistory(true); + try { + const res = await FinanceApiService.getModelAccountBalanceHistory(account); + const history = res?.data || res || []; + setBalanceHistory(history); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '获取余额历史失败'); + } finally { + setLoadingHistory(false); + } + }, []); + + useEffect(() => { + fetchModelAccounts(); + }, [fetchModelAccounts]); + + useEffect(() => { + if (activeTab === 'recharge-records') { + fetchList(); + } else if (activeTab === 'balances') { + fetchBalances(); + } + }, [activeTab, fetchBalances, fetchList]); + + useEffect(() => { + if (activeTab === 'recharge-records') { + fetchList(page, size); + } + }, [activeTab, page, size, fetchList]); + + const handleAdd = () => { + modalForm.resetFields(); + modalForm.setFieldsValue({ + recharge_date: dayjs(), + }); + setModalVisible(true); + }; + + const handleDelete = async (id: string) => { + try { + await FinanceApiService.deleteModelAccountRechargeRecord(id); + message.success('删除成功'); + fetchList(page, size); + } catch (e: any) { + message.error(e?.response?.data?.message || e?.message || '删除失败'); + } + }; + + const handleModalOk = async () => { + try { + const values = await modalForm.validateFields(); + const payload = { + account: values.account, + amount: values.amount, + recharge_date: values.recharge_date.format('YYYY-MM-DD'), + remark: values.remark || '', + }; + + await FinanceApiService.createModelAccountRechargeRecord(payload); + message.success('创建成功'); + setModalVisible(false); + fetchList(page, size); + if (activeTab === 'balances') { + fetchBalances(); + } + } catch (e: any) { + if (e?.errorFields) { + return; + } + message.error(e?.response?.data?.message || e?.message || '操作失败'); + } + }; + + const handleModalCancel = () => { + setModalVisible(false); + modalForm.resetFields(); + }; + + const columns: ColumnsType = useMemo(() => [ + { title: 'ID', dataIndex: 'id', key: 'id', width: 100, ellipsis: true }, + { title: '账号名称', dataIndex: 'provider', key: 'provider', width: 180 }, + { title: '账号', dataIndex: 'account', key: 'account', width: 200 }, + { + title: '充值金额', + dataIndex: 'amount', + key: 'amount', + width: 120, + render: (amount: number, record: any) => { + if (amount === null || amount === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${amount.toFixed(2)} ${currency}`; + }, + }, + { + title: '充值日期', + dataIndex: 'recharge_date', + key: 'recharge_date', + width: 120, + }, + { title: '操作人', dataIndex: 'operator_name', key: 'operator_name', width: 120 }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 200, ellipsis: true }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 }, + { + title: '操作', + key: 'actions', + fixed: 'right', + width: 120, + render: (_: any, record: any) => ( + handleDelete(record.id)} + okText="确认" + cancelText="取消" + > + + + ), + }, + ], []); + + const accountOptions = useMemo(() => { + return modelAccounts.map((acc: any) => { + const name = acc.name || acc.provider || acc.account || '未知账号'; + const apiType = acc.api_type ? ` (${acc.api_type})` : ''; + const label = `${name}${apiType} - ${acc.account || ''}`; + return { + label, + value: acc.account, + name: acc.name, + api_type: acc.api_type, + account: acc.account, + }; + }); + }, [modelAccounts]); + + const handleViewBalanceHistory = useCallback((account: string) => { + setBalanceHistoryVisible(true); + fetchBalanceHistory(account); + }, [fetchBalanceHistory]); + + const balanceColumns: ColumnsType = useMemo(() => [ + { title: '账号名称', dataIndex: 'provider', key: 'provider', width: 180 }, + { title: '账号', dataIndex: 'account', key: 'account', width: 180 }, + { + title: '当前余额', + dataIndex: 'balance', + key: 'balance', + width: 150, + render: (balance: number, record: any) => { + if (balance === null || balance === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${balance.toFixed(2)} ${currency}`; + }, + }, + { + title: '更新时间', + dataIndex: 'created_at', + key: 'created_at', + width: 180, + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + width: 220, + ellipsis: true, + render: (text: string) => ( + + {text || '-'} + + ), + }, + { + title: '操作', + key: 'actions', + fixed: 'right', + width: 160, + render: (_: any, record: any) => ( + + + + + ), + }, + ], [balanceForm, handleViewBalanceHistory]); + + const balanceHistoryColumns: ColumnsType = useMemo(() => [ + { + title: '余额', + dataIndex: 'balance', + key: 'balance', + width: 150, + render: (balance: number, record: any) => { + if (balance === null || balance === undefined) return '-'; + const currency = record.currency || 'USD'; + return `$${balance.toFixed(2)} ${currency}`; + }, + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + width: 220, + ellipsis: true, + render: (text: string) => ( + + {text || '-'} + + ), + }, + { title: '更新时间', dataIndex: 'created_at', key: 'created_at', width: 180 }, + ], []); + + return ( + <> + 模型账号管理} + > + + + +
fetchList(1, size)} layout="inline"> + + + + + + + + + + + + + + + + + +
+
r.id} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ + current: page, + pageSize: size, + total, + onChange: (p, s) => { + setPage(p); + setSize(s); + fetchList(p, s); + }, + }} + scroll={{ x: 1400 }} + /> + + + + + + + +
r.account || r.id} + columns={balanceColumns} + dataSource={balances} + loading={loadingBalances} + scroll={{ x: 1000 }} + /> + + + + + { + setBalanceHistoryVisible(false); + setBalanceHistory([]); + }} + > +
r.id} + columns={balanceHistoryColumns} + dataSource={balanceHistory} + loading={loadingHistory} + pagination={false} + scroll={{ x: 800 }} + /> + + + +
+ + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + options={accountOptions} + disabled={balanceModalMode === 'edit'} + /> + + + + + + + + +
+ + ); +}; + +export default FinanceModelAccountRechargeRecords; + diff --git a/src/pages/FinancePaymentRecords.tsx b/src/pages/FinancePaymentRecords.tsx new file mode 100644 index 0000000..8fc125b --- /dev/null +++ b/src/pages/FinancePaymentRecords.tsx @@ -0,0 +1,165 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space, Select, Popconfirm, message } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; + +const FinancePaymentRecords: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { + page: p, + size: s, + user_id: vals.user_id, + order_id: vals.order_id, + paypal_order_id: vals.paypal_order_id, + status: vals.status, + refund_status: vals.refund_status, + payer_email: vals.payer_email, + start, + end, + } as any; + }; + + const fetchList = async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listPaymentRecords(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchList(); }, []); + + const columns: ColumnsType = useMemo(() => ([ + { title: 'ID', dataIndex: 'id', key: 'id', width: 80 }, + { title: '用户ID', dataIndex: 'user_id', key: 'user_id', width: 120 }, + { title: '订单ID', dataIndex: 'order_id', key: 'order_id', width: 150 }, + { title: 'PayPal订单ID', dataIndex: 'paypal_order_id', key: 'paypal_order_id', width: 180 }, + { title: 'PayPal捕获ID', dataIndex: 'paypal_capture_id', key: 'paypal_capture_id', width: 180 }, + { title: '状态', dataIndex: 'status', key: 'status', width: 100 }, + { + title: '支付金额', + dataIndex: 'amount', + key: 'amount', + width: 120, + render: (amount: number, record: any) => { + if (amount === null || amount === undefined) return '-'; + const currency = record.currency || 'USD'; + // amount 是最小货币单位,如美分,需要转换为实际金额 + const actualAmount = amount / 100; + return `${actualAmount.toFixed(2)} ${currency}`; + } + }, + { title: '货币', dataIndex: 'currency', key: 'currency', width: 80 }, + { title: '付款人邮箱', dataIndex: 'payer_email', key: 'payer_email', width: 200 }, + { title: '付款人姓名', dataIndex: 'payer_name', key: 'payer_name', width: 150 }, + { title: '付款人ID', dataIndex: 'payer_id', key: 'payer_id', width: 150 }, + { title: '退款状态', dataIndex: 'refund_status', key: 'refund_status', width: 100 }, + { + title: '已退款金额', + dataIndex: 'refunded_amount', + key: 'refunded_amount', + width: 120, + render: (amount: number, record: any) => { + if (amount === null || amount === undefined || amount === 0) return '-'; + const currency = record.currency || 'USD'; + const actualAmount = amount / 100; + return `${actualAmount.toFixed(2)} ${currency}`; + } + }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 }, + { title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 180 }, + { + title: '操作', key: 'actions', fixed: 'right', width: 140, + render: (_: any, record: any) => { + const canRefund = record?.status === 'COMPLETED' && record?.refund_status !== 'full'; + if (!canRefund) return null; + const onRefund = async () => { + try { + message.loading({ content: '正在发起退款...', key: 'refund' }); + await FinanceApiService.refundPaymentRecord({ order_id: record.order_id, paypal_capture_id: record.paypal_capture_id }); + message.success({ content: '退款已提交', key: 'refund', duration: 2 }); + fetchList(page, size); + } catch (e: any) { + const errorMsg = e?.response?.data?.message || e?.message || '退款失败'; + message.error({ content: errorMsg, key: 'refund' }); + } + }; + return ( + + + + ); + } + } + ]), []); + + return ( + 充值记录}> +
fetchList(1, size)} style={{ marginBottom: 12 }}> + + + + + + + + + + + + + + + + + + + + + + + + + +
r.id} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ current: page, pageSize: size, total, onChange: (p, s) => { setPage(p); setSize(s); fetchList(p, s); } }} + scroll={{ x: 1800 }} + /> + + ); +}; + +export default FinancePaymentRecords; + diff --git a/src/pages/FinanceSandboxCosts.tsx b/src/pages/FinanceSandboxCosts.tsx new file mode 100644 index 0000000..051f232 --- /dev/null +++ b/src/pages/FinanceSandboxCosts.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; + +const FinanceSandboxCosts: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { page: p, size: s, user_id: vals.user_id, project: vals.project, start, end } as any; + }; + + const fetchList = async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listSandboxRecords(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchList(); }, []); + + const columns: ColumnsType = useMemo(() => ([ + { title: '用户id', dataIndex: 'user_id', key: 'user_id' }, + { title: '对话id', dataIndex: 'project_id', key: 'project_id' }, + { title: '任务id', dataIndex: 'chat_id', key: 'chat_id' }, + { title: '花费额度', dataIndex: 'total_cost_balance', key: 'total_cost_balance' }, + { title: '花费金额', dataIndex: 'total_cost_usd', key: 'total_cost_usd' }, + { title: '支付状态', dataIndex: 'billing_status', key: 'billing_status' }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at' }, + { title: '修改时间', dataIndex: 'updated_at', key: 'updated_at' }, + ]), []); + + return ( + 沙箱费用}> +
fetchList(1, size)} style={{ marginBottom: 12 }}> + + + + + + + + + + + + + + + + +
r.sandbox_id || `${r.sandbox_id}-${r.created_at}`} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ current: page, pageSize: size, total, onChange: (p, s) => { setPage(p); setSize(s); fetchList(p, s); } }} + scroll={{ x: 1200 }} + /> + + ); +}; + +export default FinanceSandboxCosts; + + diff --git a/src/pages/FinanceTokenUsage.tsx b/src/pages/FinanceTokenUsage.tsx new file mode 100644 index 0000000..1a1c97e --- /dev/null +++ b/src/pages/FinanceTokenUsage.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; + +const FinanceTokenUsage: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { page: p, size: s, user_id: vals.user_id, project: vals.project, start, end } as any; + }; + + const fetchList = async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listTokenUsages(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchList(); }, []); + + const columns: ColumnsType = useMemo(() => ([ + { title: '用户id', dataIndex: 'user_id', key: 'user_id' }, + { title: 'API分组', dataIndex: 'api_group', key: 'api_group' }, + { title: '对话id', dataIndex: 'project_id', key: 'project_id' }, + { title: '日期', dataIndex: 'day', key: 'day' }, + { title: '小时', dataIndex: 'hour', key: 'hour' }, + { title: '服务商', dataIndex: 'provider', key: 'provider' }, + { title: '账号', dataIndex: 'account', key: 'account' }, + { title: '模型', dataIndex: 'model', key: 'model' }, + { title: '代理类型', dataIndex: 'agent_type', key: 'agent_type' }, + { title: '任务id', dataIndex: 'chat_id', key: 'chat_id' }, + { title: '花费金额(总)', dataIndex: 'cost', key: 'cost' }, + { title: '提示Tokens', dataIndex: 'prompt_token', key: 'prompt_token' }, + { title: '输出Tokens', dataIndex: 'completion_token', key: 'completion_token' }, + { title: '缓存创建Tokens', dataIndex: 'cache_create_token', key: 'cache_create_token' }, + { title: '缓存命中Tokens', dataIndex: 'cache_read_token', key: 'cache_read_token' }, + { title: '提示花费', dataIndex: 'prompt_cost', key: 'prompt_cost' }, + { title: '输出花费', dataIndex: 'completion_cost', key: 'completion_cost' }, + { title: '缓存创建花费', dataIndex: 'cache_create_cost', key: 'cache_create_cost' }, + { title: '缓存命花费', dataIndex: 'cache_read_cost', key: 'cache_read_cost' }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at' }, + { title: '修改时间', dataIndex: 'updated_at', key: 'updated_at' }, + ]), []); + + return ( + Token 费用}> +
fetchList(1, size)} style={{ marginBottom: 12 }}> + + + + + + + + + + + + + + + + +
r.id || `${r.user_id}-${r.day}-${r.hour}-${r.model}-${r.account}`} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ current: page, pageSize: size, total, onChange: (p, s) => { setPage(p); setSize(s); fetchList(p, s); } }} + scroll={{ x: 1200 }} + /> + + ); +}; + +export default FinanceTokenUsage; + + diff --git a/src/pages/FinanceTransactionLogs.tsx b/src/pages/FinanceTransactionLogs.tsx new file mode 100644 index 0000000..26bd32d --- /dev/null +++ b/src/pages/FinanceTransactionLogs.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Card, Table, Typography, Form, Input, DatePicker, Button, Space, Select } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { FinanceApiService } from '../services/api'; + +const FinanceTransactionLogs: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(20); + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + + const buildParams = (p = page, s = size) => { + const vals = form.getFieldsValue(); + const range = vals.range as any[] | undefined; + const start = range?.[0]?.format?.('YYYY-MM-DD') || undefined; + const end = range?.[1]?.format?.('YYYY-MM-DD') || undefined; + return { + page: p, + size: s, + user_id: vals.user_id, + order_id: vals.order_id, + type: vals.type, + status: vals.status, + start, + end, + } as any; + }; + + const fetchList = async (p = page, s = size) => { + setLoading(true); + try { + const res = await FinanceApiService.listTransactionLogs(buildParams(p, s)); + setData(res?.data || res?.list || []); + setTotal(res?.total || 0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchList(); }, []); + + const columns: ColumnsType = useMemo(() => ([ + { title: '流水ID', dataIndex: 'transaction_id', key: 'transaction_id' }, + { title: '用户id', dataIndex: 'user_id', key: 'user_id' }, + { title: '订单id', dataIndex: 'order_id', key: 'order_id' }, + { title: '流水类型', dataIndex: 'type', key: 'type' }, + { title: '金额(最小单位)', dataIndex: 'amount', key: 'amount' }, + { title: '货币', dataIndex: 'currency', key: 'currency' }, + { title: '操作前余额', dataIndex: 'balance_before', key: 'balance_before' }, + { title: '操作后余额', dataIndex: 'balance_after', key: 'balance_after' }, + { title: '状态', dataIndex: 'status', key: 'status' }, + { title: '描述', dataIndex: 'description', key: 'description' }, + { title: 'PayPal交易ID', dataIndex: 'paypal_transaction_id', key: 'paypal_transaction_id' }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at' }, + ]), []); + + return ( + 用户流水}> +
fetchList(1, size)} style={{ marginBottom: 12 }}> + + + + + + + + + + + + + + + + + + + +
r.id || r.transaction_id} + columns={columns} + dataSource={data} + loading={loading} + pagination={{ current: page, pageSize: size, total, onChange: (p, s) => { setPage(p); setSize(s); fetchList(p, s); } }} + scroll={{ x: 1200 }} + /> + + ); +}; + +export default FinanceTransactionLogs; + + + diff --git a/src/pages/GoalfyMaxUsers.tsx b/src/pages/GoalfyMaxUsers.tsx new file mode 100644 index 0000000..dce2b5c --- /dev/null +++ b/src/pages/GoalfyMaxUsers.tsx @@ -0,0 +1,532 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { GoalfyMaxUsersApi } from '../services/api' +import { getAllUserLevelConfigs } from '../services/userLevelConfigApi' +import type { UserLevelConfig } from '../types/userLevelConfig' +import { Table, Button, Modal, Form, Input, Tag, Space, Popconfirm, Select, message, Row, Col, Card, Statistic } from 'antd' +import { EditOutlined, DeleteOutlined, StopOutlined, CheckCircleOutlined, UserOutlined, PlusOutlined, DollarOutlined, MinusCircleOutlined } from '@ant-design/icons' + +type User = { + id: number + user_id: number + username: string + email: string + nickname?: string + avatar?: string + is_banned: boolean + ban_reason?: string + last_login_at?: string + created_at?: string + user_level_code?: string + user_level_id?: number + user_level?: { + id: number + level_name: string + level_code: string + } + version?: number // 版本:1-用户版 2-观察版 3-用户版和观察版 + goalfy_hub_permission?: number // 是否具备GoalfyHub权限 (0: 不具备, 1: 具备) + balance?: number // 用户余额(美元) +} + +const { Option } = Select + +export default function GoalfyMaxUsers() { + const [loading, setLoading] = useState(false) + const [list, setList] = useState([]) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [size, setSize] = useState(10) + const [keyword, setKeyword] = useState('') + const [status, setStatus] = useState(undefined) + const [editOpen, setEditOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [createOpen, setCreateOpen] = useState(false) + const [form] = Form.useForm() + const [createForm] = Form.useForm() + const [userLevels, setUserLevels] = useState([]) + const [addBalanceOpen, setAddBalanceOpen] = useState(false) + const [addingBalanceUser, setAddingBalanceUser] = useState(null) + const [addBalanceForm] = Form.useForm() + const [deductBalanceOpen, setDeductBalanceOpen] = useState(false) + const [deductingBalanceUser, setDeductingBalanceUser] = useState(null) + const [deductBalanceForm] = Form.useForm() + + // 加载用户等级配置 + const fetchUserLevels = async () => { + try { + const levels = await getAllUserLevelConfigs() + // 显示所有等级(包括已禁用的),以便在编辑时能正确回显 + setUserLevels(levels) + } catch (e) { + console.error('获取用户等级配置失败', e) + } + } + + useEffect(() => { + fetchUserLevels() + }, []) + + const params = useMemo(() => ({ username: keyword, email: '', status, page, size }), [keyword, status, page, size]) + + const fetchList = async () => { + setLoading(true) + try { + const res = await GoalfyMaxUsersApi.list(params) + setList(res?.data?.users ?? []) + setTotal(res?.data?.total ?? 0) + } catch (e) { + message.error('获取用户列表失败') + } finally { + setLoading(false) + } + } + + useEffect(() => { fetchList() }, [params.page, params.size]) + + const openEdit = (u: User) => { + setEditing(u) + const mappedLevelId = (() => { + if (u.user_level_id) return u.user_level_id + if (u.user_level_code) { + const level = userLevels.find(l => l.level_code === u.user_level_code) + return level?.id + } + if (u.user_level?.level_code) { + const level = userLevels.find(l => l.level_code === u.user_level?.level_code) + return level?.id + } + return undefined + })() + + // 将version值转换为下拉框多选数组 + // version: 1-用户版, 2-观察版, 3-用户版和观察版 + const versionValue = u.version ?? 1 + const versionOptions: string[] = [] + if (versionValue === 1 || versionValue === 3) { + versionOptions.push('1') // 用户版 + } + if (versionValue === 2 || versionValue === 3) { + versionOptions.push('2') // 观察版 + } + + form.setFieldsValue({ + nickname: u.nickname, + email: u.email, + user_level_id: mappedLevelId, + version: versionOptions, + goalfy_hub_permission: u.goalfy_hub_permission + }) + setEditOpen(true) + } + + const submitEdit = async () => { + try { + const values = await form.validateFields() + if (!editing) return + + const payload: any = { + nickname: values.nickname, + email: values.email, + avatar: values.avatar, + goalfy_hub_permission: values.goalfy_hub_permission, + } + if ('avatar' in payload) delete payload.avatar + if (values.user_level_id) { + const level = userLevels.find(l => l.id === values.user_level_id) + if (level) { + payload.user_level_code = level.level_code + } + delete payload.user_level_id + } + // 处理version字段:将下拉框多选数组转换为INT值 + // [] -> 1 (默认用户版), ['1'] -> 1, ['2'] -> 2, ['1', '2'] -> 3 + if (values.version && Array.isArray(values.version)) { + if (values.version.length === 0) { + payload.version = 1 // 默认用户版 + } else if (values.version.length === 1) { + payload.version = parseInt(values.version[0]) + } else if (values.version.length === 2) { + payload.version = 3 // 用户版和观察版 + } + } else { + // 如果没有选择,保持原值或默认值 + payload.version = editing.version ?? 1 + } + + await GoalfyMaxUsersApi.update(editing.id, payload) + message.success('更新成功') + setEditOpen(false) + fetchList() + } catch {} + } + + const openCreate = () => { + createForm.resetFields() + setCreateOpen(true) + } + + const submitCreate = async () => { + try { + const values = await createForm.validateFields() + const payload: any = { ...values } + if ('avatar' in payload) delete payload.avatar + if (values.user_level_id) { + const level = userLevels.find(l => l.id === values.user_level_id) + if (level) { + payload.user_level_code = level.level_code + } + delete payload.user_level_id + } + // 处理version字段:将下拉框多选数组转换为INT值 + // [] -> 1 (默认用户版), ['1'] -> 1, ['2'] -> 2, ['1', '2'] -> 3 + if (values.version && Array.isArray(values.version)) { + if (values.version.length === 0) { + payload.version = 1 // 默认用户版 + } else if (values.version.length === 1) { + payload.version = parseInt(values.version[0]) + } else if (values.version.length === 2) { + payload.version = 3 // 用户版和观察版 + } + } else { + payload.version = 1 // 默认用户版 + } + await GoalfyMaxUsersApi.create(payload) + message.success('创建成功') + setCreateOpen(false) + fetchList() + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败') + } + } + + const handleDelete = async (u: User) => { + await GoalfyMaxUsersApi.delete(u.id) + message.success('删除成功') + fetchList() + } + + const handleBan = async (u: User) => { + Modal.confirm({ + title: `封禁用户 ${u.username}`, + content: ( +
+ + + + + ), + onOk: async () => { + const { reason } = form.getFieldsValue() + await GoalfyMaxUsersApi.ban(u.id, reason) + message.success('已封禁') + fetchList() + } + }) + } + + const handleUnban = async (u: User) => { + await GoalfyMaxUsersApi.unban(u.id) + message.success('已解封') + fetchList() + } + + const openAddBalance = (u: User) => { + setAddingBalanceUser(u) + addBalanceForm.resetFields() + setAddBalanceOpen(true) + } + + const submitAddBalance = async () => { + try { + const values = await addBalanceForm.validateFields() + if (!addingBalanceUser) return + await GoalfyMaxUsersApi.addBalance(addingBalanceUser.id, values.amount) + message.success('增加余额成功') + setAddBalanceOpen(false) + fetchList() + } catch (error: any) { + message.error(error?.response?.data?.message || '增加余额失败') + } + } + + const openDeductBalance = (u: User) => { + setDeductingBalanceUser(u) + deductBalanceForm.resetFields() + setDeductBalanceOpen(true) + } + + const submitDeductBalance = async () => { + try { + const values = await deductBalanceForm.validateFields() + if (!deductingBalanceUser) return + await GoalfyMaxUsersApi.deductBalance(deductingBalanceUser.id, values.amount) + message.success('减少余额成功') + setDeductBalanceOpen(false) + fetchList() + } catch (error: any) { + message.error(error?.response?.data?.message || '减少余额失败') + } + } + + const columns = [ + { title: '用户名', dataIndex: 'username', key: 'username' }, + { title: '邮箱', dataIndex: 'email', key: 'email' }, + { title: '昵称', dataIndex: 'nickname', key: 'nickname' }, + { + title: '用户等级', + dataIndex: 'user_level', + key: 'user_level', + render: (_: any, record: User) => { + const displayName = (() => { + if (record.user_level?.level_name) return record.user_level.level_name + if (record.user_level_code) { + const level = userLevels.find(l => l.level_code === record.user_level_code) + if (level?.level_name) return level.level_name + } + return null + })() + return displayName ? ( + {displayName} + ) : ( + 未设置 + ) + } + }, + { + title: '版本', + dataIndex: 'version', + key: 'version', + render: (version: number) => { + let tagText = '未知' + let color = 'default' + if (version === 1) { + tagText = '用户版' + color = 'blue' + } else if (version === 2) { + tagText = '观察版' + color = 'green' + } else if (version === 3) { + tagText = '用户版和观察版' + color = 'purple' + } + return {tagText} + }, + }, + { + title: 'GoalfyHub 权限', + dataIndex: 'goalfy_hub_permission', + key: 'goalfy_hub_permission', + render: (permission: number) => { + return permission === 1 ? 具备 : 不具备 + }, + }, + { + title: '余额(美元)', + dataIndex: 'balance', + key: 'balance', + render: (balance: number | undefined) => { + const amount = balance ?? 0 + return ${amount.toFixed(2)} + } + }, + { title: '状态', dataIndex: 'is_banned', key: 'is_banned', render: (v: boolean) => v ? 封禁 : 正常 }, + { title: '最后登录', dataIndex: 'last_login_at', key: 'last_login_at' }, + { title: '创建时间', dataIndex: 'created_at', key: 'created_at' }, + { + title: '操作', key: 'action', render: (_: any, u: User) => ( + + + + + {!u.is_banned && } + {u.is_banned && } + handleDelete(u)}> + + + + ) + } + ] + + const totalUsers = total + const bannedUsers = (list || []).filter(i => i.is_banned).length + const normalUsers = totalUsers - bannedUsers + + return ( +
+ {/* 统计卡片 */} + +
+ + } /> + + + + + + + + + + + + + + + {/* 搜索与筛选 */} + + setKeyword(e.target.value)} allowClear style={{ width: 240 }} /> + + + + + + + {/* 列表 */} +
{ setPage(p); setSize(s || 10) } + }} + /> + + {/* 编辑弹窗 */} + setEditOpen(false)} onOk={submitEdit} okText="保存"> +
+ + + + + + + + + + + + + + + + +
+ + {/* 创建用户弹窗 */} + setCreateOpen(false)} onOk={submitCreate} okText="创建"> +
+ + + + + + + + + + + + + + + + + + + +
+ + {/* 增加余额弹窗 */} + setAddBalanceOpen(false)} onOk={submitAddBalance} okText="确认"> +
+ parseFloat(e.target.value) || 0} + getValueProps={(value) => ({ value: value || '' })} + > + + + {addingBalanceUser && ( +
+
当前余额: ${addingBalanceUser.balance?.toFixed(2) || '0.00'}
+
+ )} + +
+ + {/* 减少余额弹窗 */} + setDeductBalanceOpen(false)} onOk={submitDeductBalance} okText="确认"> +
+ parseFloat(e.target.value) || 0} + getValueProps={(value) => ({ value: value || '' })} + > + + + {deductingBalanceUser && ( +
+
当前余额: ${deductingBalanceUser.balance?.toFixed(2) || '0.00'}
+
+ )} + +
+ + ) +} + + diff --git a/src/pages/InviteCodes.tsx b/src/pages/InviteCodes.tsx new file mode 100644 index 0000000..537826e --- /dev/null +++ b/src/pages/InviteCodes.tsx @@ -0,0 +1,954 @@ +import React, { useState, useEffect } from 'react'; +import { + Card, + Table, + Button, + Space, + Tag, + Popconfirm, + message, + Input, + Select, + DatePicker, + Row, + Col, + Statistic, + Modal, + Form, + Badge, + Tabs, +} from 'antd'; +import { PlusOutlined, DeleteOutlined, CopyOutlined, EditOutlined, BellOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { InviteCodeApiService } from '../services/inviteCodeApi'; +import { InviteCodeApplicationApiService } from '../services/inviteCodeApplicationApi'; +import { getAllUserLevelConfigs } from '../services/userLevelConfigApi'; +import type { InviteCode, InviteCodeStatistics, InviteCodeCreateRequest, InviteCodeUpdateRequest, ClientOption } from '../types/inviteCode'; +import type { InviteCodeApplication, InviteCodeApplicationStatistics } from '../types/inviteCodeApplication'; +import type { UserLevelConfig } from '../types/userLevelConfig'; +import dayjs from 'dayjs'; + +const { RangePicker } = DatePicker; +const { Option } = Select; + +const InviteCodes: React.FC = () => { + const [inviteCodes, setInviteCodes] = useState([]); + const [statistics, setStatistics] = useState(null); + const [loading, setLoading] = useState(false); + const [creating, setCreating] = useState(false); + const [createModalVisible, setCreateModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editingInviteCode, setEditingInviteCode] = useState(null); + const [clientOptions, setClientOptions] = useState([]); + const [userLevels, setUserLevels] = useState([]); + const [createForm] = Form.useForm(); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 20, + total: 0, + }); + + // 申请管理相关状态 + const [applications, setApplications] = useState([]); + const [applicationStatistics, setApplicationStatistics] = useState(null); + const [pendingCount, setPendingCount] = useState(0); + const [applicationModalVisible, setApplicationModalVisible] = useState(false); + const [selectedApplications, setSelectedApplications] = useState([]); + const [rejectModalVisible, setRejectModalVisible] = useState(false); + const [rejectingApplication, setRejectingApplication] = useState(null); + const [applicationPagination, setApplicationPagination] = useState({ + current: 1, + pageSize: 20, + total: 0, + }); + + // 搜索条件 + const [filters, setFilters] = useState({ + code: '', + is_used: undefined as boolean | undefined, + dateRange: null as [dayjs.Dayjs, dayjs.Dayjs] | null, + }); + + // 获取邀请码列表 + const fetchInviteCodes = async () => { + setLoading(true); + try { + const params: any = { + page: pagination.current, + size: pagination.pageSize, + }; + + if (filters.code) params.code = filters.code; + if (filters.is_used !== undefined) params.is_used = filters.is_used; + if (filters.dateRange) { + params.start_time = filters.dateRange[0].format('YYYY-MM-DD'); + params.end_time = filters.dateRange[1].format('YYYY-MM-DD'); + } + + const response = await InviteCodeApiService.getInviteCodeList(params); + setInviteCodes(response.list); + setPagination(prev => ({ ...prev, total: response.total })); + } catch (error) { + message.error('获取邀请码列表失败'); + } finally { + setLoading(false); + } + }; + + // 获取统计信息 + const fetchStatistics = async () => { + try { + const stats = await InviteCodeApiService.getStatistics(); + setStatistics(stats); + } catch (error) { + console.error('获取统计信息失败:', error); + } + }; + + // 获取客户端选项 + const fetchClientOptions = async () => { + try { + const options = await InviteCodeApiService.getClientOptions(); + setClientOptions(options); + } catch (error) { + console.error('获取客户端选项失败:', error); + } + }; + + // 获取用户等级列表 + const fetchUserLevels = async () => { + try { + const levels = await getAllUserLevelConfigs(); + // 只显示启用状态的等级 + setUserLevels(levels.filter(level => level.status === 1)); + } catch (error) { + console.error('获取用户等级列表失败:', error); + } + }; + + // 获取申请列表 + const fetchApplications = async () => { + setLoading(true); + try { + const params = { + status: 'pending' as const, + page: applicationPagination.current, + size: applicationPagination.pageSize, + }; + const response = await InviteCodeApplicationApiService.getApplicationList(params); + setApplications(response.list); + setApplicationPagination(prev => ({ ...prev, total: response.total })); + } catch (error) { + message.error('获取申请列表失败'); + } finally { + setLoading(false); + } + }; + + // 获取待处理申请数量 + const fetchPendingCount = async () => { + try { + const count = await InviteCodeApplicationApiService.getPendingCount(); + setPendingCount(count); + } catch (error) { + console.error('获取待处理数量失败:', error); + } + }; + + // 获取申请统计 + const fetchApplicationStatistics = async () => { + try { + const stats = await InviteCodeApplicationApiService.getStatistics(); + setApplicationStatistics(stats); + } catch (error) { + console.error('获取申请统计失败:', error); + } + }; + + // 创建邀请码 + const handleCreate = async (values: any) => { + setCreating(true); + try { + // 解析邮箱列表(支持换行和逗号分隔) + let emails: string[] = []; + if (values.emails) { + const emailText = values.emails.trim(); + if (emailText) { + // 先按换行分割,再按逗号分割 + const rawEmails = emailText + .split(/[\n,,]/) + .map((email: string) => email.trim()) + .filter((email: string) => email.length > 0); + + // 去重处理(转换为小写后去重) + const emailSet = new Set(); + emails = rawEmails.filter(email => { + const lowerEmail = email.toLowerCase(); + if (emailSet.has(lowerEmail)) { + return false; // 已存在,跳过 + } + emailSet.add(lowerEmail); + return true; // 新邮箱,保留 + }); + } + } + + const requestData: InviteCodeCreateRequest = { + emails: emails.length > 0 ? emails : undefined, + user_level_id: values.user_level_id, + expires_at: values.expires_at ? dayjs(values.expires_at).toISOString() : undefined, + }; + + const response = await InviteCodeApiService.createInviteCode(requestData); + + // 处理响应(可能是单个对象或数组) + const createdCodes = Array.isArray(response) ? response : [response]; + const count = createdCodes.length; + + if (count === 1) { + message.success('邀请码创建成功'); + } else { + message.success(`成功创建 ${count} 个邀请码`); + } + + setCreateModalVisible(false); + createForm.resetFields(); // 清空表单 + fetchInviteCodes(); + fetchStatistics(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建邀请码失败'); + } finally { + setCreating(false); + } + }; + + // 编辑邀请码 + const handleEdit = async (values: InviteCodeUpdateRequest) => { + if (!editingInviteCode) return; + + setCreating(true); + try { + await InviteCodeApiService.updateInviteCode(editingInviteCode.id, values); + message.success('邀请码更新成功'); + setEditModalVisible(false); + setEditingInviteCode(null); + fetchInviteCodes(); + fetchStatistics(); + } catch (error) { + message.error('更新邀请码失败'); + } finally { + setCreating(false); + } + }; + + // 打开编辑模态框 + const openEditModal = (inviteCode: InviteCode) => { + setEditingInviteCode(inviteCode); + setEditModalVisible(true); + }; + + // 删除邀请码 + const handleDelete = async (id: number) => { + try { + await InviteCodeApiService.deleteInviteCode(id); + message.success('删除成功'); + fetchInviteCodes(); + fetchStatistics(); + } catch (error) { + message.error('删除失败'); + } + }; + + // 复制邀请码 + const handleCopyCode = (code: string) => { + navigator.clipboard.writeText(code); + message.success('邀请码已复制到剪贴板'); + }; + + // 复制邀请链接 + const handleCopyLink = (inviteUrl?: string, code?: string) => { + const link = inviteUrl || `${window.location.origin}/register?invite=${code ?? ''}`; + navigator.clipboard.writeText(link); + message.success('邀请链接已复制到剪贴板'); + }; + + // 搜索 + const handleSearch = () => { + setPagination(prev => ({ ...prev, current: 1 })); + fetchInviteCodes(); + }; + + // 重置搜索 + const handleReset = () => { + setFilters({ + code: '', + is_used: undefined, + dateRange: null, + }); + setPagination(prev => ({ ...prev, current: 1 })); + }; + + // 审批通过申请 + const handleApproveApplication = async (applicationId: number, validDays: number = 3) => { + try { + await InviteCodeApplicationApiService.approveApplication({ + application_id: applicationId, + valid_days: validDays, + }); + message.success('申请已通过,邀请码已发送至申请人邮箱'); + fetchApplications(); + fetchPendingCount(); + fetchInviteCodes(); + fetchStatistics(); + } catch (error) { + message.error('审批失败'); + } + }; + + // 审批拒绝申请 + const handleRejectApplication = async (applicationId: number, rejectReason?: string) => { + try { + await InviteCodeApplicationApiService.rejectApplication({ + application_id: applicationId, + reject_reason: rejectReason, + }); + message.success('申请已拒绝'); + fetchApplications(); + fetchPendingCount(); + setRejectModalVisible(false); + setRejectingApplication(null); + } catch (error) { + message.error('拒绝失败'); + } + }; + + // 批量审批通过 + const handleBatchApprove = async () => { + if (selectedApplications.length === 0) { + message.warning('请选择要审批的申请'); + return; + } + try { + await InviteCodeApplicationApiService.batchApproveApplications({ + application_ids: selectedApplications, + valid_days: 3, + }); + message.success(`成功审批通过 ${selectedApplications.length} 个申请`); + setSelectedApplications([]); + fetchApplications(); + fetchPendingCount(); + fetchInviteCodes(); + fetchStatistics(); + } catch (error) { + message.error('批量审批失败'); + } + }; + + // 批量审批拒绝 + const handleBatchReject = async (rejectReason?: string) => { + if (selectedApplications.length === 0) { + message.warning('请选择要拒绝的申请'); + return; + } + try { + await InviteCodeApplicationApiService.batchRejectApplications({ + application_ids: selectedApplications, + reject_reason: rejectReason, + }); + message.success(`成功拒绝 ${selectedApplications.length} 个申请`); + setSelectedApplications([]); + fetchApplications(); + fetchPendingCount(); + } catch (error) { + message.error('批量拒绝失败'); + } + }; + + useEffect(() => { + fetchInviteCodes(); + fetchStatistics(); + fetchClientOptions(); + fetchUserLevels(); + fetchPendingCount(); + fetchApplicationStatistics(); + }, [pagination.current, pagination.pageSize]); + + // 申请列表表格列 + const applicationColumns = [ + { + title: '申请邮箱', + dataIndex: 'email', + key: 'email', + }, + { + title: '申请理由', + dataIndex: 'reason', + key: 'reason', + render: (reason: string | undefined) => reason || '-', + ellipsis: true, + }, + { + title: '申请时间', + dataIndex: 'created_at', + key: 'created_at', + render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: '操作', + key: 'action', + render: (_: any, record: InviteCodeApplication) => ( + + + + + ), + }, + ]; + + const columns = [ + { + title: '邀请码', + dataIndex: 'code', + key: 'code', + render: (code: string) => ( + + {code} + + + ), + }, + { + title: '邀请链接', + dataIndex: 'invite_url', + key: 'invite_url', + render: (_: any, record: InviteCode) => ( + + {record.invite_url ?? '-'} + + + ), + }, + { + title: '是否使用', + dataIndex: 'is_used', + key: 'is_used', + render: (isUsed: boolean) => ( + + {isUsed ? '已使用' : '未使用'} + + ), + }, + { + title: '邮箱', + dataIndex: 'email', + key: 'email', + render: (email: string | undefined) => ( + email ? {email} : - + ), + }, + { + title: '用户等级', + dataIndex: 'user_level_name', + key: 'user_level_name', + render: (userLevelName: string | undefined) => ( + userLevelName ? {userLevelName} : - + ), + }, + { + title: '客户端', + dataIndex: 'client_label', + key: 'client_label', + render: (clientLabel: string | undefined) => ( + clientLabel ? {clientLabel} : - + ), + }, + { + title: '过期时间', + dataIndex: 'expires_at', + key: 'expires_at', + render: (expiresAt: string | null) => { + if (!expiresAt) { + return 永不过期; + } + const isExpired = dayjs(expiresAt).isBefore(dayjs()); + return ( + + {dayjs(expiresAt).format('YYYY-MM-DD HH:mm:ss')} + {isExpired && ' (已过期)'} + + ); + }, + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: '操作', + key: 'action', + render: (record: InviteCode) => ( + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( +
+ + {/* 统计信息 */} + {statistics && ( + +
+ + + + + + + + + + + + + )} + + {/* 操作栏 */} + + + + + + + + {/* 搜索栏 */} + + setFilters(prev => ({ ...prev, code: e.target.value }))} + style={{ width: 200 }} + /> + + setFilters(prev => ({ ...prev, dateRange: dates }))} + /> + + + + + {/* 表格 */} +
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`, + onChange: (page, pageSize) => { + setPagination(prev => ({ ...prev, current: page, pageSize: pageSize || 20 })); + }, + }} + /> + + + {/* 创建邀请码模态框 */} + { + setCreateModalVisible(false); + createForm.resetFields(); // 关闭时清空表单 + }} + footer={null} + width={500} + > +
+ { + if (!value || value.trim() === '') { + return Promise.resolve(); + } + const emailText = value.trim(); + const emails = emailText.split(/[\n,,]/).map((e: string) => e.trim()).filter((e: string) => e.length > 0); + const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/; + for (const email of emails) { + if (!emailRegex.test(email)) { + return Promise.reject(new Error(`邮箱格式不正确: ${email}`)); + } + } + return Promise.resolve(); + }, + }, + ]} + extra="每行一个邮箱,或使用逗号分隔。支持多个邮箱,将为每个邮箱创建一个邀请码。" + > + + + + + + + + + + + + + + + +
+ + {/* 编辑邀请码模态框 */} + { + setEditModalVisible(false); + setEditingInviteCode(null); + }} + footer={null} + width={500} + > +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + {/* 待处理申请弹窗 */} + { + setApplicationModalVisible(false); + setSelectedApplications([]); + }} + width={900} + footer={[ + , + , + , + ]} + > +
{ + setSelectedApplications(selectedRowKeys as number[]); + }, + }} + pagination={{ + ...applicationPagination, + onChange: (page, pageSize) => { + setApplicationPagination(prev => ({ ...prev, current: page, pageSize: pageSize || 20 })); + fetchApplications(); + }, + }} + /> + + + {/* 拒绝申请弹窗 */} + { + setRejectModalVisible(false); + setRejectingApplication(null); + }} + footer={null} + width={500} + > +
{ + if (rejectingApplication) { + handleRejectApplication(rejectingApplication.id, values.reject_reason); + } + }} + > + + + + + + + + + + + + + +
+ + ); +}; + +export default InviteCodes; \ No newline at end of file diff --git a/src/pages/MCPProviderPricing.tsx b/src/pages/MCPProviderPricing.tsx new file mode 100644 index 0000000..a5fd86b --- /dev/null +++ b/src/pages/MCPProviderPricing.tsx @@ -0,0 +1,517 @@ +import React, { useEffect, useState } from 'react'; +import { + Table, + Button, + Modal, + Form, + Input, + InputNumber, + Tag, + Space, + Popconfirm, + message, + Card, + Select, + Switch, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + CheckCircleOutlined, + StopOutlined, +} from '@ant-design/icons'; +import type { MCPProvider } from '../types/mcpProvider'; +import { + getMCPProviderList, + createMCPProvider, + updateMCPProvider, + deleteMCPProvider, + updateMCPProviderStatus, + updateMCPProviderIsUsed, +} from '../services/mcpProviderApi'; + +const { Option } = Select; +const { TextArea } = Input; + +export default function MCPProviderPricing() { + const [loading, setLoading] = useState(false); + const [list, setList] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [size, setSize] = useState(10); + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + const [createForm] = Form.useForm(); + const [searchForm] = Form.useForm(); + + const fetchList = async (searchParams: any = {}) => { + setLoading(true); + try { + const params = { + page, + page_size: size, + ...searchParams, + }; + const res = await getMCPProviderList(params); + setList(res?.data ?? []); + setTotal(res?.total ?? 0); + } catch (e: any) { + message.error(e?.response?.data?.message || '获取列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchList(); + }, [page, size]); + + const openEdit = (provider: MCPProvider) => { + setEditing(provider); + form.setFieldsValue({ + provider: provider.provider, + base_url: provider.base_url, + auth: provider.auth, + account: provider.account, + price_type: provider.price_type, + price: provider.price, + floating_ratio: provider.floating_ratio, + is_used: provider.is_used, + status: provider.status, + description: provider.description, + }); + setEditOpen(true); + }; + + const submitEdit = async () => { + try { + const values = await form.validateFields(); + if (!editing) return; + await updateMCPProvider(editing.id, values); + message.success('更新成功'); + setEditOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '更新失败'); + } + }; + + const openCreate = () => { + createForm.resetFields(); + createForm.setFieldsValue({ + is_used: true, + status: 'active', + floating_ratio: 0.3, + }); + setCreateOpen(true); + }; + + const submitCreate = async () => { + try { + const values = await createForm.validateFields(); + await createMCPProvider(values); + message.success('创建成功'); + setCreateOpen(false); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '创建失败'); + } + }; + + const handleDelete = async (provider: MCPProvider) => { + try { + await deleteMCPProvider(provider.id); + message.success('删除成功'); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '删除失败'); + } + }; + + const handleToggleStatus = async (provider: MCPProvider) => { + try { + const newStatus = provider.status === 'active' ? 'inactive' : 'active'; + await updateMCPProviderStatus(provider.id, { status: newStatus }); + message.success(newStatus === 'active' ? '已启用' : '已禁用'); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '状态更新失败'); + } + }; + + const handleToggleIsUsed = async (provider: MCPProvider, isUsed: boolean) => { + try { + await updateMCPProviderIsUsed(provider.id, { is_used: isUsed }); + message.success(isUsed ? '已启用' : '已停用'); + fetchList(); + } catch (error: any) { + message.error(error?.response?.data?.message || '状态更新失败'); + } + }; + + const handleSearch = (values: any) => { + setPage(1); + fetchList(values); + }; + + const handleReset = () => { + searchForm.resetFields(); + setPage(1); + fetchList(); + }; + + const columns = [ + { + title: '供应商', + dataIndex: 'provider', + key: 'provider', + width: 150, + }, + { + title: 'Base URL', + dataIndex: 'base_url', + key: 'base_url', + ellipsis: true, + width: 200, + }, + { + title: '账户', + dataIndex: 'account', + key: 'account', + width: 120, + render: (text: string) => text || '-', + }, + { + title: '价格类型', + dataIndex: 'price_type', + key: 'price_type', + width: 120, + }, + { + title: '价格', + dataIndex: 'price', + key: 'price', + width: 100, + render: (value: number) => value.toFixed(6), + }, + { + title: '浮动比例', + dataIndex: 'floating_ratio', + key: 'floating_ratio', + width: 100, + render: (value: number) => `${(value * 100).toFixed(2)}%`, + }, + { + title: '使用中', + dataIndex: 'is_used', + key: 'is_used', + width: 100, + render: (value: boolean, record: MCPProvider) => ( + handleToggleIsUsed(record, checked)} + /> + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (value: string) => + value === 'active' ? ( + 启用 + ) : ( + 禁用 + ), + }, + { + title: '更新时间', + dataIndex: 'updated_at', + key: 'updated_at', + width: 180, + render: (text: string) => new Date(text).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'action', + width: 250, + fixed: 'right' as const, + render: (_: any, provider: MCPProvider) => ( + + + + handleDelete(provider)} + > + + + + ), + }, + ]; + + return ( +
+ +
+
+ + + + + + + + + + + + + + + + + +
+ +
`共 ${total} 条`, + onChange: (p, s) => { + setPage(p); + setSize(s); + }, + }} + /> + + + {/* 编辑弹窗 */} + setEditOpen(false)} + width={600} + destroyOnClose + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +