Files
goalfylearning-admin-web/FRONTEND_GUIDE.md

1528 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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(
<StrictMode>
<Provider>
<BrowserRouter>
<AuthGuard>
<App />
</AuthGuard>
</BrowserRouter>
</Provider>
</StrictMode>
)
```
**关键特点**:
- 使用 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<UserLevelConfig[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<UserLevelConfig | null>(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 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: any, config: UserLevelConfig) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(config)}>
编辑
</Button>
<Button
type="link"
icon={config.status === 1 ? <StopOutlined /> : <CheckCircleOutlined />}
onClick={() => handleToggleStatus(config)}
>
{config.status === 1 ? '禁用' : '启用'}
</Button>
<Popconfirm title="确定删除该等级配置?" onConfirm={() => handleDelete(config)}>
<Button type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={24}>
<Card>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新建等级配置
</Button>
</div>
<Table
dataSource={list}
columns={columns}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: size,
total: total,
onChange: (p, s) => {
setPage(p);
setSize(s);
},
}}
/>
</Card>
</Col>
</Row>
{/* 编辑弹窗 */}
<Modal
title="编辑等级配置"
open={editOpen}
onOk={submitEdit}
onCancel={() => setEditOpen(false)}
>
<Form form={form} layout="vertical">
<Form.Item
name="level_name"
label="等级名称"
rules={[{ required: true, message: '请输入等级名称' }]}
>
<Input placeholder="请输入等级名称" />
</Form.Item>
<Form.Item
name="project_limit"
label="项目数限制0表示不限"
rules={[{ required: true, message: '请输入项目数限制' }]}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="sort_order" label="排序顺序">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
{/* 创建弹窗 */}
<Modal
title="新建等级配置"
open={createOpen}
onOk={submitCreate}
onCancel={() => setCreateOpen(false)}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="level_name"
label="等级名称"
rules={[{ required: true, message: '请输入等级名称' }]}
>
<Input placeholder="请输入等级名称" />
</Form.Item>
<Form.Item
name="level_code"
label="等级代码"
rules={[{ required: true, message: '请输入等级代码' }]}
>
<Input placeholder="请输入等级代码vip_plus" />
</Form.Item>
<Form.Item
name="project_limit"
label="项目数限制0表示不限"
rules={[{ required: true, message: '请输入项目数限制' }]}
initialValue={0}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="sort_order" label="排序顺序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
);
}
```
---
## 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 (
<Layout>
<Routes>
{/* 默认重定向到仪表盘 */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
{/* 仪表盘 */}
<Route path="/dashboard" element={<Dashboard />} />
{/* 总览页面 */}
<Route path="/overview" element={<Overview />} />
{/* 运营页面 - 嵌套路由 */}
<Route path="/operations" element={<Navigate to="/operations/user-feedback" replace />} />
<Route path="/operations/user-feedback" element={<UserFeedback />} />
<Route path="/operations/message-push" element={<MessagePush />} />
<Route path="/operations/vendor-model-pricing" element={<VendorModelPricing />} />
{/* 监控页面 - 嵌套路由 */}
<Route path="/monitoring" element={<Navigate to="/monitoring/token-history" replace />} />
<Route path="/monitoring/token-history" element={<TokenHistoryPage />} />
<Route path="/monitoring/token-analytics" element={<TokenAnalytics />} />
<Route path="/monitoring/system-health" element={<SystemHealth />} />
{/* 财务页面 */}
<Route path="/finance" element={<Finance />} />
{/* 系统管理页面 - 嵌套路由 */}
<Route path="/system" element={<Navigate to="/system/quota-rules" replace />} />
<Route path="/system/quota-rules" element={<QuotaRulesPage />} />
<Route path="/system/user-project-quota" element={<UserProjectQuotaWrapper />} />
<Route path="/system/user-management" element={<UserManagement />} />
<Route path="/system/role-management" element={<RoleManagement />} />
<Route path="/system/goalfymax-users" element={<GoalfyMaxUsers />} />
{/* 404页面 */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Layout>
);
}
export default App;
```
**新页面的添加步骤**:
1.`src/pages/` 目录下创建新页面组件
2.`App.tsx` 中导入页面组件
3.`Routes` 中添加 `<Route>` 配置
4. 如果需要在菜单中显示,更新 `Layout.tsx` 中的导航配置
---
## 4. 菜单配置
### 菜单配置方式
**主菜单文件**: `/Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx`
菜单配置使用静态定义的方式,在 Layout 组件中硬编码:
```typescript
// Layout.tsx 中的导航菜单代码(简化版)
{/* 总览 */}
{accessiblePages.includes('/overview') && (
<a
href="#"
className={activeTab === 'overview' ? 'active' : ''}
onClick={(e) => {
e.preventDefault();
navigate('/overview');
}}
>
<span className="icon"></span> 总览 Overview
</a>
)}
{/* 运营 */}
{accessiblePages.includes('/operations') && (
<a
href="#"
className={activeTab === 'operations' ? 'active' : ''}
onClick={(e) => {
e.preventDefault();
navigate('/operations');
}}
>
<span className="icon"></span> 运营 Operations
</a>
)}
{/* 系统管理 */}
{accessiblePages.includes('/system') && (
<a
href="#"
className={activeTab === 'admin' ? 'active' : ''}
onClick={(e) => {
e.preventDefault();
navigate('/system');
}}
>
<span className="icon"></span> 系统管理 Admin
</a>
)}
```
### 子菜单配置(以系统管理为例)
```typescript
// Layout.tsx 中的系统管理子菜单代码
{activeTab === 'admin' && (
<nav className="subnav">
<div className="segment" data-tabs="admin">
<button
className={activeSubTab === 'quota-rules' ? 'active' : ''}
onClick={() => navigate('/system/quota-rules')}
>
配额/套餐
</button>
<button
className={activeSubTab === 'user-project-quota' ? 'active' : ''}
onClick={() => navigate('/system/user-project-quota')}
>
用户项目配额
</button>
<button
className={activeSubTab === 'user-management' ? 'active' : ''}
onClick={() => navigate('/system/user-management')}
>
系统用户管理
</button>
<button
className={activeSubTab === 'role-management' ? 'active' : ''}
onClick={() => navigate('/system/role-management')}
>
角色管理
</button>
<button
className={activeSubTab === 'goalfymax-users' ? 'active' : ''}
onClick={() => navigate('/system/goalfymax-users')}
>
GoalfyMax用户
</button>
</div>
</nav>
)}
```
**菜单的权限控制**:
菜单项通过 `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<T = any>(url: string, config?: any): Promise<T> {
const response = await this.instance.get<T>(url, config);
return response.data;
}
// POST 请求
async post<T = any>(
url: string,
data?: any,
config?: any
): Promise<T> {
const response = await this.instance.post<T>(url, data, config);
return response.data;
}
// PUT 请求
async put<T = any>(
url: string,
data?: any,
config?: any
): Promise<T> {
const response = await this.instance.put<T>(url, data, config);
return response.data;
}
// DELETE 请求
async delete<T = any>(
url: string,
data?: any,
config?: any
): Promise<T> {
const response = await this.instance.delete<T>(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<UserLevelConfigListResponse> => {
const response = await apiClient.get('/admin/user-level-configs', { params });
return response.data;
};
// 获取用户等级配置详情
export const getUserLevelConfigById = async (id: number): Promise<UserLevelConfig> => {
const response = await apiClient.get(`/admin/user-level-configs/${id}`);
return response.data;
};
// 创建用户等级配置
export const createUserLevelConfig = async (
data: UserLevelConfigCreateRequest
): Promise<UserLevelConfig> => {
const response = await apiClient.post('/admin/user-level-configs', data);
return response.data;
};
// 更新用户等级配置
export const updateUserLevelConfig = async (
id: number,
data: UserLevelConfigUpdateRequest
): Promise<UserLevelConfig> => {
const response = await apiClient.put(`/admin/user-level-configs/${id}`, data);
return response.data;
};
// 删除用户等级配置
export const deleteUserLevelConfig = async (id: number): Promise<void> => {
await apiClient.delete(`/admin/user-level-configs/${id}`);
};
// 更新用户等级配置状态
export const updateUserLevelConfigStatus = async (
id: number,
data: UserLevelConfigStatusRequest
): Promise<void> => {
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 中使用
<Form form={form} layout="vertical">
<Form.Item
name="level_name"
label="等级名称"
rules={[{ required: true, message: '请输入等级名称' }]}
>
<Input placeholder="请输入等级名称" />
</Form.Item>
<Form.Item
name="project_limit"
label="项目数限制"
rules={[{ required: true, message: '请输入项目数限制' }]}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
</Form>
```
### 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 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: any, record: UserLevelConfig) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(record)}>
编辑
</Button>
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>
删除
</Button>
</Space>
),
},
];
// 在 JSX 中使用
<Table
dataSource={list}
columns={columns}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: size,
total: total,
onChange: (p, s) => {
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 中使用
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新建配置
</Button>
<Modal
title="新建配置"
open={createOpen}
onOk={submitCreate}
onCancel={() => setCreateOpen(false)}
>
<Form form={createForm} layout="vertical">
{/* 表单项 */}
</Form>
</Modal>
```
---
## 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 <div>无权访问</div>;
}
return <div>有权访问</div>;
}
```
---
## 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<SystemConfigListResponse> => {
const response = await apiClient.get('/admin/system-configs', { params });
return response.data;
};
// 获取系统配置详情
export const getSystemConfigById = async (id: number): Promise<SystemConfig> => {
const response = await apiClient.get(`/admin/system-configs/${id}`);
return response.data;
};
// 创建系统配置
export const createSystemConfig = async (
data: SystemConfigCreateRequest
): Promise<SystemConfig> => {
const response = await apiClient.post('/admin/system-configs', data);
return response.data;
};
// 更新系统配置
export const updateSystemConfig = async (
id: number,
data: SystemConfigUpdateRequest
): Promise<SystemConfig> => {
const response = await apiClient.put(`/admin/system-configs/${id}`, data);
return response.data;
};
// 删除系统配置
export const deleteSystemConfig = async (id: number): Promise<void> => {
await apiClient.delete(`/admin/system-configs/${id}`);
};
// 更新系统配置状态
export const updateSystemConfigStatus = async (
id: number,
status: number
): Promise<void> => {
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<SystemConfig[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<SystemConfig | null>(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 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: any, config: SystemConfig) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(config)}>
编辑
</Button>
<Button
type="link"
icon={config.status === 1 ? <StopOutlined /> : <CheckCircleOutlined />}
onClick={() => handleToggleStatus(config)}
>
{config.status === 1 ? '禁用' : '启用'}
</Button>
<Popconfirm title="确定删除该配置?" onConfirm={() => handleDelete(config)}>
<Button type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={24}>
<Card>
<div style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
新建配置
</Button>
</div>
<Table
dataSource={list}
columns={columns}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: size,
total: total,
onChange: (p, s) => {
setPage(p);
setSize(s);
},
}}
/>
</Card>
</Col>
</Row>
{/* 编辑弹窗 */}
<Modal
title="编辑配置"
open={editOpen}
onOk={submitEdit}
onCancel={() => setEditOpen(false)}
>
<Form form={form} layout="vertical">
<Form.Item
name="config_name"
label="配置名称"
rules={[{ required: true, message: '请输入配置名称' }]}
>
<Input placeholder="请输入配置名称" />
</Form.Item>
<Form.Item
name="config_value"
label="配置值"
rules={[{ required: true, message: '请输入配置值' }]}
>
<Input placeholder="请输入配置值" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} placeholder="请输入描述" />
</Form.Item>
</Form>
</Modal>
{/* 创建弹窗 */}
<Modal
title="新建配置"
open={createOpen}
onOk={submitCreate}
onCancel={() => setCreateOpen(false)}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="config_name"
label="配置名称"
rules={[{ required: true, message: '请输入配置名称' }]}
>
<Input placeholder="请输入配置名称" />
</Form.Item>
<Form.Item
name="config_value"
label="配置值"
rules={[{ required: true, message: '请输入配置值' }]}
>
<Input placeholder="请输入配置值" />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} placeholder="请输入描述" />
</Form.Item>
</Form>
</Modal>
</div>
);
}
```
### 第4步添加路由
`/Users/youziba/goalfyagent/goalfymax-admin-web/src/App.tsx` 中添加:
```typescript
import SystemConfigs from './pages/SystemConfigs';
// 在 Routes 中添加新路由
<Route path="/system/system-configs" element={<SystemConfigs />} />
```
### 第5步添加菜单项
`/Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx` 中的系统管理子菜单中添加:
```typescript
<button
className={activeSubTab === 'system-configs' ? 'active' : ''}
onClick={() => navigate('/system/system-configs')}
>
通用配置
</button>
```
---
## 总结
**核心文件清单**:
| 文件类型 | 路径 | 说明 |
|---------|------|------|
| 类型定义 | `/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` 进行状态管理