Files
goalfylearning-admin-web/QUICK_START.md

567 lines
14 KiB
Markdown
Raw Permalink 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.

# 前端开发快速开始指南
## 项目信息
- **技术栈**: React 18 + TypeScript + Vite + Ant Design 5 + React Router 7
- **项目路径**: `/Users/youziba/goalfyagent/goalfymax-admin-web`
- **状态管理**: Jotai
- **HTTP客户端**: Axios (自动处理 Token 认证和刷新)
---
## 快速创建新的配置管理页面
以创建"通用配置"管理页面为例,需要 5 个步骤:
### 步骤 1创建类型定义文件
**文件**: `src/types/commonConfig.ts`
```typescript
export interface CommonConfig {
id: number;
config_key: string; // 配置键
config_value: string; // 配置值
description: string; // 描述
status: number; // 1=启用, 0=禁用
created_at: string;
updated_at: string;
}
export interface CommonConfigListRequest {
config_key?: string;
status?: number;
page?: number;
size?: number;
}
export interface CommonConfigListResponse {
data: CommonConfig[];
total: number;
page: number;
size: number;
}
export interface CommonConfigCreateRequest {
config_key: string;
config_value: string;
description?: string;
}
export interface CommonConfigUpdateRequest {
config_key: string;
config_value: string;
description?: string;
}
```
### 步骤 2创建 API 服务文件
**文件**: `src/services/commonConfigApi.ts`
```typescript
import { apiClient } from './api';
import type {
CommonConfig,
CommonConfigListRequest,
CommonConfigListResponse,
CommonConfigCreateRequest,
CommonConfigUpdateRequest,
} from '../types/commonConfig';
export const getCommonConfigList = async (
params: CommonConfigListRequest
): Promise<CommonConfigListResponse> => {
const response = await apiClient.get('/admin/common-configs', { params });
return response.data;
};
export const getCommonConfigById = async (id: number): Promise<CommonConfig> => {
const response = await apiClient.get(`/admin/common-configs/${id}`);
return response.data;
};
export const createCommonConfig = async (
data: CommonConfigCreateRequest
): Promise<CommonConfig> => {
const response = await apiClient.post('/admin/common-configs', data);
return response.data;
};
export const updateCommonConfig = async (
id: number,
data: CommonConfigUpdateRequest
): Promise<CommonConfig> => {
const response = await apiClient.put(`/admin/common-configs/${id}`, data);
return response.data;
};
export const deleteCommonConfig = async (id: number): Promise<void> => {
await apiClient.delete(`/admin/common-configs/${id}`);
};
export const updateCommonConfigStatus = async (
id: number,
status: number
): Promise<void> => {
await apiClient.put(`/admin/common-configs/${id}/status`, { status });
};
```
### 步骤 3创建页面组件
**文件**: `src/pages/CommonConfigs.tsx`
```typescript
import React, { useEffect, useState } from 'react';
import {
Table,
Button,
Modal,
Form,
Input,
Tag,
Space,
Popconfirm,
message,
Row,
Col,
Card,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
CheckCircleOutlined,
StopOutlined,
} from '@ant-design/icons';
import type { CommonConfig } from '../types/commonConfig';
import {
getCommonConfigList,
createCommonConfig,
updateCommonConfig,
deleteCommonConfig,
updateCommonConfigStatus,
} from '../services/commonConfigApi';
export default function CommonConfigs() {
const [loading, setLoading] = useState(false);
const [list, setList] = useState<CommonConfig[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<CommonConfig | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [form] = Form.useForm();
const [createForm] = Form.useForm();
// 获取列表
const fetchList = async () => {
setLoading(true);
try {
const res = await getCommonConfigList({ page, size });
setList(res?.data ?? []);
setTotal(res?.total ?? 0);
} catch (e) {
message.error('获取列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchList();
}, [page, size]);
const openEdit = (config: CommonConfig) => {
setEditing(config);
form.setFieldsValue({
config_key: config.config_key,
config_value: config.config_value,
description: config.description,
});
setEditOpen(true);
};
const submitEdit = async () => {
try {
const values = await form.validateFields();
if (!editing) return;
await updateCommonConfig(editing.id, values);
message.success('更新成功');
setEditOpen(false);
fetchList();
} catch (error) {
message.error('更新失败');
}
};
const openCreate = () => {
createForm.resetFields();
setCreateOpen(true);
};
const submitCreate = async () => {
try {
const values = await createForm.validateFields();
await createCommonConfig(values);
message.success('创建成功');
setCreateOpen(false);
fetchList();
} catch (error: any) {
message.error(error?.response?.data?.message || '创建失败');
}
};
const handleDelete = async (config: CommonConfig) => {
try {
await deleteCommonConfig(config.id);
message.success('删除成功');
fetchList();
} catch (error) {
message.error('删除失败');
}
};
const handleToggleStatus = async (config: CommonConfig) => {
try {
const newStatus = config.status === 1 ? 0 : 1;
await updateCommonConfigStatus(config.id, newStatus);
message.success(newStatus === 1 ? '已启用' : '已禁用');
fetchList();
} catch (error) {
message.error('状态更新失败');
}
};
const columns = [
{
title: '配置键',
dataIndex: 'config_key',
key: 'config_key',
},
{
title: '配置值',
dataIndex: 'config_value',
key: 'config_value',
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (value: number) =>
value === 1 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
},
{
title: '操作',
key: 'action',
render: (_: any, config: CommonConfig) => (
<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_key"
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} />
</Form.Item>
</Form>
</Modal>
{/* 创建弹窗 */}
<Modal
title="新建配置"
open={createOpen}
onOk={submitCreate}
onCancel={() => setCreateOpen(false)}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="config_key"
label="配置键"
rules={[{ required: true, message: '请输入配置键' }]}
>
<Input placeholder="请输入配置键app_name" />
</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在 App.tsx 中添加路由
`src/App.tsx` 中找到系统管理路由部分,添加:
```typescript
import CommonConfigs from './pages/CommonConfigs';
// 在 Routes 中的系统管理部分添加:
<Route path="/system/common-configs" element={<CommonConfigs />} />
```
### 步骤 5在 Layout.tsx 中添加菜单项
`src/components/Layout.tsx` 中找到系统管理子菜单部分(`data-tabs="admin"`),添加:
```typescript
<button
className={activeSubTab === 'common-configs' ? 'active' : ''}
onClick={() => navigate('/system/common-configs')}
>
通用配置
</button>
```
---
## 核心文件速查表
| 功能 | 文件位置 | 说明 |
|------|---------|------|
| 全局路由 | `src/App.tsx` | 所有页面路由定义 |
| 菜单导航 | `src/components/Layout.tsx` | 菜单项和导航配置 |
| API 客户端 | `src/services/api.ts` | Axios 包装,自动处理 Token |
| 权限检查 | `src/hooks/usePagePermissions.ts` | 获取用户权限信息 |
| 认证管理 | `src/hooks/useAuth.ts` | SSO 认证相关逻辑 |
| 全局状态 | `src/atoms/auth.ts` | Jotai 状态原子定义 |
---
## 常用开发模式
### 1. 获取列表并分页
```typescript
const [list, setList] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [size, setSize] = useState(10);
const [total, setTotal] = useState(0);
const fetchList = async () => {
const res = await getXxxList({ page, size });
setList(res?.data ?? []);
setTotal(res?.total ?? 0);
};
useEffect(() => {
fetchList();
}, [page, size]);
// 在 Table 中使用
<Table
dataSource={list}
pagination={{
current: page,
pageSize: size,
total: total,
onChange: (p, s) => {
setPage(p);
setSize(s);
},
}}
/>
```
### 2. 打开编辑弹窗并回显数据
```typescript
const [editOpen, setEditOpen] = useState(false);
const [editing, setEditing] = useState<T | null>(null);
const [form] = Form.useForm();
const openEdit = (record: T) => {
setEditing(record);
form.setFieldsValue({
field1: record.field1,
field2: record.field2,
});
setEditOpen(true);
};
const submitEdit = async () => {
const values = await form.validateFields();
await updateXxx(editing!.id, values);
message.success('更新成功');
setEditOpen(false);
fetchList();
};
```
### 3. 权限检查
```typescript
import { usePagePermissions } from '../hooks/usePagePermissions';
function MyComponent() {
const { getAccessiblePages } = usePagePermissions();
const accessiblePages = getAccessiblePages();
if (!accessiblePages.includes('/system')) {
return <div>无权访问</div>;
}
return <div>内容</div>;
}
```
### 4. 调用 API
```typescript
import { apiClient } from '../services/api';
// GET 请求
const data = await apiClient.get('/admin/xxx');
// POST 请求
const result = await apiClient.post('/admin/xxx', { key: 'value' });
// PUT 请求
const updated = await apiClient.put('/admin/xxx/123', { key: 'new-value' });
// DELETE 请求
await apiClient.delete('/admin/xxx/123');
```
---
## Ant Design 常用组件
| 组件 | 用途 | 导入 |
|------|------|------|
| `Button` | 按钮 | `from 'antd'` |
| `Table` | 数据表格 | `from 'antd'` |
| `Form` | 表单 | `from 'antd'` |
| `Input` | 文本输入 | `from 'antd'` |
| `InputNumber` | 数字输入 | `from 'antd'` |
| `Select` | 下拉选择 | `from 'antd'` |
| `Modal` | 弹窗对话框 | `from 'antd'` |
| `Message` | 消息提示 | `from 'antd'` |
| `Tag` | 标签 | `from 'antd'` |
| `Card` | 卡片容器 | `from 'antd'` |
| `Space` | 间距布局 | `from 'antd'` |
| `Popconfirm` | 确认弹窗 | `from 'antd'` |
---
## 开发工作流
1. **创建类型定义**`src/types/xxx.ts`
2. **创建 API 服务**`src/services/xxxApi.ts`
3. **创建页面组件**`src/pages/Xxx.tsx`
4. **添加路由**`src/App.tsx`
5. **添加菜单**`src/components/Layout.tsx`
6. **测试**`npm run dev`
---
## API 命名规范
- 列表: `GET /admin/xxx`
- 详情: `GET /admin/xxx/:id`
- 创建: `POST /admin/xxx`
- 更新: `PUT /admin/xxx/:id`
- 删除: `DELETE /admin/xxx/:id`
- 状态: `PUT /admin/xxx/:id/status`
---
## 注意事项
1. **Token 自动管理**: API 请求会自动附带 Token401 错误会自动刷新 Token
2. **权限检查**: 菜单和路由需要通过 `usePagePermissions` 检查用户权限
3. **类型安全**: 始终使用 TypeScript 定义接口,避免使用 `any`
4. **表单验证**: 使用 `Form.useForm()``validateFields()` 进行表单验证
5. **消息提示**: 使用 `message.success()` / `message.error()` 等方法提示用户
6. **异步处理**: 使用 `async/await` 简化异步逻辑
---
## 参考文档
完整的开发指南请查看: `/Users/youziba/goalfyagent/goalfymax-admin-web/FRONTEND_GUIDE.md`