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> | |||||