Files
threeonecheck_web/pages/hiddendanger/acceptance-approval.vue
2026-07-27 09:27:22 +08:00

1340 lines
41 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;">{{ formatPersonNames(rectifyData.managerNames) }}</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;">{{ formatPersonNames(rectifyData.memberNames) }}</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="text-gray margin-bottom margin-top">验收记录</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;">{{ verifyData.resultName || '暂无' }}</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;">{{ verifyData.remark || '暂无' }}</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;">{{ verifyData.verifierName || '暂无' }}</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;">{{ verifyData.verifyTime || '暂无' }}</view>
</view>
<view class="margin-top-sm">
<view>验收图片</view>
<view class="flex margin-top-xs" style="flex-wrap: wrap; gap: 10rpx;" v-if="verifyAttachments.length > 0">
<image v-for="(img, idx) in verifyAttachments" :key="idx" :src="getFullPath(img.filePath)" style="width: 136rpx;height: 136rpx;border-radius: 16rpx;" mode="aspectFill" @click="previewVerifyImage(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>
<up-radio-group
v-model="formData.approvalOpinion"
placement="row"
activeColor="#2667e9"
class="approval-radio-group"
>
<up-radio
v-for="(opt, index) in approvalOptions"
:key="opt.value"
:label="opt.label"
:name="opt.value"
:customStyle="{ marginRight: index < approvalOptions.length - 1 ? '24rpx' : '0' }"
></up-radio>
</up-radio-group>
<view class="flex margin-bottom margin-top">
<view class="text-gray">意见说明</view>
<view class="text-red">*</view>
</view>
<up-textarea v-model="formData.opinionRemark" placeholder="请输入意见说明"></up-textarea>
<view class="flex margin-bottom margin-top">
<view class="text-gray">下一步流程</view>
<view class="text-red">*</view>
</view>
<view class="static-field">{{ nextStepDisplay }}</view>
<view v-if="needNextAssignee" class="flex margin-bottom margin-top">
<view class="text-gray">下一步处理人</view>
<view class="text-red">*</view>
</view>
<view v-if="needNextAssignee" class="select-trigger" @click="openAssigneePopup">
<view class="select-content" :class="{ 'text-gray': !selectedAssigneeName }">
{{ selectedAssigneeName || '请选择下一步处理人' }}
</view>
<text class="cuIcon-unfold"></text>
</view>
<FlowAssigneePickerPopup
:show="showAssigneePopup"
:task-id="taskId"
v-model:picker-identity-id="pickerAssigneeIdentityId"
v-model:picker-name="pickerAssigneeName"
@cancel="cancelAssigneeSelect"
@confirm="confirmAssigneeSelect"
/>
<view class="flex margin-bottom margin-top">
<view class="text-gray">短信提醒</view>
<view class="text-red">*</view>
</view>
<up-radio-group v-model="sendMsgFlagRadio" placement="row" activeColor="#2667e9">
<up-radio
label="是"
name="yes"
:customStyle="{ marginRight: '48rpx' }"
></up-radio>
<up-radio label="否" name="no"></up-radio>
</up-radio-group>
<!-- 电子签名表单最底部 -->
<view class="flex justify-between margin-bottom margin-top 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
v-if="signatureUrl"
:src="signatureUrl"
class="signature-img"
mode="aspectFit"
style="width: 100%; height: 100%;"
></image>
<text v-else class="signature-placeholder">暂无签名请点击重新签名</text>
</view>
<!-- 改为 v-if 解决小程序原生 canvas 真机渲染与生命周期挂载残留问题 -->
<!-- 弹窗打开时卸载 Canvas解决微信原生 canvas 真机穿透盖住弹层的问题 -->
<view v-if="showCanvas && !showAssigneePopup" 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="onSignatureStart"
@signing="onSignatureSigning"
@end="onSignatureEnd"
@clear="onSignatureClear"
>
<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, computed, nextTick, watch } from 'vue';
import { onLoad, onHide } from '@dcloudio/uni-app';
import { flowTaskApprove, getHiddenDangerDetail, getRectifyDetail, getFlowNextNodes } from '@/request/api.js';
import FlowAssigneePickerPopup from '@/components/flow/FlowAssigneePickerPopup.vue';
import { toImageUrl } from '@/request/request.js';
import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js';
import { useDraftCache } from '@/utils/useDraftCache.js';
import {
uploadToCloud,
toSubmitFileUrl
} from '@/utils/upload.js';
// 页面参数
const rectifyId = ref('');
const hazardId = ref('');
const assignId = ref('');
const taskId = ref('');
// 下一步流程
const nextStepName = ref('');
const nextStepKey = ref('');
const nextStepLoading = ref(false);
const FLOW_END_STEP_NAME = '验收快速审结束';
const FIXED_NEXT_STEP_MAP = {
re_rectify: '隐患整改'
};
/** 当前流程节点 key来自 next-nodes 接口 data.currentTaskKey */
const currentTaskKey = ref('');
const resolveFixedNextStepName = (opinion) => FIXED_NEXT_STEP_MAP[opinion] || '';
const isFixedNextStepOpinion = (opinion) => !!resolveFixedNextStepName(opinion);
/** 验收审批属于验收子流程,需 includeSubProcess: true与 acceptance.vue 一致) */
const FLOW_INCLUDE_SUB_PROCESS = true;
const APPROVAL_TYPE_MAP = {
agree: 'agree',
reject: 'reject',
report: 'report',
re_rectify: 'rectify'
};
/** 审批意见选项:同意、驳回 */
const APPROVAL_OPTIONS_AGREE_REJECT = [
{ label: '同意', value: 'agree' },
{ label: '驳回', value: 'reject' }
];
/** 审批意见选项:同意、驳回、上报 */
const APPROVAL_OPTIONS_WITH_REPORT = [
{ label: '同意', value: 'agree' },
{ label: '驳回', value: 'reject' },
{ label: '上报', value: 'report' }
];
/** 审批意见选项:同意、驳回、上报、重新整改(验收领导审批无「重新验收」) */
const APPROVAL_OPTIONS_ALL = [
{ label: '同意', value: 'agree' },
{ label: '驳回', value: 'reject' },
{ label: '上报', value: 'report' },
{ label: '重新整改', value: 're_rectify' }
];
/** 主管领导审批州级prefecture无「上报」 */
const APPROVAL_OPTIONS_LEADER_PREFECTURE = [
{ label: '同意', value: 'agree' },
{ label: '驳回', value: 'reject' },
{ label: '重新整改', value: 're_rectify' }
];
/**
* 根据 currentTaskKey + 登录身份 deptType 决定审批意见展示项
*
* 数据来源:
* - currentTaskKeyPOST /flow/task/next-nodes 返回 data.currentTaskKey当前节点
* - deptTypestorage userInfo.userIdentity.deptType当前身份部门类型
*
* 规则:
* 1) 仅「同意、驳回」
* - section_chief_review科长审核
* - supervising_executive_review主管Executive审核
* - department_review 且 deptType !== town_street部门审核非乡镇街道
*
* 2) 「同意、驳回、上报」
* - department_review 且 deptType === town_street部门审核乡镇街道
*
* 3) supervising_leader_review_1/2主管领导一审/二审)
* - deptType === prefecture同意、驳回、重新整改无上报
* - 其他层级:四项全展示(含上报)
*/
const resolveApprovalOptionsByCurrentTaskKey = (taskKey, deptType) => {
const key = String(taskKey || '');
const dept = String(deptType || '');
if (key === 'supervising_leader_review_1' || key === 'supervising_leader_review_2') {
if (dept === 'prefecture') {
return APPROVAL_OPTIONS_LEADER_PREFECTURE;
}
return APPROVAL_OPTIONS_ALL;
}
if (key === 'department_review' && dept === 'town_street') {
return APPROVAL_OPTIONS_WITH_REPORT;
}
if (
key === 'department_review' ||
key === 'section_chief_review' ||
key === 'supervising_executive_review'
) {
return APPROVAL_OPTIONS_AGREE_REJECT;
}
// 未识别节点:与改造前 reviewLevel=0 一致,默认展示四项
return key ? APPROVAL_OPTIONS_ALL : [];
};
const approvalOptions = computed(() => (
resolveApprovalOptionsByCurrentTaskKey(currentTaskKey.value, getCurrentDeptType())
));
const needNextAssignee = computed(() => {
if (formData.approvalOpinion !== 'agree' && formData.approvalOpinion !== 'report') {
return false;
}
if (formData.approvalOpinion === 'agree' && nextStepName.value === FLOW_END_STEP_NAME) {
return false;
}
return true;
});
const nextStepDisplay = computed(() => {
if (!formData.approvalOpinion) return '请先选择审批意见';
const fixedName = resolveFixedNextStepName(formData.approvalOpinion);
if (fixedName) return fixedName;
if (nextStepLoading.value) return '加载中...';
return nextStepName.value || '暂无下一步流程';
});
const resolveTaskIdFromAssign = (assign) => {
if (!assign) return '';
if (assign.taskId) return String(assign.taskId);
if (assign.flowTaskId) return String(assign.flowTaskId);
if (assign.currentTaskId) return String(assign.currentTaskId);
return '';
};
// 从 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 resolveTaskIdFromDetail = (data) => {
if (!data) return '';
if (data.taskId) return String(data.taskId);
if (data.flowTaskId) return String(data.flowTaskId);
if (data.currentTaskId) return String(data.currentTaskId);
const assigns = data.assigns || [];
const matchedAssign = resolveAssignWithRectify(assigns);
const fromMatched = resolveTaskIdFromAssign(matchedAssign);
if (fromMatched) return fromMatched;
for (const assign of assigns) {
const id = resolveTaskIdFromAssign(assign);
if (id) return id;
}
return '';
};
const resolveMatchedBranch = (data) => {
if (!data) return null;
const branches = data.branches || [];
return branches.find((item) => item.matched) || branches[0] || null;
};
const resolveNextTaskName = (data) => {
return resolveMatchedBranch(data)?.nextNode?.taskName || '';
};
const resolveNextTaskKey = (data) => {
return resolveNextNodeTaskKey(resolveMatchedBranch(data)?.nextNode);
};
const resolveNextNodeTaskKey = (node) => {
if (!node?.taskKey) return '';
return String(node.taskKey);
};
/** 从 next-nodes 响应解析当前节点 taskKeydata.currentTaskKey */
const resolveCurrentTaskKey = (data) => {
if (data?.currentTaskKey) {
return String(data.currentTaskKey);
}
if (data?.taskKey) {
return String(data.taskKey);
}
return '';
};
const clearSelectedAssignee = () => {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
pickerAssigneeIdentityId.value = '';
pickerAssigneeName.value = '';
};
const syncApprovalOpinionWithOptions = () => {
const validValues = approvalOptions.value.map((item) => item.value);
if (formData.approvalOpinion && !validValues.includes(formData.approvalOpinion)) {
formData.approvalOpinion = '';
}
if (!needNextAssignee.value) {
clearSelectedAssignee();
}
};
/** 同意/上报自动填充意见说明;驳回、重新整改等需用户自行填写 */
const applyDefaultOpinionRemark = (opinion) => {
if (opinion === 'agree') {
formData.opinionRemark = '同意';
} else if (opinion === 'report') {
formData.opinionRemark = '上报';
} else if (opinion) {
formData.opinionRemark = '';
}
};
const resolveApproveType = (opinion) => APPROVAL_TYPE_MAP[opinion] || '';
/**
* 构建 next-nodes 预览变量(按所选审批意见)
*
* - agree同意deptType + reportLeader=false
* - report上报deptType + reportLeader=true
* - re_rectify暂仅传 deptType
*
* 注reject驳回在 buildFlowNextNodesParams 中单独传 reject: true
*
* deptType 取自 storage userInfo.userIdentity.deptType
*/
const buildPreviewVariables = (approvalOpinion) => {
const deptType = getCurrentDeptType();
const vars = {};
if (deptType) {
vars.deptType = deptType;
}
if (approvalOpinion === 'report') {
vars.reportLeader = true;
} else if (approvalOpinion === 'agree') {
vars.reportLeader = false;
}
return vars;
};
/** 构建 next-nodes 请求参数(驳回时顶层传 reject: true */
const buildFlowNextNodesParams = (currentTaskId, approvalOpinion) => {
const params = {
taskId: currentTaskId,
includeSubProcess: FLOW_INCLUDE_SUB_PROCESS
};
if (approvalOpinion === 'reject') {
params.reject = true;
} else {
params.previewVariables = buildPreviewVariables(approvalOpinion);
}
return params;
};
const getStoredUserInfo = () => {
try {
const stored = uni.getStorageSync('userInfo');
if (!stored) return {};
return typeof stored === 'string' ? JSON.parse(stored) : stored;
} catch (error) {
return {};
}
};
const getCurrentDeptType = () => {
const userInfo = getStoredUserInfo();
const deptType = userInfo.userIdentity?.deptType;
return deptType != null && deptType !== '' ? String(deptType) : '';
};
const selectedAssigneeIdentityId = ref('');
const selectedAssigneeName = ref('');
const pickerAssigneeIdentityId = ref('');
const pickerAssigneeName = ref('');
const showAssigneePopup = ref(false);
const openAssigneePopup = () => {
if (!taskId.value) {
uni.showToast({ title: '缺少任务ID', icon: 'none' });
return;
}
pickerAssigneeIdentityId.value = selectedAssigneeIdentityId.value;
pickerAssigneeName.value = selectedAssigneeName.value;
showAssigneePopup.value = true;
};
const confirmAssigneeSelect = ({ identityId, name }) => {
selectedAssigneeIdentityId.value = identityId;
selectedAssigneeName.value = name;
showAssigneePopup.value = false;
};
const cancelAssigneeSelect = () => {
showAssigneePopup.value = false;
};
// 验收记录(只读展示)
const verifyData = reactive({
verifyId: null,
resultName: '',
remark: '',
verifierName: '',
verifyTime: ''
});
const verifyAttachments = ref([]);
/** 进入页面时拉取 currentTaskKey仅用于决定审批意见可选项 */
const fetchFlowContext = async () => {
if (!taskId.value) {
return;
}
try {
const res = await getFlowNextNodes({
taskId: taskId.value,
previewVariables: buildPreviewVariables(''),
includeSubProcess: FLOW_INCLUDE_SUB_PROCESS
});
if (res.code === 0) {
const payload = res.data && typeof res.data === 'object' ? res.data : res;
const resolvedKey = resolveCurrentTaskKey(payload);
if (resolvedKey) {
currentTaskKey.value = resolvedKey;
}
syncApprovalOpinionWithOptions();
}
} catch (error) {
console.error('获取流程上下文失败:', error);
}
};
/** 选择审批意见后,按 previewVariables 预览下一步流程节点 */
const fetchNextStep = async () => {
if (!formData.approvalOpinion) {
nextStepName.value = '';
nextStepKey.value = '';
return;
}
const fixedName = resolveFixedNextStepName(formData.approvalOpinion);
if (fixedName) {
nextStepName.value = fixedName;
nextStepKey.value = '';
return;
}
nextStepLoading.value = true;
try {
const currentTaskId = taskId.value;
if (!currentTaskId) {
nextStepName.value = '';
nextStepKey.value = '';
return;
}
const res = await getFlowNextNodes(
buildFlowNextNodesParams(currentTaskId, formData.approvalOpinion)
);
if (res.code === 0) {
const payload = res.data && typeof res.data === 'object' ? res.data : res;
nextStepName.value = resolveNextTaskName(payload);
nextStepKey.value = resolveNextTaskKey(payload);
if (!needNextAssignee.value) {
clearSelectedAssignee();
}
} else {
nextStepName.value = '';
nextStepKey.value = '';
}
} catch (error) {
console.error('获取下一步流程失败:', error);
nextStepName.value = '';
nextStepKey.value = '';
} finally {
nextStepLoading.value = false;
}
};
// 整改记录数据
const rectifyData = reactive({
rectifyPlan: '',
rectificationMeasures: '',
controlMeasures: '',
rectifyResult: '',
planCost: null,
actualCost: null,
managerNames: [],
memberNames: [],
rectifierName: '',
rectifyStatusName: ''
});
const formatPersonNames = (names) => {
if (!names) return '暂无';
if (Array.isArray(names)) {
const text = names.filter(Boolean).join('、');
return text || '暂无';
}
if (typeof names === 'string') {
return names.trim() || '暂无';
}
return '暂无';
};
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({
approvalOpinion: '', // 审批意见 agree/reject/report/re_rectify
opinionRemark: '' // 意见说明
});
const sendMsgFlagRadio = ref('yes');
const sendMsgFlag = computed(() => sendMsgFlagRadio.value === 'yes');
const suppressOpinionRemarkAutofill = ref(false);
watch(() => formData.approvalOpinion, (opinion) => {
clearSelectedAssignee();
nextStepName.value = '';
nextStepKey.value = '';
if (!suppressOpinionRemarkAutofill.value) {
applyDefaultOpinionRemark(opinion);
}
if (opinion) {
fetchNextStep();
}
syncApprovalOpinionWithOptions();
});
watch(needNextAssignee, (value) => {
if (!value) {
clearSelectedAssignee();
}
});
// 电子签名及草稿相关
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 signaturePaths = ref([]); // 缓存手写签名的绘制路径
const signatureLocalPath = ref(''); // 未上传云端的本地签名临时图
const isDraftExporting = ref(false); // 是否为草稿导出(非提交上传)
let signatureExportTimer = null;
/** 将已上传的签名地址应用到预览区 */
const applySignatureFromServer = (signPath) => {
const url = signPath ? toSubmitFileUrl(signPath) : '';
if (!url) {
showCanvas.value = true;
signatureServerPath.value = '';
signatureUrl.value = '';
signatureLocalPath.value = '';
isSignatureEmpty.value = true;
return;
}
signatureServerPath.value = url;
signatureUrl.value = url;
signatureLocalPath.value = '';
showCanvas.value = false;
isSignatureEmpty.value = false;
};
/** 将本地临时签名图应用到预览区(草稿回显) */
const applySignatureFromLocal = (localPath) => {
if (!localPath) return;
signatureLocalPath.value = localPath;
signatureUrl.value = localPath;
signatureServerPath.value = '';
showCanvas.value = false;
isSignatureEmpty.value = false;
};
const acceptDraftHasContent = (data) => {
const form = data.formData || {};
return !!(
form.approvalOpinion ||
form.opinionRemark ||
data.signatureServerPath ||
data.signatureLocalPath ||
(data.signaturePaths && data.signaturePaths.length > 0)
);
};
const {
showRestoreBanner,
clear: clearDraft,
restore: restoreDraft,
save: saveDraft,
bindAutoSave
} = useDraftCache({
getKey: () => buildDraftKey(DRAFT_NS.ACCEPT_APPROVAL, rectifyId.value),
getFallbackKeys: () => {
const primary = buildDraftKey(DRAFT_NS.ACCEPT_APPROVAL, rectifyId.value);
const compact = buildDraftKeyCompact(DRAFT_NS.ACCEPT_APPROVAL, rectifyId.value);
return compact !== primary ? [compact] : [];
},
getPayload: () => ({
formData: {
approvalOpinion: formData.approvalOpinion,
opinionRemark: formData.opinionRemark
},
selectedAssigneeIdentityId: selectedAssigneeIdentityId.value,
selectedAssigneeName: selectedAssigneeName.value,
signatureServerPath: signatureServerPath.value,
signatureUrl: signatureUrl.value,
signatureLocalPath: signatureLocalPath.value,
showCanvas: showCanvas.value,
signaturePaths: signaturePaths.value,
sendMsgFlagRadio: sendMsgFlagRadio.value
}),
hasContent: acceptDraftHasContent,
applyPayload: (data) => {
const form = data.formData || {};
suppressOpinionRemarkAutofill.value = true;
formData.approvalOpinion = form.approvalOpinion || '';
formData.opinionRemark = form.opinionRemark || '';
suppressOpinionRemarkAutofill.value = false;
selectedAssigneeIdentityId.value = data.selectedAssigneeIdentityId || '';
selectedAssigneeName.value = data.selectedAssigneeName || '';
signaturePaths.value = data.signaturePaths || [];
if (data.signatureServerPath || data.signatureUrl) {
applySignatureFromServer(data.signatureServerPath || data.signatureUrl);
} else if (data.signatureLocalPath) {
applySignatureFromLocal(data.signatureLocalPath);
} else {
showCanvas.value = data.showCanvas !== undefined ? data.showCanvas : true;
signatureServerPath.value = '';
signatureUrl.value = '';
signatureLocalPath.value = '';
isSignatureEmpty.value = true;
}
sendMsgFlagRadio.value = data.sendMsgFlagRadio === 'no' ? 'no' : 'yes';
},
clearForm: () => {
formData.approvalOpinion = '';
formData.opinionRemark = '';
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
signatureServerPath.value = '';
signatureUrl.value = '';
signatureLocalPath.value = '';
showCanvas.value = true;
signaturePaths.value = [];
if (signatureRef.value) {
signatureRef.value.clear();
}
sendMsgFlagRadio.value = 'yes';
},
canSave: () => !!rectifyId.value,
requireInitialized: true,
onAfterRestore: async (data) => {
if (taskId.value && !currentTaskKey.value) {
await fetchFlowContext();
}
if (formData.approvalOpinion) {
await fetchNextStep();
}
if (data.signatureServerPath || data.signatureUrl || data.signatureLocalPath) {
return;
}
if (data.signaturePaths?.length > 0) {
setTimeout(() => {
if (signatureRef.value) {
isSignatureEmpty.value = false;
}
}, 450);
}
}
});
bindAutoSave(() => [
formData.approvalOpinion,
formData.opinionRemark,
selectedAssigneeIdentityId.value,
selectedAssigneeName.value,
signatureServerPath.value,
signatureUrl.value,
signatureLocalPath.value,
showCanvas.value,
signaturePaths.value,
sendMsgFlagRadio.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 previewVerifyImage = (index) => {
const urls = verifyAttachments.value.map(item => getFullPath(item.filePath));
uni.previewImage({
current: index,
urls: urls
});
};
const mapPersonListToNickNames = (people) => {
if (!Array.isArray(people) || people.length === 0) return [];
const names = people
.map((person) => person.nickName || person.userName || person.name || '')
.filter(Boolean);
return [...new Set(names)];
};
const resolveManagerNames = (rectify) => {
if (Array.isArray(rectify.managers) && rectify.managers.length > 0) {
return mapPersonListToNickNames(rectify.managers);
}
return rectify.managerNames || [];
};
const resolveMemberNames = (rectify) => {
if (Array.isArray(rectify.members) && rectify.members.length > 0) {
return mapPersonListToNickNames(rectify.members);
}
return rectify.memberNames || [];
};
const resolveLatestVerify = (verifies) => {
if (!Array.isArray(verifies) || verifies.length === 0) return null;
return verifies[verifies.length - 1];
};
const resolveVerifyResultName = (verify) => {
if (!verify) return '';
if (verify.resultName) return verify.resultName;
if (verify.result === 1) return '通过';
if (verify.result === 2) return '不通过';
return '';
};
const applyVerifyData = (rectify) => {
const verify = resolveLatestVerify(rectify?.verifies);
if (!verify) {
verifyData.verifyId = null;
verifyData.resultName = '';
verifyData.remark = '';
verifyData.verifierName = '';
verifyData.verifyTime = '';
verifyAttachments.value = [];
return;
}
verifyData.verifyId = verify.verifyId ?? null;
verifyData.resultName = resolveVerifyResultName(verify);
verifyData.remark = verify.remark || '';
verifyData.verifierName = verify.verifierName || '';
verifyData.verifyTime = verify.verifyTime || '';
verifyAttachments.value = verify.attachments || [];
};
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.managerNames = resolveManagerNames(rectify);
rectifyData.memberNames = resolveMemberNames(rectify);
rectifyData.rectifierName = rectify.rectifierName || '';
rectifyData.rectifyStatusName = rectify.rectifyStatusName || rectify.statusName || '';
rectifyAttachments.value = rectify.attachments || [];
applyVerifyData(rectify);
};
// 获取整改详情(有 rectifyId 时作为唯一数据源)
const fetchRectifyDetail = async () => {
if (!rectifyId.value) return;
try {
const res = await getRectifyDetail({ rectifyId: rectifyId.value });
if (res.code === 0 && res.data) {
applyRectifyData(res.data);
if (!hazardId.value && res.data.hazardId) {
hazardId.value = String(res.data.hazardId);
}
if (!assignId.value && res.data.assignId) {
assignId.value = String(res.data.assignId);
}
const resolvedTaskId = resolveTaskIdFromDetail(res.data);
if (resolvedTaskId) {
taskId.value = resolvedTaskId;
}
} else {
uni.showToast({ title: res.msg || '获取整改详情失败', icon: 'none' });
}
} catch (error) {
console.error('获取整改详情失败:', error);
uni.showToast({ title: '获取整改详情失败', icon: 'none' });
}
};
// 获取隐患详情(无 rectifyId 时,从 assigns[].rectify 取整改记录)
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);
if (!rectifyId.value && assign.rectify.rectifyId) {
rectifyId.value = String(assign.rectify.rectifyId);
}
} else {
uni.showToast({ title: '该隐患暂无整改记录', icon: 'none' });
}
const resolvedTaskId = resolveTaskIdFromAssign(assign) || resolveTaskIdFromDetail(res.data);
if (resolvedTaskId) {
taskId.value = resolvedTaskId;
}
} else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' });
}
} catch (error) {
console.error('获取隐患详情失败:', error);
uni.showToast({ title: '请求失败', icon: 'none' });
}
};
const loadPageData = async () => {
if (hazardId.value) {
await fetchDetail();
} else if (rectifyId.value) {
await fetchRectifyDetail();
}
await fetchFlowContext();
};
const validateFormBeforeSubmit = () => {
if (!taskId.value) {
uni.showToast({ title: '缺少任务ID', icon: 'none' });
return false;
}
if (!formData.approvalOpinion) {
uni.showToast({ title: '请选择审批意见', icon: 'none' });
return false;
}
if (!formData.opinionRemark?.trim()) {
uni.showToast({ title: '请输入意见说明', icon: 'none' });
return false;
}
if (!nextStepName.value) {
uni.showToast({ title: '暂无下一步流程', icon: 'none' });
return false;
}
if (!isFixedNextStepOpinion(formData.approvalOpinion) && !nextStepKey.value) {
uni.showToast({ title: '暂无下一步流程', icon: 'none' });
return false;
}
if (needNextAssignee.value && !selectedAssigneeIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return false;
}
if (!verifyData.verifyId) {
uni.showToast({ title: '缺少验收记录ID', icon: 'none' });
return false;
}
return true;
};
// 取消
const handleCancel = () => {
uni.navigateBack();
};
// 提交审批
const handleSubmit = async () => {
if (!validateFormBeforeSubmit()) {
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 && !signatureLocalPath.value) {
uni.showToast({
title: '请进行电子签名',
icon: 'none'
});
return;
}
isSubmitting.value = true;
uni.showLoading({ title: '正在提交...', mask: true });
try {
if (!signatureServerPath.value && signatureLocalPath.value) {
const { url } = await uploadToCloud(signatureLocalPath.value);
applySignatureFromServer(url);
}
await executeSubmit();
} catch (err) {
isSubmitting.value = false;
uni.hideLoading();
console.error('签名上传失败:', err);
uni.showToast({ title: '签名上传失败,请重试', icon: 'none' });
}
}
};
// 真正调取后台接口提交审批
const executeSubmit = async () => {
const params = {
taskId: taskId.value,
bizId: Number(verifyData.verifyId),
comment: formData.opinionRemark.trim(),
type: resolveApproveType(formData.approvalOpinion),
signPath: signatureServerPath.value || '',
sendMsgFlag: sendMsgFlag.value,
nextStepName: nextStepName.value
};
if (nextStepKey.value) {
params.nextStepKey = nextStepKey.value;
}
if (needNextAssignee.value) {
params.nextAssigneeIdentityId = Number(selectedAssigneeIdentityId.value);
params.nextAssigneeName = selectedAssigneeName.value;
params.nextAssigneeCategory = 'identity';
}
if (formData.approvalOpinion === 'report') {
params.reportLeader = true;
}
console.log('提交审批参数:', params);
try {
const res = await flowTaskApprove(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 onSignatureStart = () => {
isSignatureEmpty.value = false;
};
const onSignatureSigning = () => {
isSignatureEmpty.value = false;
};
const onSignatureClear = () => {
isSignatureEmpty.value = true;
signatureLocalPath.value = '';
};
const scheduleSignatureDraftExport = () => {
clearTimeout(signatureExportTimer);
signatureExportTimer = setTimeout(() => {
if (
!showCanvas.value ||
isSignatureEmpty.value ||
!signatureRef.value ||
isSubmitting.value ||
isDraftExporting.value
) {
return;
}
isDraftExporting.value = true;
signatureRef.value.confirm();
}, 600);
};
const onSignatureEnd = () => {
isSignatureEmpty.value = false;
scheduleSignatureDraftExport();
};
// 清除画布
const clearSignature = () => {
clearTimeout(signatureExportTimer);
isSignatureEmpty.value = true;
signatureLocalPath.value = '';
if (signatureRef.value) {
signatureRef.value.clear();
}
};
// 重新签字
const reSign = () => {
clearTimeout(signatureExportTimer);
isSignatureEmpty.value = true;
showCanvas.value = true;
signatureUrl.value = '';
signatureServerPath.value = '';
signatureLocalPath.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) => {
if (isDraftExporting.value) {
isDraftExporting.value = false;
applySignatureFromLocal(tempFilePath);
saveDraft();
return;
}
try {
const { url } = await uploadToCloud(tempFilePath);
applySignatureFromServer(url);
saveDraft();
if (isSubmitting.value) {
await executeSubmit();
}
} catch (err) {
isSubmitting.value = false;
uni.hideLoading();
console.error('签名上传失败:', err);
uni.showToast({ title: '签名上传失败,请重试', icon: 'none' });
}
};
onHide(() => {
if (showCanvas.value && !isSignatureEmpty.value && signatureRef.value && !isDraftExporting.value) {
isDraftExporting.value = true;
signatureRef.value.confirm();
}
saveDraft();
});
onLoad(async (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;
}
if (options.taskId) {
taskId.value = options.taskId;
}
// taskKey 仅作进入页面临时占位fetchFlowContext 会以接口 currentTaskKey 覆盖
if (options.taskKey) {
currentTaskKey.value = String(options.taskKey);
}
console.log('验收审批页面参数:', {
rectifyId: rectifyId.value,
hazardId: hazardId.value,
assignId: assignId.value,
taskId: taskId.value,
taskKey: currentTaskKey.value
});
await loadPageData();
// 页面打开时自动恢复草稿
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;
}
}
.approval-radio-group {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.static-field {
background: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 20rpx 24rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
color: #333;
line-height: 1.5;
}
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 20rpx 24rpx;
margin-bottom: 20rpx;
.select-content {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
// 签名相关样式
.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>