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,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');
},
+36 -19
View File
@@ -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`, {})
}
}
+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) => {
+155 -119
View File
@@ -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);
}
});
};
-3
View File
@@ -91,9 +91,6 @@
}
}
}
},
created() {
console.log("comment", this.comment)
}
};
</script>
+328 -220
View File
@@ -1,233 +1,341 @@
<template>
<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="author" align="right" required title="我的昵称" placeholder="请输入您的昵称..."
v-model="form.author"></tm-input>
<tm-input 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="authorUrl" align="right" title="我的网站" placeholder="请输入您的网址..."
v-model="form.authorUrl"></tm-input>
<view class="pa-24 pl-30 pr-30">
<tm-button navtie-type="form" theme="bg-gradient-blue-accent" block>提交
</tm-button>
</view>
</tm-form>
</tm-poup>
<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="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 v-if="false" name="avatar" align="right" title="我的头像" placeholder="请输入您的头像..."
v-model="form.avatar"></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>
</view>
</tm-form>
</tm-poup>
</template>
<script>
import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
import tmForm from '@/tm-vuetify/components/tm-form/tm-form.vue';
import tmInput from '@/tm-vuetify/components/tm-input/tm-input.vue';
import tmSwitch from '@/tm-vuetify/components/tm-switch/tm-switch.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
import tmForm from '@/tm-vuetify/components/tm-form/tm-form.vue';
import tmInput from '@/tm-vuetify/components/tm-input/tm-input.vue';
import tmSwitch from '@/tm-vuetify/components/tm-switch/tm-switch.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
export default {
name: 'CommentModal',
components: {
tmPoup,
tmForm,
tmInput,
tmSwitch,
tmButton
},
props: {
show: {
type: Boolean,
default: false
},
isComment: {
type: Boolean,
default: false
},
title: {
type: String,
default: ""
},
postName: {
type: String,
default: ""
}
},
data() {
return {
isShow: false,
params: {
postName: '',
title: '', // 被回复的标题 type=user =用户名 否则为文章标题
form: '',
formPage: '', // 来自哪个页面
type: 'post' // 来源文章/页面 还是用户 user=用户
},
form: {
allowNotification: true,
author: '', // 作者
avatar: '',
authorUrl: '', // 作者主页
content: '', // 评论内容
email: '', // 邮件
postName: ""
}
}
},
computed: {
calcTitle() {
if (this.isComment) {
return this.title
}
return `回复用户:${this.title}`
}
},
watch: {
show: {
immediate: true,
handler: function (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;
}
}
},
methods: {
handleResetForm() {
this.params = {
postName: '',
title: '',
form: '',
formPage: '',
type: 'post'
}
this.form = {
allowNotification: true,
author: '',
avatar: '',
authorUrl: '',
content: '',
email: '',
postName: ""
}
},
fnOnSubmit(e) {
if (e === false) {
return uni.$tm.toast('请检查所有的必填项是否填写完整!');
}
// if (!this.form.email) {
// return uni.$tm.toast('未填写邮箱地址,将无法接收提醒!');
// }
// if (this.form.email && !uni.$tm.test.email(this.form.email)) {
// return uni.$tm.toast('请填写正确的邮箱地址!');
// }
// if (this.form.authorUrl && !uni.$tm.test.url(this.form.authorUrl)) {
// return uni.$tm.toast('请输入正确的Url地址!');
// }
this.fnHandle();
},
handleSetVisitor() {
uni.setStorageSync('Visitor', JSON.stringify({
author: this.form.author,
avatar: this.form.avatar,
email: this.form.email,
authorUrl: this.form.authorUrl,
}))
},
fnHandle() {
uni.showLoading({
title: '正在提交...'
});
export default {
name: 'CommentModal',
components: {
tmPoup,
tmForm,
tmInput,
tmSwitch,
tmButton
},
props: {
show: {
type: Boolean,
default: false
},
isComment: {
type: Boolean,
default: false
},
title: {
type: String,
default: ""
},
postName: {
type: String,
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 =用户名 否则为文章标题
form: '',
formPage: '', // 来自哪个页面
type: 'post' // 来源文章/页面 还是用户 user=用户
},
form: {
allowNotification: true,
author: '', // 作者
avatar: '',
authorUrl: '', // 作者主页
content: '', // 评论内容
email: '', // 邮件
postName: "",
captchaCode: undefined,
}
}
},
computed: {
calcTitle() {
if (this.isComment) {
return this.title
}
return `回复用户:${this.title}`
}
},
watch: {
show: {
immediate: true,
handler: function(newVal) {
console.log("评论弹窗:", newVal)
if (!newVal) return;
this.isShow = true;
this.handleResetForm();
this.form.postName = this.postName;
// 评论
if (this.isComment) {
const commentForm = {
allowNotification: true,
raw: this.form.content,
content: this.form.content,
owner: {
avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
},
subjectRef: {
group: "content.halo.run",
kind: "Post",
name: this.form.postName,
version: "v1alpha1",
}
}
this.$httpApi.v2.addPostComment(commentForm)
.then(res => {
uni.$tm.toast('评论成功,可能需要审核!');
// 更新评论者信息
this.handleSetVisitor();
this.handleClose(true)
this.handleResetForm()
})
.catch(err => {
uni.$tm.toast("评论失败");
});
return;
}
this.handleGetConfig();
// 回复
const replyForm = {
allowNotification: true,
raw: this.form.content,
content: this.form.content,
owner: {
avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
},
quoteReply: this.form.postName
}
this.$httpApi.v2.addPostCommentReply(this.form.postName, replyForm)
.then(res => {
uni.$tm.toast('回复成功,可能需要审核!');
// 更新评论者信息
this.handleSetVisitor();
this.handleClose(true)
this.handleResetForm()
})
.catch(err => {
uni.$tm.toast("回复失败");
});
},
handleOnChange(isOpen) {
this.isShow = isOpen;
if(!isOpen){
this.handleInitVisitor();
}
}
},
methods: {
handleResetForm() {
this.params = {
postName: '',
title: '',
form: '',
formPage: '',
type: 'post'
}
this.form = {
allowNotification: true,
author: '',
avatar: '',
authorUrl: '',
content: '',
email: '',
postName: "",
captchaCode: "",
}
},
fnOnSubmit(e) {
console.log("评论弹窗:", e)
if (e === false) {
return uni.$tm.toast('请检查所有的必填项是否填写完整!');
}
// if (!this.form.email) {
// return uni.$tm.toast('未填写邮箱地址,将无法接收提醒!');
// }
// if (this.form.email && !uni.$tm.test.email(this.form.email)) {
// return uni.$tm.toast('请填写正确的邮箱地址!');
// }
// 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,
avatar: this.form.avatar,
email: this.form.email,
authorUrl: this.form.authorUrl,
}))
},
fnHandle() {
uni.showLoading({
title: '正在提交...'
});
// 评论
if (this.isComment) {
const commentForm = {
allowNotification: true,
raw: this.form.content,
content: this.form.content,
owner: {
// avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
},
subjectRef: {
group: "content.halo.run",
kind: "Post",
name: this.form.postName,
version: "v1alpha1",
},
captchaCode: this.form.captchaCode,
}
this.$httpApi.v2.addPostComment(commentForm)
.then(res => {
uni.$tm.toast('评论成功,可能需要审核!');
// 更新评论者信息
this.handleSetVisitor();
this.handleClose(true)
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;
}
// 回复
const replyForm = {
allowNotification: true,
raw: this.form.content,
content: this.form.content,
owner: {
// avatar: this.form.avatar,
displayName: this.form.author,
email: this.form.email,
website: this.form.authorUrl,
},
quoteReply: this.form.postName,
captchaCode: this.form.captchaCode,
}
this.$httpApi.v2.addPostCommentReply(this.form.postName, replyForm)
.then(res => {
uni.$tm.toast('回复成功,可能需要审核!');
// 更新评论者信息
this.handleSetVisitor();
this.handleClose(true)
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;
if (!isOpen) {
this.$emit("on-close", {
isSubmit: false,
refresh: false
})
}
},
handleClose(refresh = false) {
this.isShow = false;
this.$emit("on-close", {
isSubmit:false,
refresh: false
isSubmit: true,
refresh: refresh
})
}
},
handleClose(refresh = false) {
this.isShow = false;
this.$emit("on-close", {
isSubmit:true,
refresh: refresh
})
}
}
}
}
}
</script>
<style lang="scss" scoped>
.title {
margin: 24rpx 0;
}
</style>
.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 {
@@ -59,6 +86,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] : '';
}