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(); } }, });