📌发布状态:未发布
第六期:管理后台核心页面 —— 表单与 CRUD
本期目标:完成从"只看列表"到"能写完整 CRUD"的跨越
核心理念:管理后台 = 列表页(查)+ 表单页(增改)+ 弹窗(删)+ 详情页(看)
目录
- 第一章:管理后台页面类型
- 第二章:表单页设计
- 第三章:弹窗 vs 新页面
- 第四章:文件上传
- 第五章:实战——完整 CRUD 用户管理
- 第六章:AI Prompt 模板
- 第七章:课后作业
第一章:管理后台页面类型
1.1 四大页面类型
| 类型 | 用途 | 对应操作 | 典型组件 |
|---|---|---|---|
| 列表页 | 数据展示、搜索筛选 | 查(R) | Table + Pagination |
| 表单页 | 新增/编辑数据 | 增/改(C/U) | Form + Input |
| 详情页 | 查看单条数据详情 | 查(R) | Descriptions + Card |
| 弹窗 | 快捷操作、二次确认 | 删/改(D/U) | Dialog/Modal |
1.2 CRUD 闭环
列表页(查) → 点击"新增" → 表单页/弹窗(增) → 提交 → 回到列表页 → 点击"编辑" → 表单页/弹窗(改) → 提交 → 回到列表页 → 点击"删除" → 确认弹窗(删) → 删除 → 刷新列表第二章:表单页设计
2.1 表单字段设计原则
后端工程师视角:表单字段 ≈ DTO 字段
// 后端 DTOinterfaceCreateUserRequest{name:string// 必填email:string// 必填,邮箱格式phone?:string// 可选role:string// 必填,下拉选择status:string// 必填,默认 activeavatar?:string// 可选,文件上传}2.2 Vue 表单页(Element Plus)
<template> <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" > <el-form-item label="用户名" prop="name"> <el-input v-model="form.name" placeholder="请输入用户名" /> </el-form-item> <el-form-item label="邮箱" prop="email"> <el-input v-model="form.email" placeholder="请输入邮箱" /> </el-form-item> <el-form-item label="角色" prop="role"> <el-select v-model="form.role" placeholder="请选择角色"> <el-option label="管理员" value="admin" /> <el-option label="普通用户" value="user" /> </el-select> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="form.status"> <el-radio label="active">启用</el-radio> <el-radio label="inactive">禁用</el-radio> </el-radio-group> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting" @click="handleSubmit"> 提交 </el-button> <el-button @click="handleCancel">取消</el-button> </el-form-item> </el-form> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' import request from '@/utils/request' const props = defineProps<{ userId?: number // 有值=编辑,无值=新增 }>() const emit = defineEmits<['success', 'cancel']>() const formRef = ref<FormInstance>() const submitting = ref(false) const form = reactive({ name: '', email: '', role: '', status: 'active' }) // 表单校验规则 const rules: FormRules = { name: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 2, max: 20, message: '长度 2-20 个字符', trigger: 'blur' } ], email: [ { required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '邮箱格式不正确', trigger: 'blur' } ], role: [ { required: true, message: '请选择角色', trigger: 'change' } ] } // 编辑时回显数据 const fetchDetail = async () => { if (!props.userId) return const { data } = await request.get(`/api/users/${props.userId}`) Object.assign(form, data) } const handleSubmit = async () => { const valid = await formRef.value?.validate() if (!valid) return submitting.value = true try { if (props.userId) { await request.put(`/api/users/${props.userId}`, form) ElMessage.success('修改成功') } else { await request.post('/api/users', form) ElMessage.success('新增成功') } emit('success') } finally { submitting.value = false } } const handleCancel = () => { emit('cancel') } // 初始化 fetchDetail() </script>2.3 React 表单页(Ant Design)
import { useState, useEffect } from 'react' import { Form, Input, Select, Radio, Button, message } from 'antd' import request from '@/utils/request' interface UserFormProps { userId?: number onSuccess: () => void onCancel: () => void } function UserForm({ userId, onSuccess, onCancel }: UserFormProps) { const [form] = Form.useForm() const [submitting, setSubmitting] = useState(false) // 编辑时回显 useEffect(() => { if (userId) { request.get(`/api/users/${userId}`).then(({ data }) => { form.setFieldsValue(data) }) } }, [userId, form]) const handleSubmit = async () => { try { const values = await form.validateFields() setSubmitting(true) if (userId) { await request.put(`/api/users/${userId}`, values) message.success('修改成功') } else { await request.post('/api/users', values) message.success('新增成功') } onSuccess() } catch (error) { console.error(error) } finally { setSubmitting(false) } } return ( <Form form={form} layout="vertical"> <Form.Item name="name" label="用户名" rules={[ { required: true, message: '请输入用户名' }, { min: 2, max: 20, message: '长度 2-20 个字符' } ]}> <Input placeholder="请输入用户名" /> </Form.Item> <Form.Item name="email" label="邮箱" rules={[ { required: true, message: '请输入邮箱' }, { type: 'email', message: '邮箱格式不正确' } ]}> <Input placeholder="请输入邮箱" /> </Form.Item> <Form.Item name="role" label="角色" rules={[{ required: true }]}> <Select placeholder="请选择角色"> <Select.Option value="admin">管理员</Select.Option> <Select.Option value="user">普通用户</Select.Option> </Select> </Form.Item> <Form.Item name="status" label="状态" initialValue="active"> <Radio.Group> <Radio value="active">启用</Radio> <Radio value="inactive">禁用</Radio> </Radio.Group> </Form.Item> <Form.Item> <Button type="primary" loading={submitting} onClick={handleSubmit}> 提交 </Button> <Button onClick={onCancel} style={{ marginLeft: 8 }> 取消 </Button> </Form.Item> </Form> ) } export default UserForm第三章:弹窗 vs 新页面
3.1 选择原则
| 场景 | 推荐 | 原因 |
|---|---|---|
| 字段 < 8 个 | 弹窗 | 不需要跳转,操作连贯 |
| 字段 >= 8 个 | 新页面 | 空间充裕,用户体验好 |
| 复杂交互(富文本、地图) | 新页面 | 需要更多空间 |
| 快速操作 | 弹窗 | 保持上下文 |
| 需要多步骤 | 新页面 | 流程清晰 |
3.2 Vue 弹窗实现
<template> <div> <!-- 列表页内容 --> ... <el-button type="primary" @click="openDialog()">新增用户</el-button> <!-- 新增/编辑弹窗 --> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close > <UserForm :user-id="editingId" @success="handleSuccess" @cancel="dialogVisible = false" /> </el-dialog> </div> </template> <script setup> import { ref, computed } from 'vue' import UserForm from './UserForm.vue' const dialogVisible = ref(false) const editingId = ref() const dialogTitle = computed(() => editingId.value ? '编辑用户' : '新增用户') const openDialog = (id) => { editingId.value = id dialogVisible.value = true } const handleSuccess = () => { dialogVisible.value = false fetchData() // 刷新列表 } </script>3.3 React 弹窗实现
import { useState } from 'react' import { Button, Modal } from 'antd' import UserForm from './UserForm' function UserList() { const [modalVisible, setModalVisible] = useState(false) const [editingId, setEditingId] = useState<number | undefined>() const modalTitle = editingId ? '编辑用户' : '新增用户' const openModal = (id?: number) => { setEditingId(id) setModalVisible(true) } return ( <div> <Button type="primary" onClick={() => openModal()}>新增用户</Button> <Modal title={modalTitle} open={modalVisible} onCancel={() => setModalVisible(false)} footer={null} destroyOnClose > <UserForm userId={editingId} onSuccess={() => { setModalVisible(false) fetchData() }} onCancel={() => setModalVisible(false)} /> </Modal> </div> ) }第四章:文件上传
4.1 基本上传
Vue + Element Plus:
<el-upload action="/api/upload" :headers="{ Authorization: `Bearer ${token}` }" :on-success="handleUploadSuccess" :on-error="handleUploadError" accept=".jpg,.png" :limit="1" > <el-button type="primary">上传头像</el-button> </el-upload>React + Ant Design:
import { Upload, Button, message } from 'antd' import { UploadOutlined } from '@ant-design/icons' <Upload action="/api/upload" headers={{ Authorization: `Bearer ${token}` }} onChange={(info) => { if (info.file.status === 'done') { message.success('上传成功') form.setFieldsValue({ avatar: info.file.response.url }) } }} accept=".jpg,.png" maxCount={1} > <Button icon={<UploadOutlined />}>上传头像</Button> </Upload>4.2 上传前校验
// 校验文件大小和类型constbeforeUpload=(file:File)=>{constisJpgOrPng=file.type==='image/jpeg'||file.type==='image/png'if(!isJpgOrPng){message.error('只支持 JPG/PNG 格式')}constisLt2M=file.size/1024/1024<2if(!isLt2M){message.error('图片大小不能超过 2MB')}returnisJpgOrPng&&isLt2M}第五章:实战——完整 CRUD 用户管理
5.1 需求
基于第三期的用户列表页,扩展为完整的 CRUD:
- 列表页增加"新增"、“编辑”、"删除"按钮
- 新增/编辑使用弹窗
- 删除需要二次确认
- 新增/编辑成功后刷新列表
5.2 给 AI 的 Prompt
请帮我将现有的用户列表页扩展为完整的 CRUD 功能。 当前代码: [粘贴已有的列表页代码] 新增需求: 1. 列表页操作列增加"编辑"和"删除"按钮 2. 列表页顶部增加"新增用户"按钮 3. 新增/编辑使用弹窗(字段少于 8 个,适合弹窗) 4. 弹窗包含字段:用户名、邮箱、角色(下拉)、状态(单选) 5. 表单需要完整的校验规则 6. 删除操作需要二次确认 7. 提交按钮有 loading 状态 8. 新增/编辑成功后自动刷新列表 技术栈: - Vue 3 + TypeScript + Element Plus - 使用现有的 request 封装 输出要求: - 提供完整的列表页代码(含弹窗逻辑) - 提供独立的表单组件 - 严格 TypeScript,禁止 any第六章:AI Prompt 模板
6.1 表单页生成模板
请用 [技术栈] 写一个 [模块名] [新增/编辑] 表单。 表单字段: 1. [字段名]:[类型],[是否必填],[校验规则] 2. ... 接口: POST [接口地址](新增) PUT [接口地址]/:id(编辑) GET [接口地址]/:id(编辑时回显) 输出要求: - 表单校验规则完整 - 提交按钮 loading 状态 - 提交成功返回上一页/关闭弹窗 - 编辑时自动回显数据 - 严格 TypeScript6.2 弹窗组件模板
请帮我把 [表单组件] 封装到弹窗中。 要求: - 弹窗宽度 600px - 弹窗标题根据是否有 id 显示"新增"或"编辑" - destroy-on-close / destroyOnClose - 提交成功后关闭弹窗并通知父组件刷新 - 使用现有 [技术栈] 的 Dialog/Modal 组件第七章:实战
7.1 必做实战
实战 1:为列表页增加完整 CRUD
在第三期的用户列表页基础上:
- 增加新增/编辑弹窗
- 增加删除确认
- 实现表单校验
- 提交成功后刷新列表
实战 2:添加文件上传功能
在用户表单中增加头像上传功能:
- 限制文件类型(jpg/png)
- 限制文件大小(2MB)
- 上传成功后显示预览
7.2 FAQ
Q:表单校验规则怎么设计?
参考后端校验规则,前端做一层用户友好的校验。必填、长度、格式(邮箱/手机号)、范围(数字)是最常见的。
Q:弹窗还是新页面怎么选?
字段少于 8 个用弹窗,多于 8 个用新页面。特殊情况:需要富文本编辑器、地图选择等复杂交互时用新页面。
下一期预告:路由、权限与页面骨架 —— 登录、路由守卫、菜单、Axios 封装