Files
threeonecheck_web/pages/hiddendanger/acceptance.vue

613 lines
18 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.rectificationMeasures || '暂无' }}</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.controlMeasures || '暂无' }}</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">
<view style="color: #999; margin-bottom: 10rpx;">计划投资资金</view>
<view style="word-break: break-all; line-height: 1.6; color: #333;">{{ formatMoney(rectifyData.planCost) }}</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;">{{ formatMoney(rectifyData.actualCost) }}</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.rectifierName || '暂无' }}</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.rectifyStatusName || '暂无' }}</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 { toImageUrl } from '@/request/request.js';
import {
createUploadListHandlers,
buildAttachmentItem,
uploadToCloud
} from '@/utils/upload.js';
// 页面参数
const rectifyId = ref('');
const hazardId = ref('');
const assignId = ref('');
// 整改记录数据
const rectifyData = reactive({
rectifyPlan: '',
rectificationMeasures: '',
controlMeasures: '',
rectifyResult: '',
planCost: null,
actualCost: null,
rectifierName: '',
rectifyStatusName: ''
});
const formatMoney = (value) => {
if (value === null || value === undefined || value === '') {
return '暂无';
}
const num = Number(value);
if (Number.isNaN(num)) {
return '暂无';
}
return `${num.toFixed(2)}`;
};
// 整改附件
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
});
};
// 从 assigns 中找到包含整改记录的那一项
const resolveAssignWithRectify = (assigns) => {
if (!assigns?.length) return null;
if (rectifyId.value) {
const byRectifyId = assigns.find(
(item) => item.rectify && String(item.rectify.rectifyId) === String(rectifyId.value)
);
if (byRectifyId) return byRectifyId;
}
if (assignId.value) {
const byAssignId = assigns.find(
(item) => String(item.assignId) === String(assignId.value) && item.rectify
);
if (byAssignId) return byAssignId;
}
return assigns.find((item) => item.rectify) || null;
};
const applyRectifyData = (rectify) => {
if (!rectify) return;
rectifyData.rectifyPlan = rectify.rectifyPlan || '';
rectifyData.rectificationMeasures = rectify.rectificationMeasures || '';
rectifyData.controlMeasures = rectify.controlMeasures || '';
rectifyData.rectifyResult = rectify.rectifyResult || '';
rectifyData.planCost = rectify.planCost ?? null;
rectifyData.actualCost = rectify.actualCost ?? null;
rectifyData.rectifierName = rectify.rectifierName || '';
rectifyData.rectifyStatusName = rectify.rectifyStatusName || '';
rectifyAttachments.value = rectify.attachments || [];
};
// 获取隐患详情
const fetchDetail = async () => {
if (!hazardId.value) return;
try {
const res = await getHiddenDangerDetail({
hazardId: hazardId.value,
assignId: assignId.value
});
if (res.code === 0 && res.data) {
const assign = resolveAssignWithRectify(res.data.assigns);
if (assign?.rectify) {
applyRectifyData(assign.rectify);
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' });
}
};
// 取消
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
.filter((f) => f.status === 'success')
.map((file) => buildAttachmentItem(file));
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 acceptUploadInstance = getCurrentInstance();
const { afterRead, deletePic } = createUploadListHandlers(fileList1, {
watermark: {
canvasId: 'watermarkCanvas',
canvasWidthRef: canvasWidth,
canvasHeightRef: canvasHeight,
instance: acceptUploadInstance
}
});
// 电子签名画布手写线条变动回调
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 { url } = await uploadToCloud(tempFilePath);
signatureServerPath.value = url;
signatureUrl.value = url;
showCanvas.value = false;
isSignatureEmpty.value = false;
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>