hookehuyr

fix(checkin): allow task description rich text to wrap

Override inline rich-text no-wrap styles so long assignment descriptions render fully in the checkin detail page.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
...@@ -13,11 +13,8 @@ ...@@ -13,11 +13,8 @@
13 <div class="section-wrapper"> 13 <div class="section-wrapper">
14 <div class="section-title">作业描述</div> 14 <div class="section-title">作业描述</div>
15 <div class="section-content"> 15 <div class="section-content">
16 - <div v-if="displayTaskNote" class="description-text" v-html="displayTaskNote"> 16 + <div v-if="displayTaskNote" class="description-text" v-html="displayTaskNote"></div>
17 - </div> 17 + <div v-else class="no-description">暂无作业描述</div>
18 - <div v-else class="no-description">
19 - 暂无作业描述
20 - </div>
21 </div> 18 </div>
22 </div> 19 </div>
23 20
...@@ -28,18 +25,31 @@ ...@@ -28,18 +25,31 @@
28 <!-- 作业选择区域 --> 25 <!-- 作业选择区域 -->
29 <div class="mb-4"> 26 <div class="mb-4">
30 <!-- 编辑模式下直接显示文本 --> 27 <!-- 编辑模式下直接显示文本 -->
31 - <div v-if="isEditMode" class="bg-gray-50 rounded-lg p-3 border border-gray-100 flex items-center justify-between"> 28 + <div
32 - <span class="text-gray-700 font-medium">当前作业</span> 29 + v-if="isEditMode"
33 - <span class="text-gray-900 font-bold">{{ selectedTaskText }}</span> 30 + class="flex items-center justify-between rounded-lg border border-gray-100 bg-gray-50 p-3"
31 + >
32 + <span class="font-medium text-gray-700">当前作业</span>
33 + <span class="font-bold text-gray-900">{{ selectedTaskText }}</span>
34 </div> 34 </div>
35 35
36 <!-- 非编辑模式下显示选择框 --> 36 <!-- 非编辑模式下显示选择框 -->
37 <template v-else> 37 <template v-else>
38 - <van-field v-model="selectedTaskText" is-link readonly label="选择作业" placeholder="请选择本次打卡的作业" 38 + <van-field
39 - @click="showTaskPicker = true" class="rounded-lg border border-gray-100" /> 39 + v-model="selectedTaskText"
40 + is-link
41 + readonly
42 + label="选择作业"
43 + placeholder="请选择本次打卡的作业"
44 + @click="showTaskPicker = true"
45 + class="rounded-lg border border-gray-100"
46 + />
40 <van-popup v-model:show="showTaskPicker" round position="bottom"> 47 <van-popup v-model:show="showTaskPicker" round position="bottom">
41 - <van-picker :columns="taskOptions" @cancel="showTaskPicker = false" 48 + <van-picker
42 - @confirm="onConfirmTask" /> 49 + :columns="taskOptions"
50 + @cancel="showTaskPicker = false"
51 + @confirm="onConfirmTask"
52 + />
43 </van-popup> 53 </van-popup>
44 </template> 54 </template>
45 </div> 55 </div>
...@@ -56,16 +66,30 @@ ...@@ -56,16 +66,30 @@
56 /> 66 />
57 67
58 <!-- 计数次数 --> 68 <!-- 计数次数 -->
59 - <div v-if="taskType === 'count'" 69 + <div
60 - class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg"> 70 + v-if="taskType === 'count'"
71 + class="mb-4 flex items-center justify-between rounded-lg bg-gray-50 p-3"
72 + >
61 <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}次数</div> 73 <div class="text-sm font-bold text-gray-700">{{ dynamicFieldText }}次数</div>
62 - <van-stepper v-model="countValue" min="1" integer input-width="80px" button-size="28px" /> 74 + <van-stepper
75 + v-model="countValue"
76 + min="1"
77 + integer
78 + input-width="80px"
79 + button-size="28px"
80 + />
63 </div> 81 </div>
64 82
65 <!-- 新增计数对象弹框 --> 83 <!-- 新增计数对象弹框 -->
66 <AddTargetDialog 84 <AddTargetDialog
67 v-model:show="showAddTargetDialog" 85 v-model:show="showAddTargetDialog"
68 - :title="editingTarget ? (isConfirmMode ? `确认${dynamicFieldText}项` : `编辑${dynamicFieldText}项`) : `添加${dynamicFieldText}项`" 86 + :title="
87 + editingTarget
88 + ? isConfirmMode
89 + ? `确认${dynamicFieldText}项`
90 + : `编辑${dynamicFieldText}项`
91 + : `添加${dynamicFieldText}项`
92 + "
69 :fields="dynamicFormFields" 93 :fields="dynamicFormFields"
70 :initial-values="editingTarget" 94 :initial-values="editingTarget"
71 @confirm="confirmAddTarget" 95 @confirm="confirmAddTarget"
...@@ -73,23 +97,47 @@ ...@@ -73,23 +97,47 @@
73 97
74 <!-- 文本输入区域 --> 98 <!-- 文本输入区域 -->
75 <div class="text-input-area"> 99 <div class="text-input-area">
76 - <van-field v-model="message" rows="6" autosize type="textarea" 100 + <van-field
77 - :placeholder="taskType === 'count' ? '请输入留言(可选)' : (activeType === 'text' ? '请输入留言,至少需要10个字符' : '请输入留言(可选)')" /> 101 + v-model="message"
102 + rows="6"
103 + autosize
104 + type="textarea"
105 + :placeholder="
106 + taskType === 'count'
107 + ? '请输入留言(可选)'
108 + : activeType === 'text'
109 + ? '请输入留言,至少需要10个字符'
110 + : '请输入留言(可选)'
111 + "
112 + />
78 </div> 113 </div>
79 114
80 <!-- 类型选项卡 --> 115 <!-- 类型选项卡 -->
81 <div class="checkin-tabs" v-if="selectedTaskValue.length > 0"> 116 <div class="checkin-tabs" v-if="selectedTaskValue.length > 0">
82 <div class="tabs-header"> 117 <div class="tabs-header">
83 - <div class="tab-title">{{ taskType === 'count' ? '附件类型(可选)' : '附件类型' }}</div> 118 + <div class="tab-title">
119 + {{ taskType === 'count' ? '附件类型(可选)' : '附件类型' }}
120 + </div>
84 <div class="tabs-nav"> 121 <div class="tabs-nav">
85 - <div v-for="option in attachmentTypeOptions" :key="option.key" 122 + <div
86 - @click="switchType(option.key)" :class="['tab-item', 'relative', { 123 + v-for="option in attachmentTypeOptions"
87 - active: activeType === option.key 124 + :key="option.key"
88 - }]"> 125 + @click="switchType(option.key)"
126 + :class="[
127 + 'tab-item',
128 + 'relative',
129 + {
130 + active: activeType === option.key,
131 + },
132 + ]"
133 + >
89 <van-icon :name="getIconName(option.key)" size="1.2rem" /> 134 <van-icon :name="getIconName(option.key)" size="1.2rem" />
90 <span class="tab-text">{{ option.value }}</span> 135 <span class="tab-text">{{ option.value }}</span>
91 <!-- <div v-if="multiAttachmentEnabled && getTypeCount(option.key) > 0" class="absolute -top-2 -right-2 bg-red-500 text-white text-[10px] rounded-full min-w-[16px] h-[16px] flex items-center justify-center px-1"> --> 136 <!-- <div v-if="multiAttachmentEnabled && getTypeCount(option.key) > 0" class="absolute -top-2 -right-2 bg-red-500 text-white text-[10px] rounded-full min-w-[16px] h-[16px] flex items-center justify-center px-1"> -->
92 - <div v-if="getTypeCount(option.key) > 0" class="absolute -top-2 -right-2 bg-red-500 text-white text-[10px] rounded-full min-w-[16px] h-[16px] flex items-center justify-center px-1"> 137 + <div
138 + v-if="getTypeCount(option.key) > 0"
139 + class="absolute -right-2 -top-2 flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-red-500 px-1 text-[10px] text-white"
140 + >
93 {{ getTypeCount(option.key) }} 141 {{ getTypeCount(option.key) }}
94 </div> 142 </div>
95 </div> 143 </div>
...@@ -98,10 +146,20 @@ ...@@ -98,10 +146,20 @@
98 146
99 <!-- 文件上传区域 --> 147 <!-- 文件上传区域 -->
100 <div v-if="activeType !== '' && activeType !== 'text'" class="upload-area"> 148 <div v-if="activeType !== '' && activeType !== 'text'" class="upload-area">
101 - <van-uploader v-model="displayFileList" :max-count="maxCount" :max-size="maxFileSizeBytes" 149 + <van-uploader
102 - :before-read="beforeReadGuard" :after-read="afterRead" @delete="onDelete" 150 + v-model="displayFileList"
103 - @click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file" 151 + :max-count="maxCount"
104 - :deletable="true" upload-icon="plus" /> 152 + :max-size="maxFileSizeBytes"
153 + :before-read="beforeReadGuard"
154 + :after-read="afterRead"
155 + @delete="onDelete"
156 + @click-preview="onClickPreview"
157 + multiple
158 + :accept="getAcceptType()"
159 + result-type="file"
160 + :deletable="true"
161 + upload-icon="plus"
162 + />
105 163
106 <!-- 文件列表显示 --> 164 <!-- 文件列表显示 -->
107 <!-- <div v-if="fileList.length > 0" class="file-list"> 165 <!-- <div v-if="fileList.length > 0" class="file-list">
...@@ -116,7 +174,9 @@ ...@@ -116,7 +174,9 @@
116 </div> --> 174 </div> -->
117 175
118 <div class="upload-tips"> 176 <div class="upload-tips">
119 - <div class="tip-text">最多上传{{ maxCount }}个文件,每个不超过{{ maxFileSizeMb }}MB</div> 177 + <div class="tip-text">
178 + 最多上传{{ maxCount }}个文件,每个不超过{{ maxFileSizeMb }}MB
179 + </div>
120 <div class="tip-text">{{ getUploadTips() }}</div> 180 <div class="tip-text">{{ getUploadTips() }}</div>
121 </div> 181 </div>
122 </div> 182 </div>
...@@ -126,7 +186,14 @@ ...@@ -126,7 +186,14 @@
126 186
127 <!-- 提交按钮 --> 187 <!-- 提交按钮 -->
128 <div v-if="!taskDetail.is_finish || isEditMode" class="submit-area"> 188 <div v-if="!taskDetail.is_finish || isEditMode" class="submit-area">
129 - <van-button type="primary" block size="large" :loading="uploading" :disabled="isSubmitDisabled" @click="handleSubmit"> 189 + <van-button
190 + type="primary"
191 + block
192 + size="large"
193 + :loading="uploading"
194 + :disabled="isSubmitDisabled"
195 + @click="handleSubmit"
196 + >
130 {{ isEditMode ? '保存修改' : '提交' }} 197 {{ isEditMode ? '保存修改' : '提交' }}
131 </van-button> 198 </van-button>
132 </div> 199 </div>
...@@ -140,50 +207,89 @@ ...@@ -140,50 +207,89 @@
140 </van-overlay> 207 </van-overlay>
141 208
142 <!-- 音频播放器弹窗 --> 209 <!-- 音频播放器弹窗 -->
143 - <van-popup v-model:show="audioShow" position="bottom" round closeable :style="{ height: '60%', width: '100%' }"> 210 + <van-popup
211 + v-model:show="audioShow"
212 + position="bottom"
213 + round
214 + closeable
215 + :style="{ height: '60%', width: '100%' }"
216 + >
144 <div class="p-4"> 217 <div class="p-4">
145 - <h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3> 218 + <h3 class="mb-4 text-center text-lg font-medium">{{ audioTitle }}</h3>
146 - <AudioPlayer v-if="audioShow && audioUrl" :songs="[{ title: audioTitle, url: audioUrl }]" 219 + <AudioPlayer
147 - class="w-full" /> 220 + v-if="audioShow && audioUrl"
221 + :songs="[{ title: audioTitle, url: audioUrl }]"
222 + class="w-full"
223 + />
148 </div> 224 </div>
149 </van-popup> 225 </van-popup>
150 226
151 <!-- 视频播放器弹窗 --> 227 <!-- 视频播放器弹窗 -->
152 - <van-popup v-model:show="videoShow" position="center" round closeable 228 + <van-popup
153 - :style="{ width: '95%', maxHeight: '80vh' }" @close="stopVideoPlay"> 229 + v-model:show="videoShow"
230 + position="center"
231 + round
232 + closeable
233 + :style="{ width: '95%', maxHeight: '80vh' }"
234 + @close="stopVideoPlay"
235 + >
154 <div class="p-4"> 236 <div class="p-4">
155 - <h3 class="text-lg font-medium mb-4 text-center">视频预览</h3> 237 + <h3 class="mb-4 text-center text-lg font-medium">视频预览</h3>
156 - <div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;"> 238 + <div class="relative w-full overflow-hidden rounded-lg bg-black" style="aspect-ratio: 16/9">
157 <!-- 视频封面 --> 239 <!-- 视频封面 -->
158 - <div v-show="!isVideoPlaying" 240 + <div
159 - class="absolute inset-0 flex items-center justify-center cursor-pointer" 241 + v-show="!isVideoPlaying"
160 - @click="startVideoPlay"> 242 + class="absolute inset-0 flex cursor-pointer items-center justify-center"
161 - <img :src="videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" 243 + @click="startVideoPlay"
162 - :alt="videoTitle" class="w-full h-full object-cover" /> 244 + >
245 + <img
246 + :src="videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
247 + :alt="videoTitle"
248 + class="h-full w-full object-cover"
249 + />
163 <div class="absolute inset-0 flex items-center justify-center bg-black/20"> 250 <div class="absolute inset-0 flex items-center justify-center bg-black/20">
164 <div 251 <div
165 - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> 252 + class="flex h-16 w-16 items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70"
253 + >
166 <van-icon name="play-circle-o" class="text-white" size="40" /> 254 <van-icon name="play-circle-o" class="text-white" size="40" />
167 </div> 255 </div>
168 </div> 256 </div>
169 </div> 257 </div>
170 <!-- 视频播放器 --> 258 <!-- 视频播放器 -->
171 - <VideoPlayer v-if="isVideoPlaying" ref="videoPlayerRef" :video-url="videoUrl" 259 + <VideoPlayer
172 - :video-id="videoTitle" :use-native-on-ios="false" :autoplay="false" class="w-full h-full" @play="handleVideoPlay" 260 + v-if="isVideoPlaying"
173 - @pause="handleVideoPause" /> 261 + ref="videoPlayerRef"
262 + :video-url="videoUrl"
263 + :video-id="videoTitle"
264 + :use-native-on-ios="false"
265 + :autoplay="false"
266 + class="h-full w-full"
267 + @play="handleVideoPlay"
268 + @pause="handleVideoPause"
269 + />
174 </div> 270 </div>
175 </div> 271 </div>
176 </van-popup> 272 </van-popup>
177 273
178 <!-- 图片预览弹窗 --> 274 <!-- 图片预览弹窗 -->
179 - <van-image-preview v-model:show="imageShow" :images="imageList" :start-position="imageIndex" :show-index="true" /> 275 + <van-image-preview
276 + v-model:show="imageShow"
277 + :images="imageList"
278 + :start-position="imageIndex"
279 + :show-index="true"
280 + />
180 </div> 281 </div>
181 </template> 282 </template>
182 283
183 <script setup> 284 <script setup>
184 import { ref, computed, onMounted, nextTick, reactive, watch, onBeforeUnmount } from 'vue' 285 import { ref, computed, onMounted, nextTick, reactive, watch, onBeforeUnmount } from 'vue'
185 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router' 286 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
186 -import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI, reuseGratitudeFormAPI } from "@/api/checkin" 287 +import {
288 + getTaskDetailAPI,
289 + getUploadTaskInfoAPI,
290 + getSubtaskListAPI,
291 + reuseGratitudeFormAPI,
292 +} from '@/api/checkin'
187 import { useTitle } from '@vueuse/core' 293 import { useTitle } from '@vueuse/core'
188 import { useCheckin } from '@/composables/useCheckin' 294 import { useCheckin } from '@/composables/useCheckin'
189 import { useCheckinDraft } from '@/composables/useCheckinDraft' 295 import { useCheckinDraft } from '@/composables/useCheckinDraft'
...@@ -226,7 +332,7 @@ const { ...@@ -226,7 +332,7 @@ const {
226 switchType, 332 switchType,
227 initEditData, 333 initEditData,
228 gratitudeCount, 334 gratitudeCount,
229 - gratitudeFormList 335 + gratitudeFormList,
230 } = useCheckin() 336 } = useCheckin()
231 337
232 // 使用草稿缓存composable 338 // 使用草稿缓存composable
...@@ -236,19 +342,19 @@ const { ...@@ -236,19 +342,19 @@ const {
236 save_draft: saveDraft, 342 save_draft: saveDraft,
237 read_draft: readDraft, 343 read_draft: readDraft,
238 clear_draft: clearDraft, 344 clear_draft: clearDraft,
239 - cleanup_expired: cleanupExpiredDrafts 345 + cleanup_expired: cleanupExpiredDrafts,
240 } = useCheckinDraft() 346 } = useCheckinDraft()
241 347
242 // 草稿Key 348 // 草稿Key
243 -const draftKey = computed(() => { 349 +const draftKey = computed(() =>
244 - return buildDraftKey({ 350 + buildDraftKey({
245 user_id: currentUser.value?.id, 351 user_id: currentUser.value?.id,
246 task_id: route.query.task_id, 352 task_id: route.query.task_id,
247 date: route.query.date, 353 date: route.query.date,
248 task_type: route.query.task_type, 354 task_type: route.query.task_type,
249 - status: route.query.status || 'create' 355 + status: route.query.status || 'create',
250 }) 356 })
251 -}) 357 +)
252 358
253 // 动态字段文字 359 // 动态字段文字
254 const dynamicFieldText = ref('感恩') 360 const dynamicFieldText = ref('感恩')
...@@ -307,17 +413,21 @@ const autoSaveDraft = debounce(() => { ...@@ -307,17 +413,21 @@ const autoSaveDraft = debounce(() => {
307 file_list: fileList.value, // save_draft内部会过滤done状态 413 file_list: fileList.value, // save_draft内部会过滤done状态
308 count: { 414 count: {
309 gratitude_count: countValue.value, 415 gratitude_count: countValue.value,
310 - gratitude_form_list: selectedTargets.value 416 + gratitude_form_list: selectedTargets.value,
311 - } 417 + },
312 } 418 }
313 419
314 saveDraft(draftKey.value, payload) 420 saveDraft(draftKey.value, payload)
315 }, 500) 421 }, 500)
316 422
317 // 监听数据变化触发自动保存 423 // 监听数据变化触发自动保存
318 -watch([message, fileList, selectedTaskValue, countValue, selectedTargets], () => { 424 +watch(
425 + [message, fileList, selectedTaskValue, countValue, selectedTargets],
426 + () => {
319 autoSaveDraft() 427 autoSaveDraft()
320 -}, { deep: true }) 428 + },
429 + { deep: true }
430 +)
321 431
322 // 页面离开前强制保存一次 432 // 页面离开前强制保存一次
323 onBeforeRouteLeave(() => { 433 onBeforeRouteLeave(() => {
...@@ -351,13 +461,16 @@ const checkAndRestoreDraft = async () => { ...@@ -351,13 +461,16 @@ const checkAndRestoreDraft = async () => {
351 461
352 // 校验草稿中的作业是否仍然有效 462 // 校验草稿中的作业是否仍然有效
353 // 如果草稿中包含具体的作业ID,必须确保该作业在当前可用的作业列表(taskOptions)中存在 463 // 如果草稿中包含具体的作业ID,必须确保该作业在当前可用的作业列表(taskOptions)中存在
354 - const draftSubtaskId = payload.subtask_id || (payload.selected_task_value && payload.selected_task_value[0]) 464 + const draftSubtaskId =
465 + payload.subtask_id || (payload.selected_task_value && payload.selected_task_value[0])
355 if (draftSubtaskId) { 466 if (draftSubtaskId) {
356 // taskOptions 已经在 onMounted 中加载完毕 467 // taskOptions 已经在 onMounted 中加载完毕
357 - const isValidTask = taskOptions.value.some(option => option.value == draftSubtaskId) 468 + const isValidTask = taskOptions.value.some(
469 + option => String(option.value) === String(draftSubtaskId)
470 + )
358 471
359 if (!isValidTask) { 472 if (!isValidTask) {
360 - console.log('[草稿清理] 作业已失效,中断恢复流程', draftSubtaskId) 473 + console.warn('[草稿清理] 作业已失效,中断恢复流程', draftSubtaskId)
361 try { 474 try {
362 await showDialog({ 475 await showDialog({
363 title: '草稿已失效', 476 title: '草稿已失效',
...@@ -375,7 +488,8 @@ const checkAndRestoreDraft = async () => { ...@@ -375,7 +488,8 @@ const checkAndRestoreDraft = async () => {
375 } 488 }
376 489
377 // 检查是否有实质内容 490 // 检查是否有实质内容
378 - const hasContent = (payload.message && payload.message.trim()) || 491 + const hasContent =
492 + (payload.message && payload.message.trim()) ||
379 (payload.file_list && payload.file_list.length > 0) 493 (payload.file_list && payload.file_list.length > 0)
380 494
381 if (!hasContent) return 495 if (!hasContent) return
...@@ -385,11 +499,9 @@ const checkAndRestoreDraft = async () => { ...@@ -385,11 +499,9 @@ const checkAndRestoreDraft = async () => {
385 title: '发现未提交的草稿', 499 title: '发现未提交的草稿',
386 message: '上次编辑的内容未提交,是否恢复?', 500 message: '上次编辑的内容未提交,是否恢复?',
387 confirmButtonText: '恢复', 501 confirmButtonText: '恢复',
388 - cancelButtonText: '丢弃' 502 + cancelButtonText: '丢弃',
389 }) 503 })
390 -
391 // 确认恢复 504 // 确认恢复
392 - console.log('[草稿恢复] 开始恢复数据', payload)
393 505
394 if (payload.message) message.value = payload.message 506 if (payload.message) message.value = payload.message
395 if (payload.active_type) activeType.value = payload.active_type 507 if (payload.active_type) activeType.value = payload.active_type
...@@ -400,7 +512,7 @@ const checkAndRestoreDraft = async () => { ...@@ -400,7 +512,7 @@ const checkAndRestoreDraft = async () => {
400 fileList.value = payload.file_list.map(f => ({ 512 fileList.value = payload.file_list.map(f => ({
401 ...f, 513 ...f,
402 status: 'done', 514 status: 'done',
403 - message: '已上传' 515 + message: '已上传',
404 })) 516 }))
405 } 517 }
406 518
...@@ -440,7 +552,11 @@ const checkAndRestoreDraft = async () => { ...@@ -440,7 +552,11 @@ const checkAndRestoreDraft = async () => {
440 552
441 // 恢复感恩列表(计数对象) 553 // 恢复感恩列表(计数对象)
442 // 必须在恢复 selectedTaskValue 之后执行,因为 fetchTargetList 依赖 subtask_id 554 // 必须在恢复 selectedTaskValue 之后执行,因为 fetchTargetList 依赖 subtask_id
443 - if (payload.count?.gratitude_form_list && Array.isArray(payload.count.gratitude_form_list) && payload.count.gratitude_form_list.length > 0) { 555 + if (
556 + payload.count?.gratitude_form_list &&
557 + Array.isArray(payload.count.gratitude_form_list) &&
558 + payload.count.gratitude_form_list.length > 0
559 + ) {
444 const savedList = payload.count.gratitude_form_list 560 const savedList = payload.count.gratitude_form_list
445 561
446 // 如果有作业ID,先获取基础列表 562 // 如果有作业ID,先获取基础列表
...@@ -457,8 +573,9 @@ const checkAndRestoreDraft = async () => { ...@@ -457,8 +573,9 @@ const checkAndRestoreDraft = async () => {
457 573
458 savedList.forEach(savedItem => { 574 savedList.forEach(savedItem => {
459 // 尝试在 targetList 中找到对应项(获取最新状态/引用) 575 // 尝试在 targetList 中找到对应项(获取最新状态/引用)
460 - const existingItem = targetList.value.find(t => 576 + const existingItem = targetList.value.find(
461 - (savedItem.id && t.id && t.id == savedItem.id) || 577 + t =>
578 + (savedItem.id && t.id && String(t.id) === String(savedItem.id)) ||
462 (!savedItem.id && savedItem.name === t.name) 579 (!savedItem.id && savedItem.name === t.name)
463 ) 580 )
464 581
...@@ -469,7 +586,7 @@ const checkAndRestoreDraft = async () => { ...@@ -469,7 +586,7 @@ const checkAndRestoreDraft = async () => {
469 // 如果 targetList 里没有(可能是新增的,或者 targetList 变了),则直接使用草稿项 586 // 如果 targetList 里没有(可能是新增的,或者 targetList 变了),则直接使用草稿项
470 restoredTargets.push({ 587 restoredTargets.push({
471 ...savedItem, 588 ...savedItem,
472 - has_confirmed: true 589 + has_confirmed: true,
473 }) 590 })
474 // 同时也加到 targetList 里显示出来(如果是新增的) 591 // 同时也加到 targetList 里显示出来(如果是新增的)
475 targetList.value.push(restoredTargets[restoredTargets.length - 1]) 592 targetList.value.push(restoredTargets[restoredTargets.length - 1])
...@@ -484,7 +601,6 @@ const checkAndRestoreDraft = async () => { ...@@ -484,7 +601,6 @@ const checkAndRestoreDraft = async () => {
484 } 601 }
485 602
486 showToast('已恢复草稿') 603 showToast('已恢复草稿')
487 -
488 } catch (e) { 604 } catch (e) {
489 // 取消恢复,清除草稿 605 // 取消恢复,清除草稿
490 if (e !== 'cancel') console.error(e) 606 if (e !== 'cancel') console.error(e)
...@@ -493,8 +609,7 @@ const checkAndRestoreDraft = async () => { ...@@ -493,8 +609,7 @@ const checkAndRestoreDraft = async () => {
493 } 609 }
494 } 610 }
495 611
496 - 612 +const beforeReadGuard = file => {
497 -const beforeReadGuard = (file) => {
498 const files = Array.isArray(file) ? file : [file] 613 const files = Array.isArray(file) ? file : [file]
499 if (activeType.value === 'video') { 614 if (activeType.value === 'video') {
500 const hasMov = files.some(item => { 615 const hasMov = files.some(item => {
...@@ -506,7 +621,8 @@ const beforeReadGuard = (file) => { ...@@ -506,7 +621,8 @@ const beforeReadGuard = (file) => {
506 if (hasMov) { 621 if (hasMov) {
507 showDialog({ 622 showDialog({
508 title: '不支持 MOV 格式', 623 title: '不支持 MOV 格式',
509 - message: 'MOV(QuickTime)在非苹果系统/部分播放器兼容性较差,可能出现无法打开、黑屏、无声等问题。\n\n请将视频导出/转换为 MP4(更通用)后再上传。', 624 + message:
625 + 'MOV(QuickTime)在非苹果系统/部分播放器兼容性较差,可能出现无法打开、黑屏、无声等问题。\n\n请将视频导出/转换为 MP4(更通用)后再上传。',
510 confirmButtonText: '我知道了', 626 confirmButtonText: '我知道了',
511 }) 627 })
512 return false 628 return false
...@@ -530,7 +646,8 @@ const beforeReadGuard = (file) => { ...@@ -530,7 +646,8 @@ const beforeReadGuard = (file) => {
530 if (unsupportedFiles.length > 0) { 646 if (unsupportedFiles.length > 0) {
531 showDialog({ 647 showDialog({
532 title: '不支持的音频格式', 648 title: '不支持的音频格式',
533 - message: '当前音频播放基于系统浏览器能力,不同机型/系统对音频格式支持差异较大(例如 .wma 等常见无法播放)。\n\n为避免上传后无法播放,请使用 .mp3 或 .m4a(推荐)重新导出/转换后再上传。', 649 + message:
650 + '当前音频播放基于系统浏览器能力,不同机型/系统对音频格式支持差异较大(例如 .wma 等常见无法播放)。\n\n为避免上传后无法播放,请使用 .mp3 或 .m4a(推荐)重新导出/转换后再上传。',
534 confirmButtonText: '我知道了', 651 confirmButtonText: '我知道了',
535 }) 652 })
536 return false 653 return false
...@@ -547,8 +664,8 @@ const beforeReadGuard = (file) => { ...@@ -547,8 +664,8 @@ const beforeReadGuard = (file) => {
547 * @param {string} type - 文件类型 664 * @param {string} type - 文件类型
548 * @returns {number} 文件数量 665 * @returns {number} 文件数量
549 */ 666 */
550 -const getTypeCount = (type) => { 667 +const getTypeCount = type =>
551 - return fileList.value.filter(item => { 668 + fileList.value.filter(item => {
552 if (item.file_type) { 669 if (item.file_type) {
553 return item.file_type === type 670 return item.file_type === type
554 } 671 }
...@@ -560,7 +677,6 @@ const getTypeCount = (type) => { ...@@ -560,7 +677,6 @@ const getTypeCount = (type) => {
560 } 677 }
561 return false 678 return false
562 }).length 679 }).length
563 -}
564 680
565 /** 681 /**
566 * 当前显示的(经过类型过滤的)文件列表 682 * 当前显示的(经过类型过滤的)文件列表
...@@ -569,8 +685,8 @@ const getTypeCount = (type) => { ...@@ -569,8 +685,8 @@ const getTypeCount = (type) => {
569 * 2. setter: 处理 van-uploader 的更新(添加/删除),同步回 fileList 685 * 2. setter: 处理 van-uploader 的更新(添加/删除),同步回 fileList
570 */ 686 */
571 const displayFileList = computed({ 687 const displayFileList = computed({
572 - get: () => { 688 + get: () =>
573 - return fileList.value.filter(item => { 689 + fileList.value.filter(item => {
574 if (item.file_type) { 690 if (item.file_type) {
575 return item.file_type === activeType.value 691 return item.file_type === activeType.value
576 } 692 }
...@@ -580,9 +696,8 @@ const displayFileList = computed({ ...@@ -580,9 +696,8 @@ const displayFileList = computed({
580 if (activeType.value === 'audio') return item.file.type.startsWith('audio/') 696 if (activeType.value === 'audio') return item.file.type.startsWith('audio/')
581 } 697 }
582 return false 698 return false
583 - }) 699 + }),
584 - }, 700 + set: val => {
585 - set: (val) => {
586 // 找出不属于当前视图的其他文件(保留它们) 701 // 找出不属于当前视图的其他文件(保留它们)
587 const otherFiles = fileList.value.filter(item => { 702 const otherFiles = fileList.value.filter(item => {
588 if (item.file_type) { 703 if (item.file_type) {
...@@ -600,13 +715,9 @@ const displayFileList = computed({ ...@@ -600,13 +715,9 @@ const displayFileList = computed({
600 715
601 // 合并其他文件和当前视图的新文件列表 716 // 合并其他文件和当前视图的新文件列表
602 fileList.value = [...otherFiles, ...val] 717 fileList.value = [...otherFiles, ...val]
603 - } 718 + },
604 }) 719 })
605 720
606 -
607 -
608 -
609 -
610 const maxFileSizeBytes = computed(() => { 721 const maxFileSizeBytes = computed(() => {
611 const size = Number(maxFileSizeMb.value || 0) 722 const size = Number(maxFileSizeMb.value || 0)
612 if (!Number.isFinite(size) || size <= 0) return 20 * 1024 * 1024 723 if (!Number.isFinite(size) || size <= 0) return 20 * 1024 * 1024
...@@ -626,9 +737,7 @@ const displayTaskNote = computed(() => { ...@@ -626,9 +737,7 @@ const displayTaskNote = computed(() => {
626 // 打卡类型 737 // 打卡类型
627 const taskType = computed(() => route.query.task_type) 738 const taskType = computed(() => route.query.task_type)
628 739
629 - 740 +const fetchTargetList = async subtask_id => {
630 -
631 -const fetchTargetList = async (subtask_id) => {
632 const { code, data } = await reuseGratitudeFormAPI({ subtask_id }) 741 const { code, data } = await reuseGratitudeFormAPI({ subtask_id })
633 if (code === 1) { 742 if (code === 1) {
634 targetList.value = data.gratitude_form_list || [] 743 targetList.value = data.gratitude_form_list || []
...@@ -640,8 +749,9 @@ const fetchTargetList = async (subtask_id) => { ...@@ -640,8 +749,9 @@ const fetchTargetList = async (subtask_id) => {
640 const validTargets = [] 749 const validTargets = []
641 750
642 lastUsedTargetList.value.forEach(lastItem => { 751 lastUsedTargetList.value.forEach(lastItem => {
643 - const targetItem = targetList.value.find(t => 752 + const targetItem = targetList.value.find(
644 - (lastItem.id && t.id && t.id == lastItem.id) || 753 + t =>
754 + (lastItem.id && t.id && String(t.id) === String(lastItem.id)) ||
645 (!lastItem.id && lastItem.name === t.name) 755 (!lastItem.id && lastItem.name === t.name)
646 ) 756 )
647 757
...@@ -654,8 +764,9 @@ const fetchTargetList = async (subtask_id) => { ...@@ -654,8 +764,9 @@ const fetchTargetList = async (subtask_id) => {
654 764
655 // 将这些项加入 selectedTargets(去重) 765 // 将这些项加入 selectedTargets(去重)
656 validTargets.forEach(item => { 766 validTargets.forEach(item => {
657 - const exists = selectedTargets.value.some(t => 767 + const exists = selectedTargets.value.some(
658 - (item.id && t.id && t.id == item.id) || 768 + t =>
769 + (item.id && t.id && String(t.id) === String(item.id)) ||
659 (!item.id && t.name === item.name) 770 (!item.id && t.name === item.name)
660 ) 771 )
661 if (!exists) { 772 if (!exists) {
...@@ -666,24 +777,20 @@ const fetchTargetList = async (subtask_id) => { ...@@ -666,24 +777,20 @@ const fetchTargetList = async (subtask_id) => {
666 } 777 }
667 } 778 }
668 779
669 -
670 -
671 /** 780 /**
672 * 更新动态表单字段 781 * 更新动态表单字段
673 * @description 根据选中的作业选项更新动态表单字段配置 782 * @description 根据选中的作业选项更新动态表单字段配置
674 * @param {Object} option - 选中的作业选项 783 * @param {Object} option - 选中的作业选项
675 */ 784 */
676 -const updateDynamicFormFields = (option) => { 785 +const updateDynamicFormFields = option => {
677 if (option.field_list && Array.isArray(option.field_list)) { 786 if (option.field_list && Array.isArray(option.field_list)) {
678 // 处理动态表单字段 787 // 处理动态表单字段
679 - dynamicFormFields.value = option.field_list.map(field => { 788 + dynamicFormFields.value = option.field_list.map(field => ({
680 - return {
681 id: field.field || field.field_name || field.name || field.id, // 兼容多种字段名 789 id: field.field || field.field_name || field.name || field.id, // 兼容多种字段名
682 label: field.label || '未命名', 790 label: field.label || '未命名',
683 type: field.type || 'text', // 默认类型,如果后端有类型字段可替换 791 type: field.type || 'text', // 默认类型,如果后端有类型字段可替换
684 - required: true // 默认必填,如果后端有必填字段可替换 792 + required: true, // 默认必填,如果后端有必填字段可替换
685 - } 793 + }))
686 - })
687 // 确保如果有city字段,类型为textarea 794 // 确保如果有city字段,类型为textarea
688 const cityField = dynamicFormFields.value.find(f => f.id === 'city') 795 const cityField = dynamicFormFields.value.find(f => f.id === 'city')
689 if (cityField) { 796 if (cityField) {
...@@ -745,11 +852,11 @@ const onConfirmTask = async ({ selectedOptions }) => { ...@@ -745,11 +852,11 @@ const onConfirmTask = async ({ selectedOptions }) => {
745 // } 852 // }
746 // }) 853 // })
747 854
748 - 855 +const toggleTarget = item => {
749 -
750 -const toggleTarget = (item) => {
751 // 优先使用id匹配,如果id不存在,则使用name匹配 856 // 优先使用id匹配,如果id不存在,则使用name匹配
752 - const index = selectedTargets.value.findIndex(t => (item.id ? t.id === item.id : t.name === item.name)) 857 + const index = selectedTargets.value.findIndex(t =>
858 + item.id ? t.id === item.id : t.name === item.name
859 + )
753 if (index > -1) { 860 if (index > -1) {
754 // 取消选中 861 // 取消选中
755 selectedTargets.value.splice(index, 1) 862 selectedTargets.value.splice(index, 1)
...@@ -771,9 +878,9 @@ const toggleTarget = (item) => { ...@@ -771,9 +878,9 @@ const toggleTarget = (item) => {
771 * @description 重置编辑状态并显示弹窗 878 * @description 重置编辑状态并显示弹窗
772 */ 879 */
773 const openAddTargetDialog = () => { 880 const openAddTargetDialog = () => {
774 - editingTarget.value = null; // 重置编辑对象 881 + editingTarget.value = null // 重置编辑对象
775 - isConfirmMode.value = false; 882 + isConfirmMode.value = false
776 - showAddTargetDialog.value = true; 883 + showAddTargetDialog.value = true
777 } 884 }
778 885
779 /** 886 /**
...@@ -781,7 +888,7 @@ const openAddTargetDialog = () => { ...@@ -781,7 +888,7 @@ const openAddTargetDialog = () => {
781 * @description 处理弹窗确认事件,更新本地列表和选中状态 888 * @description 处理弹窗确认事件,更新本地列表和选中状态
782 * @param {Array} formFields - 表单字段数组,包含字段ID和值 889 * @param {Array} formFields - 表单字段数组,包含字段ID和值
783 */ 890 */
784 -const confirmAddTarget = async (formFields) => { 891 +const confirmAddTarget = formFields => {
785 // 将表单字段数组转换为对象 892 // 将表单字段数组转换为对象
786 const formData = formFields.reduce((acc, field) => { 893 const formData = formFields.reduce((acc, field) => {
787 if (field.id) { 894 if (field.id) {
...@@ -802,8 +909,9 @@ const confirmAddTarget = async (formFields) => { ...@@ -802,8 +909,9 @@ const confirmAddTarget = async (formFields) => {
802 } 909 }
803 910
804 // 检查是否在选中列表中 911 // 检查是否在选中列表中
805 - const selectedIndex = selectedTargets.value.findIndex(t => 912 + const selectedIndex = selectedTargets.value.findIndex(
806 - (editingTarget.value.id && t.id && t.id == editingTarget.value.id) || 913 + t =>
914 + (editingTarget.value.id && t.id && String(t.id) === String(editingTarget.value.id)) ||
807 (!editingTarget.value.id && t.name === editingTarget.value.name) 915 (!editingTarget.value.id && t.name === editingTarget.value.name)
808 ) 916 )
809 917
...@@ -818,7 +926,7 @@ const confirmAddTarget = async (formFields) => { ...@@ -818,7 +926,7 @@ const confirmAddTarget = async (formFields) => {
818 // 新增成功,更新本地列表 926 // 新增成功,更新本地列表
819 const newTarget = { 927 const newTarget = {
820 ...formData, 928 ...formData,
821 - has_confirmed: true // 新增的对象默认已确认 929 + has_confirmed: true, // 新增的对象默认已确认
822 } 930 }
823 targetList.value.push(newTarget) 931 targetList.value.push(newTarget)
824 // 默认勾选新增的对象 932 // 默认勾选新增的对象
...@@ -826,7 +934,7 @@ const confirmAddTarget = async (formFields) => { ...@@ -826,7 +934,7 @@ const confirmAddTarget = async (formFields) => {
826 showToast('新增成功') 934 showToast('新增成功')
827 } 935 }
828 936
829 - showAddTargetDialog.value = false; 937 + showAddTargetDialog.value = false
830 } 938 }
831 939
832 /** 940 /**
...@@ -834,7 +942,7 @@ const confirmAddTarget = async (formFields) => { ...@@ -834,7 +942,7 @@ const confirmAddTarget = async (formFields) => {
834 * @description 打开弹窗并填充当前对象数据进行编辑 942 * @description 打开弹窗并填充当前对象数据进行编辑
835 * @param {Object} item - 待编辑的计数对象 943 * @param {Object} item - 待编辑的计数对象
836 */ 944 */
837 -const handleTargetEdit = (item) => { 945 +const handleTargetEdit = item => {
838 editingTarget.value = item 946 editingTarget.value = item
839 isConfirmMode.value = false // 明确设置为非确认模式 947 isConfirmMode.value = false // 明确设置为非确认模式
840 showAddTargetDialog.value = true 948 showAddTargetDialog.value = true
...@@ -845,7 +953,7 @@ const handleTargetEdit = (item) => { ...@@ -845,7 +953,7 @@ const handleTargetEdit = (item) => {
845 * @description 从本地列表和选中列表中移除对象(暂未调用后端接口) 953 * @description 从本地列表和选中列表中移除对象(暂未调用后端接口)
846 * @param {Object} item - 待删除的计数对象 954 * @param {Object} item - 待删除的计数对象
847 */ 955 */
848 -const handleTargetDelete = async (item) => { 956 +const handleTargetDelete = async item => {
849 // 屏蔽删除功能, 那个接口也是不存在的 957 // 屏蔽删除功能, 那个接口也是不存在的
850 // const { code } = await gratitudeDeleteAPI({ id: item.id }) 958 // const { code } = await gratitudeDeleteAPI({ id: item.id })
851 // if (code === 1) { 959 // if (code === 1) {
...@@ -854,13 +962,11 @@ const handleTargetDelete = async (item) => { ...@@ -854,13 +962,11 @@ const handleTargetDelete = async (item) => {
854 // if (targetIndex > -1) { 962 // if (targetIndex > -1) {
855 // targetList.value.splice(targetIndex, 1) 963 // targetList.value.splice(targetIndex, 1)
856 // } 964 // }
857 -
858 // // 从选中列表中也删除 965 // // 从选中列表中也删除
859 // const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) 966 // const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id)
860 // if (selectedIndex > -1) { 967 // if (selectedIndex > -1) {
861 // selectedTargets.value.splice(selectedIndex, 1) 968 // selectedTargets.value.splice(selectedIndex, 1)
862 // } 969 // }
863 -
864 // showToast('删除成功') 970 // showToast('删除成功')
865 // } 971 // }
866 } 972 }
...@@ -887,10 +993,9 @@ const isSubmitDisabled = computed(() => { ...@@ -887,10 +993,9 @@ const isSubmitDisabled = computed(() => {
887 if (activeType.value === 'text') { 993 if (activeType.value === 'text') {
888 // 文本打卡:必须填写内容且长度不少于10个字符 994 // 文本打卡:必须填写内容且长度不少于10个字符
889 return !message.value.trim() || message.value.trim().length < 10 995 return !message.value.trim() || message.value.trim().length < 10
890 - } else { 996 + }
891 // 其他类型:必须有文件 (如果是混合模式,只要有文件就行) 997 // 其他类型:必须有文件 (如果是混合模式,只要有文件就行)
892 return fileList.value.length === 0 998 return fileList.value.length === 0
893 - }
894 }) 999 })
895 1000
896 /** 1001 /**
...@@ -902,7 +1007,8 @@ const handleSubmit = async () => { ...@@ -902,7 +1007,8 @@ const handleSubmit = async () => {
902 // 计数打卡校验 1007 // 计数打卡校验
903 if (taskType.value === 'count') { 1008 if (taskType.value === 'count') {
904 if (selectedTaskValue.value.length === 0) { 1009 if (selectedTaskValue.value.length === 0) {
905 - const taskText = taskOptions.value.find(t => t.value === selectedTaskValue.value[0])?.text || '作业' 1010 + const taskText =
1011 + taskOptions.value.find(t => t.value === selectedTaskValue.value[0])?.text || '作业'
906 showToast(`请选择${taskText}`) 1012 showToast(`请选择${taskText}`)
907 return 1013 return
908 } 1014 }
...@@ -914,7 +1020,7 @@ const handleSubmit = async () => { ...@@ -914,7 +1020,7 @@ const handleSubmit = async () => {
914 } 1020 }
915 1021
916 const extraData = { 1022 const extraData = {
917 - subtask_id: selectedTaskValue.value.length > 0 ? selectedTaskValue.value[0] : '' 1023 + subtask_id: selectedTaskValue.value.length > 0 ? selectedTaskValue.value[0] : '',
918 } 1024 }
919 1025
920 // 如果是计数打卡,添加选中的计数对象列表, 并添加次数 1026 // 如果是计数打卡,添加选中的计数对象列表, 并添加次数
...@@ -933,13 +1039,9 @@ const handleSubmit = async () => { ...@@ -933,13 +1039,9 @@ const handleSubmit = async () => {
933 await onSubmit(extraData, onSuccess) 1039 await onSubmit(extraData, onSuccess)
934 } 1040 }
935 1041
936 -
937 -
938 // 是否为编辑模式 1042 // 是否为编辑模式
939 const isEditMode = computed(() => route.query.status === 'edit') 1043 const isEditMode = computed(() => route.query.status === 'edit')
940 1044
941 -
942 -
943 /** 1045 /**
944 * 返回上一页 1046 * 返回上一页
945 */ 1047 */
...@@ -952,12 +1054,12 @@ const onClickLeft = () => { ...@@ -952,12 +1054,12 @@ const onClickLeft = () => {
952 * @param {string} type - 打卡类型 1054 * @param {string} type - 打卡类型
953 * @returns {string} 图标名称 1055 * @returns {string} 图标名称
954 */ 1056 */
955 -const getIconName = (type) => { 1057 +const getIconName = type => {
956 const iconMap = { 1058 const iconMap = {
957 - 'text': 'edit', 1059 + text: 'edit',
958 - 'image': 'photo', 1060 + image: 'photo',
959 - 'video': 'video', 1061 + video: 'video',
960 - 'audio': 'music' 1062 + audio: 'music',
961 } 1063 }
962 return iconMap[type] || 'edit' 1064 return iconMap[type] || 'edit'
963 } 1065 }
...@@ -968,9 +1070,9 @@ const getIconName = (type) => { ...@@ -968,9 +1070,9 @@ const getIconName = (type) => {
968 */ 1070 */
969 const getFileIcon = () => { 1071 const getFileIcon = () => {
970 const iconMap = { 1072 const iconMap = {
971 - 'image': 'photo', 1073 + image: 'photo',
972 - 'video': 'video', 1074 + video: 'video',
973 - 'audio': 'music' 1075 + audio: 'music',
974 } 1076 }
975 return iconMap[activeType.value] || 'description' 1077 return iconMap[activeType.value] || 'description'
976 } 1078 }
...@@ -981,9 +1083,9 @@ const getFileIcon = () => { ...@@ -981,9 +1083,9 @@ const getFileIcon = () => {
981 */ 1083 */
982 const getAcceptType = () => { 1084 const getAcceptType = () => {
983 const acceptMap = { 1085 const acceptMap = {
984 - 'image': 'image/*', 1086 + image: 'image/*',
985 - 'video': '.mp4,video/mp4', 1087 + video: '.mp4,video/mp4',
986 - 'audio': '.mp3,.m4a,.aac,.wav' 1088 + audio: '.mp3,.m4a,.aac,.wav',
987 } 1089 }
988 return acceptMap[activeType.value] || '*' 1090 return acceptMap[activeType.value] || '*'
989 } 1091 }
...@@ -994,9 +1096,9 @@ const getAcceptType = () => { ...@@ -994,9 +1096,9 @@ const getAcceptType = () => {
994 */ 1096 */
995 const getUploadTips = () => { 1097 const getUploadTips = () => {
996 const tipsMap = { 1098 const tipsMap = {
997 - 'image': '支持格式:.jpg/.jpeg/.png', 1099 + image: '支持格式:.jpg/.jpeg/.png',
998 - 'video': '支持格式:.mp4(不支持 .mov)', 1100 + video: '支持格式:.mp4(不支持 .mov)',
999 - 'audio': '支持格式:.mp3/.m4a/.aac/.wav(不支持 .wma)' 1101 + audio: '支持格式:.mp3/.m4a/.aac/.wav(不支持 .wma)',
1000 } 1102 }
1001 return tipsMap[activeType.value] || '' 1103 return tipsMap[activeType.value] || ''
1002 } 1104 }
...@@ -1005,7 +1107,7 @@ const getUploadTips = () => { ...@@ -1005,7 +1107,7 @@ const getUploadTips = () => {
1005 * 获取任务详情 1107 * 获取任务详情
1006 * @param {string} month - 月份 1108 * @param {string} month - 月份
1007 */ 1109 */
1008 -const getTaskDetail = async (month) => { 1110 +const getTaskDetail = async month => {
1009 const { code, data } = await getTaskDetailAPI({ i: route.query.task_id, month }) 1111 const { code, data } = await getTaskDetailAPI({ i: route.query.task_id, month })
1010 if (code === 1) { 1112 if (code === 1) {
1011 taskDetail.value = data 1113 taskDetail.value = data
...@@ -1016,7 +1118,7 @@ const getTaskDetail = async (month) => { ...@@ -1016,7 +1118,7 @@ const getTaskDetail = async (month) => {
1016 * 更新附件类型选项 1118 * 更新附件类型选项
1017 * @param {Array|Object} attachmentType - 附件类型数据 1119 * @param {Array|Object} attachmentType - 附件类型数据
1018 */ 1120 */
1019 -const updateAttachmentTypeOptions = (attachmentType) => { 1121 +const updateAttachmentTypeOptions = attachmentType => {
1020 const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig(attachmentType) 1122 const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig(attachmentType)
1021 attachmentTypeOptions.value = options 1123 attachmentTypeOptions.value = options
1022 1124
...@@ -1027,7 +1129,9 @@ const updateAttachmentTypeOptions = (attachmentType) => { ...@@ -1027,7 +1129,9 @@ const updateAttachmentTypeOptions = (attachmentType) => {
1027 1129
1028 // 如果是计数打卡(count),过滤掉文本(text)类型 1130 // 如果是计数打卡(count),过滤掉文本(text)类型
1029 if (taskType.value === 'count') { 1131 if (taskType.value === 'count') {
1030 - attachmentTypeOptions.value = attachmentTypeOptions.value.filter(option => option.key !== 'text') 1132 + attachmentTypeOptions.value = attachmentTypeOptions.value.filter(
1133 + option => option.key !== 'text'
1134 + )
1031 } 1135 }
1032 1136
1033 // 设置默认选中类型(非计数打卡模式下) 1137 // 设置默认选中类型(非计数打卡模式下)
...@@ -1044,82 +1148,60 @@ const updateAttachmentTypeOptions = (attachmentType) => { ...@@ -1044,82 +1148,60 @@ const updateAttachmentTypeOptions = (attachmentType) => {
1044 * @param {Object} file - 文件对象 1148 * @param {Object} file - 文件对象
1045 * @param {Object} detail - 详细信息 1149 * @param {Object} detail - 详细信息
1046 */ 1150 */
1047 -const onClickPreview = (file, detail) => { 1151 +const onClickPreview = file => {
1048 - console.log('onClickPreview - file:', file)
1049 - console.log('onClickPreview - detail:', detail)
1050 - console.log('file对象的所有属性:', Object.keys(file))
1051 -
1052 const fileName = file.name || file.file?.name || '' 1152 const fileName = file.name || file.file?.name || ''
1053 -
1054 - // 尝试多种方式获取文件URL
1055 let fileUrl = '' 1153 let fileUrl = ''
1056 1154
1057 - // 方式1: 直接从file对象获取
1058 if (file.url) { 1155 if (file.url) {
1059 fileUrl = file.url 1156 fileUrl = file.url
1060 - console.log('从file.url获取URL:', fileUrl) 1157 + } else if (file.content) {
1061 - }
1062 - // 方式2: 从file.content获取
1063 - else if (file.content) {
1064 fileUrl = file.content 1158 fileUrl = file.content
1065 - console.log('从file.content获取URL:', fileUrl) 1159 + } else if (file.objectURL) {
1066 - }
1067 - // 方式3: 从file.objectURL获取
1068 - else if (file.objectURL) {
1069 fileUrl = file.objectURL 1160 fileUrl = file.objectURL
1070 - console.log('从file.objectURL获取URL:', fileUrl) 1161 + } else if (file.file) {
1071 - }
1072 - // 方式4: 从file.file获取
1073 - else if (file.file) {
1074 if (file.file.url) { 1162 if (file.file.url) {
1075 fileUrl = file.file.url 1163 fileUrl = file.file.url
1076 - console.log('从file.file.url获取URL:', fileUrl)
1077 } else { 1164 } else {
1078 - // 创建临时URL
1079 try { 1165 try {
1080 fileUrl = URL.createObjectURL(file.file) 1166 fileUrl = URL.createObjectURL(file.file)
1081 - console.log('通过URL.createObjectURL创建URL:', fileUrl)
1082 } catch (error) { 1167 } catch (error) {
1083 console.error('创建ObjectURL失败:', error) 1168 console.error('创建ObjectURL失败:', error)
1084 } 1169 }
1085 } 1170 }
1086 - } 1171 + } else {
1087 - // 方式5: 检查是否有其他可能的URL字段
1088 - else {
1089 const possibleUrlFields = ['src', 'path', 'value', 'href', 'link'] 1172 const possibleUrlFields = ['src', 'path', 'value', 'href', 'link']
1090 for (const field of possibleUrlFields) { 1173 for (const field of possibleUrlFields) {
1091 if (file[field]) { 1174 if (file[field]) {
1092 fileUrl = file[field] 1175 fileUrl = file[field]
1093 - console.log(`从file.${field}获取URL:`, fileUrl)
1094 break 1176 break
1095 } 1177 }
1096 } 1178 }
1097 } 1179 }
1098 1180
1099 - console.log('最终提取的文件名:', fileName)
1100 - console.log('最终提取的文件URL:', fileUrl)
1101 -
1102 if (!fileUrl) { 1181 if (!fileUrl) {
1103 console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(file, null, 2)) 1182 console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(file, null, 2))
1104 showToast('无法获取文件URL,请检查文件是否上传成功') 1183 showToast('无法获取文件URL,请检查文件是否上传成功')
1105 return 1184 return
1106 } 1185 }
1107 1186
1108 - // 根据打卡类型或文件扩展名判断文件类型 1187 + let finalFileType = file.file_type
1109 - const finalFileType = file.file_type || (isAudioFile(fileName) ? 'audio' : (isVideoFile(fileName) ? 'video' : 'image')) 1188 + if (!finalFileType) {
1189 + if (isAudioFile(fileName)) {
1190 + finalFileType = 'audio'
1191 + } else if (isVideoFile(fileName)) {
1192 + finalFileType = 'video'
1193 + } else {
1194 + finalFileType = 'image'
1195 + }
1196 + }
1110 1197
1111 if (finalFileType === 'audio') { 1198 if (finalFileType === 'audio') {
1112 - console.log('准备播放音频:', fileName, fileUrl)
1113 showAudio(fileName, fileUrl) 1199 showAudio(fileName, fileUrl)
1114 } else if (finalFileType === 'video') { 1200 } else if (finalFileType === 'video') {
1115 - console.log('准备播放视频:', fileName, fileUrl)
1116 showVideo(fileName, fileUrl) 1201 showVideo(fileName, fileUrl)
1117 } else if (finalFileType === 'image') { 1202 } else if (finalFileType === 'image') {
1118 - console.log('图片预览由van-uploader组件处理,跳过文件列表点击预览')
1119 - // 图片预览由van-uploader的@click-preview事件处理,避免重复弹出
1120 return 1203 return
1121 } else { 1204 } else {
1122 - console.log('该文件类型不支持预览,文件名:', fileName, '类型:', finalFileType)
1123 showToast('该文件类型不支持预览') 1205 showToast('该文件类型不支持预览')
1124 } 1206 }
1125 } 1207 }
...@@ -1214,7 +1296,7 @@ const onClickPreview = (file, detail) => { ...@@ -1214,7 +1296,7 @@ const onClickPreview = (file, detail) => {
1214 * @param {string} fileName - 文件名 1296 * @param {string} fileName - 文件名
1215 * @returns {boolean} 1297 * @returns {boolean}
1216 */ 1298 */
1217 -const isAudioFile = (fileName) => { 1299 +const isAudioFile = fileName => {
1218 const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.flac', '.wma'] 1300 const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.flac', '.wma']
1219 return audioExtensions.some(ext => fileName.toLowerCase().includes(ext)) 1301 return audioExtensions.some(ext => fileName.toLowerCase().includes(ext))
1220 } 1302 }
...@@ -1224,7 +1306,7 @@ const isAudioFile = (fileName) => { ...@@ -1224,7 +1306,7 @@ const isAudioFile = (fileName) => {
1224 * @param {string} fileName - 文件名 1306 * @param {string} fileName - 文件名
1225 * @returns {boolean} 1307 * @returns {boolean}
1226 */ 1308 */
1227 -const isVideoFile = (fileName) => { 1309 +const isVideoFile = fileName => {
1228 const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'] 1310 const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv']
1229 return videoExtensions.some(ext => fileName.toLowerCase().includes(ext)) 1311 return videoExtensions.some(ext => fileName.toLowerCase().includes(ext))
1230 } 1312 }
...@@ -1234,7 +1316,7 @@ const isVideoFile = (fileName) => { ...@@ -1234,7 +1316,7 @@ const isVideoFile = (fileName) => {
1234 * @param {string} fileName - 文件名 1316 * @param {string} fileName - 文件名
1235 * @returns {boolean} 1317 * @returns {boolean}
1236 */ 1318 */
1237 -const isImageFile = (fileName) => { 1319 +const isImageFile = fileName => {
1238 const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'] 1320 const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
1239 return imageExtensions.some(ext => fileName.toLowerCase().includes(ext)) 1321 return imageExtensions.some(ext => fileName.toLowerCase().includes(ext))
1240 } 1322 }
...@@ -1320,11 +1402,11 @@ const stopVideoPlay = () => { ...@@ -1320,11 +1402,11 @@ const stopVideoPlay = () => {
1320 */ 1402 */
1321 onMounted(async () => { 1403 onMounted(async () => {
1322 // 获取任务详情 1404 // 获取任务详情
1323 - const current_date = route.query.date; 1405 + const current_date = route.query.date
1324 if (current_date) { 1406 if (current_date) {
1325 - getTaskDetail(dayjs(current_date).format('YYYY-MM')); 1407 + getTaskDetail(dayjs(current_date).format('YYYY-MM'))
1326 } else { 1408 } else {
1327 - getTaskDetail(dayjs().format('YYYY-MM')); 1409 + getTaskDetail(dayjs().format('YYYY-MM'))
1328 } 1410 }
1329 1411
1330 // 初始化选中的子任务ID 1412 // 初始化选中的子任务ID
...@@ -1333,15 +1415,16 @@ onMounted(async () => { ...@@ -1333,15 +1415,16 @@ onMounted(async () => {
1333 // 获取小作业列表 1415 // 获取小作业列表
1334 const subtask_list = await getSubtaskListAPI({ task_id: route.query.task_id, date: current_date }) 1416 const subtask_list = await getSubtaskListAPI({ task_id: route.query.task_id, date: current_date })
1335 if (subtask_list.code === 1) { 1417 if (subtask_list.code === 1) {
1336 - taskOptions.value = [...subtask_list.data.map(item => ({ 1418 + taskOptions.value = [
1337 - text: item.is_makeup ? '补卡:' + item.title : item.title, 1419 + ...subtask_list.data.map(item => ({
1420 + text: item.is_makeup ? `补卡:${item.title}` : item.title,
1338 value: item.id, 1421 value: item.id,
1339 note: item.note, // 作业描述 1422 note: item.note, // 作业描述
1340 is_makeup: item.is_makeup, // 是否为补录 1423 is_makeup: item.is_makeup, // 是否为补录
1341 field_list: item.field_list, // 动态字段列表 1424 field_list: item.field_list, // 动态字段列表
1342 person_type: item.person_type, // 打卡对象类型 1425 person_type: item.person_type, // 打卡对象类型
1343 attachment_type: item.attachment_type, // 附件类型 1426 attachment_type: item.attachment_type, // 附件类型
1344 - })) 1427 + })),
1345 ] 1428 ]
1346 } 1429 }
1347 1430
...@@ -1370,7 +1453,7 @@ onMounted(async () => { ...@@ -1370,7 +1453,7 @@ onMounted(async () => {
1370 1453
1371 // 初始化编辑数据 1454 // 初始化编辑数据
1372 await initEditData(taskOptions.value, { 1455 await initEditData(taskOptions.value, {
1373 - onTaskFound: (option) => { 1456 + onTaskFound: option => {
1374 updateDynamicFormFields(option) 1457 updateDynamicFormFields(option)
1375 // 更新附件类型选项 1458 // 更新附件类型选项
1376 if (option.attachment_type) { 1459 if (option.attachment_type) {
...@@ -1379,7 +1462,7 @@ onMounted(async () => { ...@@ -1379,7 +1462,7 @@ onMounted(async () => {
1379 updateAttachmentTypeOptions(taskDetail.value.attachment_type) 1462 updateAttachmentTypeOptions(taskDetail.value.attachment_type)
1380 } 1463 }
1381 }, 1464 },
1382 - ensureTargetList: async (id) => { 1465 + ensureTargetList: async id => {
1383 if (targetList.value.length === 0) { 1466 if (targetList.value.length === 0) {
1384 await fetchTargetList(id) 1467 await fetchTargetList(id)
1385 } 1468 }
...@@ -1390,9 +1473,9 @@ onMounted(async () => { ...@@ -1390,9 +1473,9 @@ onMounted(async () => {
1390 // selectedTargets.value = list 1473 // selectedTargets.value = list
1391 // } 1474 // }
1392 // }, 1475 // },
1393 - setCount: (val) => { 1476 + setCount: val => {
1394 countValue.value = val 1477 countValue.value = val
1395 - } 1478 + },
1396 }) 1479 })
1397 1480
1398 // 尝试恢复草稿 (非编辑模式) 1481 // 尝试恢复草稿 (非编辑模式)
...@@ -1439,14 +1522,16 @@ onMounted(async () => { ...@@ -1439,14 +1522,16 @@ onMounted(async () => {
1439 line-height: 1.6; 1522 line-height: 1.6;
1440 font-size: 0.95rem; 1523 font-size: 0.95rem;
1441 word-break: break-word; 1524 word-break: break-word;
1442 - overflow-wrap: break-word; 1525 + overflow-wrap: anywhere;
1443 width: 100%; 1526 width: 100%;
1444 box-sizing: border-box; 1527 box-sizing: border-box;
1445 - overflow: hidden;
1446 1528
1447 :deep(*) { 1529 :deep(*) {
1448 max-width: 100% !important; 1530 max-width: 100% !important;
1449 box-sizing: border-box; 1531 box-sizing: border-box;
1532 + white-space: normal !important;
1533 + overflow-wrap: anywhere;
1534 + word-break: break-word;
1450 } 1535 }
1451 1536
1452 :deep(img) { 1537 :deep(img) {
......