diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..6427272 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,71 @@ + + + + + \ No newline at end of file diff --git a/src/assets/style.css b/src/assets/style.css new file mode 100644 index 0000000..92ac9fe --- /dev/null +++ b/src/assets/style.css @@ -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); +} diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/BookCard.vue b/src/components/BookCard.vue new file mode 100644 index 0000000..8e01c96 --- /dev/null +++ b/src/components/BookCard.vue @@ -0,0 +1,88 @@ + + + + + \ No newline at end of file diff --git a/src/components/HeaderBar.vue b/src/components/HeaderBar.vue new file mode 100644 index 0000000..63f8f2f --- /dev/null +++ b/src/components/HeaderBar.vue @@ -0,0 +1,249 @@ + + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..2b9699b --- /dev/null +++ b/src/main.js @@ -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 \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..6c4095f --- /dev/null +++ b/src/router/index.js @@ -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 \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..38f5493 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,370 @@ +import { createStore } from 'vuex' +import service from '../utils/request' + +export default createStore({ + state: { + user: JSON.parse(sessionStorage.getItem('user')) || null, + balance: JSON.parse(sessionStorage.getItem('balance')) || 0, + vipLevel: JSON.parse(sessionStorage.getItem('vipLevel')) || 0, + borrowedBooks: JSON.parse(sessionStorage.getItem('borrowedBooks')) || [], + sessionInitialized: false + }, + getters: { + isAuthenticated: state => !!state.user, + isAdmin: state => state.user?.admin || false + }, + mutations: { + setUser(state, user) { + const admin = user.admin === 1; + const userData = { + ...user, + admin + }; + + state.user = user + console.log("user") + console.log(user) + console.log("state.user") + console.log(state.user) + sessionStorage.setItem('user', JSON.stringify(user)) + }, + setBalanceAndVip(state, { balance, vip }) { + state.balance = balance + state.vipLevel = vip + sessionStorage.setItem('balance', JSON.stringify(balance)) + sessionStorage.setItem('vipLevel', JSON.stringify(vip)) + }, + setBorrowedBooks(state, books) { + state.borrowedBooks = books + sessionStorage.setItem('borrowedBooks', JSON.stringify(books)) + }, + clearUser(state) { + state.user = null + state.balance = 0 + state.vipLevel = 0 + state.borrowedBooks = [] + sessionStorage.removeItem('user') + sessionStorage.removeItem('balance') + sessionStorage.removeItem('vipLevel') + sessionStorage.removeItem('borrowedBooks') + }, + removeBorrowedBook(state, title) { + state.borrowedBooks = state.borrowedBooks.filter(book => book.title !== title) + }, + setSessionInitialized(state, value) { + state.sessionInitialized = value + } + }, + actions: { + async initSession({ commit, dispatch }) { + try { + // 静默获取用户信息 + const userData = await service.get('/user/getinfo', { + silent: true // 避免未登录时显示错误 + }) + + if (userData && userData.username) { + const admin = userData.admin === 1; + + commit('setUser', { + username: userData.username, + pic: userData.pic || '', + admin + }) + + // 获取关联信息 + try { + await dispatch('fetchBalanceAndVip') + } catch (balanceError) { + console.warn('获取余额信息失败:', balanceError) + } + + try { + await dispatch('fetchBorrowedBooks') + } catch (booksError) { + console.warn('获取借阅书籍失败:', booksError) + } + } + } catch (error) { + console.log('未检测到有效会话,用户需要重新登录') + // 清除可能存在的无效数据 + commit('clearUser') + } finally { + commit('setSessionInitialized', true) + } + }, + + + // 用户登录 + async login({ dispatch }, { username, password }) { + const response = await service.post('/user/login', + `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + // 登录成功后获取用户信息 + const userInfo = await dispatch('fetchUser') + return { + ...response.data, + user: userInfo + } + }, + + // 用户注册 + async register(_, { username, password }) { + const response = await service.post('/user/register', + `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + return response.data + }, + + // 获取当前用户信息 - 符合接口文档 + async fetchUser({ commit, dispatch }) { + try { + const response = await service.get('/user/getinfo') + const userData = response.data + console.log("fetchUer userData") + console.log(userData) + const admin = userData.admin === 1; + // 用户信息接口直接返回用户对象 + commit('setUser', { + username: userData.username || '', + pic: userData.pic || '', + admin + }) + + // 获取关联信息 + try { + await dispatch('fetchBalanceAndVip') + } catch (balanceError) { + console.error('获取余额信息失败:', balanceError) + } + + try { + await dispatch('fetchBorrowedBooks') + } catch (booksError) { + console.error('获取借阅书籍失败:', booksError) + } + + return userData + } catch (error) { + commit('clearUser') + throw error + } + }, + + // 获取余额和VIP等级 - 符合接口文档1.5 + async fetchBalanceAndVip({ commit }) { + try { + const response = await service.post('/user/findmoney') + const resData = response.data || {} + if (resData.code === 200) { + console.log("fetchBalanceAndVip resData") + console.log(resData) + const balance = resData.data.balance + const vip = resData.data.vip + + // const message = resData.message || '' + // // 使用正则表达式解析余额和VIP等级 + // const balanceMatch = message.match(/余额为:(\d+\.?\d*)元/) + // const vipMatch = message.match(/当前VIP等级为:(\d+)/) + console.log("balance:"+balance+",vip:"+vip) + if (balance && vip) { + commit('setBalanceAndVip', { balance, vip }) + } else { + console.warn('无法解析余额或VIP信息:', message) + commit('setBalanceAndVip', { balance: 0, vip: 0 }) + } + } else { + throw new Error(resData.message || '获取余额信息失败') + } + + return resData + } catch (error) { + console.error('获取余额失败:', error) + // 不抛出错误,避免影响其他功能 + return { code: 500, message: '获取余额失败' } + } + }, + + + // 账户充值 + async recharge({ dispatch }, { money }) { // 添加 { dispatch } 解构 + const response = await service.post('/user/recharge', + `money=${money}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + // 充值后刷新余额 + await dispatch('fetchBalanceAndVip') + return response.data + }, + + // 退出登录 + async logout({ commit }) { + commit('clearUser') + return { code: 200, message: '已退出登录' } + }, + + // 查询个人借书记录 + async fetchBorrowRecords() { + const response = await service.get('/user/findone') + return response.data + }, + + // 获取当前用户已借书籍 + async fetchBorrowedBooks({ commit }) { + const response = await service.get('/user/borrow/books') + + // 按照接口文档处理响应 + if (response.data.code === 200) { + commit('setBorrowedBooks', response.data.data || []) + } else { + throw new Error(response.data.message || '获取已借书籍失败') + } + + return response.data +}, + + // 租借书籍 +async borrowBook({ dispatch }, { title }) { + const response = await service.post('/borrow/borrowbook', + `title=${encodeURIComponent(title)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + // 借书后刷新用户信息 + await dispatch('fetchUser') + return response.data +}, + +// 归还书籍 - 符合接口文档3.2 +async returnBook({ dispatch }, { title }) { + try { + const response = await service.post('/borrow/returnbook', + `title=${encodeURIComponent(title)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + // 还书成功后刷新借阅书籍列表 + await dispatch('fetchBorrowedBooks') + + return response.data + } catch (error) { + console.error('还书失败:', error) + throw error + } +}, + + // 查询全部书籍 - 符合接口文档 + async fetchBooks(_, params = {}) { + const config = { + params: { + page: params.page || 1, + pageSize: params.pageSize || 10, + title: params.keyword || '' + } + } + + const response = await service.get('/api/select', config) + const countResponse = await service.get('/api/countArticle') + + // console.log('请求书籍:', params.title) + // console.log('API响应:', response) + // 处理不同响应格式 + let list = [] + let total = countResponse.data.data + if (Array.isArray(response)) { + list = response + // total = response.length + } else if (Array.isArray(response.data)) { + list = response.data + // total = response.data.length + } else if (response.data && Array.isArray(response.data.data)) { + list = response.data.data + // total = response.data.total || response.data.data.length + } else if (response.data && Array.isArray(response.data.list)) { + list = response.data.list + // total = response.data.total || response.data.list.length + } + + return { + data: { + list, + total + } + } + }, + + // 根据书名查单本书 - 符合接口文档2.3 + async fetchBookByTitle(_, payload) { + const { title } = payload; + + try { + const response = await service.get('/api/selectone', { + params: { title } + }) + + // 根据接口文档处理响应 + if (response.data && response.data.code === 200) { + return { data: response.data.data } + } else { + throw new Error(response.data?.message || '获取书籍信息失败') + } + } catch (error) { + console.error('API请求失败:', error) + throw error + } + }, + async fetchBookById(_, payload) { + const { id } = payload; + + try { + const response = await service.get('/api/selectById/'+id) + + // 根据接口文档处理响应 + if (response.data && response.data.code === 200) { + return { data: response.data.data } + } else { + throw new Error(response.data?.message || '获取书籍信息失败') + } + } catch (error) { + console.error('API请求失败:', error) + throw error + } + }, + + // 新增书籍 + async addBook(_, bookData) { + const response = await service.post('/api/add', bookData, { + headers: { 'Content-Type': 'application/json' } + }) + + return response.data + }, + + // 管理员删除书籍 + async deleteBook(_, { title }) { + const response = await service.post('/user/delete', + `title=${encodeURIComponent(title)}`, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ) + + return response.data + }, + + // 本周热租榜 - 符合接口文档 + async fetchWeeklyRank() { + const response = await service.get('/api/rank/weekly') + return { data: Array.isArray(response) ? response : response.data || [] } + }, + + // 本月热租榜 - 符合接口文档 + async fetchMonthlyRank() { + const response = await service.get('/api/rank/monthly') + return { data: Array.isArray(response) ? response : response.data || [] } + } + } +}) \ No newline at end of file diff --git a/src/utils/date.js b/src/utils/date.js new file mode 100644 index 0000000..3e8c5fc --- /dev/null +++ b/src/utils/date.js @@ -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}` +} \ No newline at end of file diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..d24e273 --- /dev/null +++ b/src/utils/request.js @@ -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 \ No newline at end of file diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 0000000..6e019e3 --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,43 @@ +// 安全地解析JSON字符串 +export function safeParseJSON(str, defaultValue = null) { + if (!str) return defaultValue + try { + return JSON.parse(str) + } catch (error) { + console.error('JSON解析失败:', error) + return defaultValue + } +} + +// 安全地存储数据到sessionStorage +export function safeSetItem(key, value) { + try { + sessionStorage.setItem(key, JSON.stringify(value)) + return true + } catch (error) { + console.error('存储数据失败:', error) + return false + } +} + +// 安全地从sessionStorage获取数据 +export function safeGetItem(key, defaultValue = null) { + try { + const item = sessionStorage.getItem(key) + return item ? JSON.parse(item) : defaultValue + } catch (error) { + console.error('获取数据失败:', error) + return defaultValue + } +} + +// 安全地从sessionStorage删除数据 +export function safeRemoveItem(key) { + try { + sessionStorage.removeItem(key) + return true + } catch (error) { + console.error('删除数据失败:', error) + return false + } +} \ No newline at end of file diff --git a/src/views/Admin/AllBorrowRecords.vue b/src/views/Admin/AllBorrowRecords.vue new file mode 100644 index 0000000..ebf2a4e --- /dev/null +++ b/src/views/Admin/AllBorrowRecords.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/src/views/Admin/BookManagement.vue b/src/views/Admin/BookManagement.vue new file mode 100644 index 0000000..593064b --- /dev/null +++ b/src/views/Admin/BookManagement.vue @@ -0,0 +1,188 @@ + + + + + + \ No newline at end of file diff --git a/src/views/Auth/Login.vue b/src/views/Auth/Login.vue new file mode 100644 index 0000000..d5a5fa7 --- /dev/null +++ b/src/views/Auth/Login.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/src/views/Auth/Register.vue b/src/views/Auth/Register.vue new file mode 100644 index 0000000..817e739 --- /dev/null +++ b/src/views/Auth/Register.vue @@ -0,0 +1,156 @@ + + + + + \ No newline at end of file diff --git a/src/views/Books/AddBook.vue b/src/views/Books/AddBook.vue new file mode 100644 index 0000000..5d13af0 --- /dev/null +++ b/src/views/Books/AddBook.vue @@ -0,0 +1,141 @@ + + + + + \ No newline at end of file diff --git a/src/views/Books/BookDetail.vue b/src/views/Books/BookDetail.vue new file mode 100644 index 0000000..886041f --- /dev/null +++ b/src/views/Books/BookDetail.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/src/views/Books/BookList.vue b/src/views/Books/BookList.vue new file mode 100644 index 0000000..9ee4e05 --- /dev/null +++ b/src/views/Books/BookList.vue @@ -0,0 +1,202 @@ + + + + + \ No newline at end of file diff --git a/src/views/Books/EditBook.vue b/src/views/Books/EditBook.vue new file mode 100644 index 0000000..eeaf97a --- /dev/null +++ b/src/views/Books/EditBook.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/views/Borrow/BorrowBook.vue b/src/views/Borrow/BorrowBook.vue new file mode 100644 index 0000000..e92d27b --- /dev/null +++ b/src/views/Borrow/BorrowBook.vue @@ -0,0 +1,142 @@ + + + + + \ No newline at end of file diff --git a/src/views/Borrow/ReturnBook.vue b/src/views/Borrow/ReturnBook.vue new file mode 100644 index 0000000..f597661 --- /dev/null +++ b/src/views/Borrow/ReturnBook.vue @@ -0,0 +1,155 @@ + + + + + \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue new file mode 100644 index 0000000..e75e16c --- /dev/null +++ b/src/views/Home.vue @@ -0,0 +1,164 @@ + + + + + \ No newline at end of file diff --git a/src/views/Ranking/MonthlyRank.vue b/src/views/Ranking/MonthlyRank.vue new file mode 100644 index 0000000..ff3ff3f --- /dev/null +++ b/src/views/Ranking/MonthlyRank.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/src/views/Ranking/WeeklyRank.vue b/src/views/Ranking/WeeklyRank.vue new file mode 100644 index 0000000..b719fb8 --- /dev/null +++ b/src/views/Ranking/WeeklyRank.vue @@ -0,0 +1,113 @@ + + + + + \ No newline at end of file diff --git a/src/views/User/BorrowRecords.vue b/src/views/User/BorrowRecords.vue new file mode 100644 index 0000000..ffcad87 --- /dev/null +++ b/src/views/User/BorrowRecords.vue @@ -0,0 +1,96 @@ + + + + + \ No newline at end of file diff --git a/src/views/User/Profile.vue b/src/views/User/Profile.vue new file mode 100644 index 0000000..b03de23 --- /dev/null +++ b/src/views/User/Profile.vue @@ -0,0 +1,122 @@ + + + + + \ No newline at end of file diff --git a/src/views/User/Recharge.vue b/src/views/User/Recharge.vue new file mode 100644 index 0000000..a258e97 --- /dev/null +++ b/src/views/User/Recharge.vue @@ -0,0 +1,129 @@ + + + + + \ No newline at end of file diff --git a/接口文档.pdf b/接口文档.pdf new file mode 100644 index 0000000..5cd1fbd Binary files /dev/null and b/接口文档.pdf differ