feat(database): 迁移用户关联字段并优化打卡系统
- 修改数据库配置将MYSQL_DB从mys_info_v3改为mys_info_test - 新增auth_users表wechat_openid字段并建立唯一索引 - 为plan_checkins表添加计划性能索引(idx_plan_period_student) - 将checkin_records_old表关联方式从student_id改为user_id - 将plan_checkins表关联方式从student_id改为user_id并更新附件路径 - 移除routes/auth.py中的uuid导入使用安全token生成函数 - 重构routes/checkin.py打卡逻辑去除course_type字段并优化返回结构 - 添加打卡时间限制验证功能防止过期打卡操作 - 实现小组统计功能支持老师查看各组打卡情况 - 更新主页index.html界面适配新的打卡和计划功能 - 调整计划提交时间规则为周五至周四周期 - 添加月度和年度计划提交状态验证机制
This commit is contained in:
@@ -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'
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
<view class="page">
|
||||
<block wx:if="{{view === 'overview'}}">
|
||||
<view class="view-root">
|
||||
<!-- 顶部类型切换 -->
|
||||
<view class="type-tabs">
|
||||
<view
|
||||
class="type-tab {{typeIndex === index ? 'active' : ''}}"
|
||||
@@ -19,14 +20,20 @@
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
>
|
||||
<!-- 头部:类型 + 我的状态 -->
|
||||
<view class="overview-head">
|
||||
<view class="head-title-wrap">
|
||||
<view class="head-title">{{types[typeIndex]}}</view>
|
||||
<view class="head-sub">{{periodKey}}</view>
|
||||
<view class="head-sub" wx:if="{{isTeacher}}" bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}" hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}">
|
||||
{{periodKey}}
|
||||
<text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
|
||||
</view>
|
||||
<view class="head-sub" wx:else>{{periodKey}}</view>
|
||||
</view>
|
||||
<view class="head-pill">{{myChecked ? '已提交' : '待提交'}}</view>
|
||||
<view class="head-pill {{myChecked ? 'checked' : ''}}">{{myChecked ? '已提交' : '待提交'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的计划卡片 -->
|
||||
<view class="card my-status {{myChecked ? 'checked' : 'unchecked'}}">
|
||||
<view class="my-state-icon {{myChecked ? 'done' : 'todo'}}">{{myChecked ? '✓' : '●'}}</view>
|
||||
<view class="my-status-left">
|
||||
@@ -37,59 +44,141 @@
|
||||
class="btn-mystatus {{myChecked ? 'outline' : ''}}"
|
||||
bindtap="goSubmit"
|
||||
hover-class="btn-hover"
|
||||
disabled="{{typeIndex === 0 && !canSubmit}}"
|
||||
>{{myChecked ? '修改' : '新增打卡'}}</button>
|
||||
disabled="{{!canSubmit}}"
|
||||
>{{myChecked ? '修改' : '提交计划'}}</button>
|
||||
</view>
|
||||
|
||||
<view class="card my-plan" wx:if="{{myChecked && myRecord}}">
|
||||
<view class="my-plan-head">
|
||||
<text class="my-plan-title">我上传的计划</text>
|
||||
<text class="my-plan-status {{myRecord.status === '未完成' ? 'pending' : ''}}">{{myRecord.status}}</text>
|
||||
</view>
|
||||
<view class="my-plan-meta" wx:if="{{myRecord.checked_at}}">{{myRecord.checked_at}} 提交</view>
|
||||
<view class="my-plan-content" wx:if="{{myRecord.content}}">{{myRecord.content}}</view>
|
||||
<view class="my-plan-thumbs" wx:if="{{myThumbs.length}}">
|
||||
<image class="my-plan-thumb" wx:for="{{myThumbs}}" wx:key="*this" src="{{item}}" mode="aspectFill" data-index="{{index}}" bindtap="previewMyImage"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="window-msg" wx:if="{{typeIndex === 0 && message}}">{{message}}</view>
|
||||
<!-- 提交时间窗口提示 -->
|
||||
<view class="window-msg" wx:if="{{message}}">{{message}}</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="state-block" wx:if="{{loading && groups.length === 0}}">
|
||||
<view class="state-icon">◌</view>
|
||||
<view>加载中...</view>
|
||||
</view>
|
||||
|
||||
<view class="group-card" hover-class="group-card-hover" wx:for="{{groups}}" wx:key="group_name" wx:for-item="group">
|
||||
<view class="group-main" data-group="{{group.group_name}}" bindtap="openGroup">
|
||||
<view class="group-row">
|
||||
<view class="group-avatar">{{index + 1}}</view>
|
||||
<view class="group-name">{{group.group_name}}</view>
|
||||
<view class="group-counts">
|
||||
<text class="count done">已打卡 {{group.checked_count}}</text>
|
||||
<text class="count total">{{group.total}}人</text>
|
||||
<!-- 老师:显示所有小组列表 -->
|
||||
<block wx:if="{{isTeacher}}">
|
||||
<view class="section-title">小组统计</view>
|
||||
<view class="group-card" hover-class="group-card-hover" wx:for="{{groups}}" wx:key="group_name" wx:for-item="group">
|
||||
<view class="group-main" data-group="{{group.group_name}}" bindtap="openGroup">
|
||||
<view class="group-row">
|
||||
<view class="group-avatar">{{index + 1}}</view>
|
||||
<view class="group-name">{{group.group_name}}</view>
|
||||
<view class="group-counts">
|
||||
<text class="count done">已提交 {{group.checked_count}}</text>
|
||||
<text class="count total">{{group.total}}人</text>
|
||||
</view>
|
||||
<view class="chevron">›</view>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-bar" style="{{group.progressStyle}}"></view>
|
||||
</view>
|
||||
<view class="chevron">›</view>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-bar" style="width: {{group.percent}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="unassigned" wx:if="{{unassignedCount > 0}}">另有未分组 {{unassignedCount}} 人</view>
|
||||
<view class="empty" wx:if="{{!loading && groups.length === 0}}">暂无小组</view>
|
||||
<view class="unassigned" wx:if="{{unassignedCount > 0}}">另有未分组 {{unassignedCount}} 人</view>
|
||||
<view class="empty" wx:if="{{!loading && groups.length === 0}}">暂无小组</view>
|
||||
</block>
|
||||
|
||||
<!-- 学员:tab 切换(小组/个人) -->
|
||||
<block wx:else>
|
||||
<view class="student-tabs">
|
||||
<view class="student-tab {{studentTab === 'group' ? 'active' : ''}}" data-tab="group" hover-class="tab-hover" bindtap="switchStudentTab">小组</view>
|
||||
<view class="student-tab {{studentTab === 'personal' ? 'active' : ''}}" data-tab="personal" hover-class="tab-hover" bindtap="switchStudentTab">个人</view>
|
||||
</view>
|
||||
|
||||
<!-- 小组 tab:本组成员列表 -->
|
||||
<view wx:if="{{studentTab === 'group'}}" class="student-group-panel">
|
||||
<view
|
||||
class="group-period-row"
|
||||
wx:if="{{myGroup}}"
|
||||
bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}"
|
||||
hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}"
|
||||
>
|
||||
<text class="group-period-text">{{myGroup}} · {{periodKey}}</text>
|
||||
<text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
|
||||
</view>
|
||||
<view class="state-block" wx:if="{{loading && members.length === 0}}">
|
||||
<view class="state-icon">◌</view>
|
||||
<view>加载中...</view>
|
||||
</view>
|
||||
<view class="member-list" wx:elif="{{members.length > 0}}">
|
||||
<view class="member" wx:for="{{members}}" wx:key="user_id" wx:for-item="member">
|
||||
<view class="member-left">
|
||||
<view class="member-avatar {{member.filled ? 'done' : 'unchecked'}}">{{index + 1}}</view>
|
||||
<view class="member-info">
|
||||
<view class="member-name">{{member.name}}</view>
|
||||
<view class="member-meta" wx:if="{{member.filled && member.record}}">{{member.record.checked_at}} 提交</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="badge {{member.filled ? 'done' : 'unchecked'}}">{{member.filled ? '已完成' : '未完成'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" wx:elif="{{!loading && !myGroup}}">
|
||||
<view class="empty-icon">👥</view>
|
||||
<view class="empty-text">暂无小组,请联系管理员分配</view>
|
||||
</view>
|
||||
<view class="empty" wx:elif="{{!loading && members.length === 0}}">
|
||||
<view class="empty-icon">📭</view>
|
||||
<view class="empty-text">暂无成员数据</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人 tab:历史记录 -->
|
||||
<view wx:else class="student-history-panel">
|
||||
<view class="empty" wx:if="{{history.length === 0}}">
|
||||
<view class="empty-icon">📒</view>
|
||||
<view class="empty-text">暂无{{types[typeIndex]}}记录</view>
|
||||
</view>
|
||||
<view class="history-card" wx:for="{{history}}" wx:key="id">
|
||||
<view class="history-head">
|
||||
<view class="history-period">{{item.period_key}}</view>
|
||||
<view class="history-status" style="color:{{item.statusColor}};background:{{item.statusBg}}">{{item.status}}</view>
|
||||
</view>
|
||||
<view class="history-meta" wx:if="{{item.checked_at}}">{{item.checked_at}} 提交</view>
|
||||
<view class="history-content" wx:if="{{item.content}}">{{item.content}}</view>
|
||||
<view class="history-thumbs" wx:if="{{item.thumbs.length > 0}}">
|
||||
<image class="history-thumb" wx:for="{{item.thumbs}}" wx:for-item="thumb" wx:key="*this" src="{{thumb}}" mode="aspectFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 周期选择弹层 -->
|
||||
<view class="picker-mask" wx:if="{{showPeriodPicker}}" bindtap="closePeriodPicker"></view>
|
||||
<view class="picker-panel {{showPeriodPicker ? 'show' : ''}}" wx:if="{{showPeriodPicker}}">
|
||||
<view class="picker-title">选择周期</view>
|
||||
<scroll-view class="picker-list" scroll-y>
|
||||
<view
|
||||
class="picker-item {{item === periodKey ? 'active' : ''}}"
|
||||
wx:for="{{periodList}}"
|
||||
wx:key="*this"
|
||||
data-period="{{item}}"
|
||||
bindtap="onPeriodSelect"
|
||||
>
|
||||
<text>{{item}}</text>
|
||||
<text class="picker-check" wx:if="{{item === periodKey}}">✓</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 小组详情页 -->
|
||||
<block wx:else>
|
||||
<view class="view-root">
|
||||
<view class="detail-head">
|
||||
<view class="back" hover-class="back-hover" bindtap="backToOverview">‹ 返回</view>
|
||||
<view class="detail-title-wrap">
|
||||
<view class="head-title">{{selectedGroupName}}</view>
|
||||
<view class="head-sub">{{periodKey}}</view>
|
||||
<view class="head-sub" bindtap="{{periodList.length > 0 ? 'openPeriodPicker' : ''}}" hover-class="{{periodList.length > 0 ? 'head-hover' : ''}}">
|
||||
{{periodKey}} <text class="head-arrow" wx:if="{{periodList.length > 0}}"> ▾</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="view-scroll" scroll-y>
|
||||
@@ -111,6 +200,24 @@
|
||||
</view>
|
||||
<view class="empty" wx:if="{{!loading && members.length === 0}}">暂无成员</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 周期选择弹层(详情页也可用) -->
|
||||
<view class="picker-mask" wx:if="{{showPeriodPicker}}" bindtap="closePeriodPicker"></view>
|
||||
<view class="picker-panel {{showPeriodPicker ? 'show' : ''}}" wx:if="{{showPeriodPicker}}">
|
||||
<view class="picker-title">选择周期</view>
|
||||
<scroll-view class="picker-list" scroll-y>
|
||||
<view
|
||||
class="picker-item {{item === periodKey ? 'active' : ''}}"
|
||||
wx:for="{{periodList}}"
|
||||
wx:key="*this"
|
||||
data-period="{{item}}"
|
||||
bindtap="onPeriodSelect"
|
||||
>
|
||||
<text>{{item}}</text>
|
||||
<text class="picker-check" wx:if="{{item === periodKey}}">✓</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -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; }
|
||||
.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; }
|
||||
Reference in New Issue
Block a user