Vue 学习与实践大纲(后端视角)
作为一个后端大数据开发人员,对于前端不必深究;重点掌握 Vue 框架的基本用法和常用 UI 组件(Element Plus),并结合大模型的代码生成能力,快速产出可维护的管理后台页面。关键是做好"组件抽象",让生成的代码结构清晰、可复用、易扩展。
学习目标
- 能独立完成典型 CRUD 列表页(搜索、表格、分页、弹窗表单)。
- 会用组合式 API(
setup
、ref
、computed
、watch
)。 - 掌握 Element Plus 常用组件并能抽象为通用组件。
- 会封装 Axios 请求与路由守卫,完成登录鉴权与错误处理。
- 能用 LLM 生成前端骨架,并通过约束让组件结构可复用。
快速入门要点
- 组件与响应式:用
ref/reactive
管理局部状态,computed
计算派生值。 - 路由与布局:
vue-router
定义页面与主布局,登录后进入主框架。 - UI 组件:Element Plus 的表格、表单、弹窗、消息、日期、上传。
- 状态管理:Pinia 最小使用(如
userStore
存 token、用户信息)。 - 网络请求:Axios 封装,统一注入 Token,统一错误提示与重试策略。
- 权限控制:路由守卫 + 指令/组件控制按钮级权限。
- 组件抽象:props/emits/slots 约定,维持单一职责与低耦合。
目录结构建议
src/
api/ 接口请求封装与模块
components/ 可复用基础组件(表格、搜索区、分页、上传等)
router/ 路由与守卫
stores/ Pinia stores
utils/ 工具方法(时间、下载、权限判断等)
views/ 业务页面(列表、编辑、详情)
Element Plus 必会组件
- 数据展示:
ElTable
、ElPagination
、ElCard
- 表单:
ElForm
、ElInput
、ElSelect
、ElDatePicker
、ElUpload
- 反馈:
ElMessage
、ElMessageBox
、ElDialog
、ElNotification
- 布局:
ElContainer
、ElHeader
、ElAside
、ElMain
、ElRow
、ElCol
网络与权限
- Axios:创建实例,
request
拦截器写入 token;response
统一处理。 - 路由守卫:在
beforeEach
判断登录状态与权限,未登录跳转登录页。
组件抽象与复用原则
- 单一职责:展示组件不直接调接口,数据与分页交由外层控制。
- 事件导向:用
emits
暴露事件,让外层掌控行为与数据。 - 插槽扩展:复杂单元格、操作列通过 slots 扩展实现。
- 低耦合:组件避免直接依赖 store 或路由,必要数据用 props 注入。
- 可组合:通用逻辑封为 composable(如
usePagination
、useSearchForm
)。
最小项目初始化与依赖
-
环境:Node 18+,包管理器
npm
或pnpm
-
初始化命令(Windows PowerShell):
npm create vite@latest my-admin -- --template vue
cd my-admin
npm i element-plus axios pinia vue-router
入口示例(src/main.ts
):
ts
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.use(ElementPlus)
app.mount('#app')
基础路由(src/router/index.ts
):
ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('../views/Home.vue') },
{ path: '/login', component: () => import('../views/Login.vue') }
]
})
export default router
用户状态(src/stores/user.ts
):
ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ token: '' }),
actions: { setToken(t: string) { this.token = t } }
})
请求封装(src/api/http.ts
):
ts
import axios from 'axios'
const http = axios.create({ baseURL: '/api', timeout: 10000 })
http.interceptors.request.use((config) => {
// 注入 token(示例):
// const token = localStorage.getItem('token')
// if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
http.interceptors.response.use(
(res) => res.data,
(err) => Promise.reject(err)
)
export default http
通用表格组件约定与示例
约定:
- props:
columns
、data
、loading
、pagination({ page, pageSize, total })
- emits:
update:pagination
、row-click
、selection-change
、refresh
- slots:
toolbar
、cell-[columnKey]
示例组件(src/components/TablePro.vue
):
vue
<template>
<div>
<slot name="toolbar" />
<el-table
:data="data"
:loading="loading"
@row-click="(row) => emit('row-click', row)"
@selection-change="(rows) => emit('selection-change', rows)"
>
<el-table-column type="selection" width="48" />
<el-table-column
v-for="col in columns"
:key="col.key"
:prop="col.key"
:label="col.label"
:width="col.width"
:align="col.align || 'left'"
>
<template #default="scope">
<slot :name="`cell-${col.key}`" :row="scope.row">
{{ scope.row[col.key] }}
</slot>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="pagination"
class="mt-12"
background
layout="prev, pager, next, sizes, total"
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
@update:current-page="(p) => emit('update:pagination', { page: p, pageSize: pagination.pageSize })"
@update:page-size="(s) => emit('update:pagination', { page: 1, pageSize: s })"
/>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
interface Column { key: string; label: string; width?: number; align?: 'left'|'center'|'right' }
const props = defineProps({
columns: { type: Array as PropType<Column[]>, required: true },
data: { type: Array as PropType<any[]>, default: () => [] },
loading: { type: Boolean, default: false },
pagination: {
type: Object as PropType<{ page: number; pageSize: number; total: number }>,
default: undefined
}
})
const emit = defineEmits<{
(e: 'update:pagination', v: { page: number; pageSize: number }): void
(e: 'row-click', v: any): void
(e: 'selection-change', v: any[]): void
(e: 'refresh'): void
}>()
</script>
列表页示例(src/views/UserList.vue
):
vue
<template>
<el-card>
<TablePro
:columns="columns"
:data="rows"
:loading="loading"
:pagination="pagination"
@update:pagination="onPage"
>
<template #toolbar>
<el-form :inline="true" :model="query">
<el-form-item label="关键字">
<el-input v-model="query.keyword" placeholder="用户名称" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</template>
<template #cell-actions="{ row }">
<el-button size="small" @click="edit(row)">编辑</el-button>
</template>
</TablePro>
</el-card>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import TablePro from '../components/TablePro.vue'
const columns = [
{ key: 'id', label: 'ID', width: 80 },
{ key: 'name', label: '姓名' },
{ key: 'phone', label: '手机' },
{ key: 'actions', label: '操作', width: 160, align: 'center' }
]
const query = reactive({ keyword: '' })
const rows = ref<any[]>([])
const loading = ref(false)
const pagination = ref({ page: 1, pageSize: 10, total: 0 })
function onPage(v: { page: number; pageSize: number }) {
pagination.value.page = v.page
pagination.value.pageSize = v.pageSize
fetchList()
}
function reset() {
query.keyword = ''
pagination.value.page = 1
fetchList()
}
async function fetchList() {
loading.value = true
try {
// 替换为真实接口
const total = 42
const start = (pagination.value.page - 1) * pagination.value.pageSize
const end = start + pagination.value.pageSize
const data = Array.from({ length: total }).map((_, i) => ({ id: i + 1, name: `用户${i + 1}`, phone: '13800000000' }))
rows.value = data.slice(start, end)
pagination.value.total = total
} finally {
loading.value = false
}
}
function edit(row: any) {
console.log('edit', row)
}
fetchList()
</script>
LLM 提示词模板(可直接用)
- 目标:生成「Vue 3 + Element Plus」的可复用表格组件与列表页骨架。
- 约束:
- props:
columns
、data
、loading
、pagination
- emits:
update:pagination
、row-click
、selection-change
、refresh
- slots:
toolbar
、cell-[columnKey]
- 使用组合式 API 与 TypeScript,组件不写死接口调用。
- props:
- 输出:组件源码、使用示例(列表页),以及最小
main.ts
接入说明。
示例提示词:
请用 Vue 3 + Element Plus 写一个通用表格组件 TablePro.vue,满足上述 props/emits/slots 约束,并给出一个在列表页中使用它的示例。列表页包含搜索区域(关键字)、分页、刷新按钮,不写死接口,仅演示事件流与数据拼装。
实践路径与常见坑
- 路线:先定义后端接口契约 → 生成组件骨架 → 接好 Axios → 页面拼装。
- 常见坑:
- Vue 3 用 Element Plus(非 Element UI)。
- 全局样式污染,尽量使用局部样式或 CSS 变量。
- 表格数据量大时优先服务端分页,需要时做行虚拟化。
- 表单复杂时拆分子组件,避免巨型组件。