hookehuyr

feat(打卡): 支持计数打卡类型并优化提交逻辑

添加计数打卡类型的支持,包括动态字段显示和额外数据提交
重构提交逻辑,将校验和数据处理分离到单独方法
优化UI文本显示,使其更通用化
...@@ -20,9 +20,12 @@ export function useCheckin() { ...@@ -20,9 +20,12 @@ export function useCheckin() {
20 const loading = ref(false) 20 const loading = ref(false)
21 const message = ref('') 21 const message = ref('')
22 const fileList = ref([]) 22 const fileList = ref([])
23 - const activeType = ref('text') // 当前选中的打卡类型 23 + const activeType = ref('') // 当前选中的打卡类型
24 const maxCount = ref(5) 24 const maxCount = ref(5)
25 25
26 + // 打卡类型
27 + const checkinType = computed(() => route.query.type)
28 +
26 // 用于记忆不同类型的文件列表 29 // 用于记忆不同类型的文件列表
27 const fileListMemory = ref({ 30 const fileListMemory = ref({
28 text: [], 31 text: [],
...@@ -35,6 +38,11 @@ export function useCheckin() { ...@@ -35,6 +38,11 @@ export function useCheckin() {
35 * 是否可以提交 38 * 是否可以提交
36 */ 39 */
37 const canSubmit = computed(() => { 40 const canSubmit = computed(() => {
41 + // 如果是计数打卡,交由组件内部校验
42 + if (checkinType.value === 'count') {
43 + return true
44 + }
45 +
38 if (activeType.value === 'text') { 46 if (activeType.value === 'text') {
39 // 文字打卡:必须填写内容且长度不少于10个字符 47 // 文字打卡:必须填写内容且长度不少于10个字符
40 return message.value.trim() !== '' && message.value.trim().length >= 10 48 return message.value.trim() !== '' && message.value.trim().length >= 10
...@@ -249,25 +257,24 @@ export function useCheckin() { ...@@ -249,25 +257,24 @@ export function useCheckin() {
249 257
250 /** 258 /**
251 * 提交打卡 259 * 提交打卡
260 + * @param {Object} extraData - 额外提交数据
252 */ 261 */
253 - const onSubmit = async () => { 262 + const onSubmit = async (extraData = {}) => {
254 if (uploading.value) return 263 if (uploading.value) return
255 264
256 // 表单验证 265 // 表单验证
257 - if (activeType.value === 'text') { 266 + if (checkinType.value !== 'count') {
258 - if (message.value.trim().length < 10) { 267 + if (activeType.value === 'text') {
259 - showToast('打卡内容至少需要10个字符') 268 + if (message.value.trim().length < 10) {
260 - return 269 + showToast('打卡内容至少需要10个字符')
261 - } 270 + return
262 - } else { 271 + }
263 - if (fileList.value.length === 0) { 272 + } else {
264 - showToast('请先上传文件') 273 + if (fileList.value.length === 0) {
265 - return 274 + showToast('请先上传文件')
275 + return
276 + }
266 } 277 }
267 - // if (message.value.trim() === '') {
268 - // showToast('请输入打卡留言')
269 - // return
270 - // }
271 } 278 }
272 279
273 uploading.value = true 280 uploading.value = true
...@@ -284,6 +291,7 @@ export function useCheckin() { ...@@ -284,6 +291,7 @@ export function useCheckin() {
284 file_type: activeType.value, 291 file_type: activeType.value,
285 meta_id: [], 292 meta_id: [],
286 makeup_time: route.query.is_patch ? route.query.date : '', 293 makeup_time: route.query.is_patch ? route.query.date : '',
294 + ...extraData
287 } 295 }
288 296
289 // 如果有文件,添加文件ID 297 // 如果有文件,添加文件ID
......
...@@ -16,40 +16,29 @@ ...@@ -16,40 +16,29 @@
16 16
17 <!-- 打卡内容区域 --> 17 <!-- 打卡内容区域 -->
18 <div class="section-wrapper"> 18 <div class="section-wrapper">
19 - <div class="section-title">打卡留言</div> 19 + <div class="section-title">提交作业</div>
20 <div class="section-content"> 20 <div class="section-content">
21 <!-- 作业弹框选择区域 --> 21 <!-- 作业弹框选择区域 -->
22 <div class="mb-4"> 22 <div class="mb-4">
23 - <van-field 23 + <van-field v-model="selectedTaskText" is-link readonly label="选择作业" placeholder="请选择本次打卡的作业"
24 - v-model="selectedTaskText" 24 + @click="showTaskPicker = true" class="rounded-lg border border-gray-100" />
25 - is-link
26 - readonly
27 - label="选择作业"
28 - placeholder="请选择本次打卡的作业"
29 - @click="showTaskPicker = true"
30 - class="rounded-lg border border-gray-100"
31 - />
32 <van-popup v-model:show="showTaskPicker" round position="bottom"> 25 <van-popup v-model:show="showTaskPicker" round position="bottom">
33 - <van-picker 26 + <van-picker :columns="taskOptions" @cancel="showTaskPicker = false"
34 - :columns="taskOptions" 27 + @confirm="onConfirmTask" />
35 - @cancel="showTaskPicker = false"
36 - @confirm="onConfirmTask"
37 - />
38 </van-popup> 28 </van-popup>
39 </div> 29 </div>
40 <!-- 计数对象 --> 30 <!-- 计数对象 -->
41 <div v-if="checkinType === 'count'" class="mb-4"> 31 <div v-if="checkinType === 'count'" class="mb-4">
42 <div class="flex justify-between items-center mb-2 mx-2"> 32 <div class="flex justify-between items-center mb-2 mx-2">
43 - <div class="text-sm font-bold text-gray-700">计数对象</div> 33 + <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}对象</div>
44 - <van-button size="small" type="primary" plain icon="plus" @click="showAddTargetDialog = true" class="!h-7">添加</van-button> 34 + <van-button size="small" type="primary" plain icon="plus"
35 + @click="showAddTargetDialog = true" class="!h-7">添加</van-button>
45 </div> 36 </div>
46 37
47 <div class="bg-gray-50 rounded-lg p-2"> 38 <div class="bg-gray-50 rounded-lg p-2">
48 <div class="flex flex-wrap gap-2"> 39 <div class="flex flex-wrap gap-2">
49 <template v-if="targetList.length > 0"> 40 <template v-if="targetList.length > 0">
50 - <div 41 + <div v-for="(item, index) in targetList" :key="index"
51 - v-for="(item, index) in targetList"
52 - :key="index"
53 class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none" 42 class="px-4 py-1.5 rounded-full text-sm transition-colors duration-200 border cursor-pointer select-none"
54 :style="selectedTargets.some(t => t.name === item.name) ? { 43 :style="selectedTargets.some(t => t.name === item.name) ? {
55 backgroundColor: '#4caf50', 44 backgroundColor: '#4caf50',
...@@ -59,45 +48,33 @@ ...@@ -59,45 +48,33 @@
59 backgroundColor: '#ffffff', 48 backgroundColor: '#ffffff',
60 color: '#4b5563', 49 color: '#4b5563',
61 borderColor: '#e5e7eb' 50 borderColor: '#e5e7eb'
62 - }" 51 + }" @click="toggleTarget(item)">
63 - @click="toggleTarget(item)"
64 - >
65 {{ item.name }} 52 {{ item.name }}
66 </div> 53 </div>
67 </template> 54 </template>
68 <div v-else class="w-full text-center py-4 text-gray-400 text-sm"> 55 <div v-else class="w-full text-center py-4 text-gray-400 text-sm">
69 - 暂无计数对象,请点击上方添加按钮 56 + 暂无{{ dynamicFieldText }}对象,请点击上方添加按钮
70 </div> 57 </div>
71 </div> 58 </div>
72 </div> 59 </div>
73 </div> 60 </div>
74 61
75 <!-- 计数次数 --> 62 <!-- 计数次数 -->
76 - <div v-if="checkinType === 'count'" class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg"> 63 + <div v-if="checkinType === 'count'"
77 - <div class="text-sm font-bold text-gray-700">计数次数</div> 64 + class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg">
65 + <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}次数</div>
78 <van-stepper v-model="countValue" min="1" integer input-width="80px" button-size="28px" /> 66 <van-stepper v-model="countValue" min="1" integer input-width="80px" button-size="28px" />
79 </div> 67 </div>
80 68
81 <!-- 新增计数对象弹框 --> 69 <!-- 新增计数对象弹框 -->
82 - <van-dialog 70 + <van-dialog v-model:show="showAddTargetDialog" :title="`添加${dynamicFieldText}对象`" show-cancel-button
83 - v-model:show="showAddTargetDialog" 71 + confirmButtonColor="#4caf50" :before-close="onBeforeClose">
84 - title="添加计数对象"
85 - show-cancel-button
86 - confirmButtonColor="#4caf50"
87 - :before-close="onBeforeClose"
88 - >
89 <div class="p-4"> 72 <div class="p-4">
90 <div v-for="field in dynamicFormFields" :key="field.id"> 73 <div v-for="field in dynamicFormFields" :key="field.id">
91 - <van-field 74 + <van-field v-model="field.value" :label="field.label" :placeholder="'请输入' + field.label"
92 - v-model="field.value"
93 - :label="field.label"
94 - :placeholder="'请输入' + field.label"
95 :type="field.type === 'textarea' ? 'textarea' : 'text'" 75 :type="field.type === 'textarea' ? 'textarea' : 'text'"
96 - :rows="field.type === 'textarea' ? 2 : 1" 76 + :rows="field.type === 'textarea' ? 2 : 1" :autosize="field.type === 'textarea'"
97 - :autosize="field.type === 'textarea'" 77 + class="border-b border-gray-100" :required="field.required" />
98 - class="border-b border-gray-100"
99 - :required="field.required"
100 - />
101 </div> 78 </div>
102 </div> 79 </div>
103 </van-dialog> 80 </van-dialog>
...@@ -105,14 +82,14 @@ ...@@ -105,14 +82,14 @@
105 <!-- 文本输入区域 --> 82 <!-- 文本输入区域 -->
106 <div class="text-input-area"> 83 <div class="text-input-area">
107 <van-field v-model="message" rows="6" autosize type="textarea" 84 <van-field v-model="message" rows="6" autosize type="textarea"
108 - :placeholder="activeType === 'text' ? '请输入打卡留言,至少需要10个字符' : '请输入打卡留言(可选)'" 85 + :placeholder="checkinType === 'count' ? '请输入留言(可选)' : (activeType === 'text' ? '请输入留言,至少需要10个字符' : '请输入留言(可选)')"
109 - :maxlength="activeType === 'text' ? 500 : 200" show-word-limit /> 86 + :maxlength="activeType === 'text' && checkinType !== 'count' ? 500 : 200" show-word-limit />
110 </div> 87 </div>
111 88
112 - <!-- 打卡类型选项卡 --> 89 + <!-- 类型选项卡 -->
113 <div class="checkin-tabs"> 90 <div class="checkin-tabs">
114 <div class="tabs-header"> 91 <div class="tabs-header">
115 - <div class="tab-title">选择打卡类型</div> 92 + <div class="tab-title">选择类型</div>
116 <div class="tabs-nav"> 93 <div class="tabs-nav">
117 <div v-for="option in attachmentTypeOptions" :key="option.key" 94 <div v-for="option in attachmentTypeOptions" :key="option.key"
118 @click="switchType(option.key)" :class="['tab-item', { 95 @click="switchType(option.key)" :class="['tab-item', {
...@@ -125,7 +102,7 @@ ...@@ -125,7 +102,7 @@
125 </div> 102 </div>
126 103
127 <!-- 文件上传区域 --> 104 <!-- 文件上传区域 -->
128 - <div v-if="activeType !== 'text'" class="upload-area"> 105 + <div v-if="activeType !== '' && activeType !== 'text'" class="upload-area">
129 <van-uploader v-model="fileList" :max-count="maxCount" :max-size="20 * 1024 * 1024" 106 <van-uploader v-model="fileList" :max-count="maxCount" :max-size="20 * 1024 * 1024"
130 :before-read="beforeRead" :after-read="afterRead" @delete="onDelete" 107 :before-read="beforeRead" :after-read="afterRead" @delete="onDelete"
131 @click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file" 108 @click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file"
...@@ -154,9 +131,8 @@ ...@@ -154,9 +131,8 @@
154 131
155 <!-- 提交按钮 --> 132 <!-- 提交按钮 -->
156 <div v-if="!taskDetail.is_finish || route.query.status === 'edit'" class="submit-area"> 133 <div v-if="!taskDetail.is_finish || route.query.status === 'edit'" class="submit-area">
157 - <van-button type="primary" block size="large" :loading="uploading" :disabled="!canSubmit" 134 + <van-button type="primary" block size="large" :loading="uploading" @click="handleSubmit">
158 - @click="onSubmit"> 135 + {{ route.query.status === 'edit' ? '保存修改' : '提交作业' }}
159 - {{ route.query.status === 'edit' ? '保存修改' : '提交打卡' }}
160 </van-button> 136 </van-button>
161 </div> 137 </div>
162 </div> 138 </div>
...@@ -224,7 +200,7 @@ import dayjs from 'dayjs' ...@@ -224,7 +200,7 @@ import dayjs from 'dayjs'
224 200
225 const route = useRoute() 201 const route = useRoute()
226 const router = useRouter() 202 const router = useRouter()
227 -useTitle('打卡详情') 203 +useTitle('提交作业')
228 204
229 // 使用打卡composable 205 // 使用打卡composable
230 const { 206 const {
...@@ -244,6 +220,9 @@ const { ...@@ -244,6 +220,9 @@ const {
244 initEditData 220 initEditData
245 } = useCheckin() 221 } = useCheckin()
246 222
223 +// 动态字段文字
224 +const dynamicFieldText = ref('感恩')
225 +
247 // 任务详情数据 226 // 任务详情数据
248 const taskDetail = ref({}) 227 const taskDetail = ref({})
249 228
...@@ -318,7 +297,7 @@ const confirmAddTarget = () => { ...@@ -318,7 +297,7 @@ const confirmAddTarget = () => {
318 return acc 297 return acc
319 }, {}) 298 }, {})
320 299
321 - console.log('新增计数对象信息:', formData) 300 + console.log(`新增${dynamicFieldText.value}对象信息:`, formData)
322 301
323 // 添加到列表(适配原有的数据结构) 302 // 添加到列表(适配原有的数据结构)
324 targetList.value.push({ 303 targetList.value.push({
...@@ -332,6 +311,36 @@ const confirmAddTarget = () => { ...@@ -332,6 +311,36 @@ const confirmAddTarget = () => {
332 dynamicFormFields.value.forEach(field => field.value = '') 311 dynamicFormFields.value.forEach(field => field.value = '')
333 } 312 }
334 313
314 +const handleSubmit = async () => {
315 + // 1. 校验作业选择
316 + if (!selectedTaskValue.value) {
317 + showToast('请选择作业')
318 + return
319 + }
320 +
321 + const extraData = {
322 + task_option: selectedTaskValue.value // 假设字段名为 task_option
323 + }
324 +
325 + // 2. 计数打卡特定校验
326 + if (checkinType.value === 'count') {
327 + if (selectedTargets.value.length === 0) {
328 + showToast(`请选择${dynamicFieldText.value}对象`)
329 + return
330 + }
331 + if (!countValue.value || countValue.value <= 0) {
332 + showToast(`${dynamicFieldText.value}次数必须大于0`)
333 + return
334 + }
335 +
336 + // 传递额外数据
337 + extraData.targets = selectedTargets.value.map(t => t.name) // 假设传 name
338 + extraData.count = countValue.value
339 + }
340 +
341 + await onSubmit(extraData)
342 +}
343 +
335 // 作品类型选项 344 // 作品类型选项
336 const attachmentTypeOptions = ref([]) 345 const attachmentTypeOptions = ref([])
337 346
...@@ -422,41 +431,35 @@ const getTaskDetail = async (month) => { ...@@ -422,41 +431,35 @@ const getTaskDetail = async (month) => {
422 if (code) { 431 if (code) {
423 taskDetail.value = data 432 taskDetail.value = data
424 433
425 - // 获取作品类型数据 434 + const typeMap = {
426 - if (data.attachment_type && data.attachment_type.length) { 435 + 'text': '文本',
427 - // 创建类型映射 436 + 'image': '图片',
428 - const typeMap = { 437 + 'audio': '音频',
429 - 'text': '文本', 438 + 'video': '视频'
430 - 'image': '图片', 439 + }
431 - 'audio': '音频',
432 - 'video': '视频'
433 - }
434 440
435 - // attachment_type 和 file_type 合并成一个数组 合并以后的数组,就是学员编辑的时候,可以使用的类型 441 + // 处理编辑模式下的类型合并
436 - // 主要是适配先前打卡类型和后期打卡类型不一致的情况 442 + if (route.query.status === 'edit' && Array.isArray(data.attachment_type)) {
437 - if (route.query.status === 'edit') { 443 + const info = await getUploadTaskInfoAPI({ i: route.query.post_id });
438 - const info = await getUploadTaskInfoAPI({ i: route.query.post_id }); 444 + if (info.code) {
439 - if (info.code) { 445 + data.attachment_type = [...new Set([...data.attachment_type, info.data.file_type])];
440 - // 合并 attachment_type 和 file_type 数组, 里面数据需要去重复
441 - data.attachment_type = [...new Set([...data.attachment_type, info.data.file_type])];
442 - }
443 } 446 }
447 + }
444 448
445 - // 如果是数组格式,转换为对象格式 449 + if (Array.isArray(data.attachment_type)) {
446 - if (Array.isArray(data.attachment_type)) { 450 + attachmentTypeOptions.value = data.attachment_type.map(key => ({
447 - attachmentTypeOptions.value = data.attachment_type.map(key => ({ 451 + key,
448 - key, 452 + value: typeMap[key] || key
449 - value: typeMap[key] || key 453 + }))
450 - })) 454 + } else if (data.attachment_type && typeof data.attachment_type === 'object') {
451 - } else { 455 + attachmentTypeOptions.value = Object.entries(data.attachment_type).map(([key, value]) => ({
452 - // 如果是对象格式,直接使用 456 + key,
453 - attachmentTypeOptions.value = Object.entries(data.attachment_type).map(([key, value]) => ({ 457 + value
454 - key, 458 + }))
455 - value 459 + }
456 - })) 460 +
457 - } 461 + // 如果没有解析出任何类型,或者列表为空,则使用默认4种类型
458 - } else { 462 + if (attachmentTypeOptions.value.length === 0) {
459 - // 显示4种类型
460 attachmentTypeOptions.value = [ 463 attachmentTypeOptions.value = [
461 { key: 'text', value: '文本' }, 464 { key: 'text', value: '文本' },
462 { key: 'image', value: '图片' }, 465 { key: 'image', value: '图片' },
...@@ -464,6 +467,11 @@ const getTaskDetail = async (month) => { ...@@ -464,6 +467,11 @@ const getTaskDetail = async (month) => {
464 { key: 'video', value: '视频' } 467 { key: 'video', value: '视频' }
465 ] 468 ]
466 } 469 }
470 +
471 + // 设置默认选中类型(非计数打卡模式下)
472 + if (checkinType.value !== 'count' && attachmentTypeOptions.value.length > 0 && !activeType.value) {
473 + activeType.value = attachmentTypeOptions.value[0].key
474 + }
467 } 475 }
468 } 476 }
469 477
...@@ -545,7 +553,7 @@ const onClickPreview = (file, detail) => { ...@@ -545,7 +553,7 @@ const onClickPreview = (file, detail) => {
545 // 图片预览由van-uploader的@click-preview事件处理,避免重复弹出 553 // 图片预览由van-uploader的@click-preview事件处理,避免重复弹出
546 return 554 return
547 } else { 555 } else {
548 - console.log('该文件类型不支持预览,文件名:', fileName, '打卡类型:', activeType.value) 556 + console.log('该文件类型不支持预览,文件名:', fileName, '类型:', activeType.value)
549 showToast('该文件类型不支持预览') 557 showToast('该文件类型不支持预览')
550 } 558 }
551 } 559 }
......