2025-07-28 14:57:35 +08:00
|
|
|
|
<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">
|
2025-09-04 21:16:08 +08:00
|
|
|
|
<uni-easyinput type="textarea" disabled :value="form.inspectionRequirements"
|
|
|
|
|
placeholder="请输入巡检要求"></uni-easyinput>
|
2025-07-28 14:57:35 +08:00
|
|
|
|
</uni-forms-item>
|
|
|
|
|
<uni-forms-item label="图片上传">
|
|
|
|
|
<view class="example-body">
|
2025-09-04 20:45:49 +08:00
|
|
|
|
<uni-file-picker limit="3" :sourceType="sourceType" :value="img" title="最多选择3张图片"
|
|
|
|
|
file-mediatype="image" @delete="deleteImg" @select="upload"
|
|
|
|
|
:auto-upload="false"></uni-file-picker>
|
2025-07-28 14:57:35 +08:00
|
|
|
|
</view>
|
|
|
|
|
</uni-forms-item>
|
|
|
|
|
<!-- 备注 -->
|
2025-08-07 13:01:29 +08:00
|
|
|
|
<uni-forms-item label="备注1" name="remark">
|
2025-07-28 14:57:35 +08:00
|
|
|
|
<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>
|
2025-09-04 21:21:53 +08:00
|
|
|
|
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")
|
|
|
|
|
|
2025-09-04 21:16:08 +08:00
|
|
|
|
export default {
|
2025-09-04 21:21:53 +08:00
|
|
|
|
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) {
|
|
|
|
|
// H5:uni-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)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// —— 仅 H5:File 压缩(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()
|
|
|
|
|
}
|
2025-09-04 21:16:08 +08:00
|
|
|
|
}
|
2025-07-28 14:57:35 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
2025-08-07 13:01:29 +08:00
|
|
|
|
|
2025-09-04 21:06:29 +08:00
|
|
|
|
|
2025-07-28 14:57:35 +08:00
|
|
|
|
<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>
|