feat(miniapp): 实现订阅管理页面与运势页订阅入口
- 新增订阅管理页面:订阅开关、推送时间设置、模板管理 - 运势主页添加订阅入口区域 - 更新页面路由配置 - 补充运势页相关测试
This commit is contained in:
@@ -17,6 +17,12 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "运势分析"
|
"navigationBarTitleText": "运势分析"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/push-subscription/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订阅管理"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@@ -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 { mount } from '@vue/test-utils'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import FortunePage from '../index.vue'
|
import FortunePage from '../index.vue'
|
||||||
@@ -12,11 +12,46 @@ const i18n = createI18n({
|
|||||||
daily: '日运',
|
daily: '日运',
|
||||||
monthly: '月运',
|
monthly: '月运',
|
||||||
yearly: '年运',
|
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() {
|
function mountPage() {
|
||||||
return mount(FortunePage, {
|
return mount(FortunePage, {
|
||||||
global: {
|
global: {
|
||||||
@@ -29,6 +64,11 @@ function mountPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('FortunePage', () => {
|
describe('FortunePage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('wx', mockWx)
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
it('应包含运势类型切换', () => {
|
it('应包含运势类型切换', () => {
|
||||||
const wrapper = mountPage()
|
const wrapper = mountPage()
|
||||||
expect(wrapper.find('[data-test="fortune-tabs"]').exists()).toBe(true)
|
expect(wrapper.find('[data-test="fortune-tabs"]').exists()).toBe(true)
|
||||||
@@ -39,3 +79,94 @@ describe('FortunePage', () => {
|
|||||||
expect(wrapper.vm.activeTab).toBe('daily')
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -45,6 +45,19 @@
|
|||||||
<view v-if="!chart" class="no-chart-hint">
|
<view v-if="!chart" class="no-chart-hint">
|
||||||
<text class="hint-text">{{ $t('fortune.noChartHint') }}</text>
|
<text class="hint-text">{{ $t('fortune.noChartHint') }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 订阅入口 -->
|
||||||
|
<view v-if="chart" class="subscription-section">
|
||||||
|
<view v-if="isSubscribed" class="subscription-status">
|
||||||
|
<text class="subscribed-label">✅ 已订阅每日运势推送</text>
|
||||||
|
<text class="push-time-info">推送时间:每天 {{ pushTime }}</text>
|
||||||
|
<text class="manage-link" @tap="navigateToSubscription">管理订阅</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="subscription-entry">
|
||||||
|
<text class="subscribe-hint">开启每日推送,不错过每日运势</text>
|
||||||
|
<button class="subscribe-btn" @tap="handleSubscribe">订阅每日推送</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -97,6 +110,105 @@ const onMonthChange = async (e: any) => {
|
|||||||
monthlyFortune.value = await fortuneService.getMonthlyFortune(chart.value, year, month)
|
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' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -200,6 +312,63 @@ const onMonthChange = async (e: any) => {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subscription-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribed-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.push-time-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-link {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1677ff;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-btn {
|
||||||
|
width: 200px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(44, 24, 16, 255);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 375px) {
|
@media screen and (max-width: 375px) {
|
||||||
.tab {
|
.tab {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import { triggerOnShow } from '../../../__mocks__/dcloudio/uni-app'
|
||||||
|
import SubscriptionPage from '../index.vue'
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'zh-CN',
|
||||||
|
messages: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock wx 全局对象
|
||||||
|
const mockWx = {
|
||||||
|
cloud: {
|
||||||
|
callFunction: vi.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPage() {
|
||||||
|
return mount(SubscriptionPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [i18n],
|
||||||
|
stubs: {
|
||||||
|
picker: { template: '<div><slot /></div>' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PushSubscriptionPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('wx', mockWx)
|
||||||
|
vi.clearAllMocks()
|
||||||
|
// 默认设置订阅状态
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应显示订阅状态为"已开启"', async () => {
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('.status-value').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.status-value').text()).toContain('已开启')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('应显示当前推送时间', async () => {
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('.time-value').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.time-value').text()).toBe('07:00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('点击"取消订阅"应弹出确认对话框', async () => {
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
wrapper.find('.unsubscribe-btn').trigger('tap')
|
||||||
|
expect(uni.showModal).toHaveBeenCalled()
|
||||||
|
const callArgs = (uni.showModal as any).mock.calls[0][0]
|
||||||
|
expect(callArgs.title).toContain('确认取消')
|
||||||
|
expect(callArgs.content).toContain('取消后将不再收到每日运势推送')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('确认取消订阅后应调用云函数并清除缓存', async () => {
|
||||||
|
// 模拟 wx.cloud.callFunction 返回成功
|
||||||
|
mockWx.cloud.callFunction.mockResolvedValue({
|
||||||
|
result: { success: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 触发取消订阅
|
||||||
|
wrapper.find('.unsubscribe-btn').trigger('tap')
|
||||||
|
|
||||||
|
// 模拟确认对话框
|
||||||
|
const modalCall = (uni.showModal as any).mock.calls[0][0]
|
||||||
|
modalCall.success({ confirm: true })
|
||||||
|
|
||||||
|
// 等待异步操作完成(包括 setTimeout 1500ms)
|
||||||
|
await new Promise((r) => setTimeout(r, 2000))
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 验证云函数被调用
|
||||||
|
expect(mockWx.cloud.callFunction).toHaveBeenCalledWith({ name: 'unsubscribe' })
|
||||||
|
// 验证本地缓存被清除
|
||||||
|
expect(uni.removeStorageSync).toHaveBeenCalledWith('eis_push_subscription')
|
||||||
|
// 验证提示
|
||||||
|
expect(uni.showToast).toHaveBeenCalledWith({ title: '已取消订阅', icon: 'success' })
|
||||||
|
// 验证返回上一页
|
||||||
|
expect(uni.navigateBack).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('取消订阅失败时应提示"取消失败"', async () => {
|
||||||
|
// 模拟云函数返回失败
|
||||||
|
mockWx.cloud.callFunction.mockResolvedValue({
|
||||||
|
result: { success: false, error: '未找到订阅记录' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
wrapper.find('.unsubscribe-btn').trigger('tap')
|
||||||
|
const modalCall = (uni.showModal as any).mock.calls[0][0]
|
||||||
|
modalCall.success({ confirm: true })
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 50))
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(uni.showToast).toHaveBeenCalledWith({ title: '取消失败,请重试', icon: 'none' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('保存推送时间应调用云函数并更新本地缓存', async () => {
|
||||||
|
mockWx.cloud.callFunction.mockResolvedValue({
|
||||||
|
result: { success: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 修改时间
|
||||||
|
await wrapper.vm.onTimeChange({ detail: { value: '08:00' } })
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 保存
|
||||||
|
wrapper.find('.save-btn').trigger('tap')
|
||||||
|
await new Promise((r) => setTimeout(r, 50))
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 验证云函数被调用
|
||||||
|
expect(mockWx.cloud.callFunction).toHaveBeenCalledWith({
|
||||||
|
name: 'updatePushTime',
|
||||||
|
data: { pushTime: '08:00' },
|
||||||
|
})
|
||||||
|
// 验证提示
|
||||||
|
expect(uni.showToast).toHaveBeenCalledWith({ title: '保存成功', icon: 'success' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('未订阅时,应使用默认时间 07:00', async () => {
|
||||||
|
// 清空存储
|
||||||
|
;(globalThis as any).__uniStorage__ = {}
|
||||||
|
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('07:00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('时间选择应四舍五入到 30 分钟步长', async () => {
|
||||||
|
const wrapper = mountPage()
|
||||||
|
triggerOnShow()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
// 07:08 → 07:00
|
||||||
|
wrapper.vm.onTimeChange({ detail: { value: '07:08' } })
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('07:00')
|
||||||
|
|
||||||
|
// 07:15 → 07:30
|
||||||
|
wrapper.vm.onTimeChange({ detail: { value: '07:15' } })
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('07:30')
|
||||||
|
|
||||||
|
// 07:45 → 08:00
|
||||||
|
wrapper.vm.onTimeChange({ detail: { value: '07:45' } })
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('08:00')
|
||||||
|
|
||||||
|
// 22:45 → 22:00(上限 22:00)
|
||||||
|
wrapper.vm.onTimeChange({ detail: { value: '22:45' } })
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('22:00')
|
||||||
|
|
||||||
|
// 05:30 → 06:30(下限 06:00,分钟 30 保留)
|
||||||
|
wrapper.vm.onTimeChange({ detail: { value: '05:30' } })
|
||||||
|
expect(wrapper.vm.selectedTime).toBe('06:30')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<view class="subscription-manage">
|
||||||
|
<!-- 订阅状态 -->
|
||||||
|
<view class="status-card">
|
||||||
|
<text class="status-title">订阅状态</text>
|
||||||
|
<view class="status-row">
|
||||||
|
<text class="status-label">每日运势推送</text>
|
||||||
|
<text class="status-value active">已开启</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 推送时间设置 -->
|
||||||
|
<view class="setting-card">
|
||||||
|
<text class="setting-title">推送时间设置</text>
|
||||||
|
<text class="setting-hint">每日在此时间推送当日运势(30分钟步长)</text>
|
||||||
|
<picker
|
||||||
|
mode="time"
|
||||||
|
:value="selectedTime"
|
||||||
|
:start="'06:00'"
|
||||||
|
:end="'22:00'"
|
||||||
|
@change="onTimeChange"
|
||||||
|
>
|
||||||
|
<view class="time-picker">
|
||||||
|
<text class="time-label">推送时间</text>
|
||||||
|
<text class="time-value">{{ selectedTime }}</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<button
|
||||||
|
class="save-btn"
|
||||||
|
:disabled="!timeChanged"
|
||||||
|
@tap="savePushTime"
|
||||||
|
>
|
||||||
|
保存设置
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 取消订阅 -->
|
||||||
|
<view class="action-card">
|
||||||
|
<button class="unsubscribe-btn" @tap="handleUnsubscribe">
|
||||||
|
取消订阅
|
||||||
|
</button>
|
||||||
|
<text class="unsubscribe-hint">取消后将不再收到每日运势推送</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const PUSH_SUB_KEY = 'eis_push_subscription'
|
||||||
|
|
||||||
|
interface PushSubscription {
|
||||||
|
pushTime: string
|
||||||
|
pushEnabled: boolean
|
||||||
|
subscribedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTime = ref('07:00')
|
||||||
|
const savedTime = ref('07:00')
|
||||||
|
const timeChanged = ref(false)
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
uni.setNavigationBarTitle({ title: '订阅管理' })
|
||||||
|
loadSubscription()
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadSubscription() {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(PUSH_SUB_KEY)
|
||||||
|
if (raw) {
|
||||||
|
const sub = JSON.parse(raw) as PushSubscription
|
||||||
|
selectedTime.value = sub.pushTime || '07:00'
|
||||||
|
savedTime.value = selectedTime.value
|
||||||
|
timeChanged.value = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 未订阅状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTimeChange(e: any) {
|
||||||
|
// 将时间四舍五入到最近的 30 分钟步长(00 或 30)
|
||||||
|
const [hour, minute] = e.detail.value.split(':').map(Number)
|
||||||
|
const roundedMinute = minute < 15 ? 0 : minute < 45 ? 30 : 0
|
||||||
|
const adjustedHour = minute >= 45 ? hour + 1 : hour
|
||||||
|
const finalHour = Math.min(Math.max(adjustedHour, 6), 22)
|
||||||
|
selectedTime.value = `${String(finalHour).padStart(2, '0')}:${String(roundedMinute).padStart(2, '0')}`
|
||||||
|
timeChanged.value = selectedTime.value !== savedTime.value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePushTime() {
|
||||||
|
try {
|
||||||
|
const result = await wx.cloud.callFunction({
|
||||||
|
name: 'updatePushTime',
|
||||||
|
data: { pushTime: selectedTime.value },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.result.success) {
|
||||||
|
// 更新本地缓存
|
||||||
|
const raw = uni.getStorageSync(PUSH_SUB_KEY)
|
||||||
|
if (raw) {
|
||||||
|
const sub = JSON.parse(raw) as PushSubscription
|
||||||
|
sub.pushTime = selectedTime.value
|
||||||
|
uni.setStorageSync(PUSH_SUB_KEY, JSON.stringify(sub))
|
||||||
|
}
|
||||||
|
|
||||||
|
savedTime.value = selectedTime.value
|
||||||
|
timeChanged.value = false
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save push time error:', err)
|
||||||
|
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnsubscribe() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认取消',
|
||||||
|
content: '取消后将不再收到每日运势推送,确定要取消吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
const result = await wx.cloud.callFunction({
|
||||||
|
name: 'unsubscribe',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.result.success) {
|
||||||
|
// 清除本地缓存
|
||||||
|
uni.removeStorageSync(PUSH_SUB_KEY)
|
||||||
|
uni.showToast({ title: '已取消订阅', icon: 'success' })
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '取消失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unsubscribe error:', err)
|
||||||
|
uni.showToast({ title: '取消失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.subscription-manage {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: rgba(244, 244, 245, 0.3);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card,
|
||||||
|
.setting-card,
|
||||||
|
.action-card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title,
|
||||||
|
.setting-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.active {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-label {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-value {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(44, 24, 16, 255);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn[disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsubscribe-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #ff4d4f;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsubscribe-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user