hookehuyr

feat(privacy): 添加隐私政策弹窗和常量配置

- 新增 PrivacyPopup 组件,支持多层级标题和内容展示
- 新增 privacy.js 常量文件,统一管理隐私政策内容
- 更新 search 和 visitorList 页面,集成隐私弹窗
- 更新项目配置文件(components.d.ts, config/index.js, jsconfig.json)
- 更新 CLAUDE.md 和 CHANGELOG.md 文档

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -38,6 +38,7 @@ pnpm build:h5 # 构建H5端生产包
- `@/api``src/api`
- `@/stores``src/stores`
- `@/hooks``src/hooks`
- `@/constants``src/constants`
### 认证与授权流程
......
......@@ -18,6 +18,7 @@ declare module 'vue' {
OfflineQrCode: typeof import('./src/components/offlineQrCode.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
PrivacyPopup: typeof import('./src/components/PrivacyPopup.vue')['default']
QrCode: typeof import('./src/components/qrCode.vue')['default']
QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default']
ReserveCard: typeof import('./src/components/reserveCard.vue')['default']
......
......@@ -36,6 +36,7 @@ export default defineConfig(async (merge) => {
"@/api": path.resolve(__dirname, "../src/api"),
"@/stores": path.resolve(__dirname, "../src/stores"),
"@/hooks": path.resolve(__dirname, "../src/hooks"),
"@/constants": path.resolve(__dirname, "../src/constants"),
},
sourceRoot: 'src',
outputRoot: 'dist',
......
# CHANGELOG
## [Unreleased]
- feat: 新增隐私政策和用户服务协议确认弹窗
- 新增 `PrivacyPopup` 组件
- 新增 `src/constants/privacy.js` Mock 数据
-`search` (查询页) 和 `visitorList` (参观者列表页) 增加首次进入时的隐私协议弹窗逻辑
- 用户需同意协议才能继续使用,否则返回上一页
- 修改 verificationResult 页面,将空的 view 替换为 photograph 图标
- 调整 verificationResult 页面 photograph 图标大小为 52rpx 并设置水平居中
......
......@@ -10,7 +10,8 @@
"@/composables/*": ["src/composables/*"],
"@/api/*": ["src/api/*"],
"@/stores/*": ["src/stores/*"],
"@/hooks/*": ["src/hooks/*"]
"@/hooks/*": ["src/hooks/*"],
"@/constants/*": ["src/constants/*"]
}
},
"include": ["src/**/*", "config/**/*"]
......
<!--
* @Date: 2026-02-02 15:58:03
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-02-02 16:18:17
* @FilePath: /xyxBooking-weapp/src/components/PrivacyPopup.vue
* @Description: 隐私协议弹窗组件
-->
<template>
<nut-popup
:visible="visible"
:close-on-click-overlay="false"
round
:style="{ width: '80%', padding: '0' }"
>
<view class="bg-white rounded-[24rpx] overflow-hidden">
<view class="pt-[48rpx] pb-[32rpx] text-center">
<text class="text-[36rpx] font-semibold text-[#333]">温馨提示</text>
</view>
<view class="px-[48rpx] pb-[48rpx] text-[30rpx] text-[#666] leading-relaxed text-center">
<text>请您在使用前仔细阅读并同意</text>
<text class="text-[#A67939] inline" @tap="showAgreement('user')">《用户服务协议》</text>
<text>和</text>
<text class="text-[#A67939] inline" @tap="showAgreement('privacy')">《隐私政策》</text>
</view>
<view class="flex border-t border-[#eee]">
<view class="flex-1 h-[96rpx] leading-[96rpx] text-center text-[32rpx] text-[#999] border-r border-[#eee] active:bg-gray-50" @tap="handleDisagree">不同意</view>
<view class="flex-1 h-[96rpx] leading-[96rpx] text-center text-[32rpx] font-medium text-[#A67939] active:bg-gray-50" @tap="handleAgree">同意</view>
</view>
</view>
</nut-popup>
<!-- 协议详情弹窗 -->
<nut-popup
:visible="detailVisible"
position="bottom"
round
:style="{ height: '80%' }"
@update:visible="val => detailVisible = val"
>
<view class="flex flex-col h-full bg-white relative overflow-hidden">
<!-- 标题栏 -->
<view class="flex-none flex items-center justify-center h-[96rpx] border-b border-[#eee] relative">
<text class="text-[32rpx] font-semibold text-[#333]">{{ detailTitle }}</text>
<view
class="absolute right-[32rpx] top-0 h-[96rpx] flex items-center justify-center text-[#999] text-[40rpx] px-[20rpx]"
@tap="detailVisible = false"
>×</view>
</view>
<!-- 内容区域 -->
<scroll-view :scroll-y="true" class="flex-1 h-0 px-[32rpx] py-[24rpx] box-border w-full">
<view v-html="detailContent"></view>
</scroll-view>
<!-- 底部确认按钮 -->
<view class="flex-none p-[32rpx] border-t border-[#eee] bg-white">
<button
class="w-full h-[88rpx] leading-[88rpx] bg-[#A67939] text-white text-[32rpx] rounded-[44rpx] active:opacity-90"
@tap="detailVisible = false"
>
我已阅读
</button>
</view>
</view>
</nut-popup>
</template>
<script setup>
import { ref } from 'vue'
import { USER_AGREEMENT, PRIVACY_POLICY } from '@/constants/privacy'
import Taro from '@tarojs/taro'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:visible', 'agree', 'disagree'])
// 协议详情控制
const detailVisible = ref(false)
const detailTitle = ref('')
const detailContent = ref('')
/**
* @description 显示协议内容
* @param {string} type - 协议类型 'user' | 'privacy'
*/
const showAgreement = (type) => {
detailTitle.value = type === 'user' ? '用户服务协议' : '隐私政策'
detailContent.value = type === 'user' ? USER_AGREEMENT : PRIVACY_POLICY
detailVisible.value = true
}
/**
* @description 处理不同意操作
*/
const handleDisagree = () => {
emit('disagree')
}
/**
* @description 处理同意操作
*/
const handleAgree = () => {
emit('agree')
emit('update:visible', false)
}
</script>
export const USER_AGREEMENT = `
<div style="font-size: 28rpx; line-height: 1.6; color: #333;">
<h1 style="text-align: center; font-size: 36rpx; font-weight: bold; margin-bottom: 20rpx; color: #000;">用户服务协议</h1>
<p style="text-align: center; font-size: 24rpx; color: #999; margin-bottom: 40rpx;">版本生效日期:2024年01月01日</p>
<p style="margin-bottom: 30rpx;">欢迎您使用西园寺预约小程序服务!</p>
<p style="margin-bottom: 30rpx;">本《用户服务协议》(以下简称“本协议”)是您(以下或称“用户”)与西园寺预约平台(以下简称“我们”或“平台”)之间关于使用我们提供的产品和服务所订立的协议。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">一、 服务说明</h2>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;">本平台提供包括但不限于门票预约、活动报名、信息查询等服务。</li>
<li style="margin-bottom: 10rpx;">我们将尽力保障服务的稳定性,但在不可抗力、系统维护等情况下,可能会暂停部分服务。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">二、 用户账号</h2>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;">您在使用本服务时可能需要注册账号或完善个人信息。</li>
<li style="margin-bottom: 10rpx;">您应保证所提供信息的真实性、准确性。如因信息不实导致无法预约或入场,责任由您自行承担。</li>
<li style="margin-bottom: 10rpx;">请妥善保管您的账号信息,任何通过您账号进行的操作均视为您本人的行为。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">三、 用户行为规范</h2>
<p style="margin-bottom: 20rpx;">用户在使用本服务过程中,不得进行以下行为:</p>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;">违反国家法律法规、危害国家安全或社会公共利益。</li>
<li style="margin-bottom: 10rpx;">传播淫秽、色情、暴力、赌博或教唆犯罪的信息。</li>
<li style="margin-bottom: 10rpx;">利用本平台进行刷票、倒卖预约名额等干扰正常秩序的行为。</li>
<li style="margin-bottom: 10rpx;">侵犯他人知识产权、隐私权等合法权益。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">四、 预约规则</h2>
<ul style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;"><strong>实名制预约</strong>:所有预约均实行实名制,入场时需核验身份信息。</li>
<li style="margin-bottom: 10rpx;"><strong>取消规则</strong>:如需取消预约,请在规定时间内在平台操作。多次无故爽约可能会影响您后续的预约资格。</li>
<li style="margin-bottom: 10rpx;"><strong>入场须知</strong>:请按照预约时段入场,配合工作人员进行核验。</li>
</ul>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">五、 免责声明</h2>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;">因不可抗力(如自然灾害、政府行为等)导致服务中断,我们不承担责任。</li>
<li style="margin-bottom: 10rpx;">用户自行泄露个人信息导致的损失,我们不承担责任。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">六、 协议修改</h2>
<p style="margin-bottom: 30rpx;">我们有权根据法律法规及业务需要修改本协议。修改后的协议将通过平台公示,您继续使用服务即视为接受修改。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">七、 联系我们</h2>
<p style="margin-bottom: 30rpx;">如您对本协议有任何疑问,请联系平台客服。</p>
</div>
`
export const PRIVACY_POLICY = `
<div style="font-size: 28rpx; line-height: 1.6; color: #333;">
<h1 style="text-align: center; font-size: 36rpx; font-weight: bold; margin-bottom: 20rpx; color: #000;">隐私政策</h1>
<p style="text-align: center; font-size: 24rpx; color: #999; margin-bottom: 40rpx;">版本生效日期:2024年01月01日</p>
<p style="margin-bottom: 30rpx;">西园寺预约平台(以下简称“我们”)深知个人信息对您的重要性,我们将按照法律法规要求,采取相应安全保护措施,尽力保护您的个人信息安全可控。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">一、 我们如何收集信息</h2>
<p style="margin-bottom: 20rpx;">为了向您提供预约服务,我们需要收集以下信息:</p>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;"><strong>身份信息</strong>:姓名、身份证号(用于实名预约核验)。</li>
<li style="margin-bottom: 10rpx;"><strong>联系方式</strong>:手机号码(用于接收预约短信通知)。</li>
<li style="margin-bottom: 10rpx;"><strong>设备信息</strong>:设备型号、操作系统版本(用于保障账号安全及系统适配)。</li>
<li style="margin-bottom: 10rpx;"><strong>日志信息</strong>:IP地址、浏览记录(用于系统维护及优化)。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">二、 我们如何使用信息</h2>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;"><strong>提供服务</strong>:用于处理您的预约请求、入场核验。</li>
<li style="margin-bottom: 10rpx;"><strong>通知服务</strong>:向您发送预约成功、变动等通知。</li>
<li style="margin-bottom: 10rpx;"><strong>安全保障</strong>:用于身份验证、防欺诈检测。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">三、 信息共享与披露</h2>
<p style="margin-bottom: 20rpx;">我们不会将您的个人信息出售给第三方。仅在以下情况可能共享:</p>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;"><strong>法律要求</strong>:根据法律法规或政府主管部门的强制性要求。</li>
<li style="margin-bottom: 10rpx;"><strong>必要关联方</strong>:为实现服务目的,可能与关联方共享必要信息(如短信服务商)。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">四、 信息存储与保护</h2>
<ol style="padding-left: 40rpx; margin-bottom: 30rpx;">
<li style="margin-bottom: 10rpx;">我们会将您的信息存储在位于中国境内的服务器上。</li>
<li style="margin-bottom: 10rpx;">我们采取加密技术、访问控制等措施保护您的信息安全。</li>
<li style="margin-bottom: 10rpx;">您的个人信息保留期限为实现服务目的所必需的最短时间,法律法规另有规定的除外。</li>
</ol>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">五、 您的权利</h2>
<p style="margin-bottom: 30rpx;">您有权访问、更正、删除您的个人信息,或注销账号。您可以通过平台相关功能或联系客服进行操作。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">六、 未成年人保护</h2>
<p style="margin-bottom: 30rpx;">若您是未成年人,建议在监护人陪同下阅读本政策,并取得监护人同意后使用我们的服务。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">七、 政策更新</h2>
<p style="margin-bottom: 30rpx;">本隐私政策可能会适时更新。更新后的内容将在平台公布,请您注意查阅。</p>
<h2 style="font-size: 32rpx; font-weight: bold; margin-top: 40rpx; margin-bottom: 20rpx; color: #000;">八、 联系我们</h2>
<p style="margin-bottom: 30rpx;">如有隐私保护相关问题,请通过平台客服渠道联系。</p>
</div>
`
<!--
* @Date: 2024-01-26 13:08:09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-22 11:45:46
* @FilePath: /git/xyxBooking-weapp/src/pages/search/index.vue
* @LastEditTime: 2026-02-02 16:02:52
* @FilePath: /xyxBooking-weapp/src/pages/search/index.vue
* @Description: 搜索页
-->
<template>
......@@ -59,13 +59,20 @@
@cancel="show_id_type_picker = false"
></nut-picker>
</nut-popup>
<PrivacyPopup
v-model:visible="showPrivacyPopup"
@agree="onPrivacyAgree"
@disagree="onPrivacyDisagree"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import Taro, { useDidShow } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import qrCodeSearch from '@/components/qrCodeSearch'
import PrivacyPopup from '@/components/PrivacyPopup.vue'
import { useGo } from '@/hooks/useGo'
const go = useGo()
......@@ -73,6 +80,35 @@ const is_search = ref(false)
const idCode = ref('')
const id_number = ref('')
const show_id_type_picker = ref(false)
const showPrivacyPopup = ref(false)
useDidShow(() => {
const hasAgreed = Taro.getStorageSync('HAS_AGREED_PRIVACY')
if (!hasAgreed) {
showPrivacyPopup.value = true
}
})
/**
* @description 隐私协议同意回调
*/
const onPrivacyAgree = () => {
Taro.setStorageSync('HAS_AGREED_PRIVACY', true)
showPrivacyPopup.value = false
}
/**
* @description 隐私协议拒绝回调
*/
const onPrivacyDisagree = () => {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
// 如果是首个页面,跳转到首页
Taro.reLaunch({ url: '/pages/index/index' })
}
}
const id_type_options = [
{ label: '身份证', value: 1 },
{ label: '其他', value: 3 }
......
......@@ -47,6 +47,11 @@
center_variant="normal"
@select="on_nav_select"
/>
<PrivacyPopup
v-model:visible="showPrivacyPopup"
@agree="onPrivacyAgree"
@disagree="onPrivacyDisagree"
/>
</view>
</template>
......@@ -57,6 +62,7 @@ import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { personListAPI, delPersonAPI } from '@/api/index'
import indexNav from '@/components/indexNav.vue'
import PrivacyPopup from '@/components/PrivacyPopup.vue'
import icon_3 from '@/assets/images/首页01@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的02@2x.png'
......@@ -131,9 +137,36 @@ const removeItem = async item => {
}
}
const showPrivacyPopup = ref(false)
useDidShow(() => {
loadList()
const hasAgreed = Taro.getStorageSync('HAS_AGREED_PRIVACY')
if (!hasAgreed) {
showPrivacyPopup.value = true
}
})
/**
* @description 隐私协议同意回调
*/
const onPrivacyAgree = () => {
Taro.setStorageSync('HAS_AGREED_PRIVACY', true)
showPrivacyPopup.value = false
}
/**
* @description 隐私协议拒绝回调
*/
const onPrivacyDisagree = () => {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
// 如果是首个页面,跳转到首页
Taro.reLaunch({ url: '/pages/index/index' })
}
}
</script>
<style lang="less">
......