From cd04c1b5efbfb8c11fdfa5bdb2c9408949c9b84f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E5=AE=8F=E8=B1=AA?= <2443753184@qq.com>
Date: Thu, 10 Sep 2026 14:11:58 +0800
Subject: [PATCH] =?UTF-8?q?feat(database):=20=E8=BF=81=E7=A7=BB=E7=94=A8?=
=?UTF-8?q?=E6=88=B7=E5=85=B3=E8=81=94=E5=AD=97=E6=AE=B5=E5=B9=B6=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=89=93=E5=8D=A1=E7=B3=BB=E7=BB=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修改数据库配置将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界面适配新的打卡和计划功能
- 调整计划提交时间规则为周五至周四周期
- 添加月度和年度计划提交状态验证机制
---
app.js | 36 +++-
pages/checkin/checkin.js | 293 +++++++++++++++++++++++++++----
pages/checkin/checkin.wxml | 238 ++++++++++++++++++-------
pages/checkin/checkin.wxss | 173 ++++++++++++++++++
pages/home/home.js | 193 ++++++++++----------
pages/home/home.json | 3 +-
pages/home/home.wxml | 25 +--
pages/home/home.wxss | 127 ++++++--------
pages/plan-submit/plan-submit.js | 37 ++--
pages/plan/plan.js | 231 +++++++++++++++++++++---
pages/plan/plan.wxml | 173 ++++++++++++++----
pages/plan/plan.wxss | 171 +++++++++++++++++-
pages/privacy/privacy.js | 14 ++
pages/profile/profile.js | 56 +++---
pages/profile/profile.wxml | 2 +-
pages/profile/profile.wxss | 5 +-
pages/webview/webview.js | 14 ++
project.private.config.json | 3 +-
utils/api.js | 58 +++++-
19 files changed, 1451 insertions(+), 401 deletions(-)
diff --git a/app.js b/app.js
index d89fd2e..cc8e930 100644
--- a/app.js
+++ b/app.js
@@ -1,11 +1,43 @@
const auth = require('./utils/auth');
+const api = require('./utils/api');
App({
onLaunch() {
- auth.ensureLogin().catch(() => {});
+ // 尝试登录并缓存用户信息
+ auth.ensureLogin()
+ .then((token) => {
+ if (token) {
+ // 登录成功后预取 profile 缓存到 globalData
+ api.request({ url: '/api/v1/profile' })
+ .then((res) => {
+ this.globalData.user = res;
+ })
+ .catch(() => {
+ // 预取失败不影响主流程
+ });
+ }
+ })
+ .catch(() => {});
},
globalData: {
token: '',
user: null
- }
+ },
+ // 全局获取用户信息(带缓存)
+ getUserInfo(force) {
+ if (this.globalData.user && !force) {
+ return Promise.resolve(this.globalData.user);
+ }
+ return auth.ensureLogin()
+ .then((token) => {
+ if (!token) return null;
+ return api.request({ url: '/api/v1/profile' });
+ })
+ .then((user) => {
+ if (user) {
+ this.globalData.user = user;
+ }
+ return user;
+ });
+ },
});
\ No newline at end of file
diff --git a/pages/checkin/checkin.js b/pages/checkin/checkin.js
index 9645cb7..035d6c9 100644
--- a/pages/checkin/checkin.js
+++ b/pages/checkin/checkin.js
@@ -43,7 +43,6 @@ function fmtTime(ts) {
Page({
data: {
- statuses: STATUSES,
statusItems: STATUS_ITEMS,
statusIndex: -1,
loading: true,
@@ -53,16 +52,34 @@ Page({
checked: false,
record: null,
statusColor: '',
- statusBg: '',
- history: [],
- group: [],
- groupName: '',
+ // 视图模式: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() {
@@ -70,7 +87,7 @@ Page({
},
onPullDownRefresh() {
- this.loadPage().then(() => wx.stopPullDownRefresh());
+ this.loadPage().finally(() => wx.stopPullDownRefresh());
},
loadPage() {
@@ -81,6 +98,10 @@ Page({
wx.switchTab({ url: '/pages/profile/profile' });
return;
}
+ // 先加载周列表,再加载当前周数据
+ return this.loadWeeks();
+ })
+ .then(() => {
return this.loadWeek();
})
.catch((err) => {
@@ -94,36 +115,120 @@ Page({
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 });
- return api.request({ url: '/api/v1/checkin/week' }).then((res) => {
- const classDate = res.class_date || '';
- const weekday = WEEKDAYS[new Date(classDate + 'T00:00:00').getDay()] || '';
- const dayNum = classDate.split('-')[2] || '';
+ 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') : '';
- this.setData({
- classDate,
+ 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,
- statusBg: statusColor ? statusColor + '1A' : ''
- });
- return this.loadGroup();
- }).then(() => {
+ 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() {
- const data = this.data.classDate ? { class_date: this.data.classDate } : {};
+ /** 加载指定小组的成员列表,不传则看自己小组 */
+ 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,
@@ -132,17 +237,57 @@ Page({
}));
const checkedCount = members.filter((m) => m.checked).length;
this.setData({
- group: members,
- groupName: res.group_name || '',
+ members,
+ groupName: res.group_name || groupName || '',
checkedCount,
total: members.length
});
- }).catch(() => {});
+ }).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() {
- return api.request({ url: '/api/v1/checkin/list', data: { page: 1, size: 20 } }).then((res) => {
- const history = (res.list || []).map((item) => {
+ 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,
@@ -151,10 +296,14 @@ Page({
bg: color + '1A'
};
});
- this.setData({ history });
- }).catch(() => {});
+ 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;
@@ -172,15 +321,56 @@ Page({
data: {
status,
class_date: this.data.classDate,
- course_type: '会员课程'
}
}).then((res) => {
const record = res.record || null;
const statusColor = record ? (STATUS_META[record.status] || '#C67B30') : '';
- this.setData({ submitting: false, checked: true, record, statusColor, statusBg: statusColor ? statusColor + '1A' : '' });
+ this.setData({ submitting: false, checked: true, record, statusColor });
wx.showToast({ title: '打卡成功', icon: 'success' });
- this.loadGroup();
- if (this.data.tab === 'personal') {
+ // 学员:本地更新 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) => {
@@ -189,6 +379,7 @@ Page({
});
},
+ /** 学员 tab 切换 */
switchTab(e) {
const tab = e.currentTarget.dataset.tab;
if (tab !== 'group' && tab !== 'personal') {
@@ -196,16 +387,50 @@ Page({
}
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 });
- const task = this.data.tab === 'group' ? this.loadWeek() : this.loadHistory();
- Promise.resolve(task).then(() => {
+ 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();
+ }
+ },
+});
\ No newline at end of file
diff --git a/pages/checkin/checkin.wxml b/pages/checkin/checkin.wxml
index 6e121e9..e094b36 100644
--- a/pages/checkin/checkin.wxml
+++ b/pages/checkin/checkin.wxml
@@ -1,3 +1,50 @@
+
+
+
+
+
+ {{weekday}}
+ {{dayNum}} ▾
+
+
+ {{checked ? '已打卡' : '未打卡'}}
+ {{checked ? '已完成今日打卡' : '请选择状态完成打卡'}}
+
+
+ {{record.status}}
+
+
+
+
+
+
+ {{item.label}}
+
+ 提交中…
+
+
+
+
+
+
+
+ {{item.shortName}}
+
+ {{item.name}}
+ {{item.checked ? ('打卡 ' + item.timeText) : ''}}
+
+ {{item.checked ? item.record.status : '未打卡'}}
+
+
+
@@ -10,7 +57,7 @@
-
+
@@ -23,86 +70,141 @@
-
+
-
-
-
- {{weekday}}
- {{dayNum}}
-
-
- {{checked ? '已打卡' : '未打卡'}}
- {{checked ? '已完成今日打卡' : '请选择状态完成打卡'}}
-
-
- {{record.status}}
+
+
+
+
+
+
+
+ 小组统计
+
+
+
+
+
+ {{index + 1}}
+ {{group.group_name}}
+
+ 已打卡 {{group.checked_count}}
+ {{group.total}}人
+
+ ›
+
+
+
+
+
+
+ 另有未分组 {{unassignedCount}} 人
+
+ 👥
+ 暂无小组
+
-
+
+
+
-
-
-
- {{item.label}}
+
+
+
+ ‹ 返回
+
+ {{selectedGroupName}}
+
+ {{classDate}} ▾
+
- 提交中…
+ {{checkedCount}}/{{total}}
-
+
+
+ ◌
+ 加载中...
+
+
+
+
+
+
+ 暂无成员
+
+
+
-
-
- 小组
- 个人
-
-
-
-
- {{groupName}} · {{classDate}}
- {{checkedCount}}/{{total}} 已打卡
-
-
- 👥
- 暂无小组,请联系管理员分配
-
-
- 📭
- 本周暂无打卡记录
-
-
- {{item.shortName}}
-
- {{item.name}}
- {{item.checked ? ('打卡 ' + item.timeText) : ''}}
-
- {{item.checked ? item.record.status : '未打卡'}}
-
+
+
+
+
+
+
+
+
+ 小组
+ 个人
-
-
- 📒
- 暂无个人打卡记录
+
+
+
+ {{groupName}} · {{classDate}}
+ {{checkedCount}}/{{total}} 已打卡
+
+
+ 👥
+ 暂无小组,请联系管理员分配
+
+
+ 📭
+ 本周暂无打卡记录
+
+
+
+
+
+
-
-
- {{item.class_date}}
- {{item.status}}
+
+
+ 📒
+ 暂无个人打卡记录
+
+
+
+ {{item.class_date}}
+ {{item.status}}
+
+ {{item.timeText}}
+
+
- {{item.timeText}}
+
+
+
+
+
+
+
+ 选择上课周
+
+
+ {{item}}
+ ✓
-
-
-
+
\ No newline at end of file
diff --git a/pages/checkin/checkin.wxss b/pages/checkin/checkin.wxss
index 649a943..61e949b 100644
--- a/pages/checkin/checkin.wxss
+++ b/pages/checkin/checkin.wxss
@@ -313,6 +313,16 @@
font-size: 22rpx;
color: #999;
}
+.history-note {
+ margin-top: 12rpx;
+ padding: 16rpx 20rpx;
+ background: #faf6f0;
+ border-radius: 8rpx;
+ font-size: 24rpx;
+ color: #5A4A3A;
+ line-height: 1.5;
+ word-break: break-all;
+}
/* 空态 */
.empty {
@@ -390,5 +400,168 @@
font-weight: 700;
}
+/* 日期 chip 可点击 */
+.date-chip-hover {
+ opacity: 0.8;
+}
+.date-arrow {
+ font-size: 24rpx;
+ color: #C67B30;
+ margin-left: 6rpx;
+}
+
+
+
+/* ---- 老师视图:小组统计卡片 ---- */
+.groups-list {
+ padding: 4rpx 4rpx 24rpx;
+}
+.group-card {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 22rpx 24rpx;
+ margin-bottom: 18rpx;
+ box-shadow: 0 6rpx 20rpx rgba(44, 24, 16, 0.05);
+}
+.group-card-hover { transform: scale(.99); box-shadow: 0 10rpx 24rpx rgba(44, 24, 16, 0.10); }
+.group-row { display: flex; align-items: center; gap: 16rpx; }
+.group-avatar {
+ width: 52rpx; height: 52rpx; border-radius: 14rpx;
+ background: linear-gradient(135deg, #F3E3CF, #FBE7D1);
+ color: #C67B30; font-size: 26rpx; font-weight: 800;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+}
+.group-name { font-size: 30rpx; font-weight: 800; color: #2C1810; flex-shrink: 0; }
+.chevron { color: #CDBFAE; font-size: 32rpx; line-height: 1; flex-shrink: 0; transition: transform .15s ease; }
+.group-card-hover .chevron { transform: translateX(4rpx); }
+.group-counts { display: flex; align-items: center; justify-content: flex-end; gap: 10rpx; flex: 1; min-width: 0; }
+.count { font-size: 22rpx; white-space: nowrap; }
+.count.done { color: #2E7D32; background: #E8F4EB; padding: 6rpx 12rpx; border-radius: 999rpx; font-weight: 700; }
+.count.total { color: #C67B30; background: #FFF3E6; padding: 6rpx 12rpx; border-radius: 999rpx; font-weight: 700; }
+.progress-track {
+ height: 10rpx; border-radius: 999rpx; background: #F2EDE7;
+ margin-top: 18rpx; overflow: hidden;
+}
+.progress-bar {
+ height: 100%; border-radius: 999rpx;
+ background: linear-gradient(90deg, #C67B30, #EFA45D);
+ transition: width .35s ease;
+ box-shadow: 0 2rpx 6rpx rgba(198, 123, 48, 0.35);
+}
+.unassigned { margin: 4rpx 8rpx 20rpx; font-size: 24rpx; color: #8B7A6A; }
+
+.section-title {
+ padding: 16rpx 4rpx 12rpx;
+ font-size: 28rpx;
+ font-weight: 700;
+ color: #6B5D50;
+}
+
+/* ---- 老师详情视图 ---- */
+.detail-head {
+ display: flex; align-items: center; gap: 16rpx;
+ padding: 8rpx 4rpx 16rpx;
+ flex-shrink: 0;
+}
+.detail-title-wrap { flex: 1; min-width: 0; }
+.back {
+ color: #C67B30; font-size: 30rpx; flex-shrink: 0;
+ padding: 10rpx 16rpx 10rpx 0; border-radius: 12rpx;
+ font-weight: 600;
+}
+.back-hover { opacity: .7; }
+.detail-title { font-size: 34rpx; font-weight: 800; color: #2C1810; }
+.detail-sub { margin-top: 6rpx; font-size: 24rpx; color: #A99A89; }
+.head-hover { opacity: .8; }
+.head-arrow { color: #C67B30; font-size: 22rpx; }
+.detail-progress {
+ flex-shrink: 0;
+ padding: 8rpx 20rpx;
+ border-radius: 999rpx;
+ font-size: 24rpx;
+ background: #FFF3E6;
+ color: #C67B30;
+ font-weight: 700;
+}
+.detail-scroll {
+ flex: 1; min-height: 0;
+ padding: 0 4rpx;
+}
+.member-list {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 8rpx 24rpx;
+ box-shadow: 0 6rpx 20rpx rgba(44, 24, 16, 0.05);
+}
+.bottom-space { height: 40rpx; }
+.state-block { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; }
+.state-icon { font-size: 40rpx; margin-bottom: 12rpx; color: #D8C7B4; animation: pulse 1.2s ease-in-out infinite; }
+@keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:1} }
+/* 周选择弹层 */
+.picker-mask {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.4);
+ z-index: 100;
+}
+.picker-panel {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ border-radius: 28rpx 28rpx 0 0;
+ z-index: 101;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+ transform: translateY(100%);
+ transition: transform .28s ease;
+}
+.picker-panel.show {
+ transform: translateY(0);
+}
+.picker-title {
+ flex-shrink: 0;
+ padding: 28rpx 32rpx 20rpx;
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #2C1810;
+ text-align: center;
+ border-bottom: 1rpx solid #F2EDE7;
+}
+.picker-list {
+ flex: 1;
+ min-height: 0;
+ padding: 8rpx 0 32rpx;
+}
+.picker-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx 32rpx;
+ font-size: 28rpx;
+ color: #2C1810;
+}
+.picker-item:active {
+ background: #FBF7F1;
+}
+.picker-item.active {
+ color: #C67B30;
+ font-weight: 700;
+}
+.picker-check {
+ font-size: 28rpx;
+ color: #C67B30;
+}
+
+
+
+/* 历史列表底部 */
+.history-footer {
+ padding: 32rpx 0;
+ text-align: center;
+ font-size: 24rpx;
+ color: #B6A996;
+}
\ No newline at end of file
diff --git a/pages/home/home.js b/pages/home/home.js
index 83f08eb..5e74ef6 100644
--- a/pages/home/home.js
+++ b/pages/home/home.js
@@ -1,78 +1,94 @@
const api = require('../../utils/api');
const auth = require('../../utils/auth');
-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 String(err.message);
- }
- return '加载失败,请稍后再试';
-}
+const H5_HOME_URL = api.H5_BASE_URL + '/';
Page({
data: {
+ // 登录态标记
+ isLoggedIn: false,
+ // web-view 模式
+ webviewUrl: '',
+ webviewError: false,
+ errorMsg: '',
+ webviewKey: 0,
+ // 游客模式
greeting: '',
displayName: '',
encouragement: '',
modules: [],
- introData: null,
- loading: true,
- error: '',
- skeletonItems: [0, 1, 2, 3, 4, 5, 6, 7, 8]
+ introData: null
},
- onShow() { this.loadHome(); },
+ _webviewLoaded: false,
+ _lastToken: null,
+ _webviewKey: 0,
+
+ onShow() {
+ const currentToken = auth.getToken();
+ const isLoggedIn = !!currentToken;
- loadHome(silent) {
- if (!silent) {
- this.setData({ loading: true, error: '' });
+ if (isLoggedIn) {
+ // 已登录:每次进入首页都重新加载 H5 首页(确保从内部页面回到首页)
+ this._lastToken = currentToken;
+ this.setData({ isLoggedIn: true });
+ this.loadWebview();
+ this._webviewLoaded = true;
+ } else {
+ // 游客模式:token 未变化且已有数据时不重新加载
+ if (currentToken === this._lastToken && this.data.displayName) {
+ return;
+ }
+ this._lastToken = currentToken;
+ this.setData({ isLoggedIn: false });
+ this.loadGuestHome();
+ this._webviewLoaded = false;
}
+ },
+
+ // ---- 已登录:web-view 模式 ----
+
+ loadWebview() {
const token = auth.getToken();
- if (!token) {
- // 游客模式:展示公开内容,不强制登录
- this.setData({
- loading: false,
- error: '',
- greeting: '欢迎来到明缘云道场',
- displayName: '游客',
- encouragement: '登录后即可打卡、提交计划',
- modules: this.getGuestModules(),
- introData: this.getIntroData()
- });
- return Promise.resolve();
- }
- return auth.ensureLogin()
- .then((token) => {
- if (!token) {
- this.setData({
- loading: false,
- error: '',
- greeting: '欢迎来到明缘云道场',
- displayName: '游客',
- encouragement: '登录后即可打卡、提交计划',
- modules: this.getGuestModules(),
- introData: this.getIntroData()
- });
- return;
- }
- return api.request({ url: '/api/v1/pages/home' });
- })
- .then((res) => {
- if (!res) return;
- this.setData({
- loading: false,
- error: '',
- greeting: res.greeting || '',
- displayName: res.display_name || '',
- encouragement: res.encouragement || '',
- modules: (res.modules || []).filter((m) => m.visible),
- introData: null
- });
- })
- .catch((err) => {
- this.setData({ loading: false, error: errMessage(err) });
- });
+ const url = api.BASE_URL + '/api/v1/webview/redirect'
+ + '?token=' + encodeURIComponent(token)
+ + '&target=' + encodeURIComponent(H5_HOME_URL)
+ + '&_t=' + Date.now();
+ // key 自增,强制 web-view 组件销毁重建,解决切 tab 回来白屏问题
+ // 时间戳参数确保 url 每次不同,强制重新加载并跳转到 H5 首页
+ this._webviewKey += 1;
+ this.setData({
+ webviewUrl: url,
+ webviewError: false,
+ errorMsg: '',
+ webviewKey: this._webviewKey
+ });
+ },
+
+ onWebviewError(e) {
+ this.setData({
+ webviewError: true,
+ errorMsg: (e && e.detail && e.detail.errMsg) || '加载失败,请检查网络后重试'
+ });
+ },
+
+ onRetry() {
+ this.setData({ webviewUrl: '', webviewError: false, errorMsg: '' });
+ setTimeout(() => {
+ this.loadWebview();
+ }, 50);
+ },
+
+ // ---- 未登录:游客原生首页 ----
+
+ loadGuestHome() {
+ this.setData({
+ greeting: '欢迎来到明缘云道场',
+ displayName: '游客',
+ encouragement: '登录后即可打卡、提交计划',
+ modules: this.getGuestModules(),
+ introData: this.getIntroData()
+ });
},
getGuestModules() {
@@ -111,47 +127,36 @@ Page({
};
},
- onPullDownRefresh() {
- this.loadHome(true).then(() => {
- wx.stopPullDownRefresh();
- });
- },
-
- onRetry() {
- this.loadHome();
- },
-
goLogin() {
wx.switchTab({ url: '/pages/profile/profile' });
},
onModuleTap(e) {
const url = e.currentTarget.dataset.url;
- const token = auth.getToken();
- // 游客模式:引导登录
- if (!token) {
- wx.showModal({
- title: '登录提示',
- content: '登录后使用此功能,是否前往登录?',
- confirmText: '去登录',
- confirmColor: '#C67B30',
- success: (res) => {
- if (res.confirm) {
- wx.switchTab({ url: '/pages/profile/profile' });
- }
+ wx.showModal({
+ title: '登录提示',
+ content: '登录后使用此功能,是否前往登录?',
+ confirmText: '去登录',
+ confirmColor: '#C67B30',
+ success: (res) => {
+ if (res.confirm) {
+ wx.switchTab({ url: '/pages/profile/profile' });
}
- });
- return;
- }
- if (!url) return;
+ }
+ });
+ },
+ // ---- 分享 ----
+ onShareAppMessage() {
+ return {
+ title: '明缘云道场 · 学习成长,每日精进',
+ path: '/pages/home/home'
+ };
+ },
- // 通过 webview redirect 接口:后端验证 token 后设置 HttpOnly cookie,再 302 到目标页
- // H5 页面内的同域请求会自动携带 cookie,H5 端零改动
- const redirectUrl = api.BASE_URL + '/api/v1/webview/redirect'
- + '?token=' + encodeURIComponent(token)
- + '&target=' + encodeURIComponent(url);
- wx.navigateTo({ url: '/pages/webview/webview?url=' + encodeURIComponent(redirectUrl) });
+ onShareTimeline() {
+ return {
+ title: '明缘云道场 · 打卡学习好工具',
+ query: 'from=timeline'
+ };
}
});
-
-
diff --git a/pages/home/home.json b/pages/home/home.json
index 09e623a..93466ce 100644
--- a/pages/home/home.json
+++ b/pages/home/home.json
@@ -1,5 +1,4 @@
{
"usingComponents": {},
- "enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
-}
\ No newline at end of file
+}
diff --git a/pages/home/home.wxml b/pages/home/home.wxml
index 9caca05..1d5cf19 100644
--- a/pages/home/home.wxml
+++ b/pages/home/home.wxml
@@ -1,15 +1,16 @@
-
-
-
-
-
-
-
-
+
+
+
+
+ !
+ 页面加载失败
+ {{errorMsg}}
+ 重新加载
+
{{greeting}}
@@ -20,11 +21,6 @@
-
- {{error}}
- 重试
-
-
-
{{introData.title}}
@@ -51,7 +46,5 @@
-
- 暂无功能入口
diff --git a/pages/home/home.wxss b/pages/home/home.wxss
index 183c863..8bbbdd6 100644
--- a/pages/home/home.wxss
+++ b/pages/home/home.wxss
@@ -36,12 +36,17 @@
color: #E8DCCB;
line-height: 1.6;
}
-.hero-skeleton {
- min-height: 240rpx;
- background: linear-gradient(90deg, #EFE6D9 25%, #FAF4EC 50%, #EFE6D9 75%);
- background-size: 200% 100%;
- animation: shimmer 1.4s ease-in-out infinite;
+.hero-login-tip {
+ margin-top: 24rpx;
+ display: inline-block;
+ padding: 12rpx 28rpx;
+ background: rgba(198, 123, 48, 0.25);
+ border: 1rpx solid rgba(227, 179, 124, 0.4);
+ border-radius: 999rpx;
+ color: #E3B37C;
+ font-size: 24rpx;
}
+.btn-hover { opacity: 0.7; }
/* ---- 模块网格 ---- */
.grid {
@@ -98,72 +103,6 @@
white-space: nowrap;
}
-/* ---- 骨架屏 ---- */
-.module-skeleton {
- box-shadow: none;
-}
-.sk {
- background: #EDE3D6;
- border-radius: 10rpx;
-}
-.sk-circle {
- width: 84rpx;
- height: 84rpx;
- border-radius: 50%;
- margin-bottom: 16rpx;
-}
-.sk-line {
- height: 24rpx;
- margin-top: 12rpx;
-}
-.w-70 { width: 70%; }
-.w-40 { width: 40%; }
-
-/* ---- 错误 / 空状态 ---- */
-.error-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20rpx;
- margin-top: 32rpx;
- padding: 22rpx 24rpx;
- border-radius: 14rpx;
- background: #FDEAEA;
- color: #C0392B;
- font-size: 24rpx;
-}
-.error-text {
- flex: 1;
- min-width: 0;
-}
-.error-retry {
- flex-shrink: 0;
- color: #C67B30;
- font-weight: 700;
- padding: 4rpx 0 4rpx 16rpx;
-}
-.link-hover { opacity: 0.7; }
-.empty {
- padding: 80rpx 0;
- text-align: center;
- color: #B6A996;
- font-size: 26rpx;
-}
-
-@keyframes shimmer {
- 0% { background-position: 200% 0; }
- 100% { background-position: -200% 0; }
-}
-.hero-login-tip {
- margin-top: 24rpx;
- display: inline-block;
- padding: 12rpx 28rpx;
- background: rgba(198, 123, 48, 0.25);
- border: 1rpx solid rgba(227, 179, 124, 0.4);
- border-radius: 999rpx;
- color: #E3B37C;
- font-size: 24rpx;
-}
/* ---- intro section ---- */
.intro-section {
margin-top: 40rpx;
@@ -205,4 +144,48 @@
font-size: 25rpx;
color: #5A4A3A;
line-height: 1.8;
-}
\ No newline at end of file
+}
+
+/* ---- web-view 错误页 ---- */
+.error-page {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 120rpx 60rpx;
+ height: 100vh;
+ box-sizing: border-box;
+}
+.error-icon {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 50%;
+ background: #FDEAEA;
+ color: #C0392B;
+ font-size: 64rpx;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 32rpx;
+}
+.error-title {
+ font-size: 32rpx;
+ font-weight: 700;
+ color: #2C1810;
+ margin-bottom: 16rpx;
+}
+.error-desc {
+ font-size: 26rpx;
+ color: #8B7A6A;
+ text-align: center;
+ line-height: 1.6;
+ margin-bottom: 40rpx;
+}
+.error-retry {
+ padding: 20rpx 60rpx;
+ background: #C67B30;
+ color: #fff;
+ font-size: 28rpx;
+ border-radius: 999rpx;
+}
diff --git a/pages/plan-submit/plan-submit.js b/pages/plan-submit/plan-submit.js
index ec949d7..9804c5d 100644
--- a/pages/plan-submit/plan-submit.js
+++ b/pages/plan-submit/plan-submit.js
@@ -111,7 +111,7 @@ Page({
record,
content: record ? (record.content || '') : '',
attachments,
- attachmentThumbs: attachments.map((p) => this.planFileUrl(p))
+ attachmentThumbs: attachments.map((p) => api.planFileUrl(p))
});
},
@@ -120,7 +120,7 @@ Page({
.then((res) => {
const history = (res.list || []).map((item) => ({
...item,
- thumbSrcs: (item.attachments || []).map((p) => this.planFileUrl(p))
+ thumbSrcs: (item.attachments || []).map((p) => api.planFileUrl(p))
}));
this.setData({ history });
}).catch(() => {});
@@ -130,16 +130,6 @@ Page({
this.setData({ content: e.detail.value });
},
- planFileUrl(path) {
- if (!path) return '';
- if (/^https?:\/\//i.test(path)) return path;
- const parts = String(path).replace(/^attachments\//, '').split('/');
- if (parts.length < 4) {
- return '';
- }
- return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/');
- },
-
chooseImages() {
const remain = MAX_IMAGES - this.data.attachments.length;
if (remain <= 0) {
@@ -173,7 +163,7 @@ Page({
if (index >= paths.length) {
this.setData({
uploading: false,
- attachmentThumbs: attachments.map((p) => this.planFileUrl(p))
+ attachmentThumbs: attachments.map((p) => api.planFileUrl(p))
});
return;
}
@@ -196,7 +186,7 @@ Page({
attachments = attachments.concat(data.attachments).slice(0, MAX_IMAGES);
this.setData({
attachments,
- attachmentThumbs: attachments.map((p) => this.planFileUrl(p))
+ attachmentThumbs: attachments.map((p) => api.planFileUrl(p))
});
} else {
wx.showToast({ title: data.error || '上传失败', icon: 'none' });
@@ -252,7 +242,7 @@ Page({
attachments.splice(index, 1);
this.setData({
attachments,
- attachmentThumbs: attachments.map((p) => this.planFileUrl(p))
+ attachmentThumbs: attachments.map((p) => api.planFileUrl(p))
});
};
if (parts.length < 4) {
@@ -346,5 +336,20 @@ Page({
} else {
doPost();
}
+ },
+ // ---- 分享 ----
+ onShareAppMessage() {
+ const typeLabel = this.data.typeLabel;
+ return {
+ title: '明缘云道场 · ' + typeLabel + '提交',
+ path: '/pages/plan/plan'
+ };
+ },
+
+ onShareTimeline() {
+ return {
+ title: '明缘云道场 · 周/月/年计划,让成长有迹可循',
+ query: 'from=timeline'
+ };
}
-});
+});
\ No newline at end of file
diff --git a/pages/plan/plan.js b/pages/plan/plan.js
index 09fb809..9cab3d3 100644
--- a/pages/plan/plan.js
+++ b/pages/plan/plan.js
@@ -1,27 +1,48 @@
const api = require('../../utils/api');
const auth = require('../../utils/auth');
+// 计划类型常量
const TYPES = ['weekly', 'monthly', 'yearly'];
const TYPE_LABELS = ['周计划', '月计划', '年计划'];
Page({
data: {
+ // 类型切换相关
types: TYPE_LABELS,
typeIndex: 0,
- view: 'overview', // overview | detail
+ // 视图模式:overview 总览 / detail 小组详情(仅老师)
+ view: 'overview',
+ // 加载状态
loading: false,
refreshing: false,
+ // 当前周期
periodKey: '',
+ // 提交相关
canSubmit: false,
message: '',
+ // 我的计划状态
myChecked: false,
myStatus: '',
myRecord: null,
myThumbs: [],
+ // 未分组人数(仅老师可见)
unassignedCount: 0,
+ // 小组列表
groups: [],
+ // 当前选中的小组
selectedGroupName: '',
- members: []
+ // 小组成员列表(老师详情 & 学员小组 tab)
+ members: [],
+ // 用户角色
+ isTeacher: false,
+ myGroup: '',
+ // 学员视图 tab:group / personal
+ studentTab: 'group',
+ // 个人历史列表
+ history: [],
+ // 周期切换相关
+ periodList: [], // 可用周期列表
+ showPeriodPicker: false,
},
onShow() {
@@ -33,53 +54,141 @@ Page({
}
wx.removeStorageSync('plan_overview_dirty');
this.setData({ view: 'overview' });
- this.loadOverview();
- }).catch(() => {});
+ // 先加载周期列表,再加载总览(总览接口会返回 is_teacher)
+ this.loadPeriods().then(() => {
+ this.loadOverview();
+ });
+ }).catch((err) => {
+ console.error('plan page init error:', err);
+ });
},
+ /** 切换计划类型(周/月/年) */
onTypeChange(e) {
const typeIndex = Number(e.currentTarget.dataset.index);
if (typeIndex === this.data.typeIndex) {
return;
}
- this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [] });
- this.loadOverview();
+ this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [], history: [], studentTab: 'group' });
+ // 切换类型后重新加载周期列表和总览
+ this.loadPeriods().then(() => {
+ this.loadOverview();
+ });
},
+ /** 下拉刷新 */
onRefresh() {
this.setData({ refreshing: true });
- this.loadOverview();
+ this.loadPeriods().then(() => {
+ return this.loadOverview();
+ }).then(() => {
+ // 学员个人 tab 也刷一下历史
+ if (!this.data.isTeacher && this.data.studentTab === 'personal') {
+ return this.loadHistory();
+ }
+ }).then(() => {
+ this.setData({ refreshing: false });
+ });
+ },
+
+ /** 加载可用周期列表 */
+ loadPeriods() {
+ const type = TYPES[this.data.typeIndex];
+ return api.request({ url: '/api/v1/plan/periods', data: { type } })
+ .then((res) => {
+ const periods = res.periods || [];
+ // 默认选第一个(最新)周期,或者后端返回的当前周期
+ let periodKey = periods[0] || res.current_period || '';
+ this.setData({
+ periodList: periods,
+ periodKey: periodKey || this.data.periodKey,
+ });
+ })
+ .catch(() => {
+ // 获取失败不影响主流程
+ console.warn('获取周期列表失败');
+ });
+ },
+
+ /** 打开周期选择器 */
+ openPeriodPicker() {
+ if (!this.data.periodList.length) {
+ return;
+ }
+ this.setData({ showPeriodPicker: true });
+ },
+
+ /** 关闭周期选择器 */
+ closePeriodPicker() {
+ this.setData({ showPeriodPicker: false });
+ },
+
+ /** 选择某个周期 */
+ onPeriodSelect(e) {
+ const period = e.currentTarget.dataset.period;
+ if (!period || period === this.data.periodKey) {
+ this.setData({ showPeriodPicker: false });
+ return;
+ }
+ this.setData({
+ periodKey: period,
+ showPeriodPicker: false,
+ });
+ if (this.data.view === 'detail') {
+ // 详情页:重新加载当前小组的该周期数据
+ this.loadDetail();
+ } else if (!this.data.isTeacher && this.data.studentTab === 'group' && this.data.myGroup) {
+ // 学员总览 + 小组 tab:刷新本组成员
+ this.loadOverview();
+ } else {
+ // 老师总览 / 学员个人 tab:重新加载总览
+ this.loadOverview();
+ }
},
+ /** 加载总览数据 */
loadOverview() {
const type = TYPES[this.data.typeIndex];
+ const periodKey = this.data.periodKey;
this.setData({ loading: true });
- api.request({ url: '/api/v1/plan/groups', data: { type } })
+ return api.request({ url: '/api/v1/plan/groups', data: { type, period_key: periodKey } })
.then((res) => {
- const groups = (res.groups || []).map((g) => ({
- ...g,
- percent: g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0
- }));
+ const groups = (res.groups || []).map((g) => {
+ const percent = g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0;
+ return {
+ ...g,
+ percent,
+ progressStyle: 'width:' + percent + '%;'
+ };
+ });
+ const isTeacher = !!res.is_teacher;
this.setData({
loading: false,
refreshing: false,
- periodKey: res.period_key || '',
+ periodKey: res.period_key || periodKey,
canSubmit: !!res.can_submit,
message: res.message || '',
myChecked: !!res.my_checked,
myStatus: res.my_status || '',
myRecord: res.my_record || null,
- myThumbs: ((res.my_record && res.my_record.attachments) || []).map((p) => this.planFileUrl(p)),
+ myThumbs: ((res.my_record && res.my_record.attachments) || []).map((p) => api.planFileUrl(p)),
unassignedCount: res.unassigned_count || 0,
- groups
+ isTeacher,
+ 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);
+ }
})
.catch(() => {
this.setData({ loading: false, refreshing: false });
- wx.showToast({ title: '加载小组失败', icon: 'none' });
+ wx.showToast({ title: '加载失败', icon: 'none' });
});
},
+ /** 打开小组详情 */
openGroup(e) {
const groupName = e.currentTarget.dataset.group;
if (!groupName) {
@@ -89,12 +198,29 @@ Page({
this.loadDetail();
},
+ /** 学员直接查看自己小组(只有一个小组时点击卡片直接进) */
+ openMyGroup() {
+ const groupName = this.data.myGroup;
+ if (!groupName) {
+ return;
+ }
+ this.setData({ selectedGroupName: groupName, view: 'detail', members: [] });
+ this.loadDetail();
+ },
+
+ /** 加载小组详情 */
loadDetail() {
const type = TYPES[this.data.typeIndex];
+ return this.loadDetailForGroup(this.data.selectedGroupName, type);
+ },
+
+ /** 加载指定小组的成员数据(不改变视图状态) */
+ loadDetailForGroup(groupName, type) {
+ type = type || TYPES[this.data.typeIndex];
this.setData({ loading: true });
- api.request({
+ return api.request({
url: '/api/v1/plan/group',
- data: { type, group_name: this.data.selectedGroupName, period_key: this.data.periodKey }
+ data: { type, group_name: groupName, period_key: this.data.periodKey }
}).then((res) => {
this.setData({
loading: false,
@@ -109,19 +235,44 @@ Page({
});
},
+ /** 学员切换 tab */
+ switchStudentTab(e) {
+ const tab = e.currentTarget.dataset.tab;
+ if (tab !== 'group' && tab !== 'personal') return;
+ this.setData({ studentTab: tab });
+ if (tab === 'personal') {
+ // 每次切到个人 tab 都刷新历史数据
+ this.loadHistory();
+ } else if (tab === 'group' && this.data.myGroup) {
+ // 每次切回小组 tab 都刷新小组成员数据
+ this.loadDetailForGroup(this.data.myGroup);
+ }
+ },
+
+ /** 加载个人计划历史 */
+ loadHistory() {
+ const type = TYPES[this.data.typeIndex];
+ return api.request({
+ 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',
+ }));
+ this.setData({ history });
+ }).catch(() => {});
+ },
+
+ /** 返回总览 */
backToOverview() {
this.setData({ view: 'overview', selectedGroupName: '', members: [] });
this.loadOverview();
},
- planFileUrl(path) {
- if (!path) return '';
- if (/^https?:\/\//i.test(path)) return path;
- const parts = String(path).replace(/^attachments\//, '').split('/');
- if (parts.length < 4) return '';
- return api.STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/');
- },
-
+ /** 预览我的图片 */
previewMyImage(e) {
const index = Number(e.currentTarget.dataset.index);
const urls = this.data.myThumbs;
@@ -130,10 +281,11 @@ Page({
wx.previewImage({ current: src, urls });
},
+ /** 去提交计划 */
goSubmit() {
const type = TYPES[this.data.typeIndex];
- if (type === 'weekly' && !this.data.canSubmit) {
- wx.showToast({ title: this.data.message || '当前不可打卡', icon: 'none' });
+ if (!this.data.canSubmit) {
+ wx.showToast({ title: this.data.message || '当前不可提交', icon: 'none' });
return;
}
const params = [
@@ -142,5 +294,26 @@ Page({
'group_name='
].join('&');
wx.navigateTo({ url: '/pages/plan-submit/plan-submit?' + params });
+ },
+
+
+ // ---- 分享 ----
+ onShareAppMessage() {
+ const type = this.data.types[this.data.typeIndex];
+ const checked = this.data.myChecked;
+ const title = checked
+ ? '我的' + type + '已提交,来明缘云道场一起成长吧'
+ : '明缘云道场 · ' + type + '管理工具';
+ return {
+ title,
+ path: '/pages/plan/plan'
+ };
+ },
+
+ onShareTimeline() {
+ return {
+ title: '明缘云道场 · 周/月/年计划,让成长有迹可循',
+ query: 'from=timeline'
+ };
}
-});
+});
\ No newline at end of file
diff --git a/pages/plan/plan.wxml b/pages/plan/plan.wxml
index 694a1f9..25ef6b3 100644
--- a/pages/plan/plan.wxml
+++ b/pages/plan/plan.wxml
@@ -1,6 +1,7 @@
+
+
{{types[typeIndex]}}
- {{periodKey}}
+
+ {{periodKey}}
+ ▾
+
+ {{periodKey}}
- {{myChecked ? '已提交' : '待提交'}}
+ {{myChecked ? '已提交' : '待提交'}}
+
{{myChecked ? '✓' : '●'}}
@@ -37,59 +44,141 @@
class="btn-mystatus {{myChecked ? 'outline' : ''}}"
bindtap="goSubmit"
hover-class="btn-hover"
- disabled="{{typeIndex === 0 && !canSubmit}}"
- >{{myChecked ? '修改' : '新增打卡'}}
-
-
-
-
- 我上传的计划
- {{myRecord.status}}
-
- {{myRecord.checked_at}} 提交
- {{myRecord.content}}
-
-
-
+ disabled="{{!canSubmit}}"
+ >{{myChecked ? '修改' : '提交计划'}}
- {{message}}
+
+ {{message}}
+
◌
加载中...
-
-
-
- {{index + 1}}
- {{group.group_name}}
-
- 已打卡 {{group.checked_count}}
- {{group.total}}人
+
+
+ 小组统计
+
+
+
+ {{index + 1}}
+ {{group.group_name}}
+
+ 已提交 {{group.checked_count}}
+ {{group.total}}人
+
+ ›
+
+
+
+
+
+
+
+ 另有未分组 {{unassignedCount}} 人
+ 暂无小组
+
+
+
+
+
+ 小组
+ 个人
+
+
+
+
+
+ {{myGroup}} · {{periodKey}}
+ ▾
+
+
+ ◌
+ 加载中...
+
+
+
+
+ {{index + 1}}
+
+ {{member.name}}
+ {{member.record.checked_at}} 提交
+
+
+ {{member.filled ? '已完成' : '未完成'}}
- ›
-
-
+
+ 👥
+ 暂无小组,请联系管理员分配
+
+
+ 📭
+ 暂无成员数据
-
- 另有未分组 {{unassignedCount}} 人
- 暂无小组
+
+
+
+ 📒
+ 暂无{{types[typeIndex]}}记录
+
+
+
+ {{item.period_key}}
+ {{item.status}}
+
+ {{item.checked_at}} 提交
+ {{item.content}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择周期
+
+
+ {{item}}
+ ✓
+
+
+
+
‹ 返回
{{selectedGroupName}}
- {{periodKey}}
+
+ {{periodKey}} ▾
+
@@ -111,6 +200,24 @@
暂无成员
+
+
+
+
+ 选择周期
+
+
+ {{item}}
+ ✓
+
+
+
-
\ No newline at end of file
+
diff --git a/pages/plan/plan.wxss b/pages/plan/plan.wxss
index 0bebaf0..adff177 100644
--- a/pages/plan/plan.wxss
+++ b/pages/plan/plan.wxss
@@ -77,4 +77,173 @@
.state-block { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; }
.state-icon { font-size: 40rpx; margin-bottom: 12rpx; color: #D8C7B4; animation: pulse 1.2s ease-in-out infinite; }
@keyframes pulse { 0%,100%{opacity:.4} 50%{opacity:1} }
-.empty { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; }
\ No newline at end of file
+.empty { padding: 64rpx 0; text-align: center; color: #A99A89; font-size: 26rpx; }
+
+/* 小组标题 */
+.section-title { padding: 8rpx 4rpx 16rpx; font-size: 28rpx; font-weight: 700; color: #6B5D50; }
+
+/* 学员小组 tab:周期选择行 */
+.group-period-row {
+ display: flex;
+ align-items: center;
+ padding: 12rpx 8rpx;
+ font-size: 28rpx;
+ font-weight: 700;
+ color: #C67B30;
+}
+.group-period-text {
+ flex: 1;
+}
+
+/* 学员单小组卡片 */
+.student-group { margin-bottom: 20rpx; }
+
+/* 学员 tab 切换 */
+.student-tabs {
+ display: flex;
+ border-bottom: 1rpx solid #EEE;
+ margin: 8rpx 0 8rpx;
+}
+.student-tab {
+ flex: 1;
+ text-align: center;
+ padding: 22rpx 0;
+ color: #7A6A5C;
+ transition: color .15s ease;
+ font-size: 28rpx;
+}
+.student-tab.active {
+ color: #C67B30;
+ font-weight: 700;
+ border-bottom: 4rpx solid #C67B30;
+ transform: translateY(-2rpx);
+}
+.tab-hover {
+ background: #f6efe6;
+}
+
+.student-group-panel {
+ padding: 4rpx 0 24rpx;
+}
+.student-history-panel {
+ padding: 16rpx 0 24rpx;
+}
+
+/* 历史记录卡片 */
+.history-card {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 24rpx;
+ margin-bottom: 18rpx;
+ box-shadow: 0 6rpx 20rpx rgba(44, 24, 16, 0.05);
+}
+.history-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.history-period {
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #2C1810;
+}
+.history-status {
+ padding: 6rpx 16rpx;
+ border-radius: 999rpx;
+ font-size: 22rpx;
+ font-weight: 700;
+}
+.history-meta {
+ margin-top: 8rpx;
+ font-size: 22rpx;
+ color: #A99A89;
+}
+.history-content {
+ margin-top: 14rpx;
+ padding: 16rpx 20rpx;
+ background: #FBF7F1;
+ border-radius: 14rpx;
+ font-size: 26rpx;
+ line-height: 1.7;
+ color: #2C1810;
+ white-space: pre-wrap;
+ word-break: break-all;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+}
+.history-thumbs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+ margin-top: 14rpx;
+}
+.history-thumb {
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 12rpx;
+ background: #F2EDE7;
+}
+
+/* 周期选择相关 */
+.head-hover { opacity: .8; }
+.head-arrow { color: #C67B30; font-size: 22rpx; }
+
+.picker-mask {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.4);
+ z-index: 100;
+}
+.picker-panel {
+ position: fixed;
+ left: 0; right: 0; bottom: 0;
+ background: #fff;
+ border-radius: 28rpx 28rpx 0 0;
+ z-index: 101;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+ transform: translateY(100%);
+ transition: transform .28s ease;
+}
+.picker-panel.show {
+ transform: translateY(0);
+}
+.picker-title {
+ flex-shrink: 0;
+ padding: 28rpx 32rpx 20rpx;
+ font-size: 30rpx;
+ font-weight: 700;
+ color: #2C1810;
+ text-align: center;
+ border-bottom: 1rpx solid #F2EDE7;
+}
+.picker-list {
+ flex: 1;
+ min-height: 0;
+ padding: 8rpx 0 32rpx;
+}
+.picker-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 24rpx 32rpx;
+ font-size: 28rpx;
+ color: #2C1810;
+}
+.picker-item:active {
+ background: #FBF7F1;
+}
+.picker-item.active {
+ color: #C67B30;
+ font-weight: 700;
+}
+.picker-check {
+ font-size: 28rpx;
+ color: #C67B30;
+}
+
+/* 底部留白 */
+.bottom-space { height: 40rpx; }
\ No newline at end of file
diff --git a/pages/privacy/privacy.js b/pages/privacy/privacy.js
index 46ee0a5..4f25bba 100644
--- a/pages/privacy/privacy.js
+++ b/pages/privacy/privacy.js
@@ -14,5 +14,19 @@ Page({
const title = TITLES[type] || '隐私政策';
this.setData({ type, title });
wx.setNavigationBarTitle({ title });
+ },
+ // ---- 分享 ----
+ onShareAppMessage() {
+ const title = this.data.title || '明缘云道场';
+ return {
+ title,
+ path: '/pages/privacy/privacy?type=' + this.data.type
+ };
+ },
+ onShareTimeline() {
+ return {
+ title: this.data.title || '明缘云道场',
+ query: 'type=' + this.data.type
+ };
}
});
diff --git a/pages/profile/profile.js b/pages/profile/profile.js
index 55e2f69..65e460c 100644
--- a/pages/profile/profile.js
+++ b/pages/profile/profile.js
@@ -40,7 +40,7 @@ Page({
},
onPullDownRefresh() {
- this.refresh().then(() => wx.stopPullDownRefresh());
+ this.refresh().finally(() => wx.stopPullDownRefresh());
},
refresh() {
@@ -225,34 +225,42 @@ Page({
wx.navigateTo({ url: '/pages/privacy/privacy?type=' + type });
},
- deleteAccount() {
+ logoutAccount() {
wx.showModal({
- title: '注销账号',
- content: '确定要注销账号吗?注销后将清除本机登录信息,需要重新登录。',
- confirmText: '注销',
+ title: '退出登录',
+ content: '确定要退出登录吗?退出后将清除本机登录信息,需要重新登录。',
+ confirmText: '退出',
confirmColor: '#c0392b',
success: (res) => {
if (!res.confirm) return;
- wx.showModal({
- title: '再次确认',
- content: '注销后本机登录状态将被清除,你可以使用手机号重新登录。确定继续吗?',
- confirmText: '确认注销',
- confirmColor: '#c0392b',
- success: (res2) => {
- if (!res2.confirm) return;
- // 仅清除前端登录信息
- try {
- api.request({ url: '/api/v1/auth/logout', method: 'POST' }).catch(() => {});
- } catch (e) {}
- auth.clearToken();
- wx.removeStorageSync('privacy_agreed');
- wx.showToast({ title: '已注销', icon: 'success' });
- setTimeout(() => {
- wx.reLaunch({ url: '/pages/home/home' });
- }, 1000);
- }
- });
+ try {
+ api.request({ url: '/api/v1/auth/logout', method: 'POST' }).catch(() => {});
+ } catch (e) {}
+ auth.clearToken();
+ wx.removeStorageSync('privacy_agreed');
+ wx.showToast({ title: '已退出', icon: 'success' });
+ setTimeout(() => {
+ wx.reLaunch({ url: '/pages/home/home' });
+ }, 1000);
}
});
+ },
+ // ---- 分享 ----
+ onShareAppMessage() {
+ const name = (this.data.profile && (this.data.profile.display_name || this.data.profile.name)) || '';
+ const title = name
+ ? name + ' 推荐你用明缘云道场,打卡学习超好用'
+ : '推荐你用明缘云道场,打卡学习超好用';
+ return {
+ title,
+ path: '/pages/home/home?from=profile_share'
+ };
+ },
+
+ onShareTimeline() {
+ return {
+ title: '明缘云道场 · 学习成长,每日精进',
+ query: 'from=timeline'
+ };
}
});
\ No newline at end of file
diff --git a/pages/profile/profile.wxml b/pages/profile/profile.wxml
index 1d187c1..a6eb377 100644
--- a/pages/profile/profile.wxml
+++ b/pages/profile/profile.wxml
@@ -110,7 +110,7 @@
-
+
diff --git a/pages/profile/profile.wxss b/pages/profile/profile.wxss
index 50e15ec..f42c65f 100644
--- a/pages/profile/profile.wxss
+++ b/pages/profile/profile.wxss
@@ -227,11 +227,10 @@ button::after { border: none; }
}
/* ---- delete account ---- */
-.btn-delete {
+.btn-logout {
background: #fff;
color: #c0392b;
border: 1rpx solid #f0b4ae;
border-radius: 12rpx;
margin-bottom: 20rpx;
-}
-/* ---- profile name edit ---- */
+}
\ No newline at end of file
diff --git a/pages/webview/webview.js b/pages/webview/webview.js
index 19347f9..3b22cb2 100644
--- a/pages/webview/webview.js
+++ b/pages/webview/webview.js
@@ -43,5 +43,19 @@ Page({
this.setData({ url: this.data._url || '' });
}, 50);
}
+ },
+ // ---- 分享 ----
+ onShareAppMessage() {
+ return {
+ title: '明缘云道场',
+ path: '/pages/home/home'
+ };
+ },
+
+ onShareTimeline() {
+ return {
+ title: '明缘云道场 · 学习成长,每日精进',
+ query: 'from=timeline'
+ };
}
});
diff --git a/project.private.config.json b/project.private.config.json
index 0c24ae1..3e4597d 100644
--- a/project.private.config.json
+++ b/project.private.config.json
@@ -42,5 +42,6 @@
},
"setting": {
"urlCheck": false
- }
+ },
+ "libVersion": "3.16.2"
}
\ No newline at end of file
diff --git a/utils/api.js b/utils/api.js
index af6b66c..a2780a2 100644
--- a/utils/api.js
+++ b/utils/api.js
@@ -1,7 +1,43 @@
-//const BASE_URL = 'https://muguaclub.cn';
-//const BASE_URL = "http://127.0.0.1:8000";
-const BASE_URL = 'https://m.muguaclub.cn/clock';
-const STATIC_BASE_URL = 'https://m.muguaclub.cn';
+// 根据小程序版本自动选择后端地址
+function _getBaseUrls() {
+ let envType = 'release';
+ try {
+ const accountInfo = wx.getAccountInfoSync();
+ envType = accountInfo.miniProgram.envVersion || 'release';
+ } catch (e) {
+ // 低版本基础库不支持时默认线上环境
+ envType = 'release';
+ }
+ switch (envType) {
+ case 'develop':
+ // 开发版:本地服务
+ return {
+ baseUrl: 'https://test.muguaclub.cn/clock',
+ staticUrl: 'https://test.muguaclub.cn',
+ h5Url: 'https://test.muguaclub.cn'
+ };
+ case 'trial':
+ // 体验版:测试环境
+ return {
+ baseUrl: 'https://test.muguaclub.cn/clock',
+ staticUrl: 'https://test.muguaclub.cn',
+ h5Url: 'https://test.muguaclub.cn'
+ };
+ case 'release':
+ default:
+ // 线上版:正式环境
+ return {
+ baseUrl: 'https://m.muguaclub.cn/clock',
+ staticUrl: 'https://m.muguaclub.cn',
+ h5Url: 'https://m.muguaclub.cn'
+ };
+ }
+}
+
+const _urls = _getBaseUrls();
+const BASE_URL = _urls.baseUrl;
+const STATIC_BASE_URL = _urls.staticUrl;
+const H5_BASE_URL = _urls.h5Url;
let redirectingProfile = false;
@@ -73,4 +109,16 @@ function request(options) {
});
}
-module.exports = { request, BASE_URL, STATIC_BASE_URL };
+/**
+ * 把计划附件的相对路径转为完整的静态资源 URL。
+ * 路径格式:attachments/{user_id}/{plan_type}/{period_key}/{filename}
+ */
+function planFileUrl(path) {
+ if (!path) return '';
+ if (/^https?:\/\//i.test(path)) return path;
+ const parts = String(path).replace(/^attachments\//, '').split('/');
+ if (parts.length < 4) return '';
+ return STATIC_BASE_URL + '/clock/static/plan/' + parts.slice(0, 4).map(encodeURIComponent).join('/');
+}
+
+module.exports = { request, planFileUrl, BASE_URL, STATIC_BASE_URL, H5_BASE_URL };