Files
threeonecheck_web/pages/hiddendanger/acceptance.vue
2026-06-03 10:16:37 +08:00

650 lines
19 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="page padding">
<view class="padding bg-white radius">
<view class="text-gray margin-bottom">整改记录</view>
<view class="padding solid radius">
<view>
<view style="color: #999; margin-bottom: 10rpx;">整改方案</view>
<view style="word-break: break-all; line-height: 1.6; color: #333;">{{ rectifyData.rectifyPlan || '暂无' }}</view>
</view>
<view class="margin-top">
<view style="color: #999; margin-bottom: 10rpx;">完成情况</view>
<view style="word-break: break-all; line-height: 1.6; color: #333;">{{ rectifyData.rectifyResult || '暂无' }}</view>
</view>
<view class="margin-top-sm">
<view>整改附件</view>
<view class="flex margin-top-xs" style="flex-wrap: wrap; gap: 10rpx;" v-if="rectifyAttachments.length > 0">
<image v-for="(img, idx) in rectifyAttachments" :key="idx" :src="getFullPath(img.filePath)" style="width: 136rpx;height: 136rpx;border-radius: 16rpx;" mode="aspectFill" @click="previewImage(idx)"></image>
</view>
<view v-else class="text-gray text-sm margin-top-xs">暂无附件</view>
</view>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">验收结果</view>
<view class="text-red">*</view>
</view>
<view class="flex" style="gap: 20rpx;">
<button :class="['result-btn', formData.result === 1 ? 'active' : '']" @click="formData.result = 1">通过</button>
<button :class="['result-btn', formData.result === 2 ? 'active' : '']" @click="formData.result = 2">不通过</button>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">验收备注</view>
</view>
<up-textarea v-model="formData.verifyRemark" placeholder="请输入验收备注"></up-textarea>
<view class="flex margin-bottom margin-top">
<view class="text-gray">验收图片/视频</view>
</view>
<up-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple imageMode="aspectFill" :maxCount="10"></up-upload>
<!-- 隐藏的 Canvas用于渲染防作弊时间戳水印 -->
<canvas canvas-id="watermarkCanvas" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'fixed', left: '-9999px', top: '-9999px' }"></canvas>
<!-- 电子签名部分 -->
<view class="flex justify-between margin-bottom margin-top-sm align-center">
<view class="text-gray flex align-center">
电子签名
<view class="text-red">*</view>
</view>
<button v-if="showCanvas" class="cu-btn sm round line-blue" style="margin: 0; padding: 0 20rpx; height: 50rpx; font-size: 22rpx;" @click="clearSignature">清除重写</button>
<button v-else class="cu-btn sm round line-blue" style="margin: 0; padding: 0 20rpx; height: 50rpx; font-size: 22rpx;" @click="reSign">重新签名</button>
</view>
<view class="signature-box margin-bottom">
<view v-if="!showCanvas" class="signature-display flex align-center justify-center" style="width: 100%; height: 160px; background-color: #f8f8f8; display: flex; align-items: center; justify-content: center;">
<image :src="signatureUrl" class="signature-img" mode="aspectFit" style="width: 100%; height: 100%;"></image>
</view>
<!-- 改为 v-if 解决小程序原生 canvas 真机渲染与生命周期挂载残留问题 -->
<view v-if="showCanvas" class="signature-pad-wrap" style="border: 1px dashed #dcdfe6; border-radius: 8rpx; overflow: hidden; background-color: #f8f8f8;">
<wd-signature
ref="signatureRef"
:width="signatureWidth"
:height="160"
backgroundColor="#f8f8f8"
penColor="#000000"
:lineWidth="3"
:enableHistory="false"
@confirm="(res) => onSignatureConfirm(res.tempFilePath)"
@start="isSignatureEmpty = false"
@signing="isSignatureEmpty = false"
@clear="isSignatureEmpty = true"
>
<template #footer></template>
</wd-signature>
</view>
</view>
<view class="flex margin-top-xl" style="gap: 20rpx;">
<button class="round flex-sub" @click="handleCancel">取消</button>
<button class="bg-blue round flex-sub" @click="handleSubmit">提交验收</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, watch, nextTick, getCurrentInstance } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { acceptanceRectification, getHiddenDangerDetail } from '@/request/api.js';
import { baseUrl, getToken, toImageUrl, imageBaseUrl } from '@/request/request.js';
import { addTimestampWatermark } from '@/utils/watermark.js';
// 页面参数
const rectifyId = ref('');
const hazardId = ref('');
const assignId = ref('');
// 整改记录数据
const rectifyData = reactive({
rectifyPlan: '',
rectifyResult: ''
});
// 整改附件
const rectifyAttachments = ref([]);
// 表单数据
const formData = reactive({
result: 1, // 验收结果 1.通过 2.不通过
verifyRemark: '' // 验收备注
});
const fileList1 = ref([]);
// 防作弊时间戳水印 Canvas 大小配置
const canvasWidth = ref(300);
const canvasHeight = ref(300);
// 电子签名及草稿相关
const showCanvas = ref(true); // 是否显示签字画板
const signatureUrl = ref(''); // 签名完整预览地址
const signatureServerPath = ref(''); // 签名服务器相对路径
const signatureWidth = ref(340); // 签名画板宽度(动态计算)
const signatureRef = ref(null); // 签名组件 ref
const isSignatureEmpty = ref(true); // 签名是否为空
const isSubmitting = ref(false); // 是否正在提交表单
const hasDraft = ref(false);
const showRestoreBanner = ref(false); // 独立控制提示 Banner
const isRestoring = ref(false); // 正在恢复标志
const isInitialized = ref(false); // 初始化标识
const signaturePaths = ref([]); // 缓存手写签名的绘制路径
const getDraftKey = () => `draft_accept_${rectifyId.value || ''}`;
// 获取完整图片路径
const getFullPath = (filePath) => {
if (!filePath) return '';
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return toImageUrl(filePath);
}
return toImageUrl(filePath);
};
// 图片预览
const previewImage = (index) => {
const urls = rectifyAttachments.value.map(item => getFullPath(item.filePath));
uni.previewImage({
current: index,
urls: urls
});
};
// 获取隐患详情
const fetchDetail = async () => {
if (!hazardId.value || !assignId.value) return;
try {
const res = await getHiddenDangerDetail({ hazardId: hazardId.value, assignId: assignId.value });
if (res.code === 0 && res.data) {
// 提取整改信息assigns[0].rectify
if (res.data.assigns && res.data.assigns.length > 0) {
const assign = res.data.assigns[0];
if (assign.rectify) {
rectifyData.rectifyPlan = assign.rectify.rectifyPlan || '';
rectifyData.rectifyResult = assign.rectify.rectifyResult || '';
if (assign.rectify.attachments) {
rectifyAttachments.value = assign.rectify.attachments;
}
console.log('整改记录:', rectifyData);
console.log('整改附件:', rectifyAttachments.value);
}
}
} else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' });
}
} catch (error) {
console.error('获取隐患详情失败:', error);
uni.showToast({ title: '请求失败', icon: 'none' });
}
};
onLoad((options) => {
if (options.rectifyId) {
rectifyId.value = options.rectifyId;
}
if (options.hazardId) {
hazardId.value = options.hazardId;
}
if (options.assignId) {
assignId.value = options.assignId;
}
console.log('验收页面参数:', { rectifyId: rectifyId.value, hazardId: hazardId.value, assignId: assignId.value });
// 获取隐患详情
fetchDetail();
});
// 取消
const handleCancel = () => {
uni.navigateBack();
};
// 提交验收
const handleSubmit = async () => {
if (!rectifyId.value) {
uni.showToast({
title: '缺少整改ID',
icon: 'none'
});
return;
}
// 电子签名验证与处理
if (showCanvas.value) {
if (!signatureRef.value || isSignatureEmpty.value) {
uni.showToast({
title: '请进行电子签名',
icon: 'none'
});
return;
}
isSubmitting.value = true;
uni.showLoading({ title: '正在提交...', mask: true });
// 触发组件导出,导出成功会回调 onSignatureConfirm
signatureRef.value.confirm();
} else {
// 已经有回显的签名
if (!signatureServerPath.value) {
uni.showToast({
title: '请进行电子签名',
icon: 'none'
});
return;
}
isSubmitting.value = true;
uni.showLoading({ title: '正在提交...', mask: true });
await executeSubmit();
}
};
// 真正调取后台接口提交验收
const executeSubmit = async () => {
// 构建附件列表
const attachments = fileList1.value.map(file => {
let url = '';
if (typeof file.url === 'string') {
url = file.url;
} else if (file.url && typeof file.url === 'object') {
url = file.url.url || file.url.path || '';
}
// 将预览绝对路径还原为服务端相对路径,避免后端保存带域名的绝对地址
if (typeof url === 'string' && url.startsWith('http')) {
url = url.replace(imageBaseUrl, '');
}
const fileName = (typeof url === 'string' && url) ? url.split('/').pop() : (file.name || '');
return {
fileName: fileName || '',
filePath: url || '',
fileType: file.type || 'image/png',
fileSize: file.size || 0
};
});
const params = {
rectifyId: Number(rectifyId.value),
result: formData.result,
verifyRemark: formData.verifyRemark || '',
attachments: attachments,
signPath: signatureServerPath.value || '' // 电子签名路径
};
console.log('提交验收参数:', params);
try {
const res = await acceptanceRectification(params);
uni.hideLoading();
if (res.code === 0) {
clearDraft(false);
uni.showToast({
title: '验收成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({
title: res.msg || '验收失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('验收失败:', error);
uni.showToast({
title: '请求失败',
icon: 'none'
});
} finally {
isSubmitting.value = false;
}
};
// 删除图片
const deletePic = (event) => {
fileList1.value.splice(event.index, 1);
};
// 新增图片
/**
* 在前端为图片进行实时渲染,添加当前系统时间戳水印(防作弊)
* @param {string} tempFilePath 原始选择的图片临时路径
* @returns {Promise<string>} 渲染后的带水印图片临时文件路径
*/
const addWatermark = (tempFilePath) => {
const instance = getCurrentInstance();
return addTimestampWatermark({
tempFilePath,
canvasId: 'watermarkCanvas',
canvasWidthRef: canvasWidth,
canvasHeightRef: canvasHeight,
instance
});
};
// 新增图片(自动加入实时前端水印渲染防作弊功能)
const afterRead = async (event) => {
let lists = [].concat(event.file);
let fileListLen = fileList1.value.length;
lists.map((item) => {
fileList1.value.push({
...item,
status: 'uploading',
message: '处理中...',
});
});
for (let i = 0; i < lists.length; i++) {
try {
// 1. 进行前端实时渲染绘制水印,得到新图片临时路径
const watermarkedUrl = await addWatermark(lists[i].url);
// 2. 将加完水印的新图片进行服务器上传
const result = await uploadFilePromise(watermarkedUrl);
let item = fileList1.value[fileListLen];
fileList1.value.splice(fileListLen, 1, {
...item,
status: 'success',
message: '',
url: toImageUrl(result.url || result.filePath || result),
});
} catch (e) {
console.error('加水印或上传失败:', e);
let item = fileList1.value[fileListLen];
fileList1.value.splice(fileListLen, 1, {
...item,
status: 'failed',
message: '处理失败',
});
}
fileListLen++;
}
};
const uploadFilePromise = (filePath) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + '/frontend/attachment/upload',
filePath: filePath,
name: 'file',
header: {
'Authorization': getToken()
},
success: (res) => {
const data = JSON.parse(res.data);
if (data.code === 0) {
resolve(data.data);
} else {
reject(data.msg || '上传失败');
}
},
fail: (err) => {
console.error('上传失败:', err);
reject(err);
}
});
});
};
// 电子签名画布手写线条变动回调
const onSignatureChange = () => {
isSignatureEmpty.value = false;
signaturePaths.value = [];
if (rectifyId.value) {
saveDraft();
}
};
// 保存草稿
const saveDraft = () => {
if (isRestoring.value || !isInitialized.value) return;
const key = getDraftKey();
const hasContent = formData.verifyRemark ||
fileList1.value.length > 0 ||
signatureServerPath.value ||
signaturePaths.value.length > 0;
if (!hasContent) {
uni.removeStorageSync(key);
hasDraft.value = false;
return;
}
const data = {
formData: {
result: formData.result,
verifyRemark: formData.verifyRemark
},
fileList1: fileList1.value,
signatureServerPath: signatureServerPath.value,
signatureUrl: signatureUrl.value,
showCanvas: showCanvas.value,
signaturePaths: signaturePaths.value
};
uni.setStorageSync(key, JSON.stringify(data));
hasDraft.value = true;
};
// 清空草稿
const clearDraft = (showToast = true) => {
const key = getDraftKey();
uni.removeStorageSync(key);
hasDraft.value = false;
showRestoreBanner.value = false;
isRestoring.value = true;
formData.result = 1;
formData.verifyRemark = '';
fileList1.value = [];
signatureServerPath.value = '';
signatureUrl.value = '';
showCanvas.value = true;
signaturePaths.value = [];
if (signatureRef.value) {
signatureRef.value.clear();
}
nextTick(() => {
isRestoring.value = false;
});
if (showToast) {
uni.showToast({ title: '草稿已清空', icon: 'none' });
}
};
// 恢复草稿
const restoreDraft = () => {
const key = getDraftKey();
const cached = uni.getStorageSync(key);
if (cached) {
try {
const data = JSON.parse(cached);
const hasContent = data.formData.verifyRemark ||
(data.fileList1 && data.fileList1.length > 0) ||
data.signatureServerPath ||
(data.signaturePaths && data.signaturePaths.length > 0);
if (!hasContent) {
isInitialized.value = true;
return;
}
isRestoring.value = true;
formData.result = data.formData.result !== undefined ? data.formData.result : 1;
formData.verifyRemark = data.formData.verifyRemark || '';
fileList1.value = data.fileList1 || [];
signatureServerPath.value = data.signatureServerPath || '';
signatureUrl.value = data.signatureUrl || '';
showCanvas.value = data.showCanvas !== undefined ? data.showCanvas : true;
signaturePaths.value = data.signaturePaths || [];
hasDraft.value = true;
showRestoreBanner.value = true;
// 延迟恢复签名画布线条重绘
if (signaturePaths.value.length > 0) {
setTimeout(() => {
if (signatureRef.value) {
isSignatureEmpty.value = false;
// wot-design-uni auto rendering
}
}, 450);
}
nextTick(() => {
isRestoring.value = false;
isInitialized.value = true;
});
uni.showToast({
title: '已自动恢复您上次未提交的内容',
icon: 'none',
duration: 2500
});
} catch (e) {
console.error('解析草稿失败:', e);
isRestoring.value = false;
isInitialized.value = true;
}
} else {
isInitialized.value = true;
}
};
// 深度监听验收项和签名笔画变化,自动保存草稿
watch(
() => [
formData.result,
formData.verifyRemark,
fileList1.value,
signatureServerPath.value,
signaturePaths.value
],
() => {
if (rectifyId.value) {
saveDraft();
}
},
{ deep: true }
);
// 清除画布
const clearSignature = () => {
isSignatureEmpty.value = true;
if (signatureRef.value) {
signatureRef.value.clear();
}
};
// 重新签字
const reSign = () => {
isSignatureEmpty.value = true;
showCanvas.value = true;
signatureUrl.value = '';
signatureServerPath.value = '';
nextTick(() => {
if (signatureRef.value) {
signatureRef.value.clear();
}
});
};
// 签名导出失败
const onSignatureError = (err) => {
console.error('签名绘制失败:', err);
uni.showToast({ title: '签名生成失败', icon: 'none' });
isSubmitting.value = false;
uni.hideLoading();
};
// 签名导出成功回调
const onSignatureConfirm = async (tempFilePath) => {
try {
// 上传签名
const res = await uploadFilePromise(tempFilePath);
// 兼容后端返回对象或者单纯字符串路径
const path = (res && typeof res === 'object') ? (res.url || res.filePath || '') : (res || '');
signatureServerPath.value = path;
signatureUrl.value = path.startsWith('http') ? path : (baseUrl.replace('/api', '') + path);
if (isSubmitting.value) {
await executeSubmit();
}
} catch (err) {
isSubmitting.value = false;
uni.hideLoading();
console.error('签名上传失败:', err);
uni.showToast({ title: '签名上传失败,请重试', icon: 'none' });
}
};
onLoad((options) => {
// 计算签名画布宽度
try {
const sysInfo = uni.getSystemInfoSync();
signatureWidth.value = sysInfo.windowWidth - 40;
} catch (e) {
console.error('获取系统信息失败:', e);
}
if (options.rectifyId) {
rectifyId.value = options.rectifyId;
}
if (options.hazardId) {
hazardId.value = options.hazardId;
}
if (options.assignId) {
assignId.value = options.assignId;
}
console.log('验收页面参数:', { rectifyId: rectifyId.value, hazardId: hazardId.value, assignId: assignId.value });
// 获取隐患详情
fetchDetail();
// 页面打开时自动恢复草稿
restoreDraft();
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #EBF2FC;
}
.result-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 8rpx;
background: #f5f5f5;
color: #666;
font-size: 28rpx;
&::after {
border: none;
}
&.active {
background: #2667E9;
color: #fff;
}
}
// 签名相关样式
.signature-box {
width: 100%;
min-height: 240rpx;
background: #f8f8f8;
border: 1rpx dashed #dcdfe6;
border-radius: 8rpx;
margin-top: 16rpx;
.signature-img {
width: 100%;
height: 100%;
}
.signature-placeholder {
color: #909399;
font-size: 28rpx;
}
}
</style>