feat(app): 添加课程页面并优化打卡和计划功能
- 新增 pages/course/course 页面,集成 web-view 显示线上课程列表 - 在 app.json 中注册课程页面并添加底部 tabbar 导航 - 优化打卡页面,添加学号显示和改进成员列表布局 - 重构计划页面,添加内容详情弹层和优化加载逻辑 - 添加阿里云 OSS 上传工具模块用于文件上传功能 - 统一姓名和学号在同一行显示,改善 UI 体验 - 修复多个页面中的绑定事件和样式问题
This commit is contained in:
@@ -5,6 +5,16 @@ const auth = require('../../utils/auth');
|
||||
const TYPES = ['weekly', 'monthly', 'yearly'];
|
||||
const TYPE_LABELS = ['周计划', '月计划', '年计划'];
|
||||
|
||||
// 个人历史卡片里最多展示的缩略图数量,其余图片在「查看详情」弹层里看全部
|
||||
const HISTORY_THUMB_LIMIT = 2;
|
||||
|
||||
// 说明:本页所有「切换状态 + 紧跟一次加载」的流程,都要求把 loading 合并进
|
||||
// 调用方那一次 setData(加载函数传 silent=true),不要在同一个 tick 里连发两次
|
||||
// setData —— 否则会触发 "recursive update detected: a data update is applying
|
||||
// during another data update" 警告(数据应用到 WXML 是同步过程,见官方文档
|
||||
// 「高级数据更新方法 / 递归更新问题」)。
|
||||
// 同理,refreshing 只由 onRefresh 写:它绑定的是 scroll-view 的
|
||||
// refresher-triggered(受控属性),别的流程顺手写它容易与组件自身状态互相回写。
|
||||
Page({
|
||||
data: {
|
||||
// 类型切换相关
|
||||
@@ -43,6 +53,8 @@ Page({
|
||||
// 周期切换相关
|
||||
periodList: [], // 可用周期列表
|
||||
showPeriodPicker: false,
|
||||
// 内容详情弹层(点击文章 / 图片后展示全文与全部图片)
|
||||
contentDetail: null,
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -87,6 +99,7 @@ Page({
|
||||
return this.loadHistory();
|
||||
}
|
||||
}).then(() => {
|
||||
// refreshing 只在这里复位:它是 scroll-view refresher-triggered 的受控值
|
||||
this.setData({ refreshing: false });
|
||||
});
|
||||
},
|
||||
@@ -130,27 +143,25 @@ Page({
|
||||
this.setData({ showPeriodPicker: false });
|
||||
return;
|
||||
}
|
||||
this.setData({
|
||||
periodKey: period,
|
||||
showPeriodPicker: false,
|
||||
});
|
||||
if (this.data.view === 'detail') {
|
||||
// 详情判断要在 setData 之前取,视图切换与 loading 合并成同一次更新
|
||||
const inDetail = this.data.view === 'detail';
|
||||
this.setData({ periodKey: period, showPeriodPicker: false, loading: true });
|
||||
if (inDetail) {
|
||||
// 详情页:重新加载当前小组的该周期数据
|
||||
this.loadDetail();
|
||||
} else if (!this.data.isTeacher && this.data.studentTab === 'group' && this.data.myGroup) {
|
||||
// 学员总览 + 小组 tab:刷新本组成员
|
||||
this.loadOverview();
|
||||
this.loadDetail(true);
|
||||
} else {
|
||||
// 老师总览 / 学员个人 tab:重新加载总览
|
||||
this.loadOverview();
|
||||
// 总览:老师看小组统计,学员看本组成员(loadOverview 内部会接着拉成员)
|
||||
this.loadOverview(true);
|
||||
}
|
||||
},
|
||||
|
||||
/** 加载总览数据 */
|
||||
loadOverview() {
|
||||
/** 加载总览数据(silent=true 时不写 loading,由调用方合并到同一次 setData) */
|
||||
loadOverview(silent) {
|
||||
const type = TYPES[this.data.typeIndex];
|
||||
const periodKey = this.data.periodKey;
|
||||
this.setData({ loading: true });
|
||||
if (!silent) {
|
||||
this.setData({ loading: true });
|
||||
}
|
||||
return api.request({ url: '/api/v1/plan/groups', data: { type, period_key: periodKey } })
|
||||
.then((res) => {
|
||||
const groups = (res.groups || []).map((g) => {
|
||||
@@ -162,9 +173,12 @@ Page({
|
||||
};
|
||||
});
|
||||
const isTeacher = !!res.is_teacher;
|
||||
// 学员总览的「小组」tab 紧接着还要拉一次本组成员:把 loading 直接定成
|
||||
// 最终值,避免「先置 false 再置 true」的连续更新,也避免中间态闪出空列表
|
||||
const willLoadMembers = !isTeacher && this.data.view === 'overview'
|
||||
&& this.data.studentTab === 'group' && !!res.my_group;
|
||||
this.setData({
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
loading: willLoadMembers,
|
||||
periodKey: res.period_key || periodKey,
|
||||
canSubmit: !!res.can_submit,
|
||||
message: res.message || '',
|
||||
@@ -177,13 +191,13 @@ Page({
|
||||
myGroup: res.my_group || '',
|
||||
groups,
|
||||
});
|
||||
// 学员:如果在总览 + 小组 tab,加载本组成员
|
||||
if (!isTeacher && this.data.view === 'overview' && this.data.studentTab === 'group' && res.my_group) {
|
||||
return this.loadDetailForGroup(res.my_group);
|
||||
// 学员:如果在总览 + 小组 tab,加载本组成员(loading 已在上面一并写入)
|
||||
if (willLoadMembers) {
|
||||
return this.loadDetailForGroup(res.my_group, undefined, true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({ loading: false, refreshing: false });
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
@@ -194,8 +208,9 @@ Page({
|
||||
if (!groupName) {
|
||||
return;
|
||||
}
|
||||
this.setData({ selectedGroupName: groupName, view: 'detail', members: [] });
|
||||
this.loadDetail();
|
||||
// 视图切换与 loading 合并成同一次 setData,避免同一 tick 连发两次更新
|
||||
this.setData({ selectedGroupName: groupName, view: 'detail', members: [], loading: true });
|
||||
this.loadDetail(true);
|
||||
},
|
||||
|
||||
/** 学员直接查看自己小组(只有一个小组时点击卡片直接进) */
|
||||
@@ -204,30 +219,37 @@ Page({
|
||||
if (!groupName) {
|
||||
return;
|
||||
}
|
||||
this.setData({ selectedGroupName: groupName, view: 'detail', members: [] });
|
||||
this.loadDetail();
|
||||
this.setData({ selectedGroupName: groupName, view: 'detail', members: [], loading: true });
|
||||
this.loadDetail(true);
|
||||
},
|
||||
|
||||
/** 加载小组详情 */
|
||||
loadDetail() {
|
||||
/** 加载小组详情(silent=true 表示 loading 已由调用方写入) */
|
||||
loadDetail(silent) {
|
||||
const type = TYPES[this.data.typeIndex];
|
||||
return this.loadDetailForGroup(this.data.selectedGroupName, type);
|
||||
return this.loadDetailForGroup(this.data.selectedGroupName, type, silent);
|
||||
},
|
||||
|
||||
/** 加载指定小组的成员数据(不改变视图状态) */
|
||||
loadDetailForGroup(groupName, type) {
|
||||
loadDetailForGroup(groupName, type, silent) {
|
||||
type = type || TYPES[this.data.typeIndex];
|
||||
this.setData({ loading: true });
|
||||
if (!silent) {
|
||||
this.setData({ loading: true });
|
||||
}
|
||||
return api.request({
|
||||
url: '/api/v1/plan/group',
|
||||
data: { type, group_name: groupName, period_key: this.data.periodKey }
|
||||
}).then((res) => {
|
||||
// 附件相对路径转完整 URL(列表已不渲染缩略图,allThumbs 全量留给详情弹层)
|
||||
const members = (res.members || []).map((m) => {
|
||||
const all = (((m.record || {}).attachments) || []).map((p) => api.planFileUrl(p));
|
||||
return { ...m, allThumbs: all };
|
||||
});
|
||||
this.setData({
|
||||
loading: false,
|
||||
periodKey: res.period_key || this.data.periodKey,
|
||||
canSubmit: !!res.can_submit,
|
||||
message: res.message || '',
|
||||
members: res.members || []
|
||||
members
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
@@ -239,13 +261,18 @@ Page({
|
||||
switchStudentTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab;
|
||||
if (tab !== 'group' && tab !== 'personal') return;
|
||||
this.setData({ studentTab: tab });
|
||||
if (tab === 'personal') {
|
||||
// 每次切到个人 tab 都刷新历史数据
|
||||
this.setData({ studentTab: tab });
|
||||
this.loadHistory();
|
||||
} else if (tab === 'group' && this.data.myGroup) {
|
||||
// 每次切回小组 tab 都刷新小组成员数据
|
||||
this.loadDetailForGroup(this.data.myGroup);
|
||||
return;
|
||||
}
|
||||
if (this.data.myGroup) {
|
||||
// 每次切回小组 tab 都刷新小组成员数据;loading 合并进这次 setData
|
||||
this.setData({ studentTab: tab, loading: true });
|
||||
this.loadDetailForGroup(this.data.myGroup, undefined, true);
|
||||
} else {
|
||||
this.setData({ studentTab: tab });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -256,31 +283,76 @@ Page({
|
||||
url: '/api/v1/plan/history',
|
||||
data: { type, page: 1, size: 20 }
|
||||
}).then((res) => {
|
||||
const history = (res.list || []).map((item) => ({
|
||||
...item,
|
||||
thumbs: (item.attachments || []).map((p) => api.planFileUrl(p)).slice(0, 3),
|
||||
statusColor: item.status === '未完成' ? '#C67B30' : '#2E7D32',
|
||||
statusBg: item.status === '未完成' ? '#FFF3E6' : '#E8F4EB',
|
||||
}));
|
||||
const history = (res.list || []).map((item) => {
|
||||
const all = (item.attachments || []).map((p) => api.planFileUrl(p));
|
||||
return {
|
||||
...item,
|
||||
// 卡片里只铺前 2 张缩略图,弹层用 allThumbs 看全部
|
||||
thumbs: all.slice(0, HISTORY_THUMB_LIMIT),
|
||||
allThumbs: all,
|
||||
statusColor: item.status === '未完成' ? '#C67B30' : '#2E7D32',
|
||||
statusBg: item.status === '未完成' ? '#FFF3E6' : '#E8F4EB',
|
||||
};
|
||||
});
|
||||
this.setData({ history });
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
/** 返回总览 */
|
||||
backToOverview() {
|
||||
this.setData({ view: 'overview', selectedGroupName: '', members: [] });
|
||||
this.loadOverview();
|
||||
this.setData({ view: 'overview', selectedGroupName: '', members: [], loading: true });
|
||||
this.loadOverview(true);
|
||||
},
|
||||
|
||||
/** 预览我的图片 */
|
||||
previewMyImage(e) {
|
||||
const index = Number(e.currentTarget.dataset.index);
|
||||
const urls = this.data.myThumbs;
|
||||
const src = urls[index];
|
||||
if (!src) return;
|
||||
wx.previewImage({ current: src, urls });
|
||||
/** 点击某个成员的信息行 → 打开详情弹层 */
|
||||
openMemberDetail(e) {
|
||||
const mi = Number(e.currentTarget.dataset.mi);
|
||||
const m = this.data.members[mi];
|
||||
if (!m || !m.record) return;
|
||||
const rec = m.record || {};
|
||||
this.setData({
|
||||
contentDetail: {
|
||||
title: m.name || '',
|
||||
sub: m.student_id ? '学号 ' + m.student_id : '',
|
||||
meta: [this.data.periodKey, rec.checked_at ? rec.checked_at + ' 提交' : ''].filter(Boolean).join(' · '),
|
||||
content: rec.content || '',
|
||||
thumbs: m.allThumbs || [],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 点击我的一条历史记录 → 打开详情弹层 */
|
||||
openHistoryDetail(e) {
|
||||
const hi = Number(e.currentTarget.dataset.hi);
|
||||
const item = this.data.history[hi];
|
||||
if (!item) return;
|
||||
this.setData({
|
||||
contentDetail: {
|
||||
title: item.period_key || '',
|
||||
sub: item.status || '',
|
||||
meta: item.checked_at ? item.checked_at + ' 提交' : '',
|
||||
content: item.content || '',
|
||||
thumbs: item.allThumbs || [],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 关闭内容详情弹层 */
|
||||
closeContentDetail() {
|
||||
this.setData({ contentDetail: null });
|
||||
},
|
||||
|
||||
/** 在详情弹层内放大某张图片 */
|
||||
previewDetailImage(e) {
|
||||
const ci = Number(e.currentTarget.dataset.ci) || 0;
|
||||
const urls = (this.data.contentDetail && this.data.contentDetail.thumbs) || [];
|
||||
if (!urls.length) return;
|
||||
wx.previewImage({ current: urls[ci] || urls[0], urls });
|
||||
},
|
||||
|
||||
/** 占位:遮罩层用 catchtouchmove 阻止滚动穿透 */
|
||||
noop() {},
|
||||
|
||||
/** 去提交计划 */
|
||||
goSubmit() {
|
||||
const type = TYPES[this.data.typeIndex];
|
||||
@@ -316,4 +388,4 @@ Page({
|
||||
query: 'from=timeline'
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user