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