diff --git a/dist.zip b/dist.zip index 5e8d1bf..af49cac 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/package.json b/package.json index 125bfeb..2a2fc5a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "pinia": "^2.1.0", "vue": "^3.3.0", "vue-router": "^4.2.0", - "ali-oss": "^6.21.0" + "ali-oss": "^6.21.0", + "qrcode": "^1.5.3" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", diff --git a/src/App.vue b/src/App.vue index 672cffd..f6f3825 100644 --- a/src/App.vue +++ b/src/App.vue @@ -31,22 +31,30 @@ export default { window.routerEvents = routerEvents; // 监听路由变化,检查是否需要触发登录弹窗 - onMounted(() => { - // 立即初始化登录状态,确保在路由守卫执行前已经正确恢复用户状态 - store.initializeAuth(); - - // 如果有触发登录的事件,执行它 - if (typeof routerEvents.triggerLogin === 'function') { - routerEvents.triggerLogin(); - routerEvents.triggerLogin = null; - } - - // 输出调试信息 - if (window.$debug?.logUserState) { - window.$debug.logUserState(); - } + onMounted(async () => { + try { + // 立即初始化登录状态,确保在路由守卫执行前已经正确恢复用户状态 + // 现在会自动获取最新的用户信息 + await store.initializeAuth(); + + // 如果有触发登录的事件,执行它 + if (typeof routerEvents.triggerLogin === 'function') { + routerEvents.triggerLogin(); + routerEvents.triggerLogin = null; + } + + // 输出调试信息 + if (window.$debug?.logUserState) { + window.$debug.logUserState(); + } - store.getUnreadMessages(); + // 只有在用户已登录时才获取未读消息 + if (store.isLoggedIn) { + store.getUnreadMessages(); + } + } catch (error) { + console.error('[App] 初始化失败:', error); + } }); // 每次路由变化时也检查一次 diff --git a/src/api/bookshelf.js b/src/api/bookshelf.js index c80bde8..22d546a 100644 --- a/src/api/bookshelf.js +++ b/src/api/bookshelf.js @@ -1,174 +1,211 @@ 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 } - }); - }, - - /** - * 修改我的书籍信息 - * @param {Object} bookData 书籍数据 - */ - saveOrUpdateReadBook(bookData) { - return api.post('/all_book/saveOrUpdateReadBook', null, { - params: bookData - }); - } +export const bookshelfApi = { + /** + * 获取我阅读过的书籍列表带分页 + * @param {Object} params 查询参数 + * @param {number} params.pageNo 当前页 + * @param {number} params.pageSize 显示条数 + */ + getReadBookPage(params) { + return api.get('/all_book/getReadBookPage', { params }); + }, + + /** + * 修改我的书架信息 + * @param {Object} params 书架信息 + */ + saveOrUpdateReadBook(params) { + return api.post('/all_book/saveOrUpdateReadBook', null, { params }); + }, + + /** + * 增加我的书架阅读记录 + * @param {Object} params 书架记录 + * @param {string} params.shopId 书籍ID + * @param {string} params.name 书名 + * @param {string} params.image 封面 + * @param {string} params.novelId 章节ID + */ + addReadBook(params) { + return api.post('/all_book/addReadBook', null, { params }); + }, + + /** + * 查询这本书是否加入书架 + * @param {number} bookId 书籍ID + */ + isAddBook(bookId) { + return api.get('/all_book/isAddBook', { + params: { bookId } + }); + }, + + /** + * 移除我阅读过的书籍根据书籍标识 + * @param {string} bookId 书籍ID + */ + removeReadBook(bookId) { + return api.get('/all_book/removeReadBook', { + params: { bookId } + }); + }, + + /** + * 批量移除我阅读过的数据根据书籍标识 + * @param {string} bookIds 书籍ID列表,逗号分隔 + */ + batchRemoveReadBook(bookIds) { + return api.get('/all_book/batchRemoveReadBook', { + params: { bookIds } + }); + }, + + /** + * 根据书籍id删除书架 + * @param {number} bookId 书籍ID + */ + deleteBookshelfByBookId(bookId) { + return api.post('/all_book/deleteBookshelfByBookId', null, { + params: { bookId } + }); + }, + + /** + * 根据书籍id获取当前阅读章节 + * @param {number} bookId 书籍ID + */ + getReadChapterByBookId(bookId) { + return api.get('/all_book/getReadChapterByBookId', { + 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} params 查询参数 - * @param {string} params.bookId 书籍ID - * @param {string} params.novelId 章节ID - */ - getMyShopNovel(params) { - return api.get('/my_book/getMyShopNovel', { params }); - }, - - /** - * 添加作品或者修改作品 - * @param {Object} shopData 作品数据 - */ - saveOrUpdateShop(shopData) { - return api.post('/my_book/saveOrUpdateShop', null, { - params: shopData - }); - }, - - /** - * 增加或修改作品章节 - * @param {Object} novelData 章节数据 - */ - saveOrUpdateShopNovel(data) { - return api.post('/my_book/saveOrUpdateShopNovel', data); - } + /** + * 删除作品章节 + * @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} params 查询参数 + * @param {string} params.bookId 书籍ID + * @param {string} params.novelId 章节ID + */ + getMyShopNovel(params) { + return api.get('/my_book/getMyShopNovel', { params }); + }, + + /** + * 添加作品或者修改作品 + * @param {Object} shopData 作品数据 + */ + saveOrUpdateShop(shopData) { + return api.post('/my_book/saveOrUpdateShop', null, { + params: shopData + }); + }, + + /** + * 增加或修改作品章节 + * @param {Object} novelData 章节数据 + */ + saveOrUpdateShopNovel(data) { + return api.post('/my_book/saveOrUpdateShopNovel', data); + } }; /** * 书架-读者成就相关接口 */ 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 - }); - } + /** + * 根据用户标识和书籍标识查询该用户的成就等级 + * @param {string} bookId 书籍ID + */ + getAchievementByBookId(bookId) { + return api.get('/all_achievement/getAchievementByBookId', { + params: { bookId } + }); + }, + /** + * 获取作者设置的成就等级对象 + * @param {string} bookId 书籍ID + */ + getAchievement() { + return api.get('/all_achievement/getAchievement'); + }, + + /** + * 获取读者成就列表 + */ + getAchievementList() { + return api.get('/all_achievement/getAchievementList'); + }, + + /** + * 设置读者成就等级名称 + * @param {Object} achievementData 成就数据 + */ + setAchievementName(achievementData) { + return api.post('/all_achievement/setAchievementName', null, { + params: achievementData + }); + } }; \ No newline at end of file diff --git a/src/api/index.js b/src/api/index.js index 6b8ded6..6b6745d 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,10 +1,10 @@ import axios from 'axios'; const api = axios.create({ - // baseURL: 'http://127.0.0.1:8003/novel-admin', + // baseURL: 'http://127.0.0.1:8002/novel-admin', baseURL: 'https://prod-api.budingxiaoshuo.com/novel-admin', // baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1', - timeout: 10000, + timeout: 50000, headers: { 'Content-Type' : 'application/x-www-form-urlencoded' } diff --git a/src/api/modules.js b/src/api/modules.js index d399c30..83a0c54 100644 --- a/src/api/modules.js +++ b/src/api/modules.js @@ -1,14 +1,14 @@ // 统一导出所有API模块 export { authApi } from './auth.js'; export { homeApi } from './home.js'; -export { readBookApi, myBookApi, achievementApi } from './bookshelf.js'; +export { bookshelfApi, myBookApi, achievementApi } from './bookshelf.js'; export { moneyApi, commentApi, taskApi, writerApi, orderApi } from './user.js'; export { smsApi } from './sms.js'; // 默认导出,方便直接使用 import { authApi } from './auth.js'; import { homeApi } from './home.js'; -import { readBookApi, myBookApi, achievementApi } from './bookshelf.js'; +import { bookshelfApi, myBookApi, achievementApi } from './bookshelf.js'; import { moneyApi, commentApi, taskApi, writerApi, orderApi } from './user.js'; import { smsApi } from './sms.js'; @@ -20,7 +20,7 @@ export default { home: homeApi, // 书架相关 - readBook: readBookApi, + bookshelf: bookshelfApi, myBook: myBookApi, achievement: achievementApi, diff --git a/src/api/user.js b/src/api/user.js index a6c4d5d..a1f64eb 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -297,9 +297,9 @@ export const orderApi = { * 创建支付套餐订单 * @param {string} packageId 套餐ID */ - createPayPackageOrder(packageId) { + createPayPackageOrder(params) { return api.post('/my_order/createPayPackageOrder', null, { - params: { packageId } + params }); }, @@ -320,4 +320,40 @@ export const orderApi = { giveGift(params) { return api.post('/my_order/giveGift', null, { params }); } +}; + +/** + * 微信支付相关接口 + */ +export const wechatPayApi = { + /** + * 创建微信支付订单 + * @param {Object} params 支付参数 + * @param {string} params.orderId 订单ID + * @param {number} params.totalFee 支付金额(分) + * @param {string} params.body 商品描述 + */ + createWechatPayOrder(params) { + return api.post('/wechat_pay/createOrder', null, { params }); + }, + + /** + * 查询微信支付订单状态 + * @param {string} outTradeNo 商户订单号 + */ + queryWechatPayStatus(outTradeNo) { + return api.get('/wechat_pay/queryOrder', { + params: { outTradeNo } + }); + }, + + /** + * 取消微信支付订单 + * @param {string} outTradeNo 商户订单号 + */ + cancelWechatPayOrder(outTradeNo) { + return api.post('/wechat_pay/cancelOrder', null, { + params: { outTradeNo } + }); + } }; \ No newline at end of file diff --git a/src/components/auth/AuthorApplicationModal.vue b/src/components/auth/AuthorApplicationModal.vue index 32c9fa7..5ffb905 100644 --- a/src/components/auth/AuthorApplicationModal.vue +++ b/src/components/auth/AuthorApplicationModal.vue @@ -2,7 +2,7 @@ - 成为创作者 + {{ isEdit ? '保存修改' : '成为创作者' }} + + \ No newline at end of file diff --git a/src/components/common/PhoneModifyModal.vue b/src/components/common/PhoneModifyModal.vue new file mode 100644 index 0000000..947f0a3 --- /dev/null +++ b/src/components/common/PhoneModifyModal.vue @@ -0,0 +1,341 @@ + + + + + + + {{ maskPhoneNumber(currentPhone) }} + + + + + + + +86 + + + + + + + + + + {{ countdown > 0 ? `${countdown}s后重发` : '发送验证码' }} + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/common/WechatPayment.md b/src/components/common/WechatPayment.md new file mode 100644 index 0000000..df1c505 --- /dev/null +++ b/src/components/common/WechatPayment.md @@ -0,0 +1,244 @@ +# 微信二维码支付组件 (WechatPayment) + +基于参考博客 [H5微信支付、PC端微信支付](https://blog.csdn.net/WANGLING0108/article/details/133808856) 实现的Vue3微信支付组件。 + +## 功能特性 + +- ✅ 二维码自动生成和展示 +- ✅ 支付状态实时轮询 +- ✅ 支付超时处理(5分钟倒计时) +- ✅ 支付成功/失败/取消状态处理 +- ✅ 二维码刷新功能 +- ✅ 响应式设计(PC端/移动端适配) +- ✅ 完整的错误处理和用户提示 + +## 安装依赖 + +```bash +npm install qrcode +``` + +## 使用方式 + +### 基础用法 + +```vue + + + + + 发起支付 + + + + + + + + +``` + +## 组件属性 (Props) + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `modelValue` | Boolean | `false` | 控制支付弹窗显示/隐藏 | +| `paymentData` | Object | `{}` | 支付相关数据 | + +### paymentData 对象结构 + +```javascript +{ + orderId: '', // 订单ID(必填) + title: '', // 支付标题(必填) + amount: 0, // 支付金额,单位:分(必填) + body: '' // 商品描述(可选) +} +``` + +## 组件事件 (Events) + +| 事件名 | 参数 | 说明 | +|--------|------|------| +| `update:modelValue` | `Boolean` | 更新弹窗显示状态 | +| `success` | `String` (outTradeNo) | 支付成功回调,返回微信交易号 | +| `cancel` | - | 用户取消支付回调 | +| `failed` | `String` (errorMessage) | 支付失败回调,返回错误信息 | + +## API接口要求 + +组件需要后端提供以下API接口: + +### 1. 创建微信支付订单 +```javascript +// POST /wechat_pay/createOrder +// 请求参数 +{ + orderId: 'ORDER_123456', // 订单ID + totalFee: 1000, // 支付金额(分) + body: '商品描述' // 商品描述 +} + +// 响应格式 +{ + code: 200, + result: { + codeUrl: 'weixin://wxpay/bizpayurl?pr=xxx', // 支付二维码URL + outTradeNo: 'WX_ORDER_123456' // 微信订单号 + }, + message: 'success' +} +``` + +### 2. 查询支付状态 +```javascript +// GET /wechat_pay/queryOrder?outTradeNo=WX_ORDER_123456 +// 响应格式 +{ + code: 200, + result: { + resultCode: 'SUCCESS', // 业务结果 + returnCode: 'SUCCESS', // 通信标识 + tradeState: 'SUCCESS' // 交易状态:SUCCESS-支付成功,CLOSED-已关闭,REVOKED-已撤销 + }, + message: 'success' +} +``` + +### 3. 取消支付订单(可选) +```javascript +// POST /wechat_pay/cancelOrder +// 请求参数 +{ + outTradeNo: 'WX_ORDER_123456' // 微信订单号 +} +``` + +## 支付流程 + +1. 用户点击支付按钮 +2. 调用创建订单API获取支付二维码 +3. 显示二维码供用户扫码 +4. 开始轮询查询支付状态(每2秒一次) +5. 用户完成支付后,组件检测到支付成功 +6. 触发成功回调,关闭支付弹窗 + +## 状态说明 + +组件内部维护以下支付状态: + +- `loading`: 正在生成支付二维码 +- `pending`: 等待用户扫码支付 +- `success`: 支付成功 +- `failed`: 支付失败 +- `timeout`: 支付超时(5分钟后自动超时) + +## 样式定制 + +组件使用了项目的主色调 `#0A2463`,如需自定义样式,可以通过CSS变量或深度选择器进行修改: + +```scss +// 自定义主色调 +.wechat-payment { + --primary-color: #your-color; +} + +// 深度选择器修改样式 +:deep(.wechat-payment) { + .payment-amount { + color: #your-color; + } +} +``` + +## 注意事项 + +1. **金额单位**:组件内部金额以分为单位,显示时会自动转换为元 +2. **轮询频率**:默认每2秒轮询一次支付状态,可根据需要调整 +3. **超时时间**:默认5分钟超时,可在组件内部修改 `countdown` 初始值 +4. **错误处理**:建议在业务层面添加网络异常处理 +5. **移动端适配**:组件已适配移动端,二维码会自动缩小 + +## 完整示例 + +参考 `src/views/user/PaymentExample.vue` 文件查看完整的使用示例。 + +## 技术栈 + +- Vue 3 + Composition API +- Element Plus UI框架 +- QRCode.js 二维码生成库 +- SCSS样式预处理 + +## 浏览器兼容性 + +- Chrome 70+ +- Firefox 63+ +- Safari 12+ +- Edge 79+ + +## 参考资料 + +- [微信支付官方文档](https://pay.weixin.qq.com/wiki/doc/api/index.html) +- [H5微信支付、PC端微信支付 - CSDN博客](https://blog.csdn.net/WANGLING0108/article/details/133808856) \ No newline at end of file diff --git a/src/components/common/WechatPayment.vue b/src/components/common/WechatPayment.vue new file mode 100644 index 0000000..faef0fa --- /dev/null +++ b/src/components/common/WechatPayment.vue @@ -0,0 +1,604 @@ + + + + + + + + {{ paymentData.title }} + + ¥{{ formatAmount(paymentData.amount) }} + + + + + + + + + + + + + 请使用微信扫码支付 + 扫码后请在手机上完成支付 + + + + + 支付剩余时间: + {{ formatTime(countdown) }} + + + + + + + 正在生成支付二维码... + + + + + + + 支付成功! + 感谢您的支付,页面即将跳转... + + + + + + + 支付失败 + {{ errorMessage || '支付过程中出现异常,请重试' }} + + + + + + + 支付超时 + 二维码已过期,请重新发起支付 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/layout/layout/Header.vue b/src/layout/layout/Header.vue index 200b560..ffa6df0 100644 --- a/src/layout/layout/Header.vue +++ b/src/layout/layout/Header.vue @@ -108,6 +108,12 @@ + + + @@ -118,6 +124,7 @@ import { useMainStore } from '@/store'; import { ArrowDown, ArrowRight, Bell, Reading, List, Star, Wallet, SwitchButton } from '@element-plus/icons-vue'; import { AUTH_INJECTION_KEY } from '@/components/auth/AuthProvider.vue'; import { AUTHOR_APPLICATION_INJECTION_KEY } from '@/components/auth/AuthorApplicationProvider.vue'; +import PersonalInfoModal from '@/components/common/PersonalInfoModal.vue'; export default { name: 'AppHeader', @@ -130,6 +137,7 @@ export default { Star, Wallet, SwitchButton, + PersonalInfoModal, GiftBox: { render() { return h('svg', { @@ -150,6 +158,7 @@ export default { const route = useRoute(); const store = useMainStore(); const searchKeyword = ref(''); + const showPersonalInfoModal = ref(false); // 注入身份验证上下文 const authContext = inject(AUTH_INJECTION_KEY); @@ -193,7 +202,13 @@ export default { }; const goToUserCenter = () => { - router.push('/user/center'); + showPersonalInfoModal.value = true; + }; + + // 处理个人信息更新成功 + const handlePersonalInfoUpdate = () => { + // 刷新用户信息 + store.fetchUserInfo(); }; const goToBookshelf = () => { @@ -340,9 +355,11 @@ export default { activeMenu, searchKeyword, navigationCategories, + showPersonalInfoModal, handleSearch, goToLogin, goToUserCenter, + handlePersonalInfoUpdate, goToBookshelf, goToGiftBox, goToTaskCenter, diff --git a/src/main.js b/src/main.js index a7263ab..c4af2f6 100644 --- a/src/main.js +++ b/src/main.js @@ -17,9 +17,11 @@ const pinia = createPinia(); app.use(ElementPlus); app.use(pinia); -// 先初始化身份验证状态 +// 先初始化身份验证状态(异步执行,不阻塞应用启动) const store = useMainStore(); -store.initializeAuth(); +store.initializeAuth().catch(err => { + console.error('[Main] 初始化登录状态失败:', err); +}); // 全局挂载pinia,用于路由守卫 window.$pinia = pinia; diff --git a/src/router/index.js b/src/router/index.js index 9031466..0ae678c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -157,9 +157,10 @@ router.beforeEach((to, from, next) => { meta: to.meta }); - // 首先尝试从localStorage获取登录和作家状态 - const token = localStorage.getItem('token'); - const isLoggedIn = !!token; + // 首先尝试从localStorage获取登录和作家状态(统一使用X-Access-Token) + const token = localStorage.getItem('X-Access-Token') || localStorage.getItem('token'); + const userData = localStorage.getItem('user'); + const isLoggedIn = !!(token && userData); const isAuthor = localStorage.getItem('isAuthor') === 'true'; // 输出当前状态 diff --git a/src/store/index.js b/src/store/index.js index 591f511..71d5bdf 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -15,12 +15,28 @@ export const useMainStore = defineStore('main', { }), getters: { - isAuthenticated: (state) => state.isLoggedIn && state.user !== null, + // 计算属性:是否已认证(统一检查logic) + isAuthenticated: (state) => { + const hasToken = !!state.token || !!localStorage.getItem('X-Access-Token'); + const hasUser = !!state.user || !!localStorage.getItem('user'); + return state.isLoggedIn && hasToken && hasUser; + }, + unreadMessageCount: (state) => state.unreadMessages, // 添加获取未读消息数量的getter + // 获取前几个分类用于导航栏显示 navigationCategories: (state) => state.categories.slice(0, 3), + // 获取所有分类 - allCategories: (state) => state.categories + allCategories: (state) => state.categories, + + // 计算属性:当前token(优先使用state中的,否则从localStorage获取) + currentToken: (state) => state.token || localStorage.getItem('X-Access-Token'), + + // 计算属性:是否为作家(统一检查logic) + isCurrentAuthor: (state) => { + return state.isAuthor || localStorage.getItem('isAuthor') === 'true'; + } }, actions: { @@ -214,11 +230,11 @@ export const useMainStore = defineStore('main', { }, // 初始化,从本地存储恢复登录状态 - initializeAuth() { - // 先直接从localStorage中获取各个状态 + async initializeAuth() { + // 统一使用X-Access-Token作为主要token存储 const userData = localStorage.getItem('user'); - const token = localStorage.getItem('token'); - const isAuthor = localStorage.getItem('isAuthor') == 'true'; + const token = localStorage.getItem('X-Access-Token') || localStorage.getItem('token'); + const isAuthor = localStorage.getItem('isAuthor') === 'true'; // 恢复用户数据和登录状态 if (userData && token) { @@ -227,6 +243,11 @@ export const useMainStore = defineStore('main', { this.token = token; this.isLoggedIn = true; + // 确保X-Access-Token存在(迁移兼容) + if (!localStorage.getItem('X-Access-Token')) { + localStorage.setItem('X-Access-Token', token); + } + // 明确设置作家状态,以localStorage为准 this.isAuthor = isAuthor; @@ -235,16 +256,55 @@ export const useMainStore = defineStore('main', { this.user.isAuthor = isAuthor; } - // 输出调试信息 + console.log('[Store] 本地登录状态已恢复,正在获取最新用户信息...'); + + // 从后端获取最新的用户信息 + try { + await this.fetchLatestUserInfo(); + console.log('[Store] 用户信息已更新为最新版本'); + } catch (error) { + console.warn('[Store] 获取最新用户信息失败:', error); + // 如果是认证错误,清除登录状态 + if (error.message.includes('401') || error.message.includes('未授权')) { + console.log('[Store] Token已过期,清除登录状态'); + this.clearAuthStatus(); + return; + } + // 其他错误不影响已恢复的登录状态,继续使用本地缓存的用户信息 + console.log('[Store] 使用本地缓存的用户信息'); + } + + // 输出最终的调试信息 console.log('[Store] Auth initialized:', { isLoggedIn: this.isLoggedIn, isAuthor: this.isAuthor, - user: this.user + user: this.user, + token: token ? '已设置' : '未设置' }); } catch (e) { console.error('[Store] Error parsing user data:', e); - this.logout(); + this.clearAuthStatus(); } + } else { + console.log('[Store] 无有效登录状态,保持未登录状态'); + this.clearAuthStatus(); + } + }, + + // 获取最新用户信息(内部方法) + async fetchLatestUserInfo() { + try { + const res = await authApi.getUserByToken(); + if (res.success && res.result) { + // 更新用户信息但保持现有的登录状态 + this.handleUserInfoUpdate(res.result); + return res.result; + } else { + throw new Error(res.message || '获取用户信息失败'); + } + } catch (error) { + console.error('[Store] 获取最新用户信息失败:', error); + throw error; } }, @@ -260,11 +320,27 @@ export const useMainStore = defineStore('main', { // 设置未读消息数量 async getUnreadMessages() { - const res = await commentApi.getMyCommentNum(); - if (res.success) { - this.unreadMessages = res.result; + // 只有在已登录状态下才获取未读消息 + if (!this.isLoggedIn || !this.token) { + this.unreadMessages = 0; + return 0; } - return res.result; + + try { + const res = await commentApi.getMyCommentNum(); + if (res.success) { + this.unreadMessages = res.result; + return res.result; + } + } catch (error) { + console.error('[Store] 获取未读消息失败:', error); + // 如果是401错误,说明token已过期,不需要显示错误 + if (!error.message.includes('401') && !error.message.includes('未授权')) { + // 只有非认证错误才重置未读消息数 + this.unreadMessages = 0; + } + } + return 0; }, // 清除未读消息数量 @@ -310,6 +386,34 @@ export const useMainStore = defineStore('main', { this.handleUserInfoUpdate(res.result); } return res.result; + }, + + // 更新用户信息 + async updateUserInfo(updateData) { + try { + // 这里应该调用更新用户信息的API + const res = await authApi.updateUserInfo(updateData); + + // 更新本地用户信息 + if (this.user) { + this.user = { ...this.user, ...updateData }; + localStorage.setItem('user', JSON.stringify(this.user)); + } + + return { success: true }; + } catch (error) { + console.error('更新用户信息失败:', error); + throw error; + } + }, + + // 刷新用户信息 + async fetchUserInfo() { + try { + await this.getUserInfo(); + } catch (error) { + console.error('获取用户信息失败:', error); + } } } }); \ No newline at end of file diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..efb0d91 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,216 @@ +/** + * 登录状态检查和管理工具 + */ + +/** + * 检查当前登录状态 + * @returns {Object} 登录状态信息 + */ +export const checkLoginStatus = () => { + const token = localStorage.getItem('X-Access-Token'); + const userData = localStorage.getItem('user'); + const isAuthor = localStorage.getItem('isAuthor') === 'true'; + + try { + const user = userData ? JSON.parse(userData) : null; + return { + isLoggedIn: !!(token && user), + token, + user, + isAuthor + }; + } catch (error) { + console.warn('[Auth Utils] 解析用户数据失败:', error); + return { + isLoggedIn: false, + token: null, + user: null, + isAuthor: false + }; + } +}; + +/** + * 刷新用户信息(从后端获取最新数据) + * @returns {Promise} 最新的用户信息 + */ +export const refreshUserInfo = async () => { + const { isLoggedIn } = checkLoginStatus(); + + if (!isLoggedIn) { + throw new Error('用户未登录'); + } + + const store = window.$store; + if (store && typeof store.fetchLatestUserInfo === 'function') { + try { + const userInfo = await store.fetchLatestUserInfo(); + return userInfo; + } catch (error) { + console.error('[Auth Utils] 刷新用户信息失败:', error); + throw error; + } + } else { + throw new Error('Store不可用'); + } +}; + +/** + * 检查是否已登录,如果未登录则触发登录弹窗 + * @param {Function} callback 登录成功后的回调函数 + * @returns {Boolean} 是否已登录 + */ +export const requireLogin = (callback) => { + const { isLoggedIn } = checkLoginStatus(); + + if (isLoggedIn) { + callback && callback(); + return true; + } + + // 未登录,触发登录弹窗 + triggerLogin(callback); + return false; +}; + +/** + * 检查是否为作家身份,如果不是则触发相应弹窗 + * @param {Function} callback 申请成功后的回调函数 + * @returns {Boolean} 是否为作家 + */ +export const requireAuthor = (callback) => { + const { isLoggedIn, isAuthor } = checkLoginStatus(); + + if (!isLoggedIn) { + // 未登录,先登录再申请作家 + triggerLogin(() => { + triggerAuthorApplication(callback); + }); + return false; + } + + if (isAuthor) { + return true; + } + + // 已登录但不是作家,触发作家申请 + triggerAuthorApplication(callback); + return false; +}; + +/** + * 触发登录弹窗 + * @param {Function} callback 登录成功后的回调函数 + */ +export const triggerLogin = (callback) => { + const authContext = window.$authContext; + + if (authContext && typeof authContext.openLogin === 'function') { + authContext.openLogin(callback); + } else { + // 如果authContext还未挂载,则设置事件供之后触发 + const triggerLoginEvent = () => { + const context = window.$authContext; + if (context && typeof context.openLogin === 'function') { + context.openLogin(callback); + } + }; + + if (window.routerEvents) { + window.routerEvents.triggerLogin = triggerLoginEvent; + } else { + console.warn('[Auth Utils] 无法触发登录弹窗,routerEvents不可用'); + } + } +}; + +/** + * 触发作家申请弹窗 + * @param {Function} callback 申请成功后的回调函数 + */ +export const triggerAuthorApplication = (callback) => { + const authorContext = window.$authorApplicationContext; + + if (authorContext && typeof authorContext.openApplicationModal === 'function') { + authorContext.openApplicationModal(callback); + } else { + console.warn('[Auth Utils] 无法触发作家申请弹窗,authorApplicationContext不可用'); + } +}; + +/** + * 清除登录状态(登出) + */ +export const clearLoginStatus = () => { + // 清除本地存储 + localStorage.removeItem('X-Access-Token'); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + localStorage.removeItem('isAuthor'); + localStorage.removeItem('bookshelf'); + + console.log('[Auth Utils] 登录状态已清除'); + + // 如果有store,也清除store状态 + if (window.$store) { + window.$store.clearAuthStatus(); + } +}; + +/** + * 页面按钮点击前的登录检查装饰器 + * @param {Function} fn 原始点击处理函数 + * @param {Object} options 配置选项 + * @param {Boolean} options.requireAuth 是否需要登录 + * @param {Boolean} options.requireAuthor 是否需要作家身份 + * @returns {Function} 包装后的点击处理函数 + */ +export const withAuthCheck = (fn, options = {}) => { + return (...args) => { + const { requireAuth = false, requireAuthor = false } = options; + + if (requireAuthor) { + // 需要作家身份 + if (requireAuthor(() => fn(...args))) { + fn(...args); + } + } else if (requireAuth) { + // 需要登录 + if (requireLogin(() => fn(...args))) { + fn(...args); + } + } else { + // 不需要任何权限 + fn(...args); + } + }; +}; + +/** + * Vue组合式API版本的登录检查hook + * @returns {Object} 登录检查相关的方法 + */ +export const useAuthCheck = () => { + return { + checkLoginStatus, + requireLogin, + requireAuthor, + triggerLogin, + triggerAuthorApplication, + clearLoginStatus, + withAuthCheck, + refreshUserInfo + }; +}; + +export default { + checkLoginStatus, + requireLogin, + requireAuthor, + triggerLogin, + triggerAuthorApplication, + clearLoginStatus, + withAuthCheck, + useAuthCheck, + refreshUserInfo +}; \ No newline at end of file diff --git a/src/utils/oss.js b/src/utils/oss.js index 24a899c..31f394a 100644 --- a/src/utils/oss.js +++ b/src/utils/oss.js @@ -3,7 +3,12 @@ import { ossConfig } from '@/config/oss'; class OssService { constructor() { - this.client = new OSS(ossConfig.config); + this.client = new OSS({ + ...ossConfig.config, + secure: true, // 使用HTTPS + // 可以通过设置这个选项来减少警告 + internal: false + }); } /** diff --git a/src/views/book/chapter.vue b/src/views/book/chapter.vue index 04c5cdd..fa4e407 100644 --- a/src/views/book/chapter.vue +++ b/src/views/book/chapter.vue @@ -116,7 +116,7 @@ import { useRoute, useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, MoonNight, ArrowUp, ArrowDown, Reading, Grid } from '@element-plus/icons-vue'; import { homeApi } from '@/api/home.js'; -import { readBookApi } from '@/api/bookshelf.js'; +import { bookshelfApi } from '@/api/bookshelf.js'; import CatalogDialog from '@/components/book/CatalogDialog.vue'; export default { @@ -235,7 +235,7 @@ export default { const token = localStorage.getItem('token'); if (!token || !bookId.value || !chapterId.value || !bookDetail.value) return; - const response = await readBookApi.saveOrUpdateReadBook({ + const response = await bookshelfApi.saveOrUpdateReadBook({ shopId: bookId.value, // 书籍id novelId: chapterId.value, // 章节id name: bookDetail.value.name, @@ -285,12 +285,12 @@ export default { if (isInBookshelf.value) { // 移出书架 - await readBookApi.removeReadBook(bookId.value); + await bookshelfApi.removeReadBook(bookId.value); isInBookshelf.value = false; ElMessage.success('已移出书架'); } else { // 添加到书架 - 使用saveOrUpdateReadBook - await readBookApi.saveOrUpdateReadBook({ + await bookshelfApi.saveOrUpdateReadBook({ shopId: bookId.value, novelId: chapterId.value, name: bookDetail.value?.name || '', diff --git a/src/views/book/index.vue b/src/views/book/index.vue index 553418e..e251263 100644 --- a/src/views/book/index.vue +++ b/src/views/book/index.vue @@ -7,93 +7,105 @@ - - - - - - - - {{ book.title || book.name }} - - - 作者: - {{ book.author }} - - - {{ book.status }} - · - 大家都在读 - + + + + + - - - - 我的等级 + + {{ book.title || book.name }} + + + 作者: + {{ book.author }} + + + {{ book.status }} + · + 大家都在读 + - - + + + + 我的等级 + + + + + + + {{ userInfo.name }} + · + {{ userInfo.role }} + + {{ userInfo.intimacy }} 累计亲密值 - - - 小巴 - · - VIP会员 - - 1000 累计亲密值 - - - - 互动打赏 - - - {{ isInShelf ? '已加入书架' : '加入书架' }} - - - 点击阅读 + + + 互动打赏 + + + {{ isInShelf ? '已加入书架' : '加入书架' }} + + + + + {{ readButtonInfo.text }} + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - + + \ No newline at end of file diff --git a/src/views/user/Recharge.vue b/src/views/user/Recharge.vue index dcbfd8a..36bbde1 100644 --- a/src/views/user/Recharge.vue +++ b/src/views/user/Recharge.vue @@ -1,67 +1,84 @@ - - - 当前余额 - - {{ store.user.integral || 0 }} - 豆豆 + + + + 账户充值 + 选择充值套餐,获得更多豆豆 - - - - 选择充值套餐 - - - {{ item.num }}豆豆 - ¥{{ item.money }} - 送{{ item.giveNum }}豆豆 + + + 当前余额 + + {{ store.user.integral || 0 }} + 豆豆 - - - - 订单信息 - - 充值金额: - ¥{{ totalPrice.toFixed(2) }} - - - 获得豆豆: - {{ totalBeans }}豆豆 + + + 选择充值套餐 + + + {{ item.num }}豆豆 + ¥{{ item.money }} + 送{{ item.giveNum }}豆豆 + + - - - 合计: - - ¥{{ totalPrice.toFixed(2) }} + + + + 订单信息 + + 充值金额: + ¥{{ totalPrice.toFixed(2) }} + + + 获得豆豆: + {{ totalBeans }}豆豆 + + + + 合计: + + ¥{{ totalPrice.toFixed(2) }} + - - - - 请仔细核查并确认相关信息。因用户个人疏忽导致的充值错误,需由用户自行承担。一旦完成充值,概不退换。 - + + + 请仔细核查并确认相关信息。因用户个人疏忽导致的充值错误,需由用户自行承担。一旦完成充值,概不退换。 + - - + + + @@ -71,12 +88,23 @@ import { useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import { useMainStore } from '@/store'; import { orderApi } from '@/api/user'; +import WechatPayment from '@/components/common/WechatPayment.vue'; const router = useRouter(); const store = useMainStore(); const selectedPackage = ref(null); const rechargePackages = ref([]); +// 支付相关状态 +const showPaymentModal = ref(false); +const currentOrderId = ref(''); +const paymentData = computed(() => ({ + orderId: currentOrderId.value, + title: `充值${totalBeans.value}豆豆`, + amount: Math.round(totalPrice.value * 100), // 转换为分 + body: `充值套餐 - ${totalBeans.value}豆豆` +})); + // 计算总价 const totalPrice = computed(() => { if (selectedPackage.value !== null) { @@ -121,32 +149,64 @@ const handleRecharge = async () => { try { // 创建订单 - const result = await orderApi.createPayPackageOrder( - rechargePackages.value[selectedPackage.value].id - ); - - // 调用支付 - await orderApi.payOrder({ - orderId: result.result, - token: store.token + const result = await orderApi.createPayPackageOrder({ + packageId : rechargePackages.value[selectedPackage.value].id, + payType : 'web' }); - // 支付成功 - await orderApi.paySuccess(result.result); + // 保存订单ID并显示支付弹窗 + currentOrderId.value = result.result; + showPaymentModal.value = true; + + } catch (error) { + console.error('创建订单失败:', error); + ElMessage.error('创建订单失败,请重试'); + } +}; +// 支付成功回调 +const handlePaymentSuccess = async (outTradeNo) => { + try { + // 调用支付成功接口 + await orderApi.paySuccess(currentOrderId.value); + ElMessage.success(`充值成功,获得${totalBeans.value}豆豆`); // 更新用户信息 await store.getUserInfo(); - // 返回上一页 - router.back(); + // 关闭支付弹窗 + showPaymentModal.value = false; + + // 重置状态 + selectedPackage.value = null; + currentOrderId.value = ''; + + // 延迟跳转,让用户看到成功消息 + setTimeout(() => { + router.back(); + }, 2000); + } catch (error) { - console.error('充值失败:', error); - ElMessage.error('充值失败,请重试'); + console.error('支付确认失败:', error); + ElMessage.error('支付确认失败,请联系客服'); } }; +// 支付取消回调 +const handlePaymentCancel = () => { + showPaymentModal.value = false; + currentOrderId.value = ''; + ElMessage.info('已取消支付'); +}; + +// 支付失败回调 +const handlePaymentFailed = (errorMessage) => { + showPaymentModal.value = false; + currentOrderId.value = ''; + ElMessage.error(errorMessage || '支付失败,请重试'); +}; + onMounted(() => { getPayPackageList(); }); @@ -155,34 +215,60 @@ onMounted(() => { \ No newline at end of file diff --git a/src/views/user/a b/src/views/user/a deleted file mode 100644 index 19124ee..0000000 --- a/src/views/user/a +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - - - - - - - - - - 未读评论·{{ unreadComments.length }} - - - - - 历史评论 - - - - - - - - - - - - - - - - - - {{ item.hanHaiMember.nickName }} - 来自《{{ item.commonShop.name }}》 - - {{ item.comment }} - - {{ item.createTime }} - - - - 删除 - - - - 回复 - - - - - - - - - - - \ No newline at end of file
请使用微信扫码支付
扫码后请在手机上完成支付
正在生成支付二维码...
支付成功!
感谢您的支付,页面即将跳转...
支付失败
{{ errorMessage || '支付过程中出现异常,请重试' }}
支付超时
二维码已过期,请重新发起支付
选择充值套餐,获得更多豆豆