1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-02 09:20:44 +08:00

feat: 新增评论验证码功能和修复瞬间无法显示问题 #45 #46

1. 新增评论支持验证码功能
2. 修复瞬间功能无法正常显示内容
This commit is contained in:
小莫唐尼
2026-08-28 23:41:22 +08:00
parent 1407b58024
commit 3cdb9f4ef8
11 changed files with 631 additions and 362 deletions
+4
View File
@@ -20,6 +20,10 @@ 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');
+34 -17
View File
@@ -3,11 +3,13 @@ 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) => {
@@ -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`, {})
}
}
+10
View File
@@ -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`, {})
},
}
+2
View File
@@ -0,0 +1,2 @@
/** 评论验证码 */
export const CommentWidgetCaptchaCookies = 'comment-widget-captcha';
+3 -1
View File
@@ -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) => {
+42 -6
View File
@@ -9,12 +9,23 @@
* 修改人员:
* 修改时间:
*/
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 showCategoryInputPasswordModal = (response, category) => {
uni.showModal({
title: `[ ${category} ] 分类已加密`, // TODO 这里应该获取分类的名字,可以在弹窗之前请求后台拿到所有分类根据分类code拿到名称,但是不会在这之前发送请求
@@ -31,7 +42,9 @@ const showCategoryInputPasswordModal = (response, category) => {
// TODO 这里如果没有输入密码点击确认应该阻止窗口关闭,但是没找到方法
if (!res.content) {
uni.showToast({
title: '提示:请输入密码', icon: 'none', success: () => {
title: '提示:请输入密码',
icon: 'none',
success: () => {
setTimeout(() => {
showCategoryInputPasswordModal(response, category);
}, 800)
@@ -48,12 +61,27 @@ const showCategoryInputPasswordModal = (response, category) => {
uni.reLaunch({
url: '/pages/tabbar/category/category'
});
} else if (res.cancel) {
}
} 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)
@@ -80,16 +108,22 @@ export const setInterceptors = (http) => {
// return 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接口服务异常!'
status: 500,
message: 'API接口服务异常!'
})
} else if (response.data.status == 401) {
uni.$tm.toast(response.data.message);
@@ -98,7 +132,9 @@ export const setInterceptors = (http) => {
const category = getCategoryNameByUrl(response.config.url)
delCache('APP_CATEGORY_PWD_' + category);
uni.showToast({
title: '提示:密码不正确', icon: 'none', success: () => {
title: '提示:密码不正确',
icon: 'none',
success: () => {
setTimeout(() => {
showCategoryInputPasswordModal(response, category);
}, 800)
-3
View File
@@ -91,9 +91,6 @@
}
}
}
},
created() {
console.log("comment", this.comment)
}
};
</script>
+128 -20
View File
@@ -2,16 +2,34 @@
<tm-poup v-model="isShow" position="bottom" height="71vh" @change="handleOnChange">
<view class="title text-align-center">{{ calcTitle }}</view>
<tm-form @submit="fnOnSubmit">
<tm-input name="content" :vertical="true" required :height="220" input-type="textarea"
bg-color="grey-lighten-5" :maxlength="200" :borderBottom="false" placeholder="请输入内容,不超过200字符..."
v-model="form.content"></tm-input>
<tm-input name="content" :vertical="true" required :height="200" input-type="textarea"
bg-color="grey-lighten-5" :maxlength="200" :borderBottom="false"
:placeholder="config.editor.placeholder" v-model="form.content"></tm-input>
<tm-input name="author" align="right" required title="我的昵称" placeholder="请输入您的昵称..."
v-model="form.author"></tm-input>
<tm-input name="avatar" align="right" title="我的头像" placeholder="请输入您的头像..."
<tm-input v-if="false" name="avatar" align="right" title="我的头像" placeholder="请输入您的头像..."
v-model="form.avatar"></tm-input>
<tm-input name="email" align="right" title="邮箱地址" placeholder="请输入您的邮箱..." v-model="form.email"></tm-input>
<tm-input name="email" required align="right" title="我的邮箱" placeholder="请输入您的邮箱..." v-model="form.email"></tm-input>
<tm-input name="authorUrl" align="right" title="我的网站" placeholder="请输入您的网址..."
v-model="form.authorUrl"></tm-input>
<template v-if="config.security.captcha.anonymousCommentCaptcha">
<tm-input required name="captchaCode" align="right" title="验证码" placeholder="请输入验证码结果"
v-model="form.captchaCode">
<template v-slot:rightBtn>
<view class="captch-wrapper text-size-n">
<view v-if="captchaData.status==='loading'" class="text-grey">获取中...</view>
<view v-else-if="captchaData.status==='fail'" class="text-red"
@click="handleGetCaptchaImage()">
请重试
</view>
<image v-else :src="captchaData.image" class="w-full h-full" mode="aspectFit"
@click="handleGetCaptchaImage()"></image>
</view>
</template>
</tm-input>
</view>
</template>
<view class="pa-24 pl-30 pr-30">
<tm-button navtie-type="form" theme="bg-gradient-blue-accent" block>提交
</tm-button>
@@ -57,6 +75,28 @@ export default {
data() {
return {
isShow: false,
config: {
basic: {
enablePrivateComment: true,
},
security: {
captcha: {
enable: false,
type: "ARITHMETIC",
captchaLength: 4,
arithmeticRange: "20",
ignoreCase: true,
anonymousCommentCaptcha: false
}
},
editor: {
placeholder: "请输入内容,不超过200字符..."
}
},
captchaData: {
image: '', // 验证码
status: 'loading', // 状态 loading = 正在获取 success = 获取成功 fail = 获取失败
},
params: {
postName: '',
title: '', // 被回复的标题 type=user =用户名 否则为文章标题
@@ -71,7 +111,8 @@ export default {
authorUrl: '', // 作者主页
content: '', // 评论内容
email: '', // 邮件
postName: ""
postName: "",
captchaCode: undefined,
}
}
},
@@ -87,17 +128,15 @@ export default {
show: {
immediate: true,
handler: function(newVal) {
console.log("评论弹窗:", newVal)
if (!newVal) return;
this.isShow = true;
this.handleResetForm();
this.form.postName = this.postName;
let visitor = uni.getStorageSync('Visitor')
if (!visitor) return;
visitor = JSON.parse(visitor)
this.form.author = visitor.author;
this.form.avatar = visitor.avatar;
this.form.email = visitor.email;
this.form.authorUrl = visitor.authorUrl;
this.handleGetConfig();
this.handleInitVisitor();
}
}
},
@@ -117,10 +156,12 @@ export default {
authorUrl: '',
content: '',
email: '',
postName: ""
postName: "",
captchaCode: "",
}
},
fnOnSubmit(e) {
console.log("评论弹窗:", e)
if (e === false) {
return uni.$tm.toast('请检查所有的必填项是否填写完整!');
}
@@ -133,8 +174,51 @@ export default {
// if (this.form.authorUrl && !uni.$tm.test.url(this.form.authorUrl)) {
// return uni.$tm.toast('请输入正确的Url地址!');
// }
if (this.config?.security?.captcha?.anonymousCommentCaptcha) {
if (!this.form.captchaCode.trim()) {
return uni.$tm.toast('请填写验证码结果!');
}
} else {
this.form.captchaCode = undefined;
}
this.fnHandle();
},
handleGetConfig() {
this.$httpApi.v2Config.getCommentWidgetConfig()
.then(res => {
console.log("获取验证码配置", res)
this.config = this.$utils.deepMerge(this.config, res)
if (this.config?.security?.captcha?.anonymousCommentCaptcha) {
this.handleGetCaptchaImage();
}
})
.catch(err => {
console.log("获取验证码配置失败", err)
});
},
handleGetCaptchaImage() {
this.captchaData.status = 'loading';
this.$httpApi.v2.getCommentWidgetCaptcha()
.then(res => {
this.form.captchaCode = undefined;
this.captchaData.image = res;
this.captchaData.status = 'success';
})
.catch(err => {
console.log("获取验证码失败", err)
this.captchaData.status = 'fail';
this.captchaData.image = '';
});
},
handleInitVisitor() {
let visitor = uni.getStorageSync('Visitor')
if (!visitor) return;
visitor = JSON.parse(visitor)
this.form.author = visitor.author;
this.form.avatar = visitor.avatar;
this.form.email = visitor.email;
this.form.authorUrl = visitor.authorUrl;
},
handleSetVisitor() {
uni.setStorageSync('Visitor', JSON.stringify({
author: this.form.author,
@@ -155,7 +239,7 @@ export default {
raw: this.form.content,
content: this.form.content,
owner: {
avatar: this.form.avatar,
// avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
@@ -165,7 +249,8 @@ export default {
kind: "Post",
name: this.form.postName,
version: "v1alpha1",
}
},
captchaCode: this.form.captchaCode,
}
this.$httpApi.v2.addPostComment(commentForm)
.then(res => {
@@ -176,8 +261,15 @@ export default {
this.handleResetForm()
})
.catch(err => {
if (this.config?.security?.captcha?.anonymousCommentCaptcha) {
this.captchaData.image = err.captcha;
this.captchaData.status = 'success';
this.form.captchaCode = undefined;
uni.$tm.toast(err.detail ?? '评论失败');
} else {
uni.$tm.toast("评论失败");
});
}
})
return;
}
@@ -187,12 +279,13 @@ export default {
raw: this.form.content,
content: this.form.content,
owner: {
avatar: this.form.avatar,
// avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
},
quoteReply: this.form.postName
quoteReply: this.form.postName,
captchaCode: this.form.captchaCode,
}
this.$httpApi.v2.addPostCommentReply(this.form.postName, replyForm)
.then(res => {
@@ -203,8 +296,15 @@ export default {
this.handleResetForm()
})
.catch(err => {
if (this.config?.security?.captcha?.anonymousCommentCaptcha) {
this.captchaData.image = err.captcha;
this.captchaData.status = 'success';
this.form.captchaCode = undefined;
uni.$tm.toast(err.detail ?? '恢复失败');
} else {
uni.$tm.toast("回复失败");
});
}
})
},
handleOnChange(isOpen) {
this.isShow = isOpen;
@@ -230,4 +330,12 @@ export default {
.title {
margin: 24rpx 0;
}
.captch-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 200rpx;
height: 68rpx;
}
</style>
+30
View File
@@ -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))
}
+31
View File
@@ -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 {
@@ -60,6 +87,10 @@ export default {
})
},
// 设置默认的数据
setDefaultHaloGlobalConfigs({commit}) {
commit('setHaloConfigs', JSON.parse(JSON.stringify(DefaultHaloGlobalConfigs)))
},
// 设置默认的数据
setDefaultAppSettings({commit}) {
commit('setConfigs', JSON.parse(JSON.stringify(DefaultAppConfigs)))
},
+32
View File
@@ -0,0 +1,32 @@
/**
* 从带换行的cookie原始串提取某一条cookie(带;结尾)
* @param {string} cookieRaw setcookie原始字符串
* @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] : '';
}