diff --git a/frondend/src/utils/obsUpload.ts b/frondend/src/utils/obsUpload.ts new file mode 100644 index 0000000..44c6b66 --- /dev/null +++ b/frondend/src/utils/obsUpload.ts @@ -0,0 +1,47 @@ +/** + * 上传到今日校园 OBS 时的文件名处理。 + * + * 背景(对真实 OBS 的 5 组对照实测): + * file 部分带 filename → 204 成功 + * file 部分无 filename → 400 失败 + * 与 Content-Type 无关 + * + * 浏览器对 FormData.append(name, Blob) 会按规范自动补 filename="blob", + * 因此 `append('file', file)` 在多数浏览器能通过;但自动补出的名字无扩展名, + * 且部分环境(老 WebView、某些 App 内置浏览器)不补,直接导致 400。 + * 所以这里统一显式给出带扩展名的文件名。 + */ + +const MIME_EXT: Record = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + 'image/gif': 'gif', + 'image/bmp': 'bmp', + 'image/heic': 'heic', +} + +/** 从 MIME 推断扩展名,未知则回退 jpg(后端默认按 image/jpeg 处理)。 */ +export function extFromMime(mime?: string): string { + if (!mime) return 'jpg' + const key = mime.split(';')[0].trim().toLowerCase() + return MIME_EXT[key] || 'jpg' +} + +/** + * 为上传到 OBS 的文件生成文件名。 + * 优先用原始文件名(保留扩展名),否则按 MIME 生成。 + */ +export function resolveUploadFileName(file: File | Blob, fallbackPrefix = 'image'): string { + const anyFile = file as File + const original = typeof anyFile?.name === 'string' ? anyFile.name.trim() : '' + if (original) { + // 有扩展名直接用;否则补一个 + if (/\.[a-zA-Z0-9]{1,8}$/.test(original)) return original + return `${original}.${extFromMime(file.type)}` + } + const ext = extFromMime(file.type) + const stamp = Date.now().toString(36) + return `${fallbackPrefix}_${stamp}.${ext}` +} diff --git a/frondend/src/views/home/HomeView.vue b/frondend/src/views/home/HomeView.vue index 6db50a9..0460c79 100644 --- a/frondend/src/views/home/HomeView.vue +++ b/frondend/src/views/home/HomeView.vue @@ -7,6 +7,7 @@ import AppDialog from '@/components/ui/dialog/AppDialog.vue' import PinnedNotice from '@/components/PinnedNotice.vue' import QrScanner from '@/components/QrScanner.vue' import jsQR from 'jsqr' +import { resolveUploadFileName } from '@/utils/obsUpload' import * as authApi from '@/api/auth' import * as announceApi from '@/api/announcement' import { CPD_PASSWORD_ERROR } from '@/api' @@ -812,7 +813,13 @@ async function handleImageUpload(index: number, isEdit: boolean) { obsForm.append('policy', policy.policy || policy.Policy) obsForm.append('content-type', file.type || 'image/jpeg') } - obsForm.append('file', file) + // file 部分必须显式带 filename。 + // 实测(对真实 OBS 的 5 组对照):file 无 filename 时 OBS 直接返回 400, + // 有 filename 才返回 204;与 Content-Type 无关。 + // 浏览器对 append(name, Blob) 虽会自动补 filename="blob",但部分环境 + // 不补或补成无扩展名,故这里显式给出带扩展名的文件名。 + const fileName = resolveUploadFileName(file) + obsForm.append('file', file, fileName) const obsRes = await fetch(policy.host || policy.Host, { method: 'POST', diff --git a/frondend/src/views/leave/LeaveView.vue b/frondend/src/views/leave/LeaveView.vue index 463f1c9..fe86fe1 100644 --- a/frondend/src/views/leave/LeaveView.vue +++ b/frondend/src/views/leave/LeaveView.vue @@ -797,6 +797,7 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { useRouter } from 'vue-router' import { CalendarCheck, CalendarPlus, CalendarClock, LoaderCircle, Trash2, X, MapPin, Upload, MessageSquare, Smartphone, Edit3 } from 'lucide-vue-next' +import { resolveUploadFileName } from '@/utils/obsUpload' import * as leaveApi from '@/api/leave' import * as authApi from '@/api/auth' import type { LeaveItem, LeaveReportPlace, LeaveType } from '@/api/leave' @@ -1031,7 +1032,8 @@ async function uploadImageToObs(file: File): Promise { obsForm.append('policy', policy.policy || policy.Policy) obsForm.append('content-type', file.type || 'image/jpeg') } - obsForm.append('file', file) + // file 部分必须显式带 filename,否则 OBS 返回 400(实测结论,与 Content-Type 无关) + obsForm.append('file', file, resolveUploadFileName(file)) const obsRes = await fetch(policy.host || policy.Host, { method: 'POST',