This commit is contained in:
2025-09-04 21:21:53 +08:00
parent 19b6251011
commit 3ca2451b2f

View File

@@ -38,326 +38,293 @@
</template> </template>
<script> <script>
import { import { addRecord } from "@/api/inspection/record.js"
addRecord import { addWatermarkToImage } from "@/utils/watermark.js"
} from "@/api/inspection/record.js" import appConfig from "@/config"
// 如果你已有 uploadImg 封装且内部就是 uni.uploadFile可以继续用
// 但前提是它支持 H5 直接传 File 对象(不是 URL 字符串)。 // ------- URL 安全拼接,避免出现 /undefined/common/upload -------
// 这里我示范“直接用 uni.uploadFile”的写法更稳妥。 function joinURL(base, path) {
import { const b = (base || "").replace(/\/+$/, "")
addWatermarkToImage const p = (path || "").replace(/^\/+/, "")
} from "@/utils/watermark.js" return `${b}/${p}`
import appConfig from '@/config' }
const UPLOAD_URL = appConfig.baseUrl + "/common/upload" // 按你后端改 const BASE_URL = appConfig?.baseUrl || (process.env.VUE_APP_BASE_API || "")
const UPLOAD_URL = joinURL(BASE_URL, "/common/upload")
export default { export default {
data() { data() {
return { return {
form: { form: {
inspectionType: 0, inspectionType: 0,
inspectorId: this.$store.state.user.nickName, inspectorId: this.$store.state.user.nickName,
inspectionPoint: "", inspectionPoint: "",
inspectionRequirements: "", inspectionRequirements: "",
inspectionImg: "", inspectionImg: "",
ImgUrl: [], // 后端返回的文件名/URL ImgUrl: [], // 后端返回的文件名/URL
remark: "" remark: ""
}, },
img: [], // 绑定给 uni-file-picker 预览列表 img: [], // uni-file-picker 预览
sourceType: ['camera'], sourceType: ["camera"],
msgType: '', msgType: "",
messageText: '', messageText: "",
isUploading: false, isUploading: false,
isMobileBrowser: false isMobileBrowser: false
} }
}, },
methods: { methods: {
async submit() { async submit() {
if (this.isUploading) { if (this.isUploading) {
this.showMessage('warning', '图片正在上传中,请稍等') this.showMessage("warning", "图片正在上传中,请稍等")
return return
} }
if (!Array.isArray(this.form.ImgUrl) || this.form.ImgUrl.length === 0) { if (!Array.isArray(this.form.ImgUrl) || this.form.ImgUrl.length === 0) {
this.showMessage('error', '请选择要上传的图片') this.showMessage("error", "请选择要上传的图片")
return return
} }
this.form.inspectionImg = this.joinList() this.form.inspectionImg = this.joinList()
try { try {
const res = await addRecord(this.form) const res = await addRecord(this.form)
if (res.code === 200) { if (res.code === 200) {
this.showMessage('success', '打卡成功') this.showMessage("success", "打卡成功")
} else { } else {
this.showMessage('error', '打卡失败') this.showMessage("error", res.msg || "打卡失败")
} }
} catch (err) { } catch (err) {
this.showMessage('error', `提交失败: ${err.message}`) this.showMessage("error", `提交失败: ${err.message}`)
} }
}, },
// uni-file-picker 删除e.tempFile 里通常有 {url/path},与你传给 value 的结构需一致 // 删除预览 & 同步删除结果
deleteImg(e) { deleteImg(e) {
const url = e.tempFile?.url || e.tempFile?.path const url = e?.tempFile?.url || e?.tempFile?.path
const idx = this.img.findIndex(x => x.url === url) const idx = this.img.findIndex((x) => x.url === url)
if (idx !== -1) { if (idx !== -1) {
this.img.splice(idx, 1) // 释放预览 URL 内存
// 同步移除已上传结果(按你的业务,可以用同索引移除或按返回名移除) try {
this.form.ImgUrl.splice(idx, 1) const toRevoke = this.img[idx]?.url
} toRevoke && toRevoke.startsWith("blob:") && URL.revokeObjectURL(toRevoke)
}, } catch {}
this.img.splice(idx, 1)
// 选择文件(支持多张) this.form.ImgUrl.splice(idx, 1)
async upload(e) { }
if (!e?.tempFiles?.length) return },
if (this.isUploading) {
this.showMessage('warning', '图片正在上传中,请稍等') // 选择文件(支持多张)
return async upload(e) {
} if (!e?.tempFiles?.length) return
if (this.isUploading) {
this.isUploading = true this.showMessage("warning", "图片正在上传中,请稍等")
try { return
// 逐张处理 }
for (const tf of e.tempFiles) {
await this.handleImageUpload(tf) // 这里传入每个 tempFile this.isUploading = true
} try {
// this.showMessage('success', '图片处理/上传完成') for (const tf of e.tempFiles) {
} catch (err) { await this.handleImageUpload(tf)
console.error(err) }
this.showMessage('error', `图片上传失败:${err.message || err}`) } catch (err) {
} finally { console.error(err)
this.isUploading = false this.showMessage("error", `图片上传失败:${err.message || err}`)
} } finally {
}, this.isUploading = false
}
// 单张处理:加水印 ->(可选)压缩 -> 上传 },
async handleImageUpload(tempFile) {
// tempFile.file: H5 下是 File 对象App/小程序是临时路径 // 单张:加水印 -> 压缩(H5) -> 预览 -> 上传
const raw = tempFile.file || tempFile // 兼容各端 async handleImageUpload(tempFile) {
// 生成水印文字 // H5uni-file-picker 的 e.tempFiles[i].file 就是 File
const watermarkText = `${this.form.inspectionPoint || '未命名地点'} \n${this.getCurrentDate()}` const rawFile =
// 加水印(返回 File/Blob tempFile?.file instanceof File
const watermarked = await addWatermarkToImage(raw, watermarkText) ? tempFile.file
// 可选:移动端压缩(你已有方法,按需启用) : tempFile instanceof File
// const finalFile = await this.compressImageForMobile(watermarked) ? tempFile
: null
const finalFile = watermarked if (!rawFile) {
throw new Error("未获取到有效的 File 对象(仅支持 H5")
// 预览:用本地 URL仅供前端显示 }
const previewUrl = URL.createObjectURL(finalFile)
this.img.push({ const watermarkText = `${this.form.inspectionPoint || "未命名地点"} \n${this.getCurrentDate()}`
url: previewUrl, // uni-file-picker 识别 url
name: finalFile.name || `image_${Date.now()}.jpg`, // 水印(返回 Blob 或 File
extname: 'jpg' let watermarked = await addWatermarkToImage(rawFile, watermarkText)
}) if (!(watermarked instanceof File)) {
// 统一转回 File
// 真正上传:**不要传 previewUrl 字符串**,而是传二进制 `file` watermarked = this.blobToFile(
// H5 下uni.uploadFile 支持传 `file: File` watermarked,
const uploadedName = await this.uploadFileWithUni(finalFile) rawFile.name || `image_${Date.now()}.jpg`
// 回填后端返回的文件名/URL )
this.form.ImgUrl.push(uploadedName) }
},
// 压缩(仅 H5
// 用 uni.uploadFile 直接传二进制 const finalFile = await this.compressImageForMobile(watermarked, {
uploadFileWithUni(file) { quality: 0.6,
return new Promise((resolve, reject) => { maxWidth: 1200,
uni.uploadFile({ maxHeight: 1200,
url: UPLOAD_URL, mime: "image/jpeg"
name: 'file', // 后端接收字段名(按你的后端改) })
file, // H5 直接传 File 对象(重点!)
// 如果后端还需要额外字段: // 预览(本地 blob URL
// formData: { bizType: 'inspection' }, const previewUrl = URL.createObjectURL(finalFile)
success: (res) => { this.img.push({
try { url: previewUrl,
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res name: finalFile.name || `image_${Date.now()}.jpg`,
.data extname: "jpg"
if (data.code === 200) { })
// 假设后端返回 { code:200, fileName: 'xxx.jpg', url:'...' }
resolve(data.fileName || data.url) // 上传H5 用 file 字段)
} else { const uploadedName = await this.uploadFileWithUni(finalFile)
reject(new Error(data.msg || '上传失败')) this.form.ImgUrl.push(uploadedName)
}
} catch (e) { // 可选:延时释放预览 URL避免立即失效
reject(new Error('上传响应解析失败')) setTimeout(() => {
} try {
}, URL.revokeObjectURL(previewUrl)
fail: (err) => reject(err) } catch {}
}) }, 30000)
}) },
},
// —— 仅 H5File 压缩canvas 尺寸+质量)——
cancel() { async compressImageForMobile(file, opts = {}) {
uni.reLaunch({ const { quality = 0.6, maxWidth = 1200, maxHeight = 1200, mime = "image/jpeg" } = opts
url: '/pages/work/index' if (!(file instanceof File)) return file
})
}, const dataURL = await new Promise((resolve, reject) => {
const reader = new FileReader()
joinList() { reader.onload = () => resolve(reader.result)
return this.form.ImgUrl.join(',') reader.onerror = () => reject(new Error("文件读取失败"))
}, reader.readAsDataURL(file)
})
getCurrentDate() {
const now = new Date() const img = await new Promise((resolve, reject) => {
const y = now.getFullYear() const image = new Image()
const m = String(now.getMonth() + 1).padStart(2, '0') image.onload = () => resolve(image)
const d = String(now.getDate()).padStart(2, '0') image.onerror = () => reject(new Error("图片加载失败"))
const hh = String(now.getHours()).padStart(2, '0') image.src = dataURL
const mm = String(now.getMinutes()).padStart(2, '0') })
const ss = String(now.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}` const { w, h } = this.calcTargetSize(img.width, img.height, maxWidth, maxHeight)
}, const canvas = document.createElement("canvas")
canvas.width = w
dialogConfirm() { canvas.height = h
if (this.msgType === 'success') { const ctx = canvas.getContext("2d")
uni.reLaunch({ ctx.imageSmoothingQuality = "high"
url: '/pages/work/index' ctx.drawImage(img, 0, 0, w, h)
})
} const blob = await this.canvasToBlob(canvas, mime, quality)
}, if (!blob) return file
if (blob.size >= file.size) return file // 压缩后更大就不压了
showMessage(type, text) { return this.blobToFile(blob, file.name)
this.msgType = type },
this.messageText = text
this.$refs.alertDialog.open() // 计算目标尺寸
}, calcTargetSize(imgW, imgH, maxW, maxH) {
let w = imgW,
checkMobileBrowser() { h = imgH
const ua = navigator.userAgent.toLowerCase() if (w > maxW || h > maxH) {
const isMobile = /iphone|ipod|android|windows phone|mobile|blackberry/.test(ua) const ratio = Math.min(maxW / w, maxH / h)
this.isMobileBrowser = isMobile || window.__uniAppWebview w = Math.round(w * ratio)
}, h = Math.round(h * ratio)
// 计算目标尺寸 }
calcTargetSize(imgW, imgH, maxW, maxH) { return { w, h }
let w = imgW, },
h = imgH
if (w > maxW || h > maxH) { // toBlob 兼容
const ratio = Math.min(maxW / w, maxH / h) canvasToBlob(canvas, type, quality) {
w = Math.round(w * ratio) return new Promise((resolve) => {
h = Math.round(h * ratio) if (canvas.toBlob) {
} canvas.toBlob((blob) => resolve(blob), type, quality)
return { } else {
w, const dataURL = canvas.toDataURL(type, quality)
h const arr = dataURL.split(",")
} const mime = arr[0].match(/:(.*?);/)[1]
}, const bstr = atob(arr[1])
let n = bstr.length
// iOS 兼容 toBlob老 Safari const u8arr = new Uint8Array(n)
canvasToBlob(canvas, type, quality) { while (n--) u8arr[n] = bstr.charCodeAt(n)
return new Promise((resolve) => { resolve(new Blob([u8arr], { type: mime }))
if (canvas.toBlob) { }
canvas.toBlob((blob) => resolve(blob), type, quality) })
} else { },
// fallback: toDataURL -> Blob
const dataURL = canvas.toDataURL(type, quality) // Blob -> File
const arr = dataURL.split(',') blobToFile(blob, filename) {
const mime = arr[0].match(/:(.*?);/)[1] return new File([blob], filename || `img_${Date.now()}.jpg`, {
const bstr = atob(arr[1]) type: blob.type || "image/jpeg"
let n = bstr.length })
const u8arr = new Uint8Array(n) },
while (n--) u8arr[n] = bstr.charCodeAt(n)
resolve(new Blob([u8arr], { // H5: 用 uni.uploadFile 直接传二进制 File
type: mime uploadFileWithUni(file) {
})) return new Promise((resolve, reject) => {
} uni.uploadFile({
}) url: UPLOAD_URL,
}, name: "file",
file, // H5 传 File不要传 filePath
// H5: DataURL -> File success: (res) => {
blobToFile(blob, filename) { try {
return new File([blob], filename || `img_${Date.now()}.jpg`, { const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data
type: blob.type || 'image/jpeg' if (data.code === 200) {
}) resolve(data.fileName || data.url)
}, } else {
reject(new Error(data.msg || "上传失败"))
/** }
* 压缩图片(移动端友好) } catch (e) {
* @param {File|Object|String} input H5: File小程序/APPtempFile 对象或临时路径 reject(new Error("上传响应解析失败"))
* @param {Object} opts { quality: 0.6, maxWidth: 1200, maxHeight: 1200, mime: 'image/jpeg' } }
* @returns Promise<File|string> H5 返回 File小程序/APP 返回压缩后的临时路径 },
*/ fail: (err) => reject(err)
async compressImageForMobile(input, opts = {}) { })
const { })
quality = 0.6, },
maxWidth = 1200,
maxHeight = 1200, cancel() {
mime = 'image/jpeg' uni.reLaunch({ url: "/pages/work/index" })
} = opts },
// #ifdef H5 joinList() {
// —— H5使用 canvas 压缩 —— // return this.form.ImgUrl.join(",")
const file = input instanceof File ? input : (input?.file instanceof File ? input.file : null) },
if (!file) return input // 没拿到 File 就直接返回原始值
getCurrentDate() {
// 读取为 dataURL const now = new Date()
const dataURL = await new Promise((resolve, reject) => { const y = now.getFullYear()
const reader = new FileReader() const m = String(now.getMonth() + 1).padStart(2, "0")
reader.onload = () => resolve(reader.result) const d = String(now.getDate()).padStart(2, "0")
reader.onerror = () => reject(new Error('文件读取失败')) const hh = String(now.getHours()).padStart(2, "0")
reader.readAsDataURL(file) const mm = String(now.getMinutes()).padStart(2, "0")
}) const ss = String(now.getSeconds()).padStart(2, "0")
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
// 图片加载 },
const img = await new Promise((resolve, reject) => {
const image = new Image() dialogConfirm() {
image.onload = () => resolve(image) if (this.msgType === "success") {
image.onerror = () => reject(new Error('图片加载失败')) uni.reLaunch({ url: "/pages/work/index" })
image.src = dataURL }
}) },
const { showMessage(type, text) {
w, this.msgType = type
h this.messageText = text
} = this.calcTargetSize(img.width, img.height, maxWidth, maxHeight) this.$refs.alertDialog.open()
// 如果尺寸本来就不大,直接走质量压缩(或原图) },
const canvas = document.createElement('canvas')
canvas.width = w checkMobileBrowser() {
canvas.height = h const ua = navigator.userAgent.toLowerCase()
const ctx = canvas.getContext('2d') const isMobile = /iphone|ipod|android|windows phone|mobile|blackberry/.test(ua)
ctx.imageSmoothingQuality = 'high' this.isMobileBrowser = isMobile || window.__uniAppWebview
ctx.drawImage(img, 0, 0, w, h) }
},
const blob = await this.canvasToBlob(canvas, mime, quality) onLoad(option) {
if (!blob) return file this.form.inspectionPoint = option.inspectionPoint || ""
this.form.inspectionRequirements = option.inspectionRequirements || ""
// 若压缩后反而更大,则返回原图 this.form.inspectionPointId = option.inspectionPointId || ""
if (blob.size > file.size) return file },
mounted() {
return this.blobToFile(blob, file.name) if (!BASE_URL) {
// #endif console.warn("[上传] baseUrl 未配置,当前上传地址可能异常:", UPLOAD_URL)
}
// #ifdef MP-WEIXIN || APP-PLUS this.checkMobileBrowser()
// —— 小程序 / APP使用 uni.compressImage —— // }
// 允许传入 input 为对象(如 tempFile或字符串路径
const srcPath = typeof input === 'string' ?
input :
(input?.tempFilePath || input?.path || input?.filePath)
if (!srcPath) return input
// uni.compressImage 文档quality 0-100部分端支持 width/height
const q = Math.max(1, Math.min(100, Math.round(quality * 100)))
// 尝试带上 width/height端上不支持也会忽略
const compressed = await new Promise((resolve, reject) => {
uni.compressImage({
src: srcPath,
quality: q,
width: maxWidth,
height: maxHeight,
success: (res) => resolve(res.tempFilePath || res.apFilePath || res.target ||
res.path),
fail: (err) => reject(err)
})
})
return compressed
// #endif
},
},
onLoad(option) {
this.form.inspectionPoint = option.inspectionPoint || ''
this.form.inspectionRequirements = option.inspectionRequirements || ''
this.form.inspectionPointId = option.inspectionPointId || ''
},
mounted() {
this.checkMobileBrowser()
}
} }
</script> </script>