ソースを参照

!24 feat(restrict-read): 实现文章限制阅读功能
* feat(restrict-read): 实现文章限制阅读功能
* Merge remote-tracking branch 'origin/v2.0-beta' into v2.0-beta
* feat(restrict-read): 实现文章限制阅读功能

liuyiwuqing 9 ヶ月 前
コミット
e8d13c674f

+ 23 - 23
api/v2/all.api.js

@@ -165,49 +165,49 @@ export default {
         return HttpHandler.Get(`/apis/api.plugin.halo.run/v1alpha1/plugins/PluginLinks/links`, params)
     },
 
-
     /**
-     * 校验文章访问密码
-     */
-    checkPostVerifyCode: (verifyCode, postId) => {
-        return HttpHandler.Get(`/apis/tools.muyin.site/v1alpha1/verificationCode/check?code=${verifyCode}`, null, {
+     * 限制阅读校验
+     * @param restrictType
+     * @param code
+     * @param keyId
+     * @returns {HttpPromise<any>}
+     */
+    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'),
+            }
+        })
     },
     /**
      * 获取二维码信息

+ 243 - 0
components/restrict-read-skeleton/restrict-read-skeleton.vue

@@ -0,0 +1,243 @@
+<script>
+export default {
+  name: 'restrict-read-skeleton',
+  props: {
+    loading: {
+      type: Boolean,
+      default: true
+    },
+    hover: {
+      type: Boolean,
+      default: false
+    },
+    buttonText: {
+      type: String,
+      default: '刷新'
+    },
+    buttonColor: {
+      type: String,
+      default: '#07c160'
+    },
+    buttonSize: {
+      type: String,
+      default: 'normal', // 'small', 'normal', 'large'
+      validator: value => ['small', 'normal', 'large'].includes(value)
+    },
+    lines: {
+      type: Number,
+      default: 4,
+      validator: value => value >= 1 && value <= 6
+    },
+    skeletonColor: {
+      type: String,
+      default: '#f5f5f5'
+    },
+    skeletonHighlight: {
+      type: String,
+      default: '#e8e8e8'
+    },
+    animationDuration: {
+      type: Number,
+      default: 1.5
+    },
+    showButton: {
+      type: Boolean,
+      default: true
+    },
+    tipText: {
+      type: String,
+      default: '' // 默认不显示提示文字
+    },
+    tipColor: {
+      type: String,
+      default: '#666666' // 提示文字颜色
+    },
+    tipSize: {
+      type: Number,
+      default: 24 // 提示文字大小,单位rpx
+    }
+  },
+  methods: {
+    handleRefresh() {
+      this.$emit('refresh');
+    },
+    onTouchStart() {
+      this.$emit('touchstart');
+    },
+    onTouchEnd() {
+      this.$emit('touchend');
+    }
+  }
+};
+</script>
+
+<template>
+  <!-- 骨架屏容器 -->
+  <view class="container">
+    <!-- 骨架屏内容 -->
+    <view class="skeleton" v-if="loading">
+      <!-- 内容区域 -->
+      <view class="skeleton-body">
+        <view
+            v-for="(item, index) in Array(lines).fill(0)"
+            :key="index"
+            class="skeleton-line"
+            :class="{
+            'short': index === lines - 2,
+            'shorter': index === lines - 1
+          }"
+          :style="{
+            background: `linear-gradient(90deg, ${skeletonColor} 25%, ${skeletonHighlight} 50%, ${skeletonColor} 75%)`,
+            animationDuration: `${animationDuration}s`
+          }">
+        </view>
+      </view>
+    </view>
+
+    <!-- 实际内容 -->
+    <view v-else>
+      <slot></slot>
+    </view>
+
+    <!-- 提示文字和按钮容器 -->
+    <view v-if="showButton" class="button-container">
+      <!-- 提示文字 -->
+      <text 
+        v-if="tipText" 
+        class="tip-text"
+        :style="{
+          color: tipColor,
+          fontSize: tipSize + 'rpx'
+        }"
+      >{{ tipText }}</text>
+      
+      <!-- 按钮 -->
+      <button
+        class="overlay-button"
+        :class="[buttonSize, { 'button-hover': hover }]"
+        hover-class="none"
+        @touchstart="onTouchStart"
+        @touchend="onTouchEnd"
+        @click="handleRefresh"
+        :style="{ backgroundColor: buttonColor }"
+      >
+        {{ buttonText }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+/* 容器样式 */
+.container {
+  position: relative;
+  width: 100%;
+  min-height: 200rpx;
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
+  }
+}
+
+/* 骨架屏样式 */
+.skeleton {
+  width: 100%;
+  height: 100%;
+  padding: 30rpx;
+
+  &-body {
+    margin: 20rpx 0;
+  }
+
+  &-line {
+    height: 32rpx;
+    background-size: 400% 100%;
+    border-radius: 8rpx;
+    margin-bottom: 20rpx;
+    animation: skeleton-loading 1.5s ease infinite;
+
+    &.short {
+      width: 70%;
+    }
+
+    &.shorter {
+      width: 50%;
+    }
+  }
+}
+
+/* 按钮容器样式 */
+.button-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  z-index: 2;
+}
+
+/* 提示文字样式 */
+.tip-text {
+  margin-bottom: 20rpx;
+  text-align: center;
+  line-height: 1.4;
+}
+
+/* 按钮样式 */
+.overlay-button {
+  position: relative; // 改为相对定位
+  transform: none; // 移除transform
+  color: white;
+  border-radius: 50rpx;
+  padding: 0 40rpx;
+  height: 80rpx;
+  line-height: 80rpx;
+  font-size: 28rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+
+  &:active {
+    transform: scale(0.95); // 简化active状态的transform
+  }
+
+  &.small {
+    height: 60rpx;
+    line-height: 60rpx;
+    font-size: 24rpx;
+    padding: 0 30rpx;
+  }
+
+  &.large {
+    height: 100rpx;
+    line-height: 100rpx;
+    font-size: 32rpx;
+    padding: 0 50rpx;
+  }
+}
+
+.button-hover {
+  transform: translateY(-4rpx); // 简化hover状态的transform
+  box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.15);
+}
+
+/* 骨架屏动画 */
+@keyframes skeleton-loading {
+  0% {
+    background-position: 100% 50%;
+  }
+  100% {
+    background-position: 0 50%;
+  }
+}
+</style>

+ 3 - 1
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: ``
 })

+ 0 - 7
pages.json

@@ -259,13 +259,6 @@
               }
             }
           }
-        }, 
-        {
-          "path": "advertise/advertise",
-          "style": {
-            "navigationBarTitleText": "广告页面",
-            "enablePullDownRefresh": false
-          }
         },
         {
           "path": "submit-link/submit-link",

+ 0 - 145
pagesA/advertise/advertise.vue

@@ -1,145 +0,0 @@
-<template>
-    <tm-fullView>
-        <tm-sheet :shadow="24">
-            <tm-alerts label="观看视频即可获取注册码" close></tm-alerts>
-            <tm-divider color="red" model="dashed" :text="codeDataShow?'请复制下方注册码':'请点击“获取注册码”'"></tm-divider>
-            <view class="ma-20" v-show="!codeDataShow">
-                <tm-button theme="bg-gradient-orange-accent" :round="24" block @click="openVideoAd">获取注册码</tm-button>
-            </view>
-            <view class="ma-20" v-show="codeDataShow">
-                <tm-coupon :hdata="codeData" color="orange" @click="fnCopyCode"></tm-coupon>
-            </view>
-        </tm-sheet>
-        <!-- 		<tm-sheet :shadow="24">
-            <tm-images @load="loadimg" src="https://picsum.photos/300?id=7"></tm-images>
-        </tm-sheet> -->
-    </tm-fullView>
-
-</template>
-
-<script>
-let videoAd = null;
-import tmFullView from "@/tm-vuetify/components/tm-fullView/tm-fullView.vue"
-import tmMenubars from "@/tm-vuetify/components/tm-menubars/tm-menubars.vue"
-import tmSheet from "@/tm-vuetify/components/tm-sheet/tm-sheet.vue"
-import tmAlerts from "@/tm-vuetify/components/tm-alerts/tm-alerts.vue"
-import tmDivider from "@/tm-vuetify/components/tm-divider/tm-divider.vue"
-import tmCoupon from '@/tm-vuetify/components/tm-coupon/tm-coupon.vue'
-import tmButton from "@/tm-vuetify/components/tm-button/tm-button.vue"
-import tmImages from "@/tm-vuetify/components/tm-images/tm-images.vue"
-
-export default {
-    components: {
-        tmFullView,
-        tmMenubars,
-        tmSheet,
-        tmAlerts,
-        tmDivider,
-        tmCoupon,
-        tmButton,
-        tmImages
-    },
-    data() {
-        return {
-            adUnitId: '',
-            codeDataShow: false,
-            codeData: {
-                // img: 'https://lywq.muyin.site/logo.png',
-                title: "请获取",
-                btnText: '复制',
-                time: '有效期5分钟',
-                sale: '',
-                saleLable: '注册码',
-                saleSplit: ''
-            }
-        }
-    },
-    computed: {
-        toolsPluginConfigs() {
-            return this.$tm.vx.getters().getConfigs?.pluginConfig?.toolsPlugin || {};
-        }
-    },
-    onLoad(options) {
-        // #ifdef MP-WEIXIN
-        wx.hideShareMenu();
-        this.adLoad();
-        // #endif
-        uni.onCopyUrl((result) => {
-            setTimeout(() => {
-                uni.setClipboardData({
-                    data: "禁止复制哦",
-                })
-            }, 1000);
-        })
-    },
-    methods: {
-        adLoad() {
-            if (wx.createRewardedVideoAd) {
-                videoAd = wx.createRewardedVideoAd({
-                    adUnitId: this.toolsPluginConfigs.rewardedVideoAdId //你的广告key
-                })
-                videoAd.onError(err => {
-                })
-                videoAd.onClose((status) => {
-                    if (status && status.isEnded || status === undefined) {
-                        //这里写广告播放完成后的事件
-                        this.getRegistrationCode();
-                    } else {
-                        // 广告播放未完成
-                    }
-                })
-            }
-        },
-        openVideoAd: function () {
-            if (videoAd && this.toolsPluginConfigs.rewardedVideoAdId !== '') {
-                videoAd.show().catch(err => {
-                    // 失败重试
-                    console.log("广告拉取失败")
-                    videoAd.load().then(() => videoAd.show())
-                })
-            } else {
-                this.getRegistrationCode();
-            }
-        },
-        getRegistrationCode() {
-            uni.showLoading({
-                title: '正在获取...'
-            });
-            this.$httpApi.v2.getPostVerifyCode()
-                .then(res => {
-                    if (res.code === 200) {
-                        uni.$tm.toast('获取成功!');
-                        this.codeData.title = res.data;
-                        this.codeDataShow = true;
-                    } else {
-                        uni.$tm.toast('操作失败,请重试!');
-                    }
-                })
-                .catch(err => {
-                    uni.$tm.toast(err.message);
-                });
-        },
-        fnCopyCode() {
-            uni.setClipboardData({
-                data: this.codeData.title,
-                showToast: false,
-                success: () => {
-                    uni.showToast({
-                        icon: 'none',
-                        title: '复制成功!'
-                    });
-                    setTimeout(() => {
-                        uni.navigateBack()
-                    }, 500);
-                },
-                fail: () => {
-                    uni.showToast({
-                        icon: 'none',
-                        title: '复制失败!'
-                    });
-                }
-            });
-        }
-    }
-}
-</script>

+ 227 - 163
pagesA/article-detail/article-detail.vue

@@ -77,23 +77,46 @@
             <view class="content ml-24 mr-24">
                 <!-- markdown渲染 -->
                 <view class="markdown-wrap">
+                  <view v-if="checkPostRestrictRead(result)">
+                    <view v-if="showContentArr.length == 0">
+                      <restrict-read-skeleton
+                          :loading="true"
+                          :lines="3"
+                          :tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result)}可见」`"
+                          button-text="查看更多"
+                          button-color="#1890ff"
+                          skeleton-color="#f0f0f0"
+                          skeleton-highlight="#e0e0e0"
+                          animation-duration="2"
+                          @refresh="readMore"
+                      />
+                    </view>
+                    <view v-else v-for="showContent in showContentArr">
+                      <mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
+                               :loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
+                               :tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
+                               :content="showContent" :markdown="true" :showLineNumber="true" :showLanguageName="true"
+                               :copyByLongPress="true"/>
+                      <restrict-read-skeleton
+                          :loading="true"
+                          :lines="3"
+                          :tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result)}可见」`"
+                          button-text="查看更多"
+                          button-color="#1890ff"
+                          skeleton-color="#f0f0f0"
+                          skeleton-highlight="#e0e0e0"
+                          animation-duration="2"
+                          @refresh="readMore"
+                      />
+                    </view>
+                  </view>
+                  <view v-else>
                     <mp-html class="evan-markdown" lazy-load :domain="markdownConfig.domain"
                              :loading-img="markdownConfig.loadingGif" :scroll-table="true" :selectable="true"
                              :tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
                              :content="result.content.raw" :markdown="true" :showLineNumber="true" :showLanguageName="true"
                              :copyByLongPress="true"/>
-                    <tm-more v-if="showValidVisitMore" :disabled="true" :maxHeight="1" :isRemovBar="true"
-                             @click="showValidVisitMorePop()">
-                        <view class="text-size-n pa-24">
-                            以下内容已隐藏,请验证后查看完整内容……
-                        </view>
-                        <template v-slot:more="{ data }">
-                            <view class="">
-                                <text class="text-blue text-size-m text-weight-b">文章部分内容已加密点击解锁</text>
-                                <text class="text-blue iconfont icon-lock-fill text-size-s ml-5"></text>
-                            </view>
-                        </template>
-                    </tm-more>
+                  </view>
                 </view>
 
                 <!-- 版权声明 -->
@@ -223,14 +246,41 @@
             </view>
         </tm-poup>
 
-        <!-- 密码访问解密弹窗 -->
-        <tm-dialog :disabled="true" :input-val.sync="validVisitModal.value" title="验证提示" confirmText="立即验证"
-                   :showCancel="validVisitModal.useCancel" model="confirm" v-model="validVisitModal.show"
-                   content="博主设置了需要密码才能查看该文章内容,请输入文章密码进行验证" theme="split" confirmColor="blue shadow-blue-0"
-                   @cancel="closeValidVisitPop" @confirm="fnValidVisitPwd"></tm-dialog>
-        <!-- 是否前往获取验证码 -->
-        <tm-dialog v-model="showGetPassword" title="免费获取验证码" content="是否前往获取验证码?" @confirm="toAdvertise"
-                   @cancel="closeAllPop"></tm-dialog>
+        <!-- 验证码弹窗 -->
+        <tm-dialog v-model="verificationCodeModal.show" :disabled="true" title="验证提示"
+                   :confirmText="verificationCodeModal.confirmText"
+                   :showCancel="true" model="verificationCodeModal.model" theme="split" confirmColor="blue shadow-blue-0"
+                   @cancel="verificationCodeModal.show = false"
+                   @confirm="restrictReadCheckOrViewVideo">
+          <template #default>
+            <view class="pa-20">
+              <!-- 扫码验证模式 -->
+              <view v-if="verificationCodeModal.type === 'scan'" class="flex flex-col flex-center">
+                <text class="mb-20">请扫描下方二维码获取验证码</text>
+                <tm-images
+                    :width="180"
+                    :height="180"
+                    :src="verificationCodeModal.imgUrl"
+                    preview
+                    round="5"
+                ></tm-images>
+                <tm-input bg-color="grey-lighten-5" required v-model="restrictReadInputCode" placeholder="请输入验证码"
+                          :border-bottom="false" :flat="true"></tm-input>
+              </view>
+
+              <!-- 观看视频模式 -->
+              <view v-else class="flex flex-col flex-center">
+                <text class="mb-20">点击观看视频之后,可访问</text>
+              </view>
+            </view>
+          </template>
+        </tm-dialog>
+        <!-- 密码弹窗 -->
+        <tm-dialog v-model="passwordModal.show" :disabled="true" title="验证提示" confirmText="确定" content="请输入密码"
+                   :showCancel="true" model="confirm" theme="split" confirmColor="blue shadow-blue-0"
+                   :input-val.sync="restrictReadInputCode"
+                   @cancel="passwordModal.show = false"
+                   @confirm="restrictReadCheck"></tm-dialog>
 
         <!-- 评论弹窗 -->
         <block v-if="calcIsShowComment">
@@ -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;
-
-                    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;
+                    const restrictRead = checkPostRestrictRead(this.result);
+
+                    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('<!-- restrictRead start -->')[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;

+ 2 - 2
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 => {

+ 0 - 1
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 = {}

+ 140 - 0
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 = `<!-- ${restrictType}:restrict-read-html-tpl start -->`;
+    const endTag = `<!-- ${restrictType}:restrict-read-html-tpl end -->`;
+
+    // 使用正则模糊匹配(允许前后有空白字符)
+    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 = `<!-- ${restrictType}:restrict-read-html-tpl start -->`;
+    const endTag = `<!-- ${restrictType}:restrict-read-html-tpl end -->`;
+
+    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<void>}
+ */
+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();
+}