Files
goalfylearning-admin-web/FRONTEND_GUIDE.md

39 KiB
Raw Permalink Blame History

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;

新页面的添加步骤:

  1. src/pages/ 目录下创建新页面组件
  2. App.tsx 中导入页面组件
  3. Routes 中添加 <Route> 配置
  4. 如果需要在菜单中显示,更新 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 的 style prop 进行内联样式

创建新的配置管理页面的完整步骤

第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 菜单和导航

最佳实践:

  1. 按照现有的目录结构和命名规范创建新文件
  2. 使用 TypeScript 定义强类型
  3. 在 API 层使用 apiClient 进行请求
  4. 在页面中使用 Ant Design 组件
  5. 通过 usePagePermissions hook 检查权限
  6. 使用 Jotai 的 useAtom 进行状态管理