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

1219 lines
34 KiB
Vue
Raw Permalink 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="padding page">
<view class="padding bg-white radius">
<view class="flex margin-bottom">
<view>隐患</view>
<view class="text-red">*</view>
</view>
<view class="picker-input readonly" v-if="hazardLocked">
<text :class="selectedHazard ? '' : 'text-gray'">{{ selectedHazard || '加载中...' }}</text>
</view>
<view class="picker-input" v-else @click="showHazardPicker = true">
<text :class="selectedHazard ? '' : 'text-gray'">{{ selectedHazard || '请选择隐患' }}</text>
</view>
<view class="flex margin-bottom margin-top">
<view>整改时限</view>
</view>
<view class="picker-input readonly">
<text :class="formData.rectifyDeadline ? '' : 'text-gray'">{{ formData.rectifyDeadline || '请先选择隐患' }}</text>
</view>
<view class="margin-bottom margin-top">隐患治理责任单位</view>
<view class="picker-input readonly">
<text :class="selectedDeptName ? '' : 'text-gray'">{{ selectedDeptName || '请先选择隐患' }}</text>
</view>
<view class="margin-bottom margin-top">主要负责人</view>
<view class="picker-input readonly">
<text :class="formData.responsiblePerson ? '' : 'text-gray'">{{ formData.responsiblePerson || '请先选择隐患' }}</text>
</view>
<view class="ai-btn-wrapper margin-top margin-bottom">
<button class="ai-analyze-btn" :loading="aiGenerating" :disabled="aiGenerating" @click="handleAiGenerate">
<text v-if="!aiGenerating" class="cuIcon-magic ai-btn-icon"></text>
{{ aiGenerating ? 'AI生成中...' : 'AI 生成销号方案' }}
</button>
</view>
<view class="margin-bottom margin-top">主要治理内容</view>
<up-textarea v-model="formData.mainTreatmentContent" placeholder="暂无" disabled></up-textarea>
<view class="margin-bottom margin-top">隐患治理完成内容</view>
<up-textarea v-model="formData.treatmentResult" placeholder="暂无" disabled></up-textarea>
<view class="margin-bottom margin-top">隐患治理责任单位自行验收的情况</view>
<up-textarea v-model="formData.selfVerifyContent" placeholder="暂无" disabled></up-textarea>
<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>
<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 signature-action-btn" @click="clearSignature">清除重写</button>
<button v-else class="cu-btn sm round line-blue signature-action-btn" @click="reSign">重新签名</button>
</view>
<view class="signature-box margin-bottom">
<view v-if="!showCanvas" class="signature-display flex align-center justify-center">
<image
v-if="signatureUrl"
:src="signatureUrl"
class="signature-img"
mode="aspectFit"
></image>
<text v-else class="signature-placeholder">暂无签名请点击重新签名</text>
</view>
<view v-if="showCanvas && !showAssigneePopup" class="signature-pad-wrap">
<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>
<up-picker
v-if="!hazardLocked"
:show="showHazardPicker"
:columns="hazardColumns"
@confirm="onHazardConfirm"
@cancel="showHazardPicker = false"
@close="showHazardPicker = false"
></up-picker>
<FlowAssigneePickerPopup
:show="showAssigneePopup"
:task-id="taskId"
v-model:picker-identity-id="pickerAssigneeIdentityId"
v-model:picker-name="pickerAssigneeName"
@cancel="cancelAssigneeSelect"
@confirm="confirmAssigneeSelect"
/>
</view>
</template>
<script setup>
import { ref, reactive, computed, watch, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import {
flowTaskApprove,
getHiddenDangerDetail,
getRectifyDetail,
generateWriteoffContent,
getFlowNextNodes,
getWriteoffForm
} from '@/request/api.js';
import FlowAssigneePickerPopup from '@/components/flow/FlowAssigneePickerPopup.vue';
import { uploadToCloud, toSubmitFileUrl } from '@/utils/upload.js';
import { buildWriteoffHazardFromOptions } from '@/utils/hazardNav.js';
const showHazardPicker = ref(false);
const hazardLocked = ref(false);
const selectedHazard = ref('');
const selectedHazardId = ref('');
const applyId = ref('');
const assignId = ref('');
const taskId = ref('');
const hazardColumns = ref([['暂无数据']]);
const acceptanceHazardList = ref([]);
const selectedDeptName = ref('');
const aiGenerating = ref(false);
const sendMsgFlagRadio = ref('yes');
const sendMsgFlag = computed(() => sendMsgFlagRadio.value === 'yes');
const nextStepName = ref('');
const nextStepKey = ref('');
const nextStepLoading = ref(false);
const FLOW_END_STEP_NAME = '销号子流程结束';
const FIXED_NEXT_STEP_MAP = {
re_rectify: '隐患整改',
verify: '隐患验收'
};
/** 当前流程节点 key来自 next-nodes 接口 data.currentTaskKey */
const currentTaskKey = ref('');
const resolveFixedNextStepName = (opinion) => FIXED_NEXT_STEP_MAP[opinion] || '';
const isFixedNextStepOpinion = (opinion) => !!resolveFixedNextStepName(opinion);
const APPROVAL_TYPE_MAP = {
agree: 'agree',
reject: 'reject',
report: 'report',
re_rectify: 'rectify',
verify: 'verify'
};
/** 审批意见选项:同意、驳回 */
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' },
{ label: '重新验收', value: 'verify' }
];
/** 主管领导审批州级prefecture无「上报」 */
const APPROVAL_OPTIONS_LEADER_PREFECTURE = [
{ label: '同意', value: 'agree' },
{ label: '驳回', value: 'reject' },
{ label: '重新整改', value: 're_rectify' },
{ label: '重新验收', value: 'verify' }
];
/**
* 根据 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;
}
return [];
};
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 selectedAssigneeIdentityId = ref('');
const selectedAssigneeName = ref('');
const pickerAssigneeIdentityId = ref('');
const pickerAssigneeName = ref('');
const showAssigneePopup = ref(false);
const showCanvas = ref(true);
const signatureUrl = ref('');
const signatureServerPath = ref('');
const signatureWidth = ref(340);
const signatureRef = ref(null);
const isSignatureEmpty = ref(true);
const isSubmitting = ref(false);
const signatureLocalPath = ref('');
const formData = reactive({
rectifyDeadline: '',
responsibleDeptId: '',
responsiblePerson: '',
mainTreatmentContent: '',
treatmentResult: '',
selfVerifyContent: '',
approvalOpinion: '',
opinionRemark: ''
});
const syncApprovalOpinionWithOptions = () => {
const validValues = approvalOptions.value.map((item) => item.value);
if (formData.approvalOpinion && !validValues.includes(formData.approvalOpinion)) {
formData.approvalOpinion = '';
}
if (!needNextAssignee.value) {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
pickerAssigneeIdentityId.value = '';
pickerAssigneeName.value = '';
}
};
/** 同意/上报自动填充意见说明;驳回、重新整改、重新验收需用户自行填写 */
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] || '';
const findHazardInList = (hazardId) => {
return acceptanceHazardList.value.find(
(item) => String(item.hazardId) === String(hazardId) || String(item.id) === String(hazardId)
);
};
const fillHazardRelatedFields = (hazard) => {
formData.rectifyDeadline = hazard.deadline || '';
selectedDeptName.value = hazard.deptName || '';
formData.responsiblePerson = hazard.rectifierName || '';
formData.responsibleDeptId = hazard.deptId || '';
};
const applyWriteoffHazardPreview = (hazard) => {
if (!hazard) return;
selectedHazard.value = hazard.title || hazard.hazardTitle || `隐患${hazard.hazardId}`;
selectedHazardId.value = String(hazard.hazardId);
if (hazard.assignId) {
assignId.value = String(hazard.assignId);
}
if (hazard.taskId) {
taskId.value = String(hazard.taskId);
}
fillHazardRelatedFields(hazard);
};
const resolveTaskIdFromHazard = (hazard) => {
if (!hazard) return '';
const candidates = [
hazard.taskId,
hazard.flowTaskId,
hazard.currentTaskId,
hazard.flowTask?.taskId,
hazard.currentTask?.taskId
];
for (const id of candidates) {
if (id != null && id !== '') return String(id);
}
return '';
};
const resolveTaskIdFromAssign = (assign) => {
if (!assign) return '';
const candidates = [
assign.taskId,
assign.flowTaskId,
assign.currentTaskId,
assign.rectify?.taskId,
assign.rectify?.flowTaskId,
assign.rectify?.currentTaskId,
assign.flow?.taskId,
assign.currentTask?.taskId
];
for (const id of candidates) {
if (id != null && id !== '') return String(id);
}
return '';
};
const resolveAssignWithRectify = (assigns, currentAssignId) => {
if (!assigns?.length) return null;
if (currentAssignId) {
const byAssignId = assigns.find(
(item) => String(item.assignId) === String(currentAssignId) && item.rectify
);
if (byAssignId) return byAssignId;
}
return assigns.find((item) => item.rectify) || assigns[0] || 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, assignId.value);
const fromMatched = resolveTaskIdFromAssign(matchedAssign);
if (fromMatched) return fromMatched;
for (const assign of assigns) {
const id = resolveTaskIdFromAssign(assign);
if (id) return id;
}
return '';
};
const resolveNextTaskName = (data) => {
if (!data) return '';
const branches = data.branches || [];
const matchedBranch = branches.find((item) => item.matched) || branches[0];
return matchedBranch?.nextNode?.taskName || '';
};
const resolveMatchedBranch = (data) => {
if (!data) return null;
const branches = data.branches || [];
return branches.find((item) => item.matched) || branches[0] || null;
};
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 '';
return String(data.currentTaskKey);
};
/**
* 构建 next-nodes 预览变量(按所选审批意见)
*
* - agree同意deptType + reportLeader=false
* - report上报deptType + reportLeader=true
* - re_rectify / verify暂仅传 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: false
};
if (approvalOpinion === 'reject') {
params.reject = true;
} else {
params.previewVariables = buildPreviewVariables(approvalOpinion);
}
return params;
};
/** 进入页面时拉取 currentTaskKey仅用于决定审批意见可选项 */
const fetchFlowContext = async () => {
if (!taskId.value) {
currentTaskKey.value = '';
return;
}
try {
const res = await getFlowNextNodes({
taskId: taskId.value,
previewVariables: buildPreviewVariables(''),
includeSubProcess: false
});
if (res.code === 0) {
const payload = res.data && typeof res.data === 'object' ? res.data : res;
currentTaskKey.value = resolveCurrentTaskKey(payload);
syncApprovalOpinionWithOptions();
} else {
currentTaskKey.value = '';
}
} catch (error) {
console.error('获取流程上下文失败:', error);
currentTaskKey.value = '';
}
};
const getCurrentDeptType = () => {
const userInfo = getStoredUserInfo();
const deptType = userInfo.userIdentity?.deptType;
return deptType != null && deptType !== '' ? String(deptType) : '';
};
const getStoredUserInfo = () => {
try {
const stored = uni.getStorageSync('userInfo');
if (!stored) return {};
return typeof stored === 'string' ? JSON.parse(stored) : stored;
} catch (error) {
return {};
}
};
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;
};
/** 选择审批意见后,按 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) {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
pickerAssigneeIdentityId.value = '';
pickerAssigneeName.value = '';
}
} else {
nextStepName.value = '';
nextStepKey.value = '';
}
} catch (error) {
console.error('获取下一步流程失败:', error);
nextStepName.value = '';
nextStepKey.value = '';
} finally {
nextStepLoading.value = false;
}
};
const resolveTaskIdForHazard = async (hazard) => {
const presetTaskId = taskId.value;
const fromHazard = resolveTaskIdFromHazard(hazard);
if (fromHazard) {
taskId.value = fromHazard;
}
if (!hazard?.hazardId) {
return;
}
if (!assignId.value && hazard.assignId) {
assignId.value = String(hazard.assignId);
}
if (taskId.value) {
return;
}
try {
const params = { hazardId: hazard.hazardId };
if (assignId.value) {
params.assignId = assignId.value;
}
const res = await getHiddenDangerDetail(params);
if (res.code === 0 && res.data) {
taskId.value = resolveTaskIdFromDetail(res.data) || presetTaskId;
}
} catch (error) {
console.error('获取隐患任务信息失败:', error);
}
if (!taskId.value && presetTaskId) {
taskId.value = presetTaskId;
}
};
const resetFlowSelection = () => {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
nextStepName.value = '';
nextStepKey.value = '';
currentTaskKey.value = '';
};
const applySelectedHazard = async (hazard) => {
if (!hazard) return;
selectedHazard.value = hazard.title || hazard.hazardTitle || `隐患${hazard.hazardId}`;
selectedHazardId.value = hazard.hazardId;
assignId.value = hazard.assignId ? String(hazard.assignId) : '';
fillHazardRelatedFields(hazard);
resetFlowSelection();
await resolveTaskIdForHazard(hazard);
await fetchFlowContext();
};
const formatDeadline = (value) => {
if (!value) return '';
return String(value).split(' ')[0];
};
const applyWriteoffFormData = (data) => {
if (!data) return;
if (data.hazardId != null) {
selectedHazardId.value = String(data.hazardId);
}
selectedHazard.value = data.title || '';
formData.rectifyDeadline = formatDeadline(data.rectifyDeadline);
formData.responsibleDeptId = data.responsibleDeptId ?? '';
selectedDeptName.value = data.responsibleDeptName || '';
formData.responsiblePerson = data.responsiblePerson || '';
if (data.applyId != null) {
applyId.value = String(data.applyId);
}
formData.mainTreatmentContent = data.mainTreatmentContent || '';
formData.treatmentResult = data.treatmentResult || '';
formData.selfVerifyContent = data.selfVerifyContent || '';
};
const fetchWriteoffForm = async (hazardId) => {
if (!hazardId) return null;
try {
const res = await getWriteoffForm(hazardId);
if (res.code === 0 && res.data) {
applyWriteoffFormData(res.data);
} else {
uni.showToast({ title: res.msg || '获取销号审批表单失败', icon: 'none' });
}
return res;
} catch (error) {
console.error('获取销号审批表单失败:', error);
uni.showToast({ title: '获取销号审批表单失败', icon: 'none' });
return null;
}
};
const preselectHazardById = async (hazardId) => {
const hazard = findHazardInList(hazardId);
if (!hazard) return false;
await applySelectedHazard(hazard);
return true;
};
const onHazardConfirm = async (e) => {
if (e.value && e.value.length > 0) {
const index = e.indexs[0];
const hazard = acceptanceHazardList.value[index];
if (hazard) {
await applySelectedHazard(hazard);
}
}
showHazardPicker.value = false;
};
watch(() => formData.approvalOpinion, (opinion) => {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
pickerAssigneeIdentityId.value = '';
pickerAssigneeName.value = '';
nextStepName.value = '';
nextStepKey.value = '';
applyDefaultOpinionRemark(opinion);
if (opinion) {
fetchNextStep();
}
syncApprovalOpinionWithOptions();
});
watch(needNextAssignee, (value) => {
if (!value) {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
pickerAssigneeIdentityId.value = '';
pickerAssigneeName.value = '';
}
});
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 resetSignature = () => {
showCanvas.value = true;
signatureUrl.value = '';
signatureServerPath.value = '';
signatureLocalPath.value = '';
isSignatureEmpty.value = true;
if (signatureRef.value) {
signatureRef.value.clear();
}
};
const onSignatureStart = () => {
isSignatureEmpty.value = false;
};
const onSignatureSigning = () => {
isSignatureEmpty.value = false;
};
const onSignatureClear = () => {
isSignatureEmpty.value = true;
signatureLocalPath.value = '';
};
const onSignatureEnd = () => {
isSignatureEmpty.value = false;
};
const clearSignature = () => {
isSignatureEmpty.value = true;
signatureLocalPath.value = '';
if (signatureRef.value) {
signatureRef.value.clear();
}
};
const reSign = () => {
isSignatureEmpty.value = true;
showCanvas.value = true;
signatureUrl.value = '';
signatureServerPath.value = '';
signatureLocalPath.value = '';
nextTick(() => {
if (signatureRef.value) {
signatureRef.value.clear();
}
});
};
const onSignatureConfirm = async (tempFilePath) => {
try {
const { url } = await uploadToCloud(tempFilePath);
applySignatureFromServer(url);
if (isSubmitting.value) {
await executeSubmit();
}
} catch (err) {
isSubmitting.value = false;
uni.hideLoading();
console.error('签名上传失败:', err);
uni.showToast({ title: '签名上传失败,请重试', icon: 'none' });
}
};
const handleAiGenerate = async () => {
if (!selectedHazardId.value) {
uni.showToast({ title: '请先选择隐患', icon: 'none' });
return;
}
aiGenerating.value = true;
try {
const hazardRes = await getHiddenDangerDetail({ hazardId: selectedHazardId.value });
if (hazardRes.code !== 0 || !hazardRes.data) {
uni.showToast({ title: '获取隐患详情失败', icon: 'none' });
return;
}
const assigns = hazardRes.data.assigns;
if (!assigns || assigns.length === 0 || !assigns[0].rectify) {
uni.showToast({ title: '该隐患暂无整改记录', icon: 'none' });
return;
}
const rectifyId = assigns[0].rectify.rectifyId;
const rectifyRes = await getRectifyDetail({ rectifyId });
if (rectifyRes.code !== 0 || !rectifyRes.data) {
uni.showToast({ title: '获取整改详情失败', icon: 'none' });
return;
}
const rectifyPlan = rectifyRes.data.rectifyPlan;
if (!rectifyPlan) {
uni.showToast({ title: '整改方案内容为空', icon: 'none' });
return;
}
const aiRes = await generateWriteoffContent({ rectifyContent: rectifyPlan });
if (aiRes.code === 0 && aiRes.data) {
formData.mainTreatmentContent = aiRes.data.mainContent || '';
formData.treatmentResult = aiRes.data.completionContent || '';
formData.selfVerifyContent = aiRes.data.selfInspection || '';
uni.showToast({ title: 'AI生成成功', icon: 'success' });
} else {
uni.showToast({ title: aiRes.msg || 'AI生成失败', icon: 'none' });
}
} catch (error) {
console.error('AI生成销号方案失败:', error);
uni.showToast({ title: 'AI生成失败请重试', icon: 'none' });
} finally {
aiGenerating.value = false;
}
};
const handleCancel = () => {
uni.navigateBack();
};
const ensureSignatureAndSubmit = async () => {
if (showCanvas.value) {
if (!signatureRef.value || isSignatureEmpty.value) {
uni.showToast({ title: '请进行电子签名', icon: 'none' });
return;
}
isSubmitting.value = true;
uni.showLoading({ title: '正在提交...', mask: true });
signatureRef.value.confirm();
return;
}
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 validateFormBeforeSubmit = () => {
if (!taskId.value) {
uni.showToast({ title: '缺少任务ID', icon: 'none' });
return false;
}
if (!applyId.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;
}
return true;
};
const handleSubmit = async () => {
if (!selectedHazardId.value) {
uni.showToast({ title: '请选择隐患', icon: 'none' });
return;
}
if (!validateFormBeforeSubmit()) {
return;
}
await ensureSignatureAndSubmit();
};
const executeSubmit = async () => {
const params = {
taskId: taskId.value,
bizId: Number(applyId.value),
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;
}
try {
const res = await flowTaskApprove(params);
uni.hideLoading();
if (res.code === 0) {
uni.showToast({ title: '审批成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({ title: res.msg || '审批失败', icon: 'none' });
}
} catch (error) {
console.error('审批失败:', error);
uni.showToast({ title: '请求失败', icon: 'none' });
} finally {
isSubmitting.value = false;
uni.hideLoading();
}
};
onLoad(async (options) => {
try {
const sysInfo = uni.getSystemInfoSync();
signatureWidth.value = sysInfo.windowWidth - 40;
} catch (error) {
console.error('获取系统信息失败:', error);
}
const hazardId = options?.hazardId;
hazardLocked.value = options?.locked === '1';
if (options?.assignId) {
assignId.value = String(options.assignId);
}
if (options?.taskId) {
taskId.value = String(options.taskId);
}
// taskKey 仅作进入页面临时占位fetchFlowContext 会以接口 currentTaskKey 覆盖
if (options?.taskKey) {
currentTaskKey.value = String(options.taskKey);
}
const hazardFromOptions = buildWriteoffHazardFromOptions(options);
if (hazardFromOptions) {
applyWriteoffHazardPreview(hazardFromOptions);
}
if (hazardId) {
selectedHazardId.value = String(hazardId);
await fetchWriteoffForm(hazardId);
if (taskId.value) {
await fetchFlowContext();
}
} else if (taskId.value) {
await fetchFlowContext();
}
});
</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;
}
}
.ai-btn-wrapper {
display: flex;
justify-content: flex-end;
}
.ai-analyze-btn {
display: flex;
align-items: center;
justify-content: center;
height: 72rpx;
padding: 0 32rpx;
font-size: 28rpx;
color: #fff;
background: linear-gradient(135deg, #4facfe 0%, #2668EA 100%);
border-radius: 36rpx;
border: none;
&::after {
border: none;
}
.ai-btn-icon {
margin-right: 8rpx;
font-size: 30rpx;
}
&[disabled] {
opacity: 0.7;
}
}
.picker-input {
background: #fff;
border-radius: 8rpx;
padding: 24rpx 20rpx;
margin-bottom: 20rpx;
border: 1rpx solid #eee;
text {
font-size: 28rpx;
}
&.readonly {
background: #f5f5f5;
color: #666;
}
}
.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;
}
.approval-radio-group {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.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-action-btn {
margin: 0;
padding: 0 20rpx;
height: 50rpx;
font-size: 22rpx;
}
.signature-box {
width: 100%;
min-height: 240rpx;
background: #f8f8f8;
border: 1rpx dashed #dcdfe6;
border-radius: 8rpx;
.signature-display {
width: 100%;
height: 160px;
background-color: #f8f8f8;
}
.signature-pad-wrap {
border: 1px dashed #dcdfe6;
border-radius: 8rpx;
overflow: hidden;
background-color: #f8f8f8;
}
.signature-img {
width: 100%;
height: 100%;
}
.signature-placeholder {
color: #909399;
font-size: 28rpx;
}
}
</style>