一、 系统 前端界面设计要求与效果
(一)系统功能结构图
设计一个基于Vue.js的图书管理系统前端界面。要充分体现Vue的核心特性和应用场景,同时结合信息管理专业的知识。要求系统分为仪表盘、图书管理、借阅管理和用户管理四个主要模块,每个模块有独立的功能和界面。以下是系统功能结构图:

因为是基于Vue.js的图书管理系统前端界面设计,所以只涉及到表示层,以及为了演示设计的部分数据模型和数据层的书籍信息、用户信息、借阅记录。
(二)界面的完整代码
界面的完整代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图书管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6366F1',
accent: '#F59E0B',
neutral: '#6B7280',
success: '#10B981',
warning: '#F59E0B',
danger: '#EF4444',
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.transition-custom {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.scale-hover {
transition: transform 0.2s ease-in-out;
}
.scale-hover:hover {
transform: scale(1.02);
}
}
</style>
</head>
<body class="font-inter bg-gray-50 text-gray-800 min-h-screen flex flex-col">
<div id="app">
<!-- 导航栏 -->
<nav class="bg-white shadow-md sticky top-0 z-50 transition-all duration-300" :class="{'bg-primary/95 text-white': isScrolled}">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fa fa-book text-2xl text-primary"></i>
<span class="text-xl font-bold">图书管理系统</span>
</div>
<div class="hidden md:flex items-center space-x-6">
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'dashboard'}">
<i class="fa fa-tachometer mr-1"></i>仪表盘
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'books'}">
<i class="fa fa-book mr-1"></i>图书管理
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'borrows'}">
<i class="fa fa-exchange mr-1"></i>借阅管理
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'users'}">
<i class="fa fa-users mr-1"></i>用户管理
</a>
</div>
<div class="flex items-center space-x-4">
<div class="relative hidden md:block">
<input type="text" placeholder="搜索图书..." class="pl-9 pr-4 py-2 rounded-full bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/50 w-48 transition-all duration-300 focus:w-64">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div class="relative">
<button class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center hover:bg-gray-300 transition-colors">
<i class="fa fa-user"></i>
</button>
</div>
<button class="md:hidden" @click="toggleMobileMenu">
<i class="fa fa-bars text-xl"></i>
</button>
</div>
</div>
<!-- 移动端菜单 -->
<div class="md:hidden bg-white border-t border-gray-100 shadow-lg absolute w-full left-0 transition-all duration-300 transform" :class="{'translate-y-0': isMobileMenuOpen, '-translate-y-full': !isMobileMenuOpen}">
<div class="container mx-auto px-4 py-2">
<div class="flex flex-col space-y-3 py-2">
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'dashboard'}">
<i class="fa fa-tachometer mr-2"></i>仪表盘
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'books'}">
<i class="fa fa-book mr-2"></i>图书管理
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'borrows'}">
<i class="fa fa-exchange mr-2"></i>借阅管理
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'users'}">
<i class="fa fa-users mr-2"></i>用户管理
</a>
<div class="relative">
<input type="text" placeholder="搜索图书..." class="w-full pl-9 pr-4 py-2 rounded-lg bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/50">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
</div>
</div>
</div>
</nav>
<!-- 主内容区 -->
<main class="flex-grow container mx-auto px-4 py-6">
<!-- 仪表盘视图 -->
<div v-if="currentView === 'dashboard'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">仪表盘</h1>
<p class="text-gray-600">欢迎使用图书管理系统,以下是系统概览</p>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">总藏书量</p>
<h3 class="text-3xl font-bold mt-1">{{ books.length }}</h3>
<p class="text-success text-sm mt-2">
<i class="fa fa-arrow-up"></i> 5.2% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fa fa-book text-primary text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">借出图书</p>
<h3 class="text-3xl font-bold mt-1">{{ borrowedBooksCount }}</h3>
<p class="text-danger text-sm mt-2">
<i class="fa fa-arrow-down"></i> 2.8% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center">
<i class="fa fa-exchange text-accent text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">注册用户</p>
<h3 class="text-3xl font-bold mt-1">{{ users.length }}</h3>
<p class="text-success text-sm mt-2">
<i class="fa fa-arrow-up"></i> 12.3% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-secondary/10 flex items-center justify-center">
<i class="fa fa-users text-secondary text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">逾期未还</p>
<h3 class="text-3xl font-bold mt-1">{{ overdueBooksCount }}</h3>
<p class="text-warning text-sm mt-2">
<i class="fa fa-arrow-up"></i> 3.1% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fa fa-calendar-times-o text-danger text-xl"></i>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow lg:col-span-2">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">借阅趋势</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 text-sm rounded-md bg-primary/10 text-primary">周</button>
<button class="px-3 py-1 text-sm rounded-md hover:bg-gray-100">月</button>
<button class="px-3 py-1 text-sm rounded-md hover:bg-gray-100">年</button>
</div>
</div>
<div class="h-80">
<canvas id="borrowChart"></canvas>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">分类统计</h3>
<button class="text-primary hover:text-primary/80">
<i class="fa fa-refresh"></i>
</button>
</div>
<div class="h-80">
<canvas id="categoryChart"></canvas>
</div>
</div>
</div>
<!-- 最近借阅 -->
<div class="bg-white rounded-xl p-6 card-shadow">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">最近借阅记录</h3>
<button class="text-primary hover:text-primary/80">查看全部</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">图书</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅人</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">应归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="borrow in recentBorrows" :key="borrow.id" class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" :src="borrow.book.cover" alt="">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ borrow.book.title }}</div>
<div class="text-sm text-gray-500">{{ borrow.book.author }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ borrow.user?.name || '未知用户' }}</div>
<div class="text-sm text-gray-500">{{ borrow.user?.studentId || '未知ID' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.borrowDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.dueDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="borrow.isReturned" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
已归还
</span>
<span v-else-if="isOverdue(borrow.dueDate)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
已逾期
</span>
<span v-else class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
借阅中
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 图书管理视图 -->
<div v-if="currentView === 'books'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">图书管理</h1>
<p class="text-gray-600">管理系统中的所有图书信息</p>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="bookSearchQuery" placeholder="搜索图书标题/作者" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="bookCategoryFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有分类</option>
<option value="计算机">计算机</option>
<option value="文学">文学</option>
<option value="历史">历史</option>
<option value="科学">科学</option>
<option value="艺术">艺术</option>
<option value="经济">经济</option>
</select>
</div>
<div>
<select v-model="bookStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="available">可借阅</option>
<option value="borrowed">已借出</option>
</select>
</div>
<div class="flex justify-end">
<button @click="openBookModal(null)" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 添加图书
</button>
</div>
</div>
</div>
<!-- 图书列表 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="book in filteredBooks" :key="book.id" class="bg-white rounded-xl overflow-hidden card-shadow scale-hover">
<div class="flex">
<div class="w-1/3 bg-gray-200">
<img :src="book.cover" alt="Book cover" class="w-full h-full object-cover">
</div>
<div class="w-2/3 p-4">
<h3 class="font-bold text-lg mb-1 line-clamp-1">{{ book.title }}</h3>
<p class="text-gray-600 text-sm mb-1 line-clamp-1">{{ book.author }}</p>
<p class="text-gray-500 text-xs mb-3">
<span class="bg-gray-100 px-2 py-0.5 rounded text-xs">{{ book.category }}</span>
</p>
<div class="flex justify-between items-center mt-auto">
<span v-if="book.isBorrowed" class="text-xs px-2 py-1 bg-red-100 text-red-800 rounded-full">
已借出
</span>
<span v-else class="text-xs px-2 py-1 bg-green-100 text-green-800 rounded-full">
可借阅
</span>
<div class="flex space-x-1">
<button @click="openBookModal(book)" class="p-1.5 rounded-full hover:bg-gray-100 text-gray-600 transition-colors">
<i class="fa fa-edit"></i>
</button>
<button @click="deleteBook(book.id)" class="p-1.5 rounded-full hover:bg-red-100 text-red-600 transition-colors">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentPage - 1) * booksPerPage + 1 }} 到 {{ Math.min(currentPage * booksPerPage, filteredBooks.length) }} 共 {{ filteredBooks.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevPage" :disabled="currentPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalPages" :key="page" @click="currentPage = page" :class="{'bg-primary text-white border-primary': page === currentPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextPage" :disabled="currentPage === totalPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- 借阅管理视图 -->
<div v-if="currentView === 'borrows'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">借阅管理</h1>
<p class="text-gray-600">管理图书的借阅和归还</p>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="borrowSearchQuery" placeholder="搜索图书/借阅人" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="borrowStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="borrowed">借阅中</option>
<option value="returned">已归还</option>
<option value="overdue">已逾期</option>
</select>
</div>
<div>
<div class="flex items-center space-x-2">
<button class="w-full px-4 py-2 rounded-lg border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-calendar mr-2"></i> 时间范围
</button>
</div>
</div>
<div class="flex justify-end">
<button @click="openBorrowModal" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 新增借阅
</button>
</div>
</div>
</div>
<!-- 借阅记录表格 -->
<div class="bg-white rounded-xl p-6 card-shadow overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">图书信息</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅人</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">应归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">实际归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="borrow in filteredBorrows" :key="borrow.id" class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" :src="borrow.book?.cover || 'https://picsum.photos/seed/default/100/100'" alt="Book cover">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ borrow.book?.title || '未知图书' }}</div>
<div class="text-sm text-gray-500">{{ borrow.book?.author || '未知作者' }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ borrow.user.name }}</div>
<div class="text-sm text-gray-500">{{ borrow.user.studentId }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.borrowDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.dueDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.returnDate || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="borrow.isReturned" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
已归还
</span>
<span v-else-if="isOverdue(borrow.dueDate)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
已逾期
</span>
<span v-else class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
借阅中
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-1">
<button v-if="!borrow.isReturned" @click="returnBook(borrow)" class="p-1.5 rounded-full hover:bg-green-100 text-green-600 transition-colors">
<i class="fa fa-check"></i> 归还
</button>
<button @click="viewBorrowDetails(borrow)" class="p-1.5 rounded-full hover:bg-gray-100 text-gray-600 transition-colors">
<i class="fa fa-eye"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentBorrowPage - 1) * borrowsPerPage + 1 }} 到 {{ Math.min(currentBorrowPage * borrowsPerPage, filteredBorrows.length) }} 共 {{ filteredBorrows.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevBorrowPage" :disabled="currentBorrowPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalBorrowPages" :key="page" @click="currentBorrowPage = page" :class="{'bg-primary text-white border-primary': page === currentBorrowPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentBorrowPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextBorrowPage" :disabled="currentBorrowPage === totalBorrowPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- 用户管理视图 -->
<div v-if="currentView === 'users'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">用户管理</h1>
<p class="text-gray-600">管理系统中的所有用户</p>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="userSearchQuery" placeholder="搜索用户名/学号" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="userRoleFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有角色</option>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<select v-model="userStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="active">活跃</option>
<option value="blocked">已封禁</option>
</select>
</div>
<div class="flex justify-end">
<button @click="openUserModal(null)" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 添加用户
</button>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="user in filteredUsers" :key="user.id" class="bg-white rounded-xl overflow-hidden card-shadow scale-hover">
<div class="p-4">
<div class="flex items-center mb-4">
<div class="w-16 h-16 rounded-full bg-gray-200 overflow-hidden">
<img :src="user.avatar || 'https://picsum.photos/seed/defaultuser/200/200'" alt="User avatar" class="w-full h-full object-cover">
</div>
<div class="ml-4">
<h3 class="font-bold text-lg">{{ user.name || '未知用户' }}</h3>
<p class="text-gray-600 text-sm">{{ user.studentId || '未知ID' }}</p>
<div class="flex items-center mt-1">
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">
{{ user.role === 'student' ? '学生' : user.role === 'teacher' ? '教师' : '管理员' }}
</span>
<span v-if="user.isBlocked" class="ml-2 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800">
已封禁
</span>
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 text-center mb-4">
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">借阅中</p>
<p class="font-bold">{{ getBorrowingCount(user.id) }}</p>
</div>
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">已归还</p>
<p class="font-bold">{{ getReturnedCount(user.id) }}</p>
</div>
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">逾期</p>
<p class="font-bold text-red-500">{{ getOverdueCount(user.id) }}</p>
</div>
</div>
<div class="flex justify-end space-x-2">
<button @click="openUserModal(user)" class="px-3 py-1.5 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
<i class="fa fa-edit mr-1"></i> 编辑
</button>
<button @click="toggleUserBlock(user)" class="px-3 py-1.5 rounded-lg" :class="user.isBlocked ? 'bg-green-500 text-white hover:bg-green-600' : 'bg-red-500 text-white hover:bg-red-600'">
<i class="fa" :class="user.isBlocked ? 'fa-unlock-alt mr-1' : 'fa-lock mr-1'"></i>
{{ user.isBlocked ? '解封' : '封禁' }}
</button>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentUserPage - 1) * usersPerPage + 1 }} 到 {{ Math.min(currentUserPage * usersPerPage, filteredUsers.length) }} 共 {{ filteredUsers.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevUserPage" :disabled="currentUserPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalUserPages" :key="page" @click="currentUserPage = page" :class="{'bg-primary text-white border-primary': page === currentUserPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentUserPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextUserPage" :disabled="currentUserPage === totalUserPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<div class="flex items-center">
<i class="fa fa-book text-primary text-xl mr-2"></i>
<span class="font-bold text-lg">图书管理系统</span>
</div>
<p class="text-gray-500 text-sm mt-1">信息管理与信息系统专业课程设计</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-github text-xl"></i>
</a>
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-envelope text-xl"></i>
</a>
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-linkedin text-xl"></i>
</a>
</div>
</div>
<div class="mt-6 pt-6 border-t border-gray-100 text-center text-gray-500 text-sm">
© 2025 图书管理系统 | 设计与开发
</div>
</div>
</footer>
<!-- 添加/编辑图书模态框 -->
<div v-if="isBookModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBookModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">{{ editingBook ? '编辑图书' : '添加图书' }}</h3>
<button @click="closeBookModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveBook">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">图书封面 URL</label>
<input type="text" v-model="form.bookCover" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">图书标题</label>
<input type="text" v-model="form.bookTitle" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">作者</label>
<input type="text" v-model="form.bookAuthor" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">出版社</label>
<input type="text" v-model="form.bookPublisher" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">出版年份</label>
<input type="number" v-model="form.bookYear" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">分类</label>
<select v-model="form.bookCategory" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="计算机">计算机</option>
<option value="文学">文学</option>
<option value="历史">历史</option>
<option value="科学">科学</option>
<option value="艺术">艺术</option>
<option value="经济">经济</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">ISBN</label>
<input type="text" v-model="form.bookISBN" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">简介</label>
<textarea v-model="form.bookDescription" rows="4" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeBookModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 新增借阅模态框 -->
<div v-if="isBorrowModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBorrowModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">新增借阅记录</h3>
<button @click="closeBorrowModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveBorrow">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">选择图书</label>
<select v-model="form.borrowBookId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">请选择图书</option>
<option v-for="book in availableBooks" :key="book.id" :value="book.id">
{{ book.title }} - {{ book.author }}
</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">选择用户</label>
<select v-model="form.borrowUserId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">请选择用户</option>
<option v-for="user in users" :key="user.id" :value="user.id">
{{ user.name }} - {{ user.studentId }}
</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">借阅日期</label>
<input type="date" v-model="form.borrowDate" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">应归还日期</label>
<input type="date" v-model="form.dueDate" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeBorrowModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 添加/编辑用户模态框 -->
<div v-if="isUserModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeUserModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">{{ editingUser ? '编辑用户' : '添加用户' }}</h3>
<button @click="closeUserModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveUser">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">用户头像 URL</label>
<input type="text" v-model="form.userAvatar" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">姓名</label>
<input type="text" v-model="form.userName" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">学号/工号</label>
<input type="text" v-model="form.userStudentId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">角色</label>
<select v-model="form.userRole" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">联系方式</label>
<input type="text" v-model="form.userContact" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
<input type="email" v-model="form.userEmail" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
<textarea v-model="form.userNotes" rows="3" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeUserModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 借阅详情模态框 -->
<div v-if="isBorrowDetailsModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBorrowDetailsModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">借阅详情</h3>
<button @click="closeBorrowDetailsModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<div class="flex items-center mb-6">
<div class="w-20 h-20 rounded-lg bg-gray-200 overflow-hidden">
<img :src="selectedBorrow.book.cover" alt="Book cover" class="w-full h-full object-cover">
</div>
<div class="ml-4">
<h3 class="font-bold text-lg">{{ selectedBorrow.book.title }}</h3>
<p class="text-gray-600 text-sm">{{ selectedBorrow.book.author }}</p>
<p class="text-gray-500 text-xs mt-1">
<span class="bg-gray-100 px-2 py-0.5 rounded text-xs">{{ selectedBorrow.book.category }}</span>
</p>
</div>
</div>
<div class="space-y-4">
<div class="flex justify-between">
<span class="text-gray-600">借阅人</span>
<span class="font-medium">{{ selectedBorrow.user.name }} ({{ selectedBorrow.user.studentId }})</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">借阅日期</span>
<span class="font-medium">{{ selectedBorrow.borrowDate }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">应归还日期</span>
<span class="font-medium">{{ selectedBorrow.dueDate }}</span>
</div>
<div class="flex justify-between" v-if="selectedBorrow.returnDate">
<span class="text-gray-600">实际归还日期</span>
<span class="font-medium">{{ selectedBorrow.returnDate }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">借阅状态</span>
<span class="font-medium" :class="getStatusColor(selectedBorrow)">
{{ getStatusText(selectedBorrow) }}
</span>
</div>
<div class="flex justify-between" v-if="isOverdue(selectedBorrow.dueDate) && !selectedBorrow.isReturned">
<span class="text-gray-600">逾期天数</span>
<span class="font-medium text-red-500">{{ getOverdueDays(selectedBorrow.dueDate) }} 天</span>
</div>
</div>
<div class="mt-6 pt-6 border-t border-gray-100">
<h4 class="font-medium mb-3">借阅历史</h4>
<div class="space-y-3">
<div class="bg-gray-50 p-3 rounded-lg" v-for="history in getBorrowHistory(selectedBorrow.book.id)" :key="history.id">
<div class="flex justify-between text-sm">
<span class="font-medium">{{ history.user.name }}</span>
<span class="text-gray-500">{{ history.borrowDate }} - {{ history.returnDate || '未归还' }}</span>
</div>
<div class="flex justify-between text-xs mt-1">
<span>{{ history.isReturned ? '已归还' : '借阅中' }}</span>
<span v-if="history.isReturned && history.returnDate > history.dueDate" class="text-red-500">
逾期 {{ getOverdueDays(history.dueDate, history.returnDate) }} 天
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 确认对话框 -->
<div v-if="isConfirmDialogOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeConfirmDialog">
<div class="bg-white rounded-xl w-full max-w-md p-6">
<h3 class="text-lg font-bold mb-3">{{ confirmDialogTitle }}</h3>
<p class="text-gray-600 mb-6">{{ confirmDialogMessage }}</p>
<div class="flex justify-end space-x-3">
<button @click="closeConfirmDialog" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button @click="confirmAction" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
确认
</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
// 导航相关
const currentView = ref('borrows');
const isScrolled = ref(false);
const isMobileMenuOpen = ref(false);
// 数据模型
const books = ref([
{
id: 1,
title: "Python数据分析实战",
author: "李小明",
publisher: "电子工业出版社",
year: 2023,
category: "计算机",
isbn: "9787121456789",
description: "本书全面介绍了Python在数据分析领域的应用,涵盖NumPy、Pandas、Matplotlib等库的使用。",
cover: "https://picsum.photos/seed/python/200/300",
isBorrowed: false
},
{
id: 2,
title: "Web前端开发技术",
author: "王建国",
publisher: "人民邮电出版社",
year: 2022,
category: "计算机",
isbn: "9787115589012",
description: "本书详细讲解了HTML、CSS、JavaScript等前端技术,以及Vue.js框架的使用。",
cover: "https://picsum.photos/seed/web/200/300",
isBorrowed: true
},
{
id: 3,
title: "数据结构与算法",
author: "张教授",
publisher: "清华大学出版社",
year: 2021,
category: "计算机",
isbn: "9787302587463",
description: "本书系统介绍了常用的数据结构和算法,适合计算机专业学生和从业人员阅读。",
cover: "https://picsum.photos/seed/algorithm/200/300",
isBorrowed: false
},
{
id: 4,
title: "平凡的世界",
author: "路遥",
publisher: "人民文学出版社",
year: 2005,
category: "文学",
isbn: "9787020049297",
description: "这部长篇小说以中国20世纪70年代中期到80年代中期十年间为背景,通过复杂的矛盾纠葛,以孙少安和孙少平两兄弟为中心,展示了普通人在大时代历史进程中所走过的艰难曲折的道路。",
cover: "https://picsum.photos/seed/literature/200/300",
isBorrowed: false
},
{
id: 5,
title: "明朝那些事儿",
author: "当年明月",
publisher: "中国友谊出版公司",
year: 2009,
category: "历史",
isbn: "9787505725462",
description: "《明朝那些事儿》主要讲述的是从1344年到1644年这三百年间关于明朝的一些故事。以史料为基础,以年代和具体人物为主线,并加入了小说的笔法,语言幽默风趣。",
cover: "https://picsum.photos/seed/history/200/300",
isBorrowed: true
},
{
id: 6,
title: "时间简史",
author: "史蒂芬·霍金",
publisher: "湖南科学技术出版社",
year: 2018,
category: "科学",
isbn: "9787535794567",
description: "《时间简史》自1988年首版以来,被翻译成40种文字,累计销售量突破2500万册,是畅销全世界的科学著作。",
cover: "https://picsum.photos/seed/science/200/300",
isBorrowed: false
},
{
id: 7,
title: "艺术的故事",
author: "贡布里希",
publisher: "广西美术出版社",
year: 2016,
category: "艺术",
isbn: "9787549413866",
description: "《艺术的故事》概括地叙述了从最早的洞窟绘画到当今的实验艺术的发展历程,以阐明艺术史是'各种传统不断迂回、不断改变的历史,每一件作品在这历史中都既回顾过去又导向未来'。",
cover: "https://picsum.photos/seed/art/200/300",
isBorrowed: false
},
{
id: 8,
title: "经济学原理",
author: "N·格里高利·曼昆",
publisher: "北京大学出版社",
year: 2015,
category: "经济",
isbn: "9787301255891",
description: "《经济学原理》分为微观经济学和宏观经济学两部分,是世界上最流行的经济学教材。",
cover: "https://picsum.photos/seed/economics/200/300",
isBorrowed: true
}
]);
const users = ref([
{
id: 1,
name: "张三",
studentId: "2022001",
role: "student",
contact: "13800138001",
email: "[email protected]",
avatar: "https://picsum.photos/seed/user1/200/200",
notes: "计算机系大三学生,借阅记录良好",
isBlocked: false
},
{
id: 2,
name: "李四",
studentId: "2022002",
role: "student",
contact: "13900139002",
email: "[email protected]",
avatar: "https://picsum.photos/seed/user2/200/200",
notes: "数学系大二学生,有逾期记录",
isBlocked: false
},
{
id: 3,
name: "王五",
studentId: "T2021001",
role: "teacher",
contact: "13700137003",
email: "[email protected]",
avatar: "https://picsum.photos/seed/user3/200/200",
notes: "计算机系教授,经常借阅专业书籍",
isBlocked: false
},
{
id: 4,
name: "赵六",
studentId: "2022003",
role: "student",
contact: "13600136004",
email: "[email protected]",
avatar: "https://picsum.photos/seed/user4/200/200",
notes: "历史系大一学生",
isBlocked: false
},
{
id: 5,
name: "管理员",
studentId: "A0001",
role: "admin",
contact: "13500135005",
email: "[email protected]",
avatar: "https://picsum.photos/seed/admin/200/200",
notes: "系统管理员",
isBlocked: false
}
]);
const borrows = ref([
{
id: 1,
bookId: 2,
userId: 1,
borrowDate: "2025-06-01",
dueDate: "2025-06-15",
returnDate: null,
isReturned: false
},
{
id: 2,
bookId: 5,
userId: 4,
borrowDate: "2025-05-20",
dueDate: "2025-06-03",
returnDate: null,
isReturned: false
},
{
id: 3,
bookId: 8,
userId: 3,
borrowDate: "2025-05-10",
dueDate: "2025-05-24",
returnDate: "2025-05-25",
isReturned: true
},
{
id: 4,
bookId: 1,
userId: 2,
borrowDate: "2025-05-15",
dueDate: "2025-05-29",
returnDate: "2025-05-30",
isReturned: true
},
{
id: 5,
bookId: 2,
userId: 3,
borrowDate: "2025-04-10",
dueDate: "2025-04-24",
returnDate: "2025-04-20",
isReturned: true
}
]);
// 搜索和筛选
const bookSearchQuery = ref('');
const bookCategoryFilter = ref('');
const bookStatusFilter = ref('');
const borrowSearchQuery = ref('');
const borrowStatusFilter = ref('');
const userSearchQuery = ref('');
const userRoleFilter = ref('');
const userStatusFilter = ref('');
// 分页
const currentPage = ref(1);
const booksPerPage = ref(9);
const currentBorrowPage = ref(1);
const borrowsPerPage = ref(10);
const currentUserPage = ref(1);
const usersPerPage = ref(6);
// 模态框
const isBookModalOpen = ref(false);
const isBorrowModalOpen = ref(false);
const isUserModalOpen = ref(false);
const isBorrowDetailsModalOpen = ref(false);
const isConfirmDialogOpen = ref(false);
const editingBook = ref(null);
const editingUser = ref(null);
const selectedBorrow = ref(null);
const confirmDialogTitle = ref('');
const confirmDialogMessage = ref('');
let confirmCallback = null;
// 表单数据
const form = ref({
bookId: null,
bookCover: '',
bookTitle: '',
bookAuthor: '',
bookPublisher: '',
bookYear: '',
bookCategory: '计算机',
bookISBN: '',
bookDescription: '',
borrowBookId: '',
borrowUserId: '',
borrowDate: '',
dueDate: '',
userId: null,
userAvatar: '',
userName: '',
userStudentId: '',
userRole: 'student',
userContact: '',
userEmail: '',
userNotes: ''
});
// 计算属性
const filteredBooks = computed(() => {
return books.value.filter(book => {
const titleMatch = book.title.toLowerCase().includes(bookSearchQuery.value.toLowerCase());
const authorMatch = book.author.toLowerCase().includes(bookSearchQuery.value.toLowerCase());
const categoryMatch = bookCategoryFilter.value ? book.category === bookCategoryFilter.value : true;
const statusMatch = bookStatusFilter.value === 'available' ? !book.isBorrowed :
bookStatusFilter.value === 'borrowed' ? book.isBorrowed : true;
return (titleMatch || authorMatch) && categoryMatch && statusMatch;
});
});
const totalPages = computed(() => {
return Math.ceil(filteredBooks.value.length / booksPerPage.value);
});
const paginatedBooks = computed(() => {
const start = (currentPage.value - 1) * booksPerPage.value;
const end = start + booksPerPage.value;
return filteredBooks.value.slice(start, end);
});
const filteredBorrows = computed(() => {
return borrows.value
.map(borrow => {
const book = books.value.find(b => b.id === borrow.bookId);
const user = users.value.find(u => u.id === borrow.userId);
// 添加调试信息
if (!book) {
console.warn('找不到对应的图书:', borrow);
}
if (!user) {
console.warn('找不到对应的用户:', borrow);
}
return { ...borrow, book, user };
})
.filter(borrow => {
// 过滤掉没有关联图书或用户的记录
if (!borrow.book || !borrow.user) {
console.warn('过滤无效借阅记录:', borrow);
return false;
}
// 应用搜索和筛选条件
const bookMatch = borrow.book.title.toLowerCase().includes(borrowSearchQuery.value.toLowerCase());
const userMatch = borrow.user.name.toLowerCase().includes(borrowSearchQuery.value.toLowerCase());
let statusMatch = true;
if (borrowStatusFilter.value === 'borrowed') {
statusMatch = !borrow.isReturned && !isOverdue(borrow.dueDate);
} else if (borrowStatusFilter.value === 'returned') {
statusMatch = borrow.isReturned;
} else if (borrowStatusFilter.value === 'overdue') {
statusMatch = !borrow.isReturned && isOverdue(borrow.dueDate);
}
return (bookMatch || userMatch) && statusMatch;
});
});
const totalBorrowPages = computed(() => {
return Math.ceil(filteredBorrows.value.length / borrowsPerPage.value);
});
const paginatedBorrows = computed(() => {
const start = (currentBorrowPage.value - 1) * borrowsPerPage.value;
const end = start + borrowsPerPage.value;
return filteredBorrows.value.slice(start, end);
});
const filteredUsers = computed(() => {
return users.value.filter(user => {
const nameMatch = user.name.toLowerCase().includes(userSearchQuery.value.toLowerCase());
const idMatch = user.studentId.toLowerCase().includes(userSearchQuery.value.toLowerCase());
const roleMatch = userRoleFilter.value ? user.role === userRoleFilter.value : true;
const statusMatch = userStatusFilter.value === 'active' ? !user.isBlocked :
userStatusFilter.value === 'blocked' ? user.isBlocked : true;
return (nameMatch || idMatch) && roleMatch && statusMatch;
});
});
const totalUserPages = computed(() => {
return Math.ceil(filteredUsers.value.length / usersPerPage.value);
});
const paginatedUsers = computed(() => {
const start = (currentUserPage.value - 1) * usersPerPage.value;
const end = start + usersPerPage.value;
return filteredUsers.value.slice(start, end);
});
const borrowedBooksCount = computed(() => {
return books.value.filter(book => book.isBorrowed).length;
});
const overdueBooksCount = computed(() => {
return borrows.value.filter(borrow => !borrow.isReturned && isOverdue(borrow.dueDate)).length;
});
const recentBorrows = computed(() => {
return [...borrows.value]
.sort((a, b) => new Date(b.borrowDate) - new Date(a.borrowDate))
.slice(0, 5)
.map(borrow => {
return {
...borrow,
book: books.value.find(b => b.id === borrow.bookId),
user: users.value.find(u => u.id === borrow.userId)
};
});
});
const availableBooks = computed(() => {
return books.value.filter(book => !book.isBorrowed);
});
// 方法
const toggleMobileMenu = () => {
isMobileMenuOpen.value = !isMobileMenuOpen.value;
};
const changeView = (view) => {
currentView.value = view;
isMobileMenuOpen.value = false;
};
const handleScroll = () => {
if (window.scrollY > 10) {
isScrolled.value = true;
} else {
isScrolled.value = false;
}
};
const isOverdue = (dueDate, returnDate = new Date().toISOString().split('T')[0]) => {
return dueDate && returnDate > dueDate;
};
const getOverdueDays = (dueDate, returnDate = new Date().toISOString().split('T')[0]) => {
if (!isOverdue(dueDate, returnDate)) return 0;
const due = new Date(dueDate);
const ret = new Date(returnDate);
const diffTime = Math.abs(ret - due);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const openBookModal = (book) => {
editingBook.value = book;
if (book) {
form.value = {
bookId: book.id,
bookCover: book.cover,
bookTitle: book.title,
bookAuthor: book.author,
bookPublisher: book.publisher,
bookYear: book.year,
bookCategory: book.category,
bookISBN: book.isbn,
bookDescription: book.description
};
} else {
form.value = {
bookId: null,
bookCover: 'https://picsum.photos/seed/default/200/300',
bookTitle: '',
bookAuthor: '',
bookPublisher: '',
bookYear: '',
bookCategory: '计算机',
bookISBN: '',
bookDescription: ''
};
}
isBookModalOpen.value = true;
};
const closeBookModal = () => {
isBookModalOpen.value = false;
};
const saveBook = () => {
if (!form.value.bookTitle || !form.value.bookAuthor) {
alert('请填写图书标题和作者');
return;
}
if (editingBook.value) {
// 更新现有图书
const index = books.value.findIndex(b => b.id === form.value.bookId);
if (index !== -1) {
books.value[index] = {
...books.value[index],
cover: form.value.bookCover,
title: form.value.bookTitle,
author: form.value.bookAuthor,
publisher: form.value.bookPublisher,
year: form.value.bookYear,
category: form.value.bookCategory,
isbn: form.value.bookISBN,
description: form.value.bookDescription
};
}
} else {
// 添加新图书
const newBook = {
id: books.value.length > 0 ? Math.max(...books.value.map(b => b.id)) + 1 : 1,
cover: form.value.bookCover,
title: form.value.bookTitle,
author: form.value.bookAuthor,
publisher: form.value.bookPublisher,
year: form.value.bookYear,
category: form.value.bookCategory,
isbn: form.value.bookISBN,
description: form.value.bookDescription,
isBorrowed: false
};
books.value.push(newBook);
}
isBookModalOpen.value = false;
showToast(editingBook.value ? '图书更新成功' : '图书添加成功');
};
const deleteBook = (id) => {
confirmDialogTitle.value = '确认删除';
confirmDialogMessage.value = '确定要删除这本书吗?删除后将无法恢复。';
confirmCallback = () => {
const borrowExists = borrows.value.some(borrow => borrow.bookId === id);
if (borrowExists) {
alert('无法删除,这本书正在被借阅');
return;
}
books.value = books.value.filter(book => book.id !== id);
showToast('图书已删除');
};
isConfirmDialogOpen.value = true;
};
const openBorrowModal = () => {
const today = new Date().toISOString().split('T')[0];
const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 14);
form.value = {
borrowBookId: '',
borrowUserId: '',
borrowDate: today,
dueDate: dueDate.toISOString().split('T')[0]
};
isBorrowModalOpen.value = true;
};
const closeBorrowModal = () => {
isBorrowModalOpen.value = false;
};
const saveBorrow = () => {
if (!form.value.borrowBookId || !form.value.borrowUserId) {
alert('请选择图书和用户');
return;
}
if (form.value.borrowDate > form.value.dueDate) {
alert('应归还日期不能早于借阅日期');
return;
}
const book = books.value.find(b => b.id === parseInt(form.value.borrowBookId));
if (book.isBorrowed) {
alert('这本书已经被借出');
return;
}
const newBorrow = {
id: borrows.value.length > 0 ? Math.max(...borrows.value.map(b => b.id)) + 1 : 1,
bookId: parseInt(form.value.borrowBookId),
userId: parseInt(form.value.borrowUserId),
borrowDate: form.value.borrowDate,
dueDate: form.value.dueDate,
returnDate: null,
isReturned: false
};
borrows.value.push(newBorrow);
// 更新图书状态
const bookIndex = books.value.findIndex(b => b.id === newBorrow.bookId);
if (bookIndex !== -1) {
books.value[bookIndex].isBorrowed = true;
}
isBorrowModalOpen.value = false;
isBorrowModalOpen.value = false;
showToast('借阅记录已添加');
};
const returnBook = (borrow) => {
confirmDialogTitle.value = '确认归还';
confirmDialogMessage.value = `确定这本书已经归还了吗?`;
confirmCallback = () => {
const index = borrows.value.findIndex(b => b.id === borrow.id);
if (index !== -1) {
borrows.value[index].isReturned = true;
borrows.value[index].returnDate = new Date().toISOString().split('T')[0];
// 更新图书状态
const bookIndex = books.value.findIndex(b => b.id === borrow.bookId);
if (bookIndex !== -1) {
books.value[bookIndex].isBorrowed = false;
}
showToast('图书已归还');
}
};
isConfirmDialogOpen.value = true;
};
const openUserModal = (user) => {
editingUser.value = user;
if (user) {
form.value = {
userId: user.id,
userAvatar: user.avatar,
userName: user.name,
userStudentId: user.studentId,
userRole: user.role,
userContact: user.contact,
userEmail: user.email,
userNotes: user.notes
};
} else {
form.value = {
userId: null,
userAvatar: 'https://picsum.photos/seed/defaultuser/200/200',
userName: '',
userStudentId: '',
userRole: 'student',
userContact: '',
userEmail: '',
userNotes: ''
};
}
isUserModalOpen.value = true;
};
const closeUserModal = () => {
isUserModalOpen.value = false;
};
const saveUser = () => {
if (!form.value.userName || !form.value.userStudentId) {
alert('请填写用户名和学号/工号');
return;
}
if (editingUser.value) {
// 更新现有用户
const index = users.value.findIndex(u => u.id === form.value.userId);
if (index !== -1) {
users.value[index] = {
...users.value[index],
avatar: form.value.userAvatar,
name: form.value.userName,
studentId: form.value.userStudentId,
role: form.value.userRole,
contact: form.value.userContact,
email: form.value.userEmail,
notes: form.value.userNotes
};
}
} else {
// 添加新用户
const newUser = {
id: users.value.length > 0 ? Math.max(...users.value.map(u => u.id)) + 1 : 1,
avatar: form.value.userAvatar,
name: form.value.userName,
studentId: form.value.userStudentId,
role: form.value.userRole,
contact: form.value.userContact,
email: form.value.userEmail,
notes: form.value.userNotes,
isBlocked: false
};
users.value.push(newUser);
}
isUserModalOpen.value = false;
showToast(editingUser.value ? '用户更新成功' : '用户添加成功');
};
const toggleUserBlock = (user) => {
confirmDialogTitle.value = user.isBlocked ? '确认解封' : '确认封禁';
confirmDialogMessage.value = user.isBlocked
? `确定要解封用户 ${user.name} 吗?`
: `确定要封禁用户 ${user.name} 吗?封禁后用户将无法借阅图书。`;
confirmCallback = () => {
const index = users.value.findIndex(u => u.id === user.id);
if (index !== -1) {
users.value[index].isBlocked = !users.value[index].isBlocked;
showToast(users.value[index].isBlocked ? '用户已封禁' : '用户已解封');
}
};
isConfirmDialogOpen.value = true;
};
const viewBorrowDetails = (borrow) => {
selectedBorrow.value = {
...borrow,
book: books.value.find(b => b.id === borrow.bookId),
user: users.value.find(u => u.id === borrow.userId)
};
isBorrowDetailsModalOpen.value = true;
};
const closeBorrowDetailsModal = () => {
isBorrowDetailsModalOpen.value = false;
};
const closeConfirmDialog = () => {
isConfirmDialogOpen.value = false;
confirmCallback = null;
};
const confirmAction = () => {
if (typeof confirmCallback === 'function') {
confirmCallback();
}
closeConfirmDialog();
};
const getBorrowingCount = (userId) => {
return borrows.value.filter(borrow =>
borrow.userId === userId && !borrow.isReturned
).length;
};
const getReturnedCount = (userId) => {
return borrows.value.filter(borrow =>
borrow.userId === userId && borrow.isReturned
).length;
};
const getOverdueCount = (userId) => {
return borrows.value.filter(borrow =>
borrow.userId === userId && !borrow.isReturned && isOverdue(borrow.dueDate)
).length;
};
const getBorrowHistory = (bookId) => {
return borrows.value
.filter(borrow => borrow.bookId === bookId)
.map(borrow => ({
...borrow,
user: users.value.find(u => u.id === borrow.userId)
}));
};
const getStatusText = (borrow) => {
if (borrow.isReturned) return '已归还';
if (isOverdue(borrow.dueDate)) return '已逾期';
return '借阅中';
};
const getStatusColor = (borrow) => {
if (borrow.isReturned) return 'text-green-600';
if (isOverdue(borrow.dueDate)) return 'text-red-600';
return 'text-blue-600';
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const prevBorrowPage = () => {
if (currentBorrowPage.value > 1) {
currentBorrowPage.value--;
}
};
const nextBorrowPage = () => {
if (currentBorrowPage.value < totalBorrowPages.value) {
currentBorrowPage.value++;
}
};
const prevUserPage = () => {
if (currentUserPage.value > 1) {
currentUserPage.value--;
}
};
const nextUserPage = () => {
if (currentUserPage.value < totalUserPages.value) {
currentUserPage.value++;
}
};
const showToast = (message) => {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-20 opacity-0';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.remove('translate-y-20', 'opacity-0');
}, 100);
setTimeout(() => {
toast.classList.add('translate-y-20', 'opacity-0');
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 3000);
};
// 初始化图表
const initCharts = () => {
// 借阅趋势图表
const borrowCtx = document.getElementById('borrowChart');
if (borrowCtx) {
new Chart(borrowCtx, {
type: 'line',
data: {
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
datasets: [{
label: '借阅数量',
data: [12, 19, 15, 17, 20, 14, 16],
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}, {
label: '归还数量',
data: [8, 15, 10, 14, 18, 12, 13],
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 分类统计图表
const categoryCtx = document.getElementById('categoryChart');
if (categoryCtx) {
new Chart(categoryCtx, {
type: 'doughnut',
data: {
labels: ['计算机', '文学', '历史', '科学', '艺术', '经济'],
datasets: [{
data: [25, 20, 15, 12, 10, 18],
backgroundColor: [
'#3B82F6',
'#6366F1',
'#8B5CF6',
'#EC4899',
'#F59E0B',
'#10B981'
],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
},
cutout: '70%'
}
});
}
};
// 生命周期钩子
onMounted(() => {
window.addEventListener('scroll', handleScroll);
initCharts();
});
return {
// 数据
currentView,
isScrolled,
isMobileMenuOpen,
books,
users,
borrows,
bookSearchQuery,
bookCategoryFilter,
bookStatusFilter,
borrowSearchQuery,
borrowStatusFilter,
userSearchQuery,
userRoleFilter,
userStatusFilter,
currentPage,
booksPerPage,
currentBorrowPage,
borrowsPerPage,
currentUserPage,
usersPerPage,
isBookModalOpen,
isBorrowModalOpen,
isUserModalOpen,
isBorrowDetailsModalOpen,
isConfirmDialogOpen,
editingBook,
editingUser,
selectedBorrow,
confirmDialogTitle,
confirmDialogMessage,
form,
// 计算属性
filteredBooks,
totalPages,
paginatedBooks,
filteredBorrows,
totalBorrowPages,
paginatedBorrows,
filteredUsers,
totalUserPages,
paginatedUsers,
borrowedBooksCount,
overdueBooksCount,
recentBorrows,
availableBooks,
// 方法
toggleMobileMenu,
changeView,
handleScroll,
isOverdue,
getOverdueDays,
openBookModal,
closeBookModal,
saveBook,
deleteBook,
openBorrowModal,
closeBorrowModal,
saveBorrow,
returnBook,
openUserModal,
closeUserModal,
saveUser,
toggleUserBlock,
viewBorrowDetails,
closeBorrowDetailsModal,
closeConfirmDialog,
confirmAction,
getBorrowingCount,
getReturnedCount,
getOverdueCount,
getBorrowHistory,
getStatusText,
getStatusColor,
prevPage,
nextPage,
prevBorrowPage,
nextBorrowPage,
prevUserPage,
nextUserPage,
showToast
};
}
}).mount('#app');
</script>
</body>
</html>
(三)系统界面效果
1.仪表盘
系统默认界面(仪表盘视图)的效果如下:

2.图书管理

3.借阅管理

4.用户管理

以下是对图书管理系统界面设计代码的详细解析:
二 、整体架构
此图书管理系统前端基于Vue.js构建,采用单页面应用(SPA)的架构。HTML文件为入口,引入各类外部库与资源,借助Vue.js动态渲染页面内容。系统运用响应式设计,能适配不同屏幕尺寸。
(一)功能模块
- 导航栏模块:包含系统标题、主菜单、搜索框与用户图标,支持移动端菜单展开与收缩。
- 仪表盘模块:展示系统关键统计数据,如总藏书量、借出图书、注册用户和逾期未还数量,还有借阅趋势与分类统计图表,以及最近借阅记录。
- 图书管理模块:可搜索、筛选图书,添加、编辑和删除图书信息,支持分页显示。
- 借阅管理模块:能搜索、筛选借阅记录,新增借阅,处理图书归还,支持分页显示。
- 用户管理模块:可搜索、筛选用户,添加、编辑用户信息,封禁和解封用户,支持分页显示。
- 页脚模块:显示系统标题与版权信息、社交链接。
- 模态框模块:用于添加/编辑图书、新增借阅、添加/编辑用户、借阅详情、确认对话框。
(二)代码树形结构
整个系统的界面采用单页面应用(SPA)的架构,即只有一个index.html。以下是index.html页面的树形结构图:
index.html
├── <head>
│ ├── 元数据与页面标题
│ ├── 引入外部库(Tailwind CSS、Font Awesome、Chart.js、Vue.js)
│ ├── Tailwind CSS 配置
│ └── 自定义样式
├── <body>
│ ├── <div id="app">
│ ├── <nav> 导航栏
│ │ ├── 系统标题
│ │ ├── 主菜单(桌面端)
│ │ ├── 搜索框
│ │ ├── 用户图标
│ │ └── 移动端菜单
│ ├── <main> 主内容区
│ │ ├── <div v-if="currentView === 'dashboard'"> 仪表盘视图
│ │ │ ├── 统计卡片
│ │ │ ├── 图表区域
│ │ │ └── 最近借阅记录
│ │ ├── <div v-if="currentView === 'books'"> 图书管理视图
│ │ │ ├── 搜索和筛选
│ │ │ ├── 图书列表
│ │ │ └── 分页
│ │ ├── <div v-if="currentView === 'borrows'"> 借阅管理视图
│ │ │ ├── 搜索和筛选
│ │ │ ├── 借阅记录表格
│ │ │ └── 分页
│ │ └── <div v-if="currentView === 'users'"> 用户管理视图
│ │ ├── 搜索和筛选
│ │ ├── 用户列表
│ │ └── 分页
│ ├── <footer> 页脚
│ │ ├── 系统标题与版权信息
│ │ └── 社交链接
│ └── 模态框部分
│ ├── <div v-if="isBookModalOpen"> 添加/编辑图书模态框
│ │ ├── 模态框标题栏
│ │ │ ├── 标题文本
│ │ │ └── 关闭按钮
│ │ └── 图书表单
│ │ ├── 图书封面URL输入
│ │ ├── 图书标题输入
│ │ ├── 作者输入
│ │ ├── 出版社输入
│ │ ├── 出版年份输入
│ │ ├── 分类选择
│ │ ├── ISBN输入
│ │ ├── 简介文本框
│ │ └── 操作按钮
│ │ ├── 取消按钮
│ │ └── 保存按钮
│ ├── <div v-if="isBorrowModalOpen"> 新增借阅模态框
│ │ ├── 模态框标题栏
│ │ │ ├── 标题文本
│ │ │ └── 关闭按钮
│ │ └── 借阅表单
│ │ ├── 选择图书下拉框
│ │ ├── 选择用户下拉框
│ │ ├── 借阅日期选择
│ │ ├── 应归还日期选择
│ │ └── 操作按钮
│ │ ├── 取消按钮
│ │ └── 保存按钮
│ ├── <div v-if="isUserModalOpen"> 添加/编辑用户模态框
│ │ ├── 模态框标题栏
│ │ │ ├── 标题文本
│ │ │ └── 关闭按钮
│ │ └── 用户表单
│ │ ├── 用户头像URL输入
│ │ ├── 姓名输入
│ │ ├── 学号/工号输入
│ │ ├── 角色选择
│ │ ├── 联系方式输入
│ │ ├── 邮箱输入
│ │ ├── 备注文本框
│ │ └── 操作按钮
│ │ ├── 取消按钮
│ │ └── 保存按钮
│ ├── <div v-if="isBorrowDetailsModalOpen"> 借阅详情模态框
│ │ ├── 模态框标题栏
│ │ │ ├── 标题文本
│ │ │ └── 关闭按钮
│ │ ├── 图书信息
│ │ │ ├── 图书封面
│ │ │ ├── 图书标题
│ │ │ ├── 作者
│ │ │ └── 分类标签
│ │ ├── 借阅详情
│ │ │ ├── 借阅人信息
│ │ │ ├── 借阅日期
│ │ │ ├── 应归还日期
│ │ │ ├── 实际归还日期
│ │ │ ├── 借阅状态
│ │ │ └── 逾期天数(如适用)
│ │ └── 借阅历史
│ │ └── 历史记录列表
│ │ ├── 借阅人
│ │ ├── 借阅时间范围
│ │ └── 状态
│ └── <div v-if="isConfirmDialogOpen"> 确认对话框
│ ├── 标题
│ ├── 确认信息
│ └── 操作按钮
│ ├── 取消按钮
│ └── 确认按钮
三 、代码详细解析
HTML页面,基本是包含两个部分,head和body。
(一)<head> 部分
Head部分的代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图书管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6366F1',
accent: '#F59E0B',
neutral: '#6B7280',
success: '#10B981',
warning: '#F59E0B',
danger: '#EF4444',
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.transition-custom {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.scale-hover {
transition: transform 0.2s ease-in-out;
}
.scale-hover:hover {
transform: scale(1.02);
}
}
</style>
</head>
以下是详细的代码说明:
1. 元数据与页面标题:
(1)meta charset="UTF-8":设定字符编码为UTF - 8。
(2)meta name="viewport" content="width=device-width, initial-scale=1.0":确保页面在移动设备上正确显示。
(3)<title>图书管理系统</title>:设置页面标题。
2. 引入外部库:
(1)Tailwind CSS:用于快速构建响应式UI。
(2)Font Awesome:提供图标库。
(3)Chart.js:用于绘制图表。
(4)Vue.js:构建交互式界面的JavaScript框架。
(5)Google Fonts:引入Inter字体。
3. Tailwind CSS配置:
扩展颜色和字体家族,方便在HTML中使用自定义类名。
4. 自定义样式:
定义自定义实用类,如卡片阴影、过渡效果和悬停缩放效果。
(二)<body> 部分
<body> 部分包含了一个id为app的<div> ,这是Vue.js应用的挂载点,整个图书管理系统的前端界面都将在这个容器内渲染。以下是对body部分各模块的详细解析,其中第一部分是导航栏。如上代码树形结构可以看出来,
<body>
├── <div id="app">
├── <nav> 导航栏
│ ├── 系统标题
│ ├── 主菜单(桌面端)
│ ├── 搜索框
│ ├── 用户图标
│ └── 移动端菜单
├── <main> 主内容区
1. 导航栏 (<nav>)
在body中的<div id="app">下第一个模块为导航栏<nav>,<nav>里包含了两大部分,桌面端菜单(电脑的浏览器)和移动端菜单。
<nav class="bg-white shadow-md sticky top-0 z-50 transition-all duration-300" :class="{'bg-primary/95 text-white': isScrolled}">
<!-- 导航栏内容 -->
</nav>
导航栏固定在页面顶部,滚动时背景颜色会改变。

滚动后的效果:

( 1 ) <nav> 的 样式与布局:
- bg-white shadow-md sticky top-0 z-50:设置导航栏背景为白色,添加阴影效果,使其固定在页面顶部,并设置较高的层叠顺序。
- transition-all duration-300:添加过渡效果,使导航栏样式变化更平滑。
- :class="{'bg-primary/95 text-white': isScrolled}":这是 Vue.js 的动态类绑定。当 isScrolled 为 true 时,导航栏背景变为半透明的主色调,文字变为白色。
( 2 ) 导航栏内容:
导航栏由系统标题、主菜单(桌面端)、搜索框、用户图标和移动端菜单组成。代码树形结构如下:
<nav> 导航栏
├── 系统标题
├── 主菜单(桌面端)
├── 搜索框
├── 用户图标
└── 移动端菜单
导航栏内容都放在以下div中。而这个div是放在<nav>中。
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<!-- 系统标题、主菜单和搜索框、用户图标 -->
</div>
1系统标题:
<div class="flex items-center space-x-2">
<i class="fa fa-book text-2xl text-primary"></i>
<span class="text-xl font-bold">图书管理系统</span>
</div>
使用Font Awesome图标和文字展示系统标题。
2主菜单(桌面端):
放在"系统标题"的div之后。
<div class="hidden md:flex items-center space-x-6">
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'dashboard'}">
<i class="fa fa-tachometer mr-1"></i>仪表盘
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'books'}">
<i class="fa fa-book mr-1"></i>图书管理
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'borrows'}">
<i class="fa fa-exchange mr-1"></i>借阅管理
</a>
<a href="#" class="font-medium hover:text-primary transition-colors" :class="{'text-primary': currentView === 'users'}">
<i class="fa fa-users mr-1"></i>用户管理
</a>
</div>
- hidden md:flex:在小屏幕设备上隐藏,在中等及以上屏幕设备上显示。
- :class="{'text-primary': currentView === 'dashboard'}":根据当前视图 currentView 的值,动态设置菜单项的文字颜色。
3搜索框和用户图标:
放在"主菜单"之后。
<div class="flex items-center space-x-4">
<div class="relative hidden md:block">
<input type="text" placeholder="搜索图书..." class="pl-9 pr-4 py-2 rounded-full bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/50 w-48 transition-all duration-300 focus:w-64">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div class="relative">
<button class="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center hover:bg-gray-300 transition-colors">
<i class="fa fa-user"></i>
</button>
</div>
<!-- 移动端菜单按钮 -->
<button class="md:hidden" @click="toggleMobileMenu">
<i class="fa fa-bars text-xl"></i>
</button>
</div>
- 在中等及以上屏幕设备上显示,输入框获得焦点时会有动画效果。
- 显示用户图标,悬停时背景颜色变化。
- 移动端菜单按钮:在小屏幕设备上显示,点击时调用 toggleMobileMenu 方法切换移动端菜单的显示状态。
( 3 ) 移动端菜单:
放在以下div中。而这个div是放在<nav>中,与桌面端导航栏对齐。
<div class="md:hidden bg-white border-t border-gray-100 shadow-lg absolute w-full left-0 transition-all duration-300 transform" :class="{'translate-y-0': isMobileMenuOpen, '-translate-y-full': !isMobileMenuOpen}">
<!-- 移动端菜单项 -->
</div>
- md:hidden:在中等及以上屏幕设备上隐藏。
- :class="{'translate-y-0': isMobileMenuOpen, '-translate-y-full': !isMobileMenuOpen}":根据 isMobileMenuOpen 的值,控制移动端菜单的显示与隐藏,使用过渡效果实现滑动动画。
移动端菜单项内容如下:
<div class="container mx-auto px-4 py-2">
<div class="flex flex-col space-y-3 py-2">
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'dashboard'}">
<i class="fa fa-tachometer mr-2"></i>仪表盘
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'books'}">
<i class="fa fa-book mr-2"></i>图书管理
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'borrows'}">
<i class="fa fa-exchange mr-2"></i>借阅管理
</a>
<a href="#" class="py-2 px-3 hover:bg-gray-100 rounded-lg transition-colors" :class="{'bg-primary/10 text-primary': currentView === 'users'}">
<i class="fa fa-users mr-2"></i>用户管理
</a>
<div class="relative">
<input type="text" placeholder="搜索图书..." class="w-full pl-9 pr-4 py-2 rounded-lg bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary/50">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
</div>
</div>
( 三 ) <body>部分之 主内容区 (<main>)
接着<nav>继续实现body中的其他内容。放在<main>标签中。<main>与<nav>对齐。如上代码树形结构可以看出来。
<body>
├── <div id="app">
├── <nav> 导航栏
├── <main> 主内容区
<main class="flex-grow container mx-auto px-4 py-6">
<!-- 仪表盘视图、图书管理视图、借阅管理视图、用户管理视图 -->
</main>
以下为主内容区 (<main>),包括了仪表盘视图 、图书管理视图 、借阅管理视图 、用户管理视图4个部分。代码树形结构如下:
<main> 主内容区
├── <div v-if="currentView === 'dashboard'"> 仪表盘视图
│ ├── 统计卡片
│ ├── 图表区域
│ └── 最近借阅记录
├── <div v-if="currentView === 'books'"> 图书管理视图
│ ├── 搜索和筛选
│ ├── 图书列表
│ └── 分页
├── <div v-if="currentView === 'borrows'"> 借阅管理视图
│ ├── 搜索和筛选
│ ├── 借阅记录表格
│ └── 分页
└── <div v-if="currentView === 'users'"> 用户管理视图
├── 搜索和筛选
├── 用户列表
└── 分页
1. 仪表盘视图 (<div v-if="currentView === 'dashboard'">)
仪表盘视图是<main>中的第一部分。
<div v-if="currentView === 'dashboard'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">仪表盘</h1>
<p class="text-gray-600">欢迎使用图书管理系统,以下是系统概览</p>
</div>
<!-- 仪表盘内容 -->
</div>
v-if 是Vue.js的条件渲染指令,当 currentView 为 'dashboard' 时显示该部分内容。
仪表盘视图又分为上中下三部分,分别是统计卡片、图表区域和最近借阅记录。仪表盘的界面效果如下:

(1) 统计卡片:
统计卡片是仪表盘视图中的第一部分。
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">总藏书量</p>
<h3 class="text-3xl font-bold mt-1">{{ books.length }}</h3>
<p class="text-success text-sm mt-2">
<i class="fa fa-arrow-up"></i> 5.2% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
<i class="fa fa-book text-primary text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">借出图书</p>
<h3 class="text-3xl font-bold mt-1">{{ borrowedBooksCount }}</h3>
<p class="text-danger text-sm mt-2">
<i class="fa fa-arrow-down"></i> 2.8% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center">
<i class="fa fa-exchange text-accent text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">注册用户</p>
<h3 class="text-3xl font-bold mt-1">{{ users.length }}</h3>
<p class="text-success text-sm mt-2">
<i class="fa fa-arrow-up"></i> 12.3% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-secondary/10 flex items-center justify-center">
<i class="fa fa-users text-secondary text-xl"></i>
</div>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow scale-hover">
<div class="flex justify-between items-start">
<div>
<p class="text-gray-500 text-sm">逾期未还</p>
<h3 class="text-3xl font-bold mt-1">{{ overdueBooksCount }}</h3>
<p class="text-warning text-sm mt-2">
<i class="fa fa-arrow-up"></i> 3.1% <span class="text-gray-500">较上月</span>
</p>
</div>
<div class="w-12 h-12 rounded-full bg-danger/10 flex items-center justify-center">
<i class="fa fa-calendar-times-o text-danger text-xl"></i>
</div>
</div>
</div>
</div>
使用网格布局显示统计卡片,每个卡片显示不同的统计信息,如总藏书量 、借出图书 、注册用户 和逾期未还数量 。{{ books.length }} 是Vue.js的插值表达式,用于显示 books 数组的长度。
(2) 图表区域:
图表区域是仪表盘视图中的第二部分。
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl p-6 card-shadow lg:col-span-2">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">借阅趋势</h3>
<div class="flex space-x-2">
<button class="px-3 py-1 text-sm rounded-md bg-primary/10 text-primary">周</button>
<button class="px-3 py-1 text-sm rounded-md hover:bg-gray-100">月</button>
<button class="px-3 py-1 text-sm rounded-md hover:bg-gray-100">年</button>
</div>
</div>
<div class="h-80">
<canvas id="borrowChart"></canvas>
</div>
</div>
<div class="bg-white rounded-xl p-6 card-shadow">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">分类统计</h3>
<button class="text-primary hover:text-primary/80">
<i class="fa fa-refresh"></i>
</button>
</div>
<div class="h-80">
<canvas id="categoryChart"></canvas>
</div>
</div>
</div>
使用网格布局显示两个图表区域,分别是借阅趋势 和分类统计 ,使用 canvas 元素绘制图表。
(3) 最近借阅记录:
最近借阅记录是仪表盘视图中的第三部分。
<div class="bg-white rounded-xl p-6 card-shadow">
<div class="flex justify-between items-center mb-6">
<h3 class="font-bold text-lg">最近借阅记录</h3>
<button class="text-primary hover:text-primary/80">查看全部</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">图书</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅人</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">应归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="borrow in recentBorrows" :key="borrow.id" class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" :src="borrow.book.cover" alt="">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ borrow.book.title }}</div>
<div class="text-sm text-gray-500">{{ borrow.book.author }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ borrow.user?.name || '未知用户' }}</div>
<div class="text-sm text-gray-500">{{ borrow.user?.studentId || '未知ID' }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.borrowDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.dueDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="borrow.isReturned" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
已归还
</span>
<span v-else-if="isOverdue(borrow.dueDate)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
已逾期
</span>
<span v-else class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
借阅中
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
使用表格显示最近借阅记录,v-for是Vue.js的列表渲染指令,用于遍历 recentBorrows 数组并渲染表格行。
2. 图书管理视图 (<div v-if="currentView === 'books'">)
图书管理视图是<main>中的第二部分。
<div v-if="currentView === 'books'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">图书管理</h1>
<p class="text-gray-600">管理系统中的所有图书信息</p>
</div>
<!-- 图书管理内容 -->
</div>
当 currentView 为 'books' 时显示该部分内容。
可以在index.html中搜索const currentView = ref('dashboard');将其中的'dashboard'改为'books' ,再刷新页面,即可看到内容。图书管理视图分为搜索和筛选、图书列表、分页三个部分。界面效果如下:

(1) 搜索和筛选:
搜索和筛选是图书管理视图中的第一部分。
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="bookSearchQuery" placeholder="搜索图书标题/作者" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="bookCategoryFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有分类</option>
<option value="计算机">计算机</option>
<option value="文学">文学</option>
<option value="历史">历史</option>
<option value="科学">科学</option>
<option value="艺术">艺术</option>
<option value="经济">经济</option>
</select>
</div>
<div>
<select v-model="bookStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="available">可借阅</option>
<option value="borrowed">已借出</option>
</select>
</div>
<div class="flex justify-end">
<button @click="openBookModal(null)" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 添加图书
</button>
</div>
</div>
</div>
提供搜索框和下拉选择框用于筛选图书,v-model是Vue.js的双向数据绑定指令,将输入框和下拉选择框的值与Vue实例中的数据绑定。点击"添加图书"按钮调用openBookModal方法,弹出添加图书模态窗。
(2) 图书列表:
图书列表是图书管理视图中的第二部分。
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="book in filteredBooks" :key="book.id" class="bg-white rounded-xl overflow-hidden card-shadow scale-hover">
<div class="flex">
<div class="w-1/3 bg-gray-200">
<img :src="book.cover" alt="Book cover" class="w-full h-full object-cover">
</div>
<div class="w-2/3 p-4">
<h3 class="font-bold text-lg mb-1 line-clamp-1">{{ book.title }}</h3>
<p class="text-gray-600 text-sm mb-1 line-clamp-1">{{ book.author }}</p>
<p class="text-gray-500 text-xs mb-3">
<span class="bg-gray-100 px-2 py-0.5 rounded text-xs">{{ book.category }}</span>
</p>
<div class="flex justify-between items-center mt-auto">
<span v-if="book.isBorrowed" class="text-xs px-2 py-1 bg-red-100 text-red-800 rounded-full">
已借出
</span>
<span v-else class="text-xs px-2 py-1 bg-green-100 text-green-800 rounded-full">
可借阅
</span>
<div class="flex space-x-1">
<button @click="openBookModal(book)" class="p-1.5 rounded-full hover:bg-gray-100 text-gray-600 transition-colors">
<i class="fa fa-edit"></i>
</button>
<button @click="deleteBook(book.id)" class="p-1.5 rounded-full hover:bg-red-100 text-red-600 transition-colors">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
使用网格布局显示图书列表,v-for 遍历 filteredBooks 数组并渲染图书卡片。点击"编辑"按钮调用 openBookModal 方法,弹出编辑图书模态窗,点击"删除"按钮调用 deleteBook 方法,弹出确认删除图书模态窗。
(3) 分页:
分页是图书管理视图中的第三部分。
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentPage - 1) * booksPerPage + 1 }} 到 {{ Math.min(currentPage * booksPerPage, filteredBooks.length) }} 共 {{ filteredBooks.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevPage" :disabled="currentPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalPages" :key="page" @click="currentPage = page" :class="{'bg-primary text-white border-primary': page === currentPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextPage" :disabled="currentPage === totalPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
提供分页功能,显示当前页码和总记录数。点击"上一页"和"下一页"按钮分别调用 prevPage 和 nextPage 方法,点击页码按钮更新 currentPage 的值。
3. 借阅管理视图 (<div v-if="currentView === 'borrows'">)
借阅管理视图是<main>中的第三部分。
<div v-if="currentView === 'borrows'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">借阅管理</h1>
<p class="text-gray-600">管理图书的借阅和归还</p>
</div>
<!-- 借阅管理内容 -->
</div>
当 currentView 为 'borrows' 时显示该部分内容。
可以在index.html中搜索const currentView = ref('dashboard');将其中的'dashboard'改为'borrows' ,再刷新页面,即可看到内容。借阅管理视图分为搜索和筛选、借阅记录列表、分页三个部分。界面效果如下:

(1) 搜索和筛选:
搜索和筛选是借阅管理视图中的第一部分。
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="borrowSearchQuery" placeholder="搜索图书/借阅人" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="borrowStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="borrowed">借阅中</option>
<option value="returned">已归还</option>
<option value="overdue">已逾期</option>
</select>
</div>
<div>
<div class="flex items-center space-x-2">
<button class="w-full px-4 py-2 rounded-lg border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-calendar mr-2"></i> 时间范围
</button>
</div>
</div>
<div class="flex justify-end">
<button @click="openBorrowModal" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 新增借阅
</button>
</div>
</div>
</div>
提供搜索框和下拉选择框用于筛选借阅记录,v-model 实现双向数据绑定。点击"新增借阅"按钮调用openBorrowModal方法,弹出新增借阅记录模态窗。
(2)借阅记录 列表:
借阅记录列表是借阅管理视图中的第二部分。
<div class="bg-white rounded-xl p-6 card-shadow overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">图书信息</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅人</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">借阅日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">应归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">实际归还日期</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="borrow in filteredBorrows" :key="borrow.id" class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img class="h-10 w-10 rounded-full" :src="borrow.book?.cover || 'https://picsum.photos/seed/default/100/100'" alt="Book cover">
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ borrow.book?.title || '未知图书' }}</div>
<div class="text-sm text-gray-500">{{ borrow.book?.author || '未知作者' }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ borrow.user.name }}</div>
<div class="text-sm text-gray-500">{{ borrow.user.studentId }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.borrowDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.dueDate }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ borrow.returnDate || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span v-if="borrow.isReturned" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
已归还
</span>
<span v-else-if="isOverdue(borrow.dueDate)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
已逾期
</span>
<span v-else class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
借阅中
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex justify-end space-x-1">
<button v-if="!borrow.isReturned" @click="returnBook(borrow)" class="p-1.5 rounded-full hover:bg-green-100 text-green-600 transition-colors">
<i class="fa fa-check"></i> 归还
</button>
<button @click="viewBorrowDetails(borrow)" class="p-1.5 rounded-full hover:bg-gray-100 text-gray-600 transition-colors">
<i class="fa fa-eye"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
使用网格布局显示借阅列表,v-for 遍历 filteredBorrows数组并渲染借阅表格卡片。点击"归还"按钮调用 returnBook方法,弹出确认归还图书模态窗,点击"查看详情"按钮调用 viewBorrowDetails方法,弹出该书的借阅详情和借阅历史模态窗。
在借阅管理视图中,filteredBorrows计算属性可能没有正确地将图书和用户对象关联到借阅记录中,导致可能部分数据对不上,因此对borrow.book进行了空值检查。
(3) 分页:
分页是借阅管理视图中的第三部分。
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentBorrowPage - 1) * borrowsPerPage + 1 }} 到 {{ Math.min(currentBorrowPage * borrowsPerPage, filteredBorrows.length) }} 共 {{ filteredBorrows.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevBorrowPage" :disabled="currentBorrowPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalBorrowPages" :key="page" @click="currentBorrowPage = page" :class="{'bg-primary text-white border-primary': page === currentBorrowPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentBorrowPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextBorrowPage" :disabled="currentBorrowPage === totalBorrowPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
提供分页功能,显示当前页码和总记录数。点击"上一页"和"下一页"按钮分别调用 prevBorrowPage和 nextBorrowPage方法,点击页码按钮更新 currentBorrowPage的值。
4 . 用户管理视图 (<div v-if="currentView === 'users'">)
用户管理视图是<main>中的第四部分。
<div v-if="currentView === 'users'">
<div class="mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold mb-2">用户管理</h1>
<p class="text-gray-600">管理系统中的所有用户</p>
</div>
<!-- 用户管理内容 -->
</div>
当 currentView 为 'users' 时显示该部分内容。
可以在index.html中搜索const currentView = ref('dashboard');将其中的'dashboard'改为'users' ,再刷新页面,即可看到内容。借阅管理视图分为搜索和筛选、借阅记录列表、分页三个部分。界面效果如下:

(1) 搜索和筛选:
搜索和筛选是用户管理视图中的第一部分。
<div class="bg-white rounded-xl p-6 card-shadow mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="relative">
<input type="text" v-model="userSearchQuery" placeholder="搜索用户名/学号" class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<div>
<select v-model="userRoleFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有角色</option>
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<select v-model="userStatusFilter" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">所有状态</option>
<option value="active">活跃</option>
<option value="blocked">已封禁</option>
</select>
</div>
<div class="flex justify-end">
<button @click="openUserModal(null)" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fa fa-plus mr-2"></i> 添加用户
</button>
</div>
</div>
</div>
提供搜索框和下拉选择框用于筛选用户信息,v-model 实现双向数据绑定。
(2)用户 列表:
用户列表是用户管理视图中的第二部分。
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="user in filteredUsers" :key="user.id" class="bg-white rounded-xl overflow-hidden card-shadow scale-hover">
<div class="p-4">
<div class="flex items-center mb-4">
<div class="w-16 h-16 rounded-full bg-gray-200 overflow-hidden">
<img :src="user.avatar || 'https://picsum.photos/seed/defaultuser/200/200'" alt="User avatar" class="w-full h-full object-cover">
</div>
<div class="ml-4">
<h3 class="font-bold text-lg">{{ user.name || '未知用户' }}</h3>
<p class="text-gray-600 text-sm">{{ user.studentId || '未知ID' }}</p>
<div class="flex items-center mt-1">
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-800">
{{ user.role === 'student' ? '学生' : user.role === 'teacher' ? '教师' : '管理员' }}
</span>
<span v-if="user.isBlocked" class="ml-2 text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-800">
已封禁
</span>
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 text-center mb-4">
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">借阅中</p>
<p class="font-bold">{{ getBorrowingCount(user.id) }}</p>
</div>
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">已归还</p>
<p class="font-bold">{{ getReturnedCount(user.id) }}</p>
</div>
<div class="bg-gray-50 p-2 rounded-lg">
<p class="text-sm text-gray-500">逾期</p>
<p class="font-bold text-red-500">{{ getOverdueCount(user.id) }}</p>
</div>
</div>
<div class="flex justify-end space-x-2">
<button @click="openUserModal(user)" class="px-3 py-1.5 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
<i class="fa fa-edit mr-1"></i> 编辑
</button>
<button @click="toggleUserBlock(user)" class="px-3 py-1.5 rounded-lg" :class="user.isBlocked ? 'bg-green-500 text-white hover:bg-green-600' : 'bg-red-500 text-white hover:bg-red-600'">
<i class="fa" :class="user.isBlocked ? 'fa-unlock-alt mr-1' : 'fa-lock mr-1'"></i>
{{ user.isBlocked ? '解封' : '封禁' }}
</button>
</div>
</div>
</div>
</div>
使用网格布局显示用户列表,v-for 遍历 filteredUsers数组并渲染用户卡片。点击"编辑"按钮调用 openUserModal方法,弹出编辑用户模态窗,点击"封禁"按钮调用 toggleUserBlock方法,弹出确认是否封禁用户的模态窗。
(3) 分页:
分页是用户管理视图中的第三部分。
<div class="mt-8 flex justify-between items-center">
<div class="text-sm text-gray-500">
显示 {{ (currentUserPage - 1) * usersPerPage + 1 }} 到 {{ Math.min(currentUserPage * usersPerPage, filteredUsers.length) }} 共 {{ filteredUsers.length }} 条记录
</div>
<div class="flex space-x-1">
<button @click="prevUserPage" :disabled="currentUserPage === 1" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<button v-for="page in totalUserPages" :key="page" @click="currentUserPage = page" :class="{'bg-primary text-white border-primary': page === currentUserPage, 'border-gray-300 text-gray-600 hover:bg-gray-50': page !== currentUserPage}" class="px-3 py-1 rounded border transition-colors">
{{ page }}
</button>
<button @click="nextUserPage" :disabled="currentUserPage === totalUserPages" class="px-3 py-1 rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
提供分页功能,显示当前页码和总记录数。点击"上一页"和"下一页"按钮分别调用 prevUserPage和 nextUserPage方法,点击页码按钮更新 currentUserPage的值。
( 四 ) <body>部分之 页脚 (<footer>)
通常页脚会包含系统标题、版权信息和社交链接等内容,用于提供额外的信息和导航。
<footer class="bg-white border-t border-gray-200 py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<div class="flex items-center">
<i class="fa fa-book text-primary text-xl mr-2"></i>
<span class="font-bold text-lg">图书管理系统</span>
</div>
<p class="text-gray-500 text-sm mt-1">信息管理与信息系统专业课程设计</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-github text-xl"></i>
</a>
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-envelope text-xl"></i>
</a>
<a href="#" class="text-gray-500 hover:text-primary transition-colors">
<i class="fa fa-linkedin text-xl"></i>
</a>
</div>
</div>
<div class="mt-6 pt-6 border-t border-gray-100 text-center text-gray-500 text-sm">
© 2025 图书管理系统 | 设计与开发
</div>
</div>
</footer>
页脚包含系统标题、版权信息和社交链接。界面效果如下:

( 五 ) <body>部分之 模态框
在这个图书管理系统中,模态框起到了重要的交互作用,它可以在不切换页面的情况下让用户完成特定的操作,包括了添加/编辑图书模态框 、新增借阅模态框 、添加/编辑用户模态框 、借阅详情模态框 、确认对话框这些模态窗。以下将对系统中的模态框进行详细解析。
接着<nav>继续实现body中的其他内容。放在<main>标签中。<main>与<nav>对齐。如上代码树形结构可以看出来。
<body>
├── <div id="app">
├── <nav> 导航栏
├── <main> 主内容区
├── <footer> 页脚
└── 模态框部分
├── <div v-if="isBookModalOpen"> 添加/编辑图书模态框
├── <div v-if="isBorrowModalOpen"> 新增借阅模态框
├── <div v-if="isUserModalOpen"> 添加/编辑用户模态框
├── <div v-if="isBorrowDetailsModalOpen"> 借阅详情模态框
└── <div v-if="isConfirmDialogOpen"> 确认对话框
1. 添加/编辑图书模态框
在代码中,模态框的代码是与<footer>标签对齐的。添加/编辑图书模态框的相关代码如下:
<!-- 添加/编辑图书模态框 -->
<div v-if="isBookModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBookModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">{{ editingBook ? '编辑图书' : '添加图书' }}</h3>
<button @click="closeBookModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveBook">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">图书封面 URL</label>
<input type="text" v-model="form.bookCover" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">图书标题</label>
<input type="text" v-model="form.bookTitle" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">作者</label>
<input type="text" v-model="form.bookAuthor" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">出版社</label>
<input type="text" v-model="form.bookPublisher" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">出版年份</label>
<input type="number" v-model="form.bookYear" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">分类</label>
<select v-model="form.bookCategory" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="计算机">计算机</option>
<option value="文学">文学</option>
<option value="历史">历史</option>
<option value="科学">科学</option>
<option value="艺术">艺术</option>
<option value="经济">经济</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">ISBN</label>
<input type="text" v-model="form.bookISBN" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">简介</label>
<textarea v-model="form.bookDescription" rows="4" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeBookModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
代码解析:
- 显示与隐藏控制:使用v-if="isBookModalOpen"来控制模态框的显示与隐藏,isBookModalOpen是一个Vue响应式数据。
- 背景遮罩:class="fixed inset-0 bg-black bg-opacity-50" 创建了一个覆盖整个屏幕的半透明黑色背景,增强了模态框的聚焦效果。
- 标题动态显示:通过 {{ editingBook ? '编辑图书' : '添加图书' }} 根据是否处于编辑状态动态显示标题。
- 表单数据绑定:使用 v-model 指令将表单输入项与 form 对象的属性进行绑定,方便数据的收集和处理。
- 关闭模态框:点击背景(@click.self="closeBookModal")或关闭按钮(@click="closeBookModal")可以关闭模态框。
- 表单提交:表单提交事件使用 @submit.prevent="saveBook" 阻止默认提交行为,并调用 saveBook 方法保存图书信息。
添加图书模态框的效果如下:

编辑图书模态框的效果如下:

2. 新增借阅模态框
新增借阅模态框的代码如下:
<div v-if="isBorrowModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBorrowModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">新增借阅记录</h3>
<button @click="closeBorrowModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveBorrow">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">选择图书</label>
<select v-model="form.borrowBookId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">请选择图书</option>
<option v-for="book in availableBooks" :key="book.id" :value="book.id">
{{ book.title }} - {{ book.author }}
</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">选择用户</label>
<select v-model="form.borrowUserId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="">请选择用户</option>
<option v-for="user in users" :key="user.id" :value="user.id">
{{ user.name }} - {{ user.studentId }}
</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">借阅日期</label>
<input type="date" v-model="form.borrowDate" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">应归还日期</label>
<input type="date" v-model="form.dueDate" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeBorrowModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
代码解析:
- 显示与隐藏控制:通过一个布尔型的Vue响应式数据来控制模态框的显示与隐藏。
- 背景遮罩:创建一个覆盖整个屏幕的半透明背景。
- 表单元素:包含图书选择、借阅人选择、借阅日期、应归还日期等表单输入项。
- 关闭和提交按钮:提供关闭模态框和提交借阅信息的按钮。
新增借阅记录模态框的效果如下:

3. 添加/编辑用户模态框
添加/编辑用户模态框的代码如下:
<div v-if="isUserModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeUserModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">{{ editingUser ? '编辑用户' : '添加用户' }}</h3>
<button @click="closeUserModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<form @submit.prevent="saveUser">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">用户头像 URL</label>
<input type="text" v-model="form.userAvatar" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">姓名</label>
<input type="text" v-model="form.userName" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">学号/工号</label>
<input type="text" v-model="form.userStudentId" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">角色</label>
<select v-model="form.userRole" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
<option value="student">学生</option>
<option value="teacher">教师</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">联系方式</label>
<input type="text" v-model="form.userContact" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
<input type="email" v-model="form.userEmail" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">备注</label>
<textarea v-model="form.userNotes" rows="3" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
</div>
<div class="flex justify-end space-x-3 mt-6">
<button type="button" @click="closeUserModal" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
保存
</button>
</div>
</form>
</div>
</div>
</div>
代码解析:
- 显示与隐藏控制:使用一个布尔型的 Vue 响应式数据来控制模态框的显示与隐藏。
- 背景遮罩:创建一个覆盖整个屏幕的半透明背景。
- 表单元素:包含用户名、学号、角色、状态等表单输入项。
- 关闭和提交按钮:提供关闭模态框和保存用户信息的按钮。
添加用户的模态框效果如下:

编辑用户的模态框效果如下:

4. 借阅详情模态框
借阅详情模态框用于显示借阅记录的详细信息,代码如下:
<div v-if="isBorrowDetailsModalOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeBorrowDetailsModal">
<div class="bg-white rounded-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-100">
<div class="flex justify-between items-center">
<h3 class="text-lg font-bold">借阅详情</h3>
<button @click="closeBorrowDetailsModal" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<div class="flex items-center mb-6">
<div class="w-20 h-20 rounded-lg bg-gray-200 overflow-hidden">
<img :src="selectedBorrow.book.cover" alt="Book cover" class="w-full h-full object-cover">
</div>
<div class="ml-4">
<h3 class="font-bold text-lg">{{ selectedBorrow.book.title }}</h3>
<p class="text-gray-600 text-sm">{{ selectedBorrow.book.author }}</p>
<p class="text-gray-500 text-xs mt-1">
<span class="bg-gray-100 px-2 py-0.5 rounded text-xs">{{ selectedBorrow.book.category }}</span>
</p>
</div>
</div>
<div class="space-y-4">
<div class="flex justify-between">
<span class="text-gray-600">借阅人</span>
<span class="font-medium">{{ selectedBorrow.user.name }} ({{ selectedBorrow.user.studentId }})</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">借阅日期</span>
<span class="font-medium">{{ selectedBorrow.borrowDate }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">应归还日期</span>
<span class="font-medium">{{ selectedBorrow.dueDate }}</span>
</div>
<div class="flex justify-between" v-if="selectedBorrow.returnDate">
<span class="text-gray-600">实际归还日期</span>
<span class="font-medium">{{ selectedBorrow.returnDate }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">借阅状态</span>
<span class="font-medium" :class="getStatusColor(selectedBorrow)">
{{ getStatusText(selectedBorrow) }}
</span>
</div>
<div class="flex justify-between" v-if="isOverdue(selectedBorrow.dueDate) && !selectedBorrow.isReturned">
<span class="text-gray-600">逾期天数</span>
<span class="font-medium text-red-500">{{ getOverdueDays(selectedBorrow.dueDate) }} 天</span>
</div>
</div>
<div class="mt-6 pt-6 border-t border-gray-100">
<h4 class="font-medium mb-3">借阅历史</h4>
<div class="space-y-3">
<div class="bg-gray-50 p-3 rounded-lg" v-for="history in getBorrowHistory(selectedBorrow.book.id)" :key="history.id">
<div class="flex justify-between text-sm">
<span class="font-medium">{{ history.user.name }}</span>
<span class="text-gray-500">{{ history.borrowDate }} - {{ history.returnDate || '未归还' }}</span>
</div>
<div class="flex justify-between text-xs mt-1">
<span>{{ history.isReturned ? '已归还' : '借阅中' }}</span>
<span v-if="history.isReturned && history.returnDate > history.dueDate" class="text-red-500">
逾期 {{ getOverdueDays(history.dueDate, history.returnDate) }} 天
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
代码解析:
- 显示与隐藏控制:通过一个布尔型的 Vue 响应式数据来控制模态框的显示与隐藏。
- 背景遮罩:创建一个覆盖整个屏幕的半透明背景。
- 详情信息展示:显示图书信息、借阅人信息、借阅日期、应归还日期、实际归还日期、状态等详细信息。
- 关闭按钮:提供关闭模态框的按钮。
借阅详情模态框效果如下:

5. 确认对话框
确认对话框通常用于确认一些重要的操作,如删除图书 、归还确认、封禁用户等。其实现方式如下:
<!-- 确认对话框示例 -->
<div v-if="isConfirmDialogOpen" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" @click.self="closeConfirmDialog">
<div class="bg-white rounded-xl w-full max-w-md p-6">
<h3 class="text-lg font-bold mb-3">{{ confirmDialogTitle }}</h3>
<p class="text-gray-600 mb-6">{{ confirmDialogMessage }}</p>
<div class="flex justify-end space-x-3">
<button @click="closeConfirmDialog" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button @click="confirmAction" class="px-4 py-2 rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors">
确认
</button>
</div>
</div>
</div>
代码解析:
- 显示与隐藏控制:使用 v-if="isConfirmDialogOpen" 来控制对话框的显示与隐藏,isConfirmDialogOpen 是一个Vue响应式数据。
- 背景遮罩:创建一个覆盖整个屏幕的半透明黑色背景。
- 确认信息显示:通过{{ confirmDialogTitle }}和 {{ confirmDialogMessage}} 动态显示标题和确认信息。
- 取消和确认按钮:点击取消按钮(@click="closeConfirmDialog")关闭对话框,点击确认按钮(@click="confirmAction")执行相应的操作。
封禁用户的模态框效果如下:

综上所述,这些模态框通过Vue的响应式数据和事件处理机制,结合CSS样式,实现了良好的用户交互体验。
四 、Vue.js原理说明
(一) 响应式原理
Vue.js利用Object.defineProperty ()或Proxy实现数据的响应式。当数据发生变化时,Vue 会自动更新与之绑定的DOM元素。例如,{{ books.length }} 会随着 books 数组的变化而更新显示。
(二) 指令系统
(1)v-if:条件渲染指令,根据表达式的值决定是否渲染元素。
(2)v-for:列表渲染指令,用于遍历数组或对象并渲染元素。
(3)v-model:双向数据绑定指令,将表单元素的值与Vue实例中的数据绑定。
(4):class:动态类绑定指令,根据表达式的值动态添加或移除类名。
(5)@click:事件绑定指令,绑定点击事件并调用Vue实例中的方法。
(三) 事件处理
通过 @ 符号绑定DOM事件,如 @click、@submit 等,可调用Vue实例中的方法。例如,点击"添加图书"按钮调用 openBookModal 方法。
(四) 组件化开发
虽然当前系统的代码未明确使用组件,但Vue.js支持将页面拆分为多个组件,提高代码的可维护性和复用性。
有关Vue 3构建的图书管理系统的JavaScript逻辑部分,将在下一篇文章中讲解。