| | |
| | | emit("update:modelValue", ""); |
| | | }; |
| | | |
| | | /** |
| | | * 上传前压缩图片:按最大宽度等比缩放 + JPEG 质量压缩 |
| | | * 手机原图 2-5MB → 通常 200-500KB,避免被 nginx 的 client_max_body_size 拦截 |
| | | */ |
| | | const compressImage = (file, maxWidth = 1280, quality = 0.75) => |
| | | new Promise((resolve) => { |
| | | // 非图片或环境不支持时直接返回原文件 |
| | | if (!/^image\//.test(file.type) || typeof document === "undefined") { |
| | | resolve(file); |
| | | return; |
| | | } |
| | | const objectUrl = URL.createObjectURL(file); |
| | | const img = new Image(); |
| | | img.onload = () => { |
| | | URL.revokeObjectURL(objectUrl); |
| | | let width = img.naturalWidth || img.width; |
| | | let height = img.naturalHeight || img.height; |
| | | if (!width || !height) { |
| | | resolve(file); |
| | | return; |
| | | } |
| | | if (width > maxWidth) { |
| | | height = Math.round((height * maxWidth) / width); |
| | | width = maxWidth; |
| | | } |
| | | const canvas = document.createElement("canvas"); |
| | | canvas.width = width; |
| | | canvas.height = height; |
| | | canvas.getContext("2d").drawImage(img, 0, 0, width, height); |
| | | canvas.toBlob( |
| | | (blob) => resolve(blob || file), |
| | | "image/jpeg", |
| | | quality |
| | | ); |
| | | }; |
| | | img.onerror = () => { |
| | | URL.revokeObjectURL(objectUrl); |
| | | resolve(file); |
| | | }; |
| | | img.src = objectUrl; |
| | | }); |
| | | |
| | | const onChange = async (e) => { |
| | | const file = e.target.files && e.target.files[0]; |
| | | e.target.value = ""; |
| | |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | // 上传前先压缩:手机原图通常 2-5MB,压到 1280px 宽、质量 0.75 后一般 200-500KB |
| | | // 既省手机流量,也避免被 nginx 的 client_max_body_size 拦住 |
| | | const compressed = await compressImage(file); |
| | | const fd = new FormData(); |
| | | fd.append("file", file); |
| | | fd.append("file", compressed, file.name || "photo.jpg"); |
| | | const res = await axios.post(import.meta.env.VITE_APP_BASE_API + "/common/upload", fd, { |
| | | headers: { |
| | | Authorization: "Bearer " + getToken(), |
| | |
| | | }); |
| | | const body = res && res.data; |
| | | if (body && body.code === 200) { |
| | | emit("update:modelValue", body.url || body.fileName || ""); |
| | | // 后端返回值有两个坑: |
| | | // fileName = "upload/2026/10/27/x.jpg"(相对 profile 目录,缺 /profile 前缀) |
| | | // url = "http://10.136.58.73upload/..."(IP 后连斜杠都没有,是坏的) |
| | | // 所以这里统一规范成 /profile/upload/... 形式:相对路径,手机经端口转发也能访问 |
| | | let path = String(body.fileName || "").replace(/^\/+/, ""); |
| | | if (path && !path.startsWith("profile/")) { |
| | | path = "profile/" + path; |
| | | } |
| | | path = path ? "/" + path : ""; |
| | | emit("update:modelValue", path); |
| | | ElMessage.success("上传成功"); |
| | | } else { |
| | | ElMessage.error((body && body.msg) || "上传失败"); |
| | | } |
| | | } catch (err) { |
| | | ElMessage.error("上传失败,请检查网络后重试"); |
| | | const status = err && err.response ? err.response.status : 0; |
| | | if (status === 413) { |
| | | ElMessage.error("照片太大,上传被服务器拒绝,请压缩后重试"); |
| | | } else if (status === 401) { |
| | | ElMessage.error("登录已过期,请重新登录后再上传"); |
| | | } else if (status === 415) { |
| | | ElMessage.error("文件格式不支持,请选择图片文件"); |
| | | } else if (status) { |
| | | ElMessage.error("上传失败(HTTP " + status + "),请重试"); |
| | | } else { |
| | | ElMessage.error("上传失败,请检查网络后重试"); |
| | | } |
| | | } finally { |
| | | loading.value = false; |
| | | } |