| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- <template>
- <CustomNav title="健康提醒" leftType="back" />
- <view class="content">
- <view class="notification-toggle">
- <text class="notification-label">消息通知</text>
- <switch :checked="notificationsEnabled" @change="onNotificationChange" />
- </view>
- <view class="template-info">
- <text class="tmpl-title">每日健康上报提醒</text>
- <text class="tmpl-meta">模板编号:7536 · 模板ID:ACS7cwcbx0F0Y_YaB4GZr7rWP7BO2-7wQOtYsnUjmFI</text>
- </view>
- <view class="reminder-list">
- <view class="reminder-item" v-for="(reminder, index) in reminders" :key="index">
- <view class="reminder-info">
- <text class="reminder-title">{{ reminder.title }}</text>
- <text class="reminder-time">{{ reminder.time }}</text>
- </view>
- <switch :checked="reminder.enabled" @change="toggleReminder(index)" />
- </view>
- </view>
- </view>
-
- <!-- 权限引导弹窗 -->
- <view class="permission-modal" v-if="showPermissionGuide">
- <view class="modal-mask" @click="closePermissionGuide"></view>
- <view class="modal-content">
- <view class="modal-header">
- <text class="modal-title">系统提示</text>
- </view>
- <view class="modal-body">
- <view class="modal-text">检测到您可能误关闭了消息通知权限,或之前曾拒绝过授权。{{ '\n' }}消息通知权限能帮助您及时接收血压、血糖等重要健康提醒!{{ '\n' }}
- 若您希望开启通知,请按以下步骤操作{{ '\n' }}(若您无需通知,可点击下方取消按钮):{{ '\n' }}{{ '\n' }}1. 点击下方【去开启】按钮{{ '\n' }}2. 进入【通知管理】选项{{ '\n' }}3. 开启【接收通知】开关{{ '\n' }}4. 确保各项健康提醒均为【接收】状态</view>
- </view>
- <view class="modal-footer">
- <button class="modal-button cancel" @click="closePermissionGuide">取消</button>
- <button class="modal-button confirm" @click="openSettings">去开启</button>
- </view>
- </view>
- </view>
-
- <TabBar />
- </template>
- <script setup lang="ts">
- import { ref, onMounted, onUnmounted, computed } from 'vue'
- import { onShow, onHide } from '@dcloudio/uni-app'
- import CustomNav from '@/components/custom-nav.vue'
- import TabBar from '@/components/tab-bar.vue'
- interface Reminder {
- title: string
- time: string
- enabled: boolean
- }
- const reminders = ref<Reminder[]>([
- { title: '喝水提醒', time: '08:00', enabled: true },
- { title: '运动提醒', time: '18:00', enabled: false },
- { title: '测量血压', time: '07:00', enabled: true },
- { title: '测量血糖', time: '12:00', enabled: false },
- { title: '服药提醒', time: '09:00', enabled: true }
- ])
- // 模板 ID & 其他展示信息
- const TEMPLATE_ID = 'ACS7cwcbx0F0Y_YaB4GZr7rWP7BO2-7wQOtYsnUjmFI'
- const TEMPLATE_NO = '7536'
- // 全局消息开关
- const notificationsEnabled = ref<boolean>(false)
- // 是否显示权限引导
- const showPermissionGuide = ref<boolean>(false)
- // 记录页面进入来源:'healthIndex' | 'subscribe' | 'unknown'
- const entrySource = ref<'healthIndex' | 'subscribe' | 'unknown'>('unknown')
- const entrySourceText = computed(() => {
- switch (entrySource.value) {
- case 'healthIndex':
- return '来自 健康首页'
- case 'subscribe':
- return '来自 订阅消息'
- default:
- return '来源 未知'
- }
- })
- onMounted(() => {
- try {
- const val = (uni as any).getStorageSync('notificationsEnabled')
- console.log('已获取全局消息开关状态:', val)
- if (typeof val === 'boolean') notificationsEnabled.value = val
- } catch (e) {
- // 忽略错误
- }
-
- // 检查用户订阅状态
- // checkSubscriptionStatus()
-
- // 尝试判断来源:优先检查页面栈的上一个页面;若无(直接打开),则检查 launch options 的 query
- try {
- const pages = (getCurrentPages as any)()
- console.log('当前页面栈:', pages)
-
- if (pages && pages.length >= 1) {
- const currentPage = pages[pages.length - 1]
- const currentRoute = currentPage?.route || currentPage?.__route || currentPage?.$page?.route
- const currentOptions = currentPage?.options || {}
-
- console.log('当前页面路由:', currentRoute)
- console.log('当前页面参数:', currentOptions)
-
- // 检查当前页面是否包含 from=subscribe 参数
- if (currentOptions.from === 'subscribe') {
- entrySource.value = 'subscribe'
- console.log('通过当前页面参数识别为订阅消息来源')
- }
-
- // 检查当前页面是否包含 templateId 相关参数
- if (currentOptions.templateId === TEMPLATE_ID || currentOptions.template_id === TEMPLATE_ID) {
- entrySource.value = 'subscribe'
- console.log('通过当前页面的模板ID参数识别为订阅消息来源')
- }
- }
- if (pages && pages.length >= 2) {
- const prev = pages[pages.length - 2]
- const prevRoute = prev?.route || prev?.__route || prev?.$page?.route
- console.log('上一页路由:', prevRoute)
- if (prevRoute && String(prevRoute).includes('pages/public/health/index')) {
- entrySource.value = 'healthIndex'
- console.log('识别来源为健康首页')
- }
- } else {
- // 如果没有上一页信息,检查小程序启动参数(例如用户通过订阅消息打开)
- if ((uni as any).getLaunchOptionsSync) {
- const launch = (uni as any).getLaunchOptionsSync()
- console.log('启动参数:', launch)
- if (launch && launch.query) {
- console.log('查询参数:', launch.query)
- // 如果你在推送消息里把 from=subscribe 作为参数传入,这里会命中
- if (launch.query.from === 'subscribe') {
- entrySource.value = 'subscribe'
- console.log('通过 from 参数识别为订阅消息来源')
- }
- // 有些平台会把 templateId 或其它字段放在 query 中,可据此判断
- if (launch.query.templateId === TEMPLATE_ID || launch.query.template_id === TEMPLATE_ID) {
- entrySource.value = 'subscribe'
- console.log('通过模板ID参数识别为订阅消息来源')
- }
- }
- }
- }
- } catch (e) {
- console.error('识别入口来源时出错:', e)
- }
-
- // 显示来源提示(用于测试),延迟一点以保证 UI 已就绪
- try {
- setTimeout(() => {
- console.log('入口来源:', entrySource.value, entrySourceText.value)
- }, 250)
- } catch (e) {
- // 忽略错误
- }
-
- if (entrySource.value === 'unknown') {
- console.log('入口来源默认为未知')
- }
- })
- // 监听页面显示/隐藏(用于检测用户将小程序/APP切到后台或再次回到前台)
- onShow(() => {
- console.log('[reminder] 页面/应用返回前台(onShow)', { entrySource: entrySource.value })
- // 在页面返回前台时,检查订阅设置;如果用户关闭了通知订阅主开关或对本模板拒绝,则主动关闭本地消息开关
- try {
- if (typeof (uni as any).getSetting === 'function') {
- ;(uni as any).getSetting({
- withSubscriptions: true,
- success(res: any) {
- try {
- const subs = res.subscriptionsSetting || {}
- const mainSwitch = subs.mainSwitch
- const itemSettings = subs.itemSettings || {}
- const templateStatus = itemSettings[TEMPLATE_ID]
- console.log('[reminder][onShow] 订阅设置检查结果:', { mainSwitch, templateStatus })
- // 若主开关关闭或模板被拒绝/屏蔽,则主动关闭本地通知开关
- if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
- console.log('[reminder][onShow] 发现订阅被关闭或本模板被拒绝,主动关闭通知开关')
- notificationsEnabled.value = false
- //showPermissionGuide.value = (mainSwitch === false)
- try {
- ;(uni as any).setStorageSync('notificationsEnabled', false)
- } catch (e) {}
- } else {
- // 订阅为开启或未明确拒绝,不修改本地开关(按要求)
- console.log('[reminder][onShow] 订阅设置未发现关闭,不更改本地开关状态')
- }
- } catch (e) {
- console.error('[reminder][onShow] 处理 getSetting 返回值时出错:', e)
- }
- },
- fail(err: any) {
- console.error('[reminder][onShow] 获取订阅设置失败:', err)
- }
- })
- } else {
- console.log('[reminder][onShow] 当前环境不支持 getSetting,跳过订阅检查')
- }
- } catch (e) {
- console.error('[reminder][onShow] 检查订阅设置时异常:', e)
- }
- })
- onHide(() => {
- console.log('[reminder] 页面/应用进入后台(onHide)', { entrySource: entrySource.value })
- })
- // H5 平台兼容:document.visibilityState
- const handleVisibilityChange = () => {
- try {
- const state = (document as any).visibilityState
- if (state === 'hidden') {
- console.log('[reminder][visibilitychange] H5 页面变为 hidden(退到后台/切换窗口)')
- } else if (state === 'visible') {
- console.log('[reminder][visibilitychange] H5 页面变为 visible(回到前台)')
- }
- } catch (e) {
- // 忽略在非 H5 环境下的错误
- }
- }
- onMounted(() => {
- // 注册 H5 可见性变更监听
- try {
- if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
- document.addEventListener('visibilitychange', handleVisibilityChange)
- }
- } catch (e) {
- // 忽略
- }
- // 额外注册 uni 全局的 App 前后台事件(部分平台需要)
- try {
- if ((uni as any) && typeof (uni as any).onAppShow === 'function') {
- ;(uni as any).onAppShow((res: any) => {
- console.log('[reminder][uni.onAppShow] App 回到前台', res)
- })
- }
- if ((uni as any) && typeof (uni as any).onAppHide === 'function') {
- ;(uni as any).onAppHide(() => {
- console.log('[reminder][uni.onAppHide] App 进入后台')
- })
- }
- } catch (e) {
- // 忽略注册错误
- }
- })
- onUnmounted(() => {
- try {
- if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
- document.removeEventListener('visibilitychange', handleVisibilityChange)
- }
- } catch (e) {
- // 忽略
- }
- })
- /**
- * 检查用户订阅状态
- */
- const checkSubscriptionStatus = () => {
- // 使用 uni.getSetting 检查用户授权状态
- if (typeof (uni as any).getSetting === 'function') {
- (uni as any).getSetting({
- withSubscriptions: true, // 启用订阅设置查询
- success: (res: any) => {
- console.log('[checkSubscriptionStatus] 用户设置:', res)
-
- // 检查订阅消息设置
- if (res.subscriptionsSetting) {
- console.log('[checkSubscriptionStatus] 订阅设置详情:', JSON.stringify(res.subscriptionsSetting, null, 2))
-
- // 检查主开关
- const mainSwitch = res.subscriptionsSetting.mainSwitch
- console.log(`[checkSubscriptionStatus] 订阅主开关状态: ${mainSwitch ? '已开启(用户允许接收通知)' : '已关闭(用户禁止接收所有通知)'}`)
-
- // 检查针对特定模板的设置
- const itemSettings = res.subscriptionsSetting.itemSettings || {}
- const templateStatus = itemSettings[TEMPLATE_ID]
- console.log(`[checkSubscriptionStatus] 模板 ${TEMPLATE_ID} 状态:`, templateStatus)
-
- // 如果主开关关闭或特定模板被拒绝,则更新本地状态
- if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
- console.log('[checkSubscriptionStatus] 用户关闭了订阅设置,正在更新本地状态')
- notificationsEnabled.value = false
- // 显示权限引导
- showPermissionGuide.value = (mainSwitch === false)
- try {
- (uni as any).setStorageSync('notificationsEnabled', false)
- } catch (e) {
- console.error('[checkSubscriptionStatus] 更新存储失败:', e)
- }
- }
- // 如果模板被接受且主开关开启,更新本地状态为true
- else if (mainSwitch === true && templateStatus === 'accept') {
- console.log('[checkSubscriptionStatus] 用户开启了订阅设置,正在更新本地状态')
- notificationsEnabled.value = true
- showPermissionGuide.value = false
- try {
- (uni as any).setStorageSync('notificationsEnabled', true)
- } catch (e) {
- console.error('[checkSubscriptionStatus] 更新存储失败:', e)
- }
- }
- } else {
- console.log('[checkSubscriptionStatus] 响应中未找到订阅设置')
- }
- },
- fail: (err: any) => {
- console.error('[checkSubscriptionStatus] 获取用户设置失败:', err)
- }
- })
- } else {
- console.log('当前环境不支持 uni.getSetting')
- }
- }
- /**
- * 打开微信设置页面
- */
- const openSettings = () => {
- console.log('用户点击前往设置')
- showPermissionGuide.value = false
- if (typeof (uni as any).openSetting === 'function') {
- (uni as any).openSetting({
- success: (res: any) => {
- console.log('打开设置成功:', res)
- // 重新检查订阅状态
- checkSubscriptionStatus()
- },
- fail: (err: any) => {
- console.error('打开设置失败:', err)
- uni.showToast({ title: '打开设置失败', icon: 'none' })
- }
- })
- } else {
- uni.showToast({ title: '当前环境不支持打开设置', icon: 'none' })
- }
- }
- /**
- * 关闭权限引导弹窗
- */
- const closePermissionGuide = () => {
- console.log('用户关闭权限引导')
- showPermissionGuide.value = false
- }
- /**
- * 顶部总开关变更
- * - 打开时:调用 uni.requestSubscribeMessage 请求用户同意订阅 TEMPLATE_ID
- * - 如果用户同意(返回 accept),将状态置为 true 并持久化
- * - 否则回滚并提示
- */
- const onNotificationChange = (e: any) => {
- const newVal = e?.detail?.value
- console.log('通知开关更改为:', newVal)
-
- if (newVal) {
- // 先将开关设置为开启状态
- notificationsEnabled.value = true
-
- // 请求订阅(仅在微信/小程序有效)
- ;(uni as any).requestSubscribeMessage({
- tmplIds: [TEMPLATE_ID],
- success(res: any) {
- console.log('订阅消息请求成功结果:', res)
- // res 可能形如 { "ACS7...": 'accept' }
- const result = res && res[TEMPLATE_ID]
- if (result === 'accept') {
- console.log('用户接受了订阅')
- try {
- ;(uni as any).setStorageSync('notificationsEnabled', true)
- } catch (err) {
- // 忽略存储错误
- }
- uni.showToast({ title: '订阅成功', icon: 'success' })
- // 隐藏权限引导
- showPermissionGuide.value = false
- } else {
- console.log('用户未接受订阅,结果:', result)
- // 用户拒绝或关闭了弹窗
- notificationsEnabled.value = false
- try {
- ;(uni as any).setStorageSync('notificationsEnabled', false)
- } catch (err) {}
- uni.showToast({ title: '订阅被拒绝', icon: 'none' })
- // 显示权限引导
- showPermissionGuide.value = true
- }
- },
- fail(err: any) {
- console.log('订阅消息请求失败:', err)
- notificationsEnabled.value = false
- try {
- ;(uni as any).setStorageSync('notificationsEnabled', false)
- } catch (e) {}
- // 根据错误类型显示不同的提示信息
- if (err.errCode === 20004) {
- uni.showToast({ title: '推送权限已关闭', icon: 'none' })
- // 显示权限引导
- showPermissionGuide.value = true
- } else {
- uni.showToast({ title: '订阅请求失败', icon: 'none' })
- }
- }
- })
- } else {
- // 关闭订阅:不需要额外调用接口,只改变本地记录
- console.log('通知开关已关闭')
- notificationsEnabled.value = false
- try {
- ;(uni as any).setStorageSync('notificationsEnabled', false)
- } catch (err) {}
- uni.showToast({ title: '已关闭通知', icon: 'none' })
- // 隐藏权限引导
- showPermissionGuide.value = false
- }
- }
- const toggleReminder = (index: number) => {
- reminders.value[index].enabled = !reminders.value[index].enabled
- }
- </script>
- <style scoped>
- .content {
- padding: calc(var(--status-bar-height) + 44px) 50rpx 50rpx 50rpx;
- min-height: 100vh;
- background-color: #f5f5f5;
- box-sizing: border-box;
- }
- .reminder-list {
- background-color: #fff;
- border-radius: 12rpx;
- overflow: hidden;
- margin-top: 50rpx;
- }
- .reminder-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 30rpx 40rpx;
- border-bottom: 1rpx solid #eee;
- }
- .reminder-item:last-child {
- border-bottom: none;
- }
- .reminder-info {
- flex: 1;
- }
- .reminder-title {
- font-size: 32rpx;
- color: #000000;
- display: block;
- margin-bottom: 10rpx;
- }
- .reminder-time {
- font-size: 28rpx;
- color: #5a5a5a;
- }
- .notification-toggle {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 30rpx 40rpx;
- background-color: #fff;
- border-radius: 12rpx;
- margin-top: 20rpx;
- }
- .notification-label {
- font-size: 32rpx;
- color: #000;
- }
- .template-info {
- margin-top: 12rpx;
- padding: 0 10rpx;
- color: #666;
- font-size: 24rpx;
- }
- .tmpl-title {
- display: block;
- font-size: 26rpx;
- color: #333;
- }
- .tmpl-meta {
- display: block;
- font-size: 22rpx;
- color: #8a8a8a;
- margin-top: 6rpx;
- }
- /* 权限引导弹窗样式 */
- .permission-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 9999;
- }
- .modal-mask {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.6);
- }
- .modal-content {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 80%;
- background-color: #fff;
- border-radius: 12rpx;
- overflow: hidden;
- }
- .modal-header {
- padding: 30rpx;
- text-align: center;
- border-bottom: 1rpx solid #eee;
- }
- .modal-title {
- font-size: 32rpx;
- color: #333;
- font-weight: bold;
- }
- .modal-body {
- padding: 30rpx;
- text-align: center;
- }
- .modal-text {
- font-size: 28rpx;
- color: #666;
- line-height: 1.5;
- white-space: pre-line; /* 添加这行来支持换行 */
- }
- .modal-footer {
- display: flex;
- border-top: 1rpx solid #eee;
- }
- .modal-button {
- flex: 1;
- border: none;
- padding: 20rpx 0;
- font-size: 28rpx;
- }
- .modal-button.cancel {
- background-color: #f5f5f5;
- color: #666;
- }
- .modal-button.confirm {
- background-color: #07c160;
- color: #fff;
- }
- .modal-button:after {
- border: none;
- }
- </style>
|