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>
Showing
1 changed file
with
1317 additions
and
1232 deletions
| ... | @@ -6,105 +6,163 @@ | ... | @@ -6,105 +6,163 @@ |
| 6 | * @Description: 用户打卡详情页 | 6 | * @Description: 用户打卡详情页 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <div class="checkin-detail-page"> | 9 | + <div class="checkin-detail-page"> |
| 10 | - <!-- 页面内容 --> | 10 | + <!-- 页面内容 --> |
| 11 | - <div class="page-content"> | 11 | + <div class="page-content"> |
| 12 | - <!-- 作业描述 --> | 12 | + <!-- 作业描述 --> |
| 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"> | 18 | + </div> |
| 19 | - 暂无作业描述 | 19 | + </div> |
| 20 | - </div> | 20 | + |
| 21 | - </div> | 21 | + <!-- 打卡内容区域 --> |
| 22 | + <div class="section-wrapper"> | ||
| 23 | + <div class="section-title">提交作业</div> | ||
| 24 | + <div class="section-content"> | ||
| 25 | + <!-- 作业选择区域 --> | ||
| 26 | + <div class="mb-4"> | ||
| 27 | + <!-- 编辑模式下直接显示文本 --> | ||
| 28 | + <div | ||
| 29 | + v-if="isEditMode" | ||
| 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> | ||
| 22 | </div> | 34 | </div> |
| 23 | 35 | ||
| 24 | - <!-- 打卡内容区域 --> | 36 | + <!-- 非编辑模式下显示选择框 --> |
| 25 | - <div class="section-wrapper"> | 37 | + <template v-else> |
| 26 | - <div class="section-title">提交作业</div> | 38 | + <van-field |
| 27 | - <div class="section-content"> | 39 | + v-model="selectedTaskText" |
| 28 | - <!-- 作业选择区域 --> | 40 | + is-link |
| 29 | - <div class="mb-4"> | 41 | + readonly |
| 30 | - <!-- 编辑模式下直接显示文本 --> | 42 | + label="选择作业" |
| 31 | - <div v-if="isEditMode" class="bg-gray-50 rounded-lg p-3 border border-gray-100 flex items-center justify-between"> | 43 | + placeholder="请选择本次打卡的作业" |
| 32 | - <span class="text-gray-700 font-medium">当前作业</span> | 44 | + @click="showTaskPicker = true" |
| 33 | - <span class="text-gray-900 font-bold">{{ selectedTaskText }}</span> | 45 | + class="rounded-lg border border-gray-100" |
| 34 | - </div> | 46 | + /> |
| 35 | - | 47 | + <van-popup v-model:show="showTaskPicker" round position="bottom"> |
| 36 | - <!-- 非编辑模式下显示选择框 --> | 48 | + <van-picker |
| 37 | - <template v-else> | 49 | + :columns="taskOptions" |
| 38 | - <van-field v-model="selectedTaskText" is-link readonly label="选择作业" placeholder="请选择本次打卡的作业" | 50 | + @cancel="showTaskPicker = false" |
| 39 | - @click="showTaskPicker = true" class="rounded-lg border border-gray-100" /> | 51 | + @confirm="onConfirmTask" |
| 40 | - <van-popup v-model:show="showTaskPicker" round position="bottom"> | 52 | + /> |
| 41 | - <van-picker :columns="taskOptions" @cancel="showTaskPicker = false" | 53 | + </van-popup> |
| 42 | - @confirm="onConfirmTask" /> | 54 | + </template> |
| 43 | - </van-popup> | 55 | + </div> |
| 44 | - </template> | 56 | + <!-- 计数对象 --> |
| 45 | - </div> | 57 | + <CheckinTargetList |
| 46 | - <!-- 计数对象 --> | 58 | + v-if="taskType === 'count' && selectedTaskValue && selectedTaskValue.length > 0" |
| 47 | - <CheckinTargetList | 59 | + :dynamic-field-text="dynamicFieldText" |
| 48 | - v-if="taskType === 'count' && selectedTaskValue && selectedTaskValue.length > 0" | 60 | + :target-list="targetList" |
| 49 | - :dynamic-field-text="dynamicFieldText" | 61 | + :selected-targets="selectedTargets" |
| 50 | - :target-list="targetList" | 62 | + @add="openAddTargetDialog" |
| 51 | - :selected-targets="selectedTargets" | 63 | + @toggle="toggleTarget" |
| 52 | - @add="openAddTargetDialog" | 64 | + @edit="handleTargetEdit" |
| 53 | - @toggle="toggleTarget" | 65 | + @delete="handleTargetDelete" |
| 54 | - @edit="handleTargetEdit" | 66 | + /> |
| 55 | - @delete="handleTargetDelete" | 67 | + |
| 56 | - /> | 68 | + <!-- 计数次数 --> |
| 57 | - | 69 | + <div |
| 58 | - <!-- 计数次数 --> | 70 | + v-if="taskType === 'count'" |
| 59 | - <div v-if="taskType === 'count'" | 71 | + class="mb-4 flex items-center justify-between rounded-lg bg-gray-50 p-3" |
| 60 | - class="mb-4 flex items-center justify-between bg-gray-50 p-3 rounded-lg"> | 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 |
| 63 | - </div> | 75 | + v-model="countValue" |
| 64 | - | 76 | + min="1" |
| 65 | - <!-- 新增计数对象弹框 --> | 77 | + integer |
| 66 | - <AddTargetDialog | 78 | + input-width="80px" |
| 67 | - v-model:show="showAddTargetDialog" | 79 | + button-size="28px" |
| 68 | - :title="editingTarget ? (isConfirmMode ? `确认${dynamicFieldText}项` : `编辑${dynamicFieldText}项`) : `添加${dynamicFieldText}项`" | 80 | + /> |
| 69 | - :fields="dynamicFormFields" | 81 | + </div> |
| 70 | - :initial-values="editingTarget" | 82 | + |
| 71 | - @confirm="confirmAddTarget" | 83 | + <!-- 新增计数对象弹框 --> |
| 72 | - /> | 84 | + <AddTargetDialog |
| 73 | - | 85 | + v-model:show="showAddTargetDialog" |
| 74 | - <!-- 文本输入区域 --> | 86 | + :title=" |
| 75 | - <div class="text-input-area"> | 87 | + editingTarget |
| 76 | - <van-field v-model="message" rows="6" autosize type="textarea" | 88 | + ? isConfirmMode |
| 77 | - :placeholder="taskType === 'count' ? '请输入留言(可选)' : (activeType === 'text' ? '请输入留言,至少需要10个字符' : '请输入留言(可选)')" /> | 89 | + ? `确认${dynamicFieldText}项` |
| 78 | - </div> | 90 | + : `编辑${dynamicFieldText}项` |
| 79 | - | 91 | + : `添加${dynamicFieldText}项` |
| 80 | - <!-- 类型选项卡 --> | 92 | + " |
| 81 | - <div class="checkin-tabs" v-if="selectedTaskValue.length > 0"> | 93 | + :fields="dynamicFormFields" |
| 82 | - <div class="tabs-header"> | 94 | + :initial-values="editingTarget" |
| 83 | - <div class="tab-title">{{ taskType === 'count' ? '附件类型(可选)' : '附件类型' }}</div> | 95 | + @confirm="confirmAddTarget" |
| 84 | - <div class="tabs-nav"> | 96 | + /> |
| 85 | - <div v-for="option in attachmentTypeOptions" :key="option.key" | 97 | + |
| 86 | - @click="switchType(option.key)" :class="['tab-item', 'relative', { | 98 | + <!-- 文本输入区域 --> |
| 87 | - active: activeType === option.key | 99 | + <div class="text-input-area"> |
| 88 | - }]"> | 100 | + <van-field |
| 89 | - <van-icon :name="getIconName(option.key)" size="1.2rem" /> | 101 | + v-model="message" |
| 90 | - <span class="tab-text">{{ option.value }}</span> | 102 | + rows="6" |
| 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"> --> | 103 | + autosize |
| 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"> | 104 | + type="textarea" |
| 93 | - {{ getTypeCount(option.key) }} | 105 | + :placeholder=" |
| 94 | - </div> | 106 | + taskType === 'count' |
| 107 | + ? '请输入留言(可选)' | ||
| 108 | + : activeType === 'text' | ||
| 109 | + ? '请输入留言,至少需要10个字符' | ||
| 110 | + : '请输入留言(可选)' | ||
| 111 | + " | ||
| 112 | + /> | ||
| 113 | + </div> | ||
| 114 | + | ||
| 115 | + <!-- 类型选项卡 --> | ||
| 116 | + <div class="checkin-tabs" v-if="selectedTaskValue.length > 0"> | ||
| 117 | + <div class="tabs-header"> | ||
| 118 | + <div class="tab-title"> | ||
| 119 | + {{ taskType === 'count' ? '附件类型(可选)' : '附件类型' }} | ||
| 120 | + </div> | ||
| 121 | + <div class="tabs-nav"> | ||
| 122 | + <div | ||
| 123 | + v-for="option in attachmentTypeOptions" | ||
| 124 | + :key="option.key" | ||
| 125 | + @click="switchType(option.key)" | ||
| 126 | + :class="[ | ||
| 127 | + 'tab-item', | ||
| 128 | + 'relative', | ||
| 129 | + { | ||
| 130 | + active: activeType === option.key, | ||
| 131 | + }, | ||
| 132 | + ]" | ||
| 133 | + > | ||
| 134 | + <van-icon :name="getIconName(option.key)" size="1.2rem" /> | ||
| 135 | + <span class="tab-text">{{ option.value }}</span> | ||
| 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"> --> | ||
| 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 | + > | ||
| 141 | + {{ getTypeCount(option.key) }} | ||
| 142 | + </div> | ||
| 95 | </div> | 143 | </div> |
| 96 | - </div> | 144 | + </div> |
| 97 | - </div> | 145 | + </div> |
| 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" |
| 105 | - | 153 | + :before-read="beforeReadGuard" |
| 106 | - <!-- 文件列表显示 --> | 154 | + :after-read="afterRead" |
| 107 | - <!-- <div v-if="fileList.length > 0" class="file-list"> | 155 | + @delete="onDelete" |
| 156 | + @click-preview="onClickPreview" | ||
| 157 | + multiple | ||
| 158 | + :accept="getAcceptType()" | ||
| 159 | + result-type="file" | ||
| 160 | + :deletable="true" | ||
| 161 | + upload-icon="plus" | ||
| 162 | + /> | ||
| 163 | + | ||
| 164 | + <!-- 文件列表显示 --> | ||
| 165 | + <!-- <div v-if="fileList.length > 0" class="file-list"> | ||
| 108 | <div v-for="(item, index) in fileList" :key="index" class="file-item"> | 166 | <div v-for="(item, index) in fileList" :key="index" class="file-item"> |
| 109 | <div class="file-info" @click="previewFile(item)"> | 167 | <div class="file-info" @click="previewFile(item)"> |
| 110 | <van-icon :name="getFileIcon()" size="1rem" /> | 168 | <van-icon :name="getFileIcon()" size="1rem" /> |
| ... | @@ -115,75 +173,123 @@ | ... | @@ -115,75 +173,123 @@ |
| 115 | </div> | 173 | </div> |
| 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"> |
| 120 | - <div class="tip-text">{{ getUploadTips() }}</div> | 178 | + 最多上传{{ maxCount }}个文件,每个不超过{{ maxFileSizeMb }}MB |
| 121 | - </div> | ||
| 122 | - </div> | ||
| 123 | - </div> | ||
| 124 | </div> | 179 | </div> |
| 180 | + <div class="tip-text">{{ getUploadTips() }}</div> | ||
| 181 | + </div> | ||
| 125 | </div> | 182 | </div> |
| 126 | - | 183 | + </div> |
| 127 | - <!-- 提交按钮 --> | ||
| 128 | - <div v-if="!taskDetail.is_finish || isEditMode" class="submit-area"> | ||
| 129 | - <van-button type="primary" block size="large" :loading="uploading" :disabled="isSubmitDisabled" @click="handleSubmit"> | ||
| 130 | - {{ isEditMode ? '保存修改' : '提交' }} | ||
| 131 | - </van-button> | ||
| 132 | - </div> | ||
| 133 | </div> | 184 | </div> |
| 185 | + </div> | ||
| 186 | + | ||
| 187 | + <!-- 提交按钮 --> | ||
| 188 | + <div v-if="!taskDetail.is_finish || isEditMode" class="submit-area"> | ||
| 189 | + <van-button | ||
| 190 | + type="primary" | ||
| 191 | + block | ||
| 192 | + size="large" | ||
| 193 | + :loading="uploading" | ||
| 194 | + :disabled="isSubmitDisabled" | ||
| 195 | + @click="handleSubmit" | ||
| 196 | + > | ||
| 197 | + {{ isEditMode ? '保存修改' : '提交' }} | ||
| 198 | + </van-button> | ||
| 199 | + </div> | ||
| 200 | + </div> | ||
| 134 | 201 | ||
| 135 | - <!-- 上传加载遮罩 --> | 202 | + <!-- 上传加载遮罩 --> |
| 136 | - <van-overlay :show="loading" z-index="9999"> | 203 | + <van-overlay :show="loading" z-index="9999"> |
| 137 | - <div class="loading-wrapper" @click.stop> | 204 | + <div class="loading-wrapper" @click.stop> |
| 138 | - <van-loading vertical color="#FFFFFF">上传中...</van-loading> | 205 | + <van-loading vertical color="#FFFFFF">上传中...</van-loading> |
| 139 | - </div> | 206 | + </div> |
| 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 |
| 144 | - <div class="p-4"> | 211 | + v-model:show="audioShow" |
| 145 | - <h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3> | 212 | + position="bottom" |
| 146 | - <AudioPlayer v-if="audioShow && audioUrl" :songs="[{ title: audioTitle, url: audioUrl }]" | 213 | + round |
| 147 | - class="w-full" /> | 214 | + closeable |
| 148 | - </div> | 215 | + :style="{ height: '60%', width: '100%' }" |
| 149 | - </van-popup> | 216 | + > |
| 150 | - | 217 | + <div class="p-4"> |
| 151 | - <!-- 视频播放器弹窗 --> | 218 | + <h3 class="mb-4 text-center text-lg font-medium">{{ audioTitle }}</h3> |
| 152 | - <van-popup v-model:show="videoShow" position="center" round closeable | 219 | + <AudioPlayer |
| 153 | - :style="{ width: '95%', maxHeight: '80vh' }" @close="stopVideoPlay"> | 220 | + v-if="audioShow && audioUrl" |
| 154 | - <div class="p-4"> | 221 | + :songs="[{ title: audioTitle, url: audioUrl }]" |
| 155 | - <h3 class="text-lg font-medium mb-4 text-center">视频预览</h3> | 222 | + class="w-full" |
| 156 | - <div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;"> | 223 | + /> |
| 157 | - <!-- 视频封面 --> | 224 | + </div> |
| 158 | - <div v-show="!isVideoPlaying" | 225 | + </van-popup> |
| 159 | - class="absolute inset-0 flex items-center justify-center cursor-pointer" | 226 | + |
| 160 | - @click="startVideoPlay"> | 227 | + <!-- 视频播放器弹窗 --> |
| 161 | - <img :src="videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" | 228 | + <van-popup |
| 162 | - :alt="videoTitle" class="w-full h-full object-cover" /> | 229 | + v-model:show="videoShow" |
| 163 | - <div class="absolute inset-0 flex items-center justify-center bg-black/20"> | 230 | + position="center" |
| 164 | - <div | 231 | + round |
| 165 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | 232 | + closeable |
| 166 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | 233 | + :style="{ width: '95%', maxHeight: '80vh' }" |
| 167 | - </div> | 234 | + @close="stopVideoPlay" |
| 168 | - </div> | 235 | + > |
| 169 | - </div> | 236 | + <div class="p-4"> |
| 170 | - <!-- 视频播放器 --> | 237 | + <h3 class="mb-4 text-center text-lg font-medium">视频预览</h3> |
| 171 | - <VideoPlayer v-if="isVideoPlaying" ref="videoPlayerRef" :video-url="videoUrl" | 238 | + <div class="relative w-full overflow-hidden rounded-lg bg-black" style="aspect-ratio: 16/9"> |
| 172 | - :video-id="videoTitle" :use-native-on-ios="false" :autoplay="false" class="w-full h-full" @play="handleVideoPlay" | 239 | + <!-- 视频封面 --> |
| 173 | - @pause="handleVideoPause" /> | 240 | + <div |
| 174 | - </div> | 241 | + v-show="!isVideoPlaying" |
| 242 | + class="absolute inset-0 flex cursor-pointer items-center justify-center" | ||
| 243 | + @click="startVideoPlay" | ||
| 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 | + /> | ||
| 250 | + <div class="absolute inset-0 flex items-center justify-center bg-black/20"> | ||
| 251 | + <div | ||
| 252 | + class="flex h-16 w-16 items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70" | ||
| 253 | + > | ||
| 254 | + <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 255 | + </div> | ||
| 175 | </div> | 256 | </div> |
| 176 | - </van-popup> | 257 | + </div> |
| 177 | - | 258 | + <!-- 视频播放器 --> |
| 178 | - <!-- 图片预览弹窗 --> | 259 | + <VideoPlayer |
| 179 | - <van-image-preview v-model:show="imageShow" :images="imageList" :start-position="imageIndex" :show-index="true" /> | 260 | + v-if="isVideoPlaying" |
| 180 | - </div> | 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 | + /> | ||
| 270 | + </div> | ||
| 271 | + </div> | ||
| 272 | + </van-popup> | ||
| 273 | + | ||
| 274 | + <!-- 图片预览弹窗 --> | ||
| 275 | + <van-image-preview | ||
| 276 | + v-model:show="imageShow" | ||
| 277 | + :images="imageList" | ||
| 278 | + :start-position="imageIndex" | ||
| 279 | + :show-index="true" | ||
| 280 | + /> | ||
| 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' |
| ... | @@ -204,51 +310,51 @@ useTitle('提交作业') | ... | @@ -204,51 +310,51 @@ useTitle('提交作业') |
| 204 | 310 | ||
| 205 | // 使用打卡composable | 311 | // 使用打卡composable |
| 206 | const { | 312 | const { |
| 207 | - uploading, | 313 | + uploading, |
| 208 | - loading, | 314 | + loading, |
| 209 | - message, | 315 | + message, |
| 210 | - fileList, | 316 | + fileList, |
| 211 | - activeType, | 317 | + activeType, |
| 212 | - multiAttachmentEnabled, | 318 | + multiAttachmentEnabled, |
| 213 | - subTaskId, | 319 | + subTaskId, |
| 214 | - selectedTaskText, | 320 | + selectedTaskText, |
| 215 | - selectedTaskValue, | 321 | + selectedTaskValue, |
| 216 | - isMakeup, | 322 | + isMakeup, |
| 217 | - maxCount, | 323 | + maxCount, |
| 218 | - maxFileSizeMb, | 324 | + maxFileSizeMb, |
| 219 | - canSubmit, | 325 | + canSubmit, |
| 220 | - setMaxFileSizeMbMap, | 326 | + setMaxFileSizeMbMap, |
| 221 | - beforeRead, | 327 | + beforeRead, |
| 222 | - afterRead, | 328 | + afterRead, |
| 223 | - onDelete, | 329 | + onDelete, |
| 224 | - delItem, | 330 | + delItem, |
| 225 | - onSubmit, | 331 | + onSubmit, |
| 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 |
| 233 | const { | 339 | const { |
| 234 | - is_enabled: isDraftEnabled, | 340 | + is_enabled: isDraftEnabled, |
| 235 | - build_key: buildDraftKey, | 341 | + build_key: buildDraftKey, |
| 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('感恩') |
| ... | @@ -297,249 +403,260 @@ const imageIndex = ref(0) | ... | @@ -297,249 +403,260 @@ const imageIndex = ref(0) |
| 297 | * @description 使用debounce防抖,避免频繁写入 | 403 | * @description 使用debounce防抖,避免频繁写入 |
| 298 | */ | 404 | */ |
| 299 | const autoSaveDraft = debounce(() => { | 405 | const autoSaveDraft = debounce(() => { |
| 300 | - if (!isDraftEnabled() || route.query.status === 'edit') return | 406 | + if (!isDraftEnabled() || route.query.status === 'edit') return |
| 301 | - | 407 | + |
| 302 | - const payload = { | 408 | + const payload = { |
| 303 | - message: message.value, | 409 | + message: message.value, |
| 304 | - active_type: activeType.value, | 410 | + active_type: activeType.value, |
| 305 | - subtask_id: selectedTaskValue.value?.[0], | 411 | + subtask_id: selectedTaskValue.value?.[0], |
| 306 | - selected_task_value: selectedTaskValue.value, | 412 | + selected_task_value: selectedTaskValue.value, |
| 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(() => { |
| 324 | - autoSaveDraft() | 434 | + autoSaveDraft() |
| 325 | - autoSaveDraft.flush() // 立即执行待处理的保存 | 435 | + autoSaveDraft.flush() // 立即执行待处理的保存 |
| 326 | }) | 436 | }) |
| 327 | 437 | ||
| 328 | // 页面卸载前保存(兼容刷新/关闭tab) | 438 | // 页面卸载前保存(兼容刷新/关闭tab) |
| 329 | const handleBeforeUnload = () => { | 439 | const handleBeforeUnload = () => { |
| 330 | - autoSaveDraft() | 440 | + autoSaveDraft() |
| 331 | - autoSaveDraft.flush() | 441 | + autoSaveDraft.flush() |
| 332 | } | 442 | } |
| 333 | window.addEventListener('beforeunload', handleBeforeUnload) | 443 | window.addEventListener('beforeunload', handleBeforeUnload) |
| 334 | onBeforeUnmount(() => { | 444 | onBeforeUnmount(() => { |
| 335 | - window.removeEventListener('beforeunload', handleBeforeUnload) | 445 | + window.removeEventListener('beforeunload', handleBeforeUnload) |
| 336 | }) | 446 | }) |
| 337 | 447 | ||
| 338 | /** | 448 | /** |
| 339 | * 检查并恢复草稿 | 449 | * 检查并恢复草稿 |
| 340 | */ | 450 | */ |
| 341 | const checkAndRestoreDraft = async () => { | 451 | const checkAndRestoreDraft = async () => { |
| 342 | - if (!isDraftEnabled() || route.query.status === 'edit') return | 452 | + if (!isDraftEnabled() || route.query.status === 'edit') return |
| 343 | - | 453 | + |
| 344 | - // 先清理过期草稿 | 454 | + // 先清理过期草稿 |
| 345 | - cleanupExpiredDrafts() | 455 | + cleanupExpiredDrafts() |
| 346 | - | 456 | + |
| 347 | - const draft = readDraft(draftKey.value) | 457 | + const draft = readDraft(draftKey.value) |
| 348 | - if (!draft || !draft.payload) return | 458 | + if (!draft || !draft.payload) return |
| 349 | - | 459 | + |
| 350 | - const { payload } = draft | 460 | + const { payload } = draft |
| 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 = |
| 355 | - if (draftSubtaskId) { | 465 | + payload.subtask_id || (payload.selected_task_value && payload.selected_task_value[0]) |
| 356 | - // taskOptions 已经在 onMounted 中加载完毕 | 466 | + if (draftSubtaskId) { |
| 357 | - const isValidTask = taskOptions.value.some(option => option.value == draftSubtaskId) | 467 | + // taskOptions 已经在 onMounted 中加载完毕 |
| 358 | - | 468 | + const isValidTask = taskOptions.value.some( |
| 359 | - if (!isValidTask) { | 469 | + option => String(option.value) === String(draftSubtaskId) |
| 360 | - console.log('[草稿清理] 作业已失效,中断恢复流程', draftSubtaskId) | 470 | + ) |
| 361 | - try { | 471 | + |
| 362 | - await showDialog({ | 472 | + if (!isValidTask) { |
| 363 | - title: '草稿已失效', | 473 | + console.warn('[草稿清理] 作业已失效,中断恢复流程', draftSubtaskId) |
| 364 | - message: '您之前暂存的作业已失效(可能已截止或被删除),无法恢复。', | 474 | + try { |
| 365 | - confirmButtonText: '清空草稿', | 475 | + await showDialog({ |
| 366 | - theme: 'round-button', | 476 | + title: '草稿已失效', |
| 367 | - }) | 477 | + message: '您之前暂存的作业已失效(可能已截止或被删除),无法恢复。', |
| 368 | - } catch (e) { | 478 | + confirmButtonText: '清空草稿', |
| 369 | - // 用户点击确认或其他关闭操作 | 479 | + theme: 'round-button', |
| 370 | - } finally { | 480 | + }) |
| 371 | - clearDraft(draftKey.value) | 481 | + } catch (e) { |
| 372 | - } | 482 | + // 用户点击确认或其他关闭操作 |
| 373 | - return | 483 | + } finally { |
| 374 | - } | 484 | + clearDraft(draftKey.value) |
| 485 | + } | ||
| 486 | + return | ||
| 375 | } | 487 | } |
| 488 | + } | ||
| 376 | 489 | ||
| 377 | - // 检查是否有实质内容 | 490 | + // 检查是否有实质内容 |
| 378 | - const hasContent = (payload.message && payload.message.trim()) || | 491 | + const hasContent = |
| 379 | - (payload.file_list && payload.file_list.length > 0) | 492 | + (payload.message && payload.message.trim()) || |
| 493 | + (payload.file_list && payload.file_list.length > 0) | ||
| 380 | 494 | ||
| 381 | - if (!hasContent) return | 495 | + if (!hasContent) return |
| 382 | 496 | ||
| 383 | - try { | 497 | + try { |
| 384 | - await showConfirmDialog({ | 498 | + await showConfirmDialog({ |
| 385 | - title: '发现未提交的草稿', | 499 | + title: '发现未提交的草稿', |
| 386 | - message: '上次编辑的内容未提交,是否恢复?', | 500 | + message: '上次编辑的内容未提交,是否恢复?', |
| 387 | - confirmButtonText: '恢复', | 501 | + confirmButtonText: '恢复', |
| 388 | - cancelButtonText: '丢弃' | 502 | + cancelButtonText: '丢弃', |
| 389 | - }) | 503 | + }) |
| 390 | - | 504 | + // 确认恢复 |
| 391 | - // 确认恢复 | 505 | + |
| 392 | - console.log('[草稿恢复] 开始恢复数据', payload) | 506 | + if (payload.message) message.value = payload.message |
| 393 | - | 507 | + if (payload.active_type) activeType.value = payload.active_type |
| 394 | - if (payload.message) message.value = payload.message | 508 | + |
| 395 | - if (payload.active_type) activeType.value = payload.active_type | 509 | + // 恢复文件列表 (注意:只恢复了已完成上传的元数据,无法恢复File对象,所以无法继续上传/断点续传) |
| 510 | + // 恢复后的文件状态设为done | ||
| 511 | + if (payload.file_list && payload.file_list.length > 0) { | ||
| 512 | + fileList.value = payload.file_list.map(f => ({ | ||
| 513 | + ...f, | ||
| 514 | + status: 'done', | ||
| 515 | + message: '已上传', | ||
| 516 | + })) | ||
| 517 | + } | ||
| 396 | 518 | ||
| 397 | - // 恢复文件列表 (注意:只恢复了已完成上传的元数据,无法恢复File对象,所以无法继续上传/断点续传) | 519 | + // 恢复选中的作业 |
| 398 | - // 恢复后的文件状态设为done | 520 | + if (payload.selected_task_value && payload.selected_task_value.length > 0) { |
| 399 | - if (payload.file_list && payload.file_list.length > 0) { | 521 | + selectedTaskValue.value = payload.selected_task_value |
| 400 | - fileList.value = payload.file_list.map(f => ({ | 522 | + // 触发联动逻辑 (如updateDynamicFormFields等),这部分在selectedTaskValue的watcher或onConfirmTask中处理 |
| 401 | - ...f, | 523 | + // 但这里直接赋值可能不会触发onConfirmTask的逻辑,需要手动处理部分联动 |
| 402 | - status: 'done', | ||
| 403 | - message: '已上传' | ||
| 404 | - })) | ||
| 405 | - } | ||
| 406 | 524 | ||
| 407 | - // 恢复选中的作业 | 525 | + // 等待taskOptions加载完毕后再匹配text |
| 408 | - if (payload.selected_task_value && payload.selected_task_value.length > 0) { | 526 | + const matchOption = () => { |
| 409 | - selectedTaskValue.value = payload.selected_task_value | 527 | + const option = taskOptions.value.find(o => o.value === selectedTaskValue.value[0]) |
| 410 | - // 触发联动逻辑 (如updateDynamicFormFields等),这部分在selectedTaskValue的watcher或onConfirmTask中处理 | 528 | + if (option) { |
| 411 | - // 但这里直接赋值可能不会触发onConfirmTask的逻辑,需要手动处理部分联动 | 529 | + selectedTaskText.value = option.text |
| 412 | - | 530 | + isMakeup.value = !!option.is_makeup |
| 413 | - // 等待taskOptions加载完毕后再匹配text | 531 | + personType.value = option.person_type |
| 414 | - const matchOption = () => { | 532 | + updateDynamicFormFields(option) |
| 415 | - const option = taskOptions.value.find(o => o.value === selectedTaskValue.value[0]) | 533 | + if (option.attachment_type) updateAttachmentTypeOptions(option.attachment_type) |
| 416 | - if (option) { | ||
| 417 | - selectedTaskText.value = option.text | ||
| 418 | - isMakeup.value = !!option.is_makeup | ||
| 419 | - personType.value = option.person_type | ||
| 420 | - updateDynamicFormFields(option) | ||
| 421 | - if (option.attachment_type) updateAttachmentTypeOptions(option.attachment_type) | ||
| 422 | - } | ||
| 423 | - } | ||
| 424 | - | ||
| 425 | - if (taskOptions.value.length > 0) { | ||
| 426 | - matchOption() | ||
| 427 | - } else { | ||
| 428 | - // 如果选项还没加载完,watch taskOptions | ||
| 429 | - const unwatch = watch(taskOptions, () => { | ||
| 430 | - matchOption() | ||
| 431 | - unwatch() | ||
| 432 | - }) | ||
| 433 | - } | ||
| 434 | } | 534 | } |
| 535 | + } | ||
| 536 | + | ||
| 537 | + if (taskOptions.value.length > 0) { | ||
| 538 | + matchOption() | ||
| 539 | + } else { | ||
| 540 | + // 如果选项还没加载完,watch taskOptions | ||
| 541 | + const unwatch = watch(taskOptions, () => { | ||
| 542 | + matchOption() | ||
| 543 | + unwatch() | ||
| 544 | + }) | ||
| 545 | + } | ||
| 546 | + } | ||
| 435 | 547 | ||
| 436 | - // 恢复计数 | 548 | + // 恢复计数 |
| 437 | - if (payload.count?.gratitude_count) { | 549 | + if (payload.count?.gratitude_count) { |
| 438 | - countValue.value = payload.count.gratitude_count | 550 | + countValue.value = payload.count.gratitude_count |
| 439 | - } | 551 | + } |
| 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 ( |
| 444 | - const savedList = payload.count.gratitude_form_list | 556 | + payload.count?.gratitude_form_list && |
| 445 | - | 557 | + Array.isArray(payload.count.gratitude_form_list) && |
| 446 | - // 如果有作业ID,先获取基础列表 | 558 | + payload.count.gratitude_form_list.length > 0 |
| 447 | - if (selectedTaskValue.value && selectedTaskValue.value.length > 0) { | 559 | + ) { |
| 448 | - // 等待 fetchTargetList 完成,然后覆盖默认选中的项 | 560 | + const savedList = payload.count.gratitude_form_list |
| 449 | - await fetchTargetList(selectedTaskValue.value[0]) | 561 | + |
| 450 | - | 562 | + // 如果有作业ID,先获取基础列表 |
| 451 | - // 使用草稿中的列表覆盖 selectedTargets (注意去重或合并策略) | 563 | + if (selectedTaskValue.value && selectedTaskValue.value.length > 0) { |
| 452 | - // 这里选择:完全信任草稿中的选中状态。 | 564 | + // 等待 fetchTargetList 完成,然后覆盖默认选中的项 |
| 453 | - // 但需要注意:草稿中的对象可能只包含 id/name,而 targetList 中有完整信息。 | 565 | + await fetchTargetList(selectedTaskValue.value[0]) |
| 454 | - // 最好是基于 targetList 重新构建 selectedTargets,如果 targetList 中没有(比如新增的),则直接使用草稿中的。 | ||
| 455 | - | ||
| 456 | - const restoredTargets = [] | ||
| 457 | - | ||
| 458 | - savedList.forEach(savedItem => { | ||
| 459 | - // 尝试在 targetList 中找到对应项(获取最新状态/引用) | ||
| 460 | - const existingItem = targetList.value.find(t => | ||
| 461 | - (savedItem.id && t.id && t.id == savedItem.id) || | ||
| 462 | - (!savedItem.id && savedItem.name === t.name) | ||
| 463 | - ) | ||
| 464 | - | ||
| 465 | - if (existingItem) { | ||
| 466 | - existingItem.has_confirmed = true // 既然都在草稿里了,肯定是确认过的 | ||
| 467 | - restoredTargets.push(existingItem) | ||
| 468 | - } else { | ||
| 469 | - // 如果 targetList 里没有(可能是新增的,或者 targetList 变了),则直接使用草稿项 | ||
| 470 | - restoredTargets.push({ | ||
| 471 | - ...savedItem, | ||
| 472 | - has_confirmed: true | ||
| 473 | - }) | ||
| 474 | - // 同时也加到 targetList 里显示出来(如果是新增的) | ||
| 475 | - targetList.value.push(restoredTargets[restoredTargets.length - 1]) | ||
| 476 | - } | ||
| 477 | - }) | ||
| 478 | - | ||
| 479 | - selectedTargets.value = restoredTargets | ||
| 480 | - } else { | ||
| 481 | - // 如果没有作业ID(理论上不应该,因为计数打卡必须选作业),直接恢复 | ||
| 482 | - selectedTargets.value = savedList | ||
| 483 | - } | ||
| 484 | - } | ||
| 485 | 566 | ||
| 486 | - showToast('已恢复草稿') | 567 | + // 使用草稿中的列表覆盖 selectedTargets (注意去重或合并策略) |
| 568 | + // 这里选择:完全信任草稿中的选中状态。 | ||
| 569 | + // 但需要注意:草稿中的对象可能只包含 id/name,而 targetList 中有完整信息。 | ||
| 570 | + // 最好是基于 targetList 重新构建 selectedTargets,如果 targetList 中没有(比如新增的),则直接使用草稿中的。 | ||
| 571 | + | ||
| 572 | + const restoredTargets = [] | ||
| 573 | + | ||
| 574 | + savedList.forEach(savedItem => { | ||
| 575 | + // 尝试在 targetList 中找到对应项(获取最新状态/引用) | ||
| 576 | + const existingItem = targetList.value.find( | ||
| 577 | + t => | ||
| 578 | + (savedItem.id && t.id && String(t.id) === String(savedItem.id)) || | ||
| 579 | + (!savedItem.id && savedItem.name === t.name) | ||
| 580 | + ) | ||
| 581 | + | ||
| 582 | + if (existingItem) { | ||
| 583 | + existingItem.has_confirmed = true // 既然都在草稿里了,肯定是确认过的 | ||
| 584 | + restoredTargets.push(existingItem) | ||
| 585 | + } else { | ||
| 586 | + // 如果 targetList 里没有(可能是新增的,或者 targetList 变了),则直接使用草稿项 | ||
| 587 | + restoredTargets.push({ | ||
| 588 | + ...savedItem, | ||
| 589 | + has_confirmed: true, | ||
| 590 | + }) | ||
| 591 | + // 同时也加到 targetList 里显示出来(如果是新增的) | ||
| 592 | + targetList.value.push(restoredTargets[restoredTargets.length - 1]) | ||
| 593 | + } | ||
| 594 | + }) | ||
| 487 | 595 | ||
| 488 | - } catch (e) { | 596 | + selectedTargets.value = restoredTargets |
| 489 | - // 取消恢复,清除草稿 | 597 | + } else { |
| 490 | - if (e !== 'cancel') console.error(e) | 598 | + // 如果没有作业ID(理论上不应该,因为计数打卡必须选作业),直接恢复 |
| 491 | - clearDraft(draftKey.value) | 599 | + selectedTargets.value = savedList |
| 492 | - showToast('已丢弃草稿') | 600 | + } |
| 493 | } | 601 | } |
| 494 | -} | ||
| 495 | - | ||
| 496 | 602 | ||
| 497 | -const beforeReadGuard = (file) => { | 603 | + showToast('已恢复草稿') |
| 498 | - const files = Array.isArray(file) ? file : [file] | 604 | + } catch (e) { |
| 499 | - if (activeType.value === 'video') { | 605 | + // 取消恢复,清除草稿 |
| 500 | - const hasMov = files.some(item => { | 606 | + if (e !== 'cancel') console.error(e) |
| 501 | - const fileName = String(item?.name || '').toLowerCase() | 607 | + clearDraft(draftKey.value) |
| 502 | - const fileType = String(item?.type || '').toLowerCase() | 608 | + showToast('已丢弃草稿') |
| 503 | - return fileName.endsWith('.mov') || fileType.includes('quicktime') | 609 | + } |
| 504 | - }) | 610 | +} |
| 505 | 611 | ||
| 506 | - if (hasMov) { | 612 | +const beforeReadGuard = file => { |
| 507 | - showDialog({ | 613 | + const files = Array.isArray(file) ? file : [file] |
| 508 | - title: '不支持 MOV 格式', | 614 | + if (activeType.value === 'video') { |
| 509 | - message: 'MOV(QuickTime)在非苹果系统/部分播放器兼容性较差,可能出现无法打开、黑屏、无声等问题。\n\n请将视频导出/转换为 MP4(更通用)后再上传。', | 615 | + const hasMov = files.some(item => { |
| 510 | - confirmButtonText: '我知道了', | 616 | + const fileName = String(item?.name || '').toLowerCase() |
| 511 | - }) | 617 | + const fileType = String(item?.type || '').toLowerCase() |
| 512 | - return false | 618 | + return fileName.endsWith('.mov') || fileType.includes('quicktime') |
| 513 | - } | 619 | + }) |
| 514 | 620 | ||
| 515 | - return beforeRead(file) | 621 | + if (hasMov) { |
| 622 | + showDialog({ | ||
| 623 | + title: '不支持 MOV 格式', | ||
| 624 | + message: | ||
| 625 | + 'MOV(QuickTime)在非苹果系统/部分播放器兼容性较差,可能出现无法打开、黑屏、无声等问题。\n\n请将视频导出/转换为 MP4(更通用)后再上传。', | ||
| 626 | + confirmButtonText: '我知道了', | ||
| 627 | + }) | ||
| 628 | + return false | ||
| 516 | } | 629 | } |
| 517 | 630 | ||
| 518 | - if (activeType.value === 'audio') { | 631 | + return beforeRead(file) |
| 519 | - const supportedExt = ['mp3', 'm4a', 'aac', 'wav'] | 632 | + } |
| 520 | - const unsupportedFiles = files.filter(item => { | 633 | + |
| 521 | - const fileName = String(item?.name || '').toLowerCase() | 634 | + if (activeType.value === 'audio') { |
| 522 | - const ext = fileName.includes('.') ? fileName.split('.').pop() : '' | 635 | + const supportedExt = ['mp3', 'm4a', 'aac', 'wav'] |
| 523 | - if (supportedExt.includes(ext)) return false | 636 | + const unsupportedFiles = files.filter(item => { |
| 524 | - const fileType = String(item?.type || '').toLowerCase() | 637 | + const fileName = String(item?.name || '').toLowerCase() |
| 525 | - if (!fileType) return true | 638 | + const ext = fileName.includes('.') ? fileName.split('.').pop() : '' |
| 526 | - if (!fileType.startsWith('audio/')) return true | 639 | + if (supportedExt.includes(ext)) return false |
| 527 | - return true | 640 | + const fileType = String(item?.type || '').toLowerCase() |
| 528 | - }) | 641 | + if (!fileType) return true |
| 529 | - | 642 | + if (!fileType.startsWith('audio/')) return true |
| 530 | - if (unsupportedFiles.length > 0) { | 643 | + return true |
| 531 | - showDialog({ | 644 | + }) |
| 532 | - title: '不支持的音频格式', | ||
| 533 | - message: '当前音频播放基于系统浏览器能力,不同机型/系统对音频格式支持差异较大(例如 .wma 等常见无法播放)。\n\n为避免上传后无法播放,请使用 .mp3 或 .m4a(推荐)重新导出/转换后再上传。', | ||
| 534 | - confirmButtonText: '我知道了', | ||
| 535 | - }) | ||
| 536 | - return false | ||
| 537 | - } | ||
| 538 | 645 | ||
| 539 | - return beforeRead(file) | 646 | + if (unsupportedFiles.length > 0) { |
| 647 | + showDialog({ | ||
| 648 | + title: '不支持的音频格式', | ||
| 649 | + message: | ||
| 650 | + '当前音频播放基于系统浏览器能力,不同机型/系统对音频格式支持差异较大(例如 .wma 等常见无法播放)。\n\n为避免上传后无法播放,请使用 .mp3 或 .m4a(推荐)重新导出/转换后再上传。', | ||
| 651 | + confirmButtonText: '我知道了', | ||
| 652 | + }) | ||
| 653 | + return false | ||
| 540 | } | 654 | } |
| 541 | 655 | ||
| 542 | return beforeRead(file) | 656 | return beforeRead(file) |
| 657 | + } | ||
| 658 | + | ||
| 659 | + return beforeRead(file) | ||
| 543 | } | 660 | } |
| 544 | 661 | ||
| 545 | /** | 662 | /** |
| ... | @@ -547,20 +664,19 @@ const beforeReadGuard = (file) => { | ... | @@ -547,20 +664,19 @@ 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 | + } |
| 555 | - // 处理刚选择但未上传完成的文件(尝试推断类型) | 672 | + // 处理刚选择但未上传完成的文件(尝试推断类型) |
| 556 | - if (item.file && item.file.type) { | 673 | + if (item.file && item.file.type) { |
| 557 | - if (type === 'image') return item.file.type.startsWith('image/') | 674 | + if (type === 'image') return item.file.type.startsWith('image/') |
| 558 | - if (type === 'video') return item.file.type.startsWith('video/') | 675 | + if (type === 'video') return item.file.type.startsWith('video/') |
| 559 | - if (type === 'audio') return item.file.type.startsWith('audio/') | 676 | + if (type === 'audio') return item.file.type.startsWith('audio/') |
| 560 | - } | 677 | + } |
| 561 | - return false | 678 | + return false |
| 562 | - }).length | 679 | + }).length |
| 563 | -} | ||
| 564 | 680 | ||
| 565 | /** | 681 | /** |
| 566 | * 当前显示的(经过类型过滤的)文件列表 | 682 | * 当前显示的(经过类型过滤的)文件列表 |
| ... | @@ -569,139 +685,130 @@ const getTypeCount = (type) => { | ... | @@ -569,139 +685,130 @@ 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 | + } |
| 577 | - if (item.file && item.file.type) { | 693 | + if (item.file && item.file.type) { |
| 578 | - if (activeType.value === 'image') return item.file.type.startsWith('image/') | 694 | + if (activeType.value === 'image') return item.file.type.startsWith('image/') |
| 579 | - if (activeType.value === 'video') return item.file.type.startsWith('video/') | 695 | + if (activeType.value === 'video') return item.file.type.startsWith('video/') |
| 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) => { | 701 | + // 找出不属于当前视图的其他文件(保留它们) |
| 586 | - // 找出不属于当前视图的其他文件(保留它们) | 702 | + const otherFiles = fileList.value.filter(item => { |
| 587 | - const otherFiles = fileList.value.filter(item => { | 703 | + if (item.file_type) { |
| 588 | - if (item.file_type) { | 704 | + return item.file_type !== activeType.value |
| 589 | - return item.file_type !== activeType.value | 705 | + } |
| 590 | - } | 706 | + if (item.file && item.file.type) { |
| 591 | - if (item.file && item.file.type) { | 707 | + if (activeType.value === 'image') return !item.file.type.startsWith('image/') |
| 592 | - if (activeType.value === 'image') return !item.file.type.startsWith('image/') | 708 | + if (activeType.value === 'video') return !item.file.type.startsWith('video/') |
| 593 | - if (activeType.value === 'video') return !item.file.type.startsWith('video/') | 709 | + if (activeType.value === 'audio') return !item.file.type.startsWith('audio/') |
| 594 | - if (activeType.value === 'audio') return !item.file.type.startsWith('audio/') | 710 | + } |
| 595 | - } | 711 | + // 如果无法判断类型,且 activeType 不是 text,保守起见保留它? |
| 596 | - // 如果无法判断类型,且 activeType 不是 text,保守起见保留它? | 712 | + // 或者:如果 activeType 是 image,那么所有 image/ 相关的都算当前视图,非 image/ 的算 other |
| 597 | - // 或者:如果 activeType 是 image,那么所有 image/ 相关的都算当前视图,非 image/ 的算 other | 713 | + return true |
| 598 | - return true | 714 | + }) |
| 599 | - }) | ||
| 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 |
| 613 | - return Math.floor(size * 1024 * 1024) | 724 | + return Math.floor(size * 1024 * 1024) |
| 614 | }) | 725 | }) |
| 615 | 726 | ||
| 616 | // 显示的作业描述 | 727 | // 显示的作业描述 |
| 617 | const displayTaskNote = computed(() => { | 728 | const displayTaskNote = computed(() => { |
| 618 | - const selected_subtask_id = selectedTaskValue.value?.[0] | 729 | + const selected_subtask_id = selectedTaskValue.value?.[0] |
| 619 | - if (selected_subtask_id) { | 730 | + if (selected_subtask_id) { |
| 620 | - const option = taskOptions.value.find(o => o.value === selected_subtask_id) | 731 | + const option = taskOptions.value.find(o => o.value === selected_subtask_id) |
| 621 | - return option?.note || taskDetail.value?.note || '' | 732 | + return option?.note || taskDetail.value?.note || '' |
| 622 | - } | 733 | + } |
| 623 | - return taskDetail.value?.note || '' | 734 | + return taskDetail.value?.note || '' |
| 624 | }) | 735 | }) |
| 625 | 736 | ||
| 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 | - | 741 | + const { code, data } = await reuseGratitudeFormAPI({ subtask_id }) |
| 631 | -const fetchTargetList = async (subtask_id) => { | 742 | + if (code === 1) { |
| 632 | - const { code, data } = await reuseGratitudeFormAPI({ subtask_id }) | 743 | + targetList.value = data.gratitude_form_list || [] |
| 633 | - if (code === 1) { | 744 | + lastUsedTargetList.value = data.last_used_list || [] |
| 634 | - targetList.value = data.gratitude_form_list || [] | 745 | + |
| 635 | - lastUsedTargetList.value = data.last_used_list || [] | 746 | + // 自动选中上次使用的对象 |
| 636 | - | 747 | + if (lastUsedTargetList.value.length > 0) { |
| 637 | - // 自动选中上次使用的对象 | 748 | + // 找出 lastUsedTargetList 中存在于 targetList 的项(并获取 targetList 中的完整对象引用) |
| 638 | - if (lastUsedTargetList.value.length > 0) { | 749 | + const validTargets = [] |
| 639 | - // 找出 lastUsedTargetList 中存在于 targetList 的项(并获取 targetList 中的完整对象引用) | 750 | + |
| 640 | - const validTargets = [] | 751 | + lastUsedTargetList.value.forEach(lastItem => { |
| 641 | - | 752 | + const targetItem = targetList.value.find( |
| 642 | - lastUsedTargetList.value.forEach(lastItem => { | 753 | + t => |
| 643 | - const targetItem = targetList.value.find(t => | 754 | + (lastItem.id && t.id && String(t.id) === String(lastItem.id)) || |
| 644 | - (lastItem.id && t.id && t.id == lastItem.id) || | 755 | + (!lastItem.id && lastItem.name === t.name) |
| 645 | - (!lastItem.id && lastItem.name === t.name) | 756 | + ) |
| 646 | - ) | 757 | + |
| 647 | - | 758 | + if (targetItem) { |
| 648 | - if (targetItem) { | 759 | + // 标记为已确认,避免再次弹出确认框 |
| 649 | - // 标记为已确认,避免再次弹出确认框 | 760 | + targetItem.has_confirmed = true |
| 650 | - targetItem.has_confirmed = true | 761 | + validTargets.push(targetItem) |
| 651 | - validTargets.push(targetItem) | ||
| 652 | - } | ||
| 653 | - }) | ||
| 654 | - | ||
| 655 | - // 将这些项加入 selectedTargets(去重) | ||
| 656 | - validTargets.forEach(item => { | ||
| 657 | - const exists = selectedTargets.value.some(t => | ||
| 658 | - (item.id && t.id && t.id == item.id) || | ||
| 659 | - (!item.id && t.name === item.name) | ||
| 660 | - ) | ||
| 661 | - if (!exists) { | ||
| 662 | - selectedTargets.value.push(item) | ||
| 663 | - } | ||
| 664 | - }) | ||
| 665 | } | 762 | } |
| 763 | + }) | ||
| 764 | + | ||
| 765 | + // 将这些项加入 selectedTargets(去重) | ||
| 766 | + validTargets.forEach(item => { | ||
| 767 | + const exists = selectedTargets.value.some( | ||
| 768 | + t => | ||
| 769 | + (item.id && t.id && String(t.id) === String(item.id)) || | ||
| 770 | + (!item.id && t.name === item.name) | ||
| 771 | + ) | ||
| 772 | + if (!exists) { | ||
| 773 | + selectedTargets.value.push(item) | ||
| 774 | + } | ||
| 775 | + }) | ||
| 666 | } | 776 | } |
| 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 { | 789 | + id: field.field || field.field_name || field.name || field.id, // 兼容多种字段名 |
| 681 | - id: field.field || field.field_name || field.name || field.id, // 兼容多种字段名 | 790 | + label: field.label || '未命名', |
| 682 | - label: field.label || '未命名', | 791 | + type: field.type || 'text', // 默认类型,如果后端有类型字段可替换 |
| 683 | - type: field.type || 'text', // 默认类型,如果后端有类型字段可替换 | 792 | + required: true, // 默认必填,如果后端有必填字段可替换 |
| 684 | - required: true // 默认必填,如果后端有必填字段可替换 | 793 | + })) |
| 685 | - } | 794 | + // 确保如果有city字段,类型为textarea |
| 686 | - }) | 795 | + const cityField = dynamicFormFields.value.find(f => f.id === 'city') |
| 687 | - // 确保如果有city字段,类型为textarea | 796 | + if (cityField) { |
| 688 | - const cityField = dynamicFormFields.value.find(f => f.id === 'city') | 797 | + cityField.type = 'textarea' |
| 689 | - if (cityField) { | ||
| 690 | - cityField.type = 'textarea' | ||
| 691 | - } | ||
| 692 | - // 确保如果有unit字段,类型为textarea | ||
| 693 | - const unitField = dynamicFormFields.value.find(f => f.id === 'unit') | ||
| 694 | - if (unitField) { | ||
| 695 | - unitField.type = 'textarea' | ||
| 696 | - } | ||
| 697 | - } else { | ||
| 698 | - // 如果没有配置字段,使用默认字段 | ||
| 699 | - dynamicFormFields.value = [ | ||
| 700 | - { id: 'name', label: '姓名', type: 'text', required: true }, | ||
| 701 | - { id: 'city', label: '城市', type: 'textarea', required: true }, | ||
| 702 | - { id: 'unit', label: '单位', type: 'textarea', required: true }, | ||
| 703 | - ] | ||
| 704 | } | 798 | } |
| 799 | + // 确保如果有unit字段,类型为textarea | ||
| 800 | + const unitField = dynamicFormFields.value.find(f => f.id === 'unit') | ||
| 801 | + if (unitField) { | ||
| 802 | + unitField.type = 'textarea' | ||
| 803 | + } | ||
| 804 | + } else { | ||
| 805 | + // 如果没有配置字段,使用默认字段 | ||
| 806 | + dynamicFormFields.value = [ | ||
| 807 | + { id: 'name', label: '姓名', type: 'text', required: true }, | ||
| 808 | + { id: 'city', label: '城市', type: 'textarea', required: true }, | ||
| 809 | + { id: 'unit', label: '单位', type: 'textarea', required: true }, | ||
| 810 | + ] | ||
| 811 | + } | ||
| 705 | } | 812 | } |
| 706 | 813 | ||
| 707 | /** | 814 | /** |
| ... | @@ -711,30 +818,30 @@ const updateDynamicFormFields = (option) => { | ... | @@ -711,30 +818,30 @@ const updateDynamicFormFields = (option) => { |
| 711 | * @param {Array} param0.selectedOptions - 选中的选项数组 | 818 | * @param {Array} param0.selectedOptions - 选中的选项数组 |
| 712 | */ | 819 | */ |
| 713 | const onConfirmTask = async ({ selectedOptions }) => { | 820 | const onConfirmTask = async ({ selectedOptions }) => { |
| 714 | - const option = selectedOptions[0] | 821 | + const option = selectedOptions[0] |
| 715 | - selectedTaskText.value = option.text | 822 | + selectedTaskText.value = option.text |
| 716 | - selectedTaskValue.value = [option.value] | 823 | + selectedTaskValue.value = [option.value] |
| 717 | - isMakeup.value = !!option.is_makeup | 824 | + isMakeup.value = !!option.is_makeup |
| 718 | - showTaskPicker.value = false | 825 | + showTaskPicker.value = false |
| 719 | - personType.value = option.person_type | 826 | + personType.value = option.person_type |
| 720 | - | 827 | + |
| 721 | - // 更新动态表单字段 | 828 | + // 更新动态表单字段 |
| 722 | - updateDynamicFormFields(option) | 829 | + updateDynamicFormFields(option) |
| 723 | - | 830 | + |
| 724 | - // 更新附件类型选项 | 831 | + // 更新附件类型选项 |
| 725 | - if (option.attachment_type) { | 832 | + if (option.attachment_type) { |
| 726 | - updateAttachmentTypeOptions(option.attachment_type) | 833 | + updateAttachmentTypeOptions(option.attachment_type) |
| 727 | - } else { | 834 | + } else { |
| 728 | - // 如果小作业没有配置附件类型,尝试使用大作业的默认配置 | 835 | + // 如果小作业没有配置附件类型,尝试使用大作业的默认配置 |
| 729 | - updateAttachmentTypeOptions(taskDetail.value.attachment_type) | 836 | + updateAttachmentTypeOptions(taskDetail.value.attachment_type) |
| 730 | - } | 837 | + } |
| 731 | - | 838 | + |
| 732 | - // 如果是计数打卡,根据选中的作业ID查询计数对象 | 839 | + // 如果是计数打卡,根据选中的作业ID查询计数对象 |
| 733 | - if (taskType.value === 'count') { | 840 | + if (taskType.value === 'count') { |
| 734 | - // 切换作业时,清空之前选中的对象,避免混淆 | 841 | + // 切换作业时,清空之前选中的对象,避免混淆 |
| 735 | - selectedTargets.value = [] | 842 | + selectedTargets.value = [] |
| 736 | - await fetchTargetList(selectedTaskValue.value[0]) | 843 | + await fetchTargetList(selectedTaskValue.value[0]) |
| 737 | - } | 844 | + } |
| 738 | } | 845 | } |
| 739 | 846 | ||
| 740 | // 监听作业选择变化, 当选中的作业ID变化时, 查询对应的计数对象 | 847 | // 监听作业选择变化, 当选中的作业ID变化时, 查询对应的计数对象 |
| ... | @@ -745,25 +852,25 @@ const onConfirmTask = async ({ selectedOptions }) => { | ... | @@ -745,25 +852,25 @@ const onConfirmTask = async ({ selectedOptions }) => { |
| 745 | // } | 852 | // } |
| 746 | // }) | 853 | // }) |
| 747 | 854 | ||
| 748 | - | 855 | +const toggleTarget = item => { |
| 749 | - | 856 | + // 优先使用id匹配,如果id不存在,则使用name匹配 |
| 750 | -const toggleTarget = (item) => { | 857 | + const index = selectedTargets.value.findIndex(t => |
| 751 | - // 优先使用id匹配,如果id不存在,则使用name匹配 | 858 | + item.id ? t.id === item.id : t.name === item.name |
| 752 | - const index = selectedTargets.value.findIndex(t => (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) |
| 863 | + } else { | ||
| 864 | + // 选中逻辑:如果是第一次选中(未确认过),则弹出确认框 | ||
| 865 | + if (!item.has_confirmed) { | ||
| 866 | + editingTarget.value = item | ||
| 867 | + isConfirmMode.value = true | ||
| 868 | + showAddTargetDialog.value = true | ||
| 756 | } else { | 869 | } else { |
| 757 | - // 选中逻辑:如果是第一次选中(未确认过),则弹出确认框 | 870 | + // 已确认过,直接选中 |
| 758 | - if (!item.has_confirmed) { | 871 | + selectedTargets.value.push(item) |
| 759 | - editingTarget.value = item | ||
| 760 | - isConfirmMode.value = true | ||
| 761 | - showAddTargetDialog.value = true | ||
| 762 | - } else { | ||
| 763 | - // 已确认过,直接选中 | ||
| 764 | - selectedTargets.value.push(item) | ||
| 765 | - } | ||
| 766 | } | 872 | } |
| 873 | + } | ||
| 767 | } | 874 | } |
| 768 | 875 | ||
| 769 | /** | 876 | /** |
| ... | @@ -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,52 +888,53 @@ const openAddTargetDialog = () => { | ... | @@ -781,52 +888,53 @@ 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) { |
| 788 | - acc[field.id] = field.value | 895 | + acc[field.id] = field.value |
| 789 | - } | 896 | + } |
| 790 | - return acc | 897 | + return acc |
| 791 | - }, {}) | 898 | + }, {}) |
| 792 | - | 899 | + |
| 793 | - if (editingTarget.value) { | 900 | + if (editingTarget.value) { |
| 794 | - // 编辑模式或确认模式 | 901 | + // 编辑模式或确认模式 |
| 795 | - const index = targetList.value.findIndex(t => t === editingTarget.value) | 902 | + const index = targetList.value.findIndex(t => t === editingTarget.value) |
| 796 | - if (index > -1) { | 903 | + if (index > -1) { |
| 797 | - // 更新对象 (使用 Object.assign 保持引用) | 904 | + // 更新对象 (使用 Object.assign 保持引用) |
| 798 | - Object.assign(targetList.value[index], formData) | 905 | + Object.assign(targetList.value[index], formData) |
| 799 | - | 906 | + |
| 800 | - if (isConfirmMode.value) { | 907 | + if (isConfirmMode.value) { |
| 801 | - targetList.value[index].has_confirmed = true // 标记为已确认 | 908 | + targetList.value[index].has_confirmed = true // 标记为已确认 |
| 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 => |
| 807 | - (!editingTarget.value.id && t.name === editingTarget.value.name) | 914 | + (editingTarget.value.id && t.id && String(t.id) === String(editingTarget.value.id)) || |
| 808 | - ) | 915 | + (!editingTarget.value.id && t.name === editingTarget.value.name) |
| 809 | - | 916 | + ) |
| 810 | - // 如果是确认模式,确认后自动加入选中列表 | 917 | + |
| 811 | - if (isConfirmMode.value && selectedIndex === -1) { | 918 | + // 如果是确认模式,确认后自动加入选中列表 |
| 812 | - selectedTargets.value.push(targetList.value[index]) | 919 | + if (isConfirmMode.value && selectedIndex === -1) { |
| 813 | - } | 920 | + selectedTargets.value.push(targetList.value[index]) |
| 814 | - | 921 | + } |
| 815 | - showToast(isConfirmMode.value ? '确认成功' : '修改成功') | 922 | + |
| 816 | - } | 923 | + showToast(isConfirmMode.value ? '确认成功' : '修改成功') |
| 817 | - } else { | ||
| 818 | - // 新增成功,更新本地列表 | ||
| 819 | - const newTarget = { | ||
| 820 | - ...formData, | ||
| 821 | - has_confirmed: true // 新增的对象默认已确认 | ||
| 822 | - } | ||
| 823 | - targetList.value.push(newTarget) | ||
| 824 | - // 默认勾选新增的对象 | ||
| 825 | - selectedTargets.value.push(newTarget) | ||
| 826 | - showToast('新增成功') | ||
| 827 | } | 924 | } |
| 925 | + } else { | ||
| 926 | + // 新增成功,更新本地列表 | ||
| 927 | + const newTarget = { | ||
| 928 | + ...formData, | ||
| 929 | + has_confirmed: true, // 新增的对象默认已确认 | ||
| 930 | + } | ||
| 931 | + targetList.value.push(newTarget) | ||
| 932 | + // 默认勾选新增的对象 | ||
| 933 | + selectedTargets.value.push(newTarget) | ||
| 934 | + showToast('新增成功') | ||
| 935 | + } | ||
| 828 | 936 | ||
| 829 | - showAddTargetDialog.value = false; | 937 | + showAddTargetDialog.value = false |
| 830 | } | 938 | } |
| 831 | 939 | ||
| 832 | /** | 940 | /** |
| ... | @@ -834,10 +942,10 @@ const confirmAddTarget = async (formFields) => { | ... | @@ -834,10 +942,10 @@ 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 |
| 841 | } | 949 | } |
| 842 | 950 | ||
| 843 | /** | 951 | /** |
| ... | @@ -845,24 +953,22 @@ const handleTargetEdit = (item) => { | ... | @@ -845,24 +953,22 @@ 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) { |
| 852 | - // // 删除成功,更新本地列表 | 960 | + // // 删除成功,更新本地列表 |
| 853 | - // const targetIndex = targetList.value.findIndex(t => t.id === item.id) | 961 | + // const targetIndex = targetList.value.findIndex(t => t.id === item.id) |
| 854 | - // if (targetIndex > -1) { | 962 | + // if (targetIndex > -1) { |
| 855 | - // targetList.value.splice(targetIndex, 1) | 963 | + // targetList.value.splice(targetIndex, 1) |
| 856 | - // } | 964 | + // } |
| 857 | - | 965 | + // // 从选中列表中也删除 |
| 858 | - // // 从选中列表中也删除 | 966 | + // const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) |
| 859 | - // const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) | 967 | + // if (selectedIndex > -1) { |
| 860 | - // if (selectedIndex > -1) { | 968 | + // selectedTargets.value.splice(selectedIndex, 1) |
| 861 | - // selectedTargets.value.splice(selectedIndex, 1) | 969 | + // } |
| 862 | - // } | 970 | + // showToast('删除成功') |
| 863 | - | 971 | + // } |
| 864 | - // showToast('删除成功') | ||
| 865 | - // } | ||
| 866 | } | 972 | } |
| 867 | 973 | ||
| 868 | /** | 974 | /** |
| ... | @@ -871,26 +977,25 @@ const handleTargetDelete = async (item) => { | ... | @@ -871,26 +977,25 @@ const handleTargetDelete = async (item) => { |
| 871 | * @returns {boolean} | 977 | * @returns {boolean} |
| 872 | */ | 978 | */ |
| 873 | const isSubmitDisabled = computed(() => { | 979 | const isSubmitDisabled = computed(() => { |
| 874 | - // 1. 校验作业选择 | 980 | + // 1. 校验作业选择 |
| 875 | - if (!selectedTaskValue.value || selectedTaskValue.value.length === 0) return true | 981 | + if (!selectedTaskValue.value || selectedTaskValue.value.length === 0) return true |
| 876 | - | 982 | + |
| 877 | - // 2. 计数打卡特定校验 | 983 | + // 2. 计数打卡特定校验 |
| 878 | - if (taskType.value === 'count') { | 984 | + if (taskType.value === 'count') { |
| 879 | - // 必须选择至少一个对象 | 985 | + // 必须选择至少一个对象 |
| 880 | - if (selectedTargets.value.length === 0) return true | 986 | + if (selectedTargets.value.length === 0) return true |
| 881 | - // 次数必须大于0 | 987 | + // 次数必须大于0 |
| 882 | - if (!countValue.value || countValue.value <= 0) return true | 988 | + if (!countValue.value || countValue.value <= 0) return true |
| 883 | - return false | 989 | + return false |
| 884 | - } | 990 | + } |
| 885 | - | 991 | + |
| 886 | - // 3. 普通打卡校验 | 992 | + // 3. 普通打卡校验 |
| 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 | /** |
| ... | @@ -899,52 +1004,49 @@ const isSubmitDisabled = computed(() => { | ... | @@ -899,52 +1004,49 @@ const isSubmitDisabled = computed(() => { |
| 899 | * @returns {Promise<void>} | 1004 | * @returns {Promise<void>} |
| 900 | */ | 1005 | */ |
| 901 | const handleSubmit = async () => { | 1006 | 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 = |
| 906 | - showToast(`请选择${taskText}`) | 1011 | + taskOptions.value.find(t => t.value === selectedTaskValue.value[0])?.text || '作业' |
| 907 | - return | 1012 | + showToast(`请选择${taskText}`) |
| 908 | - } | 1013 | + return |
| 909 | - if (selectedTargets.value.length === 0) { | ||
| 910 | - const targetText = dynamicFieldText.value || '对象' | ||
| 911 | - showToast(`请选择${targetText}`) | ||
| 912 | - return | ||
| 913 | - } | ||
| 914 | } | 1014 | } |
| 915 | - | 1015 | + if (selectedTargets.value.length === 0) { |
| 916 | - const extraData = { | 1016 | + const targetText = dynamicFieldText.value || '对象' |
| 917 | - subtask_id: selectedTaskValue.value.length > 0 ? selectedTaskValue.value[0] : '' | 1017 | + showToast(`请选择${targetText}`) |
| 1018 | + return | ||
| 918 | } | 1019 | } |
| 919 | - | 1020 | + } |
| 920 | - // 如果是计数打卡,添加选中的计数对象列表, 并添加次数 | 1021 | + |
| 921 | - if (taskType.value === 'count') { | 1022 | + const extraData = { |
| 922 | - extraData.gratitude_form_list = selectedTargets.value | 1023 | + subtask_id: selectedTaskValue.value.length > 0 ? selectedTaskValue.value[0] : '', |
| 923 | - extraData.gratitude_count = countValue.value | 1024 | + } |
| 1025 | + | ||
| 1026 | + // 如果是计数打卡,添加选中的计数对象列表, 并添加次数 | ||
| 1027 | + if (taskType.value === 'count') { | ||
| 1028 | + extraData.gratitude_form_list = selectedTargets.value | ||
| 1029 | + extraData.gratitude_count = countValue.value | ||
| 1030 | + } | ||
| 1031 | + | ||
| 1032 | + // 提交成功后的回调,清除草稿 | ||
| 1033 | + const onSuccess = () => { | ||
| 1034 | + if (isDraftEnabled()) { | ||
| 1035 | + clearDraft(draftKey.value) | ||
| 924 | } | 1036 | } |
| 1037 | + } | ||
| 925 | 1038 | ||
| 926 | - // 提交成功后的回调,清除草稿 | 1039 | + await onSubmit(extraData, onSuccess) |
| 927 | - const onSuccess = () => { | ||
| 928 | - if (isDraftEnabled()) { | ||
| 929 | - clearDraft(draftKey.value) | ||
| 930 | - } | ||
| 931 | - } | ||
| 932 | - | ||
| 933 | - 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 | */ |
| 946 | const onClickLeft = () => { | 1048 | const onClickLeft = () => { |
| 947 | - router.back() | 1049 | + router.back() |
| 948 | } | 1050 | } |
| 949 | 1051 | ||
| 950 | /** | 1052 | /** |
| ... | @@ -952,14 +1054,14 @@ const onClickLeft = () => { | ... | @@ -952,14 +1054,14 @@ 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 | } |
| 964 | 1066 | ||
| 965 | /** | 1067 | /** |
| ... | @@ -967,12 +1069,12 @@ const getIconName = (type) => { | ... | @@ -967,12 +1069,12 @@ const getIconName = (type) => { |
| 967 | * @returns {string} 文件图标名称 | 1069 | * @returns {string} 文件图标名称 |
| 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 | } |
| 977 | 1079 | ||
| 978 | /** | 1080 | /** |
| ... | @@ -980,12 +1082,12 @@ const getFileIcon = () => { | ... | @@ -980,12 +1082,12 @@ const getFileIcon = () => { |
| 980 | * @returns {string} accept属性值 | 1082 | * @returns {string} accept属性值 |
| 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 | } |
| 990 | 1092 | ||
| 991 | /** | 1093 | /** |
| ... | @@ -993,50 +1095,52 @@ const getAcceptType = () => { | ... | @@ -993,50 +1095,52 @@ const getAcceptType = () => { |
| 993 | * @returns {string} 提示文本 | 1095 | * @returns {string} 提示文本 |
| 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 | } |
| 1003 | 1105 | ||
| 1004 | /** | 1106 | /** |
| 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 |
| 1012 | - } | 1114 | + } |
| 1013 | } | 1115 | } |
| 1014 | 1116 | ||
| 1015 | /** | 1117 | /** |
| 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 | + |
| 1023 | - // 设置最大文件大小映射 | 1125 | + // 设置最大文件大小映射 |
| 1024 | - if (upload_size_limit_mb_map) { | 1126 | + if (upload_size_limit_mb_map) { |
| 1025 | - setMaxFileSizeMbMap(upload_size_limit_mb_map) | 1127 | + setMaxFileSizeMbMap(upload_size_limit_mb_map) |
| 1026 | - } | 1128 | + } |
| 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( |
| 1031 | - } | 1133 | + option => option.key !== 'text' |
| 1032 | - | 1134 | + ) |
| 1033 | - // 设置默认选中类型(非计数打卡模式下) | 1135 | + } |
| 1034 | - if (taskType.value !== 'count' && attachmentTypeOptions.value.length > 0) { | 1136 | + |
| 1035 | - // 如果当前选中的类型不在新的选项中,则重置为第一个 | 1137 | + // 设置默认选中类型(非计数打卡模式下) |
| 1036 | - if (!activeType.value || !attachmentTypeOptions.value.find(o => o.key === activeType.value)) { | 1138 | + if (taskType.value !== 'count' && attachmentTypeOptions.value.length > 0) { |
| 1037 | - activeType.value = attachmentTypeOptions.value[0].key | 1139 | + // 如果当前选中的类型不在新的选项中,则重置为第一个 |
| 1038 | - } | 1140 | + if (!activeType.value || !attachmentTypeOptions.value.find(o => o.key === activeType.value)) { |
| 1141 | + activeType.value = attachmentTypeOptions.value[0].key | ||
| 1039 | } | 1142 | } |
| 1143 | + } | ||
| 1040 | } | 1144 | } |
| 1041 | 1145 | ||
| 1042 | /** | 1146 | /** |
| ... | @@ -1044,84 +1148,62 @@ const updateAttachmentTypeOptions = (attachmentType) => { | ... | @@ -1044,84 +1148,62 @@ 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) | 1152 | + const fileName = file.name || file.file?.name || '' |
| 1049 | - console.log('onClickPreview - detail:', detail) | 1153 | + let fileUrl = '' |
| 1050 | - console.log('file对象的所有属性:', Object.keys(file)) | 1154 | + |
| 1051 | - | 1155 | + if (file.url) { |
| 1052 | - const fileName = file.name || file.file?.name || '' | 1156 | + fileUrl = file.url |
| 1053 | - | 1157 | + } else if (file.content) { |
| 1054 | - // 尝试多种方式获取文件URL | 1158 | + fileUrl = file.content |
| 1055 | - let fileUrl = '' | 1159 | + } else if (file.objectURL) { |
| 1056 | - | 1160 | + fileUrl = file.objectURL |
| 1057 | - // 方式1: 直接从file对象获取 | 1161 | + } else if (file.file) { |
| 1058 | - if (file.url) { | 1162 | + if (file.file.url) { |
| 1059 | - fileUrl = file.url | 1163 | + fileUrl = file.file.url |
| 1060 | - console.log('从file.url获取URL:', fileUrl) | 1164 | + } else { |
| 1061 | - } | 1165 | + try { |
| 1062 | - // 方式2: 从file.content获取 | 1166 | + fileUrl = URL.createObjectURL(file.file) |
| 1063 | - else if (file.content) { | 1167 | + } catch (error) { |
| 1064 | - fileUrl = file.content | 1168 | + console.error('创建ObjectURL失败:', error) |
| 1065 | - console.log('从file.content获取URL:', fileUrl) | 1169 | + } |
| 1066 | - } | ||
| 1067 | - // 方式3: 从file.objectURL获取 | ||
| 1068 | - else if (file.objectURL) { | ||
| 1069 | - fileUrl = file.objectURL | ||
| 1070 | - console.log('从file.objectURL获取URL:', fileUrl) | ||
| 1071 | - } | ||
| 1072 | - // 方式4: 从file.file获取 | ||
| 1073 | - else if (file.file) { | ||
| 1074 | - if (file.file.url) { | ||
| 1075 | - fileUrl = file.file.url | ||
| 1076 | - console.log('从file.file.url获取URL:', fileUrl) | ||
| 1077 | - } else { | ||
| 1078 | - // 创建临时URL | ||
| 1079 | - try { | ||
| 1080 | - fileUrl = URL.createObjectURL(file.file) | ||
| 1081 | - console.log('通过URL.createObjectURL创建URL:', fileUrl) | ||
| 1082 | - } catch (error) { | ||
| 1083 | - console.error('创建ObjectURL失败:', error) | ||
| 1084 | - } | ||
| 1085 | - } | ||
| 1086 | - } | ||
| 1087 | - // 方式5: 检查是否有其他可能的URL字段 | ||
| 1088 | - else { | ||
| 1089 | - const possibleUrlFields = ['src', 'path', 'value', 'href', 'link'] | ||
| 1090 | - for (const field of possibleUrlFields) { | ||
| 1091 | - if (file[field]) { | ||
| 1092 | - fileUrl = file[field] | ||
| 1093 | - console.log(`从file.${field}获取URL:`, fileUrl) | ||
| 1094 | - break | ||
| 1095 | - } | ||
| 1096 | - } | ||
| 1097 | } | 1170 | } |
| 1098 | - | 1171 | + } else { |
| 1099 | - console.log('最终提取的文件名:', fileName) | 1172 | + const possibleUrlFields = ['src', 'path', 'value', 'href', 'link'] |
| 1100 | - console.log('最终提取的文件URL:', fileUrl) | 1173 | + for (const field of possibleUrlFields) { |
| 1101 | - | 1174 | + if (file[field]) { |
| 1102 | - if (!fileUrl) { | 1175 | + fileUrl = file[field] |
| 1103 | - console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(file, null, 2)) | 1176 | + break |
| 1104 | - showToast('无法获取文件URL,请检查文件是否上传成功') | 1177 | + } |
| 1105 | - return | ||
| 1106 | } | 1178 | } |
| 1107 | - | 1179 | + } |
| 1108 | - // 根据打卡类型或文件扩展名判断文件类型 | 1180 | + |
| 1109 | - const finalFileType = file.file_type || (isAudioFile(fileName) ? 'audio' : (isVideoFile(fileName) ? 'video' : 'image')) | 1181 | + if (!fileUrl) { |
| 1110 | - | 1182 | + console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(file, null, 2)) |
| 1111 | - if (finalFileType === 'audio') { | 1183 | + showToast('无法获取文件URL,请检查文件是否上传成功') |
| 1112 | - console.log('准备播放音频:', fileName, fileUrl) | 1184 | + return |
| 1113 | - showAudio(fileName, fileUrl) | 1185 | + } |
| 1114 | - } else if (finalFileType === 'video') { | 1186 | + |
| 1115 | - console.log('准备播放视频:', fileName, fileUrl) | 1187 | + let finalFileType = file.file_type |
| 1116 | - showVideo(fileName, fileUrl) | 1188 | + if (!finalFileType) { |
| 1117 | - } else if (finalFileType === 'image') { | 1189 | + if (isAudioFile(fileName)) { |
| 1118 | - console.log('图片预览由van-uploader组件处理,跳过文件列表点击预览') | 1190 | + finalFileType = 'audio' |
| 1119 | - // 图片预览由van-uploader的@click-preview事件处理,避免重复弹出 | 1191 | + } else if (isVideoFile(fileName)) { |
| 1120 | - return | 1192 | + finalFileType = 'video' |
| 1121 | } else { | 1193 | } else { |
| 1122 | - console.log('该文件类型不支持预览,文件名:', fileName, '类型:', finalFileType) | 1194 | + finalFileType = 'image' |
| 1123 | - showToast('该文件类型不支持预览') | ||
| 1124 | } | 1195 | } |
| 1196 | + } | ||
| 1197 | + | ||
| 1198 | + if (finalFileType === 'audio') { | ||
| 1199 | + showAudio(fileName, fileUrl) | ||
| 1200 | + } else if (finalFileType === 'video') { | ||
| 1201 | + showVideo(fileName, fileUrl) | ||
| 1202 | + } else if (finalFileType === 'image') { | ||
| 1203 | + return | ||
| 1204 | + } else { | ||
| 1205 | + showToast('该文件类型不支持预览') | ||
| 1206 | + } | ||
| 1125 | } | 1207 | } |
| 1126 | 1208 | ||
| 1127 | /** | 1209 | /** |
| ... | @@ -1214,9 +1296,9 @@ const onClickPreview = (file, detail) => { | ... | @@ -1214,9 +1296,9 @@ 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 | } |
| 1221 | 1303 | ||
| 1222 | /** | 1304 | /** |
| ... | @@ -1224,9 +1306,9 @@ const isAudioFile = (fileName) => { | ... | @@ -1224,9 +1306,9 @@ 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 | } |
| 1231 | 1313 | ||
| 1232 | /** | 1314 | /** |
| ... | @@ -1234,9 +1316,9 @@ const isVideoFile = (fileName) => { | ... | @@ -1234,9 +1316,9 @@ 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 | } |
| 1241 | 1323 | ||
| 1242 | /** | 1324 | /** |
| ... | @@ -1245,9 +1327,9 @@ const isImageFile = (fileName) => { | ... | @@ -1245,9 +1327,9 @@ const isImageFile = (fileName) => { |
| 1245 | * @param {string} url - 音频URL | 1327 | * @param {string} url - 音频URL |
| 1246 | */ | 1328 | */ |
| 1247 | const showAudio = (title, url) => { | 1329 | const showAudio = (title, url) => { |
| 1248 | - audioTitle.value = title | 1330 | + audioTitle.value = title |
| 1249 | - audioUrl.value = url | 1331 | + audioUrl.value = url |
| 1250 | - audioShow.value = true | 1332 | + audioShow.value = true |
| 1251 | } | 1333 | } |
| 1252 | 1334 | ||
| 1253 | /** | 1335 | /** |
| ... | @@ -1257,11 +1339,11 @@ const showAudio = (title, url) => { | ... | @@ -1257,11 +1339,11 @@ const showAudio = (title, url) => { |
| 1257 | * @param {string} cover - 视频封面URL(可选) | 1339 | * @param {string} cover - 视频封面URL(可选) |
| 1258 | */ | 1340 | */ |
| 1259 | const showVideo = (title, url, cover = '') => { | 1341 | const showVideo = (title, url, cover = '') => { |
| 1260 | - videoTitle.value = title | 1342 | + videoTitle.value = title |
| 1261 | - videoUrl.value = url | 1343 | + videoUrl.value = url |
| 1262 | - videoCover.value = cover | 1344 | + videoCover.value = cover |
| 1263 | - videoShow.value = true | 1345 | + videoShow.value = true |
| 1264 | - isVideoPlaying.value = false // 重置播放状态 | 1346 | + isVideoPlaying.value = false // 重置播放状态 |
| 1265 | } | 1347 | } |
| 1266 | 1348 | ||
| 1267 | /** | 1349 | /** |
| ... | @@ -1270,528 +1352,531 @@ const showVideo = (title, url, cover = '') => { | ... | @@ -1270,528 +1352,531 @@ const showVideo = (title, url, cover = '') => { |
| 1270 | * @param {number} index - 图片索引(可选) | 1352 | * @param {number} index - 图片索引(可选) |
| 1271 | */ | 1353 | */ |
| 1272 | const showImage = (url, index = 0) => { | 1354 | const showImage = (url, index = 0) => { |
| 1273 | - imageList.value = [url] | 1355 | + imageList.value = [url] |
| 1274 | - imageIndex.value = index | 1356 | + imageIndex.value = index |
| 1275 | - imageShow.value = true | 1357 | + imageShow.value = true |
| 1276 | } | 1358 | } |
| 1277 | 1359 | ||
| 1278 | /** | 1360 | /** |
| 1279 | * 开始播放视频 | 1361 | * 开始播放视频 |
| 1280 | */ | 1362 | */ |
| 1281 | const startVideoPlay = async () => { | 1363 | const startVideoPlay = async () => { |
| 1282 | - isVideoPlaying.value = true | 1364 | + isVideoPlaying.value = true |
| 1283 | - await nextTick() | 1365 | + await nextTick() |
| 1284 | - if (videoPlayerRef.value) { | 1366 | + if (videoPlayerRef.value) { |
| 1285 | - videoPlayerRef.value.play() | 1367 | + videoPlayerRef.value.play() |
| 1286 | - } | 1368 | + } |
| 1287 | } | 1369 | } |
| 1288 | 1370 | ||
| 1289 | /** | 1371 | /** |
| 1290 | * 处理视频播放事件 | 1372 | * 处理视频播放事件 |
| 1291 | */ | 1373 | */ |
| 1292 | const handleVideoPlay = () => { | 1374 | const handleVideoPlay = () => { |
| 1293 | - isVideoPlaying.value = true | 1375 | + isVideoPlaying.value = true |
| 1294 | } | 1376 | } |
| 1295 | 1377 | ||
| 1296 | /** | 1378 | /** |
| 1297 | * 处理视频暂停事件 | 1379 | * 处理视频暂停事件 |
| 1298 | */ | 1380 | */ |
| 1299 | const handleVideoPause = () => { | 1381 | const handleVideoPause = () => { |
| 1300 | - // 保持视频播放器可见,只在初始状态显示封面 | 1382 | + // 保持视频播放器可见,只在初始状态显示封面 |
| 1301 | } | 1383 | } |
| 1302 | 1384 | ||
| 1303 | /** | 1385 | /** |
| 1304 | * 停止视频播放 | 1386 | * 停止视频播放 |
| 1305 | */ | 1387 | */ |
| 1306 | const stopVideoPlay = () => { | 1388 | const stopVideoPlay = () => { |
| 1307 | - if (videoPlayerRef.value && typeof videoPlayerRef.value.pause === 'function') { | 1389 | + if (videoPlayerRef.value && typeof videoPlayerRef.value.pause === 'function') { |
| 1308 | - videoPlayerRef.value.pause() | 1390 | + videoPlayerRef.value.pause() |
| 1309 | - // 重置视频播放进度到开始位置 | 1391 | + // 重置视频播放进度到开始位置 |
| 1310 | - const player = videoPlayerRef.value.getPlayer() | 1392 | + const player = videoPlayerRef.value.getPlayer() |
| 1311 | - if (player && typeof player.currentTime === 'function') { | 1393 | + if (player && typeof player.currentTime === 'function') { |
| 1312 | - player.currentTime(0) | 1394 | + player.currentTime(0) |
| 1313 | - } | ||
| 1314 | } | 1395 | } |
| 1315 | - isVideoPlaying.value = false | 1396 | + } |
| 1397 | + isVideoPlaying.value = false | ||
| 1316 | } | 1398 | } |
| 1317 | 1399 | ||
| 1318 | /** | 1400 | /** |
| 1319 | * 页面挂载时的初始化逻辑 | 1401 | * 页面挂载时的初始化逻辑 |
| 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 |
| 1331 | - selectedTaskValue.value = route.query.subtask_id ? [+route.query.subtask_id] : [] | 1413 | + selectedTaskValue.value = route.query.subtask_id ? [+route.query.subtask_id] : [] |
| 1332 | - | 1414 | + |
| 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 => ({ |
| 1338 | - value: item.id, | 1420 | + text: item.is_makeup ? `补卡:${item.title}` : item.title, |
| 1339 | - note: item.note, // 作业描述 | 1421 | + value: item.id, |
| 1340 | - is_makeup: item.is_makeup, // 是否为补录 | 1422 | + note: item.note, // 作业描述 |
| 1341 | - field_list: item.field_list, // 动态字段列表 | 1423 | + is_makeup: item.is_makeup, // 是否为补录 |
| 1342 | - person_type: item.person_type, // 打卡对象类型 | 1424 | + field_list: item.field_list, // 动态字段列表 |
| 1343 | - attachment_type: item.attachment_type, // 附件类型 | 1425 | + person_type: item.person_type, // 打卡对象类型 |
| 1344 | - })) | 1426 | + attachment_type: item.attachment_type, // 附件类型 |
| 1345 | - ] | 1427 | + })), |
| 1428 | + ] | ||
| 1429 | + } | ||
| 1430 | + | ||
| 1431 | + // 如果有默认选中值,且非编辑模式(编辑模式下由initEditData统一处理,避免逻辑重复) | ||
| 1432 | + if (selectedTaskValue.value.length > 0 && !isEditMode.value) { | ||
| 1433 | + const option = taskOptions.value.find(o => o.value === selectedTaskValue.value[0]) | ||
| 1434 | + if (option) { | ||
| 1435 | + selectedTaskText.value = option.text | ||
| 1436 | + isMakeup.value = !!option.is_makeup | ||
| 1437 | + personType.value = option.person_type | ||
| 1438 | + // 初始化动态表单字段 | ||
| 1439 | + updateDynamicFormFields(option) | ||
| 1440 | + // 更新附件类型选项 | ||
| 1441 | + if (option.attachment_type) { | ||
| 1442 | + updateAttachmentTypeOptions(option.attachment_type) | ||
| 1443 | + } else { | ||
| 1444 | + updateAttachmentTypeOptions(taskDetail.value.attachment_type) | ||
| 1445 | + } | ||
| 1346 | } | 1446 | } |
| 1347 | 1447 | ||
| 1348 | - // 如果有默认选中值,且非编辑模式(编辑模式下由initEditData统一处理,避免逻辑重复) | 1448 | + // 如果是计数打卡,根据选中的作业ID查询计数对象 |
| 1349 | - if (selectedTaskValue.value.length > 0 && !isEditMode.value) { | 1449 | + if (taskType.value === 'count') { |
| 1350 | - const option = taskOptions.value.find(o => o.value === selectedTaskValue.value[0]) | 1450 | + await fetchTargetList(selectedTaskValue.value[0]) |
| 1351 | - if (option) { | ||
| 1352 | - selectedTaskText.value = option.text | ||
| 1353 | - isMakeup.value = !!option.is_makeup | ||
| 1354 | - personType.value = option.person_type | ||
| 1355 | - // 初始化动态表单字段 | ||
| 1356 | - updateDynamicFormFields(option) | ||
| 1357 | - // 更新附件类型选项 | ||
| 1358 | - if (option.attachment_type) { | ||
| 1359 | - updateAttachmentTypeOptions(option.attachment_type) | ||
| 1360 | - } else { | ||
| 1361 | - updateAttachmentTypeOptions(taskDetail.value.attachment_type) | ||
| 1362 | - } | ||
| 1363 | - } | ||
| 1364 | - | ||
| 1365 | - // 如果是计数打卡,根据选中的作业ID查询计数对象 | ||
| 1366 | - if (taskType.value === 'count') { | ||
| 1367 | - await fetchTargetList(selectedTaskValue.value[0]) | ||
| 1368 | - } | ||
| 1369 | } | 1451 | } |
| 1452 | + } | ||
| 1453 | + | ||
| 1454 | + // 初始化编辑数据 | ||
| 1455 | + await initEditData(taskOptions.value, { | ||
| 1456 | + onTaskFound: option => { | ||
| 1457 | + updateDynamicFormFields(option) | ||
| 1458 | + // 更新附件类型选项 | ||
| 1459 | + if (option.attachment_type) { | ||
| 1460 | + updateAttachmentTypeOptions(option.attachment_type) | ||
| 1461 | + } else { | ||
| 1462 | + updateAttachmentTypeOptions(taskDetail.value.attachment_type) | ||
| 1463 | + } | ||
| 1464 | + }, | ||
| 1465 | + ensureTargetList: async id => { | ||
| 1466 | + if (targetList.value.length === 0) { | ||
| 1467 | + await fetchTargetList(id) | ||
| 1468 | + } | ||
| 1469 | + }, | ||
| 1470 | + // setTargets: (list) => { | ||
| 1471 | + // // 只有当 list 不为空时才覆盖,避免覆盖掉 fetchTargetList 中设置的默认选中项 | ||
| 1472 | + // if (list && list.length > 0) { | ||
| 1473 | + // selectedTargets.value = list | ||
| 1474 | + // } | ||
| 1475 | + // }, | ||
| 1476 | + setCount: val => { | ||
| 1477 | + countValue.value = val | ||
| 1478 | + }, | ||
| 1479 | + }) | ||
| 1370 | 1480 | ||
| 1371 | - // 初始化编辑数据 | 1481 | + // 尝试恢复草稿 (非编辑模式) |
| 1372 | - await initEditData(taskOptions.value, { | 1482 | + if (!isEditMode.value) { |
| 1373 | - onTaskFound: (option) => { | 1483 | + await checkAndRestoreDraft() |
| 1374 | - updateDynamicFormFields(option) | 1484 | + } |
| 1375 | - // 更新附件类型选项 | ||
| 1376 | - if (option.attachment_type) { | ||
| 1377 | - updateAttachmentTypeOptions(option.attachment_type) | ||
| 1378 | - } else { | ||
| 1379 | - updateAttachmentTypeOptions(taskDetail.value.attachment_type) | ||
| 1380 | - } | ||
| 1381 | - }, | ||
| 1382 | - ensureTargetList: async (id) => { | ||
| 1383 | - if (targetList.value.length === 0) { | ||
| 1384 | - await fetchTargetList(id) | ||
| 1385 | - } | ||
| 1386 | - }, | ||
| 1387 | - // setTargets: (list) => { | ||
| 1388 | - // // 只有当 list 不为空时才覆盖,避免覆盖掉 fetchTargetList 中设置的默认选中项 | ||
| 1389 | - // if (list && list.length > 0) { | ||
| 1390 | - // selectedTargets.value = list | ||
| 1391 | - // } | ||
| 1392 | - // }, | ||
| 1393 | - setCount: (val) => { | ||
| 1394 | - countValue.value = val | ||
| 1395 | - } | ||
| 1396 | - }) | ||
| 1397 | - | ||
| 1398 | - // 尝试恢复草稿 (非编辑模式) | ||
| 1399 | - if (!isEditMode.value) { | ||
| 1400 | - await checkAndRestoreDraft() | ||
| 1401 | - } | ||
| 1402 | }) | 1485 | }) |
| 1403 | </script> | 1486 | </script> |
| 1404 | 1487 | ||
| 1405 | <style lang="less" scoped> | 1488 | <style lang="less" scoped> |
| 1406 | .checkin-detail-page { | 1489 | .checkin-detail-page { |
| 1407 | - min-height: 100vh; | 1490 | + min-height: 100vh; |
| 1408 | - background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); | 1491 | + background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); |
| 1409 | - padding-bottom: 100px; | 1492 | + padding-bottom: 100px; |
| 1410 | } | 1493 | } |
| 1411 | 1494 | ||
| 1412 | .page-content { | 1495 | .page-content { |
| 1413 | - padding: 1rem; | 1496 | + padding: 1rem; |
| 1414 | } | 1497 | } |
| 1415 | 1498 | ||
| 1416 | .section-wrapper { | 1499 | .section-wrapper { |
| 1417 | - background-color: #fff; | 1500 | + background-color: #fff; |
| 1418 | - border-radius: 12px; | 1501 | + border-radius: 12px; |
| 1419 | - margin-bottom: 1rem; | 1502 | + margin-bottom: 1rem; |
| 1420 | - overflow: hidden; | 1503 | + overflow: hidden; |
| 1421 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); | 1504 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); |
| 1422 | } | 1505 | } |
| 1423 | 1506 | ||
| 1424 | .section-title { | 1507 | .section-title { |
| 1425 | - font-size: 1.1rem; | 1508 | + font-size: 1.1rem; |
| 1426 | - font-weight: 600; | 1509 | + font-weight: 600; |
| 1427 | - color: #4caf50; | 1510 | + color: #4caf50; |
| 1428 | - padding: 1rem 1rem 0.5rem; | 1511 | + padding: 1rem 1rem 0.5rem; |
| 1429 | - border-bottom: 1px solid #f0f0f0; | 1512 | + border-bottom: 1px solid #f0f0f0; |
| 1430 | } | 1513 | } |
| 1431 | 1514 | ||
| 1432 | .section-content { | 1515 | .section-content { |
| 1433 | - padding: 1rem; | 1516 | + padding: 1rem; |
| 1434 | - overflow: hidden; | 1517 | + overflow: hidden; |
| 1435 | } | 1518 | } |
| 1436 | 1519 | ||
| 1437 | .description-text { | 1520 | .description-text { |
| 1438 | - color: #666; | 1521 | + color: #666; |
| 1439 | - line-height: 1.6; | 1522 | + line-height: 1.6; |
| 1440 | - font-size: 0.95rem; | 1523 | + font-size: 0.95rem; |
| 1524 | + word-break: break-word; | ||
| 1525 | + overflow-wrap: anywhere; | ||
| 1526 | + width: 100%; | ||
| 1527 | + box-sizing: border-box; | ||
| 1528 | + | ||
| 1529 | + :deep(*) { | ||
| 1530 | + max-width: 100% !important; | ||
| 1531 | + box-sizing: border-box; | ||
| 1532 | + white-space: normal !important; | ||
| 1533 | + overflow-wrap: anywhere; | ||
| 1441 | word-break: break-word; | 1534 | word-break: break-word; |
| 1442 | - overflow-wrap: break-word; | 1535 | + } |
| 1536 | + | ||
| 1537 | + :deep(img) { | ||
| 1538 | + max-width: 100% !important; | ||
| 1539 | + height: auto; | ||
| 1540 | + display: block; | ||
| 1541 | + } | ||
| 1542 | + | ||
| 1543 | + :deep(p) { | ||
| 1544 | + margin: 0.5rem 0; | ||
| 1545 | + } | ||
| 1546 | + | ||
| 1547 | + :deep(table) { | ||
| 1548 | + max-width: 100% !important; | ||
| 1549 | + border-collapse: collapse; | ||
| 1550 | + overflow-x: auto; | ||
| 1551 | + display: table; | ||
| 1443 | width: 100%; | 1552 | width: 100%; |
| 1444 | - box-sizing: border-box; | 1553 | + } |
| 1445 | - overflow: hidden; | 1554 | + |
| 1446 | - | 1555 | + :deep(pre) { |
| 1447 | - :deep(*) { | 1556 | + max-width: 100% !important; |
| 1448 | - max-width: 100% !important; | 1557 | + overflow-x: auto; |
| 1449 | - box-sizing: border-box; | 1558 | + white-space: pre-wrap; |
| 1450 | - } | 1559 | + word-wrap: break-word; |
| 1451 | - | 1560 | + } |
| 1452 | - :deep(img) { | 1561 | + |
| 1453 | - max-width: 100% !important; | 1562 | + // 富文本内容样式 |
| 1454 | - height: auto; | 1563 | + // :deep(.rich-content) { |
| 1455 | - display: block; | 1564 | + // h1, h2, h3, h4, h5, h6 { |
| 1456 | - } | 1565 | + // margin: 16px 0 12px 0; |
| 1457 | - | 1566 | + // font-weight: 600; |
| 1458 | - :deep(p) { | 1567 | + // line-height: 1.4; |
| 1459 | - margin: 0.5rem 0; | 1568 | + // } |
| 1460 | - } | 1569 | + |
| 1461 | - | 1570 | + // h2 { |
| 1462 | - :deep(table) { | 1571 | + // font-size: 20px; |
| 1463 | - max-width: 100% !important; | 1572 | + // color: #2563eb; |
| 1464 | - border-collapse: collapse; | 1573 | + // } |
| 1465 | - overflow-x: auto; | 1574 | + |
| 1466 | - display: table; | 1575 | + // h3 { |
| 1467 | - width: 100%; | 1576 | + // font-size: 18px; |
| 1468 | - } | 1577 | + // color: #059669; |
| 1469 | - | 1578 | + // } |
| 1470 | - :deep(pre) { | 1579 | + |
| 1471 | - max-width: 100% !important; | 1580 | + // h4 { |
| 1472 | - overflow-x: auto; | 1581 | + // font-size: 16px; |
| 1473 | - white-space: pre-wrap; | 1582 | + // } |
| 1474 | - word-wrap: break-word; | 1583 | + |
| 1475 | - } | 1584 | + // p { |
| 1476 | - | 1585 | + // margin: 12px 0; |
| 1477 | - // 富文本内容样式 | 1586 | + // line-height: 1.6; |
| 1478 | - // :deep(.rich-content) { | 1587 | + // color: #374151; |
| 1479 | - // h1, h2, h3, h4, h5, h6 { | 1588 | + // } |
| 1480 | - // margin: 16px 0 12px 0; | 1589 | + |
| 1481 | - // font-weight: 600; | 1590 | + // strong { |
| 1482 | - // line-height: 1.4; | 1591 | + // font-weight: 600; |
| 1483 | - // } | 1592 | + // color: #dc2626; |
| 1484 | - | 1593 | + // } |
| 1485 | - // h2 { | 1594 | + |
| 1486 | - // font-size: 20px; | 1595 | + // code { |
| 1487 | - // color: #2563eb; | 1596 | + // background: #f3f4f6; |
| 1488 | - // } | 1597 | + // padding: 2px 6px; |
| 1489 | - | 1598 | + // border-radius: 4px; |
| 1490 | - // h3 { | 1599 | + // color: #dc2626; |
| 1491 | - // font-size: 18px; | 1600 | + // font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; |
| 1492 | - // color: #059669; | 1601 | + // font-size: 0.9em; |
| 1493 | - // } | 1602 | + // } |
| 1494 | - | 1603 | + |
| 1495 | - // h4 { | 1604 | + // ol, ul { |
| 1496 | - // font-size: 16px; | 1605 | + // margin: 16px 0; |
| 1497 | - // } | 1606 | + // padding-left: 24px; |
| 1498 | - | 1607 | + |
| 1499 | - // p { | 1608 | + // li { |
| 1500 | - // margin: 12px 0; | 1609 | + // margin-bottom: 8px; |
| 1501 | - // line-height: 1.6; | 1610 | + // line-height: 1.8; |
| 1502 | - // color: #374151; | 1611 | + // } |
| 1503 | - // } | 1612 | + // } |
| 1504 | - | 1613 | + |
| 1505 | - // strong { | 1614 | + // blockquote { |
| 1506 | - // font-weight: 600; | 1615 | + // border-left: 4px solid #10b981; |
| 1507 | - // color: #dc2626; | 1616 | + // background: #f0fdf4; |
| 1508 | - // } | 1617 | + // padding: 16px; |
| 1509 | - | 1618 | + // margin: 20px 0; |
| 1510 | - // code { | 1619 | + // border-radius: 0 8px 8px 0; |
| 1511 | - // background: #f3f4f6; | 1620 | + |
| 1512 | - // padding: 2px 6px; | 1621 | + // p { |
| 1513 | - // border-radius: 4px; | 1622 | + // margin: 0; |
| 1514 | - // color: #dc2626; | 1623 | + // color: #065f46; |
| 1515 | - // font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | 1624 | + // font-style: italic; |
| 1516 | - // font-size: 0.9em; | 1625 | + // } |
| 1517 | - // } | 1626 | + // } |
| 1518 | - | 1627 | + |
| 1519 | - // ol, ul { | 1628 | + // img { |
| 1520 | - // margin: 16px 0; | 1629 | + // max-width: 100%; |
| 1521 | - // padding-left: 24px; | 1630 | + // height: auto; |
| 1522 | - | 1631 | + // border-radius: 8px; |
| 1523 | - // li { | 1632 | + // margin: 12px 0; |
| 1524 | - // margin-bottom: 8px; | 1633 | + // display: block; |
| 1525 | - // line-height: 1.8; | 1634 | + // } |
| 1526 | - // } | 1635 | + |
| 1527 | - // } | 1636 | + // // 特殊样式容器 |
| 1528 | - | 1637 | + // .warning-box { |
| 1529 | - // blockquote { | 1638 | + // background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); |
| 1530 | - // border-left: 4px solid #10b981; | 1639 | + // padding: 16px; |
| 1531 | - // background: #f0fdf4; | 1640 | + // border-radius: 12px; |
| 1532 | - // padding: 16px; | 1641 | + // margin: 16px 0; |
| 1533 | - // margin: 20px 0; | 1642 | + // border-left: 4px solid #f59e0b; |
| 1534 | - // border-radius: 0 8px 8px 0; | 1643 | + // } |
| 1535 | - | 1644 | + |
| 1536 | - // p { | 1645 | + // .info-box { |
| 1537 | - // margin: 0; | 1646 | + // background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); |
| 1538 | - // color: #065f46; | 1647 | + // padding: 16px; |
| 1539 | - // font-style: italic; | 1648 | + // border-radius: 12px; |
| 1540 | - // } | 1649 | + // margin: 20px 0; |
| 1541 | - // } | 1650 | + // } |
| 1542 | - | 1651 | + |
| 1543 | - // img { | 1652 | + // .deadline-box { |
| 1544 | - // max-width: 100%; | 1653 | + // background: #fef2f2; |
| 1545 | - // height: auto; | 1654 | + // border: 1px solid #fecaca; |
| 1546 | - // border-radius: 8px; | 1655 | + // border-radius: 8px; |
| 1547 | - // margin: 12px 0; | 1656 | + // padding: 16px; |
| 1548 | - // display: block; | 1657 | + // margin: 20px 0; |
| 1549 | - // } | 1658 | + // } |
| 1550 | - | 1659 | + |
| 1551 | - // // 特殊样式容器 | 1660 | + // // 图片容器居中 |
| 1552 | - // .warning-box { | 1661 | + // div[style*="text-align: center"] { |
| 1553 | - // background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | 1662 | + // text-align: center; |
| 1554 | - // padding: 16px; | 1663 | + |
| 1555 | - // border-radius: 12px; | 1664 | + // img { |
| 1556 | - // margin: 16px 0; | 1665 | + // margin: 12px auto; |
| 1557 | - // border-left: 4px solid #f59e0b; | 1666 | + // } |
| 1558 | - // } | 1667 | + |
| 1559 | - | 1668 | + // p { |
| 1560 | - // .info-box { | 1669 | + // color: #6b7280; |
| 1561 | - // background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); | 1670 | + // font-size: 14px; |
| 1562 | - // padding: 16px; | 1671 | + // font-style: italic; |
| 1563 | - // border-radius: 12px; | 1672 | + // margin-top: 8px; |
| 1564 | - // margin: 20px 0; | 1673 | + // } |
| 1565 | - // } | 1674 | + // } |
| 1566 | - | 1675 | + // } |
| 1567 | - // .deadline-box { | ||
| 1568 | - // background: #fef2f2; | ||
| 1569 | - // border: 1px solid #fecaca; | ||
| 1570 | - // border-radius: 8px; | ||
| 1571 | - // padding: 16px; | ||
| 1572 | - // margin: 20px 0; | ||
| 1573 | - // } | ||
| 1574 | - | ||
| 1575 | - // // 图片容器居中 | ||
| 1576 | - // div[style*="text-align: center"] { | ||
| 1577 | - // text-align: center; | ||
| 1578 | - | ||
| 1579 | - // img { | ||
| 1580 | - // margin: 12px auto; | ||
| 1581 | - // } | ||
| 1582 | - | ||
| 1583 | - // p { | ||
| 1584 | - // color: #6b7280; | ||
| 1585 | - // font-size: 14px; | ||
| 1586 | - // font-style: italic; | ||
| 1587 | - // margin-top: 8px; | ||
| 1588 | - // } | ||
| 1589 | - // } | ||
| 1590 | - // } | ||
| 1591 | } | 1676 | } |
| 1592 | 1677 | ||
| 1593 | .no-description { | 1678 | .no-description { |
| 1594 | - color: #999; | 1679 | + color: #999; |
| 1595 | - font-style: italic; | 1680 | + font-style: italic; |
| 1596 | - text-align: center; | 1681 | + text-align: center; |
| 1597 | - padding: 2rem 0; | 1682 | + padding: 2rem 0; |
| 1598 | } | 1683 | } |
| 1599 | 1684 | ||
| 1600 | .text-input-area { | 1685 | .text-input-area { |
| 1601 | - margin-bottom: 1.5rem; | 1686 | + margin-bottom: 1.5rem; |
| 1602 | 1687 | ||
| 1603 | - .van-field { | 1688 | + .van-field { |
| 1604 | - border-radius: 8px; | 1689 | + border-radius: 8px; |
| 1605 | - background-color: #f8f9fa; | 1690 | + background-color: #f8f9fa; |
| 1606 | - border: 1px solid #e9ecef; | 1691 | + border: 1px solid #e9ecef; |
| 1607 | - } | 1692 | + } |
| 1608 | } | 1693 | } |
| 1609 | 1694 | ||
| 1610 | .checkin-tabs { | 1695 | .checkin-tabs { |
| 1611 | - .tabs-header { | 1696 | + .tabs-header { |
| 1612 | - margin-bottom: 1rem; | 1697 | + margin-bottom: 1rem; |
| 1613 | - } | 1698 | + } |
| 1614 | 1699 | ||
| 1615 | - .tab-title { | 1700 | + .tab-title { |
| 1616 | - font-size: 1rem; | 1701 | + font-size: 1rem; |
| 1617 | - font-weight: 600; | 1702 | + font-weight: 600; |
| 1618 | - color: #333; | 1703 | + color: #333; |
| 1619 | - margin-bottom: 0.8rem; | 1704 | + margin-bottom: 0.8rem; |
| 1620 | - } | 1705 | + } |
| 1621 | 1706 | ||
| 1622 | - .tabs-nav { | 1707 | + .tabs-nav { |
| 1623 | - display: grid; | 1708 | + display: grid; |
| 1624 | - grid-template-columns: repeat(4, 1fr); | 1709 | + grid-template-columns: repeat(4, 1fr); |
| 1625 | - gap: 0.5rem; | 1710 | + gap: 0.5rem; |
| 1626 | - } | 1711 | + } |
| 1627 | 1712 | ||
| 1628 | - .tab-item { | 1713 | + .tab-item { |
| 1629 | - display: flex; | 1714 | + display: flex; |
| 1630 | - flex-direction: column; | 1715 | + flex-direction: column; |
| 1631 | - align-items: center; | 1716 | + align-items: center; |
| 1632 | - justify-content: center; | 1717 | + justify-content: center; |
| 1633 | - padding: 0.8rem 0.5rem; | 1718 | + padding: 0.8rem 0.5rem; |
| 1634 | - border: 2px solid #e8f5e8; | 1719 | + border: 2px solid #e8f5e8; |
| 1635 | - border-radius: 8px; | 1720 | + border-radius: 8px; |
| 1636 | - background-color: #fafffe; | 1721 | + background-color: #fafffe; |
| 1637 | - cursor: pointer; | 1722 | + cursor: pointer; |
| 1638 | - transition: all 0.3s ease; | 1723 | + transition: all 0.3s ease; |
| 1639 | 1724 | ||
| 1640 | - &:hover { | 1725 | + &:hover { |
| 1641 | - border-color: #4caf50; | 1726 | + border-color: #4caf50; |
| 1642 | - background-color: #f0fdf4; | 1727 | + background-color: #f0fdf4; |
| 1643 | - } | 1728 | + } |
| 1644 | 1729 | ||
| 1645 | - &.active { | 1730 | + &.active { |
| 1646 | - border-color: #4caf50; | 1731 | + border-color: #4caf50; |
| 1647 | - background-color: #f0fdf4; | 1732 | + background-color: #f0fdf4; |
| 1648 | 1733 | ||
| 1649 | - .van-icon { | 1734 | + .van-icon { |
| 1650 | - color: #4caf50; | 1735 | + color: #4caf50; |
| 1651 | - } | 1736 | + } |
| 1652 | 1737 | ||
| 1653 | - .tab-text { | 1738 | + .tab-text { |
| 1654 | - color: #4caf50; | 1739 | + color: #4caf50; |
| 1655 | - font-weight: 600; | 1740 | + font-weight: 600; |
| 1656 | - } | 1741 | + } |
| 1657 | - } | 1742 | + } |
| 1658 | 1743 | ||
| 1659 | - &.disabled { | 1744 | + &.disabled { |
| 1660 | - opacity: 0.5; | 1745 | + opacity: 0.5; |
| 1661 | - cursor: not-allowed; | 1746 | + cursor: not-allowed; |
| 1662 | 1747 | ||
| 1663 | - &:hover { | 1748 | + &:hover { |
| 1664 | - border-color: #e8f5e8; | 1749 | + border-color: #e8f5e8; |
| 1665 | - background-color: #fafffe; | 1750 | + background-color: #fafffe; |
| 1666 | - } | 1751 | + } |
| 1667 | - } | ||
| 1668 | } | 1752 | } |
| 1753 | + } | ||
| 1669 | 1754 | ||
| 1670 | - .tab-text { | 1755 | + .tab-text { |
| 1671 | - margin-top: 0.3rem; | 1756 | + margin-top: 0.3rem; |
| 1672 | - font-size: 0.8rem; | 1757 | + font-size: 0.8rem; |
| 1673 | - color: #666; | 1758 | + color: #666; |
| 1674 | - text-align: center; | 1759 | + text-align: center; |
| 1675 | - } | 1760 | + } |
| 1676 | } | 1761 | } |
| 1677 | 1762 | ||
| 1678 | .upload-area { | 1763 | .upload-area { |
| 1679 | - margin-top: 1rem; | 1764 | + margin-top: 1rem; |
| 1680 | 1765 | ||
| 1681 | - .van-uploader { | 1766 | + .van-uploader { |
| 1682 | - margin-bottom: 1rem; | 1767 | + margin-bottom: 1rem; |
| 1683 | - } | 1768 | + } |
| 1684 | } | 1769 | } |
| 1685 | 1770 | ||
| 1686 | .file-list { | 1771 | .file-list { |
| 1687 | - margin: 1rem 0; | 1772 | + margin: 1rem 0; |
| 1688 | } | 1773 | } |
| 1689 | 1774 | ||
| 1690 | .file-item { | 1775 | .file-item { |
| 1776 | + display: flex; | ||
| 1777 | + align-items: center; | ||
| 1778 | + justify-content: space-between; | ||
| 1779 | + padding: 0.8rem; | ||
| 1780 | + background-color: #f8f9fa; | ||
| 1781 | + border-radius: 8px; | ||
| 1782 | + margin-bottom: 0.5rem; | ||
| 1783 | + | ||
| 1784 | + .file-info { | ||
| 1691 | display: flex; | 1785 | display: flex; |
| 1692 | align-items: center; | 1786 | align-items: center; |
| 1693 | - justify-content: space-between; | 1787 | + flex: 1; |
| 1694 | - padding: 0.8rem; | 1788 | + gap: 0.5rem; |
| 1695 | - background-color: #f8f9fa; | 1789 | + cursor: pointer; |
| 1696 | - border-radius: 8px; | 1790 | + padding: 0.5rem; |
| 1697 | - margin-bottom: 0.5rem; | 1791 | + border-radius: 0.5rem; |
| 1698 | - | 1792 | + transition: background-color 0.2s; |
| 1699 | - .file-info { | 1793 | + |
| 1700 | - display: flex; | 1794 | + &:hover { |
| 1701 | - align-items: center; | 1795 | + background-color: #f5f5f5; |
| 1702 | - flex: 1; | ||
| 1703 | - gap: 0.5rem; | ||
| 1704 | - cursor: pointer; | ||
| 1705 | - padding: 0.5rem; | ||
| 1706 | - border-radius: 0.5rem; | ||
| 1707 | - transition: background-color 0.2s; | ||
| 1708 | - | ||
| 1709 | - &:hover { | ||
| 1710 | - background-color: #f5f5f5; | ||
| 1711 | - } | ||
| 1712 | } | 1796 | } |
| 1797 | + } | ||
| 1713 | 1798 | ||
| 1714 | - .file-name { | 1799 | + .file-name { |
| 1715 | - flex: 1; | 1800 | + flex: 1; |
| 1716 | - font-size: 0.9rem; | 1801 | + font-size: 0.9rem; |
| 1717 | - color: #333; | 1802 | + color: #333; |
| 1718 | - overflow: hidden; | 1803 | + overflow: hidden; |
| 1719 | - text-overflow: ellipsis; | 1804 | + text-overflow: ellipsis; |
| 1720 | - word-break: break-all; | 1805 | + word-break: break-all; |
| 1721 | - word-wrap: break-word; | 1806 | + word-wrap: break-word; |
| 1722 | - // white-space: nowrap; | 1807 | + // white-space: nowrap; |
| 1808 | + } | ||
| 1809 | + | ||
| 1810 | + .file-status { | ||
| 1811 | + font-size: 0.8rem; | ||
| 1812 | + padding: 0.2rem 0.5rem; | ||
| 1813 | + border-radius: 4px; | ||
| 1814 | + | ||
| 1815 | + &.uploading { | ||
| 1816 | + color: #1890ff; | ||
| 1817 | + background-color: #e6f7ff; | ||
| 1723 | } | 1818 | } |
| 1724 | 1819 | ||
| 1725 | - .file-status { | 1820 | + &.done { |
| 1726 | - font-size: 0.8rem; | 1821 | + color: #52c41a; |
| 1727 | - padding: 0.2rem 0.5rem; | 1822 | + background-color: #f6ffed; |
| 1728 | - border-radius: 4px; | 1823 | + } |
| 1729 | - | ||
| 1730 | - &.uploading { | ||
| 1731 | - color: #1890ff; | ||
| 1732 | - background-color: #e6f7ff; | ||
| 1733 | - } | ||
| 1734 | - | ||
| 1735 | - &.done { | ||
| 1736 | - color: #52c41a; | ||
| 1737 | - background-color: #f6ffed; | ||
| 1738 | - } | ||
| 1739 | 1824 | ||
| 1740 | - &.failed { | 1825 | + &.failed { |
| 1741 | - color: #ff4d4f; | 1826 | + color: #ff4d4f; |
| 1742 | - background-color: #fff2f0; | 1827 | + background-color: #fff2f0; |
| 1743 | - } | ||
| 1744 | } | 1828 | } |
| 1829 | + } | ||
| 1745 | 1830 | ||
| 1746 | - .delete-icon { | 1831 | + .delete-icon { |
| 1747 | - color: #999; | 1832 | + color: #999; |
| 1748 | - cursor: pointer; | 1833 | + cursor: pointer; |
| 1749 | 1834 | ||
| 1750 | - &:hover { | 1835 | + &:hover { |
| 1751 | - color: #ff4d4f; | 1836 | + color: #ff4d4f; |
| 1752 | - } | ||
| 1753 | } | 1837 | } |
| 1838 | + } | ||
| 1754 | } | 1839 | } |
| 1755 | 1840 | ||
| 1756 | .upload-tips { | 1841 | .upload-tips { |
| 1757 | - .tip-text { | 1842 | + .tip-text { |
| 1758 | - font-size: 0.8rem; | 1843 | + font-size: 0.8rem; |
| 1759 | - color: #999; | 1844 | + color: #999; |
| 1760 | - margin-bottom: 0.3rem; | 1845 | + margin-bottom: 0.3rem; |
| 1761 | - } | 1846 | + } |
| 1762 | } | 1847 | } |
| 1763 | 1848 | ||
| 1764 | .finished-notice { | 1849 | .finished-notice { |
| 1765 | - display: flex; | 1850 | + display: flex; |
| 1766 | - flex-direction: column; | 1851 | + flex-direction: column; |
| 1767 | - align-items: center; | 1852 | + align-items: center; |
| 1768 | - justify-content: center; | 1853 | + justify-content: center; |
| 1769 | - padding: 3rem 1rem; | 1854 | + padding: 3rem 1rem; |
| 1770 | - text-align: center; | 1855 | + text-align: center; |
| 1771 | } | 1856 | } |
| 1772 | 1857 | ||
| 1773 | .finished-text { | 1858 | .finished-text { |
| 1774 | - margin-top: 1rem; | 1859 | + margin-top: 1rem; |
| 1775 | - font-size: 1.1rem; | 1860 | + font-size: 1.1rem; |
| 1776 | - color: #4caf50; | 1861 | + color: #4caf50; |
| 1777 | - font-weight: 600; | 1862 | + font-weight: 600; |
| 1778 | } | 1863 | } |
| 1779 | 1864 | ||
| 1780 | .submit-area { | 1865 | .submit-area { |
| 1781 | - position: fixed; | 1866 | + position: fixed; |
| 1782 | - bottom: 0; | 1867 | + bottom: 0; |
| 1783 | - left: 0; | 1868 | + left: 0; |
| 1784 | - right: 0; | 1869 | + right: 0; |
| 1785 | - padding: 1rem; | 1870 | + padding: 1rem; |
| 1786 | - background-color: #fff; | 1871 | + background-color: #fff; |
| 1787 | - border-top: 1px solid #f0f0f0; | 1872 | + border-top: 1px solid #f0f0f0; |
| 1788 | - z-index: 100; | 1873 | + z-index: 100; |
| 1789 | } | 1874 | } |
| 1790 | 1875 | ||
| 1791 | .loading-wrapper { | 1876 | .loading-wrapper { |
| 1792 | - display: flex; | 1877 | + display: flex; |
| 1793 | - align-items: center; | 1878 | + align-items: center; |
| 1794 | - justify-content: center; | 1879 | + justify-content: center; |
| 1795 | - height: 100%; | 1880 | + height: 100%; |
| 1796 | } | 1881 | } |
| 1797 | </style> | 1882 | </style> | ... | ... |
-
Please register or login to post a comment