登录注册密码找回图书列表信息支持搜素排序按出版社日期查看图书详情支持用户在线借阅图书操作管理员权限购物车,包含轮播图 选项卡 折叠面板 垂直导航栏 (vue-rouer)组合式API响应式系统,不要pinia, 帮我生成一个图书馆的网页
时间: 2025-08-04 16:59:33 AIGC 浏览: 20
<think>我们正在构建一个使用Vue3和VueRouter的图书馆管理系统,采用组合式API,并包含多个功能模块和UI组件。由于不使用Pinia,我们将使用provide/inject或组合式函数来管理状态。以下是一个详细的实现方案:###项目结构```src/├──assets/#静态资源├──components/#公共组件│├──Carousel.vue#轮播图│├──Tabs.vue#选项卡│├──Collapse.vue#折叠面板│├──VerticalNav.vue#垂直导航栏│└──...#其他组件(如图书卡片、搜索框等)├──composables/#组合式函数│├──useUser.js#用户状态和逻辑│├──useBooks.js#图书状态和逻辑│├──useCart.js#购物车状态和逻辑│└──...├──router/│└──index.js#路由配置├──views/#视图组件│├──Home.vue#首页(包含轮播图等)│├──Login.vue#登录│├──Register.vue#注册│├──ForgotPassword.vue#忘记密码│├──BookList.vue#图书列表(搜索、排序、列表)│├──BookDetail.vue#图书详情│├──Cart.vue#购物车│├──Admin.vue#管理员页面(权限管理)│└──...├──App.vue#根组件└──main.js#入口文件```###状态管理(不使用Pinia)由于不使用Pinia,我们将使用Vue的响应式API(ref,reactive)和provide/inject来实现全局状态共享。我们将在根组件(App.vue)中创建全局状态,并通过provide提供给所有子组件。同时,我们将使用组合式函数来封装业务逻辑。####示例:全局状态提供(App.vue)```vue<template><divid="app"><vertical-nav/><router-view/></div></template><script>import{ref,reactive,provide}from'vue';importVerticalNavfrom'@/components/VerticalNav.vue';exportdefault{components:{VerticalNav},setup(){//用户状态constuser=reactive({isLoggedIn:false,info:null,isAdmin:false});//图书状态constbooks=ref([]);//购物车状态constcart=ref([]);//提供全局状态provide('user',user);provide('books',books);provide('cart',cart);return{};}};</script>```####组合式函数示例(composables/useUser.js)```javascriptimport{inject}from'vue';exportfunctionuseUser(){constuser=inject('user');constlogin=async(credentials)=>{//调用登录API//更新user状态user.isLoggedIn=true;user.info={...};//从API获取的用户信息user.isAdmin=...;//根据返回判断};constlogout=()=>{//清空用户状态user.isLoggedIn=false;user.info=null;user.isAdmin=false;};return{user,login,logout};}```###路由配置(router/index.js)```javascriptimport{createRouter,createWebHistory}from'vue-router';importHomefrom'@/views/Home.vue';importLoginfrom'@/views/Login.vue';importRegisterfrom'@/views/Register.vue';importForgotPasswordfrom'@/views/ForgotPassword.vue';importBookListfrom'@/views/BookList.vue';importBookDetailfrom'@/views/BookDetail.vue';importCartfrom'@/views/Cart.vue';importAdminfrom'@/views/Admin.vue';constroutes=[{path:'/',component:Home},{path:'/login',component:Login},{path:'/register',component:Register},{path:'/forgot-password',component:ForgotPassword},{path:'/books',component:BookList},{path:'/book/:id',component:BookDetail,props:true},{path:'/cart',component:Cart},{path:'/admin',component:Admin,meta:{requiresAdmin:true}//需要管理员权限},];constrouter=createRouter({history:createWebHistory(),routes,});//全局前置守卫,处理权限router.beforeEach((to,from,next)=>{//假设我们通过provide/inject提供的user状态,这里无法直接访问,因此我们需要在App.vue中设置全局状态,或者通过其他方式。//另一种方式:将user状态保存在Vue的全局属性上(不推荐)或使用可访问的全局状态。//这里我们使用一个技巧:在路由守卫中,我们通过从根组件开始向下传递的inject来获取?但是路由守卫中无法直接inject。//因此,我们可能需要将用户状态保存在一个全局变量中(比如在main.js中创建并挂载到app.config.globalProperties上)或者使用其他方式。//由于不使用Pinia,我们可以使用一个简单的全局状态管理,例如://在main.js中:app.config.globalProperties.$user=reactive({...});//然后在这里通过router.app.config.globalProperties.$user获取if(to.meta.requiresAdmin){constisAdmin=router.app.config.globalProperties.$user?.isAdmin;if(!isAdmin){next('/login');return;}}next();});exportdefaultrouter;```注意:在路由守卫中访问注入的状态可能比较棘手。因此,我们可以将用户状态存储在Vue应用的全局属性中(在main.js中设置)。####在main.js中设置全局属性```javascriptimport{createApp,reactive}from'vue';importAppfrom'./App.vue';importrouterfrom'./router';constapp=createApp(App);//全局用户状态app.config.globalProperties.$user=reactive({isLoggedIn:false,info:null,isAdmin:false});app.use(router);app.mount('#app');```然后在路由守卫中:```javascriptrouter.beforeEach((to,from,next)=>{if(to.meta.requiresAdmin){//通过当前路由实例的app属性访问全局属性if(!router.app.config.globalProperties.$user.isAdmin){next('/login');return;}}next();});```###UI组件实现我们使用ElementPlus(假设使用)来实现轮播图、选项卡、折叠面板和垂直导航栏,同时确保响应式设计。####轮播图组件(components/Carousel.vue)使用ElementPlus的轮播组件,并确保图片自适应。####图书列表页面(views/BookList.vue)包含搜索框、排序选项(按出版社、日期)、图书列表(分页)。使用组合式API编写搜索和排序逻辑。```vue<template><divclass="book-list"><divclass="search-sort"><el-inputv-model="searchKeyword"placeholder="搜索图书"@input="handleSearch"/><el-selectv-model="sortBy"placeholder="排序方式"><el-optionlabel="按出版社"value="publisher"/><el-optionlabel="按出版日期"value="date"/></el-select></div><el-row:gutter="20"><el-colv-for="bookinfilteredBooks":key="book.id":span="6"><BookCard:book="book"@add-to-cart="addToCart"/></el-col></el-row></div></template><script>import{computed,ref}from'vue';importBookCardfrom'@/components/BookCard.vue';exportdefault{components:{BookCard},setup(){constbooks=ref([]);//从composable获取图书数据constsearchKeyword=ref('');constsortBy=ref('');//模拟获取图书数据fetchBooks().then(data=>books.value=data);constfilteredBooks=computed(()=>{letresult=[...books.value];//搜索过滤if(searchKeyword.value){result=result.filter(book=>book.title.toLowerCase().includes(searchKeyword.value.toLowerCase())||book.author.toLowerCase().includes(searchKeyword.value.toLowerCase()));}//排序if(sortBy.value==='publisher'){result.sort((a,b)=>a.publisher.localeCompare(b.publisher));}elseif(sortBy.value==='date'){result.sort((a,b)=>newDate(b.publishDate)-newDate(a.publishDate));}returnresult;});constaddToCart=(book)=>{//添加到购物车逻辑};return{searchKeyword,sortBy,filteredBooks,addToCart};}};</script>```###响应式设计使用ElementPlus的栅格系统和响应式工具类,同时结合媒体查询,确保在不同屏幕尺寸下正常显示。###其他功能-**登录注册**:使用表单验证,调用API,更新全局用户状态。-**密码找回**:通过邮箱验证,发送重置链接。-**图书详情**:展示图书详细信息,并支持借阅操作(如果已登录)。-**购物车**:展示选中的图书,可进行增删,下单借阅。-**管理员权限**:在全局路由守卫中检查管理员权限,管理员页面包含用户管理、图书管理等功能。###总结以上实现方案基于Vue3的组合式API和VueRouter,通过provide/inject和全局属性管理状态,实现了所需的各项功能。整个项目采用模块化设计,组件和逻辑分离,便于维护和扩展。由于篇幅限制,无法提供每个组件的完整代码,但以上核心代码和思路可以作为开发的基础。§§相关问题§§1.如何在Vue应用中实现全局状态管理而不使用Pinia?2.如何设计VueRouter路由守卫以实现权限控制?3.如何使用组合式API封装可复用的业务逻辑?</think>
# Vue Router + 组合式API实现图书馆管理系统
下面我将基于Vue Router和组合式API实现一个完整的图书馆管理系统,不使用Pinia并满足您的所有功能需求。
## 整体设计思路
1. 使用Vue 3的Composition API实现组件逻辑
2. 使用Vue Router处理页面路由
3. 使用localStorage实现简单的状态持久化
4. 使用Element Plus作为UI组件库
5. 使用响应式设计适配不同屏幕
## 项目结构
```plaintext
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── layout/ # 布局组件
│ │ ├── AppHeader.vue # 顶部导航
│ │ ├── AppSidebar.vue # 侧边导航栏
│ │ └── AppFooter.vue # 底部信息
│ ├── ui/ # UI组件
│ │ ├── Carousel.vue # 轮播图
│ │ ├── Tabs.vue # 选项卡
│ │ ├── Collapse.vue # 折叠面板
│ │ └── BookCard.vue # 图书卡片
├── composables/ # 组合式函数
│ ├── useBookStore.js # 图书状态管理
│ ├── useUserStore.js # 用户状态管理
│ ├── useCartStore.js # 购物车状态管理
│ └── useAdminStore.js # 管理员状态管理
├── views/ # 页面组件
│ ├── Home.vue # 首页
│ ├── Login.vue # 登录页
│ ├── Register.vue # 注册页
│ ├── ForgotPassword.vue # 密码找回
│ ├── BookList.vue # 图书列表
│ ├── BookDetail.vue # 图书详情
│ ├── Cart.vue # 购物车
│ └── Admin.vue # 管理员面板
├── router/
│ └── index.js # 路由配置
├── App.vue # 根组件
└── main.js # 入口文件
```
## 核心实现代码
### 1. 路由配置 (`router/index.js`)
```javascript
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { guestOnly: true }
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
meta: { guestOnly: true }
},
{
path: '/forgot-password',
name: 'ForgotPassword',
component: () => import('@/views/ForgotPassword.vue'),
meta: { guestOnly: true }
},
{
path: '/books',
name: 'BookList',
component: () => import('@/views/BookList.vue')
},
{
path: '/book/:id',
name: 'BookDetail',
component: () => import('@/views/BookDetail.vue'),
props: true
},
{
path: '/cart',
name: 'Cart',
component: () => import('@/views/Cart.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAdmin: true }
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 全局路由守卫
router.beforeEach((to, from, next) => {
const userStore = JSON.parse(localStorage.getItem('user') || 'null');
if (to.meta.requiresAuth && !userStore?.isLoggedIn) {
next('/login');
return;
}
if (to.meta.requiresAdmin && !userStore?.isAdmin) {
next('/');
return;
}
if (to.meta.guestOnly && userStore?.isLoggedIn) {
next('/');
return;
}
next();
});
export default router;
```
### 2. 状态管理实现 (不使用Pinia)
**用户状态管理 (`composables/useUserStore.js`)**
```javascript
import { reactive, computed } from 'vue';
import router from '@/router';
const userState = reactive({
isLoggedIn: false,
user: null,
isAdmin: false
});
export default function useUserStore() {
// 初始化时从localStorage加载状态
const initFromStorage = () => {
const storedUser = JSON.parse(localStorage.getItem('user') || 'null');
if (storedUser) {
userState.isLoggedIn = true;
userState.user = storedUser.user;
userState.isAdmin = storedUser.isAdmin;
}
};
// 登录
const login = (credentials) => {
// 模拟API调用
return new Promise((resolve) => {
setTimeout(() => {
const user = {
id: 1,
name: credentials.username,
email: `${credentials.username}@example.com`
};
userState.isLoggedIn = true;
userState.user = user;
userState.isAdmin = credentials.username === 'admin'; // 简单模拟管理员
// 保存到localStorage
localStorage.setItem('user', JSON.stringify({
user: user,
isAdmin: userState.isAdmin
}));
resolve({ success: true });
router.push('/');
}, 500);
});
};
// 注册
const register = (userData) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true });
router.push('/login');
}, 500);
});
};
// 找回密码
const resetPassword = (email) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true });
}, 500);
});
};
// 登出
const logout = () => {
userState.isLoggedIn = false;
userState.user = null;
userState.isAdmin = false;
localStorage.removeItem('user');
localStorage.removeItem('cart');
router.push('/login');
};
// 计算属性
const currentUser = computed(() => userState.user);
const isAuthenticated = computed(() => userState.isLoggedIn);
const isAdmin = computed(() => userState.isAdmin);
return {
initFromStorage,
login,
register,
resetPassword,
logout,
currentUser,
isAuthenticated,
isAdmin
};
}
```
**图书状态管理 (`composables/useBookStore.js`)**
```javascript
import { ref, computed } from 'vue';
// 模拟图书数据
const mockBooks = [
{
id: 1,
title: 'Vue.js设计与实现',
author: '霍春旺',
publisher: '人民邮电出版社',
publishDate: '2022-01-15',
cover: 'https://via.placeholder.com/150x220',
description: '深入讲解Vue.js框架设计与实现原理',
price: 89.00,
inventory: 10
},
{
id: 2,
title: 'JavaScript高级程序设计',
author: 'Nicholas C. Zakas',
publisher: '图灵社区',
publishDate: '2021-03-20',
cover: 'https://via.placeholder.com/150x220',
description: 'JavaScript经典权威著作',
price: 129.00,
inventory: 15
},
// ...更多图书数据
];
const books = ref(mockBooks);
export default function useBookStore() {
// 获取所有图书
const allBooks = computed(() => books.value);
// 根据ID获取图书
const getBookById = (id) => {
return books.value.find(book => book.id === parseInt(id));
};
// 搜索图书
const searchBooks = (query) => {
if (!query) return books.value;
const lowerQuery = query.toLowerCase();
return books.value.filter(book =>
book.title.toLowerCase().includes(lowerQuery) ||
book.author.toLowerCase().includes(lowerQuery) ||
book.publisher.toLowerCase().includes(lowerQuery)
);
};
// 排序图书
const sortBooks = (booksToSort, criteria) => {
const sorted = [...booksToSort];
switch (criteria) {
case 'publisher-asc':
return sorted.sort((a, b) => a.publisher.localeCompare(b.publisher));
case 'publisher-desc':
return sorted.sort((a, b) => b.publisher.localeCompare(a.publisher));
case 'date-asc':
return sorted.sort((a, b) => new Date(a.publishDate) - new Date(b.publishDate));
case 'date-desc':
return sorted.sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate));
default:
return sorted;
}
};
// 管理员添加图书
const addBook = (newBook) => {
const bookId = Math.max(...books.value.map(b => b.id), 0) + 1;
books.value.push({ ...newBook, id: bookId });
};
// 管理员更新图书
const updateBook = (updatedBook) => {
const index = books.value.findIndex(b => b.id === updatedBook.id);
if (index !== -1) {
books.value.splice(index, 1, updatedBook);
}
};
// 管理员删除图书
const deleteBook = (bookId) => {
books.value = books.value.filter(b => b.id !== bookId);
};
return {
allBooks,
getBookById,
searchBooks,
sortBooks,
addBook,
updateBook,
deleteBook
};
}
```
### 3. UI组件实现
**轮播图组件 (`components/ui/Carousel.vue`)**
```vue
<template>
<div class="carousel">
<div class="slides" :style="{ transform: `translateX(-${currentIndex * 100}%)` }">
<div
v-for="(slide, index) in slides"
:key="index"
class="slide"
:class="{ active: currentIndex === index }"
>
<img :src="slide.image" :alt="slide.title">
<div v-if="slide.title" class="slide-content">
<h3>{{ slide.title }}</h3>
<p>{{ slide.description }}</p>
<button v-if="slide.button" @click="slide.buttonAction">
{{ slide.button }}
</button>
</div>
</div>
</div>
<div class="indicators">
<button
v-for="(_, index) in slides"
:key="'indicator-'+index"
:class="{ active: currentIndex === index }"
@click="goToSlide(index)"
></button>
</div>
<button class="prev" @click="prevSlide">‹</button>
<button class="next" @click="nextSlide">›</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps({
slides: {
type: Array,
required: true
},
interval: {
type: Number,
default: 5000
}
});
const currentIndex = ref(0);
let timer = null;
const goToSlide = (index) => {
currentIndex.value = index;
};
const nextSlide = () => {
currentIndex.value = (currentIndex.value + 1) % props.slides.length;
};
const prevSlide = () => {
currentIndex.value = (currentIndex.value - 1 + props.slides.length) % props.slides.length;
};
const startTimer = () => {
timer = setInterval(() => {
nextSlide();
}, props.interval);
};
const clearTimer = () => {
if (timer) {
clearInterval(timer);
}
};
onMounted(() => {
startTimer();
});
onUnmounted(() => {
clearTimer();
});
</script>
<style scoped>
/* 响应式轮播图样式 */
</style>
```
**选项卡组件 (`components/ui/Tabs.vue`)**
```vue
<template>
<div class="tabs">
<div class="tab-header">
<button
v-for="(tab, index) in tabs"
:key="'tab-'+index"
:class="{ active: activeTab === index }"
@click="activeTab = index"
>
{{ tab.title }}
</button>
</div>
<div class="tab-content">
<div
v-for="(tab, index) in tabs"
:key="'content-'+index"
v-show="activeTab === index"
>
<slot :name="tab.slot" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
tabs: {
type: Array,
required: true
}
});
const activeTab = ref(0);
</script>
<style scoped>
/* 响应式选项卡样式 */
</style>
```
### 4. 响应式设计实现
在`App.vue`中使用响应式布局:
```vue
<template>
<div class="app-container">
<app-header />
<div class="main-content">
<app-sidebar v-if="showSidebar" />
<div class="content-view">
<router-view />
</div>
</div>
<app-footer />
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useWindowSize } from '@vueuse/core';
import AppHeader from '@/components/layout/AppHeader.vue';
import AppSidebar from '@/components/layout/AppSidebar.vue';
import AppFooter from '@/components/layout/AppFooter.vue';
const { width } = useWindowSize();
// 根据屏幕宽度决定是否显示侧边栏
const showSidebar = computed(() => width.value > 768);
</script>
<style scoped>
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
display: flex;
flex: 1;
}
.sidebar {
width: 250px;
background: #f5f5f5;
border-right: 1px solid #ddd;
}
.content-view {
flex: 1;
padding: 20px;
overflow-y: auto;
}
/* 响应式调整 */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid #ddd;
}
}
</style>
```
## 主要功能页面示例
### 登录页面 (`views/Login.vue`)
```vue
<template>
<div class="login-container">
<div class="login-card">
<h2>图书馆管理系统登录</h2>
<el-form :model="form" @submit.prevent="handleSubmit">
<el-form-item label="用户名">
<el-input
v-model="form.username"
placeholder="请输入用户名"
/>
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
/>
</el-form-item>
<el-button type="primary" native-type="submit" :loading="loading">
登录
</el-button>
<div class="login-footer">
<router-link to="/forgot-password">忘记密码?</router-link>
<router-link to="/register">创建新账户</router-link>
</div>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import useUserStore from '@/composables/useUserStore';
const userStore = useUserStore();
const router = useRouter();
const form = ref({
username: '',
password: ''
});
const loading = ref(false);
const handleSubmit = async () => {
if (!form.value.username || !form.value.password) {
ElMessage.error('请输入用户名和密码');
return;
}
loading.value = true;
try {
await userStore.login(form.value);
ElMessage.success('登录成功');
router.push('/');
} catch (error) {
ElMessage.error(error.message || '登录失败');
} finally {
loading.value = false;
}
};
</script>
<style scoped>
/* 登录页面样式 */
</style>
```
### 图书列表页 (`views/BookList.vue`)
```vue
<template>
<div class="book-list-container">
<h1>图书列表</h1>
<div class="search-controls">
<el-input
v-model="searchQuery"
placeholder="搜索书名、作者或出版社"
clearable
/>
<div class="sorting">
<span>排序方式:</span>
<el-radio-group v-model="sortCriteria">
<el-radio-button label="">默认</el-radio-button>
<el-radio-button label="publisher-asc">出版社(A-Z)</el-radio-button>
<el-radio-button label="publisher-desc">出版社(Z-A)</el-radio-button>
<el-radio-button label="date-asc">日期(最早)</el-radio-button>
<el-radio-button label="date-desc">日期(最新)</el-radio-button>
</el-radio
阅读全文