news 2026/9/14 14:16:05

Vue3 + Element-Plus 图书管理系统开发实战:从工程化到组件复用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3 + Element-Plus 图书管理系统开发实战:从工程化到组件复用

简介:基于Vue3与Element-Plus构建的图书管理系统源码,面向中高级前端开发者或需要快速搭建管理后台的工程师,旨在提供一套完整可用的图书信息增删改查、借阅管理以及用户登录验证的解决方案。压缩包共33个文件,含14个Vue组件、10个JavaScript脚本、2个JSON配置文件,以及HTML、图片、图标等辅助资源,整体大小2.41MB;Vue组件负责页面交互与数据展示,JS脚本实现业务逻辑,JSON用于系统配置,目录结构清晰便于定位修改。技术选型采用vue-router与vue-pinia,展示了路由管理和状态管理的现代实践,有助于开发者理解组件化开发与响应式数据流,也方便后端人员快速上手前端。借助Element-Plus组件库,界面美观且开发高效,既适合课程设计与毕设参考,也适合企业后台模块复用。目前已有384人学习下载,源码可灵活定制扩展,具备较高的参考价值。

1. 为什么图书管理系统值得用 Vue3 和 Element-Plus 重写一套

很多老教材里的图书管理系统还停在 jQuery 或原生 JavaScript 阶段,单页表单加几个 getElementById 就能交差,但要扩展一个读者借阅记录、一个逾期统计,代码就撑不住了。用 Vue3 与 Element-Plus 重做这套系统,本质不是换皮肤,而是把数据流、组件边界和接口层一次理清。图书管理系统的实体关系非常标准——图书、读者、借阅记录、分类,正好用来演示组合式 API 怎么组织业务逻辑、表格和表单怎么高效联动。适合的人群也很明确:正在做课程设计的学生、想拿 Vue3 练手的前端新人,以及需要给内部项目快速搭后台管理界面的工程师。这套方案不需要后端团队配合,先用 Mock 数据打通全部页面,后续对接真实接口只改一层请求封装。

2. 搭建 Vue3 与 Element-Plus 的开发底座:从安装到源码目录规划

所谓“设计源码”,大多数人拿到的只是一个能跑的前端工程,后续自己改功能、加页面时才发现目录乱成一团。与其等代码写坏了再重构,不如在初始化阶段就把目录边界定下来。图书管理系统的规模不大,但该有的分层不能少:页面组件只负责渲染和交互,业务逻辑放进组合式函数,接口请求全部走统一封装,这样才能保证读者管理、借阅管理这些后续模块往里面加的时候不动老代码。

2.1 用 Vite 初始化项目并安装 Element-Plus

初始化用 Vite 是当前 Vue3 项目最稳妥的选择,比 Vue CLI 启动更快,依赖也更干净。打开终端执行:

npm create vite@latest book-manager -- --template vue cd book-manager npm install npm install element-plus @element-plus/icons-vue npm install vue-router@4 pinia axios

模板参数里的vue指定使用 Vue3 官方模板,生成的是组合式 API 风格的工程。Element-Plus 的图标是独立包,注册方式跟组件库本体分开,后文会说明具体注册代码。vue-router 必须用 4.x 版本,Pinia 是 Vue3 官方推荐的状态管理库,替代 Vue2 时代的 Vuex。axios 用来发请求,图书管理系统的增删改查都通过它跟后端通信,本地开发阶段则用 Mock 拦截。

2.2 按图书业务划分源码目录

初始模板生成的 src 目录只有 main.js 和 App.vue,不直接够用。参考后台管理项目常见的分层方式,我一般会把业务拆成下面这样:

src/ api/ // 接口请求定义,每个模块一个文件 assets/ // 静态资源 components/ // 通用组件,如图书封面上传、分页封装 composables/ // 组合式函数,如 useBookList、useBorrowRecord layout/ // 后台布局,侧边栏 + 顶部导航 + 主内容区 router/ // 路由配置与守卫 stores/ // Pinia 状态,如用户信息、借阅状态缓存 views/ // 页面组件:图书管理、读者管理、借阅管理、统计 utils/ // 工具函数,如日期格式化、ISBN 校验 mock/ // Mock 数据与拦截规则

这段目录里,composables 是 Vue3 相对 Vue2 变化最大的一层。以前图书增删改查的逻辑散落在各个 methods 里,现在用useBookList这样的函数把这些逻辑收拢,多个页面可以复用同一份代码。api 层单独抽出来是因为图书管理系统的接口路径有规律,比如/api/books/api/readers,统一放一起方便改后端地址。

2.3 配置路由、状态管理并挂载 Element-Plus

入口文件需要同时完成 Element-Plus 的注册和路由、状态管理的挂载:

// src/main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus, { locale: zhCn }) app.mount('#app')

locale: zhCn是 Element-Plus 的常用配置项,组件的内置文案比如分页器的“共 X 条”、日期选择器的月份名称,默认是英文,不设置的话页面会混着中英文。Element-Plus 这种全量引入方式对图书管理系统这种中小型项目足够,打包体积问题在第五章单独讲优化。

路由配置按后台管理布局走,侧边栏菜单对应路由表的 children:

// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('../layout/Index.vue'), redirect: '/books', children: [ { path: 'books', name: 'BookList', component: () => import('../views/book/BookList.vue') }, { path: 'readers', name: 'ReaderList', component: () => import('../views/reader/ReaderList.vue') }, { path: 'borrow', name: 'BorrowList', component: () => import('../views/borrow/BorrowList.vue') } ] } ] export default createRouter({ history: createWebHistory(), routes })

routes 里的组件都用动态导入,这样路由懒加载自动生效,首屏只加载当前页面需要的代码。图书管理系统一般有三个核心路由就够起步,后续加统计分析页面时往 children 里追加即可。

3. 用 Element-Plus 组件把图书管理核心页面做厚

组件库的价值不在把页面拼出来,而是把表格分页、表单校验这些重复劳动降到最低。图书管理系统最核心的页面是图书列表和图书编辑弹窗,这两块做扎实,读者管理、借阅记录页面就是换个数据字段的事。

3.1 图书列表页:表格、分页与检索条件联动

图书列表页是整个系统的门面,涉及 Element-Plus 的 el-table、el-pagination、el-form 三组件的协作。先看模板部分:

<template> <el-card shadow="never"> <el-form :model="queryParams" inline> <el-form-item label="书名"> <el-input v-model="queryParams.title" placeholder="模糊搜索" clearable /> </el-form-item> <el-form-item label="分类"> <el-select v-model="queryParams.category" placeholder="全部" clearable> <el-option v-for="c in categories" :key="c" :label="c" :value="c" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="resetSearch">重置</el-button> </el-form-item> </el-form> <el-table :data="bookList" v-loading="loading" border> <el-table-column prop="isbn" label="ISBN" width="150" /> <el-table-column prop="title" label="书名" min-width="200" show-overflow-tooltip /> <el-table-column prop="author" label="作者" width="120" /> <el-table-column prop="category" label="分类" width="100" /> <el-table-column prop="stock" label="库存" width="80" align="center" /> <el-table-column label="操作" width="180" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openEditor(row)">编辑</el-button> <el-button link type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" @change="fetchList" /> </el-card> </template>

表格列里show-overflow-tooltip是一个容易忽略的属性,书名过长时自动截断,鼠标悬停显示完整书名,避免表格被撑乱。分页组件用v-model:current-pagev-model:page-size双向绑定查询参数,这是 Element-Plus 分页器标准的写法,页码或每页条数变化时触发 change 事件重新拉数据。

// src/views/book/BookList.vue 脚本部分 import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getBookList, deleteBook } from '../../api/book' const bookList = ref([]) const total = ref(0) const loading = ref(false) const categories = ['文学', '历史', '计算机', '科学'] const queryParams = reactive({ title: '', category: '', page: 1, pageSize: 10 }) const fetchList = async () => { loading.value = true try { const { rows, total: count } = await getBookList(queryParams) bookList.value = rows total.value = count } finally { loading.value = false } } const handleSearch = () => { queryParams.page = 1 // 重置页码,避免搜到空页 fetchList() } const resetSearch = () => { queryParams.title = '' queryParams.category = '' queryParams.page = 1 fetchList() } onMounted(fetchList)

loading用 ref 而不是 reactive,因为页面只有这一个加载状态,ref 语义更清晰。查询时先重置页码是常见坑位:不重置的话,当前在第 5 页,输入书名搜索后第 5 页可能只有 0 条数据,用户以为搜不到,实际上数据在第 1 页。删除操作前要二次确认,直接调用 Element-Plus 的 ElMessageBox:

const handleDelete = async (row) => { await ElMessageBox.confirm(`确认删除《${row.title}》?`, '删除确认', { type: 'warning' }) await deleteBook(row.id) ElMessage.success('删除成功') // 当前页只剩一条数据时自动回退一页 if (bookList.value.length === 1 && queryParams.page > 1) { queryParams.page -= 1 } fetchList() }

删除后处理当前页最后一条数据是一个细节逻辑,不处理的话删除后页码不变,页面会短暂空白。这是真实使用中经常会遇到的体验问题。

3.2 图书新增与编辑的弹窗表单和校验规则

图书的新增和编辑共用一个 el-dialog 对话框,通过弹窗标题和表单初始值区分两者。Element-Plus 的表单校验依赖 el-form 的 model、rules 和 el-form-item 的 prop 属性:

<el-dialog v-model="dialogVisible" :title="form.id ? '编辑图书' : '新增图书'" width="520px" destroy-on-close> <el-form ref="formRef" :model="form" :rules="rules" label-width="80px"> <el-form-item label="ISBN" prop="isbn"> <el-input v-model="form.isbn" placeholder="13 位数字" /> </el-form-item> <el-form-item label="书名" prop="title"> <el-input v-model="form.title" /> </el-form-item> <el-form-item label="分类" prop="category"> <el-select v-model="form.category" placeholder="请选择"> <el-option v-for="c in categories" :key="c" :label="c" :value="c" /> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="handleSubmit">保存</el-button> </template> </el-dialog>

destroy-on-close属性值得单独说明:弹窗关闭时销毁内容,下次打开重新创建表单组件。图书编辑和新增如果共用同一个 form 对象,不销毁的话上一次的编辑数据会残留在表单里,出现“新增时表单已经填了上一本书的内容”这种问题。校验规则放在脚本里:

const rules = { isbn: [ { required: true, message: '请输入 ISBN', trigger: 'blur' }, { pattern: /^\d{13}$/, message: 'ISBN 必须为 13 位数字', trigger: 'blur' } ], title: [ { required: true, message: '请输入书名', trigger: 'blur' }, { min: 1, max: 50, message: '书名长度不能超过 50 字', trigger: 'blur' } ], category: [{ required: true, message: '请选择分类', trigger: 'change' }] }

trigger的取值有讲究:输入类组件用blur,下拉选择类组件用change。写成 blur 对 select 也生效,但用户选完分类后焦点不离开,错误提示不会消失,体验割裂。提交时先调用表单组件暴露的 validate 方法,全部通过再走接口:

const handleSubmit = async () => { await formRef.value.validate() if (form.value.id) { await updateBook(form.value) } else { await createBook(form.value) } ElMessage.success(form.value.id ? '更新成功' : '新增成功') dialogVisible.value = false fetchList() }

validate 返回的是 Promise,校验不通过时抛异常,配合 await 可以直接中断后面的提交逻辑,不需要手动判断返回值。

3.3 借阅与归还的状态字段设计和状态流转

图书借阅是比增删改查再多一层业务状态的功能。数据库或 Mock 数据里,借阅记录通常用 status 字段标识状态,前端根据状态渲染不同的标签和操作按钮:

status含义标签颜色可执行操作
0借出中warning归还
1已归还success查看详情
2已逾期danger归还并记罚金

模板里用 el-tag 根据状态渲染标签,操作列按状态决定显示哪些按钮:

<el-table-column label="状态" width="100" align="center"> <template #default="{ row }"> <el-tag :type="statusMap[row.status].type">{{ statusMap[row.status].label }}</el-tag> </template> </el-table-column> <el-table-column label="操作" width="120"> <template #default="{ row }"> <el-button v-if="row.status === 0 || row.status === 2" link type="primary" @click="handleReturn(row)"> 归还 </el-button> </template> </el-table-column>

状态驱动按钮显隐是一种比写死在接口里更灵活的设计。后端接口通常不做“能否归还”的判断,前端根据 status 控制入口,后端再校验一次,两层配合保障数据安全。归还操作的接口调用和提示在此不做展开,重点在于理解了状态机设计之后,逾期计算、罚金记录都能顺着这个模型扩展。

4. 接口层设计:Axios 封装、Mock 数据与组件复用

图书管理系统的前端源码里,接口层往往是初学者最容易忽略的地方。几乎所有页面都需要发请求,如果把 axios 直接写进每个组件,后端地址改一次要动十几个文件。封装请求层、用 Mock 模拟真实接口,是让整套源码具备后续开发能力的关键。

4.1 用 Axios 统一封装请求与错误处理

在 utils 目录建 request.js,创建 axios 实例并配置拦截器:

// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带 token request.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误码 request.interceptors.response.use( (response) => { const { code, data, message } = response.data if (code === 0) { return data } ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) }, (error) => { ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request

这里把后端返回结构约定为{ code, data, message },code 为 0 表示成功。拦截器的好处是页面代码不用每次判断 code,直接拿到 data 使用。token 从 localStorage 读取是一种常见做法,图书管理系统如果需要登录鉴权,这个位置是统一的注入点。timeout 设为 10 秒,防止接口挂起时页面一直转圈。

封装好请求实例后,api 模块里的代码就非常薄了:

// src/api/book.js import request from '../utils/request' export const getBookList = (params) => request.get('/books', { params }) export const createBook = (data) => request.post('/books', data) export const updateBook = (data) => request.put(`/books/${data.id}`, data) export const deleteBook = (id) => request.delete(`/books/${id}`)

接口函数只做参数透传和路径拼接,业务层的调用方不感知 HTTP 细节。新增图书用 POST,更新用 PUT,删除用 DELETE,借阅记录模块的接口同理。

4.2 用 vite-plugin-mock 模拟后端数据

没有后端时,Mock 层决定了整套源码能不能跑起来。vite-plugin-mock 是 Vite 生态里配置最轻量的方案,安装后在 vite.config.js 里启用:

npm install vite-plugin-mock -D
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { viteMockServe } from 'vite-plugin-mock' export default defineConfig({ plugins: [ vue(), viteMockServe({ mockPath: 'src/mock', localEnabled: true, prodEnabled: false }) ] })

mockPath 指定存放 Mock 规则的文件目录,localEnabled 为 true 表示本地开发环境开启拦截。在 src/mock 目录创建 book.js,写图书列表的模拟接口:

// src/mock/book.js export default [ { url: '/api/books', method: 'get', response: ({ query }) => { const allBooks = [ { id: 1, isbn: '9787111213826', title: '深入理解计算机系统', author: 'Randal E.Bryant', category: '计算机', stock: 5 }, { id: 2, isbn: '9787115428028', title: '算法导论', author: 'Thomas H. Cormen', category: '计算机', stock: 3 } ] const filtered = allBooks.filter((book) => { const matchTitle = !query.title || book.title.includes(query.title) const matchCategory = !query.category || book.category === query.category return matchTitle && matchCategory }) return { code: 0, data: { rows: filtered, total: filtered.length } } } } ]

Mock 文件里写模拟数据时,最好把过滤逻辑也模拟出来。否则前端分页查询调通了,换成真实后端时才发现条件过滤根本没实现。这里用了数组的 filter 方法分别匹配 title 的模糊查询和 category 的精确匹配,与前端查询参数的字段一一对应。

4.3 组件复用与源码可维护性

图书管理系统里有两类组件值得从页面里抽离:一类是跨页面复用的,如图书封面选择器;另一类是逻辑复杂到影响页面阅读的,如 ISBN 自动格式化输入框。抽离时用 defineProps 和 defineEmits 定义接口:

<!-- components/IsbnInput.vue --> <script setup> const props = defineProps({ modelValue: { type: String, default: '' } }) const emit = defineEmits(['update:modelValue']) const handleInput = (value) => { // 过滤非数字字符并限制 13 位 const cleaned = value.replace(/\D/g, '').slice(0, 13) emit('update:modelValue', cleaned) } </script> <template> <el-input :model-value="modelValue" @input="handleInput" placeholder="仅允许数字" /> </template>

这个组件用modelValueupdate:modelValue实现 v-model 协议,父组件可以像使用原生输入框一样使用它。抽离后图书编辑表单、读者证号输入框都能复用 ISBN 的数字过滤逻辑,这是 Element-Plus 表单体系里做自定义输入组件最常见的模式。

组件/工具位置职责
request.jssrc/utilsaxios 实例、拦截器、统一错误处理
useBookListsrc/composables列表加载、分页、搜索逻辑
IsbnInputsrc/componentsISBN 输入格式化
借阅状态映射src/utils/borrowStatus.js状态到标签文案/颜色的映射

5. 构建优化与源码交付前必须处理的三个坑

图书管理系统做完功能后不能着急交付,还有几个问题会直接影响别人拿到源码能不能跑起来。第一关是构建配置:MemElement-Plus 全量引入会让打包体积接近 1MB,用按需引入能瘦身一半以上。推荐用 unplugin-vue-components 和 unplugin-auto-import 两个 Vite 插件自动按需加载组件。另外把 vue、vue-router、pinia 单独拆出 vendor 块,让浏览器缓存公共依赖,二次加载更快。

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ], build: { rollupOptions: { output: { manualChunks: { vue: ['vue', 'vue-router', 'pinia'] } } } } })

配置之后无需在 main.js 里全量注册 Element-Plus,组件和 API 会在使用时自动导入,注意 main.js 里的全量注册代码要删掉,否则体积优化的效果会打折扣。

第二个常见坑是路由刷新后 404。用 createWebHistory 模式时,把打包产物部署到 nginx 根路径以外的目录,或者直接双击 index.html 打开,刷新就会白屏。nignx 里加一行 try_files 即可解决:

location / { try_files $uri $uri/ /index.html; }

第三个坑是 Element-Plus 的中文语言包只在全局注册时生效,按需引入后 el-pagination 的分页文案会退回英文。需要在入口文件里单独配置 ConfigProvider,或者在使用 el-config-provider 包裹根组件,传入 zh-cn 语言包。这些细节排查完,再对照图书列表增删改查、借书还书流程、搜索分页组合三个场景逐项验证,这套 Vue3 与 Element-Plus 的图书管理系统源码就可以正常交给下一个开发者了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:13:45

SpringBoot+Vue美食评测系统全栈实践指南

简介&#xff1a;本资源是一套面向Java全栈开发初学者与课程设计者的美食评测系统完整实践方案&#xff0c;聚焦人工智能在信息化管理中的落地应用&#xff0c;解决餐饮领域用户评价、菜品检索与个性化推荐等典型业务问题。压缩包共667个文件&#xff0c;含190个Java后端逻辑代…

作者头像 李华