import { request, redirectToLogin } from '../../utils/request' import * as auth from '../../utils/auth' import { HoroscopeData } from '../../types/models' import { setTabBarSelected } from '../../utils/tabbar' import { logger } from '../../utils/logger' import { STORAGE_KEYS } from '../../utils/config' // 日志模块名 const LOG = 'horoscope' const COLOR_MAP: Record = { 红色: '#FF3B30', 橙色: '#FF9500', 黄色: '#FFCC00', 绿色: '#34C759', 蓝色: '#007AFF', 紫色: '#5856D6', 粉色: '#FF2D55', 白色: '#FFFFFF', 黑色: '#000000', 灰色: '#8E8E93', } // 今日日期(YYYYMMDD),用于判断运势数据是否跨天 function todayStatDate(): number { const d = new Date() const pad = (n: number) => (n < 10 ? `0${n}` : String(n)) return Number(`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`) } Page({ data: { horoscope: null as HoroscopeData | null, statDate: '', luckyColorHex: '#007AFF', loading: true, noBirth: false, error: false, errorText: '', }, onShow() { this.syncTabBar() // 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求 if (!auth.isLoggedIn()) { redirectToLogin() return } // 首帧渲染未结束就 setData 会触发「Expected updated data but get first rendering data」, // 整个 webview 渲染失败,用户视觉上仍看到上一个页面。首次 onShow 的数据拉取延迟到下一帧 const stale = !this.data.horoscope || Number(this.data.horoscope.stat_date) !== todayStatDate() if (!stale) return if (!(this as any).firstShown) { ;(this as any).firstShown = true wx.nextTick(() => this.fetchHoroscope(0)) return } this.fetchHoroscope(0) }, syncTabBar() { setTabBarSelected(2) }, fetchHoroscope(retry: number) { this.clearRetryTimer() this.setData({ loading: true, error: false, errorText: '' }) request({ url: '/horoscope/today', showError: false }) .then((res) => { logger.debug(LOG, '运势加载成功, statDate=', res && res.stat_date) this.setData({ horoscope: res, statDate: this.formatStatDate(res.stat_date), luckyColorHex: COLOR_MAP[res.lucky_color] || '#007AFF', loading: false, noBirth: false, error: false, }) }) .catch((e: any) => { const code = e && e.code logger.warn(LOG, '运势加载失败, code=', code, 'retry=', retry) const details = (e && e.details) || [] const noBirth = details.some((d: string) => d.indexOf('生日') >= 0) if (code === 10000 && noBirth) { this.setData({ noBirth: true, loading: false }) } else if (code === 90300 && retry < 3) { wx.showToast({ title: '运势生成中,请稍后重试', icon: 'none' }) // 定时器挂实例并在页面卸载时清理,避免离开页面后继续请求 ;(this as any).retryTimer = setTimeout(() => this.fetchHoroscope(retry + 1), 4000) } else { // 失败必须有可见反馈:否则会渲染一张数据为 null 的空卡 this.setData({ loading: false, error: true, errorText: (e && e.message) || '运势加载失败,请重试', }) } }) }, clearRetryTimer() { const timer = (this as any).retryTimer if (timer) { clearTimeout(timer) ;(this as any).retryTimer = null } }, onRetry() { this.fetchHoroscope(0) }, formatStatDate(d: number): string { const s = String(d) return `${s.slice(0, 4)}.${s.slice(4, 6)}.${s.slice(6, 8)}` }, onGoProfile() { // switchTab 不支持 query 传参,用 storage 标记让「我的」页 onShow 自动弹出编辑弹窗 wx.setStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT, 1) wx.switchTab({ url: '/pages/profile/profile', fail: (e) => { logger.warn(LOG, 'switchTab 个人资料失败, err=', e && e.errMsg) // 跳转失败时清除标记,避免下次进入「我的」页意外弹窗 wx.removeStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT) }, }) }, onGenerate() { wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend', fail: (e) => logger.warn(LOG, 'navigateTo AI 推荐失败, err=', e && e.errMsg), }) }, onUnload() { this.clearRetryTimer() }, })