feat(打卡): 移除留言必填校验并支持富文本显示
- 移除打卡留言的必填校验,改为可选输入 - 修改界面文案"打卡内容"为"打卡留言" - 为作业描述添加v-html支持以显示富文本内容
Showing
2 changed files
with
204 additions
and
17 deletions
| ... | @@ -105,7 +105,7 @@ export function useCheckin() { | ... | @@ -105,7 +105,7 @@ export function useCheckin() { |
| 105 | if (tokenResult.token) { | 105 | if (tokenResult.token) { |
| 106 | const suffix = /.[^.]+$/.exec(file.file.name) || '' | 106 | const suffix = /.[^.]+$/.exec(file.file.name) || '' |
| 107 | let fileName = '' | 107 | let fileName = '' |
| 108 | - | 108 | + |
| 109 | if (activeType.value === 'image') { | 109 | if (activeType.value === 'image') { |
| 110 | fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/img/${md5}${suffix}` | 110 | fileName = `mlaj/upload/checkin/${currentUser.value.mobile}/img/${md5}${suffix}` |
| 111 | } else { | 111 | } else { |
| ... | @@ -164,7 +164,7 @@ export function useCheckin() { | ... | @@ -164,7 +164,7 @@ export function useCheckin() { |
| 164 | // 检查文件类型和大小 | 164 | // 检查文件类型和大小 |
| 165 | for (const item of files) { | 165 | for (const item of files) { |
| 166 | const fileType = item.type.toLowerCase() | 166 | const fileType = item.type.toLowerCase() |
| 167 | - | 167 | + |
| 168 | // 文件大小检查 | 168 | // 文件大小检查 |
| 169 | if ((item.size / 1024 / 1024).toFixed(2) > 20) { | 169 | if ((item.size / 1024 / 1024).toFixed(2) > 20) { |
| 170 | flag = false | 170 | flag = false |
| ... | @@ -209,7 +209,7 @@ export function useCheckin() { | ... | @@ -209,7 +209,7 @@ export function useCheckin() { |
| 209 | for (const item of files) { | 209 | for (const item of files) { |
| 210 | item.status = 'uploading' | 210 | item.status = 'uploading' |
| 211 | item.message = '上传中...' | 211 | item.message = '上传中...' |
| 212 | - | 212 | + |
| 213 | const result = await handleUpload(item) | 213 | const result = await handleUpload(item) |
| 214 | if (result) { | 214 | if (result) { |
| 215 | item.status = 'done' | 215 | item.status = 'done' |
| ... | @@ -264,10 +264,10 @@ export function useCheckin() { | ... | @@ -264,10 +264,10 @@ export function useCheckin() { |
| 264 | showToast('请先上传文件') | 264 | showToast('请先上传文件') |
| 265 | return | 265 | return |
| 266 | } | 266 | } |
| 267 | - if (message.value.trim() === '') { | 267 | + // if (message.value.trim() === '') { |
| 268 | - showToast('请输入打卡留言') | 268 | + // showToast('请输入打卡留言') |
| 269 | - return | 269 | + // return |
| 270 | - } | 270 | + // } |
| 271 | } | 271 | } |
| 272 | 272 | ||
| 273 | uploading.value = true | 273 | uploading.value = true |
| ... | @@ -350,7 +350,7 @@ export function useCheckin() { | ... | @@ -350,7 +350,7 @@ export function useCheckin() { |
| 350 | if (code) { | 350 | if (code) { |
| 351 | message.value = data.note || '' | 351 | message.value = data.note || '' |
| 352 | activeType.value = data.file_type || 'text' | 352 | activeType.value = data.file_type || 'text' |
| 353 | - | 353 | + |
| 354 | // 如果有文件数据,初始化文件列表 - 使用data.files而不是data.meta | 354 | // 如果有文件数据,初始化文件列表 - 使用data.files而不是data.meta |
| 355 | if (data.files && data.files.length > 0) { | 355 | if (data.files && data.files.length > 0) { |
| 356 | fileList.value = data.files.map(item => { | 356 | fileList.value = data.files.map(item => { |
| ... | @@ -361,17 +361,17 @@ export function useCheckin() { | ... | @@ -361,17 +361,17 @@ export function useCheckin() { |
| 361 | meta_id: item.meta_id, | 361 | meta_id: item.meta_id, |
| 362 | name: item.name || '' | 362 | name: item.name || '' |
| 363 | } | 363 | } |
| 364 | - | 364 | + |
| 365 | // 对于图片类型,添加isImage标记确保正确显示 | 365 | // 对于图片类型,添加isImage标记确保正确显示 |
| 366 | if (activeType.value === 'image') { | 366 | if (activeType.value === 'image') { |
| 367 | fileItem.isImage = true | 367 | fileItem.isImage = true |
| 368 | } | 368 | } |
| 369 | - | 369 | + |
| 370 | // 为了支持文件名显示,创建一个File对象 | 370 | // 为了支持文件名显示,创建一个File对象 |
| 371 | if (item.name) { | 371 | if (item.name) { |
| 372 | fileItem.file = new File([], item.name, { type: item.type || '' }) | 372 | fileItem.file = new File([], item.name, { type: item.type || '' }) |
| 373 | } | 373 | } |
| 374 | - | 374 | + |
| 375 | return fileItem | 375 | return fileItem |
| 376 | }) | 376 | }) |
| 377 | } | 377 | } |
| ... | @@ -391,7 +391,7 @@ export function useCheckin() { | ... | @@ -391,7 +391,7 @@ export function useCheckin() { |
| 391 | activeType, | 391 | activeType, |
| 392 | maxCount, | 392 | maxCount, |
| 393 | canSubmit, | 393 | canSubmit, |
| 394 | - | 394 | + |
| 395 | // 方法 | 395 | // 方法 |
| 396 | beforeRead, | 396 | beforeRead, |
| 397 | afterRead, | 397 | afterRead, |
| ... | @@ -402,4 +402,4 @@ export function useCheckin() { | ... | @@ -402,4 +402,4 @@ export function useCheckin() { |
| 402 | resetForm, | 402 | resetForm, |
| 403 | initEditData | 403 | initEditData |
| 404 | } | 404 | } |
| 405 | -} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 405 | +} | ... | ... |
| ... | @@ -6,8 +6,7 @@ | ... | @@ -6,8 +6,7 @@ |
| 6 | <div class="section-wrapper"> | 6 | <div class="section-wrapper"> |
| 7 | <div class="section-title">作业描述</div> | 7 | <div class="section-title">作业描述</div> |
| 8 | <div class="section-content"> | 8 | <div class="section-content"> |
| 9 | - <div v-if="taskDetail.description" class="description-text"> | 9 | + <div v-if="taskDetail.description" class="description-text" v-html="taskDetail.description"> |
| 10 | - {{ taskDetail.description }} | ||
| 11 | </div> | 10 | </div> |
| 12 | <div v-else class="no-description"> | 11 | <div v-else class="no-description"> |
| 13 | 暂无作业描述 | 12 | 暂无作业描述 |
| ... | @@ -17,7 +16,7 @@ | ... | @@ -17,7 +16,7 @@ |
| 17 | 16 | ||
| 18 | <!-- 打卡内容区域 --> | 17 | <!-- 打卡内容区域 --> |
| 19 | <div class="section-wrapper"> | 18 | <div class="section-wrapper"> |
| 20 | - <div class="section-title">打卡内容</div> | 19 | + <div class="section-title">打卡留言</div> |
| 21 | <div class="section-content"> | 20 | <div class="section-content"> |
| 22 | <!-- 文本输入区域 --> | 21 | <!-- 文本输入区域 --> |
| 23 | <div class="text-input-area"> | 22 | <div class="text-input-area"> |
| ... | @@ -26,7 +25,7 @@ | ... | @@ -26,7 +25,7 @@ |
| 26 | rows="6" | 25 | rows="6" |
| 27 | autosize | 26 | autosize |
| 28 | type="textarea" | 27 | type="textarea" |
| 29 | - :placeholder="activeType === 'text' ? '请输入打卡内容,至少需要10个字符' : '请输入打卡留言(可选)'" | 28 | + :placeholder="activeType === 'text' ? '请输入打卡留言,至少需要10个字符' : '请输入打卡留言(可选)'" |
| 30 | :maxlength="activeType === 'text' ? 500 : 200" | 29 | :maxlength="activeType === 'text' ? 500 : 200" |
| 31 | show-word-limit | 30 | show-word-limit |
| 32 | /> | 31 | /> |
| ... | @@ -229,6 +228,79 @@ const getTaskDetail = async (month) => { | ... | @@ -229,6 +228,79 @@ const getTaskDetail = async (month) => { |
| 229 | const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }) | 228 | const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }) |
| 230 | if (code) { | 229 | if (code) { |
| 231 | taskDetail.value = data | 230 | taskDetail.value = data |
| 231 | + | ||
| 232 | + // TODO: 临时 mock 富文本数据,后续替换为真实数据 | ||
| 233 | + // if (!taskDetail.value.description) { | ||
| 234 | + // taskDetail.value.description = ` | ||
| 235 | + // <div class="rich-content"> | ||
| 236 | + // <h2 style="color: #2563eb; margin-bottom: 16px; font-size: 20px; font-weight: 600;">📚 本周学习任务</h2> | ||
| 237 | + | ||
| 238 | + // <p style="margin-bottom: 16px; line-height: 1.6; color: #374151;"> | ||
| 239 | + // 同学们好!本周我们将学习<strong style="color: #dc2626;">Vue3 组合式 API</strong>的核心概念。请大家认真完成以下任务: | ||
| 240 | + // </p> | ||
| 241 | + | ||
| 242 | + // <div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); padding: 16px; border-radius: 12px; margin: 16px 0; border-left: 4px solid #f59e0b;"> | ||
| 243 | + // <h3 style="color: #92400e; margin: 0 0 8px 0; font-size: 16px;">⚠️ 重要提醒</h3> | ||
| 244 | + // <p style="margin: 0; color: #78350f;">请在截止日期前提交作业,逾期将影响成绩评定。</p> | ||
| 245 | + // </div> | ||
| 246 | + | ||
| 247 | + // <h3 style="color: #059669; margin: 24px 0 12px 0; font-size: 18px;">📋 具体要求:</h3> | ||
| 248 | + | ||
| 249 | + // <ol style="margin: 16px 0; padding-left: 24px; color: #374151; line-height: 1.8;"> | ||
| 250 | + // <li style="margin-bottom: 8px;"> | ||
| 251 | + // <strong>理论学习:</strong>观看课程视频,理解 <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px; color: #dc2626;">ref</code> 和 <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px; color: #dc2626;">reactive</code> 的区别 | ||
| 252 | + // </li> | ||
| 253 | + // <li style="margin-bottom: 8px;"> | ||
| 254 | + // <strong>实践操作:</strong>完成课后练习,创建一个简单的计数器组件 | ||
| 255 | + // </li> | ||
| 256 | + // <li style="margin-bottom: 8px;"> | ||
| 257 | + // <strong>拓展思考:</strong>思考组合式 API 相比选项式 API 的优势 | ||
| 258 | + // </li> | ||
| 259 | + // </ol> | ||
| 260 | + | ||
| 261 | + // <div style="text-align: center; margin: 24px 0;"> | ||
| 262 | + // <img src="https://cdn.ipadbiz.cn/mlaj/images/vue3-composition-api.jpg" | ||
| 263 | + // alt="Vue3 组合式 API 示意图" | ||
| 264 | + // style="max-width: 100%; height: auto; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" /> | ||
| 265 | + // <p style="margin-top: 8px; color: #6b7280; font-size: 14px; font-style: italic;"> | ||
| 266 | + // Vue3 组合式 API 架构图 | ||
| 267 | + // </p> | ||
| 268 | + // </div> | ||
| 269 | + | ||
| 270 | + // <div style="background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); padding: 16px; border-radius: 12px; margin: 20px 0;"> | ||
| 271 | + // <h4 style="color: #1e40af; margin: 0 0 12px 0; font-size: 16px;">💡 学习提示</h4> | ||
| 272 | + // <ul style="margin: 0; padding-left: 20px; color: #1e3a8a;"> | ||
| 273 | + // <li style="margin-bottom: 6px;">可以参考官方文档进行学习</li> | ||
| 274 | + // <li style="margin-bottom: 6px;">建议先理解概念,再动手实践</li> | ||
| 275 | + // <li style="margin-bottom: 6px;">遇到问题可以在群里讨论</li> | ||
| 276 | + // </ul> | ||
| 277 | + // </div> | ||
| 278 | + | ||
| 279 | + // <blockquote style="border-left: 4px solid #10b981; background: #f0fdf4; padding: 16px; margin: 20px 0; border-radius: 0 8px 8px 0;"> | ||
| 280 | + // <p style="margin: 0; color: #065f46; font-style: italic; line-height: 1.6;"> | ||
| 281 | + // "学而时习之,不亦说乎?" —— 希望大家能够在实践中加深对知识的理解。 | ||
| 282 | + // </p> | ||
| 283 | + // </blockquote> | ||
| 284 | + | ||
| 285 | + // <div style="text-align: center; margin: 24px 0;"> | ||
| 286 | + // <img src="https://cdn.ipadbiz.cn/mlaj/images/coding-example.png" | ||
| 287 | + // alt="代码示例" | ||
| 288 | + // style="max-width: 100%; height: auto; border-radius: 8px; border: 2px solid #e5e7eb;" /> | ||
| 289 | + // </div> | ||
| 290 | + | ||
| 291 | + // <div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 16px; margin: 20px 0;"> | ||
| 292 | + // <h4 style="color: #dc2626; margin: 0 0 8px 0; font-size: 16px;">📅 截止时间</h4> | ||
| 293 | + // <p style="margin: 0; color: #991b1b; font-weight: 500;"> | ||
| 294 | + // 2024年12月31日 23:59 | ||
| 295 | + // </p> | ||
| 296 | + // </div> | ||
| 297 | + | ||
| 298 | + // <p style="margin-top: 24px; color: #6b7280; text-align: center; font-size: 14px;"> | ||
| 299 | + // 祝大家学习愉快!有问题随时联系老师 👨🏫 | ||
| 300 | + // </p> | ||
| 301 | + // </div> | ||
| 302 | + // ` | ||
| 303 | + // } | ||
| 232 | } | 304 | } |
| 233 | } | 305 | } |
| 234 | 306 | ||
| ... | @@ -297,6 +369,121 @@ onMounted(async () => { | ... | @@ -297,6 +369,121 @@ onMounted(async () => { |
| 297 | color: #666; | 369 | color: #666; |
| 298 | line-height: 1.6; | 370 | line-height: 1.6; |
| 299 | font-size: 0.95rem; | 371 | font-size: 0.95rem; |
| 372 | + | ||
| 373 | + // 富文本内容样式 | ||
| 374 | + // :deep(.rich-content) { | ||
| 375 | + // h1, h2, h3, h4, h5, h6 { | ||
| 376 | + // margin: 16px 0 12px 0; | ||
| 377 | + // font-weight: 600; | ||
| 378 | + // line-height: 1.4; | ||
| 379 | + // } | ||
| 380 | + | ||
| 381 | + // h2 { | ||
| 382 | + // font-size: 20px; | ||
| 383 | + // color: #2563eb; | ||
| 384 | + // } | ||
| 385 | + | ||
| 386 | + // h3 { | ||
| 387 | + // font-size: 18px; | ||
| 388 | + // color: #059669; | ||
| 389 | + // } | ||
| 390 | + | ||
| 391 | + // h4 { | ||
| 392 | + // font-size: 16px; | ||
| 393 | + // } | ||
| 394 | + | ||
| 395 | + // p { | ||
| 396 | + // margin: 12px 0; | ||
| 397 | + // line-height: 1.6; | ||
| 398 | + // color: #374151; | ||
| 399 | + // } | ||
| 400 | + | ||
| 401 | + // strong { | ||
| 402 | + // font-weight: 600; | ||
| 403 | + // color: #dc2626; | ||
| 404 | + // } | ||
| 405 | + | ||
| 406 | + // code { | ||
| 407 | + // background: #f3f4f6; | ||
| 408 | + // padding: 2px 6px; | ||
| 409 | + // border-radius: 4px; | ||
| 410 | + // color: #dc2626; | ||
| 411 | + // font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | ||
| 412 | + // font-size: 0.9em; | ||
| 413 | + // } | ||
| 414 | + | ||
| 415 | + // ol, ul { | ||
| 416 | + // margin: 16px 0; | ||
| 417 | + // padding-left: 24px; | ||
| 418 | + | ||
| 419 | + // li { | ||
| 420 | + // margin-bottom: 8px; | ||
| 421 | + // line-height: 1.8; | ||
| 422 | + // } | ||
| 423 | + // } | ||
| 424 | + | ||
| 425 | + // blockquote { | ||
| 426 | + // border-left: 4px solid #10b981; | ||
| 427 | + // background: #f0fdf4; | ||
| 428 | + // padding: 16px; | ||
| 429 | + // margin: 20px 0; | ||
| 430 | + // border-radius: 0 8px 8px 0; | ||
| 431 | + | ||
| 432 | + // p { | ||
| 433 | + // margin: 0; | ||
| 434 | + // color: #065f46; | ||
| 435 | + // font-style: italic; | ||
| 436 | + // } | ||
| 437 | + // } | ||
| 438 | + | ||
| 439 | + // img { | ||
| 440 | + // max-width: 100%; | ||
| 441 | + // height: auto; | ||
| 442 | + // border-radius: 8px; | ||
| 443 | + // margin: 12px 0; | ||
| 444 | + // display: block; | ||
| 445 | + // } | ||
| 446 | + | ||
| 447 | + // // 特殊样式容器 | ||
| 448 | + // .warning-box { | ||
| 449 | + // background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | ||
| 450 | + // padding: 16px; | ||
| 451 | + // border-radius: 12px; | ||
| 452 | + // margin: 16px 0; | ||
| 453 | + // border-left: 4px solid #f59e0b; | ||
| 454 | + // } | ||
| 455 | + | ||
| 456 | + // .info-box { | ||
| 457 | + // background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); | ||
| 458 | + // padding: 16px; | ||
| 459 | + // border-radius: 12px; | ||
| 460 | + // margin: 20px 0; | ||
| 461 | + // } | ||
| 462 | + | ||
| 463 | + // .deadline-box { | ||
| 464 | + // background: #fef2f2; | ||
| 465 | + // border: 1px solid #fecaca; | ||
| 466 | + // border-radius: 8px; | ||
| 467 | + // padding: 16px; | ||
| 468 | + // margin: 20px 0; | ||
| 469 | + // } | ||
| 470 | + | ||
| 471 | + // // 图片容器居中 | ||
| 472 | + // div[style*="text-align: center"] { | ||
| 473 | + // text-align: center; | ||
| 474 | + | ||
| 475 | + // img { | ||
| 476 | + // margin: 12px auto; | ||
| 477 | + // } | ||
| 478 | + | ||
| 479 | + // p { | ||
| 480 | + // color: #6b7280; | ||
| 481 | + // font-size: 14px; | ||
| 482 | + // font-style: italic; | ||
| 483 | + // margin-top: 8px; | ||
| 484 | + // } | ||
| 485 | + // } | ||
| 486 | + // } | ||
| 300 | } | 487 | } |
| 301 | 488 | ||
| 302 | .no-description { | 489 | .no-description { | ... | ... |
-
Please register or login to post a comment