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

412 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"
// 如果你已有 uploadImg 封装且内部就是 uni.uploadFile可以继续用
// 但前提是它支持 H5 直接传 File 对象(不是 URL 字符串)。
// 这里我示范“直接用 uni.uploadFile”的写法更稳妥。
import {
addWatermarkToImage
} from "@/utils/watermark.js"
import appConfig from '@/config'
const UPLOAD_URL = appConfig.baseUrl + "/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', '打卡失败')
}
} catch (err) {
this.showMessage('error', `提交失败: ${err.message}`)
}
},
// uni-file-picker 删除e.tempFile 里通常有 {url/path},与你传给 value 的结构需一致
deleteImg(e) {
const url = e.tempFile?.url || e.tempFile?.path
const idx = this.img.findIndex(x => x.url === url)
if (idx !== -1) {
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) // 这里传入每个 tempFile
}
// this.showMessage('success', '图片处理/上传完成')
} catch (err) {
console.error(err)
this.showMessage('error', `图片上传失败:${err.message || err}`)
} finally {
this.isUploading = false
}
},
// 单张处理:加水印 ->(可选)压缩 -> 上传
async handleImageUpload(tempFile) {
// tempFile.file: H5 下是 File 对象App/小程序是临时路径
const raw = tempFile.file || tempFile // 兼容各端
// 生成水印文字
const watermarkText = `${this.form.inspectionPoint || '未命名地点'} \n${this.getCurrentDate()}`
// 加水印(返回 File/Blob
const watermarked = await addWatermarkToImage(raw, watermarkText)
// 可选:移动端压缩(你已有方法,按需启用)
// const finalFile = await this.compressImageForMobile(watermarked)
const finalFile = watermarked
// 预览:用本地 URL仅供前端显示
const previewUrl = URL.createObjectURL(finalFile)
this.img.push({
url: previewUrl, // uni-file-picker 识别 url
name: finalFile.name || `image_${Date.now()}.jpg`,
extname: 'jpg'
})
// 真正上传:**不要传 previewUrl 字符串**,而是传二进制 `file`
// H5 下uni.uploadFile 支持传 `file: File`
const uploadedName = await this.uploadFileWithUni(finalFile)
// 回填后端返回的文件名/URL
this.form.ImgUrl.push(uploadedName)
},
// 用 uni.uploadFile 直接传二进制
uploadFileWithUni(file) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: UPLOAD_URL,
name: 'file', // 后端接收字段名(按你的后端改)
file, // H5 直接传 File 对象(重点!)
// 如果后端还需要额外字段:
// formData: { bizType: 'inspection' },
success: (res) => {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res
.data
if (data.code === 200) {
// 假设后端返回 { code:200, fileName: 'xxx.jpg', url:'...' }
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
},
// 计算目标尺寸
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
}
},
// iOS 兼容 toBlob老 Safari
canvasToBlob(canvas, type, quality) {
return new Promise((resolve) => {
if (canvas.toBlob) {
canvas.toBlob((blob) => resolve(blob), type, quality)
} else {
// fallback: toDataURL -> Blob
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
}))
}
})
},
// H5: DataURL -> File
blobToFile(blob, filename) {
return new File([blob], filename || `img_${Date.now()}.jpg`, {
type: blob.type || 'image/jpeg'
})
},
/**
* 压缩图片(移动端友好)
* @param {File|Object|String} input H5: File小程序/APPtempFile 对象或临时路径
* @param {Object} opts { quality: 0.6, maxWidth: 1200, maxHeight: 1200, mime: 'image/jpeg' }
* @returns Promise<File|string> H5 返回 File小程序/APP 返回压缩后的临时路径
*/
async compressImageForMobile(input, opts = {}) {
const {
quality = 0.6,
maxWidth = 1200,
maxHeight = 1200,
mime = 'image/jpeg'
} = opts
// #ifdef H5
// —— H5使用 canvas 压缩 —— //
const file = input instanceof File ? input : (input?.file instanceof File ? input.file : null)
if (!file) return input // 没拿到 File 就直接返回原始值
// 读取为 dataURL
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)
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
// —— 小程序 / 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>
<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>