39 KiB
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
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 行
页面功能:
- 列表展示(分页)
- 创建新配置
- 编辑配置
- 删除配置
- 切换状态(启用/禁用)
关键代码片段:
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
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;
新页面的添加步骤:
- 在
src/pages/目录下创建新页面组件 - 在
App.tsx中导入页面组件 - 在
Routes中添加<Route>配置 - 如果需要在菜单中显示,更新
Layout.tsx中的导航配置
4. 菜单配置
菜单配置方式
主菜单文件: /Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx
菜单配置使用静态定义的方式,在 Layout 组件中硬编码:
// 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>
)}
子菜单配置(以系统管理为例)
// 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() 来判断是否显示:
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
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
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 组件使用
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 组件使用
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 弹窗组件使用
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
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
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
};
};
使用方式:
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 的
styleprop 进行内联样式
创建新的配置管理页面的完整步骤
第1步:创建 TypeScript 类型定义
创建文件 /Users/youziba/goalfyagent/goalfymax-admin-web/src/types/systemConfig.ts:
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:
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:
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 中添加:
import SystemConfigs from './pages/SystemConfigs';
// 在 Routes 中添加新路由
<Route path="/system/system-configs" element={<SystemConfigs />} />
第5步:添加菜单项
在 /Users/youziba/goalfyagent/goalfymax-admin-web/src/components/Layout.tsx 中的系统管理子菜单中添加:
<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 |
菜单和导航 |
最佳实践:
- 按照现有的目录结构和命名规范创建新文件
- 使用 TypeScript 定义强类型
- 在 API 层使用
apiClient进行请求 - 在页面中使用 Ant Design 组件
- 通过
usePagePermissionshook 检查权限 - 使用 Jotai 的
useAtom进行状态管理