[JavaScript] 纯文本查看 复制代码
const salary = require('../../utils/salary');
const storage = require('../../utils/storage');
const GENDERS = ['保密', '男', '女'];
const PAYDAY_LIST = Array.from({ length: 31 }, (_, i) => `${i + 1}号`);
const FISH_TICK_MS = 100; // 摸鱼计价刷新间隔
const GATE_TICK_MS = 30 * 1000; // 可摸鱼状态刷新间隔(跨过上班/下班点时自动更新)
// 非工作时段的摸鱼拦截文案(按 todayStatus 的 phase 区分)
const FISH_GATE_TEXTS = {
before: '还没上班就想摸鱼?先养精蓄锐,等上班了再来白嫖',
break: '现在是午休,摸鱼不计薪。等开工了再来,鱼要摸在点子上',
after: '都下班了还摸什么鱼,回家躺着才是真本事',
rest: '今天不开工,摸鱼没有薪资加成,安心休息吧',
};
Page({
data: {
profile: { nickname: '', avatar: '', gender: '保密' },
configured: false,
// 带薪摸鱼
fishing: false,
fishReady: true, // 当前是否处于可开始摸鱼的工作时段
fishGateText: '', // 不可摸时的待机区文案
fishSinceText: '', // 计时中提示行(含午休/下班截止状态)
fishCapped: false, // 是否已过下班时刻(计薪截止)
fishInBreak: false, // 是否正处于午休空档(计薪暂停)
fishElapsedStr: '00:00:00',
fishEarnStr: '0.0000',
// 摸鱼统计(按日/周/月)
statToday: { earningsStr: '0.00', durationStr: '0秒' },
statWeek: { earningsStr: '0.00', durationStr: '0秒' },
statMonth: { earningsStr: '0.00', durationStr: '0秒' },
// 个人信息弹层
showProfilePanel: false,
nicknameInput: '',
genderIndex: 0,
genders: GENDERS,
// 薪资设置弹层
showSalaryPanel: false,
paydayList: PAYDAY_LIST,
form: {
monthlySalary: '',
paydayIndex: 9,
// 工作日(周一~周五)
weekdayAMStart: '09:00',
weekdayAMEnd: '12:00',
weekdayPMStart: '13:30',
weekdayPMEnd: '18:00',
// 周六:上午/下午分别勾选
satAMWork: false,
satAMStart: '09:00',
satAMEnd: '12:00',
satPMWork: false,
satPMStart: '13:30',
satPMEnd: '18:00',
// 周日:上午/下午分别勾选
sunAMWork: false,
sunAMStart: '09:00',
sunAMEnd: '12:00',
sunPMWork: false,
sunPMStart: '13:30',
sunPMEnd: '18:00',
},
// 自动计算预览:按自然月日历算出合计工时与每秒价值
preview: { monthLabel: '', hoursStr: '--', perSecondStr: '--' },
},
onShow() {
this.settings = storage.getSettings();
const profile = storage.getProfile();
this.setData({
profile,
nicknameInput: profile.nickname || '',
genderIndex: Math.max(0, GENDERS.indexOf(profile.gender || '保密')),
configured: Number(this.settings.monthlySalary) > 0,
});
this.refreshStats();
this.restoreFishing();
this.startGateTimer();
// 首次使用:自动弹出薪资设置
const app = getApp();
if (app.globalData.needSetup && !this.data.configured) {
app.globalData.needSetup = false;
this.openSalaryPanel();
}
},
onHide() {
this.stopFishTimer();
this.stopGateTimer();
},
onUnload() {
this.stopFishTimer();
this.stopGateTimer();
},
noop() {},
/* ================= 摸鱼统计 ================= */
refreshStats() {
const s = storage.statsByPeriod(new Date(), this.settings); // 按当前薪资动态重算
const fmt = (x) => ({
earningsStr: salary.formatMoney(x.earnings, 2),
durationStr: salary.formatDuration(x.duration),
});
this.setData({
statToday: fmt(s.day),
statWeek: fmt(s.week),
statMonth: fmt(s.month),
});
},
/* ================= 带薪摸鱼 ================= */
restoreFishing() {
// 已在计时中:仅确保显示刷新的定时器在跑(切后台/切页后回来时重启,幂等);
// 否则从本地存储恢复(防崩溃/杀进程丢计时状态)
const active = this.fishStart
? { startTime: this.fishStart }
: storage.getActiveFish();
if (!active || !active.startTime) return;
this.fishStart = active.startTime;
this.setData({
fishing: true,
fishSinceText: `从 ${salary.timeLabel(active.startTime)} 摸到现在`,
});
this.startFishTimer(); // 内部先 stop 再 start,重复调用安全
},
startFish() {
if (!this.data.configured) {
wx.showToast({ title: '先设置薪资才能计价', icon: 'none' });
this.openSalaryPanel();
return;
}
// 只允许在工作时段内开始摸鱼(上午/下午段内才算带薪离席)
const phase = salary.todayStatus(this.settings, new Date()).phase;
if (phase !== 'working') {
wx.showModal({
title: '先别急',
content: FISH_GATE_TEXTS[phase] || '现在不是工作时间,等上班了再来',
showCancel: false,
confirmText: '好吧',
});
this.updateFishGate();
return;
}
const start = Date.now();
this.fishStart = start;
storage.setActiveFish({ startTime: start });
this.setData({
fishing: true,
fishSinceText: `从 ${salary.timeLabel(start)} 摸到现在`,
fishCapped: false,
fishInBreak: false,
fishElapsedStr: '00:00:00',
fishEarnStr: '0.0000',
});
this.startFishTimer();
},
stopFish() {
wx.showModal({
title: '结束摸鱼?',
content: '点击确定结算这段时间白嫖的钱',
confirmText: '结算',
success: (res) => {
if (res.confirm) this.finishFish();
},
});
},
finishFish() {
this.stopFishTimer();
const start = this.fishStart;
// 摸到下班后的时间不计薪:截止到开始当天的下班时刻
const r = salary.settleFish(this.settings, start, Date.now());
// 按摸鱼开始当天所在月计价(与 recordEarnings 动态口径一致,跨月结算不漂移)
const earnings = r.duration * salary.perSecond(this.settings, new Date(start));
if (r.duration >= 1) {
storage.addRecord({ startTime: start, endTime: r.end, duration: r.duration, earnings });
}
storage.clearActiveFish();
this.fishStart = null;
this.setData({
fishing: false,
fishSinceText: '',
fishCapped: false,
fishInBreak: false,
fishElapsedStr: '00:00:00',
fishEarnStr: '0.0000',
});
this.refreshStats();
this.updateFishGate();
if (r.duration < 1) {
wx.showModal({
title: '白摸了',
content: '这次没赶上计薪时段,一分钱都没白嫖到',
confirmText: '心疼',
showCancel: false,
});
return;
}
const notes = [];
if (r.excludedSec >= 60) notes.push(`已剔除休息 ${salary.formatDuration(r.excludedSec)}`);
if (r.capped) notes.push('下班后不计薪');
wx.showModal({
title: '摸鱼结算',
content:
`本次摸鱼 ${salary.formatDuration(r.duration)}\n` +
`白嫖 ¥${salary.formatMoney(earnings, 2)}\n` +
(notes.length ? `(${notes.join(',')})\n` : '') +
this.fishGrade(r.duration),
confirmText: '血赚',
showCancel: false,
});
},
fishGrade(sec) {
if (sec < 300) return '浅摸一下,见好就收';
if (sec < 1800) return '标准带薪离席';
if (sec < 3600) return '深度摸鱼,渐入佳境';
return '摸鱼大师,老板哭了';
},
startFishTimer() {
this.stopFishTimer();
this.tickFish();
this.fishTimer = setInterval(() => this.tickFish(), FISH_TICK_MS);
},
stopFishTimer() {
if (this.fishTimer) {
clearInterval(this.fishTimer);
this.fishTimer = null;
}
},
tickFish() {
if (!this.fishStart) return;
// 只累计工作时段内的秒数:跨午休自动剔除,过下班冻结
const r = salary.settleFish(this.settings, this.fishStart, Date.now());
const phase = salary.todayStatus(this.settings, new Date()).phase;
let sinceText;
if (r.capped) {
// 忘了结算跨天:明确告知只算到开始那天的下班
const overnight = salary.dateKey(new Date(this.fishStart)) !== salary.dateKey(new Date());
sinceText = overnight ? '都跨天了,计薪只到那天下班为止' : '已过下班时刻,计薪截止到下班';
}
else if (phase === 'break') sinceText = '午休中,休息时间不计薪';
else sinceText = `从 ${salary.timeLabel(this.fishStart)} 摸到现在`;
this.setData({
fishElapsedStr: salary.formatClock(r.duration),
fishEarnStr: salary.formatMoney(r.duration * salary.perSecond(this.settings), 4),
fishCapped: r.capped,
fishInBreak: !r.capped && phase === 'break',
fishSinceText: sinceText,
});
},
/* ================= 可摸鱼状态门禁 ================= */
/** 刷新待机区状态:仅工作时段内允许开始(未配置薪资时引导设置) */
updateFishGate() {
if (this.data.fishing) return;
if (!this.data.configured) {
this.setData({ fishReady: false, fishGateText: '先设置薪资,摸鱼才有价' });
return;
}
const st = salary.todayStatus(this.settings, new Date());
const allowed = st.phase === 'working';
this.setData({
fishReady: allowed,
fishGateText: allowed ? '' : FISH_GATE_TEXTS[st.phase] || '现在不是工作时间,等上班了再来',
});
},
startGateTimer() {
this.stopGateTimer();
this.updateFishGate();
this.gateTimer = setInterval(() => this.updateFishGate(), GATE_TICK_MS);
},
stopGateTimer() {
if (this.gateTimer) {
clearInterval(this.gateTimer);
this.gateTimer = null;
}
},
/* ================= 个人信息 ================= */
openProfilePanel() {
this.setData({ showProfilePanel: true });
},
closeProfilePanel() {
this.setData({ showProfilePanel: false });
},
onChooseAvatar(e) {
const temp = e.detail.avatarUrl;
if (!temp) return;
const fs = wx.getFileSystemManager();
const old = this.data.profile.avatar;
const doSave = () => {
fs.saveFile({
tempFilePath: temp,
success: (res) => this.applyAvatar(res.savedFilePath),
fail: () => this.applyAvatar(temp), // 持久化失败时退回临时路径(本次会话可用)
});
};
if (old && old.indexOf('store_') >= 0) {
fs.removeFile({ filePath: old, complete: doSave }); // 清理旧头像文件
} else {
doSave();
}
},
applyAvatar(path) {
const profile = storage.saveProfile({ avatar: path });
getApp().globalData.profile = profile;
this.setData({ profile });
wx.showToast({ title: '头像已更新', icon: 'success' });
},
onNicknameInput(e) {
this.setData({ nicknameInput: e.detail.value });
},
onGenderChange(e) {
this.setData({ genderIndex: Number(e.detail.value) });
},
saveProfileInfo() {
const nickname = (this.data.nicknameInput || '').trim().slice(0, 16);
const profile = storage.saveProfile({
nickname,
gender: GENDERS[this.data.genderIndex] || '保密',
});
getApp().globalData.profile = profile;
this.setData({ profile, showProfilePanel: false });
wx.showToast({ title: '已保存', icon: 'success' });
},
/* ================= 薪资设置 ================= */
openSalaryPanel() {
const s = this.settings || storage.getSettings();
this.setData({
showSalaryPanel: true,
form: {
monthlySalary: s.monthlySalary ? String(s.monthlySalary) : '',
paydayIndex: (Math.round(Number(s.payday)) || 10) - 1,
weekdayAMStart: s.weekdayAMStart,
weekdayAMEnd: s.weekdayAMEnd,
weekdayPMStart: s.weekdayPMStart,
weekdayPMEnd: s.weekdayPMEnd,
satAMWork: !!s.satAMWork,
satAMStart: s.satAMStart,
satAMEnd: s.satAMEnd,
satPMWork: !!s.satPMWork,
satPMStart: s.satPMStart,
satPMEnd: s.satPMEnd,
sunAMWork: !!s.sunAMWork,
sunAMStart: s.sunAMStart,
sunAMEnd: s.sunAMEnd,
sunPMWork: !!s.sunPMWork,
sunPMStart: s.sunPMStart,
sunPMEnd: s.sunPMEnd,
},
});
this.updatePreview();
},
closeSalaryPanel() {
this.setData({ showSalaryPanel: false });
},
onSalaryInput(e) {
this.setData({ 'form.monthlySalary': e.detail.value });
this.updatePreview();
},
onPaydayChange(e) {
this.setData({ 'form.paydayIndex': Number(e.detail.value) });
},
/** 统一的时间 picker 处理:data-field 指定 form 字段 */
onTimeChange(e) {
const field = e.currentTarget.dataset.field;
if (!field) return;
this.setData({ [`form.${field}`]: e.detail.value });
this.updatePreview();
},
/** 周六上/下午复选:勾选的时段展开各自起止时间设置 */
onSatCheckChange(e) {
const vals = e.detail.value || [];
this.setData({
'form.satAMWork': vals.indexOf('am') >= 0,
'form.satPMWork': vals.indexOf('pm') >= 0,
});
this.updatePreview();
},
/** 周日上/下午复选 */
onSunCheckChange(e) {
const vals = e.detail.value || [];
this.setData({
'form.sunAMWork': vals.indexOf('am') >= 0,
'form.sunPMWork': vals.indexOf('pm') >= 0,
});
this.updatePreview();
},
/** 用当前表单构造临时 settings,按当月日历实时预览工时与每秒价值 */
updatePreview() {
const now = new Date();
const tmp = this.formToSettings();
const secs = salary.monthWorkSeconds(tmp, now.getFullYear(), now.getMonth() + 1);
const ps = salary.perSecond(tmp, now);
this.setData({
preview: {
monthLabel: `${now.getFullYear()}年${now.getMonth() + 1}月`,
hoursStr: secs > 0 ? (secs / 3600).toFixed(1) : '--',
perSecondStr: ps > 0 ? salary.formatMoney(ps, 4) : '--',
},
});
},
formToSettings() {
const f = this.data.form;
return {
monthlySalary: Number(f.monthlySalary) || 0,
weekdayAMStart: f.weekdayAMStart,
weekdayAMEnd: f.weekdayAMEnd,
weekdayPMStart: f.weekdayPMStart,
weekdayPMEnd: f.weekdayPMEnd,
satAMWork: !!f.satAMWork,
satAMStart: f.satAMStart,
satAMEnd: f.satAMEnd,
satPMWork: !!f.satPMWork,
satPMStart: f.satPMStart,
satPMEnd: f.satPMEnd,
sunAMWork: !!f.sunAMWork,
sunAMStart: f.sunAMStart,
sunAMEnd: f.sunAMEnd,
sunPMWork: !!f.sunPMWork,
sunPMStart: f.sunPMStart,
sunPMEnd: f.sunPMEnd,
};
},
/** 校验某组上/下午时段:各段有效且下午不早于上午结束 */
validateDay(prefix, label) {
const f = this.data.form;
const amStart = salary.parseHM(f[`${prefix}AMStart`]);
const amEnd = salary.parseHM(f[`${prefix}AMEnd`]);
const pmStart = salary.parseHM(f[`${prefix}PMStart`]);
const pmEnd = salary.parseHM(f[`${prefix}PMEnd`]);
if (amEnd <= amStart) {
wx.showToast({ title: `${label}上午:结束要晚于开始`, icon: 'none' });
return false;
}
if (pmEnd <= pmStart) {
wx.showToast({ title: `${label}下午:结束要晚于开始`, icon: 'none' });
return false;
}
if (pmStart < amEnd) {
wx.showToast({ title: `${label}下午不能早于上午结束`, icon: 'none' });
return false;
}
return true;
},
/** 校验周六/周日:仅校验勾选的时段,且下午不早于上午结束 */
validateWeekend(prefix, label) {
const f = this.data.form;
const amOn = f[`${prefix}AMWork`];
const pmOn = f[`${prefix}PMWork`];
if (amOn && salary.parseHM(f[`${prefix}AMEnd`]) <= salary.parseHM(f[`${prefix}AMStart`])) {
wx.showToast({ title: `${label}上午:结束要晚于开始`, icon: 'none' });
return false;
}
if (pmOn && salary.parseHM(f[`${prefix}PMEnd`]) <= salary.parseHM(f[`${prefix}PMStart`])) {
wx.showToast({ title: `${label}下午:结束要晚于开始`, icon: 'none' });
return false;
}
if (
amOn &&
pmOn &&
salary.parseHM(f[`${prefix}PMStart`]) < salary.parseHM(f[`${prefix}AMEnd`])
) {
wx.showToast({ title: `${label}下午不能早于上午结束`, icon: 'none' });
return false;
}
return true;
},
saveSalary() {
const f = this.data.form;
const monthlySalary = Number(f.monthlySalary);
if (!(monthlySalary > 0)) {
wx.showToast({ title: '月薪要大于 0', icon: 'none' });
return;
}
if (!this.validateDay('weekday', '工作日')) return;
if (!this.validateWeekend('sat', '周六')) return;
if (!this.validateWeekend('sun', '周日')) return;
const patch = this.formToSettings();
if (!(salary.monthWorkSeconds(patch, new Date().getFullYear(), new Date().getMonth() + 1) > 0)) {
wx.showToast({ title: '当月工时为 0,检查时段设置', icon: 'none' });
return;
}
this.settings = storage.saveSettings(
Object.assign({ payday: f.paydayIndex + 1 }, patch),
);
getApp().globalData.settings = this.settings;
this.setData({ configured: true, showSalaryPanel: false });
this.updateFishGate(); // 时段设置可能变了,刷新可摸鱼状态
wx.showToast({ title: '已保存,去看钱跳', icon: 'success' });
},
gotoCalendar() {
wx.switchTab({ url: '/pages/calendar/calendar' });
},
});