// index.js const storage = require('../../utils/storage') const dateUtils = require('../../utils/date') const fmt = require('../../utils/format') const api = require('../../utils/api') const subscribe = require('../../utils/subscribe') Page({ data: { persons: [], originalPersons: [], searchKeyword: '', currentFilter: 'all', totalCount: 0, upcomingCount: 0, todayText: '', atRiskText: '' }, onLoad() { this.loadPersons() }, onShow() { this.loadPersons() this.refreshQuota() // 提前查好订阅状态缓存到实例上:点击处理函数里不能再 await,否则手势上下文会丢 subscribe.getStatus().then(status => { this.subscribeStatus = status }) }, /** * 加载人员列表 */ loadPersons() { const persons = storage.getPersons() const anniversaries = storage.getAnniversaries() const personsWithAnniversaries = persons.map(person => { const personAnniversaries = anniversaries.filter(a => a.personId === person.id) let nextAnniversary = null if (personAnniversaries.length > 0) { const upcoming = personAnniversaries .map(a => { // 农历纪念日的公历日期每年都不同,必须走 getNextOccurrenceOf 而不是直接用 solarMonth/solarDay const { date, daysUntil } = dateUtils.getNextOccurrenceOf(a) return { ...a, date, daysUntil } }) .sort((a, b) => a.daysUntil - b.daysUntil) if (upcoming.length > 0) { const next = upcoming[0] nextAnniversary = { type: next.type, typeName: fmt.getTypeName(next.type, next.customTypeName), dateText: dateUtils.formatDate(next.date, 'MM月DD日'), daysUntil: next.daysUntil, daysUntilText: fmt.formatDaysUntil(next.daysUntil) } } } return { ...person, anniversaryCount: personAnniversaries.length, nextAnniversary } }) const sorted = personsWithAnniversaries.sort((a, b) => { if (!a.nextAnniversary && !b.nextAnniversary) return 0 if (!a.nextAnniversary) return 1 if (!b.nextAnniversary) return -1 return a.nextAnniversary.daysUntil - b.nextAnniversary.daysUntil }) const today = new Date() const weekdays = ['日', '一', '二', '三', '四', '五', '六'] const todayText = `${today.getMonth() + 1}月${today.getDate()}日 周${weekdays[today.getDay()]}` const upcomingCount = sorted.filter(p => p.nextAnniversary && p.nextAnniversary.daysUntil <= 7).length this.setData({ originalPersons: sorted, totalCount: sorted.length, upcomingCount, todayText }) if (this.data.currentFilter === 'all' && !this.data.searchKeyword) { this.setData({ persons: sorted }) } else { this.filterPersons() } }, /** * 搜索输入 */ onSearchInput(e) { const keyword = e.detail.value this.setData({ searchKeyword: keyword }) this.filterPersons() }, /** * 筛选切换 */ onFilterTap(e) { const filter = e.currentTarget.dataset.filter this.setData({ currentFilter: filter }, () => { this.filterPersons() }) }, /** * 筛选人员 */ filterPersons() { const { originalPersons, searchKeyword, currentFilter } = this.data const anniversaries = storage.getAnniversaries() let filtered = [...originalPersons] // 关键词搜索 if (searchKeyword) { filtered = filtered.filter(p => p.name.includes(searchKeyword) || (p.nickname && p.nickname.includes(searchKeyword)) ) } // 类型筛选 if (currentFilter !== 'all') { filtered = filtered.filter(person => { const personAnniversaries = anniversaries.filter(a => a.personId === person.id) if (currentFilter === 'birthday') { // 生日筛选:只显示有生日类型的人(公历生日或农历生日) return personAnniversaries.some(a => a.type === 'birthday' || a.type === 'lunar_birthday' ) } else if (currentFilter === 'anniversary') { // 纪念日筛选:只显示有纪念日类型的人(结婚、订婚、其他) return personAnniversaries.some(a => a.type === 'wedding' || a.type === 'engagement' || a.type === 'other' ) } else if (currentFilter === 'upcoming') { // 即将到来:只显示7天内有纪念日的人 return person.nextAnniversary && person.nextAnniversary.daysUntil <= 7 } return true }) } // 排序:与 loadPersons() 保持一致,按最近的纪念日排序 const sorted = filtered.sort((a, b) => { if (!a.nextAnniversary && !b.nextAnniversary) return 0 if (!a.nextAnniversary) return 1 if (!b.nextAnniversary) return -1 return a.nextAnniversary.daysUntil - b.nextAnniversary.daysUntil }) this.setData({ persons: sorted }) }, /** * 点击人员 */ onPersonTap(e) { const id = e.currentTarget.dataset.id wx.navigateTo({ url: `/pages/person-detail/person-detail?id=${id}` }) }, /** * 拉取额度风险,拼成给用户看的文案 * 说「谁的提醒有风险」而不是「还剩几次额度」——用户理解不了额度这种抽象概念 * @returns {Promise} 本次刷新是否成功拿到了最新数据。 * 失败时不清空/覆盖 atRiskText——网络抖动时清空反而会掩盖真实存在的风险, * 比"文案没更新"更糟;调用方需要这个返回值来判断该给用户什么反馈(见 onTopUp)。 */ async refreshQuota() { try { const res = await api.subscribe('get') if (!res || !res.success) return false const names = res.atRiskNames || [] let text = '' if (names.length > 0 && names.length <= 3) { text = names.join('、') } else if (names.length > 3) { text = names.slice(0, 3).join('、') + ' 等 ' + names.length + ' 人' } this.setData({ atRiskText: text }) return true } catch (e) { // 查询失败就不显示提示,静默处理,不打扰用户;atRiskText 保留旧值不清空(理由见上) console.warn('[index] 额度查询失败', e) return false } }, /** * 点击风险提示卡片补额度 * 注意:wx.requestSubscribeMessage / wx.openSetting 必须在这里同步调用, * 前面绝不能有 await,否则手势上下文丢失、调用必然失败 */ onTopUp() { const status = this.subscribeStatus if (status === 'rejected' || status === 'mainSwitchOff') { wx.openSetting({ withSubscriptions: true }) return } // 显式处理 status === undefined,不让它靠"没匹配上第一个 if"隐式落地: // undefined 出现在 onShow 里 subscribe.getStatus() 还没返回、用户就点了卡片的情况; // 这里没法现查状态兜底——getStatus() 是异步的,哪怕 await 一下也会丢失手势上下文, // 所以有意让 undefined 和 unknown(查询失败)一样,都保守地按 willPrompt 处理, // 与 utils/subscribe.js 里「unknown 按 willPrompt 保守处理」的取向一致。 // 代价:如果用户真实状态其实是 mainSwitchOff(通知总开关关了),这里不会拦截去设置页, // 只会静默调用 wx.requestSubscribeMessage,微信不弹窗、直接判定未同意, // 用户只会看到下面「未开启提醒」的兜底提示,不会被引导去设置页—— // 这是已知且可接受的体验降级,不在本次修复范围内。 const isConservativeFallback = status === undefined || status === 'unknown' if (isConservativeFallback) { // 不阻断流程,只留一条日志方便排查——命中这条分支时用户体验会比正常 // 情况弱一点(见上面的注释),出问题时能在控制台里定位到具体是哪种取舍 console.warn('[index] onTopUp 命中保守兜底分支,status =', status) } subscribe.requestAndReport().then(accepted => { if (!accepted) { wx.showToast({ title: '未开启提醒', icon: 'none' }) return } // 补额度这步已经成功(微信侧确实同意了),但下面刷新风险文案是另一次独立请求, // 可能失败;两者不能混为一谈,否则会出现"已补充"toast 和纹丝不动的风险卡片 // 同时出现的自相矛盾画面。用刷新结果决定 toast 措辞,不对刷新结果做绝对断言。 this.refreshQuota().then(refreshed => { if (refreshed) { wx.showToast({ title: '已补充提醒次数', icon: 'success' }) } else { wx.showToast({ title: '已补充,稍后自动更新', icon: 'none' }) } }) }) }, /** * 点击添加按钮:直接进添加纪念日(方案 B:流程合并,姓名不存在自动建人) */ onAddTap() { wx.navigateTo({ url: '/pages/add-anniversary/add-anniversary' }) } })