feat(打卡): 重构打卡页面并提取公共逻辑到composable
- 将打卡按钮改为底部悬浮样式 - 统一编辑跳转逻辑到CheckinDetailPage - 提取打卡相关逻辑到useCheckin composable - 优化打卡详情页的UI和交互
Showing
3 changed files
with
753 additions
and
172 deletions
src/composables/useCheckin.js
0 → 100644
| 1 | +import { ref, computed } from 'vue' | ||
| 2 | +import { useRoute, useRouter } from 'vue-router' | ||
| 3 | +import { showToast, showLoadingToast } from 'vant' | ||
| 4 | +import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common' | ||
| 5 | +import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin" | ||
| 6 | +import BMF from 'browser-md5-file' | ||
| 7 | +import { useAuth } from '@/contexts/auth' | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 打卡功能的composable | ||
| 11 | + * @returns {Object} 打卡相关的状态和方法 | ||
| 12 | + */ | ||
| 13 | +export function useCheckin() { | ||
| 14 | + const route = useRoute() | ||
| 15 | + const router = useRouter() | ||
| 16 | + const { currentUser } = useAuth() | ||
| 17 | + | ||
| 18 | + // 基础状态 | ||
| 19 | + const uploading = ref(false) | ||
| 20 | + const loading = ref(false) | ||
| 21 | + const message = ref('') | ||
| 22 | + const fileList = ref([]) | ||
| 23 | + const activeType = ref('text') // 当前选中的打卡类型 | ||
| 24 | + const maxCount = ref(5) | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 是否可以提交 | ||
| 28 | + */ | ||
| 29 | + const canSubmit = computed(() => { | ||
| 30 | + if (activeType.value === 'text') { | ||
| 31 | + return message.value.trim() !== '' && message.value.trim().length >= 10 | ||
| 32 | + } else { | ||
| 33 | + return fileList.value.length > 0 && message.value.trim() !== '' | ||
| 34 | + } | ||
| 35 | + }) | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 获取文件MD5值 | ||
| 39 | + * @param {File} file - 文件对象 | ||
| 40 | + * @returns {Promise<string>} MD5值 | ||
| 41 | + */ | ||
| 42 | + const getFileMD5 = (file) => { | ||
| 43 | + return new Promise((resolve, reject) => { | ||
| 44 | + const bmf = new BMF() | ||
| 45 | + bmf.md5(file, (err, md5) => { | ||
| 46 | + if (err) { | ||
| 47 | + reject(err) | ||
| 48 | + return | ||
| 49 | + } | ||
| 50 | + resolve(md5) | ||
| 51 | + }) | ||
| 52 | + }) | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 上传文件到七牛云 | ||
| 57 | + * @param {File} file - 文件对象 | ||
| 58 | + * @param {string} token - 七牛云token | ||
| 59 | + * @param {string} fileName - 文件名 | ||
| 60 | + * @returns {Promise<Object>} 上传结果 | ||
| 61 | + */ | ||
| 62 | + const uploadToQiniu = async (file, token, fileName) => { | ||
| 63 | + const formData = new FormData() | ||
| 64 | + formData.append('file', file) | ||
| 65 | + formData.append('token', token) | ||
| 66 | + formData.append('key', fileName) | ||
| 67 | + | ||
| 68 | + const config = { | ||
| 69 | + headers: { 'Content-Type': 'multipart/form-data' } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + // 根据协议选择上传地址 | ||
| 73 | + const qiniuUploadUrl = window.location.protocol === 'https:' | ||
| 74 | + ? 'https://up.qbox.me' | ||
| 75 | + : 'http://upload.qiniu.com' | ||
| 76 | + | ||
| 77 | + return await qiniuUploadAPI(qiniuUploadUrl, formData, config) | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 处理单个文件上传 | ||
| 82 | + * @param {Object} file - 文件对象 | ||
| 83 | + * @returns {Promise<Object|null>} 上传结果 | ||
| 84 | + */ | ||
| 85 | + const handleUpload = async (file) => { | ||
| 86 | + loading.value = true | ||
| 87 | + try { | ||
| 88 | + // 获取MD5值 | ||
| 89 | + const md5 = await getFileMD5(file.file) | ||
| 90 | + | ||
| 91 | + // 获取七牛token | ||
| 92 | + const tokenResult = await qiniuTokenAPI({ | ||
| 93 | + name: file.file.name, | ||
| 94 | + hash: md5 | ||
| 95 | + }) | ||
| 96 | + | ||
| 97 | + // 文件已存在,直接返回 | ||
| 98 | + if (tokenResult.data) { | ||
| 99 | + return tokenResult.data | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // 新文件上传 | ||
| 103 | + if (tokenResult.token) { | ||
| 104 | + const suffix = /.[^.]+$/.exec(file.file.name) || '' | ||
| 105 | + let fileName = '' | ||
| 106 | + | ||
| 107 | + if (activeType.value === 'image') { | ||
| 108 | + fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/img/${md5}${suffix}` | ||
| 109 | + } else { | ||
| 110 | + fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}` | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + const uploadResult = await uploadToQiniu( | ||
| 114 | + file.file, | ||
| 115 | + tokenResult.token, | ||
| 116 | + fileName | ||
| 117 | + ) | ||
| 118 | + | ||
| 119 | + if (uploadResult.filekey) { | ||
| 120 | + // 保存文件信息 | ||
| 121 | + const saveData = { | ||
| 122 | + name: file.file.name, | ||
| 123 | + filekey: uploadResult.filekey, | ||
| 124 | + hash: md5 | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + // 图片类型需要保存尺寸信息 | ||
| 128 | + if (activeType.value === 'image' && uploadResult.image_info) { | ||
| 129 | + saveData.height = uploadResult.image_info.height | ||
| 130 | + saveData.width = uploadResult.image_info.width | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + const { data } = await saveFileAPI(saveData) | ||
| 134 | + return data | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + return null | ||
| 138 | + } catch (error) { | ||
| 139 | + console.error('Upload error:', error) | ||
| 140 | + return null | ||
| 141 | + } finally { | ||
| 142 | + loading.value = false | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /** | ||
| 147 | + * 文件上传前的校验 | ||
| 148 | + * @param {File|File[]} file - 文件或文件数组 | ||
| 149 | + * @returns {boolean} 是否通过校验 | ||
| 150 | + */ | ||
| 151 | + const beforeRead = (file) => { | ||
| 152 | + let flag = true | ||
| 153 | + const files = Array.isArray(file) ? file : [file] | ||
| 154 | + | ||
| 155 | + // 检查文件数量 | ||
| 156 | + if (fileList.value.length + files.length > maxCount.value) { | ||
| 157 | + flag = false | ||
| 158 | + showToast(`最大上传数量为${maxCount.value}个`) | ||
| 159 | + return flag | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + // 检查文件类型和大小 | ||
| 163 | + for (const item of files) { | ||
| 164 | + const fileType = item.type.toLowerCase() | ||
| 165 | + | ||
| 166 | + // 文件大小检查 | ||
| 167 | + if ((item.size / 1024 / 1024).toFixed(2) > 20) { | ||
| 168 | + flag = false | ||
| 169 | + showToast('最大文件体积为20MB') | ||
| 170 | + break | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + // 文件类型检查 | ||
| 174 | + if (activeType.value === 'image') { | ||
| 175 | + const imageTypes = ['jpg', 'jpeg', 'png'] | ||
| 176 | + const validImageTypes = imageTypes.map(type => `image/${type}`) | ||
| 177 | + if (!validImageTypes.some(type => fileType.includes(type.split('/')[1]))) { | ||
| 178 | + flag = false | ||
| 179 | + showToast('请上传指定格式图片') | ||
| 180 | + break | ||
| 181 | + } | ||
| 182 | + } else if (activeType.value === 'video') { | ||
| 183 | + if (!fileType.startsWith('video/')) { | ||
| 184 | + flag = false | ||
| 185 | + showToast('请上传视频文件') | ||
| 186 | + break | ||
| 187 | + } | ||
| 188 | + } else if (activeType.value === 'audio') { | ||
| 189 | + if (!fileType.startsWith('audio/')) { | ||
| 190 | + flag = false | ||
| 191 | + showToast('请上传音频文件') | ||
| 192 | + break | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + return flag | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * 文件读取后的处理 | ||
| 202 | + * @param {File|File[]} file - 文件或文件数组 | ||
| 203 | + */ | ||
| 204 | + const afterRead = async (file) => { | ||
| 205 | + const files = Array.isArray(file) ? file : [file] | ||
| 206 | + | ||
| 207 | + for (const item of files) { | ||
| 208 | + item.status = 'uploading' | ||
| 209 | + item.message = '上传中...' | ||
| 210 | + | ||
| 211 | + const result = await handleUpload(item) | ||
| 212 | + if (result) { | ||
| 213 | + item.status = 'done' | ||
| 214 | + item.message = '上传成功' | ||
| 215 | + item.url = result.url | ||
| 216 | + item.meta_id = result.meta_id | ||
| 217 | + item.name = result.name || item.file.name | ||
| 218 | + } else { | ||
| 219 | + item.status = 'failed' | ||
| 220 | + item.message = '上传失败' | ||
| 221 | + showToast('上传失败,请重试') | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + /** | ||
| 227 | + * 删除文件 | ||
| 228 | + * @param {Object} file - 要删除的文件对象 | ||
| 229 | + */ | ||
| 230 | + const onDelete = (file) => { | ||
| 231 | + const index = fileList.value.findIndex(item => item === file) | ||
| 232 | + if (index > -1) { | ||
| 233 | + fileList.value.splice(index, 1) | ||
| 234 | + } | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + /** | ||
| 238 | + * 删除文件项 | ||
| 239 | + * @param {Object} item - 要删除的文件项 | ||
| 240 | + */ | ||
| 241 | + const delItem = (item) => { | ||
| 242 | + const index = fileList.value.findIndex(file => file === item) | ||
| 243 | + if (index > -1) { | ||
| 244 | + fileList.value.splice(index, 1) | ||
| 245 | + } | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + /** | ||
| 249 | + * 提交打卡 | ||
| 250 | + */ | ||
| 251 | + const onSubmit = async () => { | ||
| 252 | + if (uploading.value) return | ||
| 253 | + | ||
| 254 | + // 表单验证 | ||
| 255 | + if (activeType.value === 'text') { | ||
| 256 | + if (message.value.trim().length < 10) { | ||
| 257 | + showToast('打卡内容至少需要10个字符') | ||
| 258 | + return | ||
| 259 | + } | ||
| 260 | + } else { | ||
| 261 | + if (fileList.value.length === 0) { | ||
| 262 | + showToast('请先上传文件') | ||
| 263 | + return | ||
| 264 | + } | ||
| 265 | + if (message.value.trim() === '') { | ||
| 266 | + showToast('请输入打卡留言') | ||
| 267 | + return | ||
| 268 | + } | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + uploading.value = true | ||
| 272 | + showLoadingToast({ | ||
| 273 | + message: '提交中...', | ||
| 274 | + forbidClick: true, | ||
| 275 | + }) | ||
| 276 | + | ||
| 277 | + try { | ||
| 278 | + // 准备提交数据 | ||
| 279 | + const submitData = { | ||
| 280 | + task_id: route.query.id, | ||
| 281 | + note: message.value, | ||
| 282 | + file_type: activeType.value, | ||
| 283 | + meta_id: [] | ||
| 284 | + } | ||
| 285 | + | ||
| 286 | + // 如果有文件,添加文件ID | ||
| 287 | + if (fileList.value.length > 0) { | ||
| 288 | + submitData.meta_id = fileList.value | ||
| 289 | + .filter(item => item.status === 'done' && item.meta_id) | ||
| 290 | + .map(item => item.meta_id) | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + let result | ||
| 294 | + if (route.query.status === 'edit') { | ||
| 295 | + // 编辑打卡 | ||
| 296 | + result = await editUploadTaskInfoAPI({ | ||
| 297 | + i: route.query.post_id, | ||
| 298 | + note: submitData.note, | ||
| 299 | + meta_id: submitData.meta_id, | ||
| 300 | + file_type: submitData.file_type, | ||
| 301 | + }) | ||
| 302 | + } else { | ||
| 303 | + // 新增打卡 | ||
| 304 | + result = await addUploadTaskAPI(submitData) | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + if (result.code) { | ||
| 308 | + showToast('提交成功') | ||
| 309 | + router.back() | ||
| 310 | + } | ||
| 311 | + } catch (error) { | ||
| 312 | + showToast('提交失败,请重试') | ||
| 313 | + } finally { | ||
| 314 | + uploading.value = false | ||
| 315 | + } | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | + /** | ||
| 319 | + * 切换打卡类型 | ||
| 320 | + * @param {string} type - 打卡类型 | ||
| 321 | + */ | ||
| 322 | + const switchType = (type) => { | ||
| 323 | + if (activeType.value !== type) { | ||
| 324 | + activeType.value = type | ||
| 325 | + // 切换类型时清空文件列表 | ||
| 326 | + fileList.value = [] | ||
| 327 | + } | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + /** | ||
| 331 | + * 重置表单 | ||
| 332 | + */ | ||
| 333 | + const resetForm = () => { | ||
| 334 | + message.value = '' | ||
| 335 | + fileList.value = [] | ||
| 336 | + activeType.value = 'text' | ||
| 337 | + uploading.value = false | ||
| 338 | + loading.value = false | ||
| 339 | + } | ||
| 340 | + | ||
| 341 | + /** | ||
| 342 | + * 初始化编辑数据 | ||
| 343 | + */ | ||
| 344 | + const initEditData = async () => { | ||
| 345 | + if (route.query.status === 'edit') { | ||
| 346 | + try { | ||
| 347 | + const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id }) | ||
| 348 | + if (code) { | ||
| 349 | + message.value = data.note || '' | ||
| 350 | + activeType.value = data.file_type || 'text' | ||
| 351 | + | ||
| 352 | + // 如果有文件数据,初始化文件列表 - 使用data.files而不是data.meta | ||
| 353 | + if (data.files && data.files.length > 0) { | ||
| 354 | + fileList.value = data.files.map(item => { | ||
| 355 | + const fileItem = { | ||
| 356 | + url: item.value, | ||
| 357 | + status: 'done', | ||
| 358 | + message: '已上传', | ||
| 359 | + meta_id: item.meta_id, | ||
| 360 | + name: item.name || '' | ||
| 361 | + } | ||
| 362 | + | ||
| 363 | + // 对于图片类型,添加isImage标记确保正确显示 | ||
| 364 | + if (activeType.value === 'image') { | ||
| 365 | + fileItem.isImage = true | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + // 为了支持文件名显示,创建一个File对象 | ||
| 369 | + if (item.name) { | ||
| 370 | + fileItem.file = new File([], item.name, { type: item.type || '' }) | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + return fileItem | ||
| 374 | + }) | ||
| 375 | + } | ||
| 376 | + } | ||
| 377 | + } catch (error) { | ||
| 378 | + console.error('初始化编辑数据失败:', error) | ||
| 379 | + } | ||
| 380 | + } | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + return { | ||
| 384 | + // 状态 | ||
| 385 | + uploading, | ||
| 386 | + loading, | ||
| 387 | + message, | ||
| 388 | + fileList, | ||
| 389 | + activeType, | ||
| 390 | + maxCount, | ||
| 391 | + canSubmit, | ||
| 392 | + | ||
| 393 | + // 方法 | ||
| 394 | + beforeRead, | ||
| 395 | + afterRead, | ||
| 396 | + onDelete, | ||
| 397 | + delItem, | ||
| 398 | + onSubmit, | ||
| 399 | + switchType, | ||
| 400 | + resetForm, | ||
| 401 | + initEditData | ||
| 402 | + } | ||
| 403 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -15,56 +15,144 @@ | ... | @@ -15,56 +15,144 @@ |
| 15 | </div> | 15 | </div> |
| 16 | </div> | 16 | </div> |
| 17 | 17 | ||
| 18 | - <!-- 打卡类型选择 --> | 18 | + <!-- 打卡内容区域 --> |
| 19 | - <div v-if="!taskDetail.is_finish" class="section-wrapper"> | 19 | + <div class="section-wrapper"> |
| 20 | - <div class="section-title">选择打卡类型</div> | 20 | + <div class="section-title">打卡内容</div> |
| 21 | <div class="section-content"> | 21 | <div class="section-content"> |
| 22 | - <div class="checkin-types"> | 22 | + <!-- 文本输入区域 --> |
| 23 | - <div | 23 | + <div class="text-input-area"> |
| 24 | - v-for="option in attachmentTypeOptions" | 24 | + <van-field |
| 25 | - :key="option.key" | 25 | + v-model="message" |
| 26 | - @click="handleCheckinTypeClick(option.key)" | 26 | + rows="6" |
| 27 | - class="checkin-type-item" | 27 | + autosize |
| 28 | - > | 28 | + type="textarea" |
| 29 | - <van-icon | 29 | + :placeholder="activeType === 'text' ? '请输入打卡内容,至少需要10个字符' : '请输入打卡留言'" |
| 30 | - :name="getIconName(option.key)" | 30 | + :maxlength="activeType === 'text' ? 500 : 200" |
| 31 | - size="2rem" | 31 | + show-word-limit |
| 32 | - color="#4caf50" | 32 | + /> |
| 33 | + </div> | ||
| 34 | + | ||
| 35 | + <!-- 打卡类型选项卡 --> | ||
| 36 | + <div class="checkin-tabs"> | ||
| 37 | + <div class="tabs-header"> | ||
| 38 | + <div class="tab-title">选择打卡类型</div> | ||
| 39 | + <div class="tabs-nav"> | ||
| 40 | + <div | ||
| 41 | + v-for="option in attachmentTypeOptions" | ||
| 42 | + :key="option.key" | ||
| 43 | + @click="switchType(option.key)" | ||
| 44 | + :class="['tab-item', { active: activeType === option.key }]" | ||
| 45 | + > | ||
| 46 | + <van-icon | ||
| 47 | + :name="getIconName(option.key)" | ||
| 48 | + size="1.2rem" | ||
| 49 | + /> | ||
| 50 | + <span class="tab-text">{{ option.value }}</span> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + </div> | ||
| 54 | + | ||
| 55 | + <!-- 文件上传区域 --> | ||
| 56 | + <div v-if="activeType !== 'text'" class="upload-area"> | ||
| 57 | + <van-uploader | ||
| 58 | + v-model="fileList" | ||
| 59 | + :max-count="maxCount" | ||
| 60 | + :max-size="20 * 1024 * 1024" | ||
| 61 | + :before-read="beforeRead" | ||
| 62 | + :after-read="afterRead" | ||
| 63 | + @delete="onDelete" | ||
| 64 | + multiple | ||
| 65 | + :accept="getAcceptType()" | ||
| 66 | + result-type="file" | ||
| 67 | + :deletable="false" | ||
| 33 | /> | 68 | /> |
| 34 | - <span class="type-text">{{ option.value }}</span> | 69 | + |
| 70 | + <!-- 文件列表显示 --> | ||
| 71 | + <div v-if="fileList.length > 0" class="file-list"> | ||
| 72 | + <div v-for="(item, index) in fileList" :key="index" class="file-item"> | ||
| 73 | + <div class="file-info"> | ||
| 74 | + <van-icon :name="getFileIcon()" size="1rem" /> | ||
| 75 | + <span class="file-name">{{ item.name || item.file?.name }}</span> | ||
| 76 | + <span class="file-status" :class="item.status">{{ item.message }}</span> | ||
| 77 | + </div> | ||
| 78 | + <van-icon | ||
| 79 | + name="clear" | ||
| 80 | + size="1rem" | ||
| 81 | + @click="delItem(item)" | ||
| 82 | + class="delete-icon" | ||
| 83 | + /> | ||
| 84 | + </div> | ||
| 85 | + </div> | ||
| 86 | + | ||
| 87 | + <div class="upload-tips"> | ||
| 88 | + <div class="tip-text">最多上传{{ maxCount }}个文件,每个不超过20M</div> | ||
| 89 | + <div class="tip-text">{{ getUploadTips() }}</div> | ||
| 90 | + </div> | ||
| 35 | </div> | 91 | </div> |
| 36 | </div> | 92 | </div> |
| 37 | </div> | 93 | </div> |
| 38 | </div> | 94 | </div> |
| 39 | 95 | ||
| 40 | - <!-- 已完成提示 --> | 96 | + <!-- 提交按钮 --> |
| 41 | - <div v-else class="section-wrapper"> | 97 | + <div v-if="!taskDetail.is_finish || route.query.status === 'edit'" class="submit-area"> |
| 42 | - <div class="finished-notice"> | 98 | + <van-button |
| 43 | - <van-icon name="success" size="3rem" color="#4caf50" /> | 99 | + type="primary" |
| 44 | - <div class="finished-text">作业已完成</div> | 100 | + block |
| 45 | - </div> | 101 | + size="large" |
| 102 | + :loading="uploading" | ||
| 103 | + :disabled="!canSubmit" | ||
| 104 | + @click="onSubmit" | ||
| 105 | + > | ||
| 106 | + {{ route.query.status === 'edit' ? '保存修改' : '提交打卡' }} | ||
| 107 | + </van-button> | ||
| 46 | </div> | 108 | </div> |
| 47 | </div> | 109 | </div> |
| 110 | + | ||
| 111 | + <!-- 上传加载遮罩 --> | ||
| 112 | + <van-overlay :show="loading"> | ||
| 113 | + <div class="loading-wrapper" @click.stop> | ||
| 114 | + <van-loading vertical color="#FFFFFF">上传中...</van-loading> | ||
| 115 | + </div> | ||
| 116 | + </van-overlay> | ||
| 48 | </div> | 117 | </div> |
| 49 | </template> | 118 | </template> |
| 50 | 119 | ||
| 51 | <script setup> | 120 | <script setup> |
| 52 | import { ref, computed, onMounted } from 'vue' | 121 | import { ref, computed, onMounted } from 'vue' |
| 53 | import { useRoute, useRouter } from 'vue-router' | 122 | import { useRoute, useRouter } from 'vue-router' |
| 54 | -import { getTaskDetailAPI } from "@/api/checkin"; | 123 | +import { getTaskDetailAPI } from "@/api/checkin" |
| 55 | import { getTeacherFindSettingsAPI } from '@/api/teacher' | 124 | import { getTeacherFindSettingsAPI } from '@/api/teacher' |
| 56 | -import { useTitle } from '@vueuse/core'; | 125 | +import { useTitle } from '@vueuse/core' |
| 126 | +import { useCheckin } from '@/composables/useCheckin' | ||
| 57 | import dayjs from 'dayjs' | 127 | import dayjs from 'dayjs' |
| 58 | 128 | ||
| 59 | const route = useRoute() | 129 | const route = useRoute() |
| 60 | const router = useRouter() | 130 | const router = useRouter() |
| 61 | -useTitle('打卡详情'); | 131 | +useTitle('打卡详情') |
| 132 | + | ||
| 133 | +// 使用打卡composable | ||
| 134 | +const { | ||
| 135 | + uploading, | ||
| 136 | + loading, | ||
| 137 | + message, | ||
| 138 | + fileList, | ||
| 139 | + activeType, | ||
| 140 | + maxCount, | ||
| 141 | + canSubmit, | ||
| 142 | + beforeRead, | ||
| 143 | + afterRead, | ||
| 144 | + onDelete, | ||
| 145 | + delItem, | ||
| 146 | + onSubmit, | ||
| 147 | + switchType, | ||
| 148 | + initEditData | ||
| 149 | +} = useCheckin() | ||
| 62 | 150 | ||
| 63 | // 任务详情数据 | 151 | // 任务详情数据 |
| 64 | -const taskDetail = ref({}); | 152 | +const taskDetail = ref({}) |
| 65 | 153 | ||
| 66 | // 作品类型选项 | 154 | // 作品类型选项 |
| 67 | -const attachmentTypeOptions = ref([]); | 155 | +const attachmentTypeOptions = ref([]) |
| 68 | 156 | ||
| 69 | /** | 157 | /** |
| 70 | * 返回上一页 | 158 | * 返回上一页 |
| ... | @@ -84,92 +172,57 @@ const getIconName = (type) => { | ... | @@ -84,92 +172,57 @@ const getIconName = (type) => { |
| 84 | 'image': 'photo', | 172 | 'image': 'photo', |
| 85 | 'video': 'video', | 173 | 'video': 'video', |
| 86 | 'audio': 'music' | 174 | 'audio': 'music' |
| 87 | - }; | ||
| 88 | - return iconMap[type] || 'edit'; | ||
| 89 | -}; | ||
| 90 | - | ||
| 91 | -/** | ||
| 92 | - * 处理打卡类型点击事件 | ||
| 93 | - * @param {string} type - 打卡类型 | ||
| 94 | - */ | ||
| 95 | -const handleCheckinTypeClick = (type) => { | ||
| 96 | - switch (type) { | ||
| 97 | - case 'text': | ||
| 98 | - goToCheckinTextPage(); | ||
| 99 | - break; | ||
| 100 | - case 'image': | ||
| 101 | - goToCheckinImagePage(); | ||
| 102 | - break; | ||
| 103 | - case 'video': | ||
| 104 | - goToCheckinVideoPage(); | ||
| 105 | - break; | ||
| 106 | - case 'audio': | ||
| 107 | - goToCheckinAudioPage(); | ||
| 108 | - break; | ||
| 109 | - default: | ||
| 110 | - console.warn('未知的打卡类型:', type); | ||
| 111 | } | 175 | } |
| 112 | -}; | 176 | + return iconMap[type] || 'edit' |
| 113 | - | ||
| 114 | -/** | ||
| 115 | - * 跳转到文本打卡页面 | ||
| 116 | - */ | ||
| 117 | -const goToCheckinTextPage = () => { | ||
| 118 | - router.push({ | ||
| 119 | - path: '/checkin/text', | ||
| 120 | - query: { | ||
| 121 | - id: route.query.id, | ||
| 122 | - type: 'text' | ||
| 123 | - } | ||
| 124 | - }) | ||
| 125 | } | 177 | } |
| 126 | 178 | ||
| 127 | /** | 179 | /** |
| 128 | - * 跳转到图片打卡页面 | 180 | + * 获取文件图标 |
| 181 | + * @returns {string} 文件图标名称 | ||
| 129 | */ | 182 | */ |
| 130 | -const goToCheckinImagePage = () => { | 183 | +const getFileIcon = () => { |
| 131 | - router.push({ | 184 | + const iconMap = { |
| 132 | - path: '/checkin/image', | 185 | + 'image': 'photo', |
| 133 | - query: { | 186 | + 'video': 'video', |
| 134 | - id: route.query.id, | 187 | + 'audio': 'music' |
| 135 | - type: 'image' | 188 | + } |
| 136 | - } | 189 | + return iconMap[activeType.value] || 'description' |
| 137 | - }) | ||
| 138 | } | 190 | } |
| 139 | 191 | ||
| 140 | /** | 192 | /** |
| 141 | - * 跳转到视频打卡页面 | 193 | + * 获取上传文件类型 |
| 194 | + * @returns {string} accept属性值 | ||
| 142 | */ | 195 | */ |
| 143 | -const goToCheckinVideoPage = () => { | 196 | +const getAcceptType = () => { |
| 144 | - router.push({ | 197 | + const acceptMap = { |
| 145 | - path: '/checkin/video', | 198 | + 'image': 'image/*', |
| 146 | - query: { | 199 | + 'video': 'video/*', |
| 147 | - id: route.query.id, | 200 | + 'audio': '.mp3,.wav,.aac' |
| 148 | - type: 'video', | 201 | + } |
| 149 | - } | 202 | + return acceptMap[activeType.value] || '*' |
| 150 | - }) | ||
| 151 | } | 203 | } |
| 152 | 204 | ||
| 153 | /** | 205 | /** |
| 154 | - * 跳转到音频打卡页面 | 206 | + * 获取上传提示文本 |
| 207 | + * @returns {string} 提示文本 | ||
| 155 | */ | 208 | */ |
| 156 | -const goToCheckinAudioPage = () => { | 209 | +const getUploadTips = () => { |
| 157 | - router.push({ | 210 | + const tipsMap = { |
| 158 | - path: '/checkin/audio', | 211 | + 'image': '支持格式:jpg/jpeg/png', |
| 159 | - query: { | 212 | + 'video': '支持格式:视频文件', |
| 160 | - id: route.query.id, | 213 | + 'audio': '支持格式:.mp3/.wav/.aac 音频文件' |
| 161 | - type: 'audio', | 214 | + } |
| 162 | - } | 215 | + return tipsMap[activeType.value] || '' |
| 163 | - }) | ||
| 164 | } | 216 | } |
| 165 | 217 | ||
| 166 | /** | 218 | /** |
| 167 | * 获取任务详情 | 219 | * 获取任务详情 |
| 220 | + * @param {string} month - 月份 | ||
| 168 | */ | 221 | */ |
| 169 | const getTaskDetail = async (month) => { | 222 | const getTaskDetail = async (month) => { |
| 170 | - const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }); | 223 | + const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }) |
| 171 | if (code) { | 224 | if (code) { |
| 172 | - taskDetail.value = data; | 225 | + taskDetail.value = data |
| 173 | } | 226 | } |
| 174 | } | 227 | } |
| 175 | 228 | ||
| ... | @@ -178,20 +231,23 @@ const getTaskDetail = async (month) => { | ... | @@ -178,20 +231,23 @@ const getTaskDetail = async (month) => { |
| 178 | */ | 231 | */ |
| 179 | onMounted(async () => { | 232 | onMounted(async () => { |
| 180 | // 获取任务详情 | 233 | // 获取任务详情 |
| 181 | - getTaskDetail(dayjs().format('YYYY-MM')); | 234 | + getTaskDetail(dayjs().format('YYYY-MM')) |
| 182 | 235 | ||
| 183 | // 获取作品类型数据 | 236 | // 获取作品类型数据 |
| 184 | try { | 237 | try { |
| 185 | - const { code, data } = await getTeacherFindSettingsAPI(); | 238 | + const { code, data } = await getTeacherFindSettingsAPI() |
| 186 | if (code && data.task_attachment_type) { | 239 | if (code && data.task_attachment_type) { |
| 187 | attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({ | 240 | attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({ |
| 188 | key, | 241 | key, |
| 189 | value | 242 | value |
| 190 | - })); | 243 | + })) |
| 191 | } | 244 | } |
| 192 | } catch (error) { | 245 | } catch (error) { |
| 193 | - console.error('获取作品类型数据失败:', error); | 246 | + console.error('获取作品类型数据失败:', error) |
| 194 | } | 247 | } |
| 248 | + | ||
| 249 | + // 初始化编辑数据 | ||
| 250 | + await initEditData() | ||
| 195 | }) | 251 | }) |
| 196 | </script> | 252 | </script> |
| 197 | 253 | ||
| ... | @@ -199,6 +255,7 @@ onMounted(async () => { | ... | @@ -199,6 +255,7 @@ onMounted(async () => { |
| 199 | .checkin-detail-page { | 255 | .checkin-detail-page { |
| 200 | min-height: 100vh; | 256 | min-height: 100vh; |
| 201 | background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); | 257 | background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); |
| 258 | + padding-bottom: 100px; | ||
| 202 | } | 259 | } |
| 203 | 260 | ||
| 204 | .page-content { | 261 | .page-content { |
| ... | @@ -238,52 +295,148 @@ onMounted(async () => { | ... | @@ -238,52 +295,148 @@ onMounted(async () => { |
| 238 | padding: 2rem 0; | 295 | padding: 2rem 0; |
| 239 | } | 296 | } |
| 240 | 297 | ||
| 241 | -.class-status { | 298 | +.text-input-area { |
| 242 | - display: flex; | 299 | + margin-bottom: 1.5rem; |
| 243 | - align-items: center; | 300 | + |
| 244 | - gap: 0.5rem; | 301 | + .van-field { |
| 302 | + border-radius: 8px; | ||
| 303 | + background-color: #f8f9fa; | ||
| 304 | + border: 1px solid #e9ecef; | ||
| 305 | + } | ||
| 245 | } | 306 | } |
| 246 | 307 | ||
| 247 | -.status-text { | 308 | +.checkin-tabs { |
| 248 | - font-size: 0.85rem; | 309 | + .tabs-header { |
| 249 | - color: #666; | 310 | + margin-bottom: 1rem; |
| 311 | + } | ||
| 312 | + | ||
| 313 | + .tab-title { | ||
| 314 | + font-size: 1rem; | ||
| 315 | + font-weight: 600; | ||
| 316 | + color: #333; | ||
| 317 | + margin-bottom: 0.8rem; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + .tabs-nav { | ||
| 321 | + display: grid; | ||
| 322 | + grid-template-columns: repeat(4, 1fr); | ||
| 323 | + gap: 0.5rem; | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + .tab-item { | ||
| 327 | + display: flex; | ||
| 328 | + flex-direction: column; | ||
| 329 | + align-items: center; | ||
| 330 | + justify-content: center; | ||
| 331 | + padding: 0.8rem 0.5rem; | ||
| 332 | + border: 2px solid #e8f5e8; | ||
| 333 | + border-radius: 8px; | ||
| 334 | + background-color: #fafffe; | ||
| 335 | + cursor: pointer; | ||
| 336 | + transition: all 0.3s ease; | ||
| 337 | + | ||
| 338 | + &:hover { | ||
| 339 | + border-color: #4caf50; | ||
| 340 | + background-color: #f0fdf4; | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + &.active { | ||
| 344 | + border-color: #4caf50; | ||
| 345 | + background-color: #f0fdf4; | ||
| 346 | + | ||
| 347 | + .van-icon { | ||
| 348 | + color: #4caf50; | ||
| 349 | + } | ||
| 350 | + | ||
| 351 | + .tab-text { | ||
| 352 | + color: #4caf50; | ||
| 353 | + font-weight: 600; | ||
| 354 | + } | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + .tab-text { | ||
| 359 | + margin-top: 0.3rem; | ||
| 360 | + font-size: 0.8rem; | ||
| 361 | + color: #666; | ||
| 362 | + text-align: center; | ||
| 363 | + } | ||
| 364 | +} | ||
| 365 | + | ||
| 366 | +.upload-area { | ||
| 367 | + margin-top: 1rem; | ||
| 368 | + | ||
| 369 | + .van-uploader { | ||
| 370 | + margin-bottom: 1rem; | ||
| 371 | + } | ||
| 250 | } | 372 | } |
| 251 | 373 | ||
| 252 | -.checkin-types { | 374 | +.file-list { |
| 253 | - display: grid; | 375 | + margin: 1rem 0; |
| 254 | - grid-template-columns: repeat(2, 1fr); | ||
| 255 | - gap: 1rem; | ||
| 256 | } | 376 | } |
| 257 | 377 | ||
| 258 | -.checkin-type-item { | 378 | +.file-item { |
| 259 | display: flex; | 379 | display: flex; |
| 260 | - flex-direction: column; | ||
| 261 | align-items: center; | 380 | align-items: center; |
| 262 | - justify-content: center; | 381 | + justify-content: space-between; |
| 263 | - padding: 1.5rem 1rem; | 382 | + padding: 0.8rem; |
| 264 | - border: 2px solid #e8f5e8; | 383 | + background-color: #f8f9fa; |
| 265 | - border-radius: 12px; | 384 | + border-radius: 8px; |
| 266 | - background-color: #fafffe; | 385 | + margin-bottom: 0.5rem; |
| 267 | - cursor: pointer; | 386 | + |
| 268 | - transition: all 0.3s ease; | 387 | + .file-info { |
| 269 | - | 388 | + display: flex; |
| 270 | - &:hover { | 389 | + align-items: center; |
| 271 | - border-color: #4caf50; | 390 | + flex: 1; |
| 272 | - background-color: #f0fdf4; | 391 | + gap: 0.5rem; |
| 273 | - transform: translateY(-2px); | ||
| 274 | - box-shadow: 0 4px 12px rgba(76, 175, 80, 0.15); | ||
| 275 | } | 392 | } |
| 276 | 393 | ||
| 277 | - &:active { | 394 | + .file-name { |
| 278 | - transform: translateY(0); | 395 | + flex: 1; |
| 396 | + font-size: 0.9rem; | ||
| 397 | + color: #333; | ||
| 398 | + overflow: hidden; | ||
| 399 | + text-overflow: ellipsis; | ||
| 400 | + white-space: nowrap; | ||
| 401 | + } | ||
| 402 | + | ||
| 403 | + .file-status { | ||
| 404 | + font-size: 0.8rem; | ||
| 405 | + padding: 0.2rem 0.5rem; | ||
| 406 | + border-radius: 4px; | ||
| 407 | + | ||
| 408 | + &.uploading { | ||
| 409 | + color: #1890ff; | ||
| 410 | + background-color: #e6f7ff; | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + &.done { | ||
| 414 | + color: #52c41a; | ||
| 415 | + background-color: #f6ffed; | ||
| 416 | + } | ||
| 417 | + | ||
| 418 | + &.failed { | ||
| 419 | + color: #ff4d4f; | ||
| 420 | + background-color: #fff2f0; | ||
| 421 | + } | ||
| 422 | + } | ||
| 423 | + | ||
| 424 | + .delete-icon { | ||
| 425 | + color: #999; | ||
| 426 | + cursor: pointer; | ||
| 427 | + | ||
| 428 | + &:hover { | ||
| 429 | + color: #ff4d4f; | ||
| 430 | + } | ||
| 279 | } | 431 | } |
| 280 | } | 432 | } |
| 281 | 433 | ||
| 282 | -.type-text { | 434 | +.upload-tips { |
| 283 | - margin-top: 0.5rem; | 435 | + .tip-text { |
| 284 | - font-size: 0.9rem; | 436 | + font-size: 0.8rem; |
| 285 | - color: #4caf50; | 437 | + color: #999; |
| 286 | - font-weight: 500; | 438 | + margin-bottom: 0.3rem; |
| 439 | + } | ||
| 287 | } | 440 | } |
| 288 | 441 | ||
| 289 | .finished-notice { | 442 | .finished-notice { |
| ... | @@ -301,4 +454,22 @@ onMounted(async () => { | ... | @@ -301,4 +454,22 @@ onMounted(async () => { |
| 301 | color: #4caf50; | 454 | color: #4caf50; |
| 302 | font-weight: 600; | 455 | font-weight: 600; |
| 303 | } | 456 | } |
| 457 | + | ||
| 458 | +.submit-area { | ||
| 459 | + position: fixed; | ||
| 460 | + bottom: 0; | ||
| 461 | + left: 0; | ||
| 462 | + right: 0; | ||
| 463 | + padding: 1rem; | ||
| 464 | + background-color: #fff; | ||
| 465 | + border-top: 1px solid #f0f0f0; | ||
| 466 | + z-index: 100; | ||
| 467 | +} | ||
| 468 | + | ||
| 469 | +.loading-wrapper { | ||
| 470 | + display: flex; | ||
| 471 | + align-items: center; | ||
| 472 | + justify-content: center; | ||
| 473 | + height: 100%; | ||
| 474 | +} | ||
| 304 | </style> | 475 | </style> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-05-29 15:34:17 | 2 | * @Date: 2025-05-29 15:34:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-09-30 17:02:29 | 4 | + * @LastEditTime: 2025-09-30 17:58:09 |
| 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue | 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -57,19 +57,6 @@ | ... | @@ -57,19 +57,6 @@ |
| 57 | </div> | 57 | </div> |
| 58 | </div> | 58 | </div> |
| 59 | 59 | ||
| 60 | - <!-- 我要打卡按钮 --> | ||
| 61 | - <div v-if="!taskDetail.is_finish" class="text-wrapper" style="padding-bottom: 0;"> | ||
| 62 | - <van-button | ||
| 63 | - type="primary" | ||
| 64 | - block | ||
| 65 | - round | ||
| 66 | - @click="goToCheckinDetailPage" | ||
| 67 | - class="checkin-action-button" | ||
| 68 | - > | ||
| 69 | - <van-icon name="edit" size="1.2rem" /> | ||
| 70 | - <span style="margin-left: 0.5rem;">我要打卡</span> | ||
| 71 | - </van-button> | ||
| 72 | - </div> | ||
| 73 | 60 | ||
| 74 | <div class="text-wrapper"> | 61 | <div class="text-wrapper"> |
| 75 | <div class="text-header">打卡动态</div> | 62 | <div class="text-header">打卡动态</div> |
| ... | @@ -162,13 +149,26 @@ | ... | @@ -162,13 +149,26 @@ |
| 162 | <van-back-top right="5vw" bottom="10vh" /> | 149 | <van-back-top right="5vw" bottom="10vh" /> |
| 163 | </div> | 150 | </div> |
| 164 | 151 | ||
| 165 | - <div style="height: 5rem;"></div> | 152 | + <div style="height: 10rem;"></div> |
| 166 | </div> <!-- 闭合 scrollable-content --> | 153 | </div> <!-- 闭合 scrollable-content --> |
| 167 | </van-config-provider> | 154 | </van-config-provider> |
| 168 | 155 | ||
| 169 | <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog> | 156 | <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog> |
| 170 | 157 | ||
| 171 | <van-back-top right="5vw" bottom="15vh" /> | 158 | <van-back-top right="5vw" bottom="15vh" /> |
| 159 | + | ||
| 160 | + <!-- 底部悬浮打卡按钮 --> | ||
| 161 | + <div v-if="!taskDetail.is_finish" class="floating-checkin-button"> | ||
| 162 | + <van-button | ||
| 163 | + type="primary" | ||
| 164 | + round | ||
| 165 | + @click="goToCheckinDetailPage" | ||
| 166 | + class="checkin-action-button" | ||
| 167 | + > | ||
| 168 | + <van-icon name="edit" size="1.2rem" /> | ||
| 169 | + <span style="margin-left: 0.5rem;">我要打卡</span> | ||
| 170 | + </van-button> | ||
| 171 | + </div> | ||
| 172 | </AppLayout> | 172 | </AppLayout> |
| 173 | </template> | 173 | </template> |
| 174 | 174 | ||
| ... | @@ -606,34 +606,15 @@ const handLike = async (post) => { | ... | @@ -606,34 +606,15 @@ const handLike = async (post) => { |
| 606 | } | 606 | } |
| 607 | 607 | ||
| 608 | const editCheckin = (post) => { | 608 | const editCheckin = (post) => { |
| 609 | - if (post.file_type === 'image') { | 609 | + // 统一跳转到CheckinDetailPage页面处理所有类型的编辑 |
| 610 | - router.push({ | 610 | + router.push({ |
| 611 | - path: '/checkin/image', | 611 | + path: '/checkin/detail', |
| 612 | - query: { | 612 | + query: { |
| 613 | - post_id: post.id, | 613 | + post_id: post.id, |
| 614 | - type: 'image', | 614 | + type: post.file_type, |
| 615 | - status: 'edit', | 615 | + status: 'edit', |
| 616 | - } | 616 | + } |
| 617 | - }) | 617 | + }) |
| 618 | - } else if (post.file_type === 'video') { | ||
| 619 | - router.push({ | ||
| 620 | - path: '/checkin/video', | ||
| 621 | - query: { | ||
| 622 | - post_id: post.id, | ||
| 623 | - type: 'video', | ||
| 624 | - status: 'edit', | ||
| 625 | - } | ||
| 626 | - }) | ||
| 627 | - } else if (post.file_type === 'audio') { | ||
| 628 | - router.push({ | ||
| 629 | - path: '/checkin/audio', | ||
| 630 | - query: { | ||
| 631 | - post_id: post.id, | ||
| 632 | - type: 'audio', | ||
| 633 | - status: 'edit', | ||
| 634 | - } | ||
| 635 | - }) | ||
| 636 | - } | ||
| 637 | } | 618 | } |
| 638 | 619 | ||
| 639 | const delCheckin = (post) => { | 620 | const delCheckin = (post) => { |
| ... | @@ -651,6 +632,13 @@ const delCheckin = (post) => { | ... | @@ -651,6 +632,13 @@ const delCheckin = (post) => { |
| 651 | // router.go(0); | 632 | // router.go(0); |
| 652 | // 删除post_id相应的数据 | 633 | // 删除post_id相应的数据 |
| 653 | checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id); | 634 | checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id); |
| 635 | + // 检查是否还可以打卡 | ||
| 636 | + const current_date = route.query.date; | ||
| 637 | + if (current_date) { | ||
| 638 | + getTaskDetail(dayjs(current_date).format('YYYY-MM')); | ||
| 639 | + } else { | ||
| 640 | + getTaskDetail(dayjs().format('YYYY-MM')); | ||
| 641 | + } | ||
| 654 | } else { | 642 | } else { |
| 655 | showErrorToast('删除失败'); | 643 | showErrorToast('删除失败'); |
| 656 | } | 644 | } |
| ... | @@ -957,6 +945,25 @@ const formatData = (data) => { | ... | @@ -957,6 +945,25 @@ const formatData = (data) => { |
| 957 | </style> | 945 | </style> |
| 958 | 946 | ||
| 959 | <style scoped> | 947 | <style scoped> |
| 948 | +/* 底部悬浮打卡按钮样式 */ | ||
| 949 | +.floating-checkin-button { | ||
| 950 | + position: fixed; | ||
| 951 | + bottom: 6rem; | ||
| 952 | + left: 1rem; | ||
| 953 | + right: 1rem; | ||
| 954 | + z-index: 1000; | ||
| 955 | + padding: 0 1rem; | ||
| 956 | +} | ||
| 957 | + | ||
| 958 | +.floating-checkin-button .checkin-action-button { | ||
| 959 | + width: 100%; | ||
| 960 | + height: 3rem; | ||
| 961 | + font-size: 1rem; | ||
| 962 | + font-weight: 600; | ||
| 963 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||
| 964 | + border: none; | ||
| 965 | +} | ||
| 966 | + | ||
| 960 | :deep(.van-calendar__footer) { | 967 | :deep(.van-calendar__footer) { |
| 961 | display: none; | 968 | display: none; |
| 962 | } | 969 | } | ... | ... |
-
Please register or login to post a comment