1. 新增API模块统一管理所有接口,包括首页、书籍详情、书架等模块 2. 实现首页数据动态加载,包括轮播图、公告、推荐书籍和最新书籍 3. 完成书籍详情页API集成,展示书籍信息、章节目录和统计数据 4. 优化面包屑导航和路由配置,新增公告详情页路由 5. 重构书籍卡片组件,支持动态状态显示和样式优化 6. 添加加载状态处理,使用骨架屏提升用户体验 7. 实现分类菜单动态渲染,从store获取分类数据 新增API类型定义和文档说明,统一接口响应格式处理master
| @ -0,0 +1,241 @@ | |||
| # 小说网站 API 接口文档 | |||
| 本项目基于提供的 Swagger API 文档创建了完整的前端接口调用封装。 | |||
| ## 📁 文件结构 | |||
| ``` | |||
| src/api/ | |||
| ├── index.js # axios 基础配置 | |||
| ├── auth.js # 授权登录相关接口 | |||
| ├── home.js # 首页相关接口 | |||
| ├── bookshelf.js # 书架相关接口(阅读书架、我的作品、读者成就) | |||
| ├── user.js # 用户相关接口(流水、评论、任务、申请作家、礼物订阅) | |||
| ├── modules.js # 统一导出所有API模块 | |||
| ├── example.js # 使用示例 | |||
| ├── types.ts # TypeScript 类型定义 | |||
| ├── api.json # 原始 Swagger 文档 | |||
| └── README.md # 本文档 | |||
| ``` | |||
| ## 🚀 快速开始 | |||
| ### 1. 基础配置 | |||
| 项目已配置好 axios 实例,自动处理: | |||
| - ✅ X-Access-Token 认证头 | |||
| - ✅ 统一的响应格式处理 | |||
| - ✅ 错误处理和401重定向 | |||
| - ✅ 请求超时设置 | |||
| ### 2. 使用方式 | |||
| #### 方式一:按需导入 | |||
| ```javascript | |||
| import { authApi, homeApi, commentApi } from '@/api/modules.js'; | |||
| // 用户登录 | |||
| const loginResult = await authApi.appletLogin(loginData); | |||
| // 获取首页banner | |||
| const banner = await homeApi.getBanner(); | |||
| // 发表评论 | |||
| await commentApi.saveComment({ bookId: '123', content: '很棒的小说!' }); | |||
| ``` | |||
| #### 方式二:全量导入 | |||
| ```javascript | |||
| import api from '@/api/modules.js'; | |||
| // 使用方式 | |||
| const books = await api.myBook.getMyShopPage({ pageNo: 1, pageSize: 10 }); | |||
| const userInfo = await api.auth.getUserByToken(); | |||
| ``` | |||
| ### 3. 在 Vue 组件中使用 | |||
| ```vue | |||
| <template> | |||
| <div> | |||
| <div v-if="loading">加载中...</div> | |||
| <div v-else-if="error">{{ error }}</div> | |||
| <div v-else> | |||
| <!-- 渲染数据 --> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import { ref, onMounted } from 'vue'; | |||
| import { homeApi } from '@/api/modules.js'; | |||
| const loading = ref(false); | |||
| const error = ref(null); | |||
| const bannerList = ref([]); | |||
| const fetchBanner = async () => { | |||
| loading.value = true; | |||
| try { | |||
| const response = await homeApi.getBanner(); | |||
| bannerList.value = response.result; | |||
| } catch (err) { | |||
| error.value = err.message; | |||
| } finally { | |||
| loading.value = false; | |||
| } | |||
| }; | |||
| onMounted(fetchBanner); | |||
| </script> | |||
| ``` | |||
| ## 📚 API 模块说明 | |||
| ### 🔐 授权登录模块 (authApi) | |||
| ```javascript | |||
| // 微信小程序登录 | |||
| authApi.appletLogin(params) | |||
| // 绑定手机号 | |||
| authApi.bindPhone(code) | |||
| // 获取用户信息 | |||
| authApi.getUserByToken() | |||
| // 更新用户信息 | |||
| authApi.updateUserInfo(userInfo) | |||
| // 获取平台配置 | |||
| authApi.getConfig() | |||
| ``` | |||
| ### 🏠 首页模块 (homeApi) | |||
| ```javascript | |||
| // 获取banner | |||
| homeApi.getBanner() | |||
| // 获取分类列表 | |||
| homeApi.getCategoryList() | |||
| // 获取书籍详情 | |||
| homeApi.getBookDetail({ id: bookId }) | |||
| // 获取书籍目录 | |||
| homeApi.getBookCatalogList({ bookId, pageNo: 1, pageSize: 20 }) | |||
| // 获取章节详情 | |||
| homeApi.getBookCatalogDetail(chapterId) | |||
| // 投票 | |||
| homeApi.vote({ bookId, num: '1' }) | |||
| ``` | |||
| ### 📚 书架模块 | |||
| #### 阅读书架 (readBookApi) | |||
| ```javascript | |||
| // 添加到阅读记录 | |||
| readBookApi.addReadBook(bookData) | |||
| // 获取阅读记录 | |||
| readBookApi.getReadBookPage({ pageNo: 1, pageSize: 10 }) | |||
| // 移除阅读记录 | |||
| readBookApi.removeReadBook(bookId) | |||
| ``` | |||
| #### 我的作品 (myBookApi) | |||
| ```javascript | |||
| // 获取我的作品 | |||
| myBookApi.getMyShopPage({ pageNo: 1, pageSize: 10 }) | |||
| // 创建/更新作品 | |||
| myBookApi.saveOrUpdateShop(shopData) | |||
| // 获取章节列表 | |||
| myBookApi.getMyShopNovelPage({ bookId, pageNo: 1, pageSize: 20 }) | |||
| // 创建/更新章节 | |||
| myBookApi.saveOrUpdateShopNovel(novelData) | |||
| // 删除作品 | |||
| myBookApi.deleteMyShop(bookId) | |||
| ``` | |||
| ### 👤 用户模块 | |||
| #### 评论 (commentApi) | |||
| ```javascript | |||
| // 发表评论 | |||
| commentApi.saveComment({ bookId, content }) | |||
| // 获取评论列表 | |||
| commentApi.getCommentList({ bookId, pageNo: 1, pageSize: 10 }) | |||
| // 回复评论 | |||
| commentApi.replyComment({ commentId, content }) | |||
| // 删除评论 | |||
| commentApi.deleteComment(commentId) | |||
| ``` | |||
| #### 任务 (taskApi) | |||
| ```javascript | |||
| // 获取签到任务 | |||
| taskApi.getSignTaskList() | |||
| // 执行签到 | |||
| taskApi.clickSignTask(taskId) | |||
| // 获取更多任务 | |||
| taskApi.getMoreTaskList() | |||
| // 获取推荐票数 | |||
| taskApi.getMyRecommendTicketNum() | |||
| ``` | |||
| ## 🔧 环境变量配置 | |||
| 在 `.env` 文件中配置API基础URL: | |||
| ```env | |||
| # 开发环境 | |||
| VITE_API_BASE_URL=http://127.0.0.1:8080 | |||
| # 生产环境 | |||
| VITE_API_BASE_URL=https://your-api-domain.com | |||
| ``` | |||
| ## 🛠️ 错误处理 | |||
| 所有API调用都应该使用 try-catch 进行错误处理: | |||
| ```javascript | |||
| try { | |||
| const result = await authApi.appletLogin(loginData); | |||
| // 处理成功结果 | |||
| } catch (error) { | |||
| // 处理错误 | |||
| console.error('登录失败:', error.message); | |||
| // 显示错误提示给用户 | |||
| } | |||
| ``` | |||
| ## 📝 注意事项 | |||
| 1. **认证token**: 所有需要认证的接口都会自动添加 `X-Access-Token` 头 | |||
| 2. **分页参数**: 统一使用 `pageNo` 和 `pageSize` | |||
| 3. **响应格式**: 所有接口返回统一的格式 `{ code, message, result, success, timestamp }` | |||
| 4. **错误处理**: 401错误会自动清除token并跳转到登录页 | |||
| 5. **超时设置**: 默认10秒超时,可在 `index.js` 中调整 | |||
| ## 🔗 相关链接 | |||
| - [Axios 官方文档](https://axios-http.com/) | |||
| - [Vue 3 文档](https://vuejs.org/) | |||
| - [Element Plus 文档](https://element-plus.org/) | |||
| ## 📞 技术支持 | |||
| 如有问题,请查看 `example.js` 中的使用示例或联系开发团队。 | |||
| @ -0,0 +1,200 @@ | |||
| import api from './index.js'; | |||
| /** | |||
| * 授权登录相关接口 | |||
| */ | |||
| export const authApi = { | |||
| /** | |||
| * 微信小程序授权登录 | |||
| * @param {Object} params 登录参数 | |||
| * @param {string} params.code 参数信息 | |||
| * @param {string} params.encryptedData 解密 | |||
| * @param {string} params.headimgurl 用户头像 | |||
| * @param {string} params.id 标识 | |||
| * @param {string} params.iv 解密标签 | |||
| * @param {string} params.nickName 用户姓名 | |||
| * @param {string} params.openid 用户唯一标识 | |||
| * @param {string} params.session_key 会话密钥 | |||
| * @param {string} params.shareId 邀请者销售标识 | |||
| * @param {string} params.state 类型 | |||
| * @param {string} params.vid 参数信息 | |||
| */ | |||
| async appletLogin(params) { | |||
| try { | |||
| const response = await api.get('/all_login/appletLogin', { params }); | |||
| // 登录成功后自动保存token和用户信息 | |||
| if (response.success && response.result) { | |||
| const { token, userInfo } = response.result; | |||
| if (token) { | |||
| // 保存认证token | |||
| localStorage.setItem('X-Access-Token', token); | |||
| localStorage.setItem('token', token); | |||
| console.log('[Auth] 登录成功,token已保存'); | |||
| } | |||
| if (userInfo) { | |||
| // 保存用户信息 | |||
| localStorage.setItem('user', JSON.stringify(userInfo)); | |||
| // 更新store状态 | |||
| try { | |||
| import('../store/index.js').then(({ useMainStore }) => { | |||
| const store = useMainStore(); | |||
| store.user = userInfo; | |||
| store.isLoggedIn = true; | |||
| store.token = token; | |||
| store.isAuthor = userInfo.isAuthor || false; | |||
| // 同步到localStorage | |||
| localStorage.setItem('isAuthor', userInfo.isAuthor || false); | |||
| console.log('[Auth] Store状态已更新'); | |||
| }); | |||
| } catch (err) { | |||
| console.warn('[Auth] 无法更新store状态:', err); | |||
| } | |||
| } | |||
| } | |||
| return response; | |||
| } catch (error) { | |||
| console.error('[Auth] 登录失败:', error); | |||
| throw error; | |||
| } | |||
| }, | |||
| /** | |||
| * 绑定手机号码 | |||
| * @param {string} code 授权码 | |||
| */ | |||
| bindPhone(code) { | |||
| return api.get('/all_login/bindPhone', { | |||
| params: { code } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取平台基础配置信息 | |||
| */ | |||
| getConfig() { | |||
| return api.get('/all_login/getConfig'); | |||
| }, | |||
| /** | |||
| * 获取用户信息 | |||
| */ | |||
| async getUserByToken() { | |||
| try { | |||
| const response = await api.get('/all_login/getUserByToken'); | |||
| // 更新本地用户信息 | |||
| if (response.success && response.result) { | |||
| localStorage.setItem('user', JSON.stringify(response.result)); | |||
| // 更新store | |||
| try { | |||
| import('../store/index.js').then(({ useMainStore }) => { | |||
| const store = useMainStore(); | |||
| store.user = response.result; | |||
| store.isAuthor = response.result.isAuthor || false; | |||
| localStorage.setItem('isAuthor', response.result.isAuthor || false); | |||
| }); | |||
| } catch (err) { | |||
| console.warn('[Auth] 无法更新store状态:', err); | |||
| } | |||
| } | |||
| return response; | |||
| } catch (error) { | |||
| console.error('[Auth] 获取用户信息失败:', error); | |||
| throw error; | |||
| } | |||
| }, | |||
| /** | |||
| * 更新用户信息 | |||
| * @param {Object} userInfo 用户信息 | |||
| * @param {string} userInfo.avatarUrl 头像 | |||
| * @param {string} userInfo.details 简介 | |||
| * @param {string} userInfo.name 别名 | |||
| * @param {string} userInfo.nickName 昵称 | |||
| * @param {string} userInfo.phone 电话 | |||
| */ | |||
| async updateUserInfo(userInfo) { | |||
| try { | |||
| const response = await api.post('/all_login/updateUserInfo', null, { | |||
| params: userInfo | |||
| }); | |||
| // 更新成功后重新获取用户信息 | |||
| if (response.success) { | |||
| await this.getUserByToken(); | |||
| } | |||
| return response; | |||
| } catch (error) { | |||
| console.error('[Auth] 更新用户信息失败:', error); | |||
| throw error; | |||
| } | |||
| }, | |||
| /** | |||
| * 手动登出 | |||
| * 清除所有登录状态和用户数据 | |||
| */ | |||
| logout() { | |||
| console.log('[Auth] 手动登出'); | |||
| // 清除本地存储 | |||
| localStorage.removeItem('X-Access-Token'); | |||
| localStorage.removeItem('token'); | |||
| localStorage.removeItem('user'); | |||
| localStorage.removeItem('isAuthor'); | |||
| localStorage.removeItem('bookshelf'); | |||
| // 清除store状态 | |||
| try { | |||
| import('../store/index.js').then(({ useMainStore }) => { | |||
| const store = useMainStore(); | |||
| store.logout(); | |||
| console.log('[Auth] Store状态已清除'); | |||
| }); | |||
| } catch (err) { | |||
| console.warn('[Auth] 无法清除store状态:', err); | |||
| } | |||
| // 跳转到首页 | |||
| if (window.location.pathname !== '/') { | |||
| window.location.href = '/'; | |||
| } | |||
| }, | |||
| /** | |||
| * 检查当前登录状态 | |||
| */ | |||
| checkLoginStatus() { | |||
| const token = localStorage.getItem('X-Access-Token'); | |||
| const user = localStorage.getItem('user'); | |||
| try { | |||
| const userInfo = user ? JSON.parse(user) : null; | |||
| return { | |||
| isLoggedIn: !!(token && userInfo), | |||
| token, | |||
| user: userInfo, | |||
| isAuthor: userInfo?.isAuthor || localStorage.getItem('isAuthor') === 'true' | |||
| }; | |||
| } catch (err) { | |||
| console.warn('[Auth] 解析用户信息失败:', err); | |||
| return { | |||
| isLoggedIn: false, | |||
| token: null, | |||
| user: null, | |||
| isAuthor: false | |||
| }; | |||
| } | |||
| } | |||
| }; | |||
| @ -0,0 +1,156 @@ | |||
| import api from './index.js'; | |||
| /** | |||
| * 书架-阅读书架相关接口 | |||
| */ | |||
| export const readBookApi = { | |||
| /** | |||
| * 增加我的书架阅读记录 | |||
| * @param {Object} bookData 书籍数据 | |||
| */ | |||
| addReadBook(bookData) { | |||
| return api.post('/all_book/addReadBook', null, { | |||
| params: bookData | |||
| }); | |||
| }, | |||
| /** | |||
| * 批量移除我阅读过的数据根据书籍标识 | |||
| * @param {string} bookIds 书籍ID列表,逗号分隔 | |||
| */ | |||
| batchRemoveReadBook(bookIds) { | |||
| return api.get('/all_book/batchRemoveReadBook', { | |||
| params: { bookIds } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取我阅读过的书籍列表带分页 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getReadBookPage(params) { | |||
| return api.get('/all_book/getReadBookPage', { params }); | |||
| }, | |||
| /** | |||
| * 移除我阅读过的书籍根据书籍标识 | |||
| * @param {string} bookId 书籍ID | |||
| */ | |||
| removeReadBook(bookId) { | |||
| return api.get('/all_book/removeReadBook', { | |||
| params: { bookId } | |||
| }); | |||
| } | |||
| }; | |||
| /** | |||
| * 书架-我的作品相关接口 | |||
| */ | |||
| export const myBookApi = { | |||
| /** | |||
| * 删除作品章节 | |||
| * @param {Object} params 删除参数 | |||
| * @param {string} params.bookId 书籍ID | |||
| * @param {string} params.id 章节ID | |||
| */ | |||
| deleteMyNovel(params) { | |||
| return api.post('/my_book/deleteMyNovel', null, { params }); | |||
| }, | |||
| /** | |||
| * 删除我的作品 | |||
| * @param {string} bookId 书籍ID | |||
| */ | |||
| deleteMyShop(bookId) { | |||
| return api.post('/my_book/deleteMyShop', null, { | |||
| params: { bookId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 多选删除我的作品 | |||
| * @param {string} bookIds 书籍ID列表,逗号分隔 | |||
| */ | |||
| deleteMyShopList(bookIds) { | |||
| return api.post('/my_book/deleteMyShopList', null, { | |||
| params: { bookIds } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取我的小说章节列表带分页 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.bookId 书籍ID | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| * @param {number} params.reverse 是否倒序 | |||
| * @param {number} params.status 状态 | |||
| */ | |||
| getMyShopNovelPage(params) { | |||
| return api.post('/my_book/getMyShopNovelPage', null, { params }); | |||
| }, | |||
| /** | |||
| * 获取我的作品带分页 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getMyShopPage(params) { | |||
| return api.get('/my_book/getMyShopPage', { params }); | |||
| }, | |||
| /** | |||
| * 添加作品或者修改作品 | |||
| * @param {Object} shopData 作品数据 | |||
| */ | |||
| saveOrUpdateShop(shopData) { | |||
| return api.post('/my_book/saveOrUpdateShop', null, { | |||
| params: shopData | |||
| }); | |||
| }, | |||
| /** | |||
| * 增加或修改作品章节 | |||
| * @param {Object} novelData 章节数据 | |||
| */ | |||
| saveOrUpdateShopNovel(novelData) { | |||
| return api.post('/my_book/saveOrUpdateShopNovel', null, { | |||
| params: novelData | |||
| }); | |||
| } | |||
| }; | |||
| /** | |||
| * 书架-读者成就相关接口 | |||
| */ | |||
| export const achievementApi = { | |||
| /** | |||
| * 根据用户标识和书籍标识查询该用户的成就等级 | |||
| * @param {string} bookId 书籍ID | |||
| */ | |||
| getAchievement(bookId) { | |||
| return api.get('/all_achievement/getAchievement', { | |||
| params: { bookId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取读者成就列表 | |||
| */ | |||
| getAchievementList() { | |||
| return api.get('/all_achievement/getAchievementList'); | |||
| }, | |||
| /** | |||
| * 设置读者成就等级名称 | |||
| * @param {Object} achievementData 成就数据 | |||
| */ | |||
| setAchievementName(achievementData) { | |||
| return api.post('/all_achievement/setAchievementName', null, { | |||
| params: achievementData | |||
| }); | |||
| } | |||
| }; | |||
| @ -0,0 +1,321 @@ | |||
| /** | |||
| * API使用示例 | |||
| * 展示如何在Vue组件中使用这些API接口 | |||
| */ | |||
| // 导入方式1:按需导入 | |||
| import { authApi, homeApi, commentApi, checkAuthStatus, clearAuthStatus } from './modules.js'; | |||
| // 导入方式2:全量导入 | |||
| import api from './modules.js'; | |||
| // 使用示例 | |||
| export const apiExamples = { | |||
| // 1. 用户登录示例 | |||
| async userLogin() { | |||
| try { | |||
| const loginData = { | |||
| code: 'wx_auth_code', | |||
| nickName: '用户昵称', | |||
| headimgurl: 'http://avatar.url', | |||
| openid: 'wx_openid' | |||
| }; | |||
| const response = await authApi.appletLogin(loginData); | |||
| if (response.success) { | |||
| console.log('登录成功:', response.result); | |||
| // token和用户信息已自动保存,store状态已更新 | |||
| return response.result; | |||
| } | |||
| } catch (error) { | |||
| console.error('登录失败:', error.message); | |||
| throw error; | |||
| } | |||
| }, | |||
| // 2. 检查登录状态示例 | |||
| checkUserLoginStatus() { | |||
| // 使用辅助函数检查登录状态 | |||
| const authStatus = authApi.checkLoginStatus(); | |||
| console.log('当前登录状态:', authStatus); | |||
| if (authStatus.isLoggedIn) { | |||
| console.log('用户已登录:', authStatus.user); | |||
| console.log('是否为作家:', authStatus.isAuthor); | |||
| } else { | |||
| console.log('用户未登录'); | |||
| } | |||
| return authStatus; | |||
| }, | |||
| // 3. 处理需要认证的API调用 | |||
| async callProtectedApi() { | |||
| try { | |||
| // 首先检查登录状态 | |||
| if (!checkAuthStatus()) { | |||
| throw new Error('用户未登录'); | |||
| } | |||
| // 调用需要认证的接口 | |||
| const response = await api.myBook.getMyShopPage({ pageNo: 1, pageSize: 10 }); | |||
| return response.result; | |||
| } catch (error) { | |||
| if (error.message.includes('401') || error.message.includes('未授权')) { | |||
| console.log('认证失败,已自动处理登录状态'); | |||
| // 401错误已经被自动处理,这里可以提示用户重新登录 | |||
| return null; | |||
| } | |||
| throw error; | |||
| } | |||
| }, | |||
| // 4. 获取首页数据示例 | |||
| async getHomeData() { | |||
| try { | |||
| // 并行请求多个接口 | |||
| const [banner, categories, newBooks] = await Promise.all([ | |||
| homeApi.getBanner(), | |||
| homeApi.getCategoryList(), | |||
| homeApi.getNewList({ pageNo: 1, pageSize: 10 }) | |||
| ]); | |||
| return { | |||
| banner: banner.result, | |||
| categories: categories.result, | |||
| newBooks: newBooks.result | |||
| }; | |||
| } catch (error) { | |||
| console.error('获取首页数据失败:', error.message); | |||
| throw error; | |||
| } | |||
| }, | |||
| // 5. 获取书籍详情示例 | |||
| async getBookDetail(bookId) { | |||
| try { | |||
| const response = await homeApi.getBookDetail({ id: bookId }); | |||
| return response.result; | |||
| } catch (error) { | |||
| console.error('获取书籍详情失败:', error.message); | |||
| throw error; | |||
| } | |||
| }, | |||
| // 6. 发表评论示例(需要登录) | |||
| async postComment(bookId, content) { | |||
| try { | |||
| // 检查登录状态 | |||
| if (!checkAuthStatus()) { | |||
| throw new Error('请先登录后再发表评论'); | |||
| } | |||
| const response = await commentApi.saveComment({ | |||
| bookId, | |||
| content | |||
| }); | |||
| if (response.success) { | |||
| console.log('评论发表成功'); | |||
| return response.result; | |||
| } | |||
| } catch (error) { | |||
| console.error('发表评论失败:', error.message); | |||
| throw error; | |||
| } | |||
| }, | |||
| // 7. 分页获取数据示例 | |||
| async getMyBooks(page = 1, size = 10) { | |||
| try { | |||
| const response = await api.myBook.getMyShopPage({ | |||
| pageNo: page, | |||
| pageSize: size | |||
| }); | |||
| return { | |||
| list: response.result.records, | |||
| total: response.result.total, | |||
| current: response.result.current | |||
| }; | |||
| } catch (error) { | |||
| console.error('获取我的作品失败:', error.message); | |||
| throw error; | |||
| } | |||
| }, | |||
| // 8. 手动登出示例 | |||
| async logout() { | |||
| try { | |||
| // 调用登出API | |||
| authApi.logout(); | |||
| console.log('已登出'); | |||
| } catch (error) { | |||
| console.error('登出失败:', error.message); | |||
| // 即使API调用失败,也要清除本地状态 | |||
| clearAuthStatus(); | |||
| } | |||
| }, | |||
| // 9. 文件上传示例(如果需要) | |||
| async uploadImage(file) { | |||
| try { | |||
| // 检查登录状态 | |||
| if (!checkAuthStatus()) { | |||
| throw new Error('请先登录'); | |||
| } | |||
| const formData = new FormData(); | |||
| formData.append('file', file); | |||
| // 注意:这里需要根据实际的上传接口调整 | |||
| const response = await fetch('/api/upload', { | |||
| method: 'POST', | |||
| headers: { | |||
| 'X-Access-Token': localStorage.getItem('X-Access-Token') | |||
| }, | |||
| body: formData | |||
| }); | |||
| if (response.status === 401) { | |||
| // 401错误会被axios拦截器处理 | |||
| throw new Error('认证失败'); | |||
| } | |||
| return await response.json(); | |||
| } catch (error) { | |||
| console.error('文件上传失败:', error.message); | |||
| throw error; | |||
| } | |||
| } | |||
| }; | |||
| // Vue 3 Composition API 使用示例 | |||
| export function useApiExamples() { | |||
| const { authApi, homeApi, commentApi } = api; | |||
| // 响应式数据 | |||
| const loading = ref(false); | |||
| const error = ref(null); | |||
| const authStatus = ref(authApi.checkLoginStatus()); | |||
| // 封装API调用 | |||
| const callApi = async (apiFunction, ...args) => { | |||
| loading.value = true; | |||
| error.value = null; | |||
| try { | |||
| const result = await apiFunction(...args); | |||
| return result; | |||
| } catch (err) { | |||
| error.value = err.message; | |||
| throw err; | |||
| } finally { | |||
| loading.value = false; | |||
| } | |||
| }; | |||
| // 带认证检查的API调用 | |||
| const callProtectedApi = async (apiFunction, ...args) => { | |||
| // 检查登录状态 | |||
| const currentAuthStatus = authApi.checkLoginStatus(); | |||
| authStatus.value = currentAuthStatus; | |||
| if (!currentAuthStatus.isLoggedIn) { | |||
| error.value = '请先登录'; | |||
| throw new Error('请先登录'); | |||
| } | |||
| return callApi(apiFunction, ...args); | |||
| }; | |||
| // 监听认证状态变化 | |||
| const refreshAuthStatus = () => { | |||
| authStatus.value = authApi.checkLoginStatus(); | |||
| }; | |||
| // 登录方法 | |||
| const login = async (...args) => { | |||
| const result = await callApi(authApi.appletLogin, ...args); | |||
| refreshAuthStatus(); // 登录后刷新状态 | |||
| return result; | |||
| }; | |||
| // 登出方法 | |||
| const logout = () => { | |||
| authApi.logout(); | |||
| refreshAuthStatus(); // 登出后刷新状态 | |||
| }; | |||
| return { | |||
| loading, | |||
| error, | |||
| authStatus, | |||
| callApi, | |||
| callProtectedApi, | |||
| refreshAuthStatus, | |||
| // 具体的API方法 | |||
| login, | |||
| logout, | |||
| getBanner: (...args) => callApi(homeApi.getBanner, ...args), | |||
| postComment: (...args) => callProtectedApi(commentApi.saveComment, ...args), | |||
| getMyBooks: (...args) => callProtectedApi(api.myBook.getMyShopPage, ...args) | |||
| }; | |||
| } | |||
| // 错误处理最佳实践示例 | |||
| export const errorHandlingExamples = { | |||
| // 标准错误处理模式 | |||
| async standardErrorHandling() { | |||
| try { | |||
| const response = await homeApi.getBanner(); | |||
| return response.result; | |||
| } catch (error) { | |||
| // 根据错误类型进行不同处理 | |||
| if (error.message.includes('网络')) { | |||
| // 网络错误 | |||
| console.error('网络连接失败,请检查网络设置'); | |||
| } else if (error.message.includes('401')) { | |||
| // 认证错误(已被自动处理) | |||
| console.error('认证失败,请重新登录'); | |||
| } else if (error.message.includes('403')) { | |||
| // 权限错误 | |||
| console.error('没有权限访问此资源'); | |||
| } else { | |||
| // 其他错误 | |||
| console.error('请求失败:', error.message); | |||
| } | |||
| throw error; | |||
| } | |||
| }, | |||
| // 重试机制示例 | |||
| async withRetry(apiFunction, maxRetries = 3, delay = 1000) { | |||
| let lastError; | |||
| for (let i = 0; i < maxRetries; i++) { | |||
| try { | |||
| return await apiFunction(); | |||
| } catch (error) { | |||
| lastError = error; | |||
| // 401错误不重试 | |||
| if (error.message.includes('401')) { | |||
| throw error; | |||
| } | |||
| // 最后一次重试失败 | |||
| if (i === maxRetries - 1) { | |||
| throw error; | |||
| } | |||
| // 等待后重试 | |||
| await new Promise(resolve => setTimeout(resolve, delay)); | |||
| console.log(`第${i + 1}次重试...`); | |||
| } | |||
| } | |||
| throw lastError; | |||
| } | |||
| }; | |||
| @ -0,0 +1,157 @@ | |||
| import api from './index.js'; | |||
| /** | |||
| * 首页相关接口 | |||
| */ | |||
| export const homeApi = { | |||
| /** | |||
| * 获取首页banner | |||
| */ | |||
| getBanner() { | |||
| return api.get('/all_index/getBanner'); | |||
| }, | |||
| /** | |||
| * 获取书城区域列表 | |||
| */ | |||
| getBookAreaList() { | |||
| return api.get('/all_index/getBookAreaList'); | |||
| }, | |||
| /** | |||
| * 根据目录查询章节小说信息明细 | |||
| * @param {string} id 目录ID | |||
| */ | |||
| getBookCatalogDetail(id) { | |||
| return api.get('/all_index/getBookCatalogDetail', { | |||
| params: { id } | |||
| }); | |||
| }, | |||
| /** | |||
| * 根据书本标识获取书本目录列表 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.bookId 书本ID | |||
| * @param {string} params.orderBy 排序方式 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getBookCatalogList(params) { | |||
| return api.get('/all_index/getBookCatalogList', { params }); | |||
| }, | |||
| /** | |||
| * 根据书本标识获取书本详细信息 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.id 书本ID | |||
| * @param {string} params.token token | |||
| */ | |||
| getBookDetail(params) { | |||
| return api.get('/all_index/getBookDetail', { params }); | |||
| }, | |||
| /** | |||
| * 获取书城分类列表 | |||
| */ | |||
| getCategoryList() { | |||
| return api.get('/all_index/getCategoryList'); | |||
| }, | |||
| /** | |||
| * 获取亲密度排行版 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.bookId 书本ID | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getIntimacyRankList(params) { | |||
| return api.get('/all_index/getIntimacyRankList', { params }); | |||
| }, | |||
| /** | |||
| * 获取首页最新小说列表带分页 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getNewList(params) { | |||
| return api.get('/all_index/getNewList', { params }); | |||
| }, | |||
| /** | |||
| * 获取首页公告 | |||
| */ | |||
| getNotice() { | |||
| return api.get('/all_index/getNotice'); | |||
| }, | |||
| /** | |||
| * 获取公告详情 | |||
| * @param {string} id 公告ID | |||
| */ | |||
| getNoticeById(id) { | |||
| return api.get('/all_index/getNoticeById', { | |||
| params: { id } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取公告列表带分页 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getNoticePage(params) { | |||
| return api.get('/all_index/getNoticePage', { params }); | |||
| }, | |||
| /** | |||
| * 获取首页精品推荐小说列表带分页 | |||
| */ | |||
| getRecommendList(params) { | |||
| return api.get('/all_index/getRecommendList', { params }); | |||
| }, | |||
| /** | |||
| * 根据书本标识进行投票 | |||
| * @param {Object} params 投票参数 | |||
| * @param {string} params.bookId 书本ID | |||
| * @param {string} params.num 投票数 | |||
| */ | |||
| vote(params) { | |||
| return api.get('/all_index/vote', { params }); | |||
| }, | |||
| /** | |||
| * 获取排行榜数据 - 根据不同榜单类型和分类获取书籍列表 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.rankType 榜单类型 (1:推荐榜 2:完本榜 3:阅读榜 4:口碑榜 5:新书榜 6:高分榜) | |||
| * @param {string} params.categoryId 分类ID | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getRankingList(params) { | |||
| const { rankType, categoryId, pageNo = 1, pageSize = 20 } = params; | |||
| // 根据不同的榜单类型调用不同的接口 | |||
| switch (rankType) { | |||
| case '1': // 推荐榜 | |||
| return api.get('/all_index/getRecommendList', { | |||
| params: { pageNo, pageSize, categoryId } | |||
| }); | |||
| case '5': // 新书榜 | |||
| return api.get('/all_index/getNewList', { | |||
| params: { pageNo, pageSize, categoryId } | |||
| }); | |||
| case '3': // 阅读榜 (使用亲密度排行榜作为替代) | |||
| return api.get('/all_index/getIntimacyRankList', { | |||
| params: { pageNo, pageSize, bookId: categoryId } | |||
| }); | |||
| default: | |||
| // 其他榜单暂时使用推荐列表作为占位 | |||
| return api.get('/all_index/getRecommendList', { | |||
| params: { pageNo, pageSize, categoryId } | |||
| }); | |||
| } | |||
| } | |||
| }; | |||
| @ -0,0 +1,41 @@ | |||
| // 统一导出所有API模块 | |||
| export { authApi } from './auth.js'; | |||
| export { homeApi } from './home.js'; | |||
| export { readBookApi, myBookApi, achievementApi } from './bookshelf.js'; | |||
| export { moneyApi, commentApi, taskApi, writerApi, orderApi } from './user.js'; | |||
| // 导出辅助函数 | |||
| export { checkAuthStatus, clearAuthStatus } from './index.js'; | |||
| // 默认导出,方便直接使用 | |||
| import { authApi } from './auth.js'; | |||
| import { homeApi } from './home.js'; | |||
| import { readBookApi, myBookApi, achievementApi } from './bookshelf.js'; | |||
| import { moneyApi, commentApi, taskApi, writerApi, orderApi } from './user.js'; | |||
| import { checkAuthStatus, clearAuthStatus } from './index.js'; | |||
| export default { | |||
| // 授权登录 | |||
| auth: authApi, | |||
| // 首页 | |||
| home: homeApi, | |||
| // 书架相关 | |||
| readBook: readBookApi, | |||
| myBook: myBookApi, | |||
| achievement: achievementApi, | |||
| // 用户相关 | |||
| money: moneyApi, | |||
| comment: commentApi, | |||
| task: taskApi, | |||
| writer: writerApi, | |||
| order: orderApi, | |||
| // 辅助工具 | |||
| utils: { | |||
| checkAuthStatus, | |||
| clearAuthStatus | |||
| } | |||
| }; | |||
| @ -0,0 +1,144 @@ | |||
| /** | |||
| * API接口类型定义 | |||
| */ | |||
| // 基础响应类型 | |||
| export interface ApiResponse<T = any> { | |||
| code: number; | |||
| message: string; | |||
| result: T; | |||
| success: boolean; | |||
| timestamp: number; | |||
| } | |||
| // 分页参数 | |||
| export interface PaginationParams { | |||
| pageNo?: number; | |||
| pageSize?: number; | |||
| } | |||
| // 分页响应 | |||
| export interface PaginationResponse<T> { | |||
| records: T[]; | |||
| total: number; | |||
| current: number; | |||
| size: number; | |||
| pages: number; | |||
| } | |||
| // 用户登录参数 | |||
| export interface LoginParams { | |||
| code?: string; | |||
| encryptedData?: string; | |||
| headimgurl?: string; | |||
| id?: string; | |||
| iv?: string; | |||
| nickName?: string; | |||
| openid?: string; | |||
| session_key?: string; | |||
| shareId?: string; | |||
| state?: string; | |||
| vid?: string; | |||
| } | |||
| // 用户信息 | |||
| export interface UserInfo { | |||
| avatarUrl?: string; | |||
| details?: string; | |||
| name?: string; | |||
| nickName?: string; | |||
| phone?: string; | |||
| } | |||
| // 书籍信息 | |||
| export interface BookInfo { | |||
| id: string; | |||
| name: string; | |||
| author: string; | |||
| image: string; | |||
| details: string; | |||
| shopClass: string; | |||
| shopCion: string; | |||
| status: number; | |||
| bookStatus: number; | |||
| tuiNum: number; | |||
| qmNum: number; | |||
| } | |||
| // 章节信息 | |||
| export interface ChapterInfo { | |||
| id: string; | |||
| bookId: string; | |||
| title: string; | |||
| details: string; | |||
| num: number; | |||
| sort: number; | |||
| status: number; | |||
| isPay: string; | |||
| } | |||
| // 评论信息 | |||
| export interface CommentInfo { | |||
| id: string; | |||
| bookId: string; | |||
| content: string; | |||
| userId: string; | |||
| createTime: string; | |||
| } | |||
| // 任务信息 | |||
| export interface TaskInfo { | |||
| id: string; | |||
| name: string; | |||
| description: string; | |||
| reward: number; | |||
| status: number; | |||
| } | |||
| // 礼物信息 | |||
| export interface GiftInfo { | |||
| id: string; | |||
| name: string; | |||
| price: number; | |||
| image: string; | |||
| description: string; | |||
| } | |||
| // 订单信息 | |||
| export interface OrderInfo { | |||
| id: string; | |||
| giftId: string; | |||
| num: number; | |||
| totalPrice: number; | |||
| status: number; | |||
| createTime: string; | |||
| } | |||
| // 成就信息 | |||
| export interface AchievementInfo { | |||
| id: string; | |||
| bookId: string; | |||
| userId: string; | |||
| oneName: string; | |||
| twoName: string; | |||
| threeName: string; | |||
| oneNum: number; | |||
| twoNum: number; | |||
| threeNum: number; | |||
| } | |||
| // 作家信息 | |||
| export interface WriterInfo { | |||
| penName: string; | |||
| details: string; | |||
| } | |||
| // 流水记录 | |||
| export interface MoneyLogInfo { | |||
| id: string; | |||
| amount: number; | |||
| type: string; | |||
| description: string; | |||
| createTime: string; | |||
| status: number; | |||
| } | |||
| @ -0,0 +1,268 @@ | |||
| import api from './index.js'; | |||
| /** | |||
| * 我的-流水相关接口 | |||
| */ | |||
| export const moneyApi = { | |||
| /** | |||
| * 获取我的流水列表带分页 | |||
| * @param {Object} params 查询参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| * @param {number} params.status 状态 | |||
| */ | |||
| getMyMoneyLogPage(params) { | |||
| return api.get('/all_money/getMyMoneyLogPage', { params }); | |||
| }, | |||
| /** | |||
| * 获取我的可用积分数 | |||
| */ | |||
| getMyMoneyNum() { | |||
| return api.get('/all_money/getMyMoneyNum'); | |||
| } | |||
| }; | |||
| /** | |||
| * 我的-评论相关接口 | |||
| */ | |||
| export const commentApi = { | |||
| /** | |||
| * 删除评论信息 | |||
| * @param {string} commentId 评论ID | |||
| */ | |||
| deleteComment(commentId) { | |||
| return api.get('/my_comment/deleteComment', { | |||
| params: { commentId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取评论详情 | |||
| * @param {string} commentId 评论ID | |||
| */ | |||
| getCommentDetail(commentId) { | |||
| return api.post('/my_comment/getCommentDetail', null, { | |||
| params: { commentId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 根据书籍标识查询评论信息列表带分页 | |||
| * @param {Object} params 查询参数 | |||
| * @param {string} params.bookId 书籍ID | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getCommentList(params) { | |||
| return api.get('/my_comment/getCommentList', { params }); | |||
| }, | |||
| /** | |||
| * 获取我的评论列表 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getMyCommentList(params) { | |||
| return api.get('/my_comment/getMyCommentList', { params }); | |||
| }, | |||
| /** | |||
| * 获取我的评论数 | |||
| */ | |||
| getMyCommentNum() { | |||
| return api.get('/my_comment/getMyCommentNum'); | |||
| }, | |||
| /** | |||
| * 回复评论信息 | |||
| * @param {Object} params 回复参数 | |||
| * @param {string} params.commentId 评论ID | |||
| * @param {string} params.content 回复内容 | |||
| */ | |||
| replyComment(params) { | |||
| return api.post('/my_comment/replyComment', null, { params }); | |||
| }, | |||
| /** | |||
| * 保存评论信息 | |||
| * @param {Object} params 评论参数 | |||
| * @param {string} params.bookId 书籍ID | |||
| * @param {string} params.content 评论内容 | |||
| */ | |||
| saveComment(params) { | |||
| return api.post('/my_comment/saveComment', null, { params }); | |||
| }, | |||
| /** | |||
| * 更新评论已读状态 | |||
| * @param {string} commentId 评论ID | |||
| */ | |||
| updateCommentRead(commentId) { | |||
| return api.post('/my_comment/updateCommentRead', null, { | |||
| params: { commentId } | |||
| }); | |||
| } | |||
| }; | |||
| /** | |||
| * 我的-任务中心相关接口 | |||
| */ | |||
| export const taskApi = { | |||
| /** | |||
| * 点击更多任务 | |||
| * @param {string} taskId 任务ID | |||
| */ | |||
| clickMoreTask(taskId) { | |||
| return api.post('/my_task/clickMoreTask', null, { | |||
| params: { taskId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 点击签到任务 | |||
| * @param {string} taskId 任务ID | |||
| */ | |||
| clickSignTask(taskId) { | |||
| return api.get('/my_task/clickSignTask', { | |||
| params: { taskId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取更多任务列表 | |||
| * @param {string} token token | |||
| */ | |||
| getMoreTaskList(token) { | |||
| return api.get('/my_task/getMoreTaskList', { | |||
| params: { token } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取更多任务记录列表 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getMoreTaskRecordPage(params) { | |||
| return api.get('/my_task/getMoreTaskRecordPage', { params }); | |||
| }, | |||
| /** | |||
| * 获取我的推荐票数 | |||
| */ | |||
| getMyRecommendTicketNum() { | |||
| return api.get('/my_task/getMyRecommendTicketNum'); | |||
| }, | |||
| /** | |||
| * 获取我的推荐任务列表 | |||
| * @param {string} token token | |||
| */ | |||
| getSignTaskList(token) { | |||
| return api.get('/my_task/getSignTaskList', { | |||
| params: { token } | |||
| }); | |||
| }, | |||
| /** | |||
| * 获取我的推荐任务记录列表 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getSignTaskRecordPage(params) { | |||
| return api.get('/my_task/getSignTaskRecordPage', { params }); | |||
| } | |||
| }; | |||
| /** | |||
| * 我的-申请成为作家相关接口 | |||
| */ | |||
| export const writerApi = { | |||
| /** | |||
| * 查询我的笔名以及简介 | |||
| */ | |||
| getMyWriter() { | |||
| return api.get('/my_writer/getMyWriter'); | |||
| }, | |||
| /** | |||
| * 填写或修改笔名以及简介成为作家 | |||
| * @param {Object} params 作家信息 | |||
| * @param {string} params.details 简介 | |||
| * @param {string} params.penName 笔名 | |||
| */ | |||
| saveOrUpdateWriter(params) { | |||
| return api.post('/my_writer/saveOrUpdateWriter', null, { params }); | |||
| } | |||
| }; | |||
| /** | |||
| * 我的-礼物订阅接口 | |||
| */ | |||
| export const orderApi = { | |||
| /** | |||
| * 创建订单 | |||
| * @param {Object} params 订单参数 | |||
| * @param {string} params.giftId 礼物ID | |||
| * @param {number} params.num 数量 | |||
| * @param {string} params.token token | |||
| */ | |||
| createOrder(params) { | |||
| return api.post('/my_order/createOrder', null, { params }); | |||
| }, | |||
| /** | |||
| * 查询礼物详情 | |||
| * @param {string} giftId 礼物ID | |||
| */ | |||
| getGiftDetail(giftId) { | |||
| return api.get('/my_order/getGiftDetail', { | |||
| params: { giftId } | |||
| }); | |||
| }, | |||
| /** | |||
| * 查询互动打赏礼物信息列表 | |||
| * @param {Object} params 分页参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| */ | |||
| getInteractionGiftList(params) { | |||
| return api.get('/my_order/getInteractionGiftList', { params }); | |||
| }, | |||
| /** | |||
| * 查询我的礼物包订单列表 | |||
| * @param {Object} params 查询参数 | |||
| * @param {number} params.pageNo 当前页 | |||
| * @param {number} params.pageSize 显示条数 | |||
| * @param {string} params.token token | |||
| */ | |||
| getMyGiftList(params) { | |||
| return api.get('/my_order/getMyGiftList', { params }); | |||
| }, | |||
| /** | |||
| * 支付订单 | |||
| * @param {Object} params 支付参数 | |||
| * @param {string} params.orderId 订单ID | |||
| * @param {string} params.token token | |||
| */ | |||
| payOrder(params) { | |||
| return api.post('/my_order/payOrder', null, { params }); | |||
| }, | |||
| /** | |||
| * 支付成功 | |||
| * @param {string} orderId 订单ID | |||
| */ | |||
| paySuccess(orderId) { | |||
| return api.post('/my_order/paySuccess', null, { | |||
| params: { orderId } | |||
| }); | |||
| } | |||
| }; | |||
| @ -0,0 +1,330 @@ | |||
| <template> | |||
| <div class="notice-detail"> | |||
| <!-- 加载状态 --> | |||
| <div v-if="loading" class="loading-container"> | |||
| <el-skeleton :rows="8" animated> | |||
| <template #template> | |||
| <el-skeleton-item variant="text" style="width: 60%; margin-bottom: 20px;" /> | |||
| <el-skeleton-item variant="text" style="width: 30%; margin-bottom: 30px;" /> | |||
| <el-skeleton-item variant="text" style="width: 100%; margin-bottom: 15px;" /> | |||
| <el-skeleton-item variant="text" style="width: 100%; margin-bottom: 15px;" /> | |||
| <el-skeleton-item variant="text" style="width: 80%; margin-bottom: 15px;" /> | |||
| <el-skeleton-item variant="text" style="width: 100%; margin-bottom: 15px;" /> | |||
| <el-skeleton-item variant="text" style="width: 90%; margin-bottom: 15px;" /> | |||
| <el-skeleton-item variant="text" style="width: 70%;" /> | |||
| </template> | |||
| </el-skeleton> | |||
| </div> | |||
| <!-- 错误状态 --> | |||
| <div v-else-if="error" class="error-container"> | |||
| <el-empty description="加载失败"> | |||
| <el-button type="primary" @click="fetchNoticeDetail">重新加载</el-button> | |||
| </el-empty> | |||
| </div> | |||
| <!-- 公告内容 --> | |||
| <div v-else-if="noticeDetail" class="notice-content"> | |||
| <!-- 头部信息 --> | |||
| <div class="notice-header"> | |||
| <h1 class="notice-title">{{ noticeDetail.title }}</h1> | |||
| <div class="notice-meta" style="padding-bottom: 10px;"> | |||
| <span v-if="noticeDetail.titleText" class="notice-time"> | |||
| {{ noticeDetail.titleText }} | |||
| </span> | |||
| </div> | |||
| <div class="notice-meta"> | |||
| <span v-if="noticeDetail.typeText" class="notice-type"> | |||
| <el-tag :type="getTagType(noticeDetail.typeText)">{{ noticeDetail.typeText }}</el-tag> | |||
| </span> | |||
| <span v-if="noticeDetail.createTime" class="notice-time"> | |||
| 发布时间:{{ formatTime(noticeDetail.createTime) }} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <!-- 内容区域 --> | |||
| <div class="notice-body"> | |||
| <div v-if="noticeDetail.details" class="notice-text" v-html="noticeDetail.details"></div> | |||
| <div v-else class="no-content"> | |||
| <el-empty description="暂无内容" /> | |||
| </div> | |||
| </div> | |||
| <!-- 返回按钮 --> | |||
| <div class="notice-footer"> | |||
| <el-button @click="goBack">返回</el-button> | |||
| </div> | |||
| </div> | |||
| <!-- 无数据状态 --> | |||
| <div v-else class="no-data"> | |||
| <el-empty description="公告不存在" /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script setup> | |||
| import { ref, onMounted } from 'vue'; | |||
| import { useRoute, useRouter } from 'vue-router'; | |||
| import { ElMessage } from 'element-plus'; | |||
| import { homeApi } from '@/api/home.js'; | |||
| import Breadcrumb from '@/layout/components/Breadcrumb.vue'; | |||
| const route = useRoute(); | |||
| const router = useRouter(); | |||
| // 响应式数据 | |||
| const loading = ref(true); | |||
| const error = ref(false); | |||
| const noticeDetail = ref(null); | |||
| // 获取公告详情 | |||
| const fetchNoticeDetail = async () => { | |||
| try { | |||
| loading.value = true; | |||
| error.value = false; | |||
| const noticeId = route.params.id; | |||
| if (!noticeId) { | |||
| throw new Error('公告ID不存在'); | |||
| } | |||
| const response = await homeApi.getNoticeById(noticeId); | |||
| if (response && response.success) { | |||
| noticeDetail.value = response.result; | |||
| } else { | |||
| throw new Error(response?.message || '获取公告详情失败'); | |||
| } | |||
| } catch (err) { | |||
| console.error('获取公告详情失败:', err); | |||
| error.value = true; | |||
| ElMessage.error(err.message || '获取公告详情失败'); | |||
| } finally { | |||
| loading.value = false; | |||
| } | |||
| }; | |||
| // 格式化时间 | |||
| const formatTime = (timeStr) => { | |||
| if (!timeStr) return ''; | |||
| const date = new Date(timeStr); | |||
| return date.toLocaleString('zh-CN', { | |||
| year: 'numeric', | |||
| month: '2-digit', | |||
| day: '2-digit', | |||
| hour: '2-digit', | |||
| minute: '2-digit' | |||
| }); | |||
| }; | |||
| // 获取标签类型 | |||
| const getTagType = (typeText) => { | |||
| const typeMap = { | |||
| '系统公告': 'info', | |||
| '活动公告': 'success', | |||
| '维护公告': 'warning', | |||
| '紧急公告': 'danger' | |||
| }; | |||
| return typeMap[typeText] || 'info'; | |||
| }; | |||
| // 返回上一页 | |||
| const goBack = () => { | |||
| if (window.history.length > 1) { | |||
| router.go(-1); | |||
| } else { | |||
| router.push('/'); | |||
| } | |||
| }; | |||
| // 组件挂载时获取数据 | |||
| onMounted(() => { | |||
| fetchNoticeDetail(); | |||
| }); | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .notice-detail { | |||
| // margin: 0 auto; | |||
| margin: 20px 0; | |||
| padding: 40px; | |||
| background: #fff; | |||
| border-radius: 8px; | |||
| box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); | |||
| .loading-container, | |||
| .error-container, | |||
| .no-data { | |||
| padding: 40px 20px; | |||
| text-align: center; | |||
| } | |||
| .notice-content { | |||
| .notice-header { | |||
| border-bottom: 2px solid #f5f5f5; | |||
| padding-bottom: 20px; | |||
| margin-bottom: 30px; | |||
| .notice-title { | |||
| font-size: 28px; | |||
| font-weight: 600; | |||
| color: #333; | |||
| line-height: 1.4; | |||
| margin: 0 0 15px 0; | |||
| } | |||
| .notice-meta { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 20px; | |||
| font-size: 14px; | |||
| color: #666; | |||
| .notice-type { | |||
| :deep(.el-tag) { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| .notice-time { | |||
| color: #999; | |||
| } | |||
| } | |||
| } | |||
| .notice-body { | |||
| margin-bottom: 40px; | |||
| .notice-text { | |||
| font-size: 16px; | |||
| line-height: 1.8; | |||
| color: #333; | |||
| word-wrap: break-word; | |||
| // 富文本内容样式 | |||
| :deep(h1), | |||
| :deep(h2), | |||
| :deep(h3), | |||
| :deep(h4), | |||
| :deep(h5), | |||
| :deep(h6) { | |||
| margin: 20px 0 15px 0; | |||
| color: #0A2463; | |||
| } | |||
| :deep(p) { | |||
| margin: 15px 0; | |||
| line-height: 1.8; | |||
| } | |||
| :deep(img) { | |||
| max-width: 100%; | |||
| height: auto; | |||
| border-radius: 4px; | |||
| margin: 15px 0; | |||
| } | |||
| :deep(ul), | |||
| :deep(ol) { | |||
| margin: 15px 0; | |||
| padding-left: 30px; | |||
| } | |||
| :deep(li) { | |||
| margin: 8px 0; | |||
| line-height: 1.6; | |||
| } | |||
| :deep(blockquote) { | |||
| margin: 20px 0; | |||
| padding: 15px 20px; | |||
| background: #f8f9fa; | |||
| border-left: 4px solid #0A2463; | |||
| border-radius: 4px; | |||
| } | |||
| :deep(code) { | |||
| background: #f1f3f4; | |||
| padding: 2px 6px; | |||
| border-radius: 3px; | |||
| font-family: 'Courier New', monospace; | |||
| font-size: 14px; | |||
| } | |||
| :deep(pre) { | |||
| background: #f8f9fa; | |||
| padding: 15px; | |||
| border-radius: 4px; | |||
| overflow-x: auto; | |||
| margin: 15px 0; | |||
| } | |||
| :deep(table) { | |||
| width: 100%; | |||
| border-collapse: collapse; | |||
| margin: 20px 0; | |||
| } | |||
| :deep(th), | |||
| :deep(td) { | |||
| border: 1px solid #e0e0e0; | |||
| padding: 10px; | |||
| text-align: left; | |||
| } | |||
| :deep(th) { | |||
| background: #f5f5f5; | |||
| font-weight: 600; | |||
| } | |||
| :deep(a) { | |||
| color: #0A2463; | |||
| text-decoration: none; | |||
| &:hover { | |||
| text-decoration: underline; | |||
| } | |||
| } | |||
| } | |||
| .no-content { | |||
| padding: 60px 20px; | |||
| } | |||
| } | |||
| .notice-footer { | |||
| text-align: center; | |||
| padding-top: 20px; | |||
| border-top: 1px solid #f5f5f5; | |||
| } | |||
| } | |||
| } | |||
| // 响应式设计 | |||
| @media (max-width: 768px) { | |||
| .notice-detail { | |||
| margin: 0; | |||
| padding: 15px; | |||
| border-radius: 0; | |||
| box-shadow: none; | |||
| .notice-content .notice-header { | |||
| .notice-title { | |||
| font-size: 24px; | |||
| } | |||
| .notice-meta { | |||
| flex-direction: column; | |||
| align-items: flex-start; | |||
| gap: 10px; | |||
| } | |||
| } | |||
| .notice-content .notice-body .notice-text { | |||
| font-size: 15px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||