Support personal records and persistent smoking visibility. Add upload quota preflight, file cleanup, privacy entry, image caching and revision-based page refresh across the backend, admin and mini program.
1684 lines
55 KiB
JavaScript
1684 lines
55 KiB
JavaScript
const { tryLoginRenewal } = require('../../utils/subscribe');
|
||
const { getLayout } = require('../../utils/layout');
|
||
// pages/period/period.js - 姨姨(美柚风格日历模式)
|
||
const { api } = require('../../utils/request');
|
||
|
||
|
||
Page({
|
||
onLoginSubscribeTap: function() { return tryLoginRenewal(); },
|
||
onResize: function() { this.setData(getLayout()); },
|
||
data: {
|
||
// 日历
|
||
currentYear: 0,
|
||
currentMonth: 0,
|
||
calendarDays: [],
|
||
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
||
selectedDate: '',
|
||
selectedDateLabel: '',
|
||
selectedWeekday: '',
|
||
|
||
// 统计数据
|
||
cycleDay: 0,
|
||
predictedDays: 0,
|
||
avgCycle: 0,
|
||
periodStatusText: '暂无数据',
|
||
periodStatusClass: '',
|
||
|
||
// 每日记录
|
||
dailyRecord: {},
|
||
hasDetailContent: false,
|
||
|
||
// 周期列表
|
||
cycles: [],
|
||
savedCycles: [], // period_records 原始周期记录(用于姨姨设置弹窗预填,还原真实周期参数)
|
||
dailyRecordsMap: {}, // key: dateStr -> record
|
||
|
||
// 弹窗控制
|
||
showCycleModal: false,
|
||
showFormula: false,
|
||
showPeriodModal: false,
|
||
// 经期详情表单(量、颜色、疼痛)
|
||
periodForm: { amount: '', color: '', pain: '' },
|
||
periodAmountOptions: [
|
||
{ value: '极少', label: '极少' },
|
||
{ value: '少', label: '少' },
|
||
{ value: '中等', label: '中等' },
|
||
{ value: '多', label: '多' },
|
||
{ value: '极多', label: '极多' }
|
||
],
|
||
periodColorOptions: [
|
||
{ value: '浅红', label: '浅红', dot: '#FFCDD2' },
|
||
{ value: '鲜红', label: '鲜红', dot: '#FF1744' },
|
||
{ value: '深红', label: '深红', dot: '#B71C1C' },
|
||
{ value: '褐色', label: '褐色', dot: '#8D6E63' },
|
||
{ value: '黑色', label: '黑色', dot: '#424242' }
|
||
],
|
||
periodPainOptions: [
|
||
{ value: '无', label: '无' },
|
||
{ value: '轻微', label: '轻微' },
|
||
{ value: '中微', label: '中微' },
|
||
{ value: '剧烈', label: '剧烈' }
|
||
],
|
||
cycleForm: {
|
||
start_date: '',
|
||
end_date: '',
|
||
cycle_length: 28,
|
||
period_length: 5,
|
||
// 推算结果
|
||
predicted_start: '',
|
||
predicted_end: '',
|
||
predicted_ovulation_day: '',
|
||
predicted_ovulation_start: '',
|
||
predicted_ovulation_end: ''
|
||
},
|
||
|
||
// 症状
|
||
showSymptomModal: false,
|
||
selectedSymptoms: [],
|
||
symptomMap: {},
|
||
symptomOptions: ['腹痛', '腰酸', '乏力', '头痛', '情绪波动', '乳房胀痛', '恶心', '腹泻', '畏寒', '失眠', '食欲增加', '无不适'],
|
||
|
||
// 心情
|
||
showMoodModal: false,
|
||
moodOptions: [
|
||
{ emoji: '😊', label: '开心' },
|
||
{ emoji: '😌', label: '平静' },
|
||
{ emoji: '😔', label: '低落' },
|
||
{ emoji: '😤', label: '烦躁' },
|
||
{ emoji: '😰', label: '焦虑' },
|
||
{ emoji: '🥱', label: '疲倦' },
|
||
{ emoji: '😭', label: '难过' },
|
||
{ emoji: '🥰', label: '甜蜜' }
|
||
],
|
||
|
||
// 白带
|
||
showDischargeModal: false,
|
||
dischargeForm: { status: '', flow: '', abnormal: [] },
|
||
dischargeAbnormalMap: {},
|
||
dischargeStatusOptions: [
|
||
{ value: '干燥', label: '干燥' },
|
||
{ value: '粘稠', label: '粘稠' },
|
||
{ value: '稀糊状', label: '稀糊状' },
|
||
{ value: '水状', label: '水状' },
|
||
{ value: '蛋清状', label: '蛋清状' }
|
||
],
|
||
dischargeFlowOptions: [
|
||
{ value: '少量', label: '少量' },
|
||
{ value: '适中', label: '适中' },
|
||
{ value: '大量', label: '大量' }
|
||
],
|
||
dischargeAbnormalOptions: [
|
||
{ value: '带血丝', label: '带血丝' },
|
||
{ value: '白带增多', label: '白带增多' },
|
||
{ value: '泡沫状', label: '泡沫状' },
|
||
{ value: '渣渣多', label: '渣渣多' },
|
||
{ value: '偏黄', label: '偏黄' },
|
||
{ value: '偏绿', label: '偏绿' },
|
||
{ value: '异味', label: '异味' }
|
||
],
|
||
|
||
// 体温
|
||
showTempModal: false,
|
||
tempValue: '',
|
||
|
||
// 体重
|
||
showWeightModal: false,
|
||
weightValue: '',
|
||
|
||
// 备注
|
||
showNoteModal: false,
|
||
noteValue: '',
|
||
|
||
// 权限设置
|
||
periodVisible: true,
|
||
visibilitySaving: false,
|
||
hasCouple: false,
|
||
accessLoading: true,
|
||
accessDenied: false,
|
||
selectionRequired: false,
|
||
myUserId: 0,
|
||
partnerUserId: 0,
|
||
myPreview: {},
|
||
partnerPreview: {},
|
||
|
||
// 爱爱记录(支持一天多条)
|
||
showSexModal: false,
|
||
sexRecords: [], // 当前选中日期的爱爱记录列表
|
||
sexRecordsMap: {}, // key: dateStr -> [{id, record_time, measure, measure_custom}]
|
||
sexForm: {
|
||
record_time: '',
|
||
measure: '',
|
||
remark: ''
|
||
},
|
||
_lastFetchTime: 0,
|
||
measureOptions: [
|
||
{ value: '无措施', label: '无措施', icon: '🚫' },
|
||
{ value: '避孕套', label: '避孕套', icon: '🛡️' },
|
||
{ value: '短效避孕药', label: '短效避孕药', icon: '💊' },
|
||
{ value: '长效避孕药', label: '长效避孕药', icon: '💊' },
|
||
{ value: '紧急避孕药', label: '紧急避孕药', icon: '💊' },
|
||
{ value: '自慰', label: '自慰', icon: '🖐️' },
|
||
{ value: '体外排精', label: '体外排精', icon: '⚠️' },
|
||
{ value: '未射精', label: '未射精', icon: '⏹️' },
|
||
{ value: '节育环', label: '节育环', icon: '⭕' },
|
||
{ value: '其他措施', label: '其他措施', icon: '📌' }
|
||
]
|
||
},
|
||
|
||
onLoad: function() {
|
||
this.setData(getLayout());
|
||
var now = new Date();
|
||
this.setData({
|
||
currentYear: now.getFullYear(),
|
||
currentMonth: now.getMonth() + 1
|
||
});
|
||
// 权限必须先从服务端确认,避免缓存中的姨姨数据短暂泄露。
|
||
this.loadData();
|
||
},
|
||
|
||
onShow: function() {
|
||
// 返回本页时重新确认伴侣是否更改了可见权限。
|
||
if (!this.data.accessLoading) this.loadData();
|
||
if (!this._permissionTimer) {
|
||
var self = this;
|
||
this._permissionTimer = setInterval(function() {
|
||
api.periodVisibleGet().then(function(access) {
|
||
var denied = access.can_access === false;
|
||
if (denied !== self.data.accessDenied) {
|
||
if (denied) self.setData({
|
||
accessDenied: true, cycles: [], savedCycles: [], dailyRecordsMap: {}, sexRecordsMap: {},
|
||
showCycleModal: false, showPeriodModal: false
|
||
});
|
||
self.loadData();
|
||
}
|
||
}).catch(function() {});
|
||
}, 10000);
|
||
}
|
||
},
|
||
|
||
onHide: function() {
|
||
if (this._permissionTimer) clearInterval(this._permissionTimer);
|
||
this._permissionTimer = null;
|
||
},
|
||
|
||
onUnload: function() {
|
||
if (this._permissionTimer) clearInterval(this._permissionTimer);
|
||
this._permissionTimer = null;
|
||
},
|
||
|
||
onPullDownRefresh: async function() {
|
||
await this.loadData();
|
||
wx.stopPullDownRefresh();
|
||
},
|
||
|
||
// ========== 数据加载 ==========
|
||
loadData: async function() {
|
||
try {
|
||
const res = await api.periodList();
|
||
var daily = res.daily || [];
|
||
var sexRecords = res.sex_records || [];
|
||
|
||
// 构建每日记录映射
|
||
var dailyMap = {};
|
||
for (var i = 0; i < daily.length; i++) {
|
||
var item = daily[i];
|
||
// 解析经期详情 JSON(量/颜色/疼痛),供弹窗高亮
|
||
if (item.period_detail) {
|
||
try {
|
||
item.period_detail = JSON.parse(item.period_detail);
|
||
} catch (e) {
|
||
item.period_detail = null;
|
||
}
|
||
}
|
||
dailyMap[item.record_date] = item;
|
||
}
|
||
|
||
// 构建爱爱记录映射(按日期分组)
|
||
var sexMap = {};
|
||
for (var i = 0; i < sexRecords.length; i++) {
|
||
var dateStr = sexRecords[i].record_date;
|
||
if (!sexMap[dateStr]) sexMap[dateStr] = [];
|
||
sexMap[dateStr].push(sexRecords[i]);
|
||
}
|
||
|
||
// 读取权限设置
|
||
var periodVisible = res.period_visible !== undefined ? !!res.period_visible : this.data.periodVisible;
|
||
|
||
if (res.can_access === false) {
|
||
try {
|
||
} catch (e) {}
|
||
this.setData({
|
||
cycles: [], savedCycles: [], dailyRecordsMap: {}, sexRecordsMap: {},
|
||
dailyRecord: {}, hasDetailContent: false,
|
||
showCycleModal: false, showPeriodModal: false, showSymptomModal: false,
|
||
showMoodModal: false, showDischargeModal: false, showTempModal: false,
|
||
showWeightModal: false, showNoteModal: false, showSexModal: false,
|
||
periodVisible: periodVisible, hasCouple: true,
|
||
accessLoading: false, accessDenied: true, selectionRequired: false
|
||
});
|
||
this.calculateStats([]);
|
||
this.buildCalendar();
|
||
return;
|
||
}
|
||
|
||
if (res.selection_required) {
|
||
this.setData({
|
||
cycles: [], savedCycles: [], dailyRecordsMap: {}, sexRecordsMap: {},
|
||
dailyRecord: {}, hasDetailContent: false,
|
||
periodVisible: periodVisible, hasCouple: true,
|
||
accessLoading: false, accessDenied: false, selectionRequired: true,
|
||
myUserId: res.my_user_id, partnerUserId: res.partner_user_id,
|
||
myPreview: res.my_preview || {}, partnerPreview: res.partner_preview || {}
|
||
});
|
||
this.calculateStats([]);
|
||
if (!this._selectionPromptShown) {
|
||
this._selectionPromptShown = true;
|
||
setTimeout(() => this.choosePeriodSource(), 0);
|
||
}
|
||
return;
|
||
}
|
||
|
||
var savedCycles = res.cycles || [];
|
||
var cycles = this._buildDisplayCycles(dailyMap, savedCycles);
|
||
|
||
this.setData({
|
||
cycles: cycles,
|
||
savedCycles: savedCycles,
|
||
dailyRecordsMap: dailyMap,
|
||
sexRecordsMap: sexMap,
|
||
periodVisible: periodVisible,
|
||
hasCouple: !!res.has_couple,
|
||
accessLoading: false,
|
||
accessDenied: false,
|
||
selectionRequired: false,
|
||
_lastFetchTime: Date.now()
|
||
});
|
||
|
||
// 写入本地缓存
|
||
|
||
this.calculateStats(cycles);
|
||
this.buildCalendar();
|
||
|
||
// 如果选中日期存在,刷新详情
|
||
if (this.data.selectedDate) {
|
||
this.refreshSelectedDayDetail();
|
||
}
|
||
} catch(e) {
|
||
console.error('从服务器加载数据失败:', e);
|
||
this.setData({ accessLoading: false });
|
||
wx.showToast({ title: '姨姨数据加载失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// 从 dailyRecordsMap 动态生成周期列表
|
||
// 将有月经记录(period > 0)的连续日期合并为一个周期
|
||
_buildCyclesFromDaily: function(dailyMap) {
|
||
if (!dailyMap) return [];
|
||
// 收集所有有月经记录的日期,按日期排序
|
||
var periodDates = [];
|
||
for (var dateStr in dailyMap) {
|
||
if (dailyMap.hasOwnProperty(dateStr)) {
|
||
var record = dailyMap[dateStr];
|
||
if (record && parseInt(record.period) > 0) {
|
||
periodDates.push(dateStr);
|
||
}
|
||
}
|
||
}
|
||
if (periodDates.length === 0) return [];
|
||
|
||
periodDates.sort();
|
||
|
||
// 将连续日期合并为周期
|
||
var cycles = [];
|
||
var currentStart = periodDates[0];
|
||
var currentEnd = periodDates[0];
|
||
var prevDate = periodDates[0];
|
||
|
||
for (var i = 1; i < periodDates.length; i++) {
|
||
var curr = periodDates[i];
|
||
// 检查是否连续(相邻日期差1天)
|
||
var prev = new Date(prevDate);
|
||
var next = new Date(curr);
|
||
var diff = Math.round((next - prev) / (1000 * 60 * 60 * 24));
|
||
if (diff === 1) {
|
||
currentEnd = curr;
|
||
} else {
|
||
// 不连续,结束当前周期,开始新周期
|
||
cycles.push({
|
||
start_date: currentStart,
|
||
end_date: currentEnd,
|
||
cycle_length: 28,
|
||
period_length: this._dayDiff(currentStart, currentEnd) + 1
|
||
});
|
||
currentStart = curr;
|
||
currentEnd = curr;
|
||
}
|
||
prevDate = curr;
|
||
}
|
||
// 最后一个周期
|
||
cycles.push({
|
||
start_date: currentStart,
|
||
end_date: currentEnd,
|
||
cycle_length: 28,
|
||
period_length: this._dayDiff(currentStart, currentEnd) + 1
|
||
});
|
||
|
||
// 按 start_date 降序(最新的在前面)
|
||
cycles.sort(function(a, b) { return b.start_date.localeCompare(a.start_date); });
|
||
|
||
// 计算每个周期的 cycle_length(与前一个周期的间隔)
|
||
for (var i = 0; i < cycles.length; i++) {
|
||
if (i < cycles.length - 1) {
|
||
// 当前周期与下一个(更早的)周期的间隔
|
||
var diff = this._dayDiff(cycles[i + 1].start_date, cycles[i].start_date);
|
||
if (diff > 0) {
|
||
cycles[i].cycle_length = diff;
|
||
}
|
||
}
|
||
}
|
||
|
||
return cycles;
|
||
},
|
||
|
||
// 计算两个日期字符串之间的天数差(date2 - date1)
|
||
_dayDiff: function(dateStr1, dateStr2) {
|
||
var d1 = new Date(dateStr1);
|
||
var d2 = new Date(dateStr2);
|
||
return Math.round((d2 - d1) / (1000 * 60 * 60 * 24));
|
||
},
|
||
|
||
// ========== 统计数据计算 ==========
|
||
calculateStats: function(cycles) {
|
||
if (!cycles || cycles.length === 0) {
|
||
this.setData({
|
||
cycleDay: 0,
|
||
predictedDays: 0,
|
||
avgCycle: 0,
|
||
periodStatusText: '暂无数据',
|
||
periodStatusClass: ''
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 排序副本,不修改原数组
|
||
var sorted = [].concat(cycles).sort(function(a, b) { return b.start_date.localeCompare(a.start_date); });
|
||
|
||
var now = new Date();
|
||
var today = this._formatDate(now);
|
||
var last = sorted[0];
|
||
var lastStart = new Date(last.start_date);
|
||
var lastEnd = new Date(last.end_date);
|
||
|
||
// 平均周期
|
||
var totalCycle = 0;
|
||
var cycleCount = 0;
|
||
for (var i = 0; i < sorted.length; i++) {
|
||
var c = parseInt(sorted[i].cycle_length);
|
||
if (c > 0) { totalCycle += c; cycleCount++; }
|
||
}
|
||
var avgCycle = cycleCount > 0 ? Math.round(totalCycle / cycleCount) : 28;
|
||
var predictionCycle = parseInt(last.cycle_length) || avgCycle;
|
||
|
||
// 周期天数计算
|
||
// 如果今天在经期内:显示"经期第X天"(从开始日算)
|
||
// 如果今天在经期后:显示"周期第X天"(从开始日算),同时显示"经期结束第Y天"
|
||
var diffFromStart = Math.floor((now - lastStart) / (1000 * 60 * 60 * 24));
|
||
var cycleDay = diffFromStart + 1;
|
||
if (cycleDay < 1) cycleDay = 1;
|
||
|
||
// 预计下次:从开始日算起,经过 avgCycle 天为下次经期开始
|
||
// 剩余天数 = avgCycle - cycleDay
|
||
var predictedDays = predictionCycle - cycleDay;
|
||
if (predictedDays < 0) predictedDays = 0;
|
||
|
||
// 状态
|
||
var statusText = '';
|
||
var statusClass = '';
|
||
if (today >= last.start_date && today <= last.end_date) {
|
||
statusText = '生理期 · 留意身体状态,适当休息';
|
||
statusClass = 'period';
|
||
} else if (predictedDays <= 0) {
|
||
statusText = '本次生理期已结束';
|
||
statusClass = 'safe';
|
||
} else if (predictedDays <= 7) {
|
||
statusText = '预计 ' + predictedDays + ' 天后生理期开始';
|
||
statusClass = 'ovulation';
|
||
} else {
|
||
statusText = '距离下次生理期还有 ' + predictedDays + ' 天';
|
||
statusClass = 'safe';
|
||
}
|
||
|
||
this.setData({
|
||
cycleDay: cycleDay,
|
||
predictedDays: predictedDays,
|
||
avgCycle: predictionCycle,
|
||
periodStatusText: statusText,
|
||
periodStatusClass: statusClass
|
||
});
|
||
},
|
||
|
||
// ========== 日历构建 ==========
|
||
buildCalendar: function() {
|
||
var year = this.data.currentYear;
|
||
var month = this.data.currentMonth;
|
||
var dailyMap = this.data.dailyRecordsMap;
|
||
|
||
var daysInMonth = new Date(year, month, 0).getDate();
|
||
// 当月 1 号是星期几(0=周日,1=周一 ... 6=周六),用于月首补齐空白格,使日期对齐到正确的星期列
|
||
var firstWeekday = new Date(year, month - 1, 1).getDay();
|
||
|
||
var today = this._formatDate(new Date());
|
||
var selectedDate = this.data.selectedDate;
|
||
var days = [];
|
||
|
||
// 月首补齐空白占位格,让 1 号落在正确的星期列(修复星期与日期错位问题)
|
||
for (var i = 0; i < firstWeekday; i++) {
|
||
days.push({
|
||
day: '',
|
||
dateStr: '',
|
||
className: 'empty',
|
||
periodLevel: 0,
|
||
hasSex: false,
|
||
sexCount: 0,
|
||
hasRecord: false,
|
||
isFuture: false,
|
||
isToday: false
|
||
});
|
||
}
|
||
|
||
// 只显示当月日期,不补位上月和下月
|
||
for (var d = 1; d <= daysInMonth; d++) {
|
||
var dateStr = year + '-' + this._pad(month) + '-' + this._pad(d);
|
||
var dayInfo = this._buildDayInfo(d, dateStr, today, selectedDate, dailyMap);
|
||
days.push(dayInfo);
|
||
}
|
||
|
||
this.setData({ calendarDays: days });
|
||
},
|
||
|
||
_buildDayInfo: function(dayNum, dateStr, today, selectedDate, dailyMap) {
|
||
var className = '';
|
||
if (dateStr === today) className += 'today ';
|
||
if (dateStr === selectedDate) className += 'selected ';
|
||
|
||
// 标记未来日期(仅用于点击限制)
|
||
var isFuture = dateStr > today;
|
||
|
||
var record = dailyMap[dateStr];
|
||
var sexMap = this.data.sexRecordsMap;
|
||
var sexRecords = sexMap[dateStr] || [];
|
||
var periodLevel = 0;
|
||
var hasSex = sexRecords.length > 0;
|
||
var sexCount = sexRecords.length;
|
||
var hasRecord = false;
|
||
var phaseClass = ''; // 周期阶段:period/safe/ovulation
|
||
|
||
if (record) {
|
||
periodLevel = parseInt(record.period) || 0;
|
||
hasRecord = record.symptoms || record.mood || record.discharge || record.temperature || record.weight || record.note;
|
||
if (periodLevel > 0) className += 'period-day ';
|
||
}
|
||
|
||
// 计算周期阶段背景色
|
||
phaseClass = this._getCyclePhase(dateStr);
|
||
if (phaseClass) className += phaseClass + ' ';
|
||
|
||
return {
|
||
day: dayNum,
|
||
dateStr: dateStr,
|
||
className: className,
|
||
periodLevel: periodLevel,
|
||
hasSex: hasSex,
|
||
sexCount: sexCount,
|
||
hasRecord: hasRecord,
|
||
isFuture: isFuture,
|
||
isToday: dateStr === today
|
||
};
|
||
},
|
||
|
||
// 判断某日期属于哪个周期阶段
|
||
// 返回值:'phase-period' 已记录经期 | 'phase-predicted' 预测经期 | 'phase-ovulation-day' 排卵日 | 'phase-ovulation' 排卵期 | 'phase-safe' 安全期
|
||
_getCyclePhase: function(dateStr) {
|
||
var cycles = this.data.cycles;
|
||
if (!cycles || cycles.length === 0) return '';
|
||
|
||
// 排序副本,不修改原数组
|
||
var sorted = [].concat(cycles).sort(function(a, b) { return b.start_date.localeCompare(a.start_date); });
|
||
var last = sorted[0];
|
||
var avgCycle = parseInt(last.cycle_length) || this.data.avgCycle || 28;
|
||
var periodLen = parseInt(last.period_length) || 5;
|
||
|
||
var current = new Date(dateStr);
|
||
var dateTime = current.getTime();
|
||
|
||
var lastStart = new Date(last.start_date);
|
||
var lastEnd = new Date(last.end_date);
|
||
|
||
// ----- 1. 检查是否在已记录的经期内(已知周期) -----
|
||
for (var i = 0; i < cycles.length; i++) {
|
||
var cs = new Date(cycles[i].start_date);
|
||
var ce = new Date(cycles[i].end_date);
|
||
if (dateTime >= cs.getTime() && dateTime <= ce.getTime()) {
|
||
return 'phase-period';
|
||
}
|
||
}
|
||
|
||
// ----- 2. 预测下一次经期(只预测下一次) -----
|
||
var nextPeriodStart = new Date(lastStart);
|
||
nextPeriodStart.setDate(nextPeriodStart.getDate() + avgCycle);
|
||
var nextPeriodEnd = new Date(nextPeriodStart);
|
||
nextPeriodEnd.setDate(nextPeriodEnd.getDate() + periodLen - 1);
|
||
|
||
if (dateTime >= nextPeriodStart.getTime() && dateTime <= nextPeriodEnd.getTime()) {
|
||
return 'phase-predicted';
|
||
}
|
||
|
||
// ----- 3. 计算排卵日 & 排卵期(基于下一次经期推算) -----
|
||
// 排卵日 = 下次经期前14天
|
||
var ovulationDay = new Date(nextPeriodStart);
|
||
ovulationDay.setDate(ovulationDay.getDate() - 14);
|
||
var ovDayTime = ovulationDay.getTime();
|
||
|
||
// 排卵期 = 排卵日前3天 ~ 排卵日后3天(共7天,排卵日居中)
|
||
var ovStart = new Date(ovulationDay);
|
||
ovStart.setDate(ovStart.getDate() - 3);
|
||
var ovEnd = new Date(ovulationDay);
|
||
ovEnd.setDate(ovEnd.getDate() + 3);
|
||
|
||
if (dateTime >= ovStart.getTime() && dateTime <= ovEnd.getTime()) {
|
||
// 如果是排卵日当天
|
||
if (dateStr === this._formatDate(ovulationDay)) {
|
||
return 'phase-ovulation-day';
|
||
}
|
||
return 'phase-ovulation';
|
||
}
|
||
|
||
// ----- 4. 其他为安全期 -----
|
||
return 'phase-safe';
|
||
},
|
||
|
||
// ========== 日历导航 ==========
|
||
prevMonth: function() {
|
||
var y = this.data.currentYear;
|
||
var m = this.data.currentMonth;
|
||
if (m === 1) { y--; m = 12; } else { m--; }
|
||
this.setData({ currentYear: y, currentMonth: m, selectedDate: '' });
|
||
this.buildCalendar();
|
||
},
|
||
|
||
nextMonth: function() {
|
||
var y = this.data.currentYear;
|
||
var m = this.data.currentMonth;
|
||
if (m === 12) { y++; m = 1; } else { m++; }
|
||
this.setData({ currentYear: y, currentMonth: m, selectedDate: '' });
|
||
this.buildCalendar();
|
||
},
|
||
|
||
// ========== 日期点击 ==========
|
||
onDayTap: function(e) {
|
||
var dateStr = e.currentTarget.dataset.date;
|
||
var today = this._formatDate(new Date());
|
||
|
||
// 禁止选择未来日期
|
||
if (dateStr > today) {
|
||
wx.showToast({ title: '无法记录未来的日期', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
var weekday = new Date(dateStr).getDay();
|
||
var weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||
var parts = dateStr.split('-');
|
||
var label = parseInt(parts[0]) + '年' + parseInt(parts[1]) + '月' + parseInt(parts[2]) + '日';
|
||
|
||
this.setData({
|
||
selectedDate: dateStr,
|
||
selectedDateLabel: label,
|
||
selectedWeekday: weekNames[weekday]
|
||
});
|
||
|
||
this.refreshSelectedDayDetail();
|
||
this.buildCalendar(); // 刷新选中状态
|
||
},
|
||
|
||
refreshSelectedDayDetail: function() {
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) return;
|
||
|
||
var dailyMap = this.data.dailyRecordsMap;
|
||
var record = dailyMap[dateStr] || {};
|
||
var sexMap = this.data.sexRecordsMap;
|
||
var sexRecords = sexMap[dateStr] || [];
|
||
|
||
this.setData({
|
||
dailyRecord: record,
|
||
sexRecords: sexRecords,
|
||
hasDetailContent: !!(parseInt(record.period) > 0 || sexRecords.length > 0 || record.symptoms || record.mood || record.discharge || record.temperature || record.weight || record.note)
|
||
});
|
||
},
|
||
|
||
// ========== 快捷事件操作 ==========
|
||
// 通用:清空每日记录中的单个字段并保存
|
||
_clearField: function(field) {
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var mapRecord = this.data.dailyRecordsMap[dateStr] || {};
|
||
var record = JSON.parse(JSON.stringify(mapRecord));
|
||
if (field === 'period') {
|
||
record.period = '0';
|
||
record.period_detail = null;
|
||
} else if (field === 'discharge') {
|
||
record.discharge = '';
|
||
record.discharge_detail = null;
|
||
} else if (field === 'temperature') {
|
||
record.temperature = null;
|
||
} else if (field === 'weight') {
|
||
record.weight = null;
|
||
}
|
||
this._saveDailyRecord(record);
|
||
},
|
||
|
||
// 月经 - 有记录时打开详情弹窗可查看/修改,无记录时也打开弹窗
|
||
onPeriodTap: function() {
|
||
this.togglePeriod();
|
||
},
|
||
|
||
// 白带 - 有记录时打开详情弹窗可查看/修改,无记录时也打开弹窗
|
||
onDischargeTap: function() {
|
||
this.showDischargePicker();
|
||
},
|
||
|
||
// 体温 - 有记录时点击直接清空,无记录时打开输入弹窗
|
||
onTempTap: function() {
|
||
if (this.data.dailyRecord.temperature) {
|
||
this._clearField('temperature');
|
||
} else {
|
||
this.showTempInput();
|
||
}
|
||
},
|
||
|
||
// 体重 - 有记录时点击直接清空,无记录时打开输入弹窗
|
||
onWeightTap: function() {
|
||
if (this.data.dailyRecord.weight) {
|
||
this._clearField('weight');
|
||
} else {
|
||
this.showWeightInput();
|
||
}
|
||
},
|
||
|
||
// 经期 - 显示详情弹窗(量、颜色、疼痛)
|
||
togglePeriod: function() {
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var today = this._formatDate(new Date());
|
||
if (dateStr > today) {
|
||
wx.showToast({ title: '无法记录未来的日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var record = this.data.dailyRecord || {};
|
||
// 从已有记录解析经期详情
|
||
var periodDetail = record.period_detail || {};
|
||
this.setData({
|
||
showPeriodModal: true,
|
||
periodForm: {
|
||
amount: periodDetail.amount || '',
|
||
color: periodDetail.color || '',
|
||
pain: periodDetail.pain || ''
|
||
}
|
||
});
|
||
},
|
||
// 点击选项只切换选中状态,点确定才保存
|
||
onPeriodAmount: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.periodForm;
|
||
if (form.amount === val) {
|
||
form.amount = '';
|
||
} else {
|
||
form.amount = val;
|
||
}
|
||
this.setData({ periodForm: form });
|
||
},
|
||
onPeriodColor: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.periodForm;
|
||
if (form.color === val) {
|
||
form.color = '';
|
||
} else {
|
||
form.color = val;
|
||
}
|
||
this.setData({ periodForm: form });
|
||
},
|
||
onPeriodPain: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.periodForm;
|
||
if (form.pain === val) {
|
||
form.pain = '';
|
||
} else {
|
||
form.pain = val;
|
||
}
|
||
this.setData({ periodForm: form });
|
||
},
|
||
confirmPeriodDetail: function() {
|
||
var form = this.data.periodForm;
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
if (form.amount || form.color || form.pain) {
|
||
record.period = '1';
|
||
record.period_detail = {
|
||
amount: form.amount,
|
||
color: form.color,
|
||
pain: form.pain
|
||
};
|
||
} else {
|
||
record.period = '0';
|
||
record.period_detail = null;
|
||
}
|
||
this.setData({ showPeriodModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closePeriodModal: function() {
|
||
this.setData({ showPeriodModal: false });
|
||
},
|
||
|
||
// 爱爱 - 显示爱爱记录弹窗
|
||
showSexRecords: function() {
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var today = this._formatDate(new Date());
|
||
if (dateStr > today) {
|
||
wx.showToast({ title: '无法记录未来的日期', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
var sexMap = this.data.sexRecordsMap;
|
||
var records = sexMap[dateStr] || [];
|
||
|
||
// 设置默认时间(当前时间,精确到分钟)
|
||
var now = new Date();
|
||
var hh = this._pad(now.getHours());
|
||
var mm = this._pad(now.getMinutes());
|
||
var defaultTime = hh + ':' + mm;
|
||
|
||
this.setData({
|
||
showSexModal: true,
|
||
sexRecords: records,
|
||
'sexForm.record_time': defaultTime,
|
||
'sexForm.measure': '',
|
||
'sexForm.remark': ''
|
||
});
|
||
},
|
||
|
||
// 爱爱 - 时间选择
|
||
onSexTimeChange: function(e) {
|
||
this.setData({ 'sexForm.record_time': e.detail.value });
|
||
},
|
||
|
||
// 爱爱 - 措施选择
|
||
onSexMeasureSelect: function(e) {
|
||
var measure = e.currentTarget.dataset.measure;
|
||
this.setData({ 'sexForm.measure': measure });
|
||
},
|
||
// 爱爱 - 备注输入
|
||
onSexRemarkInput: function(e) {
|
||
this.setData({ 'sexForm.remark': e.detail.value });
|
||
},
|
||
|
||
// 爱爱 - 确认添加记录
|
||
confirmSexAdd: async function() {
|
||
var dateStr = this.data.selectedDate;
|
||
var form = this.data.sexForm;
|
||
if (!form.record_time) {
|
||
wx.showToast({ title: '请选择时间', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!form.measure) {
|
||
wx.showToast({ title: '请选择措施', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
wx.showLoading({ title: '保存中...', mask: true });
|
||
try {
|
||
await api.periodSexAdd({
|
||
record_date: dateStr,
|
||
record_time: form.record_time,
|
||
measure: form.measure,
|
||
measure_custom: '',
|
||
remark: form.remark || ''
|
||
});
|
||
wx.hideLoading();
|
||
|
||
// 刷新本地数据
|
||
await this._refreshSexRecords(dateStr);
|
||
// 添加成功后关闭弹窗
|
||
this.setData({ showSexModal: false });
|
||
wx.showToast({ title: '添加成功', icon: 'success' });
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
console.error('添加爱爱记录失败:', e);
|
||
wx.showToast({ title: '添加失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// 删除每日记录中的单个字段(月经/白带/体温/体重)
|
||
deletePeriodField: async function(e) {
|
||
var field = e.currentTarget.dataset.field;
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) return;
|
||
|
||
var fieldLabel = { period: '月经记录', discharge: '白带记录', temperature: '体温记录', weight: '体重记录' };
|
||
var label = fieldLabel[field] || '该记录';
|
||
|
||
wx.showModal({
|
||
title: '确认删除',
|
||
content: '确定删除' + label + '吗?',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
var mapRecord = this.data.dailyRecordsMap[dateStr] || {};
|
||
var record = JSON.parse(JSON.stringify(mapRecord));
|
||
if (field === 'period') {
|
||
record.period = '0';
|
||
record.period_detail = null;
|
||
} else if (field === 'discharge') {
|
||
record.discharge = '';
|
||
record.discharge_detail = null;
|
||
} else if (field === 'temperature') {
|
||
record.temperature = null;
|
||
} else if (field === 'weight') {
|
||
record.weight = null;
|
||
}
|
||
this._saveDailyRecord(record);
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 爱爱 - 删除记录
|
||
deleteSexRecord: async function(e) {
|
||
var id = e.currentTarget.dataset.id;
|
||
var dateStr = this.data.selectedDate;
|
||
|
||
wx.showModal({
|
||
title: '确认删除',
|
||
content: '确定删除这条爱爱记录吗?',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
wx.showLoading({ title: '删除中...', mask: true });
|
||
try {
|
||
await api.periodSexDelete({ id: id });
|
||
wx.hideLoading();
|
||
await this._refreshSexRecords(dateStr);
|
||
wx.showToast({ title: '已删除', icon: 'success' });
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
console.error('删除爱爱记录失败:', e);
|
||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 爱爱 - 长按日历格子删除
|
||
onDayLongPress: function(e) {
|
||
var dateStr = e.currentTarget.dataset.date;
|
||
var sexMap = this.data.sexRecordsMap;
|
||
var records = sexMap[dateStr] || [];
|
||
|
||
if (records.length === 0) {
|
||
wx.showToast({ title: '该日期没有爱爱记录', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 显示操作菜单选择要删除的记录
|
||
var itemList = records.map(function(r) {
|
||
var label = r.record_time ? r.record_time.substring(0, 5) : '未知时间';
|
||
label += ' - ' + (r.measure || '未知');
|
||
return label;
|
||
});
|
||
|
||
var self = this;
|
||
wx.showActionSheet({
|
||
itemList: itemList,
|
||
success: function(res) {
|
||
var idx = res.tapIndex;
|
||
var record = records[idx];
|
||
wx.showModal({
|
||
title: '确认删除',
|
||
content: '确定删除这条爱爱记录吗?',
|
||
success: async (modalRes) => {
|
||
if (modalRes.confirm) {
|
||
wx.showLoading({ title: '删除中...', mask: true });
|
||
try {
|
||
await api.periodSexDelete({ id: record.id });
|
||
wx.hideLoading();
|
||
await self._refreshSexRecords(dateStr);
|
||
wx.showToast({ title: '已删除', icon: 'success' });
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
console.error('删除爱爱记录失败:', e);
|
||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
|
||
// 爱爱 - 刷新某日期的爱爱记录
|
||
_refreshSexRecords: async function(dateStr) {
|
||
try {
|
||
var res = await api.periodSexList({ record_date: dateStr });
|
||
var records = Array.isArray(res) ? res : (res.records || []);
|
||
var sexMap = this.data.sexRecordsMap;
|
||
sexMap[dateStr] = records;
|
||
|
||
this.setData({
|
||
sexRecords: records,
|
||
sexRecordsMap: sexMap,
|
||
'sexForm.record_time': this._pad(new Date().getHours()) + ':' + this._pad(new Date().getMinutes()),
|
||
'sexForm.measure': '',
|
||
'sexForm.remark': ''
|
||
});
|
||
|
||
// 重新生成周期并写入缓存
|
||
var newCycles = this._buildDisplayCycles(this.data.dailyRecordsMap, this.data.savedCycles);
|
||
this.setData({ cycles: newCycles });
|
||
this.calculateStats(newCycles);
|
||
|
||
// 刷新日历和详情
|
||
this.buildCalendar();
|
||
this.refreshSelectedDayDetail();
|
||
} catch(e) {
|
||
console.error('刷新爱爱记录失败:', e);
|
||
}
|
||
},
|
||
|
||
// 症状
|
||
showSymptomPicker: function() {
|
||
if (!this.data.selectedDate) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var current = mapRecord.symptoms || '';
|
||
var selected = current ? current.split(',') : [];
|
||
var symptomMap = {};
|
||
for (var i = 0; i < selected.length; i++) {
|
||
symptomMap[selected[i]] = true;
|
||
}
|
||
this.setData({
|
||
showSymptomModal: true,
|
||
selectedSymptoms: selected,
|
||
symptomMap: symptomMap
|
||
});
|
||
},
|
||
toggleSymptom: function(e) {
|
||
var symptom = e.currentTarget.dataset.symptom;
|
||
var arr = [].concat(this.data.selectedSymptoms);
|
||
var idx = arr.indexOf(symptom);
|
||
if (symptom === '无不适') {
|
||
this.setData({ selectedSymptoms: ['无不适'], symptomMap: { '无不适': true } });
|
||
return;
|
||
}
|
||
var noIdx = arr.indexOf('无不适');
|
||
if (noIdx > -1) arr.splice(noIdx, 1);
|
||
if (idx > -1) {
|
||
arr.splice(idx, 1);
|
||
} else {
|
||
arr.push(symptom);
|
||
}
|
||
var symptomMap = {};
|
||
for (var i = 0; i < arr.length; i++) {
|
||
symptomMap[arr[i]] = true;
|
||
}
|
||
this.setData({ selectedSymptoms: arr, symptomMap: symptomMap });
|
||
},
|
||
confirmSymptoms: function() {
|
||
var symptomsStr = this.data.selectedSymptoms.join(',');
|
||
// 从完整 map 中获取记录,避免丢失已有字段
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
record.symptoms = symptomsStr;
|
||
this.setData({ showSymptomModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeSymptomModal: function() {
|
||
this.setData({ showSymptomModal: false });
|
||
},
|
||
|
||
// 心情
|
||
showMoodPicker: function() {
|
||
this.setData({ showMoodModal: true });
|
||
},
|
||
selectMood: function(e) {
|
||
var mood = e.currentTarget.dataset.mood;
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
record.mood = record.mood === mood ? '' : mood;
|
||
this.setData({ showMoodModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeMoodModal: function() {
|
||
this.setData({ showMoodModal: false });
|
||
},
|
||
|
||
// 白带
|
||
showDischargePicker: function() {
|
||
if (!this.data.selectedDate) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var record = this.data.dailyRecord || {};
|
||
var dischargeDetail = record.discharge_detail || {};
|
||
var abnormalArr = dischargeDetail.abnormal || [];
|
||
var abnormalMap = {};
|
||
for (var i = 0; i < abnormalArr.length; i++) {
|
||
abnormalMap[abnormalArr[i]] = true;
|
||
}
|
||
this.setData({
|
||
showDischargeModal: true,
|
||
dischargeForm: {
|
||
status: dischargeDetail.status || '',
|
||
flow: dischargeDetail.flow || '',
|
||
abnormal: abnormalArr
|
||
},
|
||
dischargeAbnormalMap: abnormalMap
|
||
});
|
||
},
|
||
// 点击选项只切换选中状态,点确定才保存
|
||
onDischargeStatus: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.dischargeForm;
|
||
if (form.status === val) {
|
||
form.status = '';
|
||
} else {
|
||
form.status = val;
|
||
}
|
||
this.setData({ dischargeForm: form });
|
||
},
|
||
onDischargeFlow: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.dischargeForm;
|
||
if (form.flow === val) {
|
||
form.flow = '';
|
||
} else {
|
||
form.flow = val;
|
||
}
|
||
this.setData({ dischargeForm: form });
|
||
},
|
||
onDischargeAbnormal: function(e) {
|
||
var val = e.currentTarget.dataset.value;
|
||
var form = this.data.dischargeForm;
|
||
var arr = [].concat(form.abnormal);
|
||
var idx = arr.indexOf(val);
|
||
if (idx > -1) {
|
||
arr.splice(idx, 1);
|
||
} else {
|
||
arr.push(val);
|
||
}
|
||
var abnormalMap = {};
|
||
for (var i = 0; i < arr.length; i++) {
|
||
abnormalMap[arr[i]] = true;
|
||
}
|
||
this.setData({
|
||
dischargeForm: {
|
||
status: form.status,
|
||
flow: form.flow,
|
||
abnormal: arr
|
||
},
|
||
dischargeAbnormalMap: abnormalMap
|
||
});
|
||
},
|
||
confirmDischargeDetail: function() {
|
||
var form = this.data.dischargeForm;
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
var parts = [];
|
||
if (form.status) parts.push(form.status);
|
||
if (form.flow) parts.push(form.flow);
|
||
if (form.abnormal.length > 0) parts.push(form.abnormal.join('、'));
|
||
record.discharge = parts.join(' · ') || '';
|
||
record.discharge_detail = {
|
||
status: form.status,
|
||
flow: form.flow,
|
||
abnormal: form.abnormal
|
||
};
|
||
this.setData({ showDischargeModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeDischargeModal: function() {
|
||
this.setData({ showDischargeModal: false });
|
||
},
|
||
|
||
// 体温
|
||
showTempInput: function() {
|
||
if (!this.data.selectedDate) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var val = mapRecord.temperature || '';
|
||
this.setData({ showTempModal: true, tempValue: String(val) });
|
||
},
|
||
onTempInput: function(e) {
|
||
this.setData({ tempValue: e.detail.value });
|
||
},
|
||
confirmTemp: function() {
|
||
var val = this.data.tempValue;
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
// 已有体温记录且输入框为空或与原有值相同 → 清除
|
||
if (record.temperature && (val === '' || val === undefined || val === null || String(val) === String(record.temperature))) {
|
||
record.temperature = null;
|
||
this.setData({ showTempModal: false });
|
||
this._saveDailyRecord(record);
|
||
return;
|
||
}
|
||
if (val === '' || val === undefined || val === null) {
|
||
wx.showToast({ title: '请先输入体温', icon: 'none' });
|
||
return;
|
||
}
|
||
if (isNaN(parseFloat(val))) {
|
||
wx.showToast({ title: '请输入有效体温', icon: 'none' });
|
||
return;
|
||
}
|
||
record.temperature = val;
|
||
this.setData({ showTempModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeTempModal: function() {
|
||
this.setData({ showTempModal: false });
|
||
},
|
||
|
||
// 体重
|
||
showWeightInput: function() {
|
||
if (!this.data.selectedDate) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var val = mapRecord.weight || '';
|
||
this.setData({ showWeightModal: true, weightValue: String(val) });
|
||
},
|
||
onWeightInput: function(e) {
|
||
this.setData({ weightValue: e.detail.value });
|
||
},
|
||
confirmWeight: function() {
|
||
var val = this.data.weightValue;
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
// 已有体重记录且输入框为空或与原有值相同 → 清除
|
||
if (record.weight && (val === '' || val === undefined || val === null || String(val) === String(record.weight))) {
|
||
record.weight = null;
|
||
this.setData({ showWeightModal: false });
|
||
this._saveDailyRecord(record);
|
||
return;
|
||
}
|
||
if (val === '' || val === undefined || val === null) {
|
||
wx.showToast({ title: '请先输入体重', icon: 'none' });
|
||
return;
|
||
}
|
||
if (isNaN(parseFloat(val))) {
|
||
wx.showToast({ title: '请输入有效体重', icon: 'none' });
|
||
return;
|
||
}
|
||
record.weight = val;
|
||
this.setData({ showWeightModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeWeightModal: function() {
|
||
this.setData({ showWeightModal: false });
|
||
},
|
||
|
||
// 备注
|
||
showNoteInput: function() {
|
||
if (!this.data.selectedDate) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var val = mapRecord.note || '';
|
||
this.setData({ showNoteModal: true, noteValue: val });
|
||
},
|
||
onNoteInput: function(e) {
|
||
this.setData({ noteValue: e.detail.value });
|
||
},
|
||
confirmNote: function() {
|
||
var mapRecord = this.data.dailyRecordsMap[this.data.selectedDate] || {};
|
||
var record = this._cloneRecord(mapRecord);
|
||
record.note = this.data.noteValue;
|
||
this.setData({ showNoteModal: false });
|
||
this._saveDailyRecord(record);
|
||
},
|
||
closeNoteModal: function() {
|
||
this.setData({ showNoteModal: false });
|
||
},
|
||
|
||
// ========== 保存每日记录 ==========
|
||
_cloneRecord: function(record) {
|
||
return JSON.parse(JSON.stringify(record));
|
||
},
|
||
|
||
_saveDailyRecord: async function(record) {
|
||
var dateStr = this.data.selectedDate;
|
||
if (!dateStr) {
|
||
wx.showToast({ title: '请先选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 二次校验:禁止保存未来日期
|
||
var today = this._formatDate(new Date());
|
||
if (dateStr > today) {
|
||
wx.showToast({ title: '无法记录未来的日期', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
record.record_date = dateStr;
|
||
|
||
wx.showLoading({ title: '保存中...', mask: true });
|
||
try {
|
||
// 确保 period 字段明确处理:'0'=无, '1'=少量, '2'=中量, '3'=大量
|
||
var periodVal = record.period;
|
||
if (periodVal === undefined || periodVal === null || periodVal === '') {
|
||
periodVal = '0';
|
||
} else {
|
||
periodVal = String(periodVal);
|
||
}
|
||
|
||
await api.periodDailySave({
|
||
record_date: dateStr,
|
||
period: periodVal,
|
||
sex: '0',
|
||
symptoms: record.symptoms || '',
|
||
mood: record.mood || '',
|
||
discharge: record.discharge || '',
|
||
temperature: record.temperature !== undefined && record.temperature !== null && record.temperature !== '' ? String(record.temperature) : null,
|
||
weight: record.weight !== undefined && record.weight !== null && record.weight !== '' ? String(record.weight) : null,
|
||
note: record.note || '',
|
||
period_detail: record.period_detail ? JSON.stringify(record.period_detail) : ''
|
||
});
|
||
wx.hideLoading();
|
||
|
||
// 合并已有记录,避免覆盖丢失字段
|
||
var map = this.data.dailyRecordsMap;
|
||
var existing = map[dateStr] || {};
|
||
var merged = {};
|
||
// 先复制已有记录的所有字段
|
||
for (var k in existing) { merged[k] = existing[k]; }
|
||
// 再用新记录覆盖
|
||
for (var k in record) { merged[k] = record[k]; }
|
||
// 确保 merged.period 是字符串类型
|
||
if (merged.period !== undefined && merged.period !== null) {
|
||
merged.period = String(merged.period);
|
||
}
|
||
map[dateStr] = merged;
|
||
|
||
var sexMap = this.data.sexRecordsMap;
|
||
var sexRecords = sexMap[dateStr] || [];
|
||
|
||
this.setData({
|
||
dailyRecord: merged,
|
||
dailyRecordsMap: map,
|
||
hasDetailContent: !!(parseInt(merged.period) > 0 || sexRecords.length > 0 || merged.symptoms || merged.mood || merged.discharge || merged.temperature || merged.weight || merged.note)
|
||
});
|
||
|
||
// 重新生成周期并刷新统计
|
||
var newCycles = this._buildDisplayCycles(map, this.data.savedCycles);
|
||
this.setData({ cycles: newCycles });
|
||
this.calculateStats(newCycles);
|
||
|
||
// ★ 将更新后的数据写入本地缓存
|
||
|
||
this.buildCalendar();
|
||
wx.showToast({ title: '保存成功', icon: 'success' });
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
console.error('保存每日记录失败:', e);
|
||
wx.showToast({ title: '保存失败: ' + (e.error || e.errMsg || '未知错误'), icon: 'none' });
|
||
}
|
||
},
|
||
|
||
// ========== 姨姨设置弹窗 ==========
|
||
showPeriodSettings: async function() {
|
||
// 优先读取用户已保存的周期记录(period_records),还原上次保存的开始/结束日期与周期参数
|
||
// 修复:保存后再次打开弹窗被重置为默认值的问题
|
||
var saved = this.data.savedCycles || [];
|
||
var lastSaved = saved.length > 0 ? saved[0] : null;
|
||
|
||
var startStr;
|
||
var endStr;
|
||
var cycleLength;
|
||
var periodLength;
|
||
|
||
if (lastSaved && lastSaved.start_date && lastSaved.end_date) {
|
||
startStr = lastSaved.start_date;
|
||
endStr = lastSaved.end_date;
|
||
periodLength = parseInt(lastSaved.period_length) || 5;
|
||
cycleLength = parseInt(lastSaved.cycle_length) || this.data.avgCycle || 28;
|
||
} else {
|
||
// 无历史记录时使用默认值:结束日为今天,开始日 = 结束日 - 持续天数 + 1
|
||
periodLength = 5;
|
||
var endDate = new Date();
|
||
var startDate = new Date();
|
||
startDate.setDate(startDate.getDate() - periodLength + 1);
|
||
startStr = this._formatDate(startDate);
|
||
endStr = this._formatDate(endDate);
|
||
cycleLength = this.data.avgCycle || 28;
|
||
}
|
||
|
||
var form = {
|
||
start_date: startStr,
|
||
end_date: endStr,
|
||
cycle_length: cycleLength,
|
||
period_length: periodLength,
|
||
predicted_start: '',
|
||
predicted_end: '',
|
||
predicted_ovulation_day: '',
|
||
predicted_ovulation_start: '',
|
||
predicted_ovulation_end: ''
|
||
};
|
||
|
||
// 立即计算推算结果
|
||
form = this._computePrediction(form);
|
||
|
||
// 加载权限设置(先 await,确保保存时 periodVisible 已是最新值,避免误存 0)
|
||
try {
|
||
var pv = await api.periodVisibleGet();
|
||
if (pv && pv.period_visible !== undefined) {
|
||
this.setData({ periodVisible: !!pv.period_visible, hasCouple: !!pv.has_couple });
|
||
}
|
||
} catch (e) {
|
||
console.error('加载权限设置失败:', e);
|
||
}
|
||
|
||
this.setData({
|
||
showCycleModal: true,
|
||
showFormula: false,
|
||
cycleForm: form
|
||
});
|
||
},
|
||
onPeriodVisibleChange: function(e) {
|
||
if (this.data.visibilitySaving) return;
|
||
var visible = !!e.detail.value;
|
||
var oldValue = this.data.periodVisible;
|
||
this.setData({ periodVisible: visible, visibilitySaving: true });
|
||
api.periodVisibleSet({ period_visible: visible ? 1 : 0 }).then(() => {
|
||
this.setData({ visibilitySaving: false });
|
||
}).catch(() => {
|
||
this.setData({ periodVisible: oldValue, visibilitySaving: false });
|
||
wx.showToast({ title: '权限设置失败', icon: 'none' });
|
||
});
|
||
},
|
||
|
||
closeCycleModal: function() {
|
||
this.setData({ showCycleModal: false, showFormula: false });
|
||
},
|
||
toggleFormula: function() {
|
||
this.setData({ showFormula: !this.data.showFormula });
|
||
},
|
||
onCycleStartChange: function(e) {
|
||
var startDate = e.detail.value;
|
||
var form = this.data.cycleForm;
|
||
form.start_date = startDate;
|
||
|
||
// 根据开始日期和持续天数自动计算结束日期
|
||
var s = new Date(startDate);
|
||
s.setDate(s.getDate() + parseInt(form.period_length) - 1);
|
||
form.end_date = this._formatDate(s);
|
||
|
||
// 更新推算
|
||
form = this._computePrediction(form);
|
||
this.setData({ cycleForm: form });
|
||
},
|
||
onCycleEndChange: function(e) {
|
||
var endDate = e.detail.value;
|
||
var form = this.data.cycleForm;
|
||
form.end_date = endDate;
|
||
|
||
// 根据开始和结束日期自动计算持续天数
|
||
var s = new Date(form.start_date);
|
||
var e = new Date(endDate);
|
||
var diffDays = Math.round((e.getTime() - s.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||
if (diffDays > 0) {
|
||
form.period_length = diffDays;
|
||
}
|
||
|
||
// 更新推算
|
||
form = this._computePrediction(form);
|
||
this.setData({ cycleForm: form });
|
||
},
|
||
adjustCycle: function(e) {
|
||
var dir = parseInt(e.currentTarget.dataset.dir);
|
||
var form = this.data.cycleForm;
|
||
form.cycle_length += dir;
|
||
if (form.cycle_length < 15) form.cycle_length = 15;
|
||
if (form.cycle_length > 60) form.cycle_length = 60;
|
||
|
||
form = this._computePrediction(form);
|
||
this.setData({ cycleForm: form });
|
||
},
|
||
adjustPeriod: function(e) {
|
||
var dir = parseInt(e.currentTarget.dataset.dir);
|
||
var form = this.data.cycleForm;
|
||
form.period_length += dir;
|
||
if (form.period_length < 1) form.period_length = 1;
|
||
if (form.period_length > 15) form.period_length = 15;
|
||
|
||
// 根据开始日期和新的持续天数计算结束日期
|
||
var s = new Date(form.start_date);
|
||
s.setDate(s.getDate() + parseInt(form.period_length) - 1);
|
||
form.end_date = this._formatDate(s);
|
||
|
||
form = this._computePrediction(form);
|
||
this.setData({ cycleForm: form });
|
||
},
|
||
|
||
// 推算下次姨妈信息
|
||
_computePrediction: function(form) {
|
||
if (!form.start_date) return form;
|
||
|
||
var start = new Date(form.start_date);
|
||
var cycleLen = parseInt(form.cycle_length) || 28;
|
||
var periodLen = parseInt(form.period_length) || 5;
|
||
|
||
// 下次姨妈开始 = 上次来时 + 周期长度
|
||
var nextStart = new Date(start);
|
||
nextStart.setDate(nextStart.getDate() + cycleLen);
|
||
|
||
// 下次姨妈结束 = 下次开始 + 持续天数 - 1
|
||
var nextEnd = new Date(nextStart);
|
||
nextEnd.setDate(nextEnd.getDate() + periodLen - 1);
|
||
|
||
// 排卵日 = 下次姨妈开始前14天
|
||
var ovDay = new Date(nextStart);
|
||
ovDay.setDate(ovDay.getDate() - 14);
|
||
|
||
// 排卵期 = 排卵日前3天 ~ 排卵日后3天
|
||
var ovStart = new Date(ovDay);
|
||
ovStart.setDate(ovStart.getDate() - 3);
|
||
var ovEnd = new Date(ovDay);
|
||
ovEnd.setDate(ovEnd.getDate() + 3);
|
||
|
||
form.predicted_start = this._formatDate(nextStart);
|
||
form.predicted_end = this._formatDate(nextEnd);
|
||
form.predicted_ovulation_day = this._formatDate(ovDay);
|
||
form.predicted_ovulation_start = this._formatDate(ovStart);
|
||
form.predicted_ovulation_end = this._formatDate(ovEnd);
|
||
|
||
return form;
|
||
},
|
||
saveCycle: async function() {
|
||
var fd = this.data.cycleForm;
|
||
if (!fd.start_date || !fd.end_date) {
|
||
wx.showToast({ title: '请选择日期', icon: 'none' });
|
||
return;
|
||
}
|
||
if (fd.start_date > fd.end_date) {
|
||
wx.showToast({ title: '结束日期不能早于开始日期', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
wx.showLoading({ title: '保存中...' });
|
||
try {
|
||
var saved = this.data.savedCycles || [];
|
||
var lastSaved = saved.length > 0 ? saved[0] : null;
|
||
var cycleData = {
|
||
start_date: fd.start_date,
|
||
end_date: fd.end_date,
|
||
cycle_length: fd.cycle_length,
|
||
period_length: fd.period_length
|
||
};
|
||
if (lastSaved && lastSaved.id) {
|
||
cycleData.id = lastSaved.id;
|
||
cycleData.symptoms = lastSaved.symptoms || '';
|
||
cycleData.note = lastSaved.note || '';
|
||
await api.periodUpdate(cycleData);
|
||
} else {
|
||
await api.periodAdd(cycleData);
|
||
}
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '保存成功', icon: 'success' });
|
||
this.closeCycleModal();
|
||
// 等待重新拉取数据并重算周期/安全期后再结束
|
||
await this.loadData();
|
||
} catch(e) {
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '保存失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
onCycleLongPress: function(e) {
|
||
var dataset = e.currentTarget.dataset;
|
||
var id = dataset.id;
|
||
var startDate = dataset.start;
|
||
var endDate = dataset.end;
|
||
var self = this;
|
||
wx.showModal({
|
||
title: '删除周期',
|
||
content: '确定删除 ' + startDate + ' 至 ' + endDate + ' 的周期记录吗?',
|
||
confirmText: '删除',
|
||
confirmColor: '#e6437f',
|
||
success: async function(res) {
|
||
if (!res.confirm) return;
|
||
wx.showLoading({ title: '删除中...', mask: true });
|
||
try {
|
||
await api.periodDelete({
|
||
id: id || 0,
|
||
start_date: startDate,
|
||
end_date: endDate
|
||
});
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '已删除', icon: 'success' });
|
||
self.loadData();
|
||
} catch (err) {
|
||
wx.hideLoading();
|
||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
closeSexModal: function() {
|
||
this.setData({ showSexModal: false });
|
||
},
|
||
|
||
preventMove: function() {},
|
||
|
||
// ========== 工具 ==========
|
||
_periodDetailText: function(detail) {
|
||
if (!detail) return '有记录';
|
||
var parts = [];
|
||
if (detail.amount) parts.push(detail.amount);
|
||
if (detail.color) parts.push(detail.color);
|
||
if (detail.pain) parts.push(detail.pain);
|
||
return parts.join(' · ') || '有记录';
|
||
},
|
||
_dischargeDetailText: function(detail) {
|
||
if (!detail) return '';
|
||
var parts = [];
|
||
if (detail.status) parts.push(detail.status);
|
||
if (detail.flow) parts.push(detail.flow);
|
||
if (detail.abnormal && detail.abnormal.length > 0) parts.push(detail.abnormal.join('、'));
|
||
return parts.join(' · ') || '';
|
||
},
|
||
_formatDate: function(date) {
|
||
return date.getFullYear() + '-' + this._pad(date.getMonth() + 1) + '-' + this._pad(date.getDate());
|
||
},
|
||
_pad: function(n) {
|
||
return n < 10 ? '0' + n : '' + n;
|
||
},
|
||
|
||
// 已保存的周期设置是推算依据;逐日标记仅补充没有周期记录的手动输入。
|
||
_buildDisplayCycles: function(dailyMap, savedCycles) {
|
||
var saved = (savedCycles || []).map(function(item) {
|
||
return Object.assign({}, item, {
|
||
cycle_length: parseInt(item.cycle_length) || 28,
|
||
period_length: parseInt(item.period_length) || 5
|
||
});
|
||
});
|
||
saved.sort(function(a, b) { return b.start_date.localeCompare(a.start_date); });
|
||
var dailyCycles = this._buildCyclesFromDaily(dailyMap).filter(function(item) {
|
||
return !saved.some(function(record) {
|
||
return item.start_date <= record.end_date && item.end_date >= record.start_date;
|
||
});
|
||
});
|
||
if (saved.length > 0) {
|
||
dailyCycles.forEach(function(item) {
|
||
if (item.start_date > saved[0].start_date) {
|
||
item.cycle_length = saved[0].cycle_length;
|
||
item.period_length = saved[0].period_length;
|
||
}
|
||
});
|
||
}
|
||
return saved.concat(dailyCycles).sort(function(a, b) {
|
||
return b.start_date.localeCompare(a.start_date);
|
||
});
|
||
},
|
||
|
||
choosePeriodSource: function() {
|
||
var self = this;
|
||
function label(name, preview) {
|
||
var count = preview.cycles || 0;
|
||
var detail = count ? count + '次周期' : (preview.items || 0) + '条记录';
|
||
return name + '(' + detail + (preview.latest ? ',最近' + preview.latest : '') + ')';
|
||
}
|
||
wx.showActionSheet({
|
||
itemList: [label('使用我的记录', this.data.myPreview), label('使用对方记录', this.data.partnerPreview)],
|
||
success: function(choice) {
|
||
var keepUserId = choice.tapIndex === 0 ? self.data.myUserId : self.data.partnerUserId;
|
||
wx.showModal({
|
||
title: '确认选择',
|
||
content: '选择后,另一方绑定前的姨姨记录会永久删除,无法恢复。确定继续吗?',
|
||
confirmColor: '#FF6B9D',
|
||
success: async function(result) {
|
||
if (!result.confirm) return;
|
||
wx.showLoading({ title: '处理中...', mask: true });
|
||
try {
|
||
await api.periodSourceSelect({ user_id: keepUserId });
|
||
await self.loadData();
|
||
} catch (err) {
|
||
wx.showToast({ title: '选择失败,请重试', icon: 'none' });
|
||
await self.loadData();
|
||
} finally {
|
||
wx.hideLoading();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
},
|
||
// ========== 导航 ==========
|
||
goBack: function() { wx.navigateBack(); },
|
||
goHome: function() { wx.redirectTo({ url: '/pages/index/index' }); },
|
||
goProfile: function() { wx.redirectTo({ url: '/pages/profile/profile' }); }
|
||
});
|