parent
51b7169a18
commit
4730b9962b
@ -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,45 @@
|
||||
<template>
|
||||
<el-container class="app-container">
|
||||
<el-header>
|
||||
<HeaderBar />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="footer-content">
|
||||
<p>图书馆管理系统 © {{ new Date().getFullYear() }}</p>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import HeaderBar from './components/HeaderBar.vue'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
background-color: #545c64;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
@ -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,83 @@
|
||||
<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">
|
||||
<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.number > 0 ? 'success' : 'danger'" size="small">
|
||||
{{ book.number > 0 ? '可借' : '已借完' }}
|
||||
</el-tag>
|
||||
<span>库存: {{ book.number }}本</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
book: {
|
||||
type: Object,
|
||||
required: 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,179 @@
|
||||
<!-- 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="isAdmin">
|
||||
图书管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="allBorrowRecords" v-if="isAdmin">
|
||||
用户借阅记录
|
||||
</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>
|
||||
<el-dropdown-item @click="goToProfile">个人中心</el-dropdown-item>
|
||||
<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 { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Reading, ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const user = computed(() => store.state.user)
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
|
||||
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 goToProfile = () => {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
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: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,22 @@
|
||||
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)
|
||||
|
||||
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: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('../views/User/Profile.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
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) => {
|
||||
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,47 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: 'http://localhost:8877',
|
||||
timeout: 10000,
|
||||
withCredentials: true // 允许携带cookie
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// 这里可以添加token等全局请求头
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 业务错误处理
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
}
|
||||
return res
|
||||
},
|
||||
error => {
|
||||
// 处理HTTP错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 未登录处理
|
||||
store.dispatch('user/logout')
|
||||
router.push('/login')
|
||||
ElMessage.error('请先登录')
|
||||
} else {
|
||||
ElMessage.error(error.message || '请求失败')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
@ -0,0 +1,186 @@
|
||||
<!-- src/views/Admin/BookManagement.vue -->
|
||||
<template>
|
||||
<div class="book-management">
|
||||
<el-card>
|
||||
<div class="header">
|
||||
<h2 class="page-title">图书管理</h2>
|
||||
<el-button type="primary" @click="goToAddBook">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加图书
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<el-table :data="books" border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="封面" width="100">
|
||||
<template #default="scope">
|
||||
<el-image :src="scope.row.url" width="60" height="80" fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="书名" />
|
||||
<el-table-column prop="category" label="分类" width="120" />
|
||||
<el-table-column prop="money" label="日租金" width="100" />
|
||||
<el-table-column prop="number" label="库存" width="80" />
|
||||
<el-table-column prop="borrowCount" label="借阅次数" width="100" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.number > 0 ? 'success' : 'danger'">
|
||||
{{ scope.row.number > 0 ? '可借' : '已借完' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(scope.row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
background />
|
||||
</div>
|
||||
|
||||
<div class="no-data" v-if="books.length === 0 && !loading">
|
||||
<el-empty description="暂无图书数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus } from '@element-plus/icons-vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const books = ref([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const searchKeyword = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchBooks()
|
||||
})
|
||||
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await axios.get('/api/select', { params })
|
||||
if (response.data.code === 200) {
|
||||
books.value = response.data.data.list
|
||||
total.value = response.data.data.total
|
||||
} else {
|
||||
ElMessage.error('获取图书列表失败')
|
||||
total.value = books.value.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取图书列表失败:', error)
|
||||
ElMessage.error('获取图书列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
fetchBooks()
|
||||
}
|
||||
|
||||
const goToAddBook = () => {
|
||||
router.push('/api/add')
|
||||
}
|
||||
|
||||
const handleDelete = async (book) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除《${book.title}》吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await axios.delete(`/api/select/${book.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchBooks()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -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('book/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,175 @@
|
||||
<template>
|
||||
<div class="book-detail-container" v-if="book">
|
||||
<el-card class="book-card">
|
||||
<div class="book-header">
|
||||
<el-image :src="book.url" class="cover-large" fit="cover" />
|
||||
<div class="book-info">
|
||||
<h1 class="title">{{ book.title }}</h1>
|
||||
<div class="meta">
|
||||
<el-tag type="info" size="large">{{ book.content }}</el-tag>
|
||||
<el-tag type="success" size="large">¥{{ book.money }}/天</el-tag>
|
||||
</div>
|
||||
<div class="status">
|
||||
<el-tag :type="book.number > 0 ? 'success' : 'danger'" size="large">
|
||||
{{ book.number > 0 ? '可借' : '已借完' }}
|
||||
</el-tag>
|
||||
<span>库存: {{ book.number }}本</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="book.number <= 0"
|
||||
@click="borrowBook"
|
||||
v-if="!isAdmin">
|
||||
立即借阅
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="large"
|
||||
@click="deleteBook"
|
||||
v-if="isAdmin">
|
||||
删除书籍
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="book-description">
|
||||
<h3>书籍简介</h3>
|
||||
<p>{{ book.description || '暂无简介' }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-empty v-else description="书籍不存在" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const bookId = route.params.id
|
||||
const book = ref(null)
|
||||
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchBook()
|
||||
})
|
||||
|
||||
const fetchBook = async () => {
|
||||
try {
|
||||
const response = await store.dispatch('book/fetchBookById', bookId)
|
||||
book.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取书籍详情失败:', error)
|
||||
ElMessage.error('获取书籍详情失败')
|
||||
// 预设数据
|
||||
book.value = {
|
||||
id: bookId,
|
||||
title: '示例书籍',
|
||||
url: 'https://picsum.photos/id/24/300/400',
|
||||
money: 5.00,
|
||||
number: 8,
|
||||
content: '文学小说',
|
||||
description: '这是一本示例书籍,用于展示书籍详情页面的效果。包含了书籍的基本信息和详细介绍,用户可以在这里查看书籍的具体内容并进行借阅操作。',
|
||||
author: '示例作者',
|
||||
publishDate: '2023-01-15'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const borrowBook = async () => {
|
||||
try {
|
||||
await store.dispatch('borrow/borrowBook', { title: book.value.title })
|
||||
ElMessage.success(`成功借阅《${book.value.title}》`)
|
||||
// 刷新书籍信息
|
||||
await fetchBook()
|
||||
} catch (error) {
|
||||
console.error('借阅失败:', error)
|
||||
ElMessage.error(error.message || '借阅失败')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBook = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除《${book.value.title}》吗?此操作不可恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await store.dispatch('book/deleteBook', { title: book.value.title })
|
||||
ElMessage.success('书籍已删除')
|
||||
router.push('/books')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-detail-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.book-header {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.cover-large {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-size: 18px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.book-description {
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,211 @@
|
||||
<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="isAdmin"
|
||||
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.category }}</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 books = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const total = ref(0)
|
||||
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
|
||||
onMounted(() => {
|
||||
fetchBooks()
|
||||
})
|
||||
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await store.dispatch('book/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: '可借', category: '科幻' },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, state: '可借', category: '历史' },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0, state: '已借完', category: '文学' },
|
||||
{ id: 4, title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15, state: '可借', category: '小说' },
|
||||
{ id: 5, title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9, state: '可借', category: '小说' },
|
||||
{ id: 6, title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7, state: '可借', category: '小说' },
|
||||
{ id: 7, title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20, state: '可借', category: '童话' },
|
||||
{ id: 8, title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18, state: '可借', category: '文学' },
|
||||
{ id: 9, title: '月亮与六便士', url: 'https://picsum.photos/id/32/200/300', money: 5, number: 16, state: '可借', category: '小说' },
|
||||
{ id: 10, title: '哈利波特', url: 'https://picsum.photos/id/33/200/300', money: 7, number: 25, state: '可借', category: '魔幻' },
|
||||
{ id: 11, title: '水浒传', url: 'https://picsum.photos/id/34/200/300', money: 6, number: 14, state: '可借', category: '古典' },
|
||||
{ id: 12, title: '三国演义', url: 'https://picsum.photos/id/35/200/300', money: 6, number: 12, state: '可借', category: '古典' }
|
||||
]
|
||||
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,147 @@
|
||||
<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"
|
||||
@click="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('book/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 },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10 },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0 },
|
||||
{ id: 4, title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15 },
|
||||
{ id: 5, title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9 },
|
||||
{ id: 6, title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7 },
|
||||
{ id: 7, title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20 },
|
||||
{ id: 8, title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18 }
|
||||
]
|
||||
total.value = books.value.length
|
||||
}
|
||||
}
|
||||
|
||||
const searchBooks = () => {
|
||||
currentPage.value = 1
|
||||
fetchBooks()
|
||||
}
|
||||
|
||||
const handleBorrow = async (book) => {
|
||||
try {
|
||||
if (book.number <= 0) {
|
||||
ElMessage.warning('该书籍已无库存')
|
||||
return
|
||||
}
|
||||
|
||||
await store.dispatch('borrow/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,145 @@
|
||||
<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">借阅时间: {{ formatDate(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 {
|
||||
await store.dispatch('returnBook', { title: book.title })
|
||||
ElMessage.success(`《${book.title}》归还成功`)
|
||||
await fetchBorrowedBooks()
|
||||
} 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,120 @@
|
||||
<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="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.number > 0 ? 'success' : 'danger'">
|
||||
{{ scope.row.number > 0 ? '可借' : '已借完' }}
|
||||
</el-tag>
|
||||
</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('book/fetchMonthlyRank')
|
||||
rankList.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取本月热租榜失败:', error)
|
||||
ElMessage.error('获取本月热租榜失败')
|
||||
// 预设数据
|
||||
books.value = [
|
||||
{ id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, borrowCount: 128 },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, borrowCount: 112 },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8, borrowCount: 98 },
|
||||
{ id: 4, title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15, borrowCount: 92 },
|
||||
{ id: 5, title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9, borrowCount: 85 },
|
||||
{ id: 6, title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7, borrowCount: 76 },
|
||||
{ id: 7, title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20, borrowCount: 72 },
|
||||
{ id: 8, title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18, borrowCount: 68 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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,120 @@
|
||||
<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="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.number > 0 ? 'success' : 'danger'">
|
||||
{{ scope.row.number > 0 ? '可借' : '已借完' }}
|
||||
</el-tag>
|
||||
</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('book/fetchWeeklyRank')
|
||||
rankList.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取本周热租榜失败:', error)
|
||||
ElMessage.error('获取本周热租榜失败')
|
||||
// 预设数据
|
||||
books.value = [
|
||||
{ id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, borrowCount: 128 },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, borrowCount: 112 },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8, borrowCount: 98 },
|
||||
{ id: 4, title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15, borrowCount: 92 },
|
||||
{ id: 5, title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9, borrowCount: 85 },
|
||||
{ id: 6, title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7, borrowCount: 76 },
|
||||
{ id: 7, title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20, borrowCount: 72 },
|
||||
{ id: 8, title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18, borrowCount: 68 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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,99 @@
|
||||
<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('borrow/fetchBorrowRecords')
|
||||
records.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取借阅记录失败:', error)
|
||||
ElMessage.error('获取借阅记录失败')
|
||||
records.value = [
|
||||
{
|
||||
id: 1,
|
||||
book_title: '三体',
|
||||
borrow_time: '2023-10-01 10:30:00',
|
||||
return_time: null,
|
||||
status: '未归还'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
book_title: '人类简史',
|
||||
borrow_time: '2023-09-15 09:15:00',
|
||||
return_time: '2023-09-30 16:40:00',
|
||||
status: '已归还'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
book_title: '百年孤独',
|
||||
borrow_time: '2023-08-20 15:20:00',
|
||||
return_time: '2023-09-05 11:30:00',
|
||||
status: '已归还'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</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,133 @@
|
||||
<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{{ vipLevel }}级</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-item label="当前借阅数量">{{ borrowedBooks.length }}本</el-descriptions-item>
|
||||
<el-descriptions-item label="累计借阅">{{ user.totalBorrowed || 0 }}本</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 vipLevel = computed(() => store.state.vipLevel)
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
const borrowedBooks = computed(() => store.state.borrowedBooks)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
store.dispatch('fetchUser')
|
||||
}catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
ElMessage.error('获取用户信息失败,展示示例数据')
|
||||
// 预设用户数据
|
||||
store.commit('setUser', {
|
||||
username: 'testuser',
|
||||
passworf:111111,
|
||||
pic: 'https://picsum.photos/id/64/100/100',
|
||||
admin: false
|
||||
})
|
||||
store.commit('setBalanceAndVip', {
|
||||
balance: 100.50,
|
||||
vip: 2
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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,129 @@
|
||||
<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">¥{{ user.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, 200, 500]"
|
||||
:key="amount"
|
||||
@click="quickRecharge(amount)">
|
||||
¥{{ amount }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const user = computed(() => store.state.user || {})
|
||||
|
||||
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('user/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