|
@@ -44,31 +44,121 @@
|
|
|
|
|
|
|
|
<!-- 添加任务弹窗 -->
|
|
<!-- 添加任务弹窗 -->
|
|
|
<view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
|
|
<view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
|
|
|
- <view class="modal" @click.stop>
|
|
|
|
|
- <view class="modal-title">添加任务</view>
|
|
|
|
|
|
|
+ <view class="modal modal-large" @click.stop>
|
|
|
|
|
+ <view class="modal-title">创建任务</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 任务模板选择 -->
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
- <input v-model="newTask.title" placeholder="任务名称" />
|
|
|
|
|
|
|
+ <view class="form-label">从模板选择(可选)</view>
|
|
|
|
|
+ <picker mode="selector" :range="taskTemplates" range-key="title" @change="onTemplateChange">
|
|
|
|
|
+ <view class="picker">{{ selectedTemplate ? selectedTemplate.title : '选择任务模板' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 必填:标题 -->
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
- <input v-model="newTask.points" type="number" placeholder="积分(1-10)" />
|
|
|
|
|
|
|
+ <view class="form-label">任务标题 <text class="required">*</text></view>
|
|
|
|
|
+ <input v-model="newTask.title" placeholder="请输入任务标题" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 内容 -->
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
- <picker mode="time" @change="onTimeChange">
|
|
|
|
|
- <view class="picker">
|
|
|
|
|
- {{ newTask.deadlineTime || '选择截止时间' }}
|
|
|
|
|
- </view>
|
|
|
|
|
- </picker>
|
|
|
|
|
|
|
+ <view class="form-label">任务内容</view>
|
|
|
|
|
+ <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 类型 -->
|
|
|
<view class="form-item">
|
|
<view class="form-item">
|
|
|
|
|
+ <view class="form-label">类型 <text class="required">*</text></view>
|
|
|
<picker mode="selector" :range="categories" @change="onCategoryChange">
|
|
<picker mode="selector" :range="categories" @change="onCategoryChange">
|
|
|
- <view class="picker">
|
|
|
|
|
- {{ newTask.category || '选择分类' }}
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <view class="picker">{{ newTask.category || '请选择类型' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 完成方式 -->
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">完成方式 <text class="required">*</text></view>
|
|
|
|
|
+ <checkbox-group @change="onCompleteTypeChange">
|
|
|
|
|
+ <label v-for="type in completeTypes" :key="type.value">
|
|
|
|
|
+ <checkbox :value="type.value" :checked="newTask.completeTypes.includes(type.value)" />
|
|
|
|
|
+ <text>{{ type.label }}</text>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </checkbox-group>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 积分和时长 -->
|
|
|
|
|
+ <view class="form-row">
|
|
|
|
|
+ <view class="form-item half">
|
|
|
|
|
+ <view class="form-label">积分 <text class="required">*</text></view>
|
|
|
|
|
+ <input v-model="newTask.points" type="number" placeholder="1-10" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item half">
|
|
|
|
|
+ <view class="form-label">时长(分钟)</view>
|
|
|
|
|
+ <input v-model="newTask.duration" type="number" placeholder="时长" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 完成孩子(多选) -->
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">完成孩子 <text class="required">*</text></view>
|
|
|
|
|
+ <checkbox-group @change="onChildSelectChange">
|
|
|
|
|
+ <label v-for="child in children" :key="child.id">
|
|
|
|
|
+ <checkbox :value="child.id" :checked="newTask.childIds.includes(child.id)" />
|
|
|
|
|
+ <text>{{ child.nickname }}</text>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </checkbox-group>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 任务性质 -->
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">任务性质 <text class="required">*</text></view>
|
|
|
|
|
+ <radio-group @change="onTaskTypeChange">
|
|
|
|
|
+ <label><radio value="onetime" :checked="newTask.taskType === 'onetime'" />一次性</label>
|
|
|
|
|
+ <label><radio value="recurring" :checked="newTask.taskType === 'recurring'" />循环任务</label>
|
|
|
|
|
+ </radio-group>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 循环任务:频率和次数 -->
|
|
|
|
|
+ <view class="form-item" v-if="newTask.taskType === 'recurring'">
|
|
|
|
|
+ <view class="form-label">频率</view>
|
|
|
|
|
+ <picker mode="selector" :range="frequencies" @change="onFrequencyChange">
|
|
|
|
|
+ <view class="picker">{{ newTask.frequency || '请选择频率' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item" v-if="newTask.taskType === 'recurring'">
|
|
|
|
|
+ <view class="form-label">周期内最多完成次数</view>
|
|
|
|
|
+ <input v-model="newTask.maxFrequency" type="number" placeholder="超过次数不记积分" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 审核设置 -->
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">需要审核</view>
|
|
|
|
|
+ <switch :checked="newTask.needReview === 1" @change="onNeedReviewChange" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item" v-if="newTask.needReview === 1">
|
|
|
|
|
+ <view class="form-label">审核人</view>
|
|
|
|
|
+ <picker mode="selector" :range="reviewTypes" range-key="label" @change="onReviewTypeChange">
|
|
|
|
|
+ <view class="picker">{{ getReviewTypeLabel(newTask.reviewType) }}</view>
|
|
|
</picker>
|
|
</picker>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 非必填:时间 -->
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">最早开始时间</view>
|
|
|
|
|
+ <picker mode="datetime" :value="newTask.earliestStart" @change="onEarliestStartChange">
|
|
|
|
|
+ <view class="picker">{{ newTask.earliestStart || '选择时间(不填表示不限时)' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="form-item">
|
|
|
|
|
+ <view class="form-label">最晚结束时间</view>
|
|
|
|
|
+ <picker mode="datetime" :value="newTask.latestEnd" @change="onLatestEndChange">
|
|
|
|
|
+ <view class="picker">{{ newTask.latestEnd || '选择时间(不填表示不限时)' }}</view>
|
|
|
|
|
+ </picker>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
<view class="modal-btns">
|
|
<view class="modal-btns">
|
|
|
<button @click="showAddModal = false">取消</button>
|
|
<button @click="showAddModal = false">取消</button>
|
|
|
- <button class="btn-primary" @click="createTask">确定</button>
|
|
|
|
|
|
|
+ <button class="btn-primary" @click="createTask">创建任务</button>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
@@ -87,21 +177,61 @@ export default {
|
|
|
tasks: [],
|
|
tasks: [],
|
|
|
isParent: false,
|
|
isParent: false,
|
|
|
currentChildId: '',
|
|
currentChildId: '',
|
|
|
|
|
+ children: [], // 孩子列表
|
|
|
|
|
+ taskTemplates: [], // 任务模板
|
|
|
|
|
+ selectedTemplate: null,
|
|
|
showAddModal: false,
|
|
showAddModal: false,
|
|
|
newTask: {
|
|
newTask: {
|
|
|
title: '',
|
|
title: '',
|
|
|
|
|
+ description: '',
|
|
|
|
|
+ category: '',
|
|
|
points: 2,
|
|
points: 2,
|
|
|
- deadlineTime: '',
|
|
|
|
|
- category: ''
|
|
|
|
|
|
|
+ duration: '',
|
|
|
|
|
+ childIds: [],
|
|
|
|
|
+ taskType: 'onetime',
|
|
|
|
|
+ frequency: '',
|
|
|
|
|
+ maxFrequency: '',
|
|
|
|
|
+ completeTypes: ['checkin'],
|
|
|
|
|
+ needReview: 0,
|
|
|
|
|
+ reviewType: 'parent',
|
|
|
|
|
+ earliestStart: '',
|
|
|
|
|
+ latestEnd: ''
|
|
|
},
|
|
},
|
|
|
- categories: ['学习类', '生活类', '运动类', '其他']
|
|
|
|
|
|
|
+ categories: ['学习类', '生活类', '运动类', '其他'],
|
|
|
|
|
+ completeTypes: [
|
|
|
|
|
+ { value: 'checkin', label: '打卡' },
|
|
|
|
|
+ { value: 'text', label: '文本' },
|
|
|
|
|
+ { value: 'audio', label: '语音' },
|
|
|
|
|
+ { value: 'image', label: '图片' },
|
|
|
|
|
+ { value: 'video', label: '视频' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ frequencies: ['每天', '每周', '每月'],
|
|
|
|
|
+ reviewTypes: [
|
|
|
|
|
+ { value: 'creator', label: '创建人' },
|
|
|
|
|
+ { value: 'parent', label: '家长' },
|
|
|
|
|
+ { value: 'teacher', label: '指导师' },
|
|
|
|
|
+ { value: 'ai', label: 'AI自动' }
|
|
|
|
|
+ ]
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
onShow() {
|
|
onShow() {
|
|
|
this.loadTasks()
|
|
this.loadTasks()
|
|
|
|
|
+ this.loadChildren()
|
|
|
this.isParent = this.$store.state.role === 'parent'
|
|
this.isParent = this.$store.state.role === 'parent'
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
|
|
+ async loadChildren() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getChildren()
|
|
|
|
|
+ this.children = res.data || []
|
|
|
|
|
+ // 默认选中第一个孩子
|
|
|
|
|
+ if (this.children.length > 0) {
|
|
|
|
|
+ this.newTask.childIds = [this.children[0].id]
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载孩子失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
async loadTasks() {
|
|
async loadTasks() {
|
|
|
try {
|
|
try {
|
|
|
const childrenRes = await getChildren()
|
|
const childrenRes = await getChildren()
|
|
@@ -127,39 +257,126 @@ export default {
|
|
|
console.error('完成任务失败', e)
|
|
console.error('完成任务失败', e)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- onTimeChange(e) {
|
|
|
|
|
- const now = new Date()
|
|
|
|
|
- const deadline = new Date(now.getFullYear(), now.getMonth(), now.getDate(), e.detail.value)
|
|
|
|
|
- this.newTask.deadlineTime = e.detail.value
|
|
|
|
|
|
|
+ // 模板选择
|
|
|
|
|
+ onTemplateChange(e) {
|
|
|
|
|
+ if (e.detail.value >= 0) {
|
|
|
|
|
+ this.selectedTemplate = this.taskTemplates[e.detail.value]
|
|
|
|
|
+ this.applyTemplate(this.selectedTemplate)
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
|
|
+ applyTemplate(template) {
|
|
|
|
|
+ this.newTask.title = template.title || ''
|
|
|
|
|
+ this.newTask.description = template.description || ''
|
|
|
|
|
+ this.newTask.category = template.category || ''
|
|
|
|
|
+ this.newTask.points = template.points || 2
|
|
|
|
|
+ this.newTask.duration = template.duration || ''
|
|
|
|
|
+ this.newTask.completeTypes = template.completeTypes ? template.completeTypes.split(',') : ['checkin']
|
|
|
|
|
+ this.newTask.needReview = template.needReview || 0
|
|
|
|
|
+ this.newTask.reviewType = template.reviewType || 'parent'
|
|
|
|
|
+ },
|
|
|
|
|
+ // 类型选择
|
|
|
onCategoryChange(e) {
|
|
onCategoryChange(e) {
|
|
|
this.newTask.category = this.categories[e.detail.value]
|
|
this.newTask.category = this.categories[e.detail.value]
|
|
|
},
|
|
},
|
|
|
|
|
+ // 完成方式选择
|
|
|
|
|
+ onCompleteTypeChange(e) {
|
|
|
|
|
+ this.newTask.completeTypes = e.detail.value
|
|
|
|
|
+ },
|
|
|
|
|
+ // 孩子多选
|
|
|
|
|
+ onChildSelectChange(e) {
|
|
|
|
|
+ this.newTask.childIds = e.detail.value.map(id => parseInt(id))
|
|
|
|
|
+ },
|
|
|
|
|
+ // 任务性质
|
|
|
|
|
+ onTaskTypeChange(e) {
|
|
|
|
|
+ this.newTask.taskType = e.detail.value
|
|
|
|
|
+ },
|
|
|
|
|
+ // 频率选择
|
|
|
|
|
+ onFrequencyChange(e) {
|
|
|
|
|
+ const freq = this.frequencies[e.detail.value]
|
|
|
|
|
+ this.newTask.frequency = freq === '每天' ? 'daily' : (freq === '每周' ? 'weekly' : 'monthly')
|
|
|
|
|
+ },
|
|
|
|
|
+ // 是否需要审核
|
|
|
|
|
+ onNeedReviewChange(e) {
|
|
|
|
|
+ this.newTask.needReview = e.detail.value ? 1 : 0
|
|
|
|
|
+ },
|
|
|
|
|
+ // 审核人选择
|
|
|
|
|
+ onReviewTypeChange(e) {
|
|
|
|
|
+ this.newTask.reviewType = this.reviewTypes[e.detail.value].value
|
|
|
|
|
+ },
|
|
|
|
|
+ getReviewTypeLabel(value) {
|
|
|
|
|
+ const found = this.reviewTypes.find(r => r.value === value)
|
|
|
|
|
+ return found ? found.label : '请选择审核人'
|
|
|
|
|
+ },
|
|
|
|
|
+ // 时间选择
|
|
|
|
|
+ onEarliestStartChange(e) {
|
|
|
|
|
+ this.newTask.earliestStart = e.detail.value
|
|
|
|
|
+ },
|
|
|
|
|
+ onLatestEndChange(e) {
|
|
|
|
|
+ this.newTask.latestEnd = e.detail.value
|
|
|
|
|
+ },
|
|
|
async createTask() {
|
|
async createTask() {
|
|
|
|
|
+ // 验证必填
|
|
|
if (!this.newTask.title) {
|
|
if (!this.newTask.title) {
|
|
|
- uni.showToast({ title: '请输入任务名称', icon: 'none' })
|
|
|
|
|
|
|
+ uni.showToast({ title: '请输入任务标题', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.newTask.category) {
|
|
|
|
|
+ uni.showToast({ title: '请选择类型', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.newTask.completeTypes || this.newTask.completeTypes.length === 0) {
|
|
|
|
|
+ uni.showToast({ title: '请选择完成方式', icon: 'none' })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.newTask.childIds || this.newTask.childIds.length === 0) {
|
|
|
|
|
+ uni.showToast({ title: '请选择完成孩子', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- const now = new Date()
|
|
|
|
|
- const deadline = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
|
|
|
|
|
- const [hours, minutes] = (this.newTask.deadlineTime || '20:00').split(':')
|
|
|
|
|
- deadline.setHours(parseInt(hours), parseInt(minutes), 0, 0)
|
|
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
await createTask({
|
|
await createTask({
|
|
|
title: this.newTask.title,
|
|
title: this.newTask.title,
|
|
|
- points: parseInt(this.newTask.points),
|
|
|
|
|
- deadline: deadline.toISOString(),
|
|
|
|
|
|
|
+ description: this.newTask.description,
|
|
|
category: this.newTask.category,
|
|
category: this.newTask.category,
|
|
|
- childId: this.currentChildId
|
|
|
|
|
|
|
+ points: parseInt(this.newTask.points) || 2,
|
|
|
|
|
+ duration: parseInt(this.newTask.duration) || null,
|
|
|
|
|
+ childIds: this.newTask.childIds,
|
|
|
|
|
+ taskType: this.newTask.taskType,
|
|
|
|
|
+ frequency: this.newTask.frequency,
|
|
|
|
|
+ maxFrequency: parseInt(this.newTask.maxFrequency) || null,
|
|
|
|
|
+ completeTypes: this.newTask.completeTypes,
|
|
|
|
|
+ needReview: this.newTask.needReview,
|
|
|
|
|
+ reviewType: this.newTask.reviewType,
|
|
|
|
|
+ earliestStart: this.newTask.earliestStart || null,
|
|
|
|
|
+ latestEnd: this.newTask.latestEnd || null
|
|
|
})
|
|
})
|
|
|
uni.showToast({ title: '创建成功', icon: 'success' })
|
|
uni.showToast({ title: '创建成功', icon: 'success' })
|
|
|
this.showAddModal = false
|
|
this.showAddModal = false
|
|
|
|
|
+ this.resetNewTask()
|
|
|
this.loadTasks()
|
|
this.loadTasks()
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('创建任务失败', e)
|
|
console.error('创建任务失败', e)
|
|
|
|
|
+ uni.showToast({ title: '创建失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ resetNewTask() {
|
|
|
|
|
+ this.newTask = {
|
|
|
|
|
+ title: '',
|
|
|
|
|
+ description: '',
|
|
|
|
|
+ category: '',
|
|
|
|
|
+ points: 2,
|
|
|
|
|
+ duration: '',
|
|
|
|
|
+ childIds: this.children.length > 0 ? [this.children[0].id] : [],
|
|
|
|
|
+ taskType: 'onetime',
|
|
|
|
|
+ frequency: '',
|
|
|
|
|
+ maxFrequency: '',
|
|
|
|
|
+ completeTypes: ['checkin'],
|
|
|
|
|
+ needReview: 0,
|
|
|
|
|
+ reviewType: 'parent',
|
|
|
|
|
+ earliestStart: '',
|
|
|
|
|
+ latestEnd: ''
|
|
|
}
|
|
}
|
|
|
|
|
+ this.selectedTemplate = null
|
|
|
},
|
|
},
|
|
|
formatTime(dateStr) {
|
|
formatTime(dateStr) {
|
|
|
if (!dateStr) return ''
|
|
if (!dateStr) return ''
|
|
@@ -254,6 +471,12 @@ export default {
|
|
|
border-radius: 20rpx;
|
|
border-radius: 20rpx;
|
|
|
padding: 40rpx;
|
|
padding: 40rpx;
|
|
|
width: 80%;
|
|
width: 80%;
|
|
|
|
|
+ max-height: 80vh;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.modal-large {
|
|
|
|
|
+ width: 90%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.modal-title {
|
|
.modal-title {
|
|
@@ -267,11 +490,49 @@ export default {
|
|
|
margin-bottom: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.form-item input, .picker {
|
|
|
|
|
|
|
+.form-label {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-label .required {
|
|
|
|
|
+ color: #FF6B6B;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item.half {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item input, .picker, .textarea {
|
|
|
border: 1rpx solid #ddd;
|
|
border: 1rpx solid #ddd;
|
|
|
border-radius: 10rpx;
|
|
border-radius: 10rpx;
|
|
|
padding: 20rpx;
|
|
padding: 20rpx;
|
|
|
font-size: 28rpx;
|
|
font-size: 28rpx;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.textarea {
|
|
|
|
|
+ min-height: 100rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item checkbox-group {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item label {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.modal-btns {
|
|
.modal-btns {
|