v2.1.0 流程改造 + 农历准确性修复 + 双向同步 + 闰月支持
部署到群晖 / deploy (push) Successful in 44s

- Phase 1: 添加纪念日合并人物创建流程(方案 B)
- Phase 2: 农历提醒按 lunarMonth/Day 计算每年公历
- Phase 3: 人员数据同步到后端(新增 /api/person)
- Phase 4: 新设备启动从云端恢复数据
- Phase 5: 工具函数收敛 utils/format.js
- Phase 6: 同步失败入队 + 启动重试
- Phase 7: 闰月生日完整支持(含 isLeapMonth + UI 警示)
- 修复 lunarInfo 数据表错位(替换为权威源 jjonline/calendar.js)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-06-02 05:51:17 +08:00
parent 06d22884b9
commit ddcfe3334e
16 changed files with 1001 additions and 167 deletions
+100 -48
View File
@@ -1,21 +1,24 @@
// add-anniversary.js
const storage = require('../../utils/storage')
const dateUtils = require('../../utils/date')
const api = require('../../utils/api')
const sync = require('../../utils/sync')
const lunar = require('../../utils/lunar')
Page({
data: {
anniversaryId: null,
personId: null,
personList: [],
personIndex: 0,
selectedPerson: '',
inputName: '',
typeList: ['公历生日', '农历生日', '结婚纪念日', '订婚纪念日', '其他纪念日'],
typeIndex: 0,
showCustomType: false,
dateValue: '',
remindDaysList: ['提前3天', '提前7天', '提前14天', '提前30天', '自定义'],
remindDaysIndex: 0,
// UI 反馈:选了农历日期时展示对应农历文本 + 闰月警示
lunarText: '',
isLeapMonth: false,
formData: {
isLunar: false,
type: 'birthday',
@@ -26,6 +29,7 @@ Page({
lunarYear: '',
lunarMonth: '',
lunarDay: '',
isLeapMonth: false,
importance: 'low',
remindEnabled: true,
remindDays: 7,
@@ -34,18 +38,17 @@ Page({
},
onLoad(options) {
// 获取人员列表
// 获取人员列表用于快捷选择
const persons = storage.getPersons()
this.setData({ personList: persons })
if (options.personId) {
// 从人员详情页进入
const index = persons.findIndex(p => p.id === options.personId)
if (index !== -1) {
this.setData({
personIndex: index,
personId: options.personId,
selectedPerson: persons[index].name
// 从人员详情页进入,预选关联人员
const person = persons.find(p => p.id === options.personId)
if (person) {
this.setData({
personId: person.id,
inputName: person.name
})
}
}
@@ -75,17 +78,23 @@ Page({
new Date(anniversary.solarYear, anniversary.solarMonth - 1, anniversary.solarDay),
'YYYY-MM-DD'
)
// 设置类型
const typeIndex = this.getTypeIndex(anniversary.type)
// 编辑时不允许改关联人员,姓名展示用 personName 或从 personList 查
const person = this.data.personList.find(p => p.id === anniversary.personId)
this.setData({
formData: anniversary,
dateValue: date,
typeIndex,
showCustomType: anniversary.type === 'other'
showCustomType: anniversary.type === 'other',
personId: anniversary.personId,
inputName: person ? person.name : (anniversary.personName || '')
})
// 农历日期:编辑时也要回显
if (anniversary.isLunar) this._refreshLunar()
wx.setNavigationBarTitle({ title: '编辑纪念日' })
}
},
@@ -105,18 +114,26 @@ Page({
},
/**
* 选择人员
* 姓名输入:用户打字时实时更新;点 chip 时也会触发
* 输入框值变了就清掉已绑定的 personId,提交时再按姓名查找/创建
*/
onPersonChange(e) {
const index = parseInt(e.detail.value)
const person = this.data.personList[index]
onNameInput(e) {
const name = e.detail.value
const matched = this.data.personList.find(p => p.name === name.trim())
this.setData({
personIndex: index,
personId: person.id,
selectedPerson: person.name
inputName: name,
personId: matched ? matched.id : null
})
},
/**
* 点已有人员快捷 chip
*/
onPickPerson(e) {
const { id, name } = e.currentTarget.dataset
this.setData({ personId: id, inputName: name })
},
/**
* 选择类型
*/
@@ -146,9 +163,8 @@ Page({
*/
onDateTypeChange(e) {
const isLunar = e.detail.value === 'lunar'
this.setData({
'formData.isLunar': isLunar
})
this.setData({ 'formData.isLunar': isLunar })
this._refreshLunar()
},
/**
@@ -157,13 +173,36 @@ Page({
onDateChange(e) {
const dateStr = e.detail.value
const parts = dateStr.split('-')
this.setData({
dateValue: dateStr,
'formData.solarYear': parseInt(parts[0]),
'formData.solarMonth': parseInt(parts[1]),
'formData.solarDay': parseInt(parts[2])
})
this._refreshLunar()
},
/**
* 选了"农历"时,把当前公历日期反算成农历,写入 formData + 提示文案
* Why:闰月生日必须在 UI 上让用户确认这是不是他想要的农历日期
*/
_refreshLunar() {
const { formData } = this.data
if (!formData.isLunar || !formData.solarYear || !formData.solarMonth || !formData.solarDay) {
this.setData({ lunarText: '', isLeapMonth: false, 'formData.isLeapMonth': false })
return
}
const solarDate = new Date(formData.solarYear, formData.solarMonth - 1, formData.solarDay)
const ld = lunar.solarToLunar(solarDate)
this.setData({
lunarText: ld.lunarText,
isLeapMonth: ld.isLeap,
'formData.lunarYear': ld.year,
'formData.lunarMonth': ld.month,
'formData.lunarDay': ld.day,
'formData.isLeapMonth': ld.isLeap
})
},
/**
@@ -237,11 +276,13 @@ Page({
* 提交
*/
async onSubmit() {
const { formData, personId, anniversaryId, personList } = this.data
const { formData, anniversaryId, inputName } = this.data
let { personId } = this.data
// 验证关联人员
if (!personId) {
wx.showToast({ title: '请选择关联人员', icon: 'none' })
// 验证姓名
const name = (inputName || '').trim()
if (!name) {
wx.showToast({ title: '请输入姓名', icon: 'none' })
return
}
@@ -263,13 +304,30 @@ Page({
await this.requestSubscribe()
} catch (err) {
console.log('用户拒绝订阅消息')
// 继续保存,即使用户拒绝订阅
}
}
// 获取人员名称
const person = personList.find(p => p.id === personId)
const personName = person ? person.name : ''
// 新增模式且没绑定 personId → 按姓名查找或自动创建
if (!anniversaryId && !personId) {
const person = storage.ensurePerson(name)
if (!person) {
wx.showToast({ title: '保存失败', icon: 'none' })
return
}
personId = person.id
}
// 农历生日:兜底反算(onDateChange/_refreshLunar 通常已填好,这里防边界)
if (formData.isLunar) {
const solarDate = new Date(formData.solarYear, formData.solarMonth - 1, formData.solarDay)
const lunarDate = lunar.solarToLunar(solarDate)
formData.lunarYear = lunarDate.year
formData.lunarMonth = lunarDate.month
formData.lunarDay = lunarDate.day
formData.isLeapMonth = lunarDate.isLeap
}
const personName = name
if (anniversaryId) {
// 编辑模式
@@ -328,21 +386,15 @@ Page({
},
/**
* 同步到自建后端
* 同步纪念日到后端(失败自动入队,启动时 flush)
*/
async syncToCloud(id, data, action) {
try {
const openid = wx.getStorageSync('openid')
if (!openid) {
console.log('未获取到openid,跳过云端同步')
return
}
const res = await api.anniversary(action, { id, ...data })
console.log('云端同步成功:', res)
} catch (err) {
console.error('云端同步失败:', err)
// 不影响本地保存
syncToCloud(id, data, action) {
const openid = wx.getStorageSync('openid')
if (!openid) {
console.log('未获取到openid,跳过云端同步')
return
}
sync.syncOrEnqueue({ kind: 'anniversary', action, data: { id, ...data } })
}
})