后端使用 AI 开发前端速成:第三期:Vue 3 深入实战 —— 列表页开发
第三期:Vue 3 深入实战 —— 列表页开发
本期目标:掌握 Vue 3 Composition API,独立完成标准管理后台列表页
核心理念:Vue 页面的逻辑 = 后端 Controller 层 —— 定义状态 → 定义方法 → 在合适时机调用
产出物:一个可运行的用户列表页(搜索 + 表格 + 分页 + 删除确认)+ 手写 Pinia store
目录
- 第一章:为什么先学 Vue
- 第二章:Vue 3 五个核心概念深入
- 第三章:Element Plus 组件库实战
- 第四章:Pinia 状态管理
- 第五章:实战——用户管理列表页
- 第六章:AI Prompt 模板与审查清单
- 第七章:课后作业
第一章:为什么先学 Vue
Vue 对后端工程师更友好
| 方面 | Vue 3 | React 18 |
|---|---|---|
| 模板语法 | 接近 HTML,直觉性强 | JSX = JS,初期会晕 |
| 条件渲染 | v-if就像 if 语句 | {condition && <X />} |
| 列表渲染 | v-for就像 for 循环 | .map() |
| 双向绑定 | v-model一行搞定 | 受控组件要写 onChange |
| 响应式 | 自动追踪依赖 | 手动管理依赖数组 |
Vue 的心智模型
数据变化 → Vue 自动检测 → 自动更新界面这就像观察者模式:你改了数据,Vue 像是一个自动通知系统,帮你刷新界面。
后端类比:
ref/reactive≈ 类的成员变量computed≈ 数据库视图(自动缓存)watch≈ 触发器 / MQ 消费者onMounted≈ 构造函数 / @PostConstruct
第二章:Vue 3 五个核心概念深入
2.1 ref —— 管理基础类型状态
<script setup> import { ref } from 'vue' // ref 返回一个响应式对象,通过 .value 访问和修改 const count = ref(0) const loading = ref(false) const keyword = ref('') // 修改时必须用 .value count.value++ loading.value = true </script> <template> <!-- 模板中自动解包,不需要 .value --> <p>{{ count }}</p> <button @click="count++">+1</button> </template>何时用 ref:
- 基础类型(string、number、boolean)
- 需要替换整个对象的场景(如接口返回的数组)
常见错误:
// ❌ 错误:解构 ref 会失去响应式const{value}=count// ❌ 错误:直接赋值不会触发更新count=1// 这是给变量赋值,不是修改 .value// ✅ 正确:通过 .value 修改count.value=12.2 reactive —— 管理对象类型状态
<script setup> import { reactive } from 'vue' // reactive 的对象可以直接修改属性,不需要 .value const form = reactive({ keyword: '', status: '', dateRange: [] }) // 直接修改属性 form.keyword = '张三' form.status = 'active' </script>何时用 reactive:
- 表单数据(天然就是对象结构)
- 配置对象、状态集合
注意点:
// ❌ 错误:解构 reactive 对象会失去响应式const{keyword}=form// keyword 不再是响应式的// ✅ 正确:使用 toRefsimport{toRefs}from'vue'const{keyword}=toRefs(form)// keyword 仍然是 ref// ❌ 错误:直接赋值新对象会断开响应式form={keyword:'',status:''}// ✅ 正确:用 Object.assign 或直接改属性Object.assign(form,{keyword:'',status:''})2.3 computed —— 派生状态(自动缓存)
<script setup> import { ref, computed } from 'vue' const firstName = ref('张') const lastName = ref('三') // 自动缓存,只有依赖变化时才重新计算 const fullName = computed(() => { console.log('重新计算 fullName') // 只在 firstName/lastName 变化时执行 return firstName.value + lastName.value }) // 带 setter 的 computed const displayName = computed({ get: () => `${lastName.value}先生`, set: (val) => { // val = '李先生' → 解析出 '李' lastName.value = val.charAt(0) } }) </script>使用场景:
- 表格数据的格式化显示
- 基于搜索条件的过滤结果
- 分页信息的文字描述(如"第 1 页,共 10 页")
2.4 watch —— 监听器
<script setup> import { ref, watch } from 'vue' const searchKeyword = ref('') const form = reactive({ keyword: '', status: '' }) // 监听 ref watch(searchKeyword, (newVal, oldVal) => { console.log('搜索词从', oldVal, '变成', newVal) fetchData() // 自动触发搜索 }) // 监听 reactive 对象的单个属性 watch(() => form.status, (newVal) => { console.log('状态变化:', newVal) }) // 监听整个 reactive 对象(深度监听) watch(form, () => { console.log('form 任何属性变化') }, { deep: true }) // 立即执行一次 watch(searchKeyword, () => { fetchData() }, { immediate: true }) </script>使用场景:
- 搜索条件变化自动触发查询
- 路由参数变化重新加载数据
- 表单数据变化自动保存草稿
注意事项:
// ❌ 错误:监听 reactive 对象不加函数包裹watch(form,...)// 这样只能监听引用变化// ✅ 正确:监听 reactive 对象的属性watch(()=>form.keyword,...)// ✅ 或者深度监听watch(form,...,{deep:true})2.5 生命周期钩子
| 钩子 | 后端类比 | 用途 |
|---|---|---|
onMounted | @PostConstruct | 页面加载完成,发起初始请求 |
onUnmounted | @PreDestroy | 页面销毁,清理定时器/WebSocket |
onUpdated | - | 数据更新后操作 DOM(少用) |
<script setup> import { onMounted, onUnmounted } from 'vue' let timer = null let controller = null onMounted(() => { fetchData() // 页面加载完自动请求数据 timer = setInterval(() => { ... }, 5000) // 开启定时器 }) onUnmounted(() => { clearInterval(timer) // 必须清理,否则内存泄漏 controller?.abort() // 取消未完成的请求 }) </script>核心洞察:
Vue 页面的逻辑和后端 Controller 层几乎一样: 定义状态变量 → 定义业务方法 → 在合适的时机调用 模板部分只是数据的"呈现层"第三章:Element Plus 组件库实战
3.1 管理后台最常用的 10 个组件
| 组件 | 用途 | 使用频率 |
|---|---|---|
el-form+el-form-item | 表单布局 | ⭐⭐⭐⭐⭐ |
el-input | 文本输入 | ⭐⭐⭐⭐⭐ |
el-select+el-option | 下拉选择 | ⭐⭐⭐⭐⭐ |
el-button | 按钮 | ⭐⭐⭐⭐⭐ |
el-table+el-table-column | 数据表格 | ⭐⭐⭐⭐⭐ |
el-pagination | 分页 | ⭐⭐⭐⭐⭐ |
el-dialog | 弹窗 | ⭐⭐⭐⭐ |
el-tag | 标签 | ⭐⭐⭐⭐ |
el-date-picker | 日期选择 | ⭐⭐⭐ |
el-message/el-message-box | 提示/确认框 | ⭐⭐⭐ |
3.2 表单组件
<template> <el-form :model="searchForm" inline> <el-form-item label="关键词"> <el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable /> </el-form-item> <el-form-item label="状态"> <el-select v-model="searchForm.status" placeholder="请选择" clearable> <el-option label="全部" value="" /> <el-option label="启用" value="active" /> <el-option label="禁用" value="inactive" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">搜索</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> </template> <script setup> import { reactive } from 'vue' const searchForm = reactive({ keyword: '', status: '' }) const handleSearch = () => { console.log('搜索条件:', searchForm) } const handleReset = () => { searchForm.keyword = '' searchForm.status = '' } </script>关键知识点:
:model="searchForm":绑定表单数据对象v-model="searchForm.keyword":双向绑定inline:表单项横向排列clearable:显示清空按钮
3.3 表格组件
<template> <el-table :data="tableData" v-loading="loading" row-key="id"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="用户名" /> <el-table-column prop="status" label="状态"> <!-- 自定义列内容 --> <template #default="{ row }"> <el-tag :type="row.status === 'active' ? 'success' : 'danger'"> {{ row.status === 'active' ? '启用' : '禁用' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="150"> <template #default="{ row }"> <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button> <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> </template>关键知识点:
:data="tableData":绑定表格数据源v-loading="loading":加载状态row-key="id":行唯一标识(用于展开行和树形数据)#default="{ row }":自定义列内容,row 是当前行数据
3.4 分页组件
<template> <el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @change="handlePageChange" /> </template> <script setup> import { reactive } from 'vue' const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) const handlePageChange = () => { fetchData() } </script>第四章:Pinia 状态管理
4.1 为什么需要 Pinia
管理后台中,多个页面需要共享状态:
- 用户信息(登录态、权限)
- 系统配置(主题、语言)
- 缓存数据(字典表、下拉选项)
后端类比:Pinia ≈ 后端的 Service 层,管理全局状态。
4.2 定义 Store
// stores/user.tsimport{defineStore}from'pinia'import{ref,computed}from'vue'// 命名规范:useXxxStoreexportconstuseUserStore=defineStore('user',()=>{// ========== State ==========consttoken=ref(localStorage.getItem('token')||'')constuserInfo=ref(null)// ========== Getter(computed)==========constisLoggedIn=computed(()=>!!token.value)constuserName=computed(()=>userInfo.value?.name||'未登录')// ========== Action ==========constsetToken=(newToken:string)=>{token.value=newToken localStorage.setItem('token',newToken)}constsetUserInfo=(info:any)=>{userInfo.value=info}constlogout=()=>{token.value=''userInfo.value=nulllocalStorage.removeItem('token')}// 必须返回所有要暴露的状态和方法return{token,userInfo,isLoggedIn,userName,setToken,setUserInfo,logout}})4.3 在组件中使用
<script setup> import { useUserStore } from '@/stores/user' // 获取 store 实例 const userStore = useUserStore() // 直接使用 console.log(userStore.isLoggedIn) console.log(userStore.token) // 调用 action userStore.setToken('xxx') userStore.logout() // 解构(需要用 storeToRefs 保持响应式) import { storeToRefs } from 'pinia' const { token, userInfo } = storeToRefs(userStore) // 现在 token.value 是响应式的 </script>第五章:实战——用户管理列表页
5.1 需求描述
开发一个完整的用户管理列表页,包含:
- 搜索区域:用户名输入框、状态下拉框、搜索按钮、重置按钮
- 数据表格:ID、用户名、邮箱、状态(Tag 组件)、创建时间、操作列
- 分页功能
- 删除操作需要二次确认
- 页面加载时自动请求数据
5.2 环境准备
npmcreate vite@latest vue-admin ----templatevue-tscdvue-adminnpminstallelement-plus axios vue-router pinianpminstall-D@types/nodenpmrun dev5.3 给 AI 的 Prompt
请帮我写一个 Vue 3.4 + TypeScript + Element Plus 的用户管理列表页。 技术栈: - Vue 3.4 Composition API + <script setup> - TypeScript(严格模式,不要用 any) - Element Plus 组件库 - Axios 请求 功能需求: 1. 搜索区域:用户名输入框、状态下拉框、搜索按钮、重置按钮 2. 表格展示:ID、用户名、邮箱、状态(用 Tag 组件)、创建时间、操作列(编辑/删除按钮) 3. 分页功能(支持页码和页大小切换) 4. 页面加载时自动请求数据 5. 删除操作需要二次确认(用 ElMessageBox) 接口定义: GET /api/users 参数:{ page: number, pageSize: number, keyword?: string, status?: string } 返回:{ code: number, data: { list: User[], total: number }, message: string } TypeScript 类型: interface User { id: number name: string email: string status: 'active' | 'inactive' createdAt: string } 状态管理要求: - 使用 ref 和 reactive 管理状态 - 搜索表单用 reactive - 表格数据用 ref 输出要求: - 使用 Element Plus 的 el-table、el-pagination、el-form 等组件 - 表格需要 loading 状态 - 空数据时显示 "暂无数据" - 代码注释用中文 - 将组件保存为 src/views/UserList.vue5.4 AI 生成后的审查清单
技术栈检查:
- 是
<script setup>不是export default - 使用了
ref和reactive - 没有使用
any
功能检查:
- 搜索时页码重置为 1
- 重置时清空搜索条件并重新请求
- 表格有 loading 状态
- 空数据显示 Empty 组件
- 删除有二次确认
安全与性能:
- 没有使用
v-html - 定时器/WebSocket 在 onUnmounted 中清理
5.5 完整代码参考
<template> <div class="user-list-page"> <!-- 搜索表单 --> <el-card class="search-card"> <el-form :model="searchForm" inline> <el-form-item label="关键词"> <el-input v-model="searchForm.keyword" placeholder="请输入用户名" clearable @keyup.enter="handleSearch" /> </el-form-item> <el-form-item label="状态"> <el-select v-model="searchForm.status" placeholder="请选择" clearable> <el-option label="全部" value="" /> <el-option label="启用" value="active" /> <el-option label="禁用" value="inactive" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button> <el-button :icon="RefreshRight" @click="handleReset">重置</el-button> </el-form-item> </el-form> </el-card> <!-- 数据表格 --> <el-card class="table-card"> <el-table :data="tableData" v-loading="loading" row-key="id"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="用户名" min-width="120" /> <el-table-column prop="email" label="邮箱" min-width="180" /> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.status === 'active' ? 'success' : 'danger'"> {{ row.status === 'active' ? '启用' : '禁用' }} </el-tag> </template> </el-table-column> <el-table-column prop="createdAt" label="创建时间" min-width="160" /> <el-table-column label="操作" width="150" fixed="right"> <template #default="{ row }"> <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button> <el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.pageSize" :total="pagination.total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @change="handlePageChange" /> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Search, RefreshRight } from '@element-plus/icons-vue' import axios from 'axios' // ========== TypeScript 类型定义 ========== interface User { id: number name: string email: string status: 'active' | 'inactive' createdAt: string } interface ApiResponse<T> { code: number data: T message: string } // ========== 状态定义 ========== const loading = ref(false) const tableData = ref<User[]>([]) const searchForm = reactive({ keyword: '', status: '' }) const pagination = reactive({ page: 1, pageSize: 10, total: 0 }) // ========== 方法定义 ========== const fetchData = async () => { loading.value = true try { const { data } = await axios.get<ApiResponse<{ list: User[]; total: number }>>('/api/users', { params: { page: pagination.page, pageSize: pagination.pageSize, ...searchForm } }) tableData.value = data.data.list pagination.total = data.data.total } catch (error) { ElMessage.error('获取数据失败') console.error(error) } finally { loading.value = false } } const handleSearch = () => { pagination.page = 1 fetchData() } const handleReset = () => { searchForm.keyword = '' searchForm.status = '' handleSearch() } const handlePageChange = () => { fetchData() } const handleEdit = (row: User) => { console.log('编辑用户:', row) // TODO: 跳转到编辑页或打开编辑弹窗 } const handleDelete = async (row: User) => { try { await ElMessageBox.confirm( `确定要删除用户 "${row.name}" 吗?`, '删除确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ) await axios.delete(`/api/users/${row.id}`) ElMessage.success('删除成功') fetchData() // 刷新列表 } catch (error: any) { if (error !== 'cancel') { ElMessage.error('删除失败') } } } // ========== 生命周期 ========== onMounted(() => { fetchData() }) </script> <style scoped> .user-list-page { padding: 20px; } .search-card { margin-bottom: 20px; } .table-card { margin-bottom: 20px; } .el-pagination { margin-top: 20px; justify-content: flex-end; } </style>第六章:AI Prompt 模板与审查清单
6.1 本期专用 Prompt 模板
模板:Vue 列表页生成
请用 Vue 3.4 + TypeScript + Element Plus 写一个 [模块名] 管理列表页。 功能需求: 1. 搜索区域:[字段列表] 2. 表格展示:[字段列表] 3. 分页功能(支持页码和页大小切换) 4. 操作列:[编辑/删除/详情] 5. [批量操作/其他功能] 接口定义: GET [接口地址] 参数:{ page, pageSize, ... } 返回:{ code, data: { list, total }, message } TypeScript 类型: interface [Entity] { [字段定义] } 输出要求: - 使用 <script setup> + Composition API - 使用 ref 和 reactive 管理状态 - 严格 TypeScript,禁止 any - 处理 loading、error、empty 三种状态 - 代码注释中文6.2 AI 代码审查清单
| 检查项 | 合格标准 | 检查方法 |
|---|---|---|
| 技术栈 | Vue 3 + TS + Element Plus | 看文件头部 import |
| 状态管理 | 用 ref/reactive,没有用选项式 API | 看 |
第七章:实战
7.1 必做实战
作业 1:手写 Pinia Store
实现一个useDictStore,管理系统的字典数据(如用户状态、订单状态等):
// 要求:// 1. state:dictMap(Record<string, any[]>),存储各类字典数据// 2. action:fetchDict(type: string),从 /api/dict/:type 获取字典数据并缓存// 3. getter:getDict(type: string),获取指定类型的字典,如果不存在自动调用 fetchDict// 4. 使用 localStorage 做持久化作业 2:扩展用户列表页
在课堂代码基础上,增加以下功能:
- 搜索条件增加"创建时间范围"(使用 el-date-picker)
- 表格增加"批量删除"功能(使用 el-table 的 selection 列)
- 操作列增加"查看详情"按钮,点击后弹出详情弹窗(el-dialog)
7.2 检验标准
| 检验项 | 标准 | 自评 |
|---|---|---|
| ref/reactive | 能正确选择使用场景 | □ |
| computed | 能写出带缓存的派生状态 | □ |
| watch | 能监听 ref 和 reactive | □ |
| Element Plus | 能独立查阅文档使用新组件 | □ |
| Pinia | 能独立写出完整的 Store | □ |
| AI 协作 | 能用 Prompt 生成完整列表页并审查 | □ |
7.3 常见问题 FAQ
Q:ref 和 reactive 到底怎么选?
表单用 reactive,其他基础类型用 ref,数组也用 ref。记住:需要替换整个对象时,用 ref。
Q:为什么我的修改不触发界面更新?
检查三个问题:
- ref 是否通过 .value 修改?
- reactive 是否解构后修改?
- 数组是否通过索引直接修改?(用 splice 或赋值新数组)
Q:Element Plus 组件名记不住怎么办?
不需要记。给 AI 描述功能,它会自动使用正确的组件。你只需要会查文档确认组件 API。
附录:Vue 3 核心 API 速查表
| API | 作用 | 使用方式 | 注意 |
|---|---|---|---|
ref | 响应式基础类型 | const x = ref(0) | 修改用.value |
reactive | 响应式对象 | const obj = reactive({...}) | 不要解构 |
computed | 派生状态 | const x = computed(() => ...) | 自动缓存 |
watch | 监听变化 | watch(source, callback, options) | reactive 要加() => |
onMounted | 挂载后执行 | onMounted(() => ...) | 发起初始请求 |
onUnmounted | 卸载前执行 | onUnmounted(() => ...) | 清理资源 |
toRefs | 解构保持响应式 | const { x } = toRefs(obj) | 配合 reactive 使用 |
nextTick | DOM 更新后执行 | await nextTick() | 操作更新后的 DOM |
下一期预告:React 18 深入实战 —— 用同样的需求,实现 React 版本的用户管理列表页,对比 Vue 和 React 的差异
