From 3cdb9f4ef8f5b5e1135d3502dc254bd28097ccff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8E=AB=E5=94=90=E5=B0=BC?= Date: Fri, 28 Aug 2026 23:41:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E5=8A=9F=E8=83=BD=E5=92=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9E=AC=E9=97=B4=E6=97=A0=E6=B3=95=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20#45=20#46?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增评论支持验证码功能 2. 修复瞬间功能无法正常显示内容 --- App.vue | 4 + api/v2/all.api.js | 55 ++- api/v2/all.config.js | 10 + common/constant/cookies.js | 2 + common/http/index.js | 4 +- common/http/interceptors.js | 274 ++++++----- components/comment-item/comment-item.vue | 3 - components/comment-modal/comment-modal.vue | 548 ++++++++++++--------- config/halo.config.js | 30 ++ store/config.js | 31 ++ utils/cookies.js | 32 ++ 11 files changed, 631 insertions(+), 362 deletions(-) create mode 100644 common/constant/cookies.js create mode 100644 config/halo.config.js create mode 100644 utils/cookies.js diff --git a/App.vue b/App.vue index b7dbcde..7710b6a 100644 --- a/App.vue +++ b/App.vue @@ -20,7 +20,11 @@ export default { // #endif uni.$tm.vx.actions('config/fetchConfigs').then((res) => { + console.log('fetchConfigs', res); }) + uni.$tm.vx.actions('config/fetchHaloConfigs').then((res) => { + console.log('fetchHaloConfigs', res); + }) // 启动检查app的配置是否已经就绪,若未就绪则设置默认的 uni.$tm.vx.actions('setting/checkAndSetDefaultAppSettings'); }, diff --git a/api/v2/all.api.js b/api/v2/all.api.js index 1cdf1d8..ab184b9 100644 --- a/api/v2/all.api.js +++ b/api/v2/all.api.js @@ -3,12 +3,14 @@ import { } from '@/utils/token.js' import HttpHandler from '@/common/http/request.js' import qs from 'qs' - +import { getCache } from "@/utils/storage"; +import { getAppConfigs } from '@/config/index.js' import { - getAppConfigs -} from '@/config/index.js' -import { getNologinEmail, getOpenid } from "@/utils/auth"; - + getNologinEmail, + getOpenid +} from "@/utils/auth"; +import { CommentWidgetCaptchaCookies } from '@/common/constant/cookies.js' + export default { getPostList: (params) => { return HttpHandler.Get(`/apis/api.content.halo.run/v1alpha1/posts`, params) @@ -52,11 +54,27 @@ export default { }, addPostComment: (data) => { - return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/comments`, data) + const captchaCode = data.captchaCode; + delete data.captchaCode; + return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/comments`, data, { + header: { + "Accept": "application/json", + "X-Captcha-Code": captchaCode, + "Cookie": getCache(CommentWidgetCaptchaCookies) ?? undefined + } + }) }, addPostCommentReply: (commentName, data) => { - return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, data) + const captchaCode = data.captchaCode; + delete data.captchaCode; + return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, data, { + header: { + "Accept": "application/json", + "X-Captcha-Code": captchaCode, + "Cookie": getCache(CommentWidgetCaptchaCookies) ?? undefined + } + }) }, getTagList: (params) => { @@ -68,19 +86,11 @@ export default { }, getMomentList: (params) => { - return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments`, params, { - custom: { - personalToken: getPersonalToken() - } - }) + return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments`, params) }, getMomentByName: (name) => { - return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {}, { - custom: { - personalToken: getPersonalToken() - } - }) + return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {}) }, getBlogStatistics: () => { @@ -176,10 +186,17 @@ export default { }, getDoubanDetail: (url) => { - return HttpHandler.Get(`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail`, { url }) + return HttpHandler.Get(`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail`, { + url + }) }, - + postTrackersCounter: (data) => { return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/trackers/counter`, data) }, + + /** 获取评论验证码 */ + getCommentWidgetCaptcha: () => { + return HttpHandler.Get(`/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate`, {}) + } } \ No newline at end of file diff --git a/api/v2/all.config.js b/api/v2/all.config.js index 15df665..2ad74e3 100644 --- a/api/v2/all.config.js +++ b/api/v2/all.config.js @@ -7,4 +7,14 @@ export default { getAppConfigs: () => { return HttpHandler.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getConfigs`, {}) }, + + /** 获取全局配置信息 */ + getHaloGlobalInfo: () => { + return HttpHandler.Get(`/actuator/globalinfo`, {}) + }, + + /** 获取评论组件配置 */ + getCommentWidgetConfig: () => { + return HttpHandler.Get(`/apis/api.commentwidget.halo.run/v1alpha1/config`, {}) + }, } diff --git a/common/constant/cookies.js b/common/constant/cookies.js new file mode 100644 index 0000000..b84ff05 --- /dev/null +++ b/common/constant/cookies.js @@ -0,0 +1,2 @@ +/** 评论验证码 */ +export const CommentWidgetCaptchaCookies = 'comment-widget-captcha'; \ No newline at end of file diff --git a/common/http/index.js b/common/http/index.js index 3624813..22f0ab2 100644 --- a/common/http/index.js +++ b/common/http/index.js @@ -16,7 +16,9 @@ import { } from "./interceptors.js"; import Request from "@/js_sdk/luch-request/luch-request"; -const http = new Request() +const http = new Request({ + withCredentials: true, // 是否携带cookie +}) /* 设置全局配置 */ http.setConfig((config) => { diff --git a/common/http/interceptors.js b/common/http/interceptors.js index 0f50ce5..7258d98 100644 --- a/common/http/interceptors.js +++ b/common/http/interceptors.js @@ -9,133 +9,169 @@ * 修改人员: * 修改时间: */ -import {delCache, setCache} from "@/utils/storage"; +import { + delCache, + setCache +} from "@/utils/storage"; +import { + CommentWidgetCaptchaCookies +} from '@/common/constant/cookies.js' +import { + extractCookieItem, + getHeaderCaseInsensitive +} from '@/utils/cookies.js' const getCategoryNameByUrl = (url) => { - const reg = '(?<=/api/content/categories/).+(?=/posts)' - return url.match(reg)[0] || '无分类名' + const reg = '(?<=/api/content/categories/).+(?=/posts)' + return url.match(reg)[0] || '无分类名' } + const showCategoryInputPasswordModal = (response, category) => { - uni.showModal({ - title: `[ ${category} ] 分类已加密`, // TODO 这里应该获取分类的名字,可以在弹窗之前请求后台拿到所有分类根据分类code拿到名称,但是不会在这之前发送请求 - content: '', - editable: true, - placeholderText: '请输入分类密码后访问', - confirmText: '验证密码', - cancelText: '暂不访问', - showCancel: true, - cancelColor: '#999999', - confirmColor: '#03a9f4', - success: (res) => { - if (res.confirm) { - // TODO 这里如果没有输入密码点击确认应该阻止窗口关闭,但是没找到方法 - if (!res.content) { - uni.showToast({ - title: '提示:请输入密码', icon: 'none', success: () => { - setTimeout(() => { - showCategoryInputPasswordModal(response, category); - }, 800) - } - }) - return; - } - // 根据请求URL正则匹配分类code,然后把输入的密码根据分类code放入缓存,然后在category.getCategoryPostList中获取,解决多个分类加密输入密码后点错的问题 - // 目前存在一个问题,比如前两个都需要密码,如果先输入第二个的密码之后,重新进来默认打开第一个还会弹窗,所以想在弹窗标题上增加分类名字 - // 另外有以下两种方式科技解决 - // TODO 1.其实这里获取到密码之后可以直接发送一个请求追加上password参数,因为后台会缓存权限,后续不输入密码也可以访问,可惜不会 - // TODO 2.另外也可以拿到密码之后,直接选中该分类追加password参数,重新请求,可惜也不会 - setCache('APP_CATEGORY_PWD_' + category, res.content) - uni.reLaunch({ - url: '/pages/tabbar/category/category' - }); - } else if (res.cancel) { - } - }, - }) + uni.showModal({ + title: `[ ${category} ] 分类已加密`, // TODO 这里应该获取分类的名字,可以在弹窗之前请求后台拿到所有分类根据分类code拿到名称,但是不会在这之前发送请求 + content: '', + editable: true, + placeholderText: '请输入分类密码后访问', + confirmText: '验证密码', + cancelText: '暂不访问', + showCancel: true, + cancelColor: '#999999', + confirmColor: '#03a9f4', + success: (res) => { + if (res.confirm) { + // TODO 这里如果没有输入密码点击确认应该阻止窗口关闭,但是没找到方法 + if (!res.content) { + uni.showToast({ + title: '提示:请输入密码', + icon: 'none', + success: () => { + setTimeout(() => { + showCategoryInputPasswordModal(response, category); + }, 800) + } + }) + return; + } + // 根据请求URL正则匹配分类code,然后把输入的密码根据分类code放入缓存,然后在category.getCategoryPostList中获取,解决多个分类加密输入密码后点错的问题 + // 目前存在一个问题,比如前两个都需要密码,如果先输入第二个的密码之后,重新进来默认打开第一个还会弹窗,所以想在弹窗标题上增加分类名字 + // 另外有以下两种方式科技解决 + // TODO 1.其实这里获取到密码之后可以直接发送一个请求追加上password参数,因为后台会缓存权限,后续不输入密码也可以访问,可惜不会 + // TODO 2.另外也可以拿到密码之后,直接选中该分类追加password参数,重新请求,可惜也不会 + setCache('APP_CATEGORY_PWD_' + category, res.content) + uni.reLaunch({ + url: '/pages/tabbar/category/category' + }); + } else if (res.cancel) {} + }, + }) +} + +const saveCookies = (response) => { + const cookies = getHeaderCaseInsensitive(response.header, 'set-cookie'); + + // app/小程序将 cookies 存储起来 + // #ifdef MP-WEIXIN || APP-PLUS + // 存储评论验证码cookies + const MatchCommentApis = [ + '/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate', + '/apis/api.halo.run/v1alpha1/comments' + ] + if (MatchCommentApis.includes(response.config.url)) { + setCache(CommentWidgetCaptchaCookies, extractCookieItem(cookies, CommentWidgetCaptchaCookies)) + } + // #endif } export const setInterceptors = (http) => { - http.interceptors.request.use((config) => { - console.log("config", config) + http.interceptors.request.use((config) => { + console.log("config", config) - // 可使用async await 做异步操作 - config.header = { - ...config.header - // ... 可以直接加参数 - }; - if (config.custom.personalToken) { - config.header['Authorization'] = `Bearer ${config.custom.personalToken}` - } - return config; - }, (config) => { - // 可使用async await 做异步操作 - return Promise.reject(config); - }); + // 可使用async await 做异步操作 + config.header = { + ...config.header + // ... 可以直接加参数 + }; + if (config.custom.personalToken) { + config.header['Authorization'] = `Bearer ${config.custom.personalToken}` + } + return config; + }, (config) => { + // 可使用async await 做异步操作 + return Promise.reject(config); + }); - http.interceptors.response.use((response) => { - /* 对响应成功做点什么 可使用async await 做异步操作*/ - // if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject() - // return Promise.reject(response) // return Promise.reject 可使promise状态进入catch - // if (response.config.custom.verification) { // 演示自定义参数的作用 - // return response.data - // } + http.interceptors.response.use((response) => { + /* 对响应成功做点什么 可使用async await 做异步操作*/ + // if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject() + // return Promise.reject(response) // return Promise.reject 可使promise状态进入catch + // if (response.config.custom.verification) { // 演示自定义参数的作用 + // return response.data + // } - if (response.statusCode == 200) { - return response.data; - } else { - return Promise.reject(response); - } - }, (response) => { - /* 对响应错误做点什么 (statusCode !== 200)*/ - if (!response.data) { - return Promise.reject({ - status: 500, message: 'API接口服务异常!' - }) - } else if (response.data.status == 401) { - uni.$tm.toast(response.data.message); - // 如果是请求分类之后报401说明密码错误,那么清除该密码,下次点击会报403弹窗再次输入密码 - if (response.config.url.indexOf('/api/content/categories') >= 0) { - const category = getCategoryNameByUrl(response.config.url) - delCache('APP_CATEGORY_PWD_' + category); - uni.showToast({ - title: '提示:密码不正确', icon: 'none', success: () => { - setTimeout(() => { - showCategoryInputPasswordModal(response, category); - }, 800) - } - }) - } else { - // 其他情况维持原来的逻辑 - delCache('APP_ADMIN_LOGIN_TOKEN'); - uni.$eShowModal({ - title: '提示', - content: '您未登录超管账号或登录已过期,是否重新登录?', - showCancel: true, - cancelText: '否', - cancelColor: '#999999', - confirmText: '是', - confirmColor: '#03a9f4' - }).then(res => { - uni.navigateTo({ - url: '/pagesB/login/login' - }) - }).catch(err => { - uni.switchTab({ - url: '/pages/tabbar/about/about' - }) - }) - } - return Promise.reject(response.data); - } else if (response.data.status == 403) { - // 如果报403是请求分类文章接口(您没有该分类的访问权限)的话说明是私密分类,需要输入密码请求 - if (response.config.url.indexOf('/api/content/categories') >= 0) { - const category = getCategoryNameByUrl(response.config.url); - showCategoryInputPasswordModal(response, category); - } - return Promise.reject(response.data); - } else { - return Promise.reject(response.data); - } + saveCookies(response); - }); -}; + if (response.statusCode == 200) { + return response.data; + } else { + return Promise.reject(response); + } + }, (response) => { + + saveCookies(response); + + /* 对响应错误做点什么 (statusCode !== 200)*/ + if (!response.data) { + return Promise.reject({ + status: 500, + message: 'API接口服务异常!' + }) + } else if (response.data.status == 401) { + uni.$tm.toast(response.data.message); + // 如果是请求分类之后报401说明密码错误,那么清除该密码,下次点击会报403弹窗再次输入密码 + if (response.config.url.indexOf('/api/content/categories') >= 0) { + const category = getCategoryNameByUrl(response.config.url) + delCache('APP_CATEGORY_PWD_' + category); + uni.showToast({ + title: '提示:密码不正确', + icon: 'none', + success: () => { + setTimeout(() => { + showCategoryInputPasswordModal(response, category); + }, 800) + } + }) + } else { + // 其他情况维持原来的逻辑 + delCache('APP_ADMIN_LOGIN_TOKEN'); + uni.$eShowModal({ + title: '提示', + content: '您未登录超管账号或登录已过期,是否重新登录?', + showCancel: true, + cancelText: '否', + cancelColor: '#999999', + confirmText: '是', + confirmColor: '#03a9f4' + }).then(res => { + uni.navigateTo({ + url: '/pagesB/login/login' + }) + }).catch(err => { + uni.switchTab({ + url: '/pages/tabbar/about/about' + }) + }) + } + return Promise.reject(response.data); + } else if (response.data.status == 403) { + // 如果报403是请求分类文章接口(您没有该分类的访问权限)的话说明是私密分类,需要输入密码请求 + if (response.config.url.indexOf('/api/content/categories') >= 0) { + const category = getCategoryNameByUrl(response.config.url); + showCategoryInputPasswordModal(response, category); + } + return Promise.reject(response.data); + } else { + return Promise.reject(response.data); + } + + }); +}; \ No newline at end of file diff --git a/components/comment-item/comment-item.vue b/components/comment-item/comment-item.vue index d550b1c..3129117 100644 --- a/components/comment-item/comment-item.vue +++ b/components/comment-item/comment-item.vue @@ -91,9 +91,6 @@ } } } - }, - created() { - console.log("comment", this.comment) } }; diff --git a/components/comment-modal/comment-modal.vue b/components/comment-modal/comment-modal.vue index 40ce546..1323892 100644 --- a/components/comment-modal/comment-modal.vue +++ b/components/comment-modal/comment-modal.vue @@ -1,233 +1,341 @@ + .title { + margin: 24rpx 0; + } + + .captch-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 200rpx; + height: 68rpx; + } + \ No newline at end of file diff --git a/config/halo.config.js b/config/halo.config.js new file mode 100644 index 0000000..a841129 --- /dev/null +++ b/config/halo.config.js @@ -0,0 +1,30 @@ +// 应用设置存储key值 +export const _HaloConfigKey = 'HALO_GLOBAL_CONFIGS'; + +// 默认的应用设置 +export const DefaultHaloGlobalConfigs = { + allowAnonymousComments: true, + allowRegistration: true, + mustVerifyEmailOnRegistration: true, + mustVerifyPhoneOnRegistration: false, + restrictRegistrationDomain: false, + shopDisabled: true, +} + +/** + * 获取应用设置 + */ +export const getHaloGlobalConfigs = () => { + let configs = uni.getStorageSync(_HaloConfigKey) + if (configs) return JSON.parse(configs) + + uni.setStorageSync(_HaloConfigKey, JSON.stringify(DefaultHaloGlobalConfigs)) + return DefaultHaloConfigs; +} + +/** + * 保存应用设置 + */ +export const setHaloGlobalConfigs = (configs) => { + uni.setStorageSync(_HaloConfigKey, JSON.stringify(configs)) +} \ No newline at end of file diff --git a/store/config.js b/store/config.js index 303f7df..2429a9c 100644 --- a/store/config.js +++ b/store/config.js @@ -6,6 +6,7 @@ * 版本:v0.1.0 */ import {DefaultAppConfigs, getAppConfigs, getAppMockJson, setAppConfigs, setAppMockJson} from '@/config/index.js' +import {DefaultHaloGlobalConfigs, getHaloGlobalConfigs, setHaloGlobalConfigs} from '@/config/halo.config.js' import v2Config from '@/api/v2/all.config.js' import utils from '@/utils/index.js' import {setTokens} from "@/utils/token"; @@ -13,10 +14,15 @@ import {setTokens} from "@/utils/token"; export default { namespaced: true, state: { + haloConfig: getHaloGlobalConfigs(), configs: getAppConfigs(), mockJson: getAppMockJson(), }, getters: { + getHaloConfig(state) { + if (state.haloConfig) return state.haloConfig; + return getHaloGlobalConfigs(); + }, getConfigs(state) { if (state.configs) return state.configs; return getAppConfigs() @@ -27,6 +33,10 @@ export default { } }, mutations: { + setHaloConfigs(state, data) { + state.haloConfig = data; + setHaloGlobalConfigs(data); + }, setConfigs(state, data) { state.configs = data; setAppConfigs(data) @@ -37,6 +47,23 @@ export default { } }, actions: { + fetchHaloConfigs({state, commit, dispatch}) { + return new Promise(async (resolve, reject) => { + try { + const res = await v2Config.getHaloGlobalInfo() + if (res) { + commit('setHaloConfigs', utils.deepMerge(DefaultHaloGlobalConfigs, res)) + resolve(res) + } else { + dispatch("setDefaultHaloGlobalConfigs"); + reject() + } + } catch (e) { + dispatch("setDefaultHaloGlobalConfigs"); + reject() + } + }) + }, fetchConfigs({state, commit, dispatch}) { return new Promise(async (resolve, reject) => { try { @@ -59,6 +86,10 @@ export default { } }) }, + // 设置默认的数据 + setDefaultHaloGlobalConfigs({commit}) { + commit('setHaloConfigs', JSON.parse(JSON.stringify(DefaultHaloGlobalConfigs))) + }, // 设置默认的数据 setDefaultAppSettings({commit}) { commit('setConfigs', JSON.parse(JSON.stringify(DefaultAppConfigs))) diff --git a/utils/cookies.js b/utils/cookies.js new file mode 100644 index 0000000..42ca9b2 --- /dev/null +++ b/utils/cookies.js @@ -0,0 +1,32 @@ +/** + * 从带换行的cookie原始串提取某一条cookie(带;结尾) + * @param {string} cookieRaw set‑cookie原始字符串 + * @param {string} cookieKey cookie名称,例如 "comment-widget-captcha" + * @returns {string} 清理换行后的cookie片段,没匹配返回'' + */ +export function extractCookieItem(cookieRaw, cookieKey) { + if (!cookieRaw || !cookieKey) return '' + + // 转义正则特殊字符 + function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + } + + const keyEscaped = escapeRegExp(cookieKey) + const reg = new RegExp(`${keyEscaped}=[\\s\\S]*?;`) + const m = cookieRaw.match(reg) + return m ? m[0].replace(/\r?\n/g, '') : '' +} + +/** + * 获取header中指定key的值,忽略大小写 + * @param {object} headers HTTP header对象 + * @param {string} name HTTP header键名 + * @returns {string} HTTP header值,没找到返回'' + */ +export function getHeaderCaseInsensitive(headers, name) { + if (!headers || typeof headers !== 'object') return ''; + const lowerName = name.toLowerCase(); + const key = Object.keys(headers).find(k => k.toLowerCase() === lowerName); + return key ? headers[key] : ''; +} \ No newline at end of file