hookehuyr

feat(attachment): 添加动态文件大小限制功能

实现附件类型配置的归一化处理,支持多种后端格式
添加文件大小限制映射和计算逻辑
更新上传组件以显示动态限制大小
添加相关测试用例
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 作业在当前周期是否已经达标,
......
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,
......