255 lines
9.2 KiB
JavaScript
255 lines
9.2 KiB
JavaScript
// 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 })
|
|
},
|
|
|
|
/**
|
|
* 点击人员进详情
|
|
* 顺带搭车补额度:仅对已勾选「总是保持以上选择」的用户,此调用不弹窗、完全无感。
|
|
* 必须同步调用且放在 navigateTo 之前,否则手势上下文丢失。
|
|
*/
|
|
onPersonTap(e) {
|
|
if (this.subscribeStatus === 'silent') subscribe.requestAndReport()
|
|
|
|
const id = e.currentTarget.dataset.id
|
|
wx.navigateTo({
|
|
url: `/pages/person-detail/person-detail?id=${id}`
|
|
})
|
|
},
|
|
|
|
/**
|
|
* 拉取额度风险,拼成给用户看的文案
|
|
* 说「谁的提醒有风险」而不是「还剩几次额度」——用户理解不了额度这种抽象概念
|
|
* @returns {Promise<boolean>} 本次刷新是否成功拿到了最新数据。
|
|
* 失败时不清空/覆盖 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'
|
|
})
|
|
}
|
|
})
|
|
|