Explorar el Código

feat: 添加隐私政策同意管理功能

在多个组件中集成了隐私政策同意管理,新增了相关方法以处理用户同意状态。更新了应用启动逻辑以支持隐私授权请求,并在必要时显示隐私政策弹窗。同时,优化了登录成功后的活动处理逻辑,确保在用户未同意政策时阻止营销弹窗的显示。
ylong hace 2 semanas
padre
commit
c40e5914cc

+ 68 - 52
App.vue

@@ -1,5 +1,7 @@
 <script>
 import { eventBus } from '@/utils/event-bus'
+import { isPolicyConsented, requestShowPrivacyPopup } from '@/utils/policy-consent-manager.js'
+
 export default {
 	globalData: {
 		// 胶囊距上距离
@@ -27,62 +29,15 @@ export default {
 
 		// 是否是冷启动
 		isColdLaunch: false,
+		needPolicyConsent: false,
+		needPrivacyPopup: false,
+		pendingLaunchOptions: null,
 	},
 	onLaunch(options) {
 		this.globalData.isColdLaunch = true;
 		console.log('冷启动onLaunch:', this.globalData.isColdLaunch)
-		let params = {};
-		if (options.query) {
-			//微信小程序的场景值解析
-			if (options.query.scene) {
-				// 对 scene 进行解码
-				const decodeScene = decodeURIComponent(options.query.scene);
-				// 拆分参数
-				const paramPairs = decodeScene.split('&');
-				paramPairs.forEach((pair) => {
-					const [key, value] = pair.split('=');
-					params[key] = value;
-				});
-
-				let keys = Object.keys(params)
-				if (keys.length > 0) {
-					console.log(params, "params");
-					// 如果链接参数中有一个名为 upsellCode 的参数
-					if (params.upsellCode) {
-						uni.setStorageSync('upsellCode', params.upsellCode)
-					}
-					//减价助力参数 reduceCode
-					if (params.reduceCode) {
-						uni.setStorageSync('reduceCode', params.reduceCode)
-					}
-
-					// 兼容商品详情二维码跳转
-					if (params.isbn) {
-						// 将 isbn 存入全局或缓存,以便 detail 页面如果在 onLoad 没拿到时可以使用
-						uni.setStorageSync('scene_isbn', params.isbn);
-					}
-					// 兼容红包二维码跳转
-					if (params.bianhao) {
-						uni.setStorageSync('scene_bianhao', params.bianhao);
-					}
-					this.slientLogin(params);
-				} else {
-					this.slientLogin();
-				}
-			} else {
-				this.slientLogin(options.query);
-				// 如果链接参数中有一个名为 upsellCode 的参数
-				if (options.query.upsellCode) {
-					uni.setStorageSync('upsellCode', options.query.upsellCode)
-				}
-				//减价助力参数 reduceCode
-				if (options.query.reduceCode) {
-					uni.setStorageSync('reduceCode', options.query.reduceCode)
-				}
-			}
-		} else {
-			this.slientLogin()
-		}
+		this.initPrivacyAuthorization()
+		this.handleLaunchWithPolicy(options)
 	},
 	onShow(options) {
 		this.$updateCartBadge();
@@ -92,6 +47,67 @@ export default {
 		uni.removeStorageSync('loginType')
 	},
 	methods: {
+		initPrivacyAuthorization() {
+			// #ifdef MP-WEIXIN
+			if (typeof wx !== 'undefined' && wx.onNeedPrivacyAuthorization) {
+				wx.onNeedPrivacyAuthorization((resolve) => {
+					requestShowPrivacyPopup(resolve)
+				})
+			}
+			// #endif
+		},
+		handleLaunchWithPolicy(options = {}) {
+			this.globalData.pendingLaunchOptions = options
+			if (!isPolicyConsented()) {
+				this.globalData.needPolicyConsent = true
+				return
+			}
+			this.runPendingLaunchLogin()
+		},
+		runPendingLaunchLogin() {
+			const options = this.globalData.pendingLaunchOptions || {}
+			let params = {};
+			if (options.query) {
+				if (options.query.scene) {
+					const decodeScene = decodeURIComponent(options.query.scene);
+					const paramPairs = decodeScene.split('&');
+					paramPairs.forEach((pair) => {
+						const [key, value] = pair.split('=');
+						params[key] = value;
+					});
+
+					let keys = Object.keys(params)
+					if (keys.length > 0) {
+						console.log(params, "params");
+						if (params.upsellCode) {
+							uni.setStorageSync('upsellCode', params.upsellCode)
+						}
+						if (params.reduceCode) {
+							uni.setStorageSync('reduceCode', params.reduceCode)
+						}
+						if (params.isbn) {
+							uni.setStorageSync('scene_isbn', params.isbn);
+						}
+						if (params.bianhao) {
+							uni.setStorageSync('scene_bianhao', params.bianhao);
+						}
+						this.slientLogin(params);
+					} else {
+						this.slientLogin();
+					}
+				} else {
+					this.slientLogin(options.query);
+					if (options.query.upsellCode) {
+						uni.setStorageSync('upsellCode', options.query.upsellCode)
+					}
+					if (options.query.reduceCode) {
+						uni.setStorageSync('reduceCode', options.query.reduceCode)
+					}
+				}
+			} else {
+				this.slientLogin()
+			}
+		},
 		parseEntryOptions(options = {}) {
 			let params = {};
 			if (!options || !options.query) return params;

+ 65 - 1
components/activity-host.vue

@@ -9,6 +9,7 @@
 import UpsellShare from "@/pages/home/components/upsell-share.vue";
 import ReduceShare from "@/pages/home/components/reduce-share.vue";
 import { eventBus } from "@/utils/event-bus";
+import { shouldBlockMarketingPopup } from "@/utils/policy-consent-manager.js";
 
 export default {
     name: "ActivityHost",
@@ -30,27 +31,43 @@ export default {
         return {
             loginSuccessHandler: null,
             appShowActivityHandler: null,
+            policyAcceptedHandler: null,
+            pendingLoginData: null,
+            pendingPageOptions: null,
         };
     },
     mounted() {
         this.bindLoginSuccess();
         this.bindAppShowActivity();
+        this.bindPolicyAccepted();
         this.tryOpenPendingActivity();
     },
     beforeDestroy() {
         this.unbindLoginSuccess();
         this.unbindAppShowActivity();
+        this.unbindPolicyAccepted();
     },
     methods: {
         bindLoginSuccess() {
             if (this.loginSuccessHandler) return;
 
             this.loginSuccessHandler = (data) => {
-                this.openByLoginSuccess(data);
+                if (shouldBlockMarketingPopup()) {
+                    this.pendingLoginData = data || null;
+                    return;
+                }
                 const app = getApp();
                 if (app && app.globalData) {
                     app.globalData.isColdLaunch = false;
                 }
+                // 优先处理带参数的入口(如分享减价链接),避免与 storage 重复弹窗
+                if (this.pendingPageOptions) {
+                    const options = this.pendingPageOptions;
+                    this.pendingPageOptions = null;
+                    this.openByPageOptions(options);
+                    return;
+                }
+                this.openByLoginSuccess(data);
             };
             eventBus.on("loginSuccess", this.loginSuccessHandler);
         },
@@ -58,6 +75,10 @@ export default {
             if (this.appShowActivityHandler) return;
 
             this.appShowActivityHandler = (data) => {
+                if (shouldBlockMarketingPopup()) {
+                    this.pendingLoginData = data || null;
+                    return;
+                }
                 this.openByLoginSuccess(data);
             };
             eventBus.on("appShowActivity", this.appShowActivityHandler);
@@ -72,7 +93,21 @@ export default {
             eventBus.off("appShowActivity", this.appShowActivityHandler);
             this.appShowActivityHandler = null;
         },
+        bindPolicyAccepted() {
+            if (this.policyAcceptedHandler) return;
+            this.policyAcceptedHandler = () => {
+                this.flushPendingMarketingPopups();
+            };
+            eventBus.on("policyAccepted", this.policyAcceptedHandler);
+        },
+        unbindPolicyAccepted() {
+            if (!this.policyAcceptedHandler) return;
+            eventBus.off("policyAccepted", this.policyAcceptedHandler);
+            this.policyAcceptedHandler = null;
+        },
         tryOpenPendingActivity() {
+            if (shouldBlockMarketingPopup()) return;
+
             const token = uni.getStorageSync("token");
             const upsellCode = uni.getStorageSync("upsellCode");
             const reduceCode = uni.getStorageSync("reduceCode");
@@ -81,6 +116,11 @@ export default {
             this.openByLoginSuccess();
         },
         openByLoginSuccess(data) {
+            if (shouldBlockMarketingPopup()) {
+                this.pendingLoginData = data || null;
+                return;
+            }
+
             const upsellCode = uni.getStorageSync("upsellCode") || data?.params?.upsellCode;
             const reduceCode = uni.getStorageSync("reduceCode") || data?.params?.reduceCode;
 
@@ -95,6 +135,14 @@ export default {
         handlePageOptions(options = {}, isColdLaunch = false) {
             if (isColdLaunch) return;
 
+            if (shouldBlockMarketingPopup()) {
+                this.pendingPageOptions = options;
+                return;
+            }
+
+            this.openByPageOptions(options);
+        },
+        openByPageOptions(options = {}) {
             const params = this.parseEntryParams(options);
             if (this.enableUpsell && params.upsellCode) {
                 this.$refs.shareRef?.open(params.upsellCode);
@@ -107,6 +155,22 @@ export default {
             }
             this.$emit("refresh-order");
         },
+        flushPendingMarketingPopups() {
+            if (shouldBlockMarketingPopup()) return;
+
+            if (this.pendingPageOptions) {
+                const options = this.pendingPageOptions;
+                this.pendingPageOptions = null;
+                this.openByPageOptions(options);
+                return;
+            }
+
+            const token = uni.getStorageSync("token");
+            if (!token) return;
+
+            this.openByLoginSuccess(this.pendingLoginData);
+            this.pendingLoginData = null;
+        },
         parseEntryParams(options = {}) {
             if (!options.scene) return options;
 

+ 65 - 0
components/policy-consent-host.vue

@@ -0,0 +1,65 @@
+<template>
+    <view class="policy-consent-host">
+        <policy-consent-modal ref="policyModal" @accepted="onPolicyAccepted" />
+        <!-- #ifdef MP-WEIXIN -->
+        <privacy-authorize-popup ref="privacyPopup" />
+        <!-- #endif -->
+    </view>
+</template>
+
+<script>
+import PolicyConsentModal from '@/components/policy-consent-modal.vue'
+import { eventBus } from '@/utils/event-bus'
+import {
+    registerPolicyHost,
+    setPolicyConsented,
+    unregisterPolicyHost,
+} from '@/utils/policy-consent-manager.js'
+
+// #ifdef MP-WEIXIN
+import PrivacyAuthorizePopup from '@/components/privacy-authorize-popup.vue'
+// #endif
+
+export default {
+    components: {
+        PolicyConsentModal,
+        // #ifdef MP-WEIXIN
+        PrivacyAuthorizePopup,
+        // #endif
+    },
+    mounted() {
+        registerPolicyHost(this)
+        this.tryOpenPolicyModal()
+    },
+    beforeDestroy() {
+        unregisterPolicyHost(this)
+    },
+    methods: {
+        tryOpenPolicyModal() {
+            const app = getApp()
+            if (!app || !app.globalData || !app.globalData.needPolicyConsent) return
+            this.$nextTick(() => {
+                this.$refs.policyModal && this.$refs.policyModal.open()
+            })
+        },
+        tryOpenPrivacyPopup() {
+            // #ifdef MP-WEIXIN
+            this.$nextTick(() => {
+                this.$refs.privacyPopup && this.$refs.privacyPopup.open()
+            })
+            // #endif
+        },
+        onPolicyAccepted() {
+            setPolicyConsented()
+            const app = getApp()
+            if (app && app.globalData) {
+                app.globalData.needPolicyConsent = false
+            }
+            eventBus.emit('policyAccepted')
+            if (app && typeof app.runPendingLaunchLogin === 'function') {
+                app.runPendingLaunchLogin()
+            }
+        },
+    },
+}
+</script>

+ 156 - 0
components/policy-consent-modal.vue

@@ -0,0 +1,156 @@
+<template>
+    <u-popup
+        v-model="visible"
+        mode="center"
+        border-radius="20"
+        width="620rpx"
+        :mask-close-able="false"
+        :z-index="policyZIndex"
+    >
+        <view class="policy-modal">
+            <view class="policy-modal__title">服务协议与隐私政策</view>
+            <view class="policy-modal__desc">
+                请你务必审慎阅读、充分理解相关协议。当你点击同意并开始使用本产品服务时,即表示你已阅读并同意下列条款。
+            </view>
+            <view class="policy-modal__links">
+                <text>我已阅读并同意</text>
+                <text class="policy-modal__link" @click.stop="openArticle('userAgreement')">《用户协议》</text>
+                <text>和</text>
+                <text class="policy-modal__link" @click.stop="openArticle('privacyPolicy')">《隐私政策》</text>
+            </view>
+            <u-checkbox v-model="checked" shape="circle" active-color="#38C148" class="policy-modal__checkbox">
+                <text class="policy-modal__checkbox-text">我已阅读并同意上述协议</text>
+            </u-checkbox>
+            <view class="policy-modal__footer">
+                <button class="policy-modal__btn policy-modal__btn--cancel" @click="handleDisagree">不同意</button>
+                <button class="policy-modal__btn policy-modal__btn--confirm" @click="handleAgree">同意并继续</button>
+            </view>
+        </view>
+    </u-popup>
+</template>
+
+<script>
+import { ARTICLE_CODE, ARTICLE_TITLE, POLICY_POPUP_Z_INDEX } from '@/utils/policy-config.js'
+
+export default {
+    data() {
+        return {
+            visible: false,
+            checked: false,
+            policyZIndex: POLICY_POPUP_Z_INDEX,
+        }
+    },
+    methods: {
+        open() {
+            this.checked = false
+            this.visible = true
+        },
+        close() {
+            this.visible = false
+        },
+        openArticle(type) {
+            const code = ARTICLE_CODE[type] || ARTICLE_CODE.orderAgreement
+            const title = ARTICLE_TITLE[type] || '协议'
+            uni.navigateTo({
+                url: `/pages-home/pages/user-agreement?code=${code}&title=${encodeURIComponent(title)}`,
+            })
+        },
+        handleAgree() {
+            if (!this.checked) {
+                uni.$u.toast('请先阅读并勾选同意协议')
+                return
+            }
+            this.$emit('accepted')
+            this.close()
+        },
+        handleDisagree() {
+            // #ifdef MP-WEIXIN
+            if (typeof wx !== 'undefined' && wx.exitMiniProgram) {
+                wx.exitMiniProgram()
+                return
+            }
+            // #endif
+            // #ifdef MP-ALIPAY
+            if (typeof my !== 'undefined' && my.exitMiniProgram) {
+                my.exitMiniProgram()
+                return
+            }
+            // #endif
+            uni.$u.toast('需同意协议后方可使用')
+        },
+    },
+}
+</script>
+
+<style lang="scss" scoped>
+.policy-modal {
+    padding: 40rpx 50rpx;
+    background: #fff;
+
+    &__title {
+        font-size: 34rpx;
+        font-weight: 600;
+        color: #333;
+        text-align: center;
+        margin-bottom: 24rpx;
+    }
+
+    &__desc {
+        font-size: 26rpx;
+        color: #666;
+        line-height: 1.7;
+        margin-bottom: 24rpx;
+    }
+
+    &__links {
+        font-size: 26rpx;
+        color: #666;
+        line-height: 1.8;
+        margin-bottom: 20rpx;
+    }
+
+    &__link {
+        color: #38c148;
+    }
+
+    &__checkbox {
+        margin-bottom: 32rpx;
+    }
+
+    &__checkbox-text {
+        font-size: 26rpx;
+        color: #333;
+    }
+
+    &__footer {
+        display: flex;
+        gap: 20rpx;
+        margin-top: 20rpx;
+    }
+
+    &__btn {
+        flex: 1;
+        height: 80rpx;
+        line-height: 80rpx;
+        font-size: 28rpx;
+        border-radius: 10rpx;
+        border: none;
+        margin: 0;
+        padding: 0;
+
+        &::after {
+            border: none;
+        }
+
+        &--cancel {
+            background: #f1f1f1;
+            color: #666;
+        }
+
+        &--confirm {
+            background: #38c148;
+            color: #fff;
+        }
+    }
+}
+</style>

+ 140 - 0
components/privacy-authorize-popup.vue

@@ -0,0 +1,140 @@
+<template>
+    <u-popup
+        v-model="visible"
+        mode="center"
+        border-radius="20"
+        width="620rpx"
+        :mask-close-able="false"
+        :z-index="privacyZIndex"
+    >
+        <view class="privacy-modal">
+            <view class="privacy-modal__title">隐私保护提示</view>
+            <view class="privacy-modal__desc">
+                在使用相关功能前,请你阅读并同意
+                <text class="privacy-modal__link" @click="openPrivacyContract">《隐私保护指引》</text>
+                。我们将严格按照指引处理你的个人信息。
+            </view>
+            <view class="privacy-modal__footer">
+                <button class="privacy-modal__btn privacy-modal__btn--cancel" @click="handleDisagree">拒绝</button>
+                <button
+                    id="agree-privacy-btn"
+                    class="privacy-modal__btn privacy-modal__btn--confirm"
+                    open-type="agreePrivacyAuthorization"
+                    @agreeprivacyauthorization="handleAgree"
+                >
+                    同意
+                </button>
+            </view>
+        </view>
+    </u-popup>
+</template>
+
+<script>
+import {
+    clearPrivacyPopupFlag,
+    clearPrivacyResolve,
+    getPrivacyResolve,
+} from '@/utils/policy-consent-manager.js'
+import { PRIVACY_POPUP_Z_INDEX } from '@/utils/policy-config.js'
+
+export default {
+    data() {
+        return {
+            visible: false,
+            privacyZIndex: PRIVACY_POPUP_Z_INDEX,
+        }
+    },
+    methods: {
+        open() {
+            this.visible = true
+        },
+        close() {
+            this.visible = false
+            clearPrivacyPopupFlag()
+        },
+        openPrivacyContract() {
+            // #ifdef MP-WEIXIN
+            if (typeof wx !== 'undefined' && wx.openPrivacyContract) {
+                wx.openPrivacyContract({
+                    fail: () => {
+                        uni.$u.toast('暂无法打开隐私指引')
+                    },
+                })
+            }
+            // #endif
+        },
+        handleAgree() {
+            const resolve = getPrivacyResolve()
+            if (resolve) {
+                resolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
+            }
+            clearPrivacyResolve()
+            this.close()
+        },
+        handleDisagree() {
+            const resolve = getPrivacyResolve()
+            if (resolve) {
+                resolve({ event: 'disagree' })
+            }
+            clearPrivacyResolve()
+            this.close()
+        },
+    },
+}
+</script>
+
+<style lang="scss" scoped>
+.privacy-modal {
+    padding: 40rpx 36rpx 32rpx;
+    background: #fff;
+
+    &__title {
+        font-size: 34rpx;
+        font-weight: 600;
+        color: #333;
+        text-align: center;
+        margin-bottom: 24rpx;
+    }
+
+    &__desc {
+        font-size: 26rpx;
+        color: #666;
+        line-height: 1.7;
+        margin-bottom: 32rpx;
+    }
+
+    &__link {
+        color: #38c148;
+    }
+
+    &__footer {
+        display: flex;
+        gap: 20rpx;
+    }
+
+    &__btn {
+        flex: 1;
+        height: 80rpx;
+        line-height: 80rpx;
+        font-size: 28rpx;
+        border-radius: 10rpx;
+        border: none;
+        margin: 0;
+        padding: 0;
+
+        &::after {
+            border: none;
+        }
+
+        &--cancel {
+            background: #f1f1f1;
+            color: #666;
+        }
+
+        &--confirm {
+            background: #38c148;
+            color: #fff;
+        }
+    }
+}
+</style>

+ 21 - 6
pages-home/pages/user-agreement.vue

@@ -8,28 +8,43 @@
 </template>
 
 <script>
+import { ARTICLE_CODE, ARTICLE_TITLE } from '@/utils/policy-config.js'
+
 export default {
     data() {
         return {
+            articleCode: ARTICLE_CODE.orderAgreement,
             agreementData: {
                 title: "",
                 content: "",
             },
         };
     },
-    onLoad() {
-        this.getOrderAgreement();
+    onLoad(options) {
+        const code = options.code || ARTICLE_CODE.orderAgreement
+        let title = ARTICLE_TITLE.orderAgreement
+        if (options.title) {
+            title = decodeURIComponent(options.title)
+        } else {
+            Object.keys(ARTICLE_CODE).forEach((key) => {
+                if (ARTICLE_CODE[key] === code && ARTICLE_TITLE[key]) {
+                    title = ARTICLE_TITLE[key]
+                }
+            })
+        }
+
+        this.articleCode = code
+        uni.setNavigationBarTitle({ title })
+        this.fetchAgreement()
     },
     methods: {
-        // 获取下单协议
-        getOrderAgreement() {
+        fetchAgreement() {
             uni.showLoading({
                 title: "加载中...",
             });
             uni.$u.http
-                .get("/token/getArticleOne?code=orderAgreement")
+                .get(`/token/getArticleOne?code=${this.articleCode}`)
                 .then((res) => {
-                    console.log(res);
                     if (res.code === 200) {
                         this.agreementData = res.data;
                     } else {

+ 6 - 0
pages/cart/index.vue

@@ -1,11 +1,17 @@
 <template>
     <view>
         <cart-container ref="cartContainer"></cart-container>
+        <policy-consent-host />
     </view>
 </template>
 
 <script>
+import PolicyConsentHost from '@/components/policy-consent-host.vue'
+
     export default {
+        components: {
+            PolicyConsentHost,
+        },
         data() {
             return {
 

+ 3 - 0
pages/home/index.vue

@@ -119,6 +119,7 @@
         <!-- 仅承接加价分享 -->
         <ActivityHost ref="activityHost" :enable-reduce="false" @refresh-order="getLastOrder"
             @view-rules="handleViewSellRules" />
+        <policy-consent-host />
     </view>
 </template>
 
@@ -135,6 +136,7 @@
     import UpsellRules from "./components/upsell-rules.vue";
     import UpsellQrcode from "./components/upsell-qrcode.vue";
     import ActivityHost from "@/components/activity-host.vue";
+    import PolicyConsentHost from "@/components/policy-consent-host.vue";
     import FloatingDrag from "@/components/floating-drag.vue";
 
     const app = getApp();
@@ -153,6 +155,7 @@ export default {
         UpsellRules,
         UpsellQrcode,
         ActivityHost,
+        PolicyConsentHost,
         FloatingDrag,
     },
     data() {

+ 15 - 1
pages/mine/index.vue

@@ -114,6 +114,7 @@
 
 		<!-- 提现进度弹窗 -->
 		<withdrawal-progress :orderInfo="currentWithdrawalOrder" @confirm="confirmWithdrawal" ref="withdrawalRef" />
+		<policy-consent-host />
 	</view>
 </template>
 
@@ -121,12 +122,15 @@
 	import WithdrawalProgress from './components/withdrawal-progress.vue';
 	import WithdrawalConfirm from '../../components/withdrawal-confirm.vue';
 	import floatingActivity from '../../components/floating-activity.vue';
+	import PolicyConsentHost from '@/components/policy-consent-host.vue';
+	import { buildAgreementPageUrl } from '@/utils/policy-config.js';
 
 	export default {
 		components: {
 			WithdrawalProgress,
 			WithdrawalConfirm,
-			floatingActivity
+			floatingActivity,
+			PolicyConsentHost,
 		},
 		data() {
 			return {
@@ -282,6 +286,16 @@
 						icon: 'https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/t11.png',
 						path: '/pages-home/pages/about-us'
 					},
+					{
+						name: '用户协议',
+						icon: 'https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/t11.png',
+						path: buildAgreementPageUrl('userAgreement')
+					},
+					{
+						name: '隐私政策',
+						icon: 'https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/t10.png',
+						path: buildAgreementPageUrl('privacyPolicy')
+					},
 					{
 						name: '我的余额',
 						icon: 'https://shuhi.oss-cn-qingdao.aliyuncs.com/mini/t12.png',

+ 4 - 1
pages/sell/index.vue

@@ -2,15 +2,18 @@
 	<view>
 		<sell-container ref="sellContainer"></sell-container>
 		<ActivityHost ref="activityHost" :enable-upsell="false"></ActivityHost>
+		<policy-consent-host />
 	</view>
 </template>
 
 <script>
 import ActivityHost from "@/components/activity-host.vue";
+import PolicyConsentHost from "@/components/policy-consent-host.vue";
 
 export default {
 	components: {
-		ActivityHost
+		ActivityHost,
+		PolicyConsentHost,
 	},
 	data() {
 		return {

+ 28 - 0
utils/policy-config.js

@@ -0,0 +1,28 @@
+/** 业务协议同意版本,协议内容改版时递增以触发重新弹窗 */
+export const POLICY_CONSENT_VERSION = '1'
+
+export const STORAGE_KEY_POLICY_CONSENT = 'policyConsentVersion'
+
+/** 弹窗层级:协议/隐私高于营销活动(分享减价等) */
+export const POLICY_POPUP_Z_INDEX = 10100
+export const PRIVACY_POPUP_Z_INDEX = 10110
+
+/** CMS 文章 code */
+export const ARTICLE_CODE = {
+    userAgreement: 'userAgreement',
+    privacyPolicy: 'privacyPolicy',
+    orderAgreement: 'orderAgreement',
+}
+
+export const ARTICLE_TITLE = {
+    userAgreement: '用户协议',
+    privacyPolicy: '隐私政策',
+    orderAgreement: '书嗨用户协议',
+}
+
+/** 协议展示页路径(含 code、title 参数) */
+export function buildAgreementPageUrl(type) {
+    const code = ARTICLE_CODE[type] || ARTICLE_CODE.orderAgreement
+    const title = ARTICLE_TITLE[type] || ARTICLE_TITLE.orderAgreement
+    return `/pages-home/pages/user-agreement?code=${code}&title=${encodeURIComponent(title)}`
+}

+ 77 - 0
utils/policy-consent-manager.js

@@ -0,0 +1,77 @@
+import {
+    POLICY_CONSENT_VERSION,
+    STORAGE_KEY_POLICY_CONSENT,
+} from '@/utils/policy-config.js'
+
+let policyHostVm = null
+let privacyResolve = null
+
+export function isPolicyConsented() {
+    return uni.getStorageSync(STORAGE_KEY_POLICY_CONSENT) === POLICY_CONSENT_VERSION
+}
+
+/** 未同意业务协议前,禁止展示分享减价/加价等活动弹窗 */
+export function shouldBlockMarketingPopup() {
+    const app = getApp()
+    if (app && app.globalData && app.globalData.needPolicyConsent) {
+        return true
+    }
+    return !isPolicyConsented()
+}
+
+export function setPolicyConsented() {
+    uni.setStorageSync(STORAGE_KEY_POLICY_CONSENT, POLICY_CONSENT_VERSION)
+}
+
+export function registerPolicyHost(vm) {
+    policyHostVm = vm
+    const app = getApp()
+    if (app && app.globalData && app.globalData.needPolicyConsent) {
+        vm.tryOpenPolicyModal()
+    }
+    if (app && app.globalData && app.globalData.needPrivacyPopup) {
+        vm.tryOpenPrivacyPopup()
+    }
+}
+
+export function unregisterPolicyHost(vm) {
+    if (policyHostVm === vm) {
+        policyHostVm = null
+    }
+}
+
+export function requestShowPolicyModal() {
+    const app = getApp()
+    if (app && app.globalData) {
+        app.globalData.needPolicyConsent = true
+    }
+    if (policyHostVm) {
+        policyHostVm.tryOpenPolicyModal()
+    }
+}
+
+export function requestShowPrivacyPopup(resolve) {
+    privacyResolve = resolve
+    const app = getApp()
+    if (app && app.globalData) {
+        app.globalData.needPrivacyPopup = true
+    }
+    if (policyHostVm) {
+        policyHostVm.tryOpenPrivacyPopup()
+    }
+}
+
+export function getPrivacyResolve() {
+    return privacyResolve
+}
+
+export function clearPrivacyResolve() {
+    privacyResolve = null
+}
+
+export function clearPrivacyPopupFlag() {
+    const app = getApp()
+    if (app && app.globalData) {
+        app.globalData.needPrivacyPopup = false
+    }
+}