From e8d13c674ffa293adbae75672a4b7d3402b6e784 Mon Sep 17 00:00:00 2001 From: liuyiwuqing <1520431201@qq.com> Date: Fri, 13 Jun 2025 03:38:44 +0000 Subject: [PATCH] =?UTF-8?q?!24=20feat(restrict-read):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E9=99=90=E5=88=B6=E9=98=85=E8=AF=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20*=20feat(restrict-read):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E9=99=90=E5=88=B6=E9=98=85=E8=AF=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=20*=20Merge=20remote-tracking=20branch=20'origin/v2.0?= =?UTF-8?q?-beta'=20into=20v2.0-beta=20*=20feat(restrict-read):=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=96=87=E7=AB=A0=E9=99=90=E5=88=B6=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v2/all.api.js | 44 +- .../restrict-read-skeleton.vue | 243 +++++++++++ config/token.config.template.js | 4 +- pages.json | 7 - pagesA/advertise/advertise.vue | 145 ------- pagesA/article-detail/article-detail.vue | 386 ++++++++++-------- pagesA/submit-link/submit-link.vue | 4 +- tm-vuetify/tool/function/vuex.js | 1 - utils/restrictRead.js | 140 +++++++ 9 files changed, 635 insertions(+), 339 deletions(-) create mode 100644 components/restrict-read-skeleton/restrict-read-skeleton.vue delete mode 100644 pagesA/advertise/advertise.vue create mode 100644 utils/restrictRead.js diff --git a/api/v2/all.api.js b/api/v2/all.api.js index c7d732a..b40494c 100644 --- a/api/v2/all.api.js +++ b/api/v2/all.api.js @@ -165,49 +165,49 @@ export default { return HttpHandler.Get(`/apis/api.plugin.halo.run/v1alpha1/plugins/PluginLinks/links`, params) }, - /** - * 校验文章访问密码 + * 限制阅读校验 + * @param restrictType + * @param code + * @param keyId + * @returns {HttpPromise} */ - checkPostVerifyCode: (verifyCode, postId) => { - return HttpHandler.Get(`/apis/tools.muyin.site/v1alpha1/verificationCode/check?code=${verifyCode}`, null, { + requestRestrictReadCheck: (restrictType, code, keyId) => { + const params = { + code: code, + templateType: 'post', + restrictType: restrictType, + keyId: keyId + } + return HttpHandler.Post(`/apis/tools.muyin.site/v1alpha1/restrict-read/check`, params, { header: { 'Authorization': getAppConfigs().pluginConfig.toolsPlugin?.Authorization, 'Wechat-Session-Id': uni.getStorageSync('openid'), - 'Post-Id': postId } }) }, - /** - * 校验文章访问密码 - */ - checkPostPasswordAccess: (password, postId) => { - return HttpHandler.Get(`/apis/tools.muyin.site/v1alpha1/visitPassword/checkPost?password=${password}`, - null, { - header: { - 'Authorization': getAppConfigs().pluginConfig.toolsPlugin?.Authorization, - 'Wechat-Session-Id': uni.getStorageSync('openid'), - 'Post-Id': postId - } - }) - }, - /** * 获取文章验证码 */ - getPostVerifyCode: () => { - return HttpHandler.Get(`/apis/tools.muyin.site/v1alpha1/verificationCode/create`, null, { + createVerificationCode: () => { + return HttpHandler.Get(`/apis/tools.muyin.site/v1alpha1/restrict-read/create`, null, { header: { 'Authorization': getAppConfigs().pluginConfig.toolsPlugin?.Authorization, } }) }, + /** * 提交友情链接 */ submitLink(form) { - return HttpHandler.Post(`/apis/linksSubmit.muyin.site/v1alpha1/submit`, form, null) + return HttpHandler.Post(`/apis/linksSubmit.muyin.site/v1alpha1/submit`, form, { + header: { + 'Authorization': getAppConfigs().pluginConfig.linksSubmitPlugin?.Authorization, + 'Wechat-Session-Id': uni.getStorageSync('openid'), + } + }) }, /** * 获取二维码信息 diff --git a/components/restrict-read-skeleton/restrict-read-skeleton.vue b/components/restrict-read-skeleton/restrict-read-skeleton.vue new file mode 100644 index 0000000..af315be --- /dev/null +++ b/components/restrict-read-skeleton/restrict-read-skeleton.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/config/token.config.template.js b/config/token.config.template.js index 12dd789..877cda1 100644 --- a/config/token.config.template.js +++ b/config/token.config.template.js @@ -1,3 +1,5 @@ +import {getAppConfigs} from "@/config/index"; + /** 配置后台管理员token */ const HaloTokenConfig = Object.freeze({ @@ -7,7 +9,7 @@ const HaloTokenConfig = Object.freeze({ /** 管理员token */ - systemToken: ``, + systemToken: getAppConfigs()?.basicConfig?.personalToken, /** 匿名用户token */ anonymousToken: `` }) diff --git a/pages.json b/pages.json index 1b516d3..8c508cb 100644 --- a/pages.json +++ b/pages.json @@ -259,13 +259,6 @@ } } } - }, - { - "path": "advertise/advertise", - "style": { - "navigationBarTitleText": "广告页面", - "enablePullDownRefresh": false - } }, { "path": "submit-link/submit-link", diff --git a/pagesA/advertise/advertise.vue b/pagesA/advertise/advertise.vue deleted file mode 100644 index 0fe750a..0000000 --- a/pagesA/advertise/advertise.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - diff --git a/pagesA/article-detail/article-detail.vue b/pagesA/article-detail/article-detail.vue index 147bf2c..152cc76 100644 --- a/pagesA/article-detail/article-detail.vue +++ b/pagesA/article-detail/article-detail.vue @@ -77,23 +77,46 @@ + + + + + + + + + + - - - 以下内容已隐藏,请验证后查看完整内容…… - - - + @@ -223,14 +246,41 @@ - - - - + + + + + + @@ -260,9 +310,23 @@ import rCanvas from '@/components/r-canvas/r-canvas.vue'; import barrage from '@/components/barrage/barrage.vue'; import {getAppConfigs} from '@/config/index.js' import {upvote} from '@/utils/upvote.js' +import { + checkPostRestrictRead, + copyToClipboard, + getRestrictReadTypeName, + getShowableContent +} from "@/utils/restrictRead"; +import HaloTokenConfig from "@/config/token.config"; +import RestrictReadSkeleton from "@/components/restrict-read-skeleton/restrict-read-skeleton.vue"; +import TmImages from "@/tm-vuetify/components/tm-images/tm-images.vue"; +import TmInput from "@/tm-vuetify/components/tm-input/tm-input.vue"; +let videoAd = null; export default { components: { + TmInput, + TmImages, + RestrictReadSkeleton, tmSkeleton, tmPoup, tmFlotbutton, @@ -305,16 +369,19 @@ export default { }, metas: [], // 自定义元数据 - // 文章加密(弹窗输入密码解密) - validVisitModal: { - show: false, - useCancel: false, - value: undefined + showContentArr: [], + restrictReadInputCode: '', + verificationCodeModal: { + show: false, + model: 'confirm', + confirmText: '确定', + type: '', + imgUrl: '', + adId: '' + }, + passwordModal: { + show: false }, - visitType: 0, // 0 未加密 1 后端部分隐藏 2 前端部分隐藏 3 全部隐藏 - visitPwd: undefined, - showValidVisitMore: true, - showGetPassword: false, commentModal: { show: false, isComment: false, @@ -397,6 +464,8 @@ export default { } }, methods: { + getRestrictReadTypeName, + checkPostRestrictRead, fnGetData() { this.loading = 'loading'; this.$httpApi.v2 @@ -409,35 +478,31 @@ export default { if (openid === '' || openid === null) { this.fnGetOpenid(); } - const visitFlag = uni.getStorageSync('visit_' + this.result?.metadata?.name); const toolsPluginEnabled = getAppConfigs().pluginConfig.toolsPlugin?.enabled; + const restrictRead = checkPostRestrictRead(this.result); - if (toolsPluginEnabled && !visitFlag) { - const annotationsMap = res?.metadata?.annotations; - if (('restrictReadEnable' in annotationsMap) && annotationsMap.restrictReadEnable === - 'true') { - this.visitType = 1; - this.showValidVisitMorePop(); - } else if ('unihalo_useVisitMorePwd' in annotationsMap) { - this.visitType = 2; - this.visitPwd = annotationsMap.unihalo_useVisitMorePwd; - this.showValidVisitMorePop(); - } else if ('unihalo_useVisitPwd' in annotationsMap) { - this.visitType = 3; - this.visitPwd = annotationsMap.unihalo_useVisitPwd; - this.showValidVisitPop(); - } else if (('restrictReadEnable' in annotationsMap) && annotationsMap - .restrictReadEnable === 'password') { - this.visitType = 4; - this.showValidVisitPop(); - } else { - this.visitType = 0; - this.showValidVisitMore = false; - } - this.fnHideContent(); - } else { - this.showValidVisitMore = false; + if (restrictRead && toolsPluginEnabled) { + const verifyCodeType = getAppConfigs().pluginConfig.toolsPlugin?.verifyCodeType; + if (verifyCodeType === 'scan') { + const scanCodeUrl = getAppConfigs().pluginConfig.toolsPlugin?.scanCodeUrl; + this.verificationCodeModal.type = 'scan'; + this.verificationCodeModal.imgUrl = this.$utils.checkImageUrl(scanCodeUrl); + this.verificationCodeModal.model = 'confirm'; + this.verificationCodeModal.confirmText = '立即验证'; + } else if (verifyCodeType === 'advert') { + const rewardedVideoAdId = getAppConfigs().pluginConfig.toolsPlugin?.rewardedVideoAdId; + this.verificationCodeModal.type = 'advert'; + this.verificationCodeModal.adId = rewardedVideoAdId; + this.verificationCodeModal.model = 'dialog'; + this.verificationCodeModal.confirmText = '观看视频'; + // #ifdef MP-WEIXIN + this.adLoad(); + // #endif + } + + const showableContentArr = getShowableContent(this.result); + this.showContentArr = showableContentArr; } this.fnSetPageTitle('文章详情'); @@ -1008,39 +1073,35 @@ export default { url: originalURL }); }, - showValidVisitPop() { - this.showValidVisitMore = true; - this.validVisitModal.value = undefined; - this.validVisitModal.show = true; - this.validVisitModal.useCancel = false; - }, - showValidVisitMorePop() { - this.showValidVisitMore = true; - this.validVisitModal.value = undefined; - this.validVisitModal.show = true; - this.validVisitModal.useCancel = true; - }, - closeValidVisitPop() { - this.validVisitModal.show = false; - this.validVisitModal.useCancel = true; - this.validVisitModal.value = undefined; - if (this.visitType === 1) { - // 显示是否前往获取验证弹窗 - this.validVisitModal.show = true; - this.showGetPassword = true; - } - }, - closeAllPop() { - this.validVisitModal.show = false; - this.validVisitModal.useCancel = true; - this.validVisitModal.value = undefined; - this.showGetPassword = false; - }, - toAdvertise() { - this.showGetPassword = false; - uni.navigateTo({ - url: '/pagesA/advertise/advertise' + readMore() { + const annotations = this.result?.metadata?.annotations; + const restrictReadEnable = annotations?.restrictReadEnable; + if (restrictReadEnable === 'password') { + this.passwordModal.show = true; + return; + } else if (restrictReadEnable === 'code') { + this.verificationCodeModal.show = true; + return; + } else if (restrictReadEnable === 'comment') { + uni.showToast({ + title: '前往web端评论后访问', + icon: 'none' }); + } else if (restrictReadEnable === 'login') { + uni.showToast({ + title: '前往web端登录后访问', + icon: 'none' + }); + } else if (restrictReadEnable === 'pay') { + uni.showToast({ + title: '前往web端支付后访问', + icon: 'none' + }); + } + // 两秒后执行 + setTimeout(() => { + copyToClipboard(`${HaloTokenConfig.BASE_API + this.result.status.permalink}`); + }, 2000); }, // 获取openid fnGetOpenid() { @@ -1058,82 +1119,85 @@ export default { }) // #endif }, - // 隐藏内容 - fnHideContent() { - switch (this.visitType) { - case 1: - const restrictReadRaw = this.result?.content?.raw.split('')[0]; - this.result.content.raw = restrictReadRaw; - return; - case 2: - case 3: - const length = this.result?.content?.raw?.length; - const first30PercentLength = Math.floor(length * 0.3); - const first30PercentRaw = this.result?.content?.raw?.substring(0, first30PercentLength); - this.result.content.raw = first30PercentRaw; - return; - case 4: - this.result.content.raw = ""; - return; - default: - return; - } + restrictReadCheckOrViewVideo() { + console.log('restrictReadCheckOrViewVideo', this.verificationCodeModal.type) + if (this.verificationCodeModal.type === 'advert') { + this.openVideoAd(); + } else { + this.restrictReadCheck(); + } }, // 校验密码 - fnValidVisitPwd() { - switch (this.visitType) { - case 0: - return; - case 1: - this.$httpApi.v2.checkPostVerifyCode(this.validVisitModal.value, this.result?.metadata?.name).then( - res => { - if (res.code === 200) { - uni.setStorageSync('visit_' + this.result?.metadata?.name, true) - this.closeAllPop(); - this.fnGetData(); - } else { - uni.showToast({ - title: '密码错误', - icon: 'none' - }); - } - }).catch(err => { - console.log(err); - }); - return; - case 2: - case 3: - if (this.visitPwd === this.validVisitModal.value) { - uni.setStorageSync('visit_' + this.result?.metadata?.name, true) - this.closeValidVisitPop(); - this.fnGetData(); - } else { - uni.showToast({ - title: '密码错误', - icon: 'none' - }); - } - return; - case 4: - this.$httpApi.v2.checkPostPasswordAccess(this.validVisitModal.value, this.result?.metadata?.name) - .then(res => { - if (res.code === 200) { - uni.setStorageSync('visit_' + this.result?.metadata?.name, true) - this.closeAllPop(); - this.fnGetData(); - } else { - uni.showToast({ - title: '密码错误', - icon: 'none' - }); - } - }).catch(err => { - console.log(err); - }); - return; - default: - return; - } + restrictReadCheck() { + if (!this.restrictReadInputCode) { + uni.showToast({ + title: '请输入内容', + icon: 'none' + }); + return; + } + this.$httpApi.v2.requestRestrictReadCheck(this.result?.metadata?.annotations?.restrictReadEnable, this.restrictReadInputCode, this.result?.metadata?.name) + .then(res => { + if (res.code === 200) { + this.passwordModal.show = false; + this.verificationCodeModal.show = false; + this.fnGetData(); + } else { + uni.showToast({ + title: '密码错误', + icon: 'none' + }); + } + }) + .catch(err => { + console.error(err); + }); + }, + adLoad() { + if (wx.createRewardedVideoAd) { + videoAd = wx.createRewardedVideoAd({ + adUnitId: this.verificationCodeModal.adId + }) + videoAd.onError(err => { + }) + videoAd.onClose((status) => { + if (status && status.isEnded || status === undefined) { + //这里写广告播放完成后的事件 + this.getVerificationCode(); + } else { + // 广告播放未完成 + } + }) + } + }, + openVideoAd: function () { + if (videoAd && this.verificationCodeModal.adId !== '') { + videoAd.show().catch(err => { + // 失败重试 + console.log("广告拉取失败") + videoAd.load().then(() => videoAd.show()) + }) + } else { + this.getVerificationCode(); + } + }, + getVerificationCode() { + uni.showLoading({ + title: '正在获取...' + }); + this.$httpApi.v2.createVerificationCode() + .then(res => { + if (res.code === 200) { + this.verificationCodeModal.show = false; + this.restrictReadInputCode = res.data; + this.restrictReadCheck(); + } else { + uni.$tm.toast('操作失败,请重试!'); + } + }) + .catch(err => { + uni.$tm.toast(err.message); + }); }, async qrCodeImageUrl() { const useDynamicQRCode = this.haloConfigs?.appConfig?.appInfo?.useDynamicQRCode; diff --git a/pagesA/submit-link/submit-link.vue b/pagesA/submit-link/submit-link.vue index 939e45e..ecd548a 100644 --- a/pagesA/submit-link/submit-link.vue +++ b/pagesA/submit-link/submit-link.vue @@ -192,7 +192,7 @@ export default { this.$httpApi.v2.submitLink(this.form) .then(res => { if (res.code === 200) { - uni.$tm.toast('友链提交成功!'); + uni.$tm.toast(res.msg); setTimeout(() => { uni.navigateTo({ url: '/pagesA/friend-links/friend-links', @@ -204,7 +204,7 @@ export default { }); }, 1000); } else { - uni.$tm.toast('操作失败,请重试!'); + uni.$tm.toast(res.msg); } }) .catch(err => { diff --git a/tm-vuetify/tool/function/vuex.js b/tm-vuetify/tool/function/vuex.js index 38a60c6..c277de4 100644 --- a/tm-vuetify/tool/function/vuex.js +++ b/tm-vuetify/tool/function/vuex.js @@ -19,7 +19,6 @@ class vuex { let t = this; const g = this.store.getters let keys = Object.keys(g); - console.log(keys) let k = keys.map((el,index)=>{ let f = el.split('/'); let tst = {} diff --git a/utils/restrictRead.js b/utils/restrictRead.js new file mode 100644 index 0000000..d510bed --- /dev/null +++ b/utils/restrictRead.js @@ -0,0 +1,140 @@ +/** + * 检查文章是否受限 + * @param post + * @returns {boolean} + */ +export function checkPostRestrictRead(post) { + const annotations = post?.metadata?.annotations; + const restrictReadEnable = annotations?.restrictReadEnable; + + if (restrictReadEnable === 'false') return false; + + const restrictType = restrictReadEnable; + const raw = post.content.raw; + + const startTag = ``; + const endTag = ``; + + // 使用正则模糊匹配(允许前后有空白字符) + const startRegex = new RegExp(`\\s*${escapeRegExp(startTag)}\\s*`); + const endRegex = new RegExp(`\\s*${escapeRegExp(endTag)}\\s*`); + + return startRegex.test(raw) && endRegex.test(raw); +} + +/** + * 替换受限内容 + * @param post + * @param replacement + * @returns {*} + */ +export function replaceRestrictedContent(post, replacement = '') { + const annotations = post?.metadata?.annotations; + const restrictReadEnable = annotations?.restrictReadEnable; + + if (restrictReadEnable === 'false') return post.content.raw; + + const restrictType = restrictReadEnable; + const raw = post.content.raw; + + const startTag = ``; + const endTag = ``; + + const startRegex = new RegExp(`\\s*${escapeRegExp(startTag)}\\s*`, 'g'); + const endRegex = new RegExp(`\\s*${escapeRegExp(endTag)}\\s*`, 'g'); + + // 构造完整匹配的正则 + const pattern = `${startRegex.source}(.*?)${endRegex.source}`; + const regex = new RegExp(pattern, 'gs'); + + return raw.replace(regex, replacement); +} + +// 常量定义(可抽离到 constants.js) +const PLACEHOLDER = 'restrict-read-placeholder'; + +/** + * 获取可展示的HTML内容块 + * @param {Object} post - 文章对象 + * @returns {string[]} - 分割后的HTML片段数组 + */ +export function getShowableContent(post) { + const restrictEnabled = checkPostRestrictRead(post); + const rawContent = post?.content?.raw || ''; + + // 替换受限内容为占位符 + const processedContent = restrictEnabled + ? replaceRestrictedContent(post, PLACEHOLDER) + : rawContent; + + // 按占位符分割内容 + const contentFragments = processedContent + .split(PLACEHOLDER) + .map(fragment => fragment.trim()) + .filter(fragment => fragment); + + // 移除最后一个元素如果它只包含HTML标签而无实际文本 + if (contentFragments.length > 0 && isHtmlEmpty(contentFragments[contentFragments.length - 1])) { + contentFragments.pop(); + } + + console.log('contentFragments:', contentFragments); + + return contentFragments; +} + +/** + * 获取受限阅读类型名称 + * @param post + * @returns {string} + */ +export function getRestrictReadTypeName(post) { + const annotations = post?.metadata?.annotations; + const restrictReadEnable = annotations?.restrictReadEnable; + + if (restrictReadEnable === 'false') return ''; + if (restrictReadEnable === 'password') return '密码'; + if (restrictReadEnable === 'code') return '验证码'; + if (restrictReadEnable === 'login') return '登录'; + if (restrictReadEnable === 'pay') return '付费'; + if (restrictReadEnable === 'comment') return '评论'; +} + +/** + * 复制文本到剪贴板 + * @param text + * @returns {Promise} + */ +export async function copyToClipboard(text) { + try { + await uni.setClipboardData({ + data: text, + success: () => { + uni.showToast({ + title: '复制成功', + icon: 'success' + }); + } + }); + } catch (error) { + console.error('复制出错:', error); + } +} + +/** + * 转义字符串用于正则表达式 + * @param string + * @returns {*} + */ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * 判断字符串去除HTML标签后是否为空 + * @param {string} html + * @returns {boolean} + */ +function isHtmlEmpty(html) { + return !html || !html.replace(/<[^>]+>/g, '').trim(); +} \ No newline at end of file