init
项目初始化
This commit is contained in:
145
pages/plan/plan.js
Normal file
145
pages/plan/plan.js
Normal file
@@ -0,0 +1,145 @@
|
||||
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
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
periodKey: '',
|
||||
canSubmit: false,
|
||||
message: '',
|
||||
myChecked: false,
|
||||
myStatus: '',
|
||||
myRecord: null,
|
||||
myThumbs: [],
|
||||
unassignedCount: 0,
|
||||
groups: [],
|
||||
selectedGroupName: '',
|
||||
members: []
|
||||
},
|
||||
|
||||
onShow() {
|
||||
auth.ensureLogin().then((token) => {
|
||||
if (!token) {
|
||||
wx.switchTab({ url: '/pages/profile/profile' });
|
||||
return;
|
||||
}
|
||||
wx.removeStorageSync('plan_overview_dirty');
|
||||
this.setData({ view: 'overview' });
|
||||
this.loadOverview();
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
onTypeChange(e) {
|
||||
const typeIndex = Number(e.currentTarget.dataset.index);
|
||||
if (typeIndex === this.data.typeIndex) {
|
||||
return;
|
||||
}
|
||||
this.setData({ typeIndex, view: 'overview', selectedGroupName: '', members: [] });
|
||||
this.loadOverview();
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.setData({ refreshing: true });
|
||||
this.loadOverview();
|
||||
},
|
||||
|
||||
loadOverview() {
|
||||
const type = TYPES[this.data.typeIndex];
|
||||
this.setData({ loading: true });
|
||||
api.request({ url: '/api/v1/plan/groups', data: { type } })
|
||||
.then((res) => {
|
||||
const groups = (res.groups || []).map((g) => ({
|
||||
...g,
|
||||
percent: g.total ? Math.round(((g.checked_count || 0) / g.total) * 100) : 0
|
||||
}));
|
||||
this.setData({
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
periodKey: res.period_key || '',
|
||||
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)),
|
||||
unassignedCount: res.unassigned_count || 0,
|
||||
groups
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({ loading: false, refreshing: false });
|
||||
wx.showToast({ title: '加载小组失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
openGroup(e) {
|
||||
const groupName = e.currentTarget.dataset.group;
|
||||
if (!groupName) {
|
||||
return;
|
||||
}
|
||||
this.setData({ selectedGroupName: groupName, view: 'detail', members: [] });
|
||||
this.loadDetail();
|
||||
},
|
||||
|
||||
loadDetail() {
|
||||
const type = TYPES[this.data.typeIndex];
|
||||
this.setData({ loading: true });
|
||||
api.request({
|
||||
url: '/api/v1/plan/group',
|
||||
data: { type, group_name: this.data.selectedGroupName, period_key: this.data.periodKey }
|
||||
}).then((res) => {
|
||||
this.setData({
|
||||
loading: false,
|
||||
periodKey: res.period_key || this.data.periodKey,
|
||||
canSubmit: !!res.can_submit,
|
||||
message: res.message || '',
|
||||
members: res.members || []
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false });
|
||||
wx.showToast({ title: '加载成员失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
const src = urls[index];
|
||||
if (!src) return;
|
||||
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' });
|
||||
return;
|
||||
}
|
||||
const params = [
|
||||
'plan_type=' + type,
|
||||
'period_key=' + encodeURIComponent(this.data.periodKey),
|
||||
'group_name='
|
||||
].join('&');
|
||||
wx.navigateTo({ url: '/pages/plan-submit/plan-submit?' + params });
|
||||
}
|
||||
});
|
||||
1
pages/plan/plan.json
Normal file
1
pages/plan/plan.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "usingComponents": {} }
|
||||
116
pages/plan/plan.wxml
Normal file
116
pages/plan/plan.wxml
Normal file
@@ -0,0 +1,116 @@
|
||||
<view class="page">
|
||||
<block wx:if="{{view === 'overview'}}">
|
||||
<view class="view-root">
|
||||
<view class="type-tabs">
|
||||
<view
|
||||
class="type-tab {{typeIndex === index ? 'active' : ''}}"
|
||||
wx:for="{{types}}"
|
||||
wx:key="index"
|
||||
data-index="{{index}}"
|
||||
hover-class="type-tab-hover"
|
||||
bindtap="onTypeChange"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="view-scroll"
|
||||
scroll-y
|
||||
refresher-enabled="{{true}}"
|
||||
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>
|
||||
<view class="head-pill">{{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">
|
||||
<view class="my-status-label">{{myChecked ? '已完成' : '未完成'}}</view>
|
||||
<view class="my-status-text">我的{{types[typeIndex]}}</view>
|
||||
</view>
|
||||
<button
|
||||
class="btn-mystatus {{myChecked ? 'outline' : ''}}"
|
||||
bindtap="goSubmit"
|
||||
hover-class="btn-hover"
|
||||
disabled="{{typeIndex === 0 && !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="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>
|
||||
</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>
|
||||
</scroll-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>
|
||||
</view>
|
||||
<scroll-view class="view-scroll" scroll-y>
|
||||
<view class="state-block" wx:if="{{loading && members.length === 0}}">
|
||||
<view class="state-icon">◌</view>
|
||||
<view>加载中...</view>
|
||||
</view>
|
||||
<view class="member-list">
|
||||
<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:if="{{!loading && members.length === 0}}">暂无成员</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
80
pages/plan/plan.wxss
Normal file
80
pages/plan/plan.wxss
Normal file
@@ -0,0 +1,80 @@
|
||||
.page { height: 100vh; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden; background: #F6F2EC; }
|
||||
.view-root { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
|
||||
.type-tabs { flex-shrink: 0; display: flex; align-items: center; gap: 8rpx; margin: 24rpx 32rpx 0; padding: 8rpx; background: #EFE7DC; border-radius: 999rpx; }
|
||||
.type-tab { flex: 1; text-align: center; padding: 16rpx 0; color: #8B7A6A; background: transparent; border-radius: 999rpx; font-size: 28rpx; font-weight: 600; transition: all .18s ease; }
|
||||
.type-tab.active { background: #fff; color: #C67B30; font-weight: 700; box-shadow: 0 4rpx 12rpx rgba(198,123,48,0.18); }
|
||||
.type-tab-hover { opacity: .85; }
|
||||
|
||||
.view-scroll { flex: 1; min-height: 0; padding: 8rpx 32rpx 32rpx; box-sizing: border-box; }
|
||||
.overview-head { display: flex; align-items: flex-end; justify-content: space-between; padding: 24rpx 4rpx 16rpx; }
|
||||
.head-title-wrap { min-width: 0; }
|
||||
.head-title { font-size: 38rpx; font-weight: 800; color: #2C1810; letter-spacing: 1rpx; }
|
||||
.head-sub { margin-top: 6rpx; font-size: 24rpx; color: #A99A89; }
|
||||
.head-pill { flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 999rpx; font-size: 22rpx; background: #fff; color: #C67B30; box-shadow: 0 2rpx 8rpx rgba(44,24,16,0.06); }
|
||||
|
||||
.detail-head { display: flex; align-items: center; gap: 16rpx; padding: 28rpx 4rpx 8rpx; }
|
||||
.detail-title-wrap { 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; }
|
||||
|
||||
.my-status { display: flex; align-items: center; gap: 18rpx; padding: 24rpx; margin-bottom: 20rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); }
|
||||
.my-status.checked { border-left: 8rpx solid #2E7D32; }
|
||||
.my-status.unchecked { border-left: 8rpx solid #C0392B; }
|
||||
.my-state-icon { width: 64rpx; height: 64rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 800; flex-shrink: 0; }
|
||||
.my-state-icon.done { background: linear-gradient(135deg, #2E7D32, #43A047); color: #fff; box-shadow: 0 4rpx 12rpx rgba(46,125,50,0.25); }
|
||||
.my-state-icon.todo { background: linear-gradient(135deg, #C0392B, #E5736C); color: #fff; box-shadow: 0 4rpx 12rpx rgba(192,57,43,0.22); }
|
||||
.my-status-left { flex: 1; min-width: 0; }
|
||||
.my-status-label { font-size: 24rpx; color: #8B7A6A; }
|
||||
.my-status-text { font-size: 32rpx; font-weight: 800; color: #2C1810; margin-top: 6rpx; }
|
||||
.btn-mystatus { width: 208rpx; margin: 0; flex-shrink: 0; background: linear-gradient(135deg, #C67B30, #E4A05E); color: #fff; border-radius: 999rpx; font-size: 28rpx; font-weight: 700; box-shadow: 0 6rpx 14rpx rgba(198,123,48,0.3); }
|
||||
.btn-mystatus.outline { background: #fff; color: #C67B30; border: 2rpx solid #C67B30; box-shadow: none; }
|
||||
.btn-mystatus[disabled] { background: #D8C7B4; color: #fff; border: none; box-shadow: none; }
|
||||
.btn-hover { opacity: .9; transform: scale(.98); }
|
||||
|
||||
.window-msg { margin: 0 8rpx 20rpx; padding: 14rpx 22rpx; background: #FFF3E6; border-radius: 14rpx; text-align: center; color: #C0392B; font-size: 24rpx; border: 1rpx solid #F7D9B9; }
|
||||
|
||||
.my-plan { margin-bottom: 20rpx; padding: 24rpx; border-radius: 20rpx; background: #fff; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); }
|
||||
.my-plan-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.my-plan-title { font-size: 26rpx; font-weight: 700; color: #6B5D50; }
|
||||
.my-plan-status { padding: 6rpx 16rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 700; background: #E8F4EB; color: #2E7D32; }
|
||||
.my-plan-status.pending { background: #FFF3E6; color: #C67B30; }
|
||||
.my-plan-meta { margin-top: 8rpx; font-size: 22rpx; color: #A99A89; }
|
||||
.my-plan-content { margin-top: 14rpx; padding: 16rpx 20rpx; background: #FBF7F1; border-radius: 14rpx; font-size: 28rpx; line-height: 1.7; color: #2C1810; white-space: pre-wrap; word-break: break-all; }
|
||||
.my-plan-thumbs { display: flex; flex-wrap: wrap; gap: 14rpx; margin-top: 16rpx; }
|
||||
.my-plan-thumb { width: 148rpx; height: 148rpx; border-radius: 14rpx; background: #F2EDE7; box-shadow: 0 2rpx 6rpx rgba(44,24,16,0.08); }
|
||||
|
||||
.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; }
|
||||
|
||||
.member-list { background: #fff; border-radius: 20rpx; padding: 8rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(44,24,16,0.05); }
|
||||
.member-left { display: flex; align-items: center; min-width: 0; }
|
||||
.member { display: flex; align-items: center; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #F2EDE7; }
|
||||
.member:last-child { border-bottom: none; }
|
||||
.member-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 26rpx; font-weight: 800; flex-shrink: 0; }
|
||||
.member-avatar.done { background: linear-gradient(135deg, #E8F4EB, #CFEAD5); color: #2E7D32; }
|
||||
.member-avatar.unchecked { background: linear-gradient(135deg, #FDECEA, #F9D9D5); color: #C0392B; }
|
||||
.member-info { margin-left: 18rpx; min-width: 0; }
|
||||
.member-name { font-size: 28rpx; font-weight: 700; color: #2C1810; }
|
||||
.member-meta { margin-top: 6rpx; font-size: 22rpx; color: #A99A89; }
|
||||
.badge { flex-shrink: 0; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 700; }
|
||||
.badge.done { background: #E8F4EB; color: #2E7D32; }
|
||||
.badge.unchecked { background: #FDECEA; color: #C0392B; }
|
||||
|
||||
.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; }
|
||||
Reference in New Issue
Block a user