feat(attachment): 添加动态文件大小限制功能
实现附件类型配置的归一化处理,支持多种后端格式 添加文件大小限制映射和计算逻辑 更新上传组件以显示动态限制大小 添加相关测试用例
Showing
6 changed files
with
373 additions
and
43 deletions
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-06 09:26:16 | 2 | * @Date: 2025-06-06 09:26:16 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-17 10:59:17 | 4 | + * @LastEditTime: 2026-01-21 13:15:07 |
| 5 | * @FilePath: /mlaj/src/api/checkin.js | 5 | * @FilePath: /mlaj/src/api/checkin.js |
| 6 | * @Description: 签到模块相关接口 | 6 | * @Description: 签到模块相关接口 |
| 7 | */ | 7 | */ |
| ... | @@ -39,12 +39,12 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param | ... | @@ -39,12 +39,12 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param |
| 39 | * @param: month 月份 | 39 | * @param: month 月份 |
| 40 | * @param: subtask_id 小作业ID | 40 | * @param: subtask_id 小作业ID |
| 41 | * @returns data: { | 41 | * @returns data: { |
| 42 | - * id 大作业id, cover 封面图, title 大作业名称, note 大作业描述, frequency 交作业的频次, cycle 交作业的周期 {0=本周期 | 30=每月 | 7=每周 | 1=每日}, | 42 | + * id 大作业id, cover 封面图, title 大作业名称, note 大作业描述, frequency 交作业的频次, cycle 交作业的周期 {0=本周期 | 30=每月 | 7=每周 | 1=每日}, |
| 43 | - * attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频], begin_date 开始时间, end_date 结束时间, | 43 | + * attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频], begin_date 开始时间, end_date 结束时间, |
| 44 | - * task_type 任务类型 [checkin=签到 | upload=上传附件 | count=计数], is_gray 作业是否应该置灰, is_finish 作业在当前周期是否已经达标, | 44 | + * task_type 任务类型 [checkin=签到 | upload=上传附件 | count=计数], is_gray 作业是否应该置灰, is_finish 作业在当前周期是否已经达标, |
| 45 | - * my_checkin_dates[] 我在日历中打过卡的日期, makeup_checkin_dates[] 我在日历中,可以补卡的日期, target_number 打卡的目标数量, | 45 | + * my_checkin_dates[] 我在日历中打过卡的日期, makeup_checkin_dates[] 我在日历中,可以补卡的日期, target_number 打卡的目标数量, |
| 46 | - * checkin_number 已经打卡的数量, checkin_avatars 最后打卡的10个人的头像, my_today_gratitude_count 我在今天的感恩次数, my_total_gratitude_count 我累计感恩次数, | 46 | + * checkin_number 已经打卡的数量, checkin_avatars 最后打卡的10个人的头像, my_today_gratitude_count 我在今天的感恩次数, my_total_gratitude_count 我累计感恩次数, |
| 47 | - * subtask_list 小作业列表 [{id,title,cycle,frequency,attachment_type,begin_date,end_date,is_finish}] , | 47 | + * subtask_list 小作业列表 [{id,title,cycle,frequency,attachment_type,begin_date,end_date,is_finish}] , |
| 48 | * } | 48 | * } |
| 49 | */ | 49 | */ |
| 50 | export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params)) | 50 | export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params)) |
| ... | @@ -57,7 +57,8 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p | ... | @@ -57,7 +57,8 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p |
| 57 | * id 作业id, | 57 | * id 作业id, |
| 58 | * title 作业名称 , | 58 | * title 作业名称 , |
| 59 | * cycle 作业周期 [0=本周期 | 30=每月 | 7=每周 | 1=每日], | 59 | * cycle 作业周期 [0=本周期 | 30=每月 | 7=每周 | 1=每日], |
| 60 | - * frequency 交作业的频次,attachment_type 上传附件的类型 [text=文本 image=图片 video=视频 audio=音频], | 60 | + * frequency 交作业的频次, |
| 61 | + * attachment_type array [object] 提交类型 [{type,max_size}] type string 提交类型 [text=文本 image=图片 video=视频 audio=音频], max_size number 文件的最大尺寸(MB) | ||
| 61 | * begin_date 开始时间, | 62 | * begin_date 开始时间, |
| 62 | * end_date 结束时间, | 63 | * end_date 结束时间, |
| 63 | * is_finish 作业在当前周期是否已经达标, | 64 | * is_finish 作业在当前周期是否已经达标, | ... | ... |
src/composables/__tests__/useCheckin.test.js
0 → 100644
| 1 | +import { describe, expect, it, vi, beforeEach } from 'vitest' | ||
| 2 | + | ||
| 3 | +vi.mock('vue-router', () => { | ||
| 4 | + return { | ||
| 5 | + useRoute: () => ({ | ||
| 6 | + query: {} | ||
| 7 | + }), | ||
| 8 | + useRouter: () => ({ | ||
| 9 | + push: vi.fn() | ||
| 10 | + }) | ||
| 11 | + } | ||
| 12 | +}) | ||
| 13 | + | ||
| 14 | +vi.mock('vant', () => { | ||
| 15 | + return { | ||
| 16 | + showToast: vi.fn(), | ||
| 17 | + showLoadingToast: vi.fn(() => ({ close: vi.fn() })) | ||
| 18 | + } | ||
| 19 | +}) | ||
| 20 | + | ||
| 21 | +vi.mock('@/api/common', () => { | ||
| 22 | + return { | ||
| 23 | + qiniuTokenAPI: vi.fn(), | ||
| 24 | + qiniuUploadAPI: vi.fn(), | ||
| 25 | + saveFileAPI: vi.fn() | ||
| 26 | + } | ||
| 27 | +}) | ||
| 28 | + | ||
| 29 | +vi.mock('@/api/checkin', () => { | ||
| 30 | + return { | ||
| 31 | + addUploadTaskAPI: vi.fn(), | ||
| 32 | + getUploadTaskInfoAPI: vi.fn(), | ||
| 33 | + editUploadTaskInfoAPI: vi.fn() | ||
| 34 | + } | ||
| 35 | +}) | ||
| 36 | + | ||
| 37 | +vi.mock('@/utils/qiniuFileHash', () => { | ||
| 38 | + return { | ||
| 39 | + qiniuFileHash: vi.fn(async () => 'hash') | ||
| 40 | + } | ||
| 41 | +}) | ||
| 42 | + | ||
| 43 | +vi.mock('@/contexts/auth', async () => { | ||
| 44 | + const { ref } = await import('vue') | ||
| 45 | + return { | ||
| 46 | + useAuth: () => ({ | ||
| 47 | + currentUser: ref({ mobile: '18800001111' }) | ||
| 48 | + }) | ||
| 49 | + } | ||
| 50 | +}) | ||
| 51 | + | ||
| 52 | +import { useCheckin } from '../useCheckin' | ||
| 53 | +import { showToast } from 'vant' | ||
| 54 | + | ||
| 55 | +describe('useCheckin 上传大小限制', () => { | ||
| 56 | + beforeEach(() => { | ||
| 57 | + vi.clearAllMocks() | ||
| 58 | + }) | ||
| 59 | + | ||
| 60 | + it('setMaxFileSizeMbMap 能更新并保留其他类型默认值', () => { | ||
| 61 | + const { activeType, maxFileSizeMb, setMaxFileSizeMbMap } = useCheckin() | ||
| 62 | + | ||
| 63 | + activeType.value = 'image' | ||
| 64 | + expect(maxFileSizeMb.value).toBe(20) | ||
| 65 | + | ||
| 66 | + setMaxFileSizeMbMap({ image: 500 }) | ||
| 67 | + expect(maxFileSizeMb.value).toBe(500) | ||
| 68 | + | ||
| 69 | + activeType.value = 'video' | ||
| 70 | + expect(maxFileSizeMb.value).toBe(20) | ||
| 71 | + }) | ||
| 72 | + | ||
| 73 | + it('setMaxFileSizeMbMap 会忽略非法值', () => { | ||
| 74 | + const { activeType, maxFileSizeMb, setMaxFileSizeMbMap } = useCheckin() | ||
| 75 | + | ||
| 76 | + activeType.value = 'audio' | ||
| 77 | + setMaxFileSizeMbMap({ audio: 0 }) | ||
| 78 | + expect(maxFileSizeMb.value).toBe(20) | ||
| 79 | + | ||
| 80 | + setMaxFileSizeMbMap({ audio: -10 }) | ||
| 81 | + expect(maxFileSizeMb.value).toBe(20) | ||
| 82 | + | ||
| 83 | + setMaxFileSizeMbMap({ audio: 'not_a_number' }) | ||
| 84 | + expect(maxFileSizeMb.value).toBe(20) | ||
| 85 | + | ||
| 86 | + setMaxFileSizeMbMap({ audio: 300 }) | ||
| 87 | + expect(maxFileSizeMb.value).toBe(300) | ||
| 88 | + }) | ||
| 89 | + | ||
| 90 | + it('beforeRead 超过动态大小会拦截并提示', () => { | ||
| 91 | + const { activeType, beforeRead, setMaxFileSizeMbMap } = useCheckin() | ||
| 92 | + | ||
| 93 | + activeType.value = 'image' | ||
| 94 | + setMaxFileSizeMbMap({ image: 1 }) | ||
| 95 | + | ||
| 96 | + const ok = beforeRead({ | ||
| 97 | + type: 'image/jpeg', | ||
| 98 | + size: 2 * 1024 * 1024 | ||
| 99 | + }) | ||
| 100 | + | ||
| 101 | + expect(ok).toBe(false) | ||
| 102 | + expect(showToast).toHaveBeenCalled() | ||
| 103 | + expect(String(showToast.mock.calls[0][0])).toContain('最大文件体积为1MB') | ||
| 104 | + }) | ||
| 105 | + | ||
| 106 | + it('beforeRead 图片类型校验失败会拦截', () => { | ||
| 107 | + const { activeType, beforeRead, setMaxFileSizeMbMap } = useCheckin() | ||
| 108 | + | ||
| 109 | + activeType.value = 'image' | ||
| 110 | + setMaxFileSizeMbMap({ image: 10 }) | ||
| 111 | + | ||
| 112 | + const ok = beforeRead({ | ||
| 113 | + type: 'application/pdf', | ||
| 114 | + size: 1 * 1024 * 1024 | ||
| 115 | + }) | ||
| 116 | + | ||
| 117 | + expect(ok).toBe(false) | ||
| 118 | + expect(showToast).toHaveBeenCalled() | ||
| 119 | + }) | ||
| 120 | +}) | ||
| 121 | + |
| ... | @@ -26,6 +26,38 @@ export function useCheckin() { | ... | @@ -26,6 +26,38 @@ export function useCheckin() { |
| 26 | const selectedTaskValue = ref([]) // 选中的任务值(Picker使用) | 26 | const selectedTaskValue = ref([]) // 选中的任务值(Picker使用) |
| 27 | const isMakeup = ref(false) // 是否为补录作业 | 27 | const isMakeup = ref(false) // 是否为补录作业 |
| 28 | const maxCount = ref(5) | 28 | const maxCount = ref(5) |
| 29 | + const maxFileSizeMbMap = ref({ | ||
| 30 | + image: 20, | ||
| 31 | + video: 20, | ||
| 32 | + audio: 20 | ||
| 33 | + }) | ||
| 34 | + | ||
| 35 | + const maxFileSizeMb = computed(() => { | ||
| 36 | + const type = String(activeType.value || '') | ||
| 37 | + const raw = maxFileSizeMbMap.value?.[type] | ||
| 38 | + const size = Number(raw) | ||
| 39 | + if (Number.isFinite(size) && size > 0) return size | ||
| 40 | + return 20 | ||
| 41 | + }) | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 设置最大文件大小映射 | ||
| 45 | + * @param {Object} map - 包含 image, video, audio 键的对象 | ||
| 46 | + */ | ||
| 47 | + | ||
| 48 | + const setMaxFileSizeMbMap = (map = {}) => { | ||
| 49 | + if (!map || typeof map !== 'object') return | ||
| 50 | + | ||
| 51 | + const next = { ...(maxFileSizeMbMap.value || {}) } | ||
| 52 | + for (const key of ['image', 'video', 'audio']) { | ||
| 53 | + const raw = map[key] | ||
| 54 | + const size = Number(raw) | ||
| 55 | + if (Number.isFinite(size) && size > 0) { | ||
| 56 | + next[key] = size | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + maxFileSizeMbMap.value = next | ||
| 60 | + } | ||
| 29 | 61 | ||
| 30 | // 打卡类型 | 62 | // 打卡类型 |
| 31 | const checkinType = computed(() => route.query.task_type) | 63 | const checkinType = computed(() => route.query.task_type) |
| ... | @@ -178,9 +210,10 @@ export function useCheckin() { | ... | @@ -178,9 +210,10 @@ export function useCheckin() { |
| 178 | const fileType = item.type.toLowerCase() | 210 | const fileType = item.type.toLowerCase() |
| 179 | 211 | ||
| 180 | // 文件大小检查 | 212 | // 文件大小检查 |
| 181 | - if ((item.size / 1024 / 1024).toFixed(2) > 20) { | 213 | + const file_size_mb = item.size / 1024 / 1024 |
| 214 | + if (Number.isFinite(file_size_mb) && file_size_mb > maxFileSizeMb.value) { | ||
| 182 | flag = false | 215 | flag = false |
| 183 | - showToast('最大文件体积为20MB') | 216 | + showToast(`最大文件体积为${maxFileSizeMb.value}MB`) |
| 184 | break | 217 | break |
| 185 | } | 218 | } |
| 186 | 219 | ||
| ... | @@ -532,11 +565,13 @@ export function useCheckin() { | ... | @@ -532,11 +565,13 @@ export function useCheckin() { |
| 532 | selectedTaskValue, | 565 | selectedTaskValue, |
| 533 | isMakeup, | 566 | isMakeup, |
| 534 | maxCount, | 567 | maxCount, |
| 568 | + maxFileSizeMb, | ||
| 535 | canSubmit, | 569 | canSubmit, |
| 536 | gratitudeCount, | 570 | gratitudeCount, |
| 537 | gratitudeFormList, | 571 | gratitudeFormList, |
| 538 | 572 | ||
| 539 | // 方法 | 573 | // 方法 |
| 574 | + setMaxFileSizeMbMap, | ||
| 540 | beforeRead, | 575 | beforeRead, |
| 541 | afterRead, | 576 | afterRead, |
| 542 | onDelete, | 577 | onDelete, | ... | ... |
| 1 | +import { describe, expect, it } from 'vitest' | ||
| 2 | +import { normalizeAttachmentTypeConfig } from '../tools' | ||
| 3 | + | ||
| 4 | +describe('normalizeAttachmentTypeConfig', () => { | ||
| 5 | + it('兼容旧数组结构: ["image","video"]', () => { | ||
| 6 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig(['image', 'video']) | ||
| 7 | + expect(options).toEqual([ | ||
| 8 | + { key: 'image', value: '图片' }, | ||
| 9 | + { key: 'video', value: '视频' } | ||
| 10 | + ]) | ||
| 11 | + expect(upload_size_limit_mb_map).toBeNull() | ||
| 12 | + }) | ||
| 13 | + | ||
| 14 | + it('兼容新数组结构: [{type,max_size}]', () => { | ||
| 15 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig([ | ||
| 16 | + { type: 'image', max_size: 500 }, | ||
| 17 | + { type: 'video', max_size: 1000 }, | ||
| 18 | + { type: 'audio', max_size: 300 } | ||
| 19 | + ]) | ||
| 20 | + | ||
| 21 | + expect(options).toEqual([ | ||
| 22 | + { key: 'image', value: '图片' }, | ||
| 23 | + { key: 'video', value: '视频' }, | ||
| 24 | + { key: 'audio', value: '音频' } | ||
| 25 | + ]) | ||
| 26 | + expect(upload_size_limit_mb_map).toEqual({ image: 500, video: 1000, audio: 300 }) | ||
| 27 | + }) | ||
| 28 | + | ||
| 29 | + it('兼容对象映射结构: [{image:500,video:1000}]', () => { | ||
| 30 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig([ | ||
| 31 | + { image: 500, video: 1000 } | ||
| 32 | + ]) | ||
| 33 | + | ||
| 34 | + expect(options).toEqual([ | ||
| 35 | + { key: 'image', value: '图片' }, | ||
| 36 | + { key: 'video', value: '视频' } | ||
| 37 | + ]) | ||
| 38 | + expect(upload_size_limit_mb_map).toEqual({ image: 500, video: 1000 }) | ||
| 39 | + }) | ||
| 40 | + | ||
| 41 | + it('兼容对象映射结构: {image:500,video:1000}', () => { | ||
| 42 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig({ | ||
| 43 | + image: 500, | ||
| 44 | + video: 1000 | ||
| 45 | + }) | ||
| 46 | + | ||
| 47 | + expect(options).toEqual([ | ||
| 48 | + { key: 'image', value: '图片' }, | ||
| 49 | + { key: 'video', value: '视频' } | ||
| 50 | + ]) | ||
| 51 | + expect(upload_size_limit_mb_map).toEqual({ image: 500, video: 1000 }) | ||
| 52 | + }) | ||
| 53 | + | ||
| 54 | + it('空值返回默认四种类型', () => { | ||
| 55 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig(null) | ||
| 56 | + expect(options.length).toBe(4) | ||
| 57 | + expect(upload_size_limit_mb_map).toBeNull() | ||
| 58 | + }) | ||
| 59 | +}) |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2022-04-18 15:59:42 | 2 | * @Date: 2022-04-18 15:59:42 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-21 10:51:15 | 4 | + * @LastEditTime: 2026-01-21 13:33:36 |
| 5 | * @FilePath: /mlaj/src/utils/tools.js | 5 | * @FilePath: /mlaj/src/utils/tools.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -152,6 +152,134 @@ const normalizeCheckinTaskItems = (list) => { | ... | @@ -152,6 +152,134 @@ const normalizeCheckinTaskItems = (list) => { |
| 152 | }); | 152 | }); |
| 153 | }; | 153 | }; |
| 154 | 154 | ||
| 155 | + | ||
| 156 | +/** | ||
| 157 | + * @description 归一化作业提交类型配置(兼容多种后端格式),并提取各类型的上传大小限制(MB)。 | ||
| 158 | + * @param {any} attachment_type 后端返回的 attachment_type 字段,可能是多种结构。 | ||
| 159 | + * @returns {{options: Array<{key: string, value: string}>, upload_size_limit_mb_map: (null|{image?: number, video?: number, audio?: number})}} | ||
| 160 | + */ | ||
| 161 | +const normalizeAttachmentTypeConfig = (attachment_type) => { | ||
| 162 | + const type_map = { | ||
| 163 | + text: '文本', | ||
| 164 | + image: '图片', | ||
| 165 | + audio: '音频', | ||
| 166 | + video: '视频' | ||
| 167 | + }; | ||
| 168 | + | ||
| 169 | + // 支持的类型 key(用于过滤未知字段,避免误把 max_size / 其他字段当成类型) | ||
| 170 | + const known_type_keys = ['text', 'image', 'audio', 'video']; | ||
| 171 | + const upload_size_limit_mb_map = {}; | ||
| 172 | + let options = []; | ||
| 173 | + | ||
| 174 | + if (Array.isArray(attachment_type)) { | ||
| 175 | + const first = attachment_type[0]; | ||
| 176 | + if (first && typeof first === 'object' && !Array.isArray(first)) { | ||
| 177 | + // 情况1:新结构(数组对象风格),例如: | ||
| 178 | + // [{ type: 'image', max_size: 500 }, { type: 'video', max_size: 1000 }] | ||
| 179 | + const has_type_style = attachment_type.some( | ||
| 180 | + (item) => item && typeof item === 'object' && !Array.isArray(item) && ('type' in item || 'max_size' in item) | ||
| 181 | + ); | ||
| 182 | + | ||
| 183 | + if (has_type_style) { | ||
| 184 | + // 生成“可选类型”列表 | ||
| 185 | + options = attachment_type | ||
| 186 | + .map((item) => { | ||
| 187 | + const key = item?.type || item?.key || item?.name || ''; | ||
| 188 | + return { | ||
| 189 | + key, | ||
| 190 | + value: type_map[key] || key | ||
| 191 | + }; | ||
| 192 | + }) | ||
| 193 | + .filter((item) => !!item.key); | ||
| 194 | + | ||
| 195 | + // 提取上传大小限制(只处理 image/video/audio) | ||
| 196 | + attachment_type.forEach((item) => { | ||
| 197 | + const key = item?.type || item?.key || item?.name; | ||
| 198 | + const size = Number(item?.max_size); | ||
| 199 | + if ((key === 'image' || key === 'video' || key === 'audio') && Number.isFinite(size) && size > 0) { | ||
| 200 | + upload_size_limit_mb_map[key] = size; | ||
| 201 | + } | ||
| 202 | + }); | ||
| 203 | + } else { | ||
| 204 | + // 情况2:映射结构(数组里装对象映射),例如: | ||
| 205 | + // [{ image: 500, video: 500 }] | ||
| 206 | + // 这里先合并为一个对象,再统一处理 | ||
| 207 | + const merged = {}; | ||
| 208 | + attachment_type.forEach((item) => { | ||
| 209 | + if (item && typeof item === 'object' && !Array.isArray(item)) { | ||
| 210 | + Object.assign(merged, item); | ||
| 211 | + } | ||
| 212 | + }); | ||
| 213 | + | ||
| 214 | + // 映射结构里 key 就是类型,value 就是大小(MB) | ||
| 215 | + const keys = Object.keys(merged).filter((k) => known_type_keys.includes(k)); | ||
| 216 | + options = keys.map((key) => ({ | ||
| 217 | + key, | ||
| 218 | + value: type_map[key] || key | ||
| 219 | + })); | ||
| 220 | + | ||
| 221 | + ['image', 'video', 'audio'].forEach((key) => { | ||
| 222 | + const size = Number(merged[key]); | ||
| 223 | + if (Number.isFinite(size) && size > 0) { | ||
| 224 | + upload_size_limit_mb_map[key] = size; | ||
| 225 | + } | ||
| 226 | + }); | ||
| 227 | + } | ||
| 228 | + } else { | ||
| 229 | + // 情况3:旧结构(字符串数组),例如:['image', 'video'] | ||
| 230 | + options = attachment_type.map((key) => ({ | ||
| 231 | + key, | ||
| 232 | + value: type_map[key] || key | ||
| 233 | + })); | ||
| 234 | + } | ||
| 235 | + } else if (attachment_type && typeof attachment_type === 'object') { | ||
| 236 | + // 情况4:对象结构(可能是类型中文映射,也可能是类型->大小) | ||
| 237 | + const keys = Object.keys(attachment_type); | ||
| 238 | + const has_size_mapping = keys.some((k) => (k === 'image' || k === 'video' || k === 'audio') && Number.isFinite(Number(attachment_type[k]))); | ||
| 239 | + | ||
| 240 | + if (has_size_mapping) { | ||
| 241 | + // 类型 -> 大小(MB),例如:{ image: 500, video: 1000 } | ||
| 242 | + options = keys | ||
| 243 | + .filter((k) => known_type_keys.includes(k)) | ||
| 244 | + .map((key) => ({ | ||
| 245 | + key, | ||
| 246 | + value: type_map[key] || key | ||
| 247 | + })); | ||
| 248 | + | ||
| 249 | + ['image', 'video', 'audio'].forEach((key) => { | ||
| 250 | + const size = Number(attachment_type[key]); | ||
| 251 | + if (Number.isFinite(size) && size > 0) { | ||
| 252 | + upload_size_limit_mb_map[key] = size; | ||
| 253 | + } | ||
| 254 | + }); | ||
| 255 | + } else { | ||
| 256 | + // 类型 -> 中文显示,或其他自定义结构,例如:{ image: '图片', video: '视频' } | ||
| 257 | + options = Object.entries(attachment_type).map(([key, value]) => ({ | ||
| 258 | + key, | ||
| 259 | + value | ||
| 260 | + })); | ||
| 261 | + } | ||
| 262 | + } else { | ||
| 263 | + options = []; | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + if (options.length === 0) { | ||
| 267 | + // 兜底:后端未配置时,默认给出四种类型 | ||
| 268 | + options = [ | ||
| 269 | + { key: 'text', value: '文本' }, | ||
| 270 | + { key: 'image', value: '图片' }, | ||
| 271 | + { key: 'audio', value: '音频' }, | ||
| 272 | + { key: 'video', value: '视频' } | ||
| 273 | + ]; | ||
| 274 | + } | ||
| 275 | + | ||
| 276 | + return { | ||
| 277 | + options, | ||
| 278 | + // 没解析到任何大小限制时,返回 null,避免覆盖 useCheckin 内部默认值 | ||
| 279 | + upload_size_limit_mb_map: Object.keys(upload_size_limit_mb_map).length ? upload_size_limit_mb_map : null | ||
| 280 | + }; | ||
| 281 | +}; | ||
| 282 | + | ||
| 155 | export { | 283 | export { |
| 156 | formatDate, | 284 | formatDate, |
| 157 | wxInfo, | 285 | wxInfo, |
| ... | @@ -162,4 +290,5 @@ export { | ... | @@ -162,4 +290,5 @@ export { |
| 162 | stringifyQuery, | 290 | stringifyQuery, |
| 163 | formatDuration, | 291 | formatDuration, |
| 164 | normalizeCheckinTaskItems, | 292 | normalizeCheckinTaskItems, |
| 293 | + normalizeAttachmentTypeConfig, | ||
| 165 | }; | 294 | }; | ... | ... |
| ... | @@ -88,7 +88,7 @@ | ... | @@ -88,7 +88,7 @@ |
| 88 | 88 | ||
| 89 | <!-- 文件上传区域 --> | 89 | <!-- 文件上传区域 --> |
| 90 | <div v-if="activeType !== '' && activeType !== 'text'" class="upload-area"> | 90 | <div v-if="activeType !== '' && activeType !== 'text'" class="upload-area"> |
| 91 | - <van-uploader v-model="fileList" :max-count="maxCount" :max-size="20 * 1024 * 1024" | 91 | + <van-uploader v-model="fileList" :max-count="maxCount" :max-size="maxFileSizeBytes" |
| 92 | :before-read="beforeRead" :after-read="afterRead" @delete="onDelete" | 92 | :before-read="beforeRead" :after-read="afterRead" @delete="onDelete" |
| 93 | @click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file" | 93 | @click-preview="onClickPreview" multiple :accept="getAcceptType()" result-type="file" |
| 94 | :deletable="true" upload-icon="plus" /> | 94 | :deletable="true" upload-icon="plus" /> |
| ... | @@ -106,7 +106,7 @@ | ... | @@ -106,7 +106,7 @@ |
| 106 | </div> --> | 106 | </div> --> |
| 107 | 107 | ||
| 108 | <div class="upload-tips"> | 108 | <div class="upload-tips"> |
| 109 | - <div class="tip-text">最多上传{{ maxCount }}个文件,每个不超过20M</div> | 109 | + <div class="tip-text">最多上传{{ maxCount }}个文件,每个不超过{{ maxFileSizeMb }}MB</div> |
| 110 | <div class="tip-text">{{ getUploadTips() }}</div> | 110 | <div class="tip-text">{{ getUploadTips() }}</div> |
| 111 | </div> | 111 | </div> |
| 112 | </div> | 112 | </div> |
| ... | @@ -174,9 +174,9 @@ | ... | @@ -174,9 +174,9 @@ |
| 174 | import { ref, computed, onMounted, nextTick, reactive, watch } from 'vue' | 174 | import { ref, computed, onMounted, nextTick, reactive, watch } from 'vue' |
| 175 | import { useRoute, useRouter } from 'vue-router' | 175 | import { useRoute, useRouter } from 'vue-router' |
| 176 | import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI, reuseGratitudeFormAPI } from "@/api/checkin" | 176 | import { getTaskDetailAPI, getUploadTaskInfoAPI, getSubtaskListAPI, reuseGratitudeFormAPI } from "@/api/checkin" |
| 177 | -import { getTeacherFindSettingsAPI } from '@/api/teacher' | ||
| 178 | import { useTitle } from '@vueuse/core' | 177 | import { useTitle } from '@vueuse/core' |
| 179 | import { useCheckin } from '@/composables/useCheckin' | 178 | import { useCheckin } from '@/composables/useCheckin' |
| 179 | +import { normalizeAttachmentTypeConfig } from '@/utils/tools' | ||
| 180 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' | 180 | import AudioPlayer from '@/components/ui/AudioPlayer.vue' |
| 181 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' | 181 | import VideoPlayer from '@/components/ui/VideoPlayer.vue' |
| 182 | import AddTargetDialog from '@/components/count/AddTargetDialog.vue' | 182 | import AddTargetDialog from '@/components/count/AddTargetDialog.vue' |
| ... | @@ -200,7 +200,9 @@ const { | ... | @@ -200,7 +200,9 @@ const { |
| 200 | selectedTaskValue, | 200 | selectedTaskValue, |
| 201 | isMakeup, | 201 | isMakeup, |
| 202 | maxCount, | 202 | maxCount, |
| 203 | + maxFileSizeMb, | ||
| 203 | canSubmit, | 204 | canSubmit, |
| 205 | + setMaxFileSizeMbMap, | ||
| 204 | beforeRead, | 206 | beforeRead, |
| 205 | afterRead, | 207 | afterRead, |
| 206 | onDelete, | 208 | onDelete, |
| ... | @@ -218,6 +220,12 @@ const dynamicFieldText = ref('感恩') | ... | @@ -218,6 +220,12 @@ const dynamicFieldText = ref('感恩') |
| 218 | // 任务详情数据 | 220 | // 任务详情数据 |
| 219 | const taskDetail = ref({}) | 221 | const taskDetail = ref({}) |
| 220 | 222 | ||
| 223 | +const maxFileSizeBytes = computed(() => { | ||
| 224 | + const size = Number(maxFileSizeMb.value || 0) | ||
| 225 | + if (!Number.isFinite(size) || size <= 0) return 20 * 1024 * 1024 | ||
| 226 | + return Math.floor(size * 1024 * 1024) | ||
| 227 | +}) | ||
| 228 | + | ||
| 221 | // 显示的作业描述 | 229 | // 显示的作业描述 |
| 222 | const displayTaskNote = computed(() => { | 230 | const displayTaskNote = computed(() => { |
| 223 | const selected_subtask_id = selectedTaskValue.value?.[0] | 231 | const selected_subtask_id = selectedTaskValue.value?.[0] |
| ... | @@ -621,35 +629,12 @@ const getTaskDetail = async (month) => { | ... | @@ -621,35 +629,12 @@ const getTaskDetail = async (month) => { |
| 621 | * @param {Array|Object} attachmentType - 附件类型数据 | 629 | * @param {Array|Object} attachmentType - 附件类型数据 |
| 622 | */ | 630 | */ |
| 623 | const updateAttachmentTypeOptions = (attachmentType) => { | 631 | const updateAttachmentTypeOptions = (attachmentType) => { |
| 624 | - const typeMap = { | 632 | + const { options, upload_size_limit_mb_map } = normalizeAttachmentTypeConfig(attachmentType) |
| 625 | - 'text': '文本', | 633 | + attachmentTypeOptions.value = options |
| 626 | - 'image': '图片', | ||
| 627 | - 'audio': '音频', | ||
| 628 | - 'video': '视频' | ||
| 629 | - } | ||
| 630 | 634 | ||
| 631 | - if (Array.isArray(attachmentType)) { | 635 | + // 设置最大文件大小映射 |
| 632 | - attachmentTypeOptions.value = attachmentType.map(key => ({ | 636 | + if (upload_size_limit_mb_map) { |
| 633 | - key, | 637 | + setMaxFileSizeMbMap(upload_size_limit_mb_map) |
| 634 | - value: typeMap[key] || key | ||
| 635 | - })) | ||
| 636 | - } else if (attachmentType && typeof attachmentType === 'object') { | ||
| 637 | - attachmentTypeOptions.value = Object.entries(attachmentType).map(([key, value]) => ({ | ||
| 638 | - key, | ||
| 639 | - value | ||
| 640 | - })) | ||
| 641 | - } else { | ||
| 642 | - attachmentTypeOptions.value = [] | ||
| 643 | - } | ||
| 644 | - | ||
| 645 | - // 如果没有解析出任何类型,或者列表为空,则使用默认4种类型 | ||
| 646 | - if (attachmentTypeOptions.value.length === 0) { | ||
| 647 | - attachmentTypeOptions.value = [ | ||
| 648 | - { key: 'text', value: '文本' }, | ||
| 649 | - { key: 'image', value: '图片' }, | ||
| 650 | - { key: 'audio', value: '音频' }, | ||
| 651 | - { key: 'video', value: '视频' } | ||
| 652 | - ] | ||
| 653 | } | 638 | } |
| 654 | 639 | ||
| 655 | // 如果是计数打卡(count),过滤掉文本(text)类型 | 640 | // 如果是计数打卡(count),过滤掉文本(text)类型 |
| ... | @@ -957,7 +942,7 @@ onMounted(async () => { | ... | @@ -957,7 +942,7 @@ onMounted(async () => { |
| 957 | 942 | ||
| 958 | // 获取小作业列表 | 943 | // 获取小作业列表 |
| 959 | const subtask_list = await getSubtaskListAPI({ task_id: route.query.task_id, date: current_date }) | 944 | const subtask_list = await getSubtaskListAPI({ task_id: route.query.task_id, date: current_date }) |
| 960 | - if (subtask_list.code) { | 945 | + if (subtask_list.code === 1) { |
| 961 | taskOptions.value = [...subtask_list.data.map(item => ({ | 946 | taskOptions.value = [...subtask_list.data.map(item => ({ |
| 962 | text: item.is_makeup ? '补卡:' + item.title : item.title, | 947 | text: item.is_makeup ? '补卡:' + item.title : item.title, |
| 963 | value: item.id, | 948 | value: item.id, | ... | ... |
-
Please register or login to post a comment