From f4c1bd957cec5090a35911f3057a5852081d7e97 Mon Sep 17 00:00:00 2001 From: zhangxiang Date: Thu, 13 Aug 2026 08:06:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(miniapp):=20=E5=AE=9E=E7=8E=B0=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E4=B8=8E=E8=BF=90?= =?UTF-8?q?=E5=8A=BF=E9=A1=B5=E8=AE=A2=E9=98=85=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增订阅管理页面:订阅开关、推送时间设置、模板管理 - 运势主页添加订阅入口区域 - 更新页面路由配置 - 补充运势页相关测试 --- everything-is-suitable-uniapp/src/pages.json | 6 + .../src/pages/fortune/__tests__/index.test.ts | 133 ++++++++- .../src/pages/fortune/index.vue | 169 +++++++++++ .../push-subscription/__tests__/index.test.ts | 193 +++++++++++++ .../src/pages/push-subscription/index.vue | 263 ++++++++++++++++++ 5 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 everything-is-suitable-uniapp/src/pages/push-subscription/__tests__/index.test.ts create mode 100644 everything-is-suitable-uniapp/src/pages/push-subscription/index.vue diff --git a/everything-is-suitable-uniapp/src/pages.json b/everything-is-suitable-uniapp/src/pages.json index c0a228e..4a112b0 100644 --- a/everything-is-suitable-uniapp/src/pages.json +++ b/everything-is-suitable-uniapp/src/pages.json @@ -17,6 +17,12 @@ "style": { "navigationBarTitleText": "运势分析" } + }, + { + "path": "pages/push-subscription/index", + "style": { + "navigationBarTitleText": "订阅管理" + } } ], "globalStyle": { diff --git a/everything-is-suitable-uniapp/src/pages/fortune/__tests__/index.test.ts b/everything-is-suitable-uniapp/src/pages/fortune/__tests__/index.test.ts index 71afe10..5b777d3 100644 --- a/everything-is-suitable-uniapp/src/pages/fortune/__tests__/index.test.ts +++ b/everything-is-suitable-uniapp/src/pages/fortune/__tests__/index.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' import { createI18n } from 'vue-i18n' import FortunePage from '../index.vue' @@ -12,11 +12,46 @@ const i18n = createI18n({ daily: '日运', monthly: '月运', yearly: '年运', + pageTitle: '运势分析', + overallScore: '综合评分: {score}', + selectDate: '选择日期', + selectMonth: '选择月份', + career: '事业: ', + wealth: '财运: ', + relationship: '感情: ', + health: '健康: ', + luckyColor: '幸运色: ', + luckyNumber: '幸运数字: ', + luckyDirection: '幸运方向: ', + noChartHint: '请先完成排盘', }, }, }, }) +// Mock wx 全局对象 +const mockWx = { + requestSubscribeMessage: vi.fn(), + cloud: { + callFunction: vi.fn(), + }, +} + +// Mock chart data +const mockChart = { + birthInfo: { + birthTime: '1990-06-15 08:30', + birthPlace: '北京', + longitude: 116.4, + latitude: 39.9, + gender: 'male', + }, + heavenlyStem: '庚', + earthlyBranch: '午', + palaces: [], + stars: [], +} + function mountPage() { return mount(FortunePage, { global: { @@ -29,6 +64,11 @@ function mountPage() { } describe('FortunePage', () => { + beforeEach(() => { + vi.stubGlobal('wx', mockWx) + vi.clearAllMocks() + }) + it('应包含运势类型切换', () => { const wrapper = mountPage() expect(wrapper.find('[data-test="fortune-tabs"]').exists()).toBe(true) @@ -39,3 +79,94 @@ describe('FortunePage', () => { expect(wrapper.vm.activeTab).toBe('daily') }) }) + +describe('FortunePage - 订阅功能', () => { + beforeEach(() => { + vi.stubGlobal('wx', mockWx) + vi.clearAllMocks() + // 设置存储中有排盘数据,使 chart 不为 null + const store = (globalThis as any).__uniStorage__ ?? {} + store['eis_ziwei_chart'] = JSON.stringify(mockChart) + ;(globalThis as any).__uniStorage__ = store + }) + + it('有排盘数据时,应显示订阅入口区域', async () => { + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + const subscriptionSection = wrapper.find('.subscription-section') + expect(subscriptionSection.exists()).toBe(true) + }) + + it('未订阅时,应显示"订阅每日推送"按钮', async () => { + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + expect(wrapper.find('.subscribe-btn').exists()).toBe(true) + expect(wrapper.find('.subscribe-btn').text()).toContain('订阅每日推送') + }) + + it('已订阅时,应显示已订阅状态和推送时间', async () => { + const store = (globalThis as any).__uniStorage__ ?? {} + store['eis_push_subscription'] = JSON.stringify({ + pushTime: '07:00', + pushEnabled: true, + subscribedAt: '2026-08-13T00:00:00.000Z', + }) + ;(globalThis as any).__uniStorage__ = store + + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + expect(wrapper.find('.subscription-status').exists()).toBe(true) + expect(wrapper.find('.subscribed-label').text()).toContain('已订阅') + expect(wrapper.find('.push-time-info').text()).toContain('07:00') + }) + + it('已订阅时,应显示"管理订阅"入口', async () => { + const store = (globalThis as any).__uniStorage__ ?? {} + store['eis_push_subscription'] = JSON.stringify({ + pushTime: '07:00', + pushEnabled: true, + subscribedAt: '2026-08-13T00:00:00.000Z', + }) + ;(globalThis as any).__uniStorage__ = store + + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + expect(wrapper.find('.manage-link').exists()).toBe(true) + expect(wrapper.find('.manage-link').text()).toContain('管理订阅') + }) + + it('点击"管理订阅"应跳转到订阅管理页面', async () => { + const store = (globalThis as any).__uniStorage__ ?? {} + store['eis_push_subscription'] = JSON.stringify({ + pushTime: '07:00', + pushEnabled: true, + subscribedAt: '2026-08-13T00:00:00.000Z', + }) + ;(globalThis as any).__uniStorage__ = store + + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + wrapper.find('.manage-link').trigger('tap') + expect(uni.navigateTo).toHaveBeenCalledWith({ url: '/pages/push-subscription/index' }) + }) + + it('无排盘数据时,不应显示订阅入口', async () => { + ;(globalThis as any).__uniStorage__ = {} + + const wrapper = mountPage() + await new Promise((r) => setTimeout(r, 50)) + await wrapper.vm.$nextTick() + + expect(wrapper.find('.subscription-section').exists()).toBe(false) + }) +}) \ No newline at end of file diff --git a/everything-is-suitable-uniapp/src/pages/fortune/index.vue b/everything-is-suitable-uniapp/src/pages/fortune/index.vue index d00d70f..50eb575 100644 --- a/everything-is-suitable-uniapp/src/pages/fortune/index.vue +++ b/everything-is-suitable-uniapp/src/pages/fortune/index.vue @@ -45,6 +45,19 @@ {{ $t('fortune.noChartHint') }} + + + + + ✅ 已订阅每日运势推送 + 推送时间:每天 {{ pushTime }} + 管理订阅 + + + + + + @@ -97,6 +110,105 @@ const onMonthChange = async (e: any) => { monthlyFortune.value = await fortuneService.getMonthlyFortune(chart.value, year, month) } } + +// ===== 订阅功能 ===== +const PUSH_SUB_KEY = 'eis_push_subscription' +const SUBSCRIBE_TEMPLATE_ID = '' // 请在微信小程序后台申请模板后填写 + +interface PushSubscription { + pushTime: string + pushEnabled: boolean + subscribedAt: string +} + +const isSubscribed = ref(false) +const pushTime = ref('07:00') + +onMounted(() => { + checkSubscription() +}) + +function checkSubscription() { + try { + const raw = uni.getStorageSync(PUSH_SUB_KEY) + if (raw) { + const sub = JSON.parse(raw) as PushSubscription + isSubscribed.value = sub.pushEnabled + pushTime.value = sub.pushTime || '07:00' + } + } catch { + isSubscribed.value = false + } +} + +async function handleSubscribe() { + if (!chart.value) { + uni.showToast({ title: '请先完成排盘', icon: 'none' }) + return + } + + if (!chart.value.birthInfo) { + uni.showToast({ title: '请先设置生辰信息', icon: 'none' }) + return + } + + try { + // 请求微信订阅消息授权 + const tmplIds = [SUBSCRIBE_TEMPLATE_ID] + if (!tmplIds[0]) { + uni.showToast({ title: '推送模板未配置', icon: 'none' }) + return + } + + const subscribeResult = await wx.requestSubscribeMessage({ tmplIds }) + if (subscribeResult[SUBSCRIBE_TEMPLATE_ID] !== 'accept') { + uni.showToast({ title: '需要同意订阅才能推送', icon: 'none' }) + return + } + + // 调用云函数存储订阅 + const birthInfo = chart.value.birthInfo + const result = await wx.cloud.callFunction({ + name: 'subscribe', + data: { + birthInfo: { + birthDate: birthInfo.birthTime?.slice(0, 10) || '', + birthHour: parseInt(birthInfo.birthTime?.slice(11, 13) || '0'), + birthMinute: parseInt(birthInfo.birthTime?.slice(14, 16) || '0'), + birthPlace: birthInfo.birthPlace || '', + longitude: birthInfo.longitude || 0, + latitude: birthInfo.latitude || 0, + gender: birthInfo.gender, + }, + templateId: SUBSCRIBE_TEMPLATE_ID, + pushTime: '07:00', + }, + }) + + if (result.result.success) { + // 本地缓存订阅状态 + const subData: PushSubscription = { + pushTime: '07:00', + pushEnabled: true, + subscribedAt: new Date().toISOString(), + } + uni.setStorageSync(PUSH_SUB_KEY, JSON.stringify(subData)) + + isSubscribed.value = true + pushTime.value = '07:00' + uni.showToast({ title: '订阅成功', icon: 'success' }) + } else { + uni.showToast({ title: '订阅失败,请重试', icon: 'none' }) + } + } catch (err) { + console.error('Subscribe error:', err) + uni.showToast({ title: '订阅失败,请重试', icon: 'none' }) + } +} + +function navigateToSubscription() { + uni.navigateTo({ url: '/pages/push-subscription/index' }) +} \ No newline at end of file