|
@@ -0,0 +1,444 @@
|
|
|
|
|
+# 管理后台前端页面开发流程规范
|
|
|
|
|
+
|
|
|
|
|
+## 适用场景
|
|
|
|
|
+
|
|
|
|
|
+管理后台页面开发,包括列表页、详情页、表单页等后台功能页面的实现流程。
|
|
|
|
|
+
|
|
|
|
|
+## 开发流程
|
|
|
|
|
+
|
|
|
|
|
+### 1. **创建页面组件**
|
|
|
|
|
+ - 位置: `src/client/admin/pages/[EntityName]List.tsx` 和 `src/client/admin/pages/[EntityName]Detail.tsx`
|
|
|
|
|
+ - 列表页组件示例:
|
|
|
|
|
+ ```tsx
|
|
|
|
|
+ import React, { useEffect, useState } from 'react';
|
|
|
|
|
+ import { Table, Button, Space, Tag, message } from 'antd';
|
|
|
|
|
+ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
|
|
|
+ import { useNavigate } from 'react-router-dom';
|
|
|
|
|
+ import type { InferResponseType } from 'hono/client';
|
|
|
|
|
+ import { yourEntityClient } from '@/client/api';
|
|
|
|
|
+
|
|
|
|
|
+ // 类型定义
|
|
|
|
|
+ type EntityItem = InferResponseType<typeof yourEntityClient[':id']['$get'], 200>;
|
|
|
|
|
+ type EntityListResponse = InferResponseType<typeof yourEntityClient.$get, 200>;
|
|
|
|
|
+
|
|
|
|
|
+ const YourEntityList: React.FC = () => {
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+ const [data, setData] = useState<EntityItem[]>([]);
|
|
|
|
|
+ const [loading, setLoading] = useState<boolean>(true);
|
|
|
|
|
+ const [pagination, setPagination] = useState({
|
|
|
|
|
+ current: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ total: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 获取数据列表
|
|
|
|
|
+ const fetchData = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const res = await yourEntityClient.$get({
|
|
|
|
|
+ query: {
|
|
|
|
|
+ page: pagination.current,
|
|
|
|
|
+ pageSize: pagination.pageSize
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) throw new Error('获取数据失败');
|
|
|
|
|
+
|
|
|
|
|
+ const result: EntityListResponse = await res.json();
|
|
|
|
|
+ setData(result.data);
|
|
|
|
|
+ setPagination({
|
|
|
|
|
+ ...pagination,
|
|
|
|
|
+ total: result.pagination.total
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error(error instanceof Error ? error.message : '获取数据失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ }, [pagination.current, pagination.pageSize]);
|
|
|
|
|
+
|
|
|
|
|
+ // 分页变化处理
|
|
|
|
|
+ const handleTableChange = (pagination: any) => {
|
|
|
|
|
+ setPagination({
|
|
|
|
|
+ ...pagination,
|
|
|
|
|
+ current: pagination.current,
|
|
|
|
|
+ pageSize: pagination.pageSize
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 操作按钮处理
|
|
|
|
|
+ const handleAdd = () => {
|
|
|
|
|
+ navigate('/your-entities/new');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleEdit = (id: number) => {
|
|
|
|
|
+ navigate(`/your-entities/${id}`);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDelete = async (id: number) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await yourEntityClient[':id'].$delete({
|
|
|
|
|
+ param: { id }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) throw new Error('删除失败');
|
|
|
|
|
+
|
|
|
|
|
+ message.success('删除成功');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error(error instanceof Error ? error.message : '删除失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 表格列定义
|
|
|
|
|
+ const columns = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: 'ID',
|
|
|
|
|
+ dataIndex: 'id',
|
|
|
|
|
+ key: 'id',
|
|
|
|
|
+ width: 80
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '名称',
|
|
|
|
|
+ dataIndex: 'name',
|
|
|
|
|
+ key: 'name'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '状态',
|
|
|
|
|
+ dataIndex: 'status',
|
|
|
|
|
+ key: 'status',
|
|
|
|
|
+ render: (status: number) => (
|
|
|
|
|
+ <Tag color={status === 1 ? 'green' : 'red'}>
|
|
|
|
|
+ {status === 1 ? '启用' : '禁用'}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '操作',
|
|
|
|
|
+ key: 'action',
|
|
|
|
|
+ render: (_: any, record: EntityItem) => (
|
|
|
|
|
+ <Space size="middle">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ icon={<EditOutlined />}
|
|
|
|
|
+ onClick={() => handleEdit(record.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 编辑
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ danger
|
|
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
|
|
+ onClick={() => handleDelete(record.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 删除
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="page-container">
|
|
|
|
|
+ <div className="page-header">
|
|
|
|
|
+ <h2>实体管理</h2>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ icon={<PlusOutlined />}
|
|
|
|
|
+ onClick={handleAdd}
|
|
|
|
|
+ >
|
|
|
|
|
+ 添加实体
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Table
|
|
|
|
|
+ columns={columns}
|
|
|
|
|
+ dataSource={data.map(item => ({ ...item, key: item.id }))}
|
|
|
|
|
+ loading={loading}
|
|
|
|
|
+ pagination={{
|
|
|
|
|
+ current: pagination.current,
|
|
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
|
|
+ total: pagination.total,
|
|
|
|
|
+ showSizeChanger: true,
|
|
|
|
|
+ showQuickJumper: true,
|
|
|
|
|
+ showTotal: (total) => `共 ${total} 条记录`
|
|
|
|
|
+ }}
|
|
|
|
|
+ onChange={handleTableChange}
|
|
|
|
|
+ bordered
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ export default YourEntityList;
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 2. **注册路由配置**
|
|
|
|
|
+ - 位置: `src/client/admin/routes.tsx`
|
|
|
|
|
+ - 添加路由配置示例:
|
|
|
|
|
+ ```typescript
|
|
|
|
|
+ import YourEntityList from './pages/YourEntityList';
|
|
|
|
|
+ import YourEntityDetail from './pages/YourEntityDetail';
|
|
|
|
|
+
|
|
|
|
|
+ export const routes = [
|
|
|
|
|
+ // ...其他路由
|
|
|
|
|
+ {
|
|
|
|
|
+ path: '/your-entities',
|
|
|
|
|
+ element: <YourEntityList />
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ path: '/your-entities/:id',
|
|
|
|
|
+ element: <YourEntityDetail />
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ path: '/your-entities/new',
|
|
|
|
|
+ element: <YourEntityDetail isNew={true} />
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 3. **添加菜单配置**
|
|
|
|
|
+ - 位置: `src/client/admin/menu.tsx`
|
|
|
|
|
+ - 添加菜单配置示例:
|
|
|
|
|
+ ```typescript
|
|
|
|
|
+ import { TableOutlined } from '@ant-design/icons';
|
|
|
|
|
+
|
|
|
|
|
+ export const menuItems = [
|
|
|
|
|
+ // ...其他菜单项
|
|
|
|
|
+ {
|
|
|
|
|
+ key: 'your-entities',
|
|
|
|
|
+ icon: <TableOutlined />,
|
|
|
|
|
+ label: '实体管理',
|
|
|
|
|
+ path: '/your-entities'
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 4. **创建表单组件**
|
|
|
|
|
+ - 位置: `src/client/admin/components/[EntityName]Form.tsx`
|
|
|
|
|
+ - 表单组件示例:
|
|
|
|
|
+ ```tsx
|
|
|
|
|
+ import React from 'react';
|
|
|
|
|
+ import { Form, Input, Select, message } from 'antd';
|
|
|
|
|
+ import type { CreateRequest, UpdateRequest } from '@/client/api/your-entity';
|
|
|
|
|
+
|
|
|
|
|
+ interface YourEntityFormProps {
|
|
|
|
|
+ initialValues?: Partial<CreateRequest>;
|
|
|
|
|
+ onFinish: (values: CreateRequest | UpdateRequest) => Promise<void>;
|
|
|
|
|
+ loading: boolean;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { Option } = Select;
|
|
|
|
|
+
|
|
|
|
|
+ const YourEntityForm: React.FC<YourEntityFormProps> = ({
|
|
|
|
|
+ initialValues,
|
|
|
|
|
+ onFinish,
|
|
|
|
|
+ loading
|
|
|
|
|
+ }) => {
|
|
|
|
|
+ const [form] = Form.useForm();
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化表单值
|
|
|
|
|
+ React.useEffect(() => {
|
|
|
|
|
+ if (initialValues) {
|
|
|
|
|
+ form.setFieldsValue(initialValues);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ form.resetFields();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [form, initialValues]);
|
|
|
|
|
+
|
|
|
|
|
+ // 表单提交处理
|
|
|
|
|
+ const handleSubmit = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const values = await form.validateFields();
|
|
|
|
|
+ await onFinish(values);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('表单验证失败,请检查输入内容');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Form
|
|
|
|
|
+ form={form}
|
|
|
|
|
+ layout="vertical"
|
|
|
|
|
+ onFinish={handleSubmit}
|
|
|
|
|
+ initialValues={initialValues}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="name"
|
|
|
|
|
+ label="名称"
|
|
|
|
|
+ rules={[
|
|
|
|
|
+ { required: true, message: '请输入名称' },
|
|
|
|
|
+ { max: 20, message: '名称不能超过20个字符' }
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Input placeholder="请输入名称" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="status"
|
|
|
|
|
+ label="状态"
|
|
|
|
|
+ rules={[{ required: true, message: '请选择状态' }]}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Select placeholder="请选择状态">
|
|
|
|
|
+ <Option value={1}>启用</Option>
|
|
|
|
|
+ <Option value={0}>禁用</Option>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="ant-btn ant-btn-primary"
|
|
|
|
|
+ disabled={loading}
|
|
|
|
|
+ >
|
|
|
|
|
+ {loading ? '提交中...' : '提交'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ export default YourEntityForm;
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 5. **实现详情页**
|
|
|
|
|
+ - 位置: `src/client/admin/pages/[EntityName]Detail.tsx`
|
|
|
|
|
+ - 详情页组件示例:
|
|
|
|
|
+ ```tsx
|
|
|
|
|
+ import React, { useEffect, useState } from 'react';
|
|
|
|
|
+ import { useParams, useNavigate } from 'react-router-dom';
|
|
|
|
|
+ import { Card, Spin, message } from 'antd';
|
|
|
|
|
+ import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
|
|
+ import { yourEntityClient } from '@/client/api';
|
|
|
|
|
+ import YourEntityForm from '../components/YourEntityForm';
|
|
|
|
|
+
|
|
|
|
|
+ // 类型定义
|
|
|
|
|
+ type EntityDetail = InferResponseType<typeof yourEntityClient[':id']['$get'], 200>;
|
|
|
|
|
+ type CreateRequest = InferRequestType<typeof yourEntityClient.$post>['json'];
|
|
|
|
|
+ type UpdateRequest = InferRequestType<typeof yourEntityClient[':id']['$put']>['json'];
|
|
|
|
|
+
|
|
|
|
|
+ interface YourEntityDetailProps {
|
|
|
|
|
+ isNew?: boolean;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const YourEntityDetail: React.FC<YourEntityDetailProps> = ({ isNew = false }) => {
|
|
|
|
|
+ const { id } = useParams<{ id: string }>();
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+ const [loading, setLoading] = useState<boolean>(!isNew);
|
|
|
|
|
+ const [initialValues, setInitialValues] = useState<Partial<CreateRequest>>({});
|
|
|
|
|
+
|
|
|
|
|
+ // 获取详情数据
|
|
|
|
|
+ const fetchDetail = async () => {
|
|
|
|
|
+ if (!id || isNew) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const res = await yourEntityClient[':id'].$get({
|
|
|
|
|
+ param: { id: Number(id) }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) throw new Error('获取详情失败');
|
|
|
|
|
+
|
|
|
|
|
+ const data: EntityDetail = await res.json();
|
|
|
|
|
+ setInitialValues(data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error(error instanceof Error ? error.message : '获取详情失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ fetchDetail();
|
|
|
|
|
+ }, [id, isNew]);
|
|
|
|
|
+
|
|
|
|
|
+ // 表单提交处理
|
|
|
|
|
+ const handleFinish = async (values: CreateRequest | UpdateRequest) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+
|
|
|
|
|
+ if (isNew) {
|
|
|
|
|
+ // 创建新实体
|
|
|
|
|
+ const res = await yourEntityClient.$post({
|
|
|
|
|
+ json: values as CreateRequest
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) throw new Error('创建失败');
|
|
|
|
|
+ message.success('创建成功');
|
|
|
|
|
+ } else if (id) {
|
|
|
|
|
+ // 更新实体
|
|
|
|
|
+ const res = await yourEntityClient[':id'].$put({
|
|
|
|
|
+ param: { id: Number(id) },
|
|
|
|
|
+ json: values as UpdateRequest
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) throw new Error('更新失败');
|
|
|
|
|
+ message.success('更新成功');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ navigate('/your-entities');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error(error instanceof Error ? error.message : '操作失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="page-container">
|
|
|
|
|
+ <div className="page-header">
|
|
|
|
|
+ <h2>{isNew ? '添加实体' : '编辑实体'}</h2>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <Spin spinning={loading}>
|
|
|
|
|
+ <YourEntityForm
|
|
|
|
|
+ initialValues={initialValues}
|
|
|
|
|
+ onFinish={handleFinish}
|
|
|
|
|
+ loading={loading}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Spin>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ export default YourEntityDetail;
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 6. **样式规范**
|
|
|
|
|
+ - 使用CSS Modules或Styled Components进行样式隔离
|
|
|
|
|
+ - 页面容器样式:
|
|
|
|
|
+ ```css
|
|
|
|
|
+ .page-container {
|
|
|
|
|
+ padding: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .page-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### 7. **权限控制**
|
|
|
|
|
+ - 使用ProtectedRoute组件包装需要权限控制的页面:
|
|
|
|
|
+ ```tsx
|
|
|
|
|
+ import { ProtectedRoute } from '@/client/admin/components/ProtectedRoute';
|
|
|
|
|
+
|
|
|
|
|
+ export const routes = [
|
|
|
|
|
+ // ...其他路由
|
|
|
|
|
+ {
|
|
|
|
|
+ path: '/your-entities',
|
|
|
|
|
+ element: (
|
|
|
|
|
+ <ProtectedRoute requiredPermissions={['your_entity:read']}>
|
|
|
|
|
+ <YourEntityList />
|
|
|
|
|
+ </ProtectedRoute>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|