parent
							
								
									b536f77447
								
							
						
					
					
						commit
						134bb8e963
					
				@ -0,0 +1,24 @@
 | 
				
			||||
# Logs
 | 
				
			||||
logs
 | 
				
			||||
*.log
 | 
				
			||||
npm-debug.log*
 | 
				
			||||
yarn-debug.log*
 | 
				
			||||
yarn-error.log*
 | 
				
			||||
pnpm-debug.log*
 | 
				
			||||
lerna-debug.log*
 | 
				
			||||
 | 
				
			||||
node_modules
 | 
				
			||||
dist
 | 
				
			||||
dist-ssr
 | 
				
			||||
*.local
 | 
				
			||||
 | 
				
			||||
# Editor directories and files
 | 
				
			||||
.vscode/*
 | 
				
			||||
!.vscode/extensions.json
 | 
				
			||||
.idea
 | 
				
			||||
.DS_Store
 | 
				
			||||
*.suo
 | 
				
			||||
*.ntvs*
 | 
				
			||||
*.njsproj
 | 
				
			||||
*.sln
 | 
				
			||||
*.sw?
 | 
				
			||||
@ -0,0 +1,3 @@
 | 
				
			||||
{
 | 
				
			||||
  "recommendations": ["Vue.volar"]
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,5 @@
 | 
				
			||||
# Vue 3 + Vite
 | 
				
			||||
 | 
				
			||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
 | 
				
			||||
 | 
				
			||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
 | 
				
			||||
@ -0,0 +1,13 @@
 | 
				
			||||
<!doctype html>
 | 
				
			||||
<html lang="en">
 | 
				
			||||
  <head>
 | 
				
			||||
    <meta charset="UTF-8" />
 | 
				
			||||
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 | 
				
			||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
				
			||||
    <title>Library_system</title>
 | 
				
			||||
  </head>
 | 
				
			||||
  <body>
 | 
				
			||||
    <div id="app"></div>
 | 
				
			||||
    <script type="module" src="/src/main.js"></script>
 | 
				
			||||
  </body>
 | 
				
			||||
</html>
 | 
				
			||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								@ -0,0 +1,26 @@
 | 
				
			||||
{
 | 
				
			||||
  "name": "library_system",
 | 
				
			||||
  "private": true,
 | 
				
			||||
  "version": "0.0.0",
 | 
				
			||||
  "type": "module",
 | 
				
			||||
  "scripts": {
 | 
				
			||||
    "dev": "vite",
 | 
				
			||||
    "build": "vite build",
 | 
				
			||||
    "preview": "vite preview"
 | 
				
			||||
  },
 | 
				
			||||
  "dependencies": {
 | 
				
			||||
    "axios": "^1.11.0",
 | 
				
			||||
    "element-plus": "^2.10.4",
 | 
				
			||||
    "pinia": "^3.0.3",
 | 
				
			||||
    "vue": "^3.5.17",
 | 
				
			||||
    "vue-router": "^4.5.1",
 | 
				
			||||
    "vuex": "^4.1.0"
 | 
				
			||||
  },
 | 
				
			||||
  "devDependencies": {
 | 
				
			||||
    "@types/node": "^24.1.0",
 | 
				
			||||
    "@vitejs/plugin-vue": "^6.0.0",
 | 
				
			||||
    "unplugin-auto-import": "^19.3.0",
 | 
				
			||||
    "unplugin-vue-components": "^28.8.0",
 | 
				
			||||
    "vite": "^7.0.4"
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
@ -0,0 +1,62 @@
 | 
				
			||||
/* Element Plus 主题定制 */
 | 
				
			||||
:root {
 | 
				
			||||
  --el-color-primary: #165dff;
 | 
				
			||||
  --el-color-primary-light-3: #3c8dff;
 | 
				
			||||
  --el-color-primary-light-5: #6baaff;
 | 
				
			||||
  --el-color-primary-light-7: #a3cfff;
 | 
				
			||||
  --el-color-primary-light-8: #d6eaff;
 | 
				
			||||
  --el-color-primary-light-9: #f4faff;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
body {
 | 
				
			||||
  font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
 | 
				
			||||
  background: #f6f8fa;
 | 
				
			||||
  color: #222;
 | 
				
			||||
  margin: 0;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.el-header, .el-footer {
 | 
				
			||||
  background: #fff;
 | 
				
			||||
  box-shadow: 0 2px 8px 0 rgba(0,0,0,0.03);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.el-menu {
 | 
				
			||||
  border-right: none;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.el-card {
 | 
				
			||||
  border-radius: 12px;
 | 
				
			||||
  box-shadow: 0 2px 12px 0 rgba(22,93,255,0.04);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.el-main {
 | 
				
			||||
  padding: 32px 24px 24px 24px;
 | 
				
			||||
  min-height: 80vh;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
a {
 | 
				
			||||
  color: var(--el-color-primary);
 | 
				
			||||
  text-decoration: none;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
a:hover {
 | 
				
			||||
  text-decoration: underline;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
/* 登录/注册页面居中 */
 | 
				
			||||
.page-center {
 | 
				
			||||
  min-height: 100vh;
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  justify-content: center;
 | 
				
			||||
  background: linear-gradient(135deg, #e3f0ff 0%, #f6f8fa 100%);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
/* 头像样式 */
 | 
				
			||||
.avatar {
 | 
				
			||||
  width: 48px;
 | 
				
			||||
  height: 48px;
 | 
				
			||||
  border-radius: 50%;
 | 
				
			||||
  object-fit: cover;
 | 
				
			||||
  box-shadow: 0 2px 8px 0 rgba(22,93,255,0.08);
 | 
				
			||||
}
 | 
				
			||||
| 
		 After Width: | Height: | Size: 496 B  | 
@ -0,0 +1,88 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <el-card class="book-card" shadow="hover" @click="$emit('click')">
 | 
				
			||||
    <div class="book-cover">
 | 
				
			||||
      <el-image :src="book.url" fit="cover" class="cover-image" :alt="book.title" />
 | 
				
			||||
    </div>
 | 
				
			||||
    <div class="book-info">
 | 
				
			||||
      <h3 class="book-title">{{ book.title }}</h3>
 | 
				
			||||
      <div class="book-meta" v-if="showMeta">
 | 
				
			||||
        <el-tag type="info" size="small">{{ book.content }}</el-tag>
 | 
				
			||||
        <el-tag type="success" size="small">¥{{ book.money }}/天</el-tag>
 | 
				
			||||
      </div>
 | 
				
			||||
      <div class="book-status"v-if="showMeta">
 | 
				
			||||
        <el-tag :type="book.state" size="small">
 | 
				
			||||
          {{ book.state }}
 | 
				
			||||
        </el-tag>
 | 
				
			||||
        <span>阅读量: {{ book.number }}次</span>
 | 
				
			||||
        <el-button @click="$emit('borrow')">借阅</el-button>
 | 
				
			||||
      </div>
 | 
				
			||||
    </div>
 | 
				
			||||
  </el-card>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
defineProps({
 | 
				
			||||
  book: {
 | 
				
			||||
    type: Object,
 | 
				
			||||
    required: true
 | 
				
			||||
  },
 | 
				
			||||
  showMeta: {
 | 
				
			||||
    type: Boolean,
 | 
				
			||||
    default: true
 | 
				
			||||
  }
 | 
				
			||||
})
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.book-card {
 | 
				
			||||
  cursor: pointer;
 | 
				
			||||
  transition: transform 0.3s;
 | 
				
			||||
  height: 100%;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-card:hover {
 | 
				
			||||
  transform: translateY(-5px);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-cover {
 | 
				
			||||
  height: 180px;
 | 
				
			||||
  overflow: hidden;
 | 
				
			||||
  border-radius: 4px;
 | 
				
			||||
  margin-bottom: 15px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.cover-image {
 | 
				
			||||
  width: 100%;
 | 
				
			||||
  height: 100%;
 | 
				
			||||
  object-fit: cover;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-info {
 | 
				
			||||
  padding: 0 10px 10px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-title {
 | 
				
			||||
  margin: 0 0 10px;
 | 
				
			||||
  font-size: 16px;
 | 
				
			||||
  height: 24px;
 | 
				
			||||
  overflow: hidden;
 | 
				
			||||
  text-overflow: ellipsis;
 | 
				
			||||
  display: -webkit-box;
 | 
				
			||||
  -webkit-box-orient: vertical;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-meta {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  gap: 5px;
 | 
				
			||||
  margin-bottom: 10px;
 | 
				
			||||
  flex-wrap: wrap;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-status {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: space-between;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  font-size: 13px;
 | 
				
			||||
  color: #666;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,249 @@
 | 
				
			||||
<!-- src/components/HeaderBar.vue -->
 | 
				
			||||
<template>
 | 
				
			||||
  <el-header class="header">
 | 
				
			||||
    <div class="logo">
 | 
				
			||||
      <el-icon><Reading /></el-icon>
 | 
				
			||||
      <span>图书馆管理系统</span>
 | 
				
			||||
    </div>
 | 
				
			||||
    <div class="nav">
 | 
				
			||||
      <el-menu 
 | 
				
			||||
        :default-active="activeIndex" 
 | 
				
			||||
        mode="horizontal" 
 | 
				
			||||
        @select="handleSelect"
 | 
				
			||||
        background-color="#545c64"
 | 
				
			||||
        text-color="#fff"
 | 
				
			||||
        active-text-color="#ffd04b">
 | 
				
			||||
        <el-menu-item index="home">首页</el-menu-item>
 | 
				
			||||
        <el-menu-item index="books">图书查询</el-menu-item>
 | 
				
			||||
        <el-menu-item index="borrow">借阅图书</el-menu-item>
 | 
				
			||||
        <el-menu-item index="return">归还图书</el-menu-item>
 | 
				
			||||
        <el-sub-menu index="ranking">
 | 
				
			||||
          <template #title>排行榜</template>
 | 
				
			||||
          <el-menu-item index="weekly">本周热租榜</el-menu-item>
 | 
				
			||||
          <el-menu-item index="monthly">本月热租榜</el-menu-item>
 | 
				
			||||
        </el-sub-menu>
 | 
				
			||||
        <el-menu-item index="bookManagement" v-if="user && user.admin">
 | 
				
			||||
          图书管理
 | 
				
			||||
        </el-menu-item>
 | 
				
			||||
        <el-menu-item index="allBorrowRecords" v-if="user && user.admin">
 | 
				
			||||
          用户借阅记录
 | 
				
			||||
        </el-menu-item>
 | 
				
			||||
      </el-menu>
 | 
				
			||||
    </div>
 | 
				
			||||
    <div class="user-info">
 | 
				
			||||
      <el-dropdown v-if="user">
 | 
				
			||||
        <span class="el-dropdown-link">
 | 
				
			||||
          <el-avatar :src="user.pic" size="small" />
 | 
				
			||||
          <span class="username">{{ user.username }}</span>
 | 
				
			||||
          <el-icon><ArrowDown /></el-icon>
 | 
				
			||||
        </span>
 | 
				
			||||
        <template #dropdown>
 | 
				
			||||
  <el-dropdown-menu>
 | 
				
			||||
    <!-- 用户信息展示区域 -->
 | 
				
			||||
    <div class="user-dropdown-info">
 | 
				
			||||
      <div class="user-name">{{ user.username }}</div>
 | 
				
			||||
      <div class="user-vip">VIP{{ vip }}级</div>
 | 
				
			||||
      <div class="user-balance">
 | 
				
			||||
        <el-icon><Wallet /></el-icon>
 | 
				
			||||
        余额: ¥{{ balance }}
 | 
				
			||||
      </div>
 | 
				
			||||
    </div>
 | 
				
			||||
    <el-divider /> <!-- 分割线 -->
 | 
				
			||||
    
 | 
				
			||||
    <!-- 保留的功能选项 -->
 | 
				
			||||
    <el-dropdown-item @click="goToRecharge">账户充值</el-dropdown-item>
 | 
				
			||||
    <el-dropdown-item @click="goToRecords">借阅记录</el-dropdown-item>
 | 
				
			||||
    <el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
 | 
				
			||||
  </el-dropdown-menu>
 | 
				
			||||
</template>
 | 
				
			||||
      </el-dropdown>
 | 
				
			||||
      <div v-else>
 | 
				
			||||
        <el-button type="text" @click="goToLogin">登录</el-button>
 | 
				
			||||
        <el-button type="text" @click="goToRegister">注册</el-button>
 | 
				
			||||
      </div>
 | 
				
			||||
    </div>
 | 
				
			||||
  </el-header>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
 | 
				
			||||
import { useRouter } from 'vue-router'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
import { Reading, ArrowDown } from '@element-plus/icons-vue'
 | 
				
			||||
import { Wallet } from '@element-plus/icons-vue' 
 | 
				
			||||
import { computed, onMounted, watch } from 'vue'
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const router = useRouter()
 | 
				
			||||
 | 
				
			||||
onMounted(async () => {
 | 
				
			||||
  // 等待会话初始化完成
 | 
				
			||||
  if (store.state.sessionInitialized && store.getters.isAuthenticated) {
 | 
				
			||||
    await fetchUserInfo()
 | 
				
			||||
  }
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
// 监听会话状态变化
 | 
				
			||||
watch(() => store.state.sessionInitialized, async (newVal) => {
 | 
				
			||||
  if (newVal && store.getters.isAuthenticated) {
 | 
				
			||||
    await fetchUserInfo()
 | 
				
			||||
  }
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
// 添加获取用户信息的方法
 | 
				
			||||
async function fetchUserInfo() {
 | 
				
			||||
  try {
 | 
				
			||||
    // 获取余额和VIP信息
 | 
				
			||||
    await store.dispatch('fetchBalanceAndVip')
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取用户信息失败:', error)
 | 
				
			||||
    // 避免在未登录状态下显示错误信息
 | 
				
			||||
    if (store.getters.isAuthenticated) {
 | 
				
			||||
      ElMessage.error('获取用户信息失败')
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const vip = computed(() => store.state.vipLevel)
 | 
				
			||||
const balance = computed(() => store.state.balance)
 | 
				
			||||
const user =  computed(() => store.state.user)
 | 
				
			||||
console.log("user.pic:"+user.pic)
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const activeIndex = computed(() => {
 | 
				
			||||
  const routeName = router.currentRoute.value.name
 | 
				
			||||
  if (routeName === 'Home') return 'home'
 | 
				
			||||
  if (routeName === 'Books' || routeName === 'BookDetail' || routeName === 'AddBook' || routeName === 'EditBook') return 'books'
 | 
				
			||||
  if (routeName === 'BorrowBook') return 'borrow'
 | 
				
			||||
  if (routeName === 'ReturnBook') return 'return'
 | 
				
			||||
  if (routeName === 'WeeklyRank') return 'weekly'
 | 
				
			||||
  if (routeName === 'MonthlyRank') return 'monthly'
 | 
				
			||||
  if (routeName === 'BookManagement') return 'bookManagement'
 | 
				
			||||
  if (routeName === 'AllBorrowRecords') return 'allBorrowRecords'
 | 
				
			||||
  return ''
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const handleSelect = (index) => {
 | 
				
			||||
  switch(index) {
 | 
				
			||||
    case 'home':
 | 
				
			||||
      router.push('/')
 | 
				
			||||
      break
 | 
				
			||||
    case 'books':
 | 
				
			||||
      router.push('/books')
 | 
				
			||||
      break
 | 
				
			||||
    case 'borrow':
 | 
				
			||||
      router.push('/borrow')
 | 
				
			||||
      break
 | 
				
			||||
    case 'return':
 | 
				
			||||
      router.push('/return')
 | 
				
			||||
      break
 | 
				
			||||
    case 'weekly':
 | 
				
			||||
      router.push('/ranking/weekly')
 | 
				
			||||
      break
 | 
				
			||||
    case 'monthly':
 | 
				
			||||
      router.push('/ranking/monthly')
 | 
				
			||||
      break
 | 
				
			||||
    case 'bookManagement':
 | 
				
			||||
      router.push('/admin/books')
 | 
				
			||||
      break
 | 
				
			||||
    case 'allBorrowRecords':
 | 
				
			||||
      router.push('/admin/borrow-records')
 | 
				
			||||
      break
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToLogin = () => {
 | 
				
			||||
  router.push('/login')
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToRegister = () => {
 | 
				
			||||
  router.push('/register')
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToRecharge = () => {
 | 
				
			||||
  router.push('/recharge')
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToRecords = () => {
 | 
				
			||||
  router.push('/borrow-records')
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const logout = () => {
 | 
				
			||||
  store.dispatch('logout')
 | 
				
			||||
  router.push('/login')
 | 
				
			||||
  ElMessage.success('退出登录成功')
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.header {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: space-between;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  padding: 0 20px;
 | 
				
			||||
  height: 60px;
 | 
				
			||||
  background-color: #545c64;
 | 
				
			||||
  color: white;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.logo {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  gap: 10px;
 | 
				
			||||
  font-size: 18px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.nav {
 | 
				
			||||
  flex: 1;
 | 
				
			||||
  margin: 0 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-info {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  gap: 5px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.username {
 | 
				
			||||
  margin: 0 5px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.el-dropdown-link {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  color: white;
 | 
				
			||||
  cursor: pointer;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-dropdown-info {
 | 
				
			||||
  padding: 5px 16px;
 | 
				
			||||
  text-align: center;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-name {
 | 
				
			||||
  font-weight: bold;
 | 
				
			||||
  margin-bottom: 4px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-vip {
 | 
				
			||||
  color: #ff9a2e;
 | 
				
			||||
  font-size: 14px;
 | 
				
			||||
  margin-bottom: 6px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-balance {
 | 
				
			||||
  font-size: 14px;
 | 
				
			||||
  color: #666;
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  justify-content: center;
 | 
				
			||||
  gap: 2px;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,25 @@
 | 
				
			||||
import { createApp } from 'vue'
 | 
				
			||||
import App from './App.vue'
 | 
				
			||||
import router from './router'
 | 
				
			||||
import store from './store'
 | 
				
			||||
import ElementPlus from 'element-plus'
 | 
				
			||||
import 'element-plus/dist/index.css'
 | 
				
			||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 | 
				
			||||
 | 
				
			||||
const app = createApp(App)
 | 
				
			||||
 | 
				
			||||
// 注册所有图标
 | 
				
			||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 | 
				
			||||
  app.component(key, component)
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
app.use(ElementPlus)
 | 
				
			||||
app.use(store)
 | 
				
			||||
app.use(router)
 | 
				
			||||
 | 
				
			||||
// 初始化会话
 | 
				
			||||
store.dispatch('initSession')
 | 
				
			||||
 | 
				
			||||
app.mount('#app')
 | 
				
			||||
 | 
				
			||||
window.store = store
 | 
				
			||||
@ -0,0 +1,131 @@
 | 
				
			||||
import { createRouter, createWebHistory } from 'vue-router'
 | 
				
			||||
import store from '../store'
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
const routes = [
 | 
				
			||||
  {
 | 
				
			||||
    path: '/',
 | 
				
			||||
    name: 'Home',
 | 
				
			||||
    component: () => import('../views/Home.vue'),
 | 
				
			||||
    meta: { requiresAuth: false }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/login',
 | 
				
			||||
    name: 'Login',
 | 
				
			||||
    component: () => import('../views/Auth/Login.vue')
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/register',
 | 
				
			||||
    name: 'Register',
 | 
				
			||||
    component: () => import('../views/Auth/Register.vue')
 | 
				
			||||
  },
 | 
				
			||||
 | 
				
			||||
  {
 | 
				
			||||
    path: '/books',
 | 
				
			||||
    name: 'Books',
 | 
				
			||||
    component: () => import('../views/Books/BookList.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/books/:id',
 | 
				
			||||
    name: 'BookDetail',
 | 
				
			||||
    component: () => import('../views/Books/BookDetail.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/books/add',
 | 
				
			||||
    name: 'AddBook',
 | 
				
			||||
    component: () => import('../views/Books/AddBook.vue'),
 | 
				
			||||
    meta: { requiresAuth: true, requiresAdmin: true }
 | 
				
			||||
  },
 | 
				
			||||
  // {
 | 
				
			||||
  //   path: '/books/edit/:id',
 | 
				
			||||
  //   name: 'EditBook',
 | 
				
			||||
  //   component: () => import('../views/Books/EditBook.vue'),
 | 
				
			||||
  //   meta: { requiresAuth: true, requiresAdmin: true }
 | 
				
			||||
  // },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/borrow',
 | 
				
			||||
    name: 'BorrowBook',
 | 
				
			||||
    component: () => import('../views/Borrow/BorrowBook.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/return',
 | 
				
			||||
    name: 'ReturnBook',
 | 
				
			||||
    component: () => import('../views/Borrow/ReturnBook.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/borrow-records',
 | 
				
			||||
    name: 'BorrowRecords',
 | 
				
			||||
    component: () => import('../views/User/BorrowRecords.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/recharge',
 | 
				
			||||
    name: 'Recharge',
 | 
				
			||||
    component: () => import('../views/User/Recharge.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/ranking/weekly',
 | 
				
			||||
    name: 'WeeklyRank',
 | 
				
			||||
    component: () => import('../views/Ranking/WeeklyRank.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/ranking/monthly',
 | 
				
			||||
    name: 'MonthlyRank',
 | 
				
			||||
    component: () => import('../views/Ranking/MonthlyRank.vue'),
 | 
				
			||||
    meta: { requiresAuth: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/admin/borrow-records',
 | 
				
			||||
    name: 'AllBorrowRecords',
 | 
				
			||||
    component: () => import('../views/Admin/AllBorrowRecords.vue'),
 | 
				
			||||
    meta: { requiresAdmin: true }
 | 
				
			||||
  },
 | 
				
			||||
  {
 | 
				
			||||
    path: '/admin/books',
 | 
				
			||||
    name: 'BookManagement',
 | 
				
			||||
    component: () => import('../views/Admin/BookManagement.vue'),
 | 
				
			||||
    meta: { requiresAdmin: true }
 | 
				
			||||
  }
 | 
				
			||||
]
 | 
				
			||||
 | 
				
			||||
const router = createRouter({
 | 
				
			||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
				
			||||
  routes
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
// 路由守卫
 | 
				
			||||
router.beforeEach((to, from, next) => {
 | 
				
			||||
  // 等待会话初始化完成
 | 
				
			||||
  if (!store.state.sessionInitialized) {
 | 
				
			||||
    return next()
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  const isAuthenticated = store.getters.isAuthenticated
 | 
				
			||||
  const isAdmin = store.getters.isAdmin
 | 
				
			||||
 | 
				
			||||
  // 如果路由需要认证但用户未登录,跳转到登录页
 | 
				
			||||
  if (to.meta.requiresAuth && !isAuthenticated) {
 | 
				
			||||
    return next('/login')
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  // 如果路由需要管理员权限但用户不是管理员
 | 
				
			||||
  if (to.meta.requiresAdmin && !isAdmin) {
 | 
				
			||||
    return next({ name: 'Home' })
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  // 如果用户已登录但访问登录/注册页,跳转到首页
 | 
				
			||||
  if ((to.name === 'Login' || to.name === 'Register') && isAuthenticated) {
 | 
				
			||||
    return next({ name: 'Home' })
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  next()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
export default router
 | 
				
			||||
@ -0,0 +1,16 @@
 | 
				
			||||
export function formatDate(dateString) {
 | 
				
			||||
  if (!dateString) return '未知时间'
 | 
				
			||||
  
 | 
				
			||||
  const date = new Date(dateString)
 | 
				
			||||
  
 | 
				
			||||
  // 处理无效日期
 | 
				
			||||
  if (isNaN(date.getTime())) return dateString
 | 
				
			||||
  
 | 
				
			||||
  const year = date.getFullYear()
 | 
				
			||||
  const month = String(date.getMonth() + 1).padStart(2, '0')
 | 
				
			||||
  const day = String(date.getDate()).padStart(2, '0')
 | 
				
			||||
  const hours = String(date.getHours()).padStart(2, '0')
 | 
				
			||||
  const minutes = String(date.getMinutes()).padStart(2, '0')
 | 
				
			||||
  
 | 
				
			||||
  return `${year}-${month}-${day} ${hours}:${minutes}`
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,73 @@
 | 
				
			||||
import axios from 'axios'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
import store from '../store/index'
 | 
				
			||||
import router from '../router/index'
 | 
				
			||||
 | 
				
			||||
// 创建axios实例
 | 
				
			||||
const service = axios.create({
 | 
				
			||||
  baseURL: 'http://localhost:8877',
 | 
				
			||||
  timeout: 10000,
 | 
				
			||||
  withCredentials: true // 允许携带cookie
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
// 请求拦截器
 | 
				
			||||
service.interceptors.request.use(
 | 
				
			||||
  config => {
 | 
				
			||||
    
 | 
				
			||||
    return config
 | 
				
			||||
  },
 | 
				
			||||
  error => {
 | 
				
			||||
    return Promise.reject(error)
 | 
				
			||||
  }
 | 
				
			||||
)
 | 
				
			||||
 | 
				
			||||
// 响应拦截器 
 | 
				
			||||
service.interceptors.response.use(
 | 
				
			||||
  response => {
 | 
				
			||||
    // 处理成功响应
 | 
				
			||||
    const res = response.data
 | 
				
			||||
    
 | 
				
			||||
    // 处理业务错误 (code !== 200)
 | 
				
			||||
    if (res && typeof res === 'object' && res.code !== undefined && res.code !== 200) {
 | 
				
			||||
      // 检查是否为静默请求
 | 
				
			||||
      if (!response.config.silent) {
 | 
				
			||||
        ElMessage.error(res.message || '请求失败')
 | 
				
			||||
      }
 | 
				
			||||
      return Promise.reject(new Error(res.message || 'Error'))
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    // 返回整个响应对象,确保组件可以访问响应头等信息
 | 
				
			||||
    return response
 | 
				
			||||
  },
 | 
				
			||||
  error => {
 | 
				
			||||
    // 处理HTTP错误
 | 
				
			||||
    if (error.response) {
 | 
				
			||||
      switch (error.response.status) {
 | 
				
			||||
        case 401:
 | 
				
			||||
          // 只有在非静默请求时才显示错误信息
 | 
				
			||||
          if (!error.config?.silent) {
 | 
				
			||||
            store.dispatch('logout')
 | 
				
			||||
            router.push('/login')
 | 
				
			||||
            ElMessage.error('请先登录')
 | 
				
			||||
          }
 | 
				
			||||
          break
 | 
				
			||||
        case 403:
 | 
				
			||||
          if (!error.config?.silent) {
 | 
				
			||||
            ElMessage.error('没有操作权限')
 | 
				
			||||
          }
 | 
				
			||||
          break
 | 
				
			||||
        default:
 | 
				
			||||
          if (!error.config?.silent) {
 | 
				
			||||
            ElMessage.error(error.response.data?.message || '请求失败')
 | 
				
			||||
          }
 | 
				
			||||
      }
 | 
				
			||||
    } else {
 | 
				
			||||
      if (!error.config?.silent) {
 | 
				
			||||
        ElMessage.error('网络错误,请检查连接')
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
    return Promise.reject(error)
 | 
				
			||||
  }
 | 
				
			||||
)
 | 
				
			||||
 | 
				
			||||
export default service
 | 
				
			||||
@ -0,0 +1,43 @@
 | 
				
			||||
// 安全地解析JSON字符串
 | 
				
			||||
export function safeParseJSON(str, defaultValue = null) {
 | 
				
			||||
  if (!str) return defaultValue
 | 
				
			||||
  try {
 | 
				
			||||
    return JSON.parse(str)
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('JSON解析失败:', error)
 | 
				
			||||
    return defaultValue
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
// 安全地存储数据到sessionStorage
 | 
				
			||||
export function safeSetItem(key, value) {
 | 
				
			||||
  try {
 | 
				
			||||
    sessionStorage.setItem(key, JSON.stringify(value))
 | 
				
			||||
    return true
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('存储数据失败:', error)
 | 
				
			||||
    return false
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
// 安全地从sessionStorage获取数据
 | 
				
			||||
export function safeGetItem(key, defaultValue = null) {
 | 
				
			||||
  try {
 | 
				
			||||
    const item = sessionStorage.getItem(key)
 | 
				
			||||
    return item ? JSON.parse(item) : defaultValue
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取数据失败:', error)
 | 
				
			||||
    return defaultValue
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
// 安全地从sessionStorage删除数据
 | 
				
			||||
export function safeRemoveItem(key) {
 | 
				
			||||
  try {
 | 
				
			||||
    sessionStorage.removeItem(key)
 | 
				
			||||
    return true
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('删除数据失败:', error)
 | 
				
			||||
    return false
 | 
				
			||||
  }
 | 
				
			||||
} 
 | 
				
			||||
@ -0,0 +1,141 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="add-book-container">
 | 
				
			||||
    <el-card class="add-book-card">
 | 
				
			||||
      <h2 class="add-book-title">添加新书</h2>
 | 
				
			||||
      <el-form 
 | 
				
			||||
        ref="addBookForm" 
 | 
				
			||||
        :model="form" 
 | 
				
			||||
        :rules="rules" 
 | 
				
			||||
        label-width="100px">
 | 
				
			||||
        <el-form-item label="书名" prop="title">
 | 
				
			||||
          <el-input v-model="form.title" placeholder="请输入书名" />
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item label="简介" prop="content">
 | 
				
			||||
          <el-input 
 | 
				
			||||
            v-model="form.content" 
 | 
				
			||||
            type="textarea" 
 | 
				
			||||
            :rows="4" 
 | 
				
			||||
            placeholder="请输入书籍简介" />
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item label="封面URL" prop="url">
 | 
				
			||||
          <el-input v-model="form.url" placeholder="请输入封面图片URL" />
 | 
				
			||||
          <div class="cover-preview" v-if="form.url">
 | 
				
			||||
            <el-image :src="form.url" fit="cover" class="preview-image" />
 | 
				
			||||
          </div>
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item label="状态" prop="state">
 | 
				
			||||
          <el-select v-model="form.state" placeholder="请选择状态">
 | 
				
			||||
            <el-option label="可借" value="可借" />
 | 
				
			||||
            <el-option label="已借完" value="已借完" />
 | 
				
			||||
            <el-option label="维护中" value="维护中" />
 | 
				
			||||
          </el-select>
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item label="价格(元/天)" prop="money">
 | 
				
			||||
          <el-input-number 
 | 
				
			||||
            v-model="form.money" 
 | 
				
			||||
            :min="0.1" 
 | 
				
			||||
            :step="0.1" 
 | 
				
			||||
            :precision="2" />
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item label="阅读量" prop="number">
 | 
				
			||||
          <el-input-number v-model="form.number" :min="1" />
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item>
 | 
				
			||||
          <el-button type="primary" @click="submitForm" :loading="loading">提交</el-button>
 | 
				
			||||
          <el-button @click="resetForm">重置</el-button>
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
      </el-form>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref } from 'vue'
 | 
				
			||||
import { useRouter } from 'vue-router'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const router = useRouter()
 | 
				
			||||
 | 
				
			||||
const form = ref({
 | 
				
			||||
  title: '',
 | 
				
			||||
  content: '',
 | 
				
			||||
  url: '',
 | 
				
			||||
  state: '可借',
 | 
				
			||||
  money: 5.0,
 | 
				
			||||
  number: 1
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const rules = ref({
 | 
				
			||||
  title: [
 | 
				
			||||
    { required: true, message: '请输入书名', trigger: 'blur' }
 | 
				
			||||
  ],
 | 
				
			||||
  content: [
 | 
				
			||||
    { required: true, message: '请输入书籍简介', trigger: 'blur' }
 | 
				
			||||
  ],
 | 
				
			||||
  url: [
 | 
				
			||||
    { required: true, message: '请输入封面URL', trigger: 'blur' }
 | 
				
			||||
  ],
 | 
				
			||||
  state: [
 | 
				
			||||
    { required: true, message: '请选择状态', trigger: 'change' }
 | 
				
			||||
  ],
 | 
				
			||||
  money: [
 | 
				
			||||
    { required: true, message: '请输入价格', trigger: 'blur' }
 | 
				
			||||
  ],
 | 
				
			||||
  number: [
 | 
				
			||||
    { required: true, message: '请输入阅读量', trigger: 'blur' }
 | 
				
			||||
  ]
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const addBookForm = ref(null)
 | 
				
			||||
const loading = ref(false)
 | 
				
			||||
 | 
				
			||||
const submitForm = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    await addBookForm.value.validate()
 | 
				
			||||
    loading.value = true
 | 
				
			||||
    
 | 
				
			||||
    await store.dispatch('addBook', form.value)
 | 
				
			||||
    ElMessage.success('添加书籍成功')
 | 
				
			||||
    router.push('/books')
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('添加书籍失败:', error)
 | 
				
			||||
    ElMessage.error(error.message || '添加书籍失败')
 | 
				
			||||
  } finally {
 | 
				
			||||
    loading.value = false
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const resetForm = () => {
 | 
				
			||||
  addBookForm.value.resetFields()
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.add-book-container {
 | 
				
			||||
  max-width: 800px;
 | 
				
			||||
  margin: 0 auto;
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.add-book-card {
 | 
				
			||||
  padding: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.add-book-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.cover-preview {
 | 
				
			||||
  margin-top: 10px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.preview-image {
 | 
				
			||||
  width: 200px;
 | 
				
			||||
  height: 250px;
 | 
				
			||||
  border-radius: 4px;
 | 
				
			||||
  border: 1px solid #eee;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,202 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="book-list-container">
 | 
				
			||||
    <div class="search-bar">
 | 
				
			||||
      <el-input 
 | 
				
			||||
        v-model="searchKeyword" 
 | 
				
			||||
        placeholder="搜索书名" 
 | 
				
			||||
        clearable
 | 
				
			||||
        @clear="fetchBooks"
 | 
				
			||||
        @keyup.enter="fetchBooks"
 | 
				
			||||
        class="search-input">
 | 
				
			||||
        <template #append>
 | 
				
			||||
          <el-button :icon="Search" @click="fetchBooks" />
 | 
				
			||||
        </template>
 | 
				
			||||
      </el-input>
 | 
				
			||||
      <el-button 
 | 
				
			||||
        type="primary" 
 | 
				
			||||
        @click="goToAddBook"
 | 
				
			||||
        v-if="user && user.admin"
 | 
				
			||||
        class="add-button">
 | 
				
			||||
        <el-icon><Plus /></el-icon>
 | 
				
			||||
        添加书籍
 | 
				
			||||
      </el-button>
 | 
				
			||||
    </div>
 | 
				
			||||
    
 | 
				
			||||
    <div class="books-grid">
 | 
				
			||||
      <el-card 
 | 
				
			||||
        v-for="book in books" 
 | 
				
			||||
        :key="book.id" 
 | 
				
			||||
        class="book-card"
 | 
				
			||||
        shadow="hover"
 | 
				
			||||
        @click="goToBookDetail(book.id)">
 | 
				
			||||
        <div class="book-cover">
 | 
				
			||||
          <el-image 
 | 
				
			||||
            :src="book.url" 
 | 
				
			||||
            fit="cover" 
 | 
				
			||||
            class="cover-image"
 | 
				
			||||
            :alt="book.title" />
 | 
				
			||||
        </div>
 | 
				
			||||
        <div class="book-info">
 | 
				
			||||
          <h3 class="book-title">{{ book.title }}</h3>
 | 
				
			||||
          <div class="book-meta">
 | 
				
			||||
            <el-tag type="info" size="small">{{ book.content }}</el-tag>
 | 
				
			||||
            <el-tag type="success" size="small">¥{{ book.money }}/天</el-tag>
 | 
				
			||||
          </div>
 | 
				
			||||
          <div class="book-status">
 | 
				
			||||
            <el-tag 
 | 
				
			||||
              :type="book.state === '正常' ? 'success' : 'danger'" 
 | 
				
			||||
              size="small">
 | 
				
			||||
              {{ book.state }}
 | 
				
			||||
            </el-tag>
 | 
				
			||||
            <span>阅读量: {{ book.number }}次</span>
 | 
				
			||||
          </div>
 | 
				
			||||
        </div>
 | 
				
			||||
      </el-card>
 | 
				
			||||
    </div>
 | 
				
			||||
    
 | 
				
			||||
    <div class="pagination">
 | 
				
			||||
      <el-pagination
 | 
				
			||||
        v-model:current-page="currentPage"
 | 
				
			||||
        v-model:page-size="pageSize"
 | 
				
			||||
        :total="total"
 | 
				
			||||
        layout="prev, pager, next, jumper"
 | 
				
			||||
        @current-change="fetchBooks"
 | 
				
			||||
        background />
 | 
				
			||||
    </div>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, onMounted, computed } from 'vue'
 | 
				
			||||
import { useRouter } from 'vue-router'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { Search, Plus } from '@element-plus/icons-vue'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const router = useRouter()
 | 
				
			||||
const user = computed(() => store.state.user)
 | 
				
			||||
const isAdmin = computed(() => user.value?.admin || false)
 | 
				
			||||
const books = ref([])
 | 
				
			||||
const searchKeyword = ref('')
 | 
				
			||||
const currentPage = ref(1)
 | 
				
			||||
const pageSize = ref(12)
 | 
				
			||||
const total = ref(0)
 | 
				
			||||
 | 
				
			||||
onMounted(() => {
 | 
				
			||||
  fetchBooks()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const fetchBooks = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    const params = {
 | 
				
			||||
      page: currentPage.value,
 | 
				
			||||
      pageSize: pageSize.value,
 | 
				
			||||
      keyword: searchKeyword.value
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    const response = await store.dispatch('fetchBooks', params)
 | 
				
			||||
    books.value = response.data.list
 | 
				
			||||
    total.value = response.data.total
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取书籍列表失败:', error)
 | 
				
			||||
    ElMessage.error('获取书籍列表失败')
 | 
				
			||||
     books.value = [
 | 
				
			||||
       { id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, state: '正常', content: '科幻' },
 | 
				
			||||
      { id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, state: '正常', content: '历史' },
 | 
				
			||||
      { id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0, state: '维护中', content: '文学' },
 | 
				
			||||
        ]
 | 
				
			||||
   
 | 
				
			||||
    total.value = books.value.length
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToBookDetail = (id) => {
 | 
				
			||||
  router.push({ name: 'BookDetail', params: { id } })
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToAddBook = () => {
 | 
				
			||||
  router.push({ name: 'AddBook' })
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.book-list-container {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.search-bar {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.search-input {
 | 
				
			||||
  width: 300px;
 | 
				
			||||
  margin-right: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.add-button {
 | 
				
			||||
  margin-left: auto;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.books-grid {
 | 
				
			||||
  display: grid;
 | 
				
			||||
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
 | 
				
			||||
  gap: 20px;
 | 
				
			||||
  margin-bottom: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-card {
 | 
				
			||||
  cursor: pointer;
 | 
				
			||||
  transition: transform 0.3s;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-card:hover {
 | 
				
			||||
  transform: translateY(-5px);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-cover {
 | 
				
			||||
  height: 180px;
 | 
				
			||||
  overflow: hidden;
 | 
				
			||||
  border-radius: 4px;
 | 
				
			||||
  margin-bottom: 15px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.cover-image {
 | 
				
			||||
  width: 100%;
 | 
				
			||||
  height: 100%;
 | 
				
			||||
  object-fit: cover;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-info {
 | 
				
			||||
  padding: 0 10px 10px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-title {
 | 
				
			||||
  margin: 0 0 10px;
 | 
				
			||||
  font-size: 16px;
 | 
				
			||||
  white-space: nowrap;
 | 
				
			||||
  overflow: hidden;
 | 
				
			||||
  text-overflow: ellipsis;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-meta {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  gap: 5px;
 | 
				
			||||
  margin-bottom: 10px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.book-status {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: space-between;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  font-size: 13px;
 | 
				
			||||
  color: #666;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.pagination {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: center;
 | 
				
			||||
  margin-top: 20px;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,3 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div>编辑图书页面</div>
 | 
				
			||||
</template>
 | 
				
			||||
@ -0,0 +1,142 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="borrow-container">
 | 
				
			||||
    <el-card class="borrow-card">
 | 
				
			||||
      <h2 class="borrow-title">借阅图书</h2>
 | 
				
			||||
      
 | 
				
			||||
      <div class="search-bar">
 | 
				
			||||
        <el-input 
 | 
				
			||||
          v-model="searchKeyword" 
 | 
				
			||||
          placeholder="搜索书名" 
 | 
				
			||||
          clearable
 | 
				
			||||
          @clear="searchBooks"
 | 
				
			||||
          @keyup.enter="searchBooks"
 | 
				
			||||
          class="search-input">
 | 
				
			||||
          <template #append>
 | 
				
			||||
            <el-button :icon="Search" @click="searchBooks" />
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-input>
 | 
				
			||||
      </div>
 | 
				
			||||
      
 | 
				
			||||
      <div class="books-grid">
 | 
				
			||||
        <BookCard 
 | 
				
			||||
          v-for="book in books" 
 | 
				
			||||
          :key="book.id" 
 | 
				
			||||
          :book="book"
 | 
				
			||||
          @borrow="handleBorrow(book)" />
 | 
				
			||||
      </div>
 | 
				
			||||
      
 | 
				
			||||
      <div class="pagination">
 | 
				
			||||
        <el-pagination
 | 
				
			||||
          v-model:current-page="currentPage"
 | 
				
			||||
          v-model:page-size="pageSize"
 | 
				
			||||
          :total="total"
 | 
				
			||||
          layout="prev, pager, next, jumper"
 | 
				
			||||
          @current-change="fetchBooks"
 | 
				
			||||
          background />
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, onMounted } from 'vue'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { Search } from '@element-plus/icons-vue'
 | 
				
			||||
import BookCard from '@/components/BookCard.vue'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
 | 
				
			||||
const books = ref([])
 | 
				
			||||
const searchKeyword = ref('')
 | 
				
			||||
const currentPage = ref(1)
 | 
				
			||||
const pageSize = ref(8)
 | 
				
			||||
const total = ref(0)
 | 
				
			||||
 | 
				
			||||
onMounted(() => {
 | 
				
			||||
  fetchBooks()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const fetchBooks = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    const params = {
 | 
				
			||||
      page: currentPage.value,
 | 
				
			||||
      pageSize: pageSize.value,
 | 
				
			||||
      keyword: searchKeyword.value
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    const response = await store.dispatch('fetchBooks', params)
 | 
				
			||||
    books.value = response.data.list
 | 
				
			||||
    total.value = response.data.total
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取书籍列表失败:', error)
 | 
				
			||||
    ElMessage.error('获取书籍列表失败')
 | 
				
			||||
     // 预设数据
 | 
				
			||||
     books.value = [
 | 
				
			||||
       { id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, state: '正常', content: '科幻' },
 | 
				
			||||
      { id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, state: '正常', content: '历史' },
 | 
				
			||||
      { id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0, state: '维护中', content: '文学' },
 | 
				
			||||
        ]
 | 
				
			||||
    total.value = books.value.length
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const searchBooks = () => {
 | 
				
			||||
  currentPage.value = 1
 | 
				
			||||
  fetchBooks()
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const handleBorrow = async (book) => {
 | 
				
			||||
  try {
 | 
				
			||||
    if (book.state !=='正常') {
 | 
				
			||||
      ElMessage.warning('该书籍维护中,无法借阅')
 | 
				
			||||
      return
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    await store.dispatch('borrowBook', { title: book.title })
 | 
				
			||||
    ElMessage.success(`成功借阅《${book.title}》`)
 | 
				
			||||
    // 刷新列表
 | 
				
			||||
    await fetchBooks()
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('借阅失败:', error)
 | 
				
			||||
    ElMessage.error(error.message || '借阅失败')
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.borrow-container {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.borrow-card {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.borrow-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.search-bar {
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
  display: flex;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.search-input {
 | 
				
			||||
  width: 300px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.books-grid {
 | 
				
			||||
  display: grid;
 | 
				
			||||
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
 | 
				
			||||
  gap: 20px;
 | 
				
			||||
  margin-bottom: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.pagination {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: center;
 | 
				
			||||
  margin-top: 20px;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,155 @@
 | 
				
			||||
<template>
 | 
				
			||||
    <div class="return-container">
 | 
				
			||||
      <el-card class="return-card">
 | 
				
			||||
        <h2 class="return-title">归还图书</h2>
 | 
				
			||||
        
 | 
				
			||||
        <div class="books-grid">
 | 
				
			||||
          <el-card 
 | 
				
			||||
            v-for="book in borrowedBooks" 
 | 
				
			||||
            :key="book.id || book.title" 
 | 
				
			||||
            class="borrowed-book-card"
 | 
				
			||||
            shadow="hover">
 | 
				
			||||
            <div class="book-info">
 | 
				
			||||
              <el-image :src="book.url" class="book-cover" fit="cover" />
 | 
				
			||||
              <div class="book-details">
 | 
				
			||||
                <h3 class="book-title">{{ book.title }}</h3>
 | 
				
			||||
                <p class="borrow-time">借阅量: {{ book.borrow_time }}</p>
 | 
				
			||||
                <el-button 
 | 
				
			||||
                  type="primary" 
 | 
				
			||||
                  @click="handleReturn(book)"
 | 
				
			||||
                  class="return-button">
 | 
				
			||||
                  立即归还
 | 
				
			||||
                </el-button>
 | 
				
			||||
              </div>
 | 
				
			||||
            </div>
 | 
				
			||||
          </el-card>
 | 
				
			||||
        </div>
 | 
				
			||||
        
 | 
				
			||||
        <div class="no-books" v-if="borrowedBooks.length === 0">
 | 
				
			||||
          <el-empty description="暂无借阅中的图书" />
 | 
				
			||||
        </div>
 | 
				
			||||
      </el-card>
 | 
				
			||||
    </div>
 | 
				
			||||
  </template>
 | 
				
			||||
  
 | 
				
			||||
  <script setup>
 | 
				
			||||
  import { ref, onMounted, computed } from 'vue'
 | 
				
			||||
  import { useStore } from 'vuex'
 | 
				
			||||
  import { ElMessage } from 'element-plus'
 | 
				
			||||
  import { formatDate } from '@/utils/date'
 | 
				
			||||
  
 | 
				
			||||
  const store = useStore()
 | 
				
			||||
  const borrowedBooks = computed(() => store.state.borrowedBooks)
 | 
				
			||||
  
 | 
				
			||||
  onMounted(async () => {
 | 
				
			||||
    await fetchBorrowedBooks()
 | 
				
			||||
  })
 | 
				
			||||
  
 | 
				
			||||
  const fetchBorrowedBooks = async () => {
 | 
				
			||||
    try {
 | 
				
			||||
      await store.dispatch('fetchBorrowedBooks')
 | 
				
			||||
    } catch (error) {
 | 
				
			||||
      console.error('获取已借书籍失败:', error)
 | 
				
			||||
      ElMessage.error('获取已借书籍失败')
 | 
				
			||||
      // 预设数据
 | 
				
			||||
    store.commit('setBorrowedBooks', [
 | 
				
			||||
      { 
 | 
				
			||||
        id: 1, 
 | 
				
			||||
        title: '三体', 
 | 
				
			||||
        url: 'https://picsum.photos/id/24/200/300', 
 | 
				
			||||
        borrow_time: '2023-10-01T10:30:00' 
 | 
				
			||||
      },
 | 
				
			||||
      { 
 | 
				
			||||
        id: 4, 
 | 
				
			||||
        title: '活着', 
 | 
				
			||||
        url: 'https://picsum.photos/id/27/200/300', 
 | 
				
			||||
        borrow_time: '2023-10-05T14:20:00' 
 | 
				
			||||
      }
 | 
				
			||||
    ])
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  const handleReturn = async (book) => {
 | 
				
			||||
    try {
 | 
				
			||||
      const response = await store.dispatch('returnBook', { title: book.title })
 | 
				
			||||
      
 | 
				
			||||
      if (response.code === 200) {
 | 
				
			||||
        ElMessage.success(`《${book.title}》归还成功`)
 | 
				
			||||
        
 | 
				
			||||
        // 从本地状态中移除已归还的书籍
 | 
				
			||||
        store.commit('removeBorrowedBook', book.title)
 | 
				
			||||
        
 | 
				
			||||
        // 重新获取借阅书籍列表以确保数据同步
 | 
				
			||||
        await fetchBorrowedBooks()
 | 
				
			||||
      } else {
 | 
				
			||||
        ElMessage.error(response.message || '归还失败')
 | 
				
			||||
      }
 | 
				
			||||
    } catch (error) {
 | 
				
			||||
      console.error('归还失败:', error)
 | 
				
			||||
      ElMessage.error(error.message || '归还失败')
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
  </script>
 | 
				
			||||
  
 | 
				
			||||
  <style scoped>
 | 
				
			||||
  .return-container {
 | 
				
			||||
    padding: 20px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .return-card {
 | 
				
			||||
    padding: 20px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .return-title {
 | 
				
			||||
    text-align: center;
 | 
				
			||||
    margin-bottom: 30px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .books-grid {
 | 
				
			||||
    display: grid;
 | 
				
			||||
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
 | 
				
			||||
    gap: 20px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .borrowed-book-card {
 | 
				
			||||
    height: 100%;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .book-info {
 | 
				
			||||
    display: flex;
 | 
				
			||||
    gap: 20px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .book-cover {
 | 
				
			||||
    width: 120px;
 | 
				
			||||
    height: 180px;
 | 
				
			||||
    border-radius: 4px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .book-details {
 | 
				
			||||
    flex: 1;
 | 
				
			||||
    display: flex;
 | 
				
			||||
    flex-direction: column;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .book-title {
 | 
				
			||||
    margin-top: 0;
 | 
				
			||||
    margin-bottom: 15px;
 | 
				
			||||
    font-size: 18px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .borrow-time {
 | 
				
			||||
    color: #666;
 | 
				
			||||
    margin-bottom: 20px;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .return-button {
 | 
				
			||||
    margin-top: auto;
 | 
				
			||||
    align-self: flex-start;
 | 
				
			||||
  }
 | 
				
			||||
  
 | 
				
			||||
  .no-books {
 | 
				
			||||
    margin: 50px 0;
 | 
				
			||||
    text-align: center;
 | 
				
			||||
  }
 | 
				
			||||
  </style>
 | 
				
			||||
@ -0,0 +1,114 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="ranking-container">
 | 
				
			||||
    <el-card class="ranking-card">
 | 
				
			||||
      <h2 class="ranking-title">本月热租榜</h2>
 | 
				
			||||
      
 | 
				
			||||
      <el-table :data="rankList" style="width: 100%">
 | 
				
			||||
        <el-table-column type="index" label="排名" width="80" />
 | 
				
			||||
        <el-table-column label="封面" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            <el-image :src="scope.row.url" class="cover-small" fit="cover" />
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        <el-table-column prop="title" label="书名" width="200" />
 | 
				
			||||
        <el-table-column prop="number" label="阅读量" width="120" />
 | 
				
			||||
        <el-table-column prop="money" label="价格(元/天)" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            ¥{{ scope.row.money }}
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        
 | 
				
			||||
        <el-table-column label="操作" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            <el-button 
 | 
				
			||||
              type="text" 
 | 
				
			||||
              @click="handleBorrow(scope.row)"
 | 
				
			||||
              :disabled="scope.row.number <= 0">
 | 
				
			||||
              立即借阅
 | 
				
			||||
            </el-button>
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
      </el-table>
 | 
				
			||||
      
 | 
				
			||||
      <div class="no-data" v-if="rankList.length === 0">
 | 
				
			||||
        <el-empty description="暂无排行数据" />
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, onMounted } from 'vue'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const rankList = ref([])
 | 
				
			||||
 | 
				
			||||
onMounted(async () => {
 | 
				
			||||
  await fetchMonthlyRank()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const fetchMonthlyRank = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    const response = await store.dispatch('fetchMonthlyRank')
 | 
				
			||||
    rankList.value = response.data
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取本月热租榜失败:', error)
 | 
				
			||||
    ElMessage.error('获取本月热租榜失败')
 | 
				
			||||
     // 预设数据
 | 
				
			||||
     books.value = [
 | 
				
			||||
      {  title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12},
 | 
				
			||||
      { title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10 },
 | 
				
			||||
      { title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8},
 | 
				
			||||
      { title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15 },
 | 
				
			||||
      { title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9 },
 | 
				
			||||
      { title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7 },
 | 
				
			||||
      { title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20 },
 | 
				
			||||
      { title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18 }
 | 
				
			||||
    ]
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const handleBorrow = async (book) => {
 | 
				
			||||
  try {
 | 
				
			||||
    if (book.number <= 0) {
 | 
				
			||||
      ElMessage.warning('该书籍已无库存')
 | 
				
			||||
      return
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    await store.dispatch('borrowBook', { title: book.title })
 | 
				
			||||
    ElMessage.success(`成功借阅《${book.title}》`)
 | 
				
			||||
    await fetchMonthlyRank()
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('借阅失败:', error)
 | 
				
			||||
    ElMessage.error(error.message || '借阅失败')
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.ranking-container {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.ranking-card {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.ranking-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.cover-small {
 | 
				
			||||
  width: 80px;
 | 
				
			||||
  height: 120px;
 | 
				
			||||
  border-radius: 4px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.no-data {
 | 
				
			||||
  margin: 40px 0;
 | 
				
			||||
  text-align: center;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,113 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="ranking-container">
 | 
				
			||||
    <el-card class="ranking-card">
 | 
				
			||||
      <h2 class="ranking-title">本周热租榜</h2>
 | 
				
			||||
      
 | 
				
			||||
      <el-table :data="rankList" style="width: 100%">
 | 
				
			||||
        <el-table-column type="index" label="排名" width="80" />
 | 
				
			||||
        <el-table-column label="封面" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            <el-image :src="scope.row.url" class="cover-small" fit="cover" />
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        <el-table-column prop="title" label="书名" width="200" />
 | 
				
			||||
        <el-table-column prop="number" label="租借次数" width="120" />
 | 
				
			||||
        <el-table-column prop="money" label="价格(元/天)" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            ¥{{ scope.row.money }}
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        <el-table-column label="操作" width="120">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            <el-button 
 | 
				
			||||
              type="text" 
 | 
				
			||||
              @click="handleBorrow(scope.row)"
 | 
				
			||||
              :disabled="scope.row.number <= 0">
 | 
				
			||||
              立即借阅
 | 
				
			||||
            </el-button>
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
      </el-table>
 | 
				
			||||
      
 | 
				
			||||
      <div class="no-data" v-if="rankList.length === 0">
 | 
				
			||||
        <el-empty description="暂无排行数据" />
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, onMounted } from 'vue'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const rankList = ref([])
 | 
				
			||||
 | 
				
			||||
onMounted(async () => {
 | 
				
			||||
  await fetchWeeklyRank()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const fetchWeeklyRank = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    const response = await store.dispatch('fetchWeeklyRank')
 | 
				
			||||
    rankList.value = response.data
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取本周热租榜失败:', error)
 | 
				
			||||
    ElMessage.error('获取本周热租榜失败')
 | 
				
			||||
     // 预设数据
 | 
				
			||||
     books.value = [
 | 
				
			||||
      {  title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12},
 | 
				
			||||
      { title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10 },
 | 
				
			||||
      { title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8},
 | 
				
			||||
      { title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15 },
 | 
				
			||||
      { title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9 },
 | 
				
			||||
      { title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7 },
 | 
				
			||||
      { title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20 },
 | 
				
			||||
      { title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18 }
 | 
				
			||||
    ]
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const handleBorrow = async (book) => {
 | 
				
			||||
  try {
 | 
				
			||||
    if (book.number <= 0) {
 | 
				
			||||
      ElMessage.warning('该书籍已无库存')
 | 
				
			||||
      return
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
    await store.dispatch('borrowBook', { title: book.title })
 | 
				
			||||
    ElMessage.success(`成功借阅《${book.title}》`)
 | 
				
			||||
    await fetchWeeklyRank()
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('借阅失败:', error)
 | 
				
			||||
    ElMessage.error(error.message || '借阅失败')
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.ranking-container {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.ranking-card {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.ranking-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.cover-small {
 | 
				
			||||
  width: 80px;
 | 
				
			||||
  height: 120px;
 | 
				
			||||
  border-radius: 4px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.no-data {
 | 
				
			||||
  margin: 40px 0;
 | 
				
			||||
  text-align: center;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,96 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="records-container">
 | 
				
			||||
    <el-card class="records-card">
 | 
				
			||||
      <h2 class="records-title">借阅记录</h2>
 | 
				
			||||
      
 | 
				
			||||
      <el-table :data="records" style="width: 100%">
 | 
				
			||||
        <el-table-column prop="title" label="书名" width="180" />
 | 
				
			||||
        <el-table-column prop="borrow_time" label="借阅时间" width="200">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            {{ formatDate(scope.row.borrow_time) }}
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        <el-table-column prop="return_time" label="归还时间" width="200">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            {{ scope.row.return_time ? formatDate(scope.row.return_time) : '尚未归还' }}
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
        <el-table-column label="状态" width="100">
 | 
				
			||||
          <template #default="scope">
 | 
				
			||||
            <el-tag :type="scope.row.return_time ? 'success' : 'warning'">
 | 
				
			||||
              {{ scope.row.return_time ? '已归还' : '借阅中' }}
 | 
				
			||||
            </el-tag>
 | 
				
			||||
          </template>
 | 
				
			||||
        </el-table-column>
 | 
				
			||||
      </el-table>
 | 
				
			||||
      
 | 
				
			||||
      <div class="no-data" v-if="records.length === 0">
 | 
				
			||||
        <el-empty description="暂无借阅记录" />
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, onMounted } from 'vue'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
import { formatDate } from '@/utils/date'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const records = ref([])
 | 
				
			||||
 | 
				
			||||
onMounted(async () => {
 | 
				
			||||
  await fetchRecords()
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const fetchRecords = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    const response = await store.dispatch('fetchBorrowRecords')
 | 
				
			||||
    records.value = response.data
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('获取借阅记录失败:', error)
 | 
				
			||||
    ElMessage.error('获取借阅记录失败')
 | 
				
			||||
    records.value = [
 | 
				
			||||
      {
 | 
				
			||||
        title: '三体',
 | 
				
			||||
        borrow_time: '2023-10-01 10:30:00',
 | 
				
			||||
        return_time: null,
 | 
				
			||||
        
 | 
				
			||||
      },
 | 
				
			||||
      {
 | 
				
			||||
        title: '人类简史',
 | 
				
			||||
        borrow_time: '2023-09-15 09:15:00',
 | 
				
			||||
        return_time: '2023-09-30 16:40:00',
 | 
				
			||||
        
 | 
				
			||||
      },
 | 
				
			||||
      {
 | 
				
			||||
        title: '百年孤独',
 | 
				
			||||
        borrow_time: '2023-08-20 15:20:00',
 | 
				
			||||
        return_time: '2023-09-05 11:30:00',
 | 
				
			||||
        
 | 
				
			||||
      }
 | 
				
			||||
    ]
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.records-container {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.records-card {
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.records-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.no-data {
 | 
				
			||||
  margin: 40px 0;
 | 
				
			||||
  text-align: center;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,122 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="profile-container">
 | 
				
			||||
    <el-card class="profile-card">
 | 
				
			||||
      <div class="profile-header">
 | 
				
			||||
        <el-avatar :src="user.pic" :size="100" class="avatar" />
 | 
				
			||||
        <div class="user-info">
 | 
				
			||||
          <h2>{{ user.username }}</h2>
 | 
				
			||||
          <div class="vip-level">
 | 
				
			||||
            <el-tag type="warning" size="large">VIP{{ vip }}级</el-tag>
 | 
				
			||||
          </div>
 | 
				
			||||
          <div class="balance">
 | 
				
			||||
            <el-text type="primary" size="large">
 | 
				
			||||
              <el-icon><Wallet /></el-icon>
 | 
				
			||||
              余额: ¥{{ balance }}
 | 
				
			||||
            </el-text>
 | 
				
			||||
          </div>
 | 
				
			||||
        </div>
 | 
				
			||||
      </div>
 | 
				
			||||
      
 | 
				
			||||
      <el-divider />
 | 
				
			||||
      
 | 
				
			||||
      <div class="profile-details">
 | 
				
			||||
        <el-descriptions title="个人信息" :column="2" border>
 | 
				
			||||
          <el-descriptions-item label="用户名">{{ user.username }}</el-descriptions-item>
 | 
				
			||||
          <el-descriptions-item label="账户类型">
 | 
				
			||||
            <el-tag :type="isAdmin ? 'danger' : 'success'">
 | 
				
			||||
              {{ isAdmin ? '管理员' : '普通用户' }}
 | 
				
			||||
            </el-tag>
 | 
				
			||||
          </el-descriptions-item>
 | 
				
			||||
          <el-descriptions-item label="注册时间">{{ formatDate(user.create_time) }}</el-descriptions-item>
 | 
				
			||||
          <el-descriptions-item label="最后登录时间">{{ formatDate(user.update_time) }}</el-descriptions-item>
 | 
				
			||||
          
 | 
				
			||||
        </el-descriptions>
 | 
				
			||||
      </div>
 | 
				
			||||
      
 | 
				
			||||
      <div class="profile-actions">
 | 
				
			||||
        <el-button type="primary" @click="goToRecharge">账户充值</el-button>
 | 
				
			||||
        <el-button @click="goToRecords">查看借阅记录</el-button>
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { computed, onMounted } from 'vue'
 | 
				
			||||
import { useRouter } from 'vue-router'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { Wallet } from '@element-plus/icons-vue'
 | 
				
			||||
import { formatDate } from '@/utils/date'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const router = useRouter()
 | 
				
			||||
 | 
				
			||||
const user = computed(() => store.state.user || {})
 | 
				
			||||
const balance = computed(() => store.state.balance)
 | 
				
			||||
const vip = computed(() => store.state.vip)
 | 
				
			||||
const isAdmin = computed(() => store.getters.isAdmin)
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
onMounted(async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    store.dispatch('fetchUser')
 | 
				
			||||
  }catch (error) {
 | 
				
			||||
    console.error('获取用户信息失败:', error)
 | 
				
			||||
    ElMessage.error('获取用户信息失败,展示示例数据')
 | 
				
			||||
    
 | 
				
			||||
  }
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const goToRecharge = () => {
 | 
				
			||||
  router.push('/recharge')
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const goToRecords = () => {
 | 
				
			||||
  router.push('/borrow-records')
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.profile-container {
 | 
				
			||||
  max-width: 900px;
 | 
				
			||||
  margin: 0 auto;
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.profile-card {
 | 
				
			||||
  padding: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.profile-header {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  align-items: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.avatar {
 | 
				
			||||
  margin-right: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.user-info h2 {
 | 
				
			||||
  margin: 0 0 10px;
 | 
				
			||||
  font-size: 24px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.vip-level {
 | 
				
			||||
  margin-bottom: 10px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.balance {
 | 
				
			||||
  font-size: 18px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.profile-details {
 | 
				
			||||
  margin: 30px 0;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.profile-actions {
 | 
				
			||||
  display: flex;
 | 
				
			||||
  justify-content: center;
 | 
				
			||||
  gap: 20px;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,135 @@
 | 
				
			||||
<template>
 | 
				
			||||
  <div class="recharge-container">
 | 
				
			||||
    <el-card class="recharge-card">
 | 
				
			||||
      <h2 class="recharge-title">账户充值</h2>
 | 
				
			||||
      
 | 
				
			||||
      <div class="balance-display">
 | 
				
			||||
        <el-text type="primary" size="large">
 | 
				
			||||
          当前余额: <span class="balance-amount">¥{{ balance || 0 }}</span>
 | 
				
			||||
        </el-text>
 | 
				
			||||
      </div>
 | 
				
			||||
      
 | 
				
			||||
      <el-form 
 | 
				
			||||
        ref="rechargeForm" 
 | 
				
			||||
        :model="form" 
 | 
				
			||||
        :rules="rules" 
 | 
				
			||||
        label-width="100px">
 | 
				
			||||
        <el-form-item label="充值金额" prop="money">
 | 
				
			||||
          <el-input-number 
 | 
				
			||||
            v-model="form.money" 
 | 
				
			||||
            :min="10" 
 | 
				
			||||
            :step="10" 
 | 
				
			||||
            :precision="2" 
 | 
				
			||||
            controls-position="right" />
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
        <el-form-item>
 | 
				
			||||
          <el-button type="primary" @click="submitRecharge">立即充值</el-button>
 | 
				
			||||
        </el-form-item>
 | 
				
			||||
      </el-form>
 | 
				
			||||
      
 | 
				
			||||
      <div class="recharge-options">
 | 
				
			||||
        <h3>快捷充值</h3>
 | 
				
			||||
        <div class="options-grid">
 | 
				
			||||
          <el-button 
 | 
				
			||||
            v-for="amount in [10, 50, 100,150,200,300,400,500]" 
 | 
				
			||||
            :key="amount" 
 | 
				
			||||
            @click="quickRecharge(amount)">
 | 
				
			||||
            ¥{{ amount }}
 | 
				
			||||
          </el-button>
 | 
				
			||||
        </div>
 | 
				
			||||
      </div>
 | 
				
			||||
    </el-card>
 | 
				
			||||
  </div>
 | 
				
			||||
</template>
 | 
				
			||||
 | 
				
			||||
<script setup>
 | 
				
			||||
import { ref, computed, onMounted } from 'vue'
 | 
				
			||||
import { useStore } from 'vuex'
 | 
				
			||||
import { ElMessage } from 'element-plus'
 | 
				
			||||
import service from '../../utils/request'
 | 
				
			||||
 | 
				
			||||
const store = useStore()
 | 
				
			||||
const user = computed(() => store.state.user || {})
 | 
				
			||||
const balance = ref(0)
 | 
				
			||||
console.log(store.state.user)
 | 
				
			||||
onMounted(async ()=>{
 | 
				
			||||
  const response = await service.post("/user/findmoney")
 | 
				
			||||
  balance.value = response.data.data.balance
 | 
				
			||||
})
 | 
				
			||||
const form = ref({
 | 
				
			||||
  money: 10
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const rules = ref({
 | 
				
			||||
  money: [
 | 
				
			||||
    { required: true, message: '请输入充值金额', trigger: 'blur' },
 | 
				
			||||
    { type: 'number', min: 0.01, message: '充值金额必须大于0', trigger: 'blur' }
 | 
				
			||||
  ]
 | 
				
			||||
})
 | 
				
			||||
 | 
				
			||||
const rechargeForm = ref(null)
 | 
				
			||||
 | 
				
			||||
const submitRecharge = async () => {
 | 
				
			||||
  try {
 | 
				
			||||
    await rechargeForm.value.validate()
 | 
				
			||||
    
 | 
				
			||||
    await store.dispatch('recharge', { money: form.value.money })
 | 
				
			||||
    ElMessage.success(`成功充值¥${form.value.money}元`)
 | 
				
			||||
    
 | 
				
			||||
    // 更新用户信息
 | 
				
			||||
    await store.dispatch('fetchUser')
 | 
				
			||||
  } catch (error) {
 | 
				
			||||
    console.error('充值失败:', error)
 | 
				
			||||
    ElMessage.error(error.message || '充值失败')
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
const quickRecharge = (amount) => {
 | 
				
			||||
  form.value.money = amount
 | 
				
			||||
  submitRecharge()
 | 
				
			||||
}
 | 
				
			||||
</script>
 | 
				
			||||
 | 
				
			||||
<style scoped>
 | 
				
			||||
.recharge-container {
 | 
				
			||||
  max-width: 600px;
 | 
				
			||||
  margin: 0 auto;
 | 
				
			||||
  padding: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.recharge-card {
 | 
				
			||||
  padding: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.recharge-title {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 30px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.balance-display {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 30px;
 | 
				
			||||
  font-size: 18px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.balance-amount {
 | 
				
			||||
  font-size: 24px;
 | 
				
			||||
  font-weight: bold;
 | 
				
			||||
  color: #f56c6c;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.recharge-options {
 | 
				
			||||
  margin-top: 40px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.recharge-options h3 {
 | 
				
			||||
  text-align: center;
 | 
				
			||||
  margin-bottom: 20px;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.options-grid {
 | 
				
			||||
  display: grid;
 | 
				
			||||
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
 | 
				
			||||
  gap: 15px;
 | 
				
			||||
}
 | 
				
			||||
</style>
 | 
				
			||||
@ -0,0 +1,13 @@
 | 
				
			||||
// vite.config.js
 | 
				
			||||
import { defineConfig } from 'vite'
 | 
				
			||||
import vue from '@vitejs/plugin-vue'
 | 
				
			||||
import path from 'path' // 确保引入 path 模块
 | 
				
			||||
 | 
				
			||||
export default defineConfig({
 | 
				
			||||
  plugins: [vue()],
 | 
				
			||||
  resolve: {
 | 
				
			||||
    alias: {
 | 
				
			||||
      '@': path.resolve(__dirname, 'src') // 关键配置:@ 指向 src 目录
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
})
 | 
				
			||||
					Loading…
					
					
				
		Reference in new issue