Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
|
bbb8eba5d1 | 3 weeks ago |
|
7533450cc6 | 4 weeks ago |
|
d4b3ce78ae | 4 weeks ago |
|
6afa988017 | 4 weeks ago |
|
19862dc69d | 4 weeks ago |
|
9e8ce09450 | 4 weeks ago |
|
b5bf87ce53 | 4 weeks ago |
|
b2e3d51c0e | 4 weeks ago |
|
44c71c9562 | 1 month ago |
|
ddc810f5ed | 1 month ago |
|
466d816aa2 | 1 month ago |
|
4730b9962b | 1 month ago |
@ -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,71 @@
|
||||
<template>
|
||||
<div v-if="sessionInitialized">
|
||||
<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>
|
||||
</div>
|
||||
<div v-else class="loading-container">
|
||||
<el-loading-spinner />
|
||||
<p>正在初始化...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import HeaderBar from './components/HeaderBar.vue'
|
||||
|
||||
const store = useStore()
|
||||
const sessionInitialized = computed(() => store.state.sessionInitialized)
|
||||
</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;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
margin-top: 20px;
|
||||
color: #606266;
|
||||
font-size: 16px;
|
||||
}
|
||||
</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,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)
|
||||
|
||||
|
||||
|
||||
|
||||
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,185 @@
|
||||
<!-- 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="100" />
|
||||
<el-table-column prop="url"label="封面" width="120">
|
||||
<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="书名" width="150"/>
|
||||
<el-table-column prop="content" label="内容" />
|
||||
<el-table-column prop="money" label="日租金" width="120" />
|
||||
<el-table-column prop="number" label="阅读量" width="120" />
|
||||
<el-table-column prop="state" label="状态" width="120">
|
||||
<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('/books/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('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,186 @@
|
||||
<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.state === '正常' ? 'success' : 'danger'" size="large">
|
||||
{{ book.state }}
|
||||
</el-tag>
|
||||
<span>阅读量: {{ book.number }}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="book.state !== '正常'"
|
||||
@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.content || '暂无简介' }}</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 book = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchBook()
|
||||
})
|
||||
|
||||
const fetchBook = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const title = route.params.title
|
||||
|
||||
// 正确传递书名参数
|
||||
const response = await store.dispatch('fetchBookByTitle', { title })
|
||||
|
||||
// 根据接口文档2.3处理响应
|
||||
if (response.data) {
|
||||
book.value = response.data
|
||||
} else {
|
||||
throw new Error('书籍不存在')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取书籍详情失败:', error)
|
||||
ElMessage.error(error.message || '获取书籍详情失败')
|
||||
|
||||
// 使用有意义的预设数据
|
||||
book.value = {
|
||||
title: route.params.title,
|
||||
url: 'https://picsum.photos/id/24/300/400',
|
||||
money: 5.00,
|
||||
number: 8,
|
||||
content: `无法获取《${route.params.title}》的详细信息`,
|
||||
state: '维护中',
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const borrowBook = async () => {
|
||||
try {
|
||||
await store.dispatch('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('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,203 @@
|
||||
<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.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 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('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,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,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,150,200,300,400,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('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