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 @@ + + + + + + @@ -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 };