Files
pasd_app/pages/work/inspection/scanSign/index.vue
2025-09-04 21:21:53 +08:00

379 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<view class="example">
<uni-forms ref="dynamicForm" :model="form" label-width="80px">
<uni-forms-item label="巡检点" name="inspectionPoint">
<uni-easyinput disabled :value="form.inspectionPoint" placeholder="请输入巡检点"></uni-easyinput>
</uni-forms-item>
<uni-forms-item label="巡检要求" name="inspectionRequirements">
<uni-easyinput type="textarea" disabled :value="form.inspectionRequirements"
placeholder="请输入巡检要求"></uni-easyinput>
</uni-forms-item>
<uni-forms-item label="图片上传">
<view class="example-body">
<uni-file-picker limit="3" :sourceType="sourceType" :value="img" title="最多选择3张图片"
file-mediatype="image" @delete="deleteImg" @select="upload"
:auto-upload="false"></uni-file-picker>
</view>
</uni-forms-item>
<!-- 备注 -->
<uni-forms-item label="备注1" name="remark">
<uni-easyinput type="textarea" v-model="form.remark" placeholder="请输入备注"></uni-easyinput>
</uni-forms-item>
</uni-forms>
<view class="button-group">
<button class="btn btn-primary" @click="submit()">提交</button>
<button class="btn btn-cancel" @click="cancel()">取消</button>
</view>
</view>
<!-- 消息提示框 -->
<view>
<uni-popup ref="alertDialog" type="dialog">
<uni-popup-dialog :type="msgType" title="消息" :content="messageText" @confirm="dialogConfirm"
showClose="false"></uni-popup-dialog>
</uni-popup>
</view>
</view>
</template>
<script>
import { addRecord } from "@/api/inspection/record.js"
import { addWatermarkToImage } from "@/utils/watermark.js"
import appConfig from "@/config"
// ------- URL 安全拼接,避免出现 /undefined/common/upload -------
function joinURL(base, path) {
const b = (base || "").replace(/\/+$/, "")
const p = (path || "").replace(/^\/+/, "")
return `${b}/${p}`
}
const BASE_URL = appConfig?.baseUrl || (process.env.VUE_APP_BASE_API || "")
const UPLOAD_URL = joinURL(BASE_URL, "/common/upload")
export default {
data() {
return {
form: {
inspectionType: 0,
inspectorId: this.$store.state.user.nickName,
inspectionPoint: "",
inspectionRequirements: "",
inspectionImg: "",
ImgUrl: [], // 后端返回的文件名/URL
remark: ""
},
img: [], // uni-file-picker 预览
sourceType: ["camera"],
msgType: "",
messageText: "",
isUploading: false,
isMobileBrowser: false
}
},
methods: {
async submit() {
if (this.isUploading) {
this.showMessage("warning", "图片正在上传中,请稍等")
return
}
if (!Array.isArray(this.form.ImgUrl) || this.form.ImgUrl.length === 0) {
this.showMessage("error", "请选择要上传的图片")
return
}
this.form.inspectionImg = this.joinList()
try {
const res = await addRecord(this.form)
if (res.code === 200) {
this.showMessage("success", "打卡成功")
} else {
this.showMessage("error", res.msg || "打卡失败")
}
} catch (err) {
this.showMessage("error", `提交失败: ${err.message}`)
}
},
// 删除预览 & 同步删除结果
deleteImg(e) {
const url = e?.tempFile?.url || e?.tempFile?.path
const idx = this.img.findIndex((x) => x.url === url)
if (idx !== -1) {
// 释放预览 URL 内存
try {
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
}
this.isUploading = true
try {
for (const tf of e.tempFiles) {
await this.handleImageUpload(tf)
}
} catch (err) {
console.error(err)
this.showMessage("error", `图片上传失败:${err.message || err}`)
} finally {
this.isUploading = false
}
},
// 单张:加水印 -> 压缩(H5) -> 预览 -> 上传
async handleImageUpload(tempFile) {
// H5uni-file-picker 的 e.tempFiles[i].file 就是 File
const rawFile =
tempFile?.file instanceof File
? tempFile.file
: tempFile instanceof File
? tempFile
: null
if (!rawFile) {
throw new Error("未获取到有效的 File 对象(仅支持 H5")
}
const watermarkText = `${this.form.inspectionPoint || "未命名地点"} \n${this.getCurrentDate()}`
// 水印(返回 Blob 或 File
let watermarked = await addWatermarkToImage(rawFile, watermarkText)
if (!(watermarked instanceof File)) {
// 统一转回 File
watermarked = this.blobToFile(
watermarked,
rawFile.name || `image_${Date.now()}.jpg`
)
}
// 压缩(仅 H5
const finalFile = await this.compressImageForMobile(watermarked, {
quality: 0.6,
maxWidth: 1200,
maxHeight: 1200,
mime: "image/jpeg"
})
// 预览(本地 blob URL
const previewUrl = URL.createObjectURL(finalFile)
this.img.push({
url: previewUrl,
name: finalFile.name || `image_${Date.now()}.jpg`,
extname: "jpg"
})
// 上传H5 用 file 字段)
const uploadedName = await this.uploadFileWithUni(finalFile)
this.form.ImgUrl.push(uploadedName)
// 可选:延时释放预览 URL避免立即失效
setTimeout(() => {
try {
URL.revokeObjectURL(previewUrl)
} catch {}
}, 30000)
},
// —— 仅 H5File 压缩canvas 尺寸+质量)——
async compressImageForMobile(file, opts = {}) {
const { quality = 0.6, maxWidth = 1200, maxHeight = 1200, mime = "image/jpeg" } = opts
if (!(file instanceof File)) return file
const dataURL = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error("文件读取失败"))
reader.readAsDataURL(file)
})
const img = await new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => resolve(image)
image.onerror = () => reject(new Error("图片加载失败"))
image.src = dataURL
})
const { w, h } = this.calcTargetSize(img.width, img.height, maxWidth, maxHeight)
const canvas = document.createElement("canvas")
canvas.width = w
canvas.height = h
const ctx = canvas.getContext("2d")
ctx.imageSmoothingQuality = "high"
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 // 压缩后更大就不压了
return this.blobToFile(blob, file.name)
},
// 计算目标尺寸
calcTargetSize(imgW, imgH, maxW, maxH) {
let w = imgW,
h = imgH
if (w > maxW || h > maxH) {
const ratio = Math.min(maxW / w, maxH / h)
w = Math.round(w * ratio)
h = Math.round(h * ratio)
}
return { w, h }
},
// toBlob 兼容
canvasToBlob(canvas, type, quality) {
return new Promise((resolve) => {
if (canvas.toBlob) {
canvas.toBlob((blob) => resolve(blob), type, quality)
} else {
const dataURL = canvas.toDataURL(type, quality)
const arr = dataURL.split(",")
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) u8arr[n] = bstr.charCodeAt(n)
resolve(new Blob([u8arr], { type: mime }))
}
})
},
// Blob -> File
blobToFile(blob, filename) {
return new File([blob], filename || `img_${Date.now()}.jpg`, {
type: blob.type || "image/jpeg"
})
},
// H5: 用 uni.uploadFile 直接传二进制 File
uploadFileWithUni(file) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: UPLOAD_URL,
name: "file",
file, // H5 传 File不要传 filePath
success: (res) => {
try {
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data
if (data.code === 200) {
resolve(data.fileName || data.url)
} else {
reject(new Error(data.msg || "上传失败"))
}
} catch (e) {
reject(new Error("上传响应解析失败"))
}
},
fail: (err) => reject(err)
})
})
},
cancel() {
uni.reLaunch({ url: "/pages/work/index" })
},
joinList() {
return this.form.ImgUrl.join(",")
},
getCurrentDate() {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, "0")
const d = String(now.getDate()).padStart(2, "0")
const hh = String(now.getHours()).padStart(2, "0")
const mm = String(now.getMinutes()).padStart(2, "0")
const ss = String(now.getSeconds()).padStart(2, "0")
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
},
dialogConfirm() {
if (this.msgType === "success") {
uni.reLaunch({ url: "/pages/work/index" })
}
},
showMessage(type, text) {
this.msgType = type
this.messageText = text
this.$refs.alertDialog.open()
},
checkMobileBrowser() {
const ua = navigator.userAgent.toLowerCase()
const isMobile = /iphone|ipod|android|windows phone|mobile|blackberry/.test(ua)
this.isMobileBrowser = isMobile || window.__uniAppWebview
}
},
onLoad(option) {
this.form.inspectionPoint = option.inspectionPoint || ""
this.form.inspectionRequirements = option.inspectionRequirements || ""
this.form.inspectionPointId = option.inspectionPointId || ""
},
mounted() {
if (!BASE_URL) {
console.warn("[上传] baseUrl 未配置,当前上传地址可能异常:", UPLOAD_URL)
}
this.checkMobileBrowser()
}
}
</script>
<style lang="scss">
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
height: 90vh;
/* 使页面高度占满整个视口 */
}
.container {
flex: 1;
/* 让 container 占满剩余空间 */
display: flex;
flex-direction: column;
/* 设置为列方向 */
}
.example {
flex: 1;
/* 让 example 占满 container 的剩余空间 */
display: flex;
flex-direction: column;
/* 设置为列方向,确保子元素垂直排列 */
padding: 15px;
background-color: #fff;
}
// 样式沉底
.button-group {
position: fixed;
bottom: 20px;
left: 0;
/* 使用 margin-top: auto 来将按钮组推到 example 容器的底部 */
display: flex;
width: 100%;
justify-content: space-around;
}
.button-group button {
flex: 1;
background: #fff;
color: #000;
/* 使按钮平分可用空间 */
/* 可能还需要设置一些其他的样式来确保按钮看起来正确,比如 text-align, padding 等 */
}
</style>