Files
王宏豪 cd04c1b5ef feat(database): 迁移用户关联字段并优化打卡系统
- 修改数据库配置将MYSQL_DB从mys_info_v3改为mys_info_test
- 新增auth_users表wechat_openid字段并建立唯一索引
- 为plan_checkins表添加计划性能索引(idx_plan_period_student)
- 将checkin_records_old表关联方式从student_id改为user_id
- 将plan_checkins表关联方式从student_id改为user_id并更新附件路径
- 移除routes/auth.py中的uuid导入使用安全token生成函数
- 重构routes/checkin.py打卡逻辑去除course_type字段并优化返回结构
- 添加打卡时间限制验证功能防止过期打卡操作
- 实现小组统计功能支持老师查看各组打卡情况
- 更新主页index.html界面适配新的打卡和计划功能
- 调整计划提交时间规则为周五至周四周期
- 添加月度和年度计划提交状态验证机制
2026-09-10 14:11:58 +08:00

436 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const api = require('../../utils/api');
const auth = require('../../utils/auth');
const STATUSES = ['正常', '迟到', '请假', '不参加讨论', '早退'];
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const STATUS_META = {
正常: '#2E7D32',
迟到: '#C67B30',
请假: '#3A7BD5',
早退: '#8B7A6A',
'不参加讨论': '#C0392B'
};
const STATUS_ITEMS = STATUSES.map((label) => ({
label,
color: STATUS_META[label] || '#C67B30'
}));
function errMessage(err) {
if (err && typeof err === 'object') {
if (err.detail) return String(err.detail);
if (err.error) return String(err.error);
if (err.message) return err.message;
}
return '';
}
function pad2(n) {
return n < 10 ? '0' + n : String(n);
}
function fmtTime(ts) {
if (!ts) return '';
const d = new Date(String(ts).replace(' ', 'T'));
if (isNaN(d.getTime())) return String(ts);
const now = new Date();
const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
const diff = Math.round((startOfDay(now) - startOfDay(d)) / 86400000);
const hm = pad2(d.getHours()) + ':' + pad2(d.getMinutes());
if (diff === 0) return '今天 ' + hm;
if (diff === 1) return '昨天 ' + hm;
return pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + hm;
}
Page({
data: {
statusItems: STATUS_ITEMS,
statusIndex: -1,
loading: true,
classDate: '',
weekday: '',
dayNum: '',
checked: false,
record: null,
statusColor: '',
// 视图模式:overview 总览 / detail 小组详情(仅老师)
view: 'overview',
// 当前选中的小组(详情页用)
selectedGroupName: '',
// 小组统计列表(老师总览用)
groups: [],
// 小组成员列表(详情页 & 学员小组 tab 用)
members: [],
checkedCount: 0,
total: 0,
// 用户角色
isTeacher: false,
myGroup: '',
unassignedCount: 0,
// 学员视图 tab:group / personal
tab: 'group',
// 个人历史
history: [],
historyPage: 1,
historyTotal: 0,
historyFinished: false,
historyLoading: false,
submitting: false,
refreshing: false,
loadError: '',
// 周选择相关
weekList: [],
showWeekPicker: false,
},
onShow() {
this.loadPage();
},
onPullDownRefresh() {
this.loadPage().finally(() => wx.stopPullDownRefresh());
},
loadPage() {
this.setData({ loadError: '' });
return auth.ensureLogin()
.then((token) => {
if (!token) {
wx.switchTab({ url: '/pages/profile/profile' });
return;
}
// 先加载周列表,再加载当前周数据
return this.loadWeeks();
})
.then(() => {
return this.loadWeek();
})
.catch((err) => {
const msg = (err && (err.error || err.detail || err.message)) || '加载失败,请重试';
this.setData({ loading: false, loadError: msg });
wx.showToast({ title: msg, icon: 'none' });
});
},
onRetry() {
this.loadPage();
},
/** 加载可用周列表 */
loadWeeks() {
return api.request({ url: '/api/v1/checkin/weeks' }).then((res) => {
const weeks = res.weeks || [];
const current = res.current_week || (weeks[0] || '');
this.setData({
weekList: weeks,
classDate: this.data.classDate || current,
});
}).catch(() => {
// 获取周列表失败不影响主流程
console.warn('获取上课周列表失败');
});
},
/** 打开周选择器 */
openWeekPicker() {
if (!this.data.weekList.length) {
return;
}
this.setData({ showWeekPicker: true });
},
/** 关闭周选择器 */
closeWeekPicker() {
this.setData({ showWeekPicker: false });
},
/** 选择某一周 */
onWeekSelect(e) {
const classDate = e.currentTarget.dataset.date;
if (!classDate || classDate === this.data.classDate) {
this.setData({ showWeekPicker: false });
return;
}
this.setData({
classDate,
showWeekPicker: false,
});
this.loadWeek();
},
/**
* 加载当前周数据:
* - 老师:加载小组统计总览
* - 学员:加载小组统计 + 本组成员列表 + 个人历史(个人 tab 延迟加载)
*/
loadWeek() {
this.setData({ loading: true });
const classDate = this.data.classDate;
const params = classDate ? { class_date: classDate } : {};
return api.request({ url: '/api/v1/checkin/groups', data: params }).then((res) => {
const date = res.class_date || '';
const weekday = WEEKDAYS[new Date(date + 'T00:00:00').getDay()] || '';
const dayNum = date.split('-')[2] || '';
const statusIndex = res.record && res.record.status
? Math.max(0, STATUSES.indexOf(res.record.status))
: -1;
const statusColor = res.record ? (STATUS_META[res.record.status] || '#C67B30') : '';
const isTeacher = !!res.is_teacher;
// 计算每个小组的百分比
const groups = (res.groups || []).map((g) => ({
...g,
percent: g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0
}));
const baseData = {
classDate: date,
weekday,
dayNum,
checked: !!res.checked,
record: res.record || null,
statusIndex,
statusColor,
isTeacher,
myGroup: res.my_group || '',
unassignedCount: res.unassigned_count || 0,
groups,
};
// 如果当前在详情页(老师),重新加载该组详情
if (isTeacher && this.data.view === 'detail' && this.data.selectedGroupName) {
this.setData({ ...baseData, members: [], loading: true });
return this.loadGroup(this.data.selectedGroupName).then(() => {
this.setData({ loading: false });
});
}
this.setData(baseData);
if (!isTeacher) {
// 学员:先设置兜底的小组名(避免/group失败时显示"暂无小组")
if (res.my_group) {
this.setData({ groupName: res.my_group });
}
// 学员:加载本组成员列表
return this.loadGroup(res.my_group || '').then(() => {
this.setData({ loading: false });
});
}
this.setData({ loading: false });
}).catch(() => {
this.setData({ loading: false });
});
},
/** 加载指定小组的成员列表,不传则看自己小组 */
loadGroup(groupName) {
const data = { class_date: this.data.classDate };
if (groupName) {
data.group_name = groupName;
}
return api.request({ url: '/api/v1/checkin/group', data }).then((res) => {
const members = (res.members || []).map((m) => ({
...m,
shortName: (m.name || '?').charAt(0),
timeText: m.checked && m.record ? fmtTime(m.record.checked_at) : ''
}));
const checkedCount = members.filter((m) => m.checked).length;
this.setData({
members,
groupName: res.group_name || groupName || '',
checkedCount,
total: members.length
});
}).catch((err) => {
console.warn('加载小组成员失败:', err);
wx.showToast({ title: '加载小组成员失败', icon: 'none' });
});
},
/** 老师:打开小组详情 */
openGroup(e) {
const groupName = e.currentTarget.dataset.group;
if (!groupName) return;
this.setData({ selectedGroupName: groupName, view: 'detail', members: [], loading: true });
this.loadGroup(groupName).then(() => {
this.setData({ loading: false });
});
},
/** 老师:返回总览 */
backToOverview() {
this.setData({ view: 'overview', selectedGroupName: '', members: [] });
},
/** 加载个人历史(第一页) */
loadHistory() {
this.setData({ historyPage: 1, historyFinished: false, historyLoading: true });
return this._loadHistoryPage(1, true).then(() => {
this.setData({ historyLoading: false });
}).catch(() => {
this.setData({ historyLoading: false });
});
},
/** 加载更多历史(翻页) */
loadMoreHistory() {
if (this.data.historyLoading || this.data.historyFinished) return;
const nextPage = this.data.historyPage + 1;
this.setData({ historyLoading: true });
return this._loadHistoryPage(nextPage, false).then(() => {
this.setData({ historyLoading: false, historyPage: nextPage });
}).catch(() => {
this.setData({ historyLoading: false });
});
},
_loadHistoryPage(page, replace) {
return api.request({ url: '/api/v1/checkin/list', data: { page, size: 20 } }).then((res) => {
const list = (res.list || []).map((item) => {
const color = STATUS_META[item.status] || '#C67B30';
return {
...item,
timeText: fmtTime(item.checked_at),
color,
bg: color + '1A'
};
});
const total = res.total || 0;
const history = replace ? list : this.data.history.concat(list);
const finished = history.length >= total;
this.setData({ history, historyTotal: total, historyFinished: finished });
});
},
onStatusTap(e) {
if (this.data.submitting) {
return;
}
const statusIndex = Number(e.currentTarget.dataset.index);
const status = STATUSES[statusIndex];
if (!status) {
return;
}
const prevIndex = this.data.statusIndex;
this.setData({ statusIndex, submitting: true });
api.request({
url: '/api/v1/checkin',
method: 'POST',
data: {
status,
class_date: this.data.classDate,
}
}).then((res) => {
const record = res.record || null;
const statusColor = record ? (STATUS_META[record.status] || '#C67B30') : '';
this.setData({ submitting: false, checked: true, record, statusColor });
wx.showToast({ title: '打卡成功', icon: 'success' });
// 学员:本地更新 members 中自己的记录,避免全量 reload
if (!this.data.isTeacher && this.data.members.length > 0) {
const myUserId = record && record.user_id;
if (myUserId) {
const members = this.data.members.map((m) => {
if (m.user_id === myUserId) {
return {
...m,
checked: true,
record: { status: record.status, checked_at: record.checked_at, note: record.note || '' },
timeText: fmtTime(record.checked_at),
};
}
return m;
});
const checkedCount = members.filter((m) => m.checked).length;
this.setData({ members, checkedCount });
}
}
// 老师视图:刷新小组统计(需要更新各小组计数)
if (this.data.isTeacher) {
this.loadWeek();
}
// 个人历史 tab:本地插入最新记录,避免全量 reload
if (!this.data.isTeacher && this.data.tab === 'personal' && record && this.data.history.length > 0) {
const color = STATUS_META[record.status] || '#C67B30';
const newItem = {
...record,
timeText: fmtTime(record.checked_at),
color,
bg: color + '1A',
};
// 检查是否已有同一天的记录(更新而非插入)
const existingIdx = this.data.history.findIndex((h) => h.class_date === record.class_date);
let history;
if (existingIdx >= 0) {
history = this.data.history.slice();
history[existingIdx] = newItem;
} else {
history = [newItem].concat(this.data.history);
}
const historyTotal = Math.max(this.data.historyTotal, this.data.historyTotal + (existingIdx >= 0 ? 0 : 1));
this.setData({ history, historyTotal });
} else if (!this.data.isTeacher && this.data.tab === 'personal') {
this.loadHistory();
}
}).catch((err) => {
this.setData({ submitting: false, statusIndex: prevIndex });
wx.showToast({ title: errMessage(err) || '打卡失败', icon: 'none' });
});
},
/** 学员 tab 切换 */
switchTab(e) {
const tab = e.currentTarget.dataset.tab;
if (tab !== 'group' && tab !== 'personal') {
return;
}
this.setData({ tab });
if (tab === 'personal') {
// 每次切到个人 tab 都刷新历史数据
this.loadHistory();
} else if (tab === 'group' && this.data.myGroup) {
// 每次切回小组 tab 都刷新小组成员数据
this.loadGroup(this.data.myGroup);
}
},
onRefresh() {
this.setData({ refreshing: true });
this.loadWeek().then(() => {
// 学员个人 tab 也刷新一下
if (!this.data.isTeacher && this.data.tab === 'personal') {
return this.loadHistory();
}
}).then(() => {
this.setData({ refreshing: false });
});
},
// ---- 分享 ----
onShareAppMessage() {
const checked = this.data.checked;
const status = this.data.record && this.data.record.status;
const title = checked
? '我今天' + status + '啦,一起在明缘云道场打卡吧'
: '明缘云道场 · 每日打卡,共同成长';
return {
title,
path: '/pages/checkin/checkin'
};
},
onShareTimeline() {
return {
title: '明缘云道场 · 每周打卡,共同进步',
query: 'from=timeline'
};
},
/** 上拉加载更多(个人历史 tab) */
onReachBottom() {
if (!this.data.isTeacher && this.data.tab === 'personal') {
this.loadMoreHistory();
}
},
});