1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-02 17:30: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 // #endif
uni.$tm.vx.actions('config/fetchConfigs').then((res) => { 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的配置是否已经就绪,若未就绪则设置默认的 // 启动检查app的配置是否已经就绪,若未就绪则设置默认的
uni.$tm.vx.actions('setting/checkAndSetDefaultAppSettings'); uni.$tm.vx.actions('setting/checkAndSetDefaultAppSettings');
}, },
+36 -19
View File
@@ -3,12 +3,14 @@ import {
} from '@/utils/token.js' } from '@/utils/token.js'
import HttpHandler from '@/common/http/request.js' import HttpHandler from '@/common/http/request.js'
import qs from 'qs' import qs from 'qs'
import { getCache } from "@/utils/storage";
import { getAppConfigs } from '@/config/index.js'
import { import {
getAppConfigs getNologinEmail,
} from '@/config/index.js' getOpenid
import { getNologinEmail, getOpenid } from "@/utils/auth"; } from "@/utils/auth";
import { CommentWidgetCaptchaCookies } from '@/common/constant/cookies.js'
export default { export default {
getPostList: (params) => { getPostList: (params) => {
return HttpHandler.Get(`/apis/api.content.halo.run/v1alpha1/posts`, params) return HttpHandler.Get(`/apis/api.content.halo.run/v1alpha1/posts`, params)
@@ -52,11 +54,27 @@ export default {
}, },
addPostComment: (data) => { 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) => { 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) => { getTagList: (params) => {
@@ -68,19 +86,11 @@ export default {
}, },
getMomentList: (params) => { getMomentList: (params) => {
return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments`, params, { return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments`, params)
custom: {
personalToken: getPersonalToken()
}
})
}, },
getMomentByName: (name) => { getMomentByName: (name) => {
return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {}, { return HttpHandler.Get(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {})
custom: {
personalToken: getPersonalToken()
}
})
}, },
getBlogStatistics: () => { getBlogStatistics: () => {
@@ -176,10 +186,17 @@ export default {
}, },
getDoubanDetail: (url) => { 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) => { postTrackersCounter: (data) => {
return HttpHandler.Post(`/apis/api.halo.run/v1alpha1/trackers/counter`, 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: () => { getAppConfigs: () => {
return HttpHandler.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getConfigs`, {}) 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"; } from "./interceptors.js";
import Request from "@/js_sdk/luch-request/luch-request"; import Request from "@/js_sdk/luch-request/luch-request";
const http = new Request() const http = new Request({
withCredentials: true, // 是否携带cookie
})
/* 设置全局配置 */ /* 设置全局配置 */
http.setConfig((config) => { 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 getCategoryNameByUrl = (url) => {
const reg = '(?<=/api/content/categories/).+(?=/posts)' const reg = '(?<=/api/content/categories/).+(?=/posts)'
return url.match(reg)[0] || '无分类名' return url.match(reg)[0] || '无分类名'
} }
const showCategoryInputPasswordModal = (response, category) => { const showCategoryInputPasswordModal = (response, category) => {
uni.showModal({ uni.showModal({
title: `[ ${category} ] 分类已加密`, // TODO 这里应该获取分类的名字,可以在弹窗之前请求后台拿到所有分类根据分类code拿到名称,但是不会在这之前发送请求 title: `[ ${category} ] 分类已加密`, // TODO 这里应该获取分类的名字,可以在弹窗之前请求后台拿到所有分类根据分类code拿到名称,但是不会在这之前发送请求
content: '', content: '',
editable: true, editable: true,
placeholderText: '请输入分类密码后访问', placeholderText: '请输入分类密码后访问',
confirmText: '验证密码', confirmText: '验证密码',
cancelText: '暂不访问', cancelText: '暂不访问',
showCancel: true, showCancel: true,
cancelColor: '#999999', cancelColor: '#999999',
confirmColor: '#03a9f4', confirmColor: '#03a9f4',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// TODO 这里如果没有输入密码点击确认应该阻止窗口关闭,但是没找到方法 // TODO 这里如果没有输入密码点击确认应该阻止窗口关闭,但是没找到方法
if (!res.content) { if (!res.content) {
uni.showToast({ uni.showToast({
title: '提示:请输入密码', icon: 'none', success: () => { title: '提示:请输入密码',
setTimeout(() => { icon: 'none',
showCategoryInputPasswordModal(response, category); success: () => {
}, 800) setTimeout(() => {
} showCategoryInputPasswordModal(response, category);
}) }, 800)
return; }
} })
// 根据请求URL正则匹配分类code,然后把输入的密码根据分类code放入缓存,然后在category.getCategoryPostList中获取,解决多个分类加密输入密码后点错的问题 return;
// 目前存在一个问题,比如前两个都需要密码,如果先输入第二个的密码之后,重新进来默认打开第一个还会弹窗,所以想在弹窗标题上增加分类名字 }
// 另外有以下两种方式科技解决 // 根据请求URL正则匹配分类code,然后把输入的密码根据分类code放入缓存,然后在category.getCategoryPostList中获取,解决多个分类加密输入密码后点错的问题
// TODO 1.其实这里获取到密码之后可以直接发送一个请求追加上password参数,因为后台会缓存权限,后续不输入密码也可以访问,可惜不会 // 目前存在一个问题,比如前两个都需要密码,如果先输入第二个的密码之后,重新进来默认打开第一个还会弹窗,所以想在弹窗标题上增加分类名字
// TODO 2.另外也可以拿到密码之后,直接选中该分类追加password参数,重新请求,可惜也不会 // 另外有以下两种方式科技解决
setCache('APP_CATEGORY_PWD_' + category, res.content) // TODO 1.其实这里获取到密码之后可以直接发送一个请求追加上password参数,因为后台会缓存权限,后续不输入密码也可以访问,可惜不会
uni.reLaunch({ // TODO 2.另外也可以拿到密码之后,直接选中该分类追加password参数,重新请求,可惜也不会
url: '/pages/tabbar/category/category' setCache('APP_CATEGORY_PWD_' + category, res.content)
}); uni.reLaunch({
} else if (res.cancel) { 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) => { export const setInterceptors = (http) => {
http.interceptors.request.use((config) => { http.interceptors.request.use((config) => {
console.log("config", config) console.log("config", config)
// 可使用async await 做异步操作 // 可使用async await 做异步操作
config.header = { config.header = {
...config.header ...config.header
// ... 可以直接加参数 // ... 可以直接加参数
}; };
if (config.custom.personalToken) { if (config.custom.personalToken) {
config.header['Authorization'] = `Bearer ${config.custom.personalToken}` config.header['Authorization'] = `Bearer ${config.custom.personalToken}`
} }
return config; return config;
}, (config) => { }, (config) => {
// 可使用async await 做异步操作 // 可使用async await 做异步操作
return Promise.reject(config); return Promise.reject(config);
}); });
http.interceptors.response.use((response) => { http.interceptors.response.use((response) => {
/* 对响应成功做点什么 可使用async await 做异步操作*/ /* 对响应成功做点什么 可使用async await 做异步操作*/
// if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject() // if (response.data.code !== 200) { // 服务端返回的状态码不等于200,则reject()
// return Promise.reject(response) // return Promise.reject 可使promise状态进入catch // return Promise.reject(response) // return Promise.reject 可使promise状态进入catch
// if (response.config.custom.verification) { // 演示自定义参数的作用 // if (response.config.custom.verification) { // 演示自定义参数的作用
// return response.data // return response.data
// } // }
if (response.statusCode == 200) { saveCookies(response);
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);
}
}); 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> </script>
+328 -220
View File
@@ -1,233 +1,341 @@
<template> <template>
<tm-poup v-model="isShow" position="bottom" height="71vh" @change="handleOnChange"> <tm-poup v-model="isShow" position="bottom" height="71vh" @change="handleOnChange">
<view class="title text-align-center">{{ calcTitle }}</view> <view class="title text-align-center">{{ calcTitle }}</view>
<tm-form @submit="fnOnSubmit"> <tm-form @submit="fnOnSubmit">
<tm-input name="content" :vertical="true" required :height="220" input-type="textarea" <tm-input name="content" :vertical="true" required :height="200" input-type="textarea"
bg-color="grey-lighten-5" :maxlength="200" :borderBottom="false" placeholder="请输入内容,不超过200字符..." bg-color="grey-lighten-5" :maxlength="200" :borderBottom="false"
v-model="form.content"></tm-input> :placeholder="config.editor.placeholder" v-model="form.content"></tm-input>
<tm-input name="author" align="right" required title="我的昵称" placeholder="请输入您的昵称..." <tm-input name="author" align="right" required title="我的昵称" placeholder="请输入您的昵称..."
v-model="form.author"></tm-input> 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> 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="请输入您的网址..." <tm-input name="authorUrl" align="right" title="我的网站" placeholder="请输入您的网址..."
v-model="form.authorUrl"></tm-input> v-model="form.authorUrl"></tm-input>
<view class="pa-24 pl-30 pr-30"> <template v-if="config.security.captcha.anonymousCommentCaptcha">
<tm-button navtie-type="form" theme="bg-gradient-blue-accent" block>提交 <tm-input required name="captchaCode" align="right" title="验证码" placeholder="请输入验证码结果"
</tm-button> v-model="form.captchaCode">
</view> <template v-slot:rightBtn>
</tm-form> <view class="captch-wrapper text-size-n">
</tm-poup> <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> </template>
<script> <script>
import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue'; import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
import tmForm from '@/tm-vuetify/components/tm-form/tm-form.vue'; import tmForm from '@/tm-vuetify/components/tm-form/tm-form.vue';
import tmInput from '@/tm-vuetify/components/tm-input/tm-input.vue'; import tmInput from '@/tm-vuetify/components/tm-input/tm-input.vue';
import tmSwitch from '@/tm-vuetify/components/tm-switch/tm-switch.vue'; import tmSwitch from '@/tm-vuetify/components/tm-switch/tm-switch.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue'; import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
export default { export default {
name: 'CommentModal', name: 'CommentModal',
components: { components: {
tmPoup, tmPoup,
tmForm, tmForm,
tmInput, tmInput,
tmSwitch, tmSwitch,
tmButton tmButton
}, },
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
default: false default: false
}, },
isComment: { isComment: {
type: Boolean, type: Boolean,
default: false default: false
}, },
title: { title: {
type: String, type: String,
default: "" default: ""
}, },
postName: { postName: {
type: String, type: String,
default: "" default: ""
} }
}, },
data() { data() {
return { return {
isShow: false, isShow: false,
params: { config: {
postName: '', basic: {
title: '', // 被回复的标题 type=user =用户名 否则为文章标题 enablePrivateComment: true,
form: '', },
formPage: '', // 来自哪个页面 security: {
type: 'post' // 来源文章/页面 还是用户 user=用户 captcha: {
}, enable: false,
form: { type: "ARITHMETIC",
allowNotification: true, captchaLength: 4,
author: '', // 作者 arithmeticRange: "20",
avatar: '', ignoreCase: true,
authorUrl: '', // 作者主页 anonymousCommentCaptcha: false
content: '', // 评论内容 }
email: '', // 邮件 },
postName: "" editor: {
} placeholder: "请输入内容,不超过200字符..."
} }
}, },
computed: { captchaData: {
calcTitle() { image: '', // 验证码
if (this.isComment) { status: 'loading', // 状态 loading = 正在获取 success = 获取成功 fail = 获取失败
return this.title },
} params: {
return `回复用户:${this.title}` postName: '',
} title: '', // 被回复的标题 type=user =用户名 否则为文章标题
}, form: '',
watch: { formPage: '', // 来自哪个页面
show: { type: 'post' // 来源文章/页面 还是用户 user=用户
immediate: true, },
handler: function (newVal) { form: {
if (!newVal) return; allowNotification: true,
this.isShow = true; author: '', // 作者
this.handleResetForm(); avatar: '',
this.form.postName = this.postName; authorUrl: '', // 作者主页
let visitor = uni.getStorageSync('Visitor') content: '', // 评论内容
if (!visitor) return; email: '', // 邮件
visitor = JSON.parse(visitor) postName: "",
this.form.author = visitor.author; captchaCode: undefined,
this.form.avatar = visitor.avatar; }
this.form.email = visitor.email; }
this.form.authorUrl = visitor.authorUrl; },
} computed: {
} calcTitle() {
}, if (this.isComment) {
methods: { return this.title
handleResetForm() { }
this.params = { return `回复用户:${this.title}`
postName: '', }
title: '', },
form: '', watch: {
formPage: '', show: {
type: 'post' immediate: true,
} handler: function(newVal) {
this.form = { console.log("评论弹窗:", newVal)
allowNotification: true, if (!newVal) return;
author: '', this.isShow = true;
avatar: '', this.handleResetForm();
authorUrl: '', this.form.postName = this.postName;
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: '正在提交...'
});
// 评论 this.handleGetConfig();
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.handleInitVisitor();
const replyForm = { }
allowNotification: true, }
raw: this.form.content, },
content: this.form.content, methods: {
owner: { handleResetForm() {
avatar: this.form.avatar, this.params = {
displayName: this.form.author, postName: '',
email: this.form.email, title: '',
website: this.form.authorUrl, form: '',
}, formPage: '',
quoteReply: this.form.postName type: 'post'
} }
this.$httpApi.v2.addPostCommentReply(this.form.postName, replyForm) this.form = {
.then(res => { allowNotification: true,
uni.$tm.toast('回复成功,可能需要审核!'); author: '',
// 更新评论者信息 avatar: '',
this.handleSetVisitor(); authorUrl: '',
this.handleClose(true) content: '',
this.handleResetForm() email: '',
}) postName: "",
.catch(err => { captchaCode: "",
uni.$tm.toast("回复失败"); }
}); },
}, fnOnSubmit(e) {
handleOnChange(isOpen) { console.log("评论弹窗:", e)
this.isShow = isOpen; if (e === false) {
if(!isOpen){ 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", { this.$emit("on-close", {
isSubmit:false, isSubmit: true,
refresh: false refresh: refresh
}) })
} }
}, }
handleClose(refresh = false) { }
this.isShow = false;
this.$emit("on-close", {
isSubmit:true,
refresh: refresh
})
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.title { .title {
margin: 24rpx 0; margin: 24rpx 0;
} }
</style>
.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 * 版本:v0.1.0
*/ */
import {DefaultAppConfigs, getAppConfigs, getAppMockJson, setAppConfigs, setAppMockJson} from '@/config/index.js' 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 v2Config from '@/api/v2/all.config.js'
import utils from '@/utils/index.js' import utils from '@/utils/index.js'
import {setTokens} from "@/utils/token"; import {setTokens} from "@/utils/token";
@@ -13,10 +14,15 @@ import {setTokens} from "@/utils/token";
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
haloConfig: getHaloGlobalConfigs(),
configs: getAppConfigs(), configs: getAppConfigs(),
mockJson: getAppMockJson(), mockJson: getAppMockJson(),
}, },
getters: { getters: {
getHaloConfig(state) {
if (state.haloConfig) return state.haloConfig;
return getHaloGlobalConfigs();
},
getConfigs(state) { getConfigs(state) {
if (state.configs) return state.configs; if (state.configs) return state.configs;
return getAppConfigs() return getAppConfigs()
@@ -27,6 +33,10 @@ export default {
} }
}, },
mutations: { mutations: {
setHaloConfigs(state, data) {
state.haloConfig = data;
setHaloGlobalConfigs(data);
},
setConfigs(state, data) { setConfigs(state, data) {
state.configs = data; state.configs = data;
setAppConfigs(data) setAppConfigs(data)
@@ -37,6 +47,23 @@ export default {
} }
}, },
actions: { 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}) { fetchConfigs({state, commit, dispatch}) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
@@ -59,6 +86,10 @@ export default {
} }
}) })
}, },
// 设置默认的数据
setDefaultHaloGlobalConfigs({commit}) {
commit('setHaloConfigs', JSON.parse(JSON.stringify(DefaultHaloGlobalConfigs)))
},
// 设置默认的数据 // 设置默认的数据
setDefaultAppSettings({commit}) { setDefaultAppSettings({commit}) {
commit('setConfigs', JSON.parse(JSON.stringify(DefaultAppConfigs))) 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] : '';
}