一单四制优化及加入工作流

This commit is contained in:
王利强
2026-07-27 09:27:22 +08:00
parent 455fd4fc07
commit 7c3285ed5a
725 changed files with 19668 additions and 2921 deletions

View File

@@ -52,8 +52,8 @@
<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>
<button :class="['result-btn', formData.result === 1 ? 'active' : '']" @click="onResultChange(1)">通过</button>
<button :class="['result-btn', formData.result === 2 ? 'active' : '']" @click="onResultChange(2)">不通过</button>
</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">验收备注</view>
@@ -63,6 +63,15 @@
<view class="text-gray">验收图片/视频</view>
</view>
<up-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple imageMode="aspectFill" :maxCount="10"></up-upload>
<view v-if="formData.result === 1" class="flex margin-bottom margin-top">
<view class="text-gray">是否快速审批</view>
<view class="text-red">*</view>
</view>
<view v-if="formData.result === 1" class="flex" style="gap: 20rpx;">
<button :class="['result-btn', formData.quickApproveRadio === 'yes' ? 'active' : '']" @click="onQuickApproveChange('yes')"></button>
<button :class="['result-btn', formData.quickApproveRadio === 'no' ? 'active' : '']" @click="onQuickApproveChange('no')"></button>
</view>
<!-- 隐藏的 Canvas用于渲染防作弊时间戳水印 -->
<canvas canvas-id="watermarkCanvas" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'fixed', left: '-9999px', top: '-9999px' }"></canvas>
@@ -71,13 +80,48 @@
<view class="text-gray">下一步流程</view>
<view class="text-red">*</view>
</view>
<view class="static-field">申请隐患销号</view>
<view class="static-field">{{ nextStepDisplay }}</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">下一步处理人</view>
<view class="text-red">*</view>
</view>
<view class="static-field">部门企业管理员</view>
<view v-if="formData.result === 2" class="static-field">{{ rectifierDisplayName }}</view>
<view v-else class="select-trigger" @click="openAssigneePopup">
<view class="select-content" :class="{ 'text-gray': !selectedAssigneeName }">
{{ selectedAssigneeName || '请选择下一步处理人' }}
</view>
<text class="cuIcon-unfold"></text>
</view>
<u-popup :show="showAssigneePopup" mode="bottom" round="20" @close="cancelAssigneeSelect">
<view class="user-popup">
<view class="popup-header">
<view class="popup-title text-bold">选择下一步处理人</view>
<view class="popup-close" @click="cancelAssigneeSelect">×</view>
</view>
<scroll-view class="user-list-scroll" scroll-y>
<view v-if="assigneeLoading" class="empty-tip">加载中...</view>
<view v-else-if="assigneeList.length === 0" class="empty-tip">暂无人员数据</view>
<template v-else>
<view
v-for="user in assigneeList"
:key="getAssigneeItemKey(user)"
class="user-item"
:class="{ active: String(pickerAssigneeIdentityId) === String(resolveAssigneeIdentityId(user)) }"
@click="onAssigneeItemClick(user)"
>
<text class="user-item-text">{{ formatAssigneeDisplayName(user) }}</text>
<text v-if="String(pickerAssigneeIdentityId) === String(resolveAssigneeIdentityId(user))" class="cuIcon-check text-blue"></text>
</view>
</template>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="cancelAssigneeSelect">取消</button>
<button class="btn-confirm bg-blue" @click="confirmAssigneeSelect">确定</button>
</view>
</view>
</u-popup>
<!-- 短信提醒暂不使用
<view class="flex margin-bottom margin-top">
@@ -115,7 +159,8 @@
<text v-else class="signature-placeholder">暂无签名请点击重新签名</text>
</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;">
<!-- 弹窗打开时卸载 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"
@@ -144,9 +189,9 @@
</template>
<script setup>
import { ref, reactive, nextTick, getCurrentInstance } from 'vue';
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue';
import { onLoad, onHide } from '@dcloudio/uni-app';
import { acceptanceRectification, getHiddenDangerDetail } from '@/request/api.js';
import { acceptanceRectification, getHiddenDangerDetail, getRectifyDetail, getFlowNextNodes, getDeptUsers } from '@/request/api.js';
import { toImageUrl } from '@/request/request.js';
import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js';
import { useDraftCache } from '@/utils/useDraftCache.js';
@@ -161,6 +206,228 @@
const rectifyId = ref('');
const hazardId = ref('');
const assignId = ref('');
const taskId = ref('');
// 下一步流程
const nextStepName = ref('');
const nextStepLoading = ref(false);
const nextStepDisplay = computed(() => {
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 resolveNextTaskName = (data) => {
if (!data) return '';
const branches = data.branches || [];
const matchedBranch = branches.find((item) => item.matched) || branches[0];
return matchedBranch?.nextNode?.taskName || '';
};
const buildPreviewVariables = () => {
if (formData.result === 2) {
return { pass: false };
}
return {
pass: true,
quickApprove: formData.quickApproveRadio === 'yes'
};
};
const getStoredUserInfo = () => {
try {
const stored = uni.getStorageSync('userInfo');
if (!stored) return {};
return typeof stored === 'string' ? JSON.parse(stored) : stored;
} catch (error) {
return {};
}
};
const getCurrentDeptId = () => {
const userInfo = getStoredUserInfo();
const identity = userInfo.userIdentity;
if (identity?.deptId != null && identity.deptId !== '') {
return String(identity.deptId);
}
if (userInfo.deptId != null && userInfo.deptId !== '') {
return String(userInfo.deptId);
}
return '';
};
const rectifierDisplayName = computed(() => rectifyData.rectifierName?.trim() || '暂无');
const selectedAssigneeIdentityId = ref('');
const selectedAssigneeName = ref('');
const pickerAssigneeIdentityId = ref('');
const pickerAssigneeName = ref('');
const showAssigneePopup = ref(false);
const assigneeList = ref([]);
const assigneeLoading = ref(false);
const resolveAssigneeIdentityId = (user) => {
if (!user) return '';
const id = user.identityId ?? user.userIdentityId ?? user.userId ?? '';
return id === '' || id == null ? '' : String(id);
};
const getAssigneeItemKey = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (identityId) return `identity-${identityId}`;
return `user-${user.userId || user.nickName || ''}`;
};
const formatAssigneeDisplayName = (user) => {
if (!user) return '';
const name = user.nickName || user.userName || user.name || '';
const identityName = user.identityName || '';
if (name && identityName) return `${name}${identityName}`;
return name || identityName || '未知人员';
};
const resolveAssigneeDeptUserType = () => (
formData.quickApproveRadio === 'yes' ? 2 : 3
);
const fetchAssigneeList = async () => {
const deptId = getCurrentDeptId();
if (!deptId) {
assigneeList.value = [];
return;
}
assigneeLoading.value = true;
try {
const res = await getDeptUsers(deptId, { type: resolveAssigneeDeptUserType() });
if (res.code === 0) {
assigneeList.value = res.data || [];
} else {
assigneeList.value = [];
}
} catch (error) {
console.error('获取部门人员失败:', error);
assigneeList.value = [];
} finally {
assigneeLoading.value = false;
}
};
const openAssigneePopup = async () => {
pickerAssigneeIdentityId.value = selectedAssigneeIdentityId.value;
pickerAssigneeName.value = selectedAssigneeName.value;
showAssigneePopup.value = true;
await fetchAssigneeList();
};
const onAssigneeItemClick = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (!identityId) return;
pickerAssigneeIdentityId.value = String(identityId);
pickerAssigneeName.value = formatAssigneeDisplayName(user);
};
const confirmAssigneeSelect = () => {
if (!pickerAssigneeIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return;
}
selectedAssigneeIdentityId.value = pickerAssigneeIdentityId.value;
selectedAssigneeName.value = pickerAssigneeName.value;
showAssigneePopup.value = false;
};
const cancelAssigneeSelect = () => {
showAssigneePopup.value = false;
};
const onResultChange = (result) => {
formData.result = result;
if (result === 2) {
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
} else if (!formData.quickApproveRadio) {
formData.quickApproveRadio = 'yes';
}
fetchNextStep();
};
const onQuickApproveChange = (value) => {
formData.quickApproveRadio = value;
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
fetchNextStep();
};
const fetchNextStep = async () => {
nextStepLoading.value = true;
try {
const currentTaskId = taskId.value;
if (!currentTaskId) {
nextStepName.value = '';
return;
}
const res = await getFlowNextNodes({
taskId: currentTaskId,
previewVariables: buildPreviewVariables(),
includeSubProcess: true
});
if (res.code === 0) {
const payload = res.data && typeof res.data === 'object' ? res.data : res;
nextStepName.value = resolveNextTaskName(payload);
} else {
nextStepName.value = '';
}
} catch (error) {
console.error('获取下一步流程失败:', error);
nextStepName.value = '';
} finally {
nextStepLoading.value = false;
}
};
// 整改记录数据
const rectifyData = reactive({
@@ -172,6 +439,7 @@
actualCost: null,
managerNames: [],
memberNames: [],
rectifierName: '',
rectifyStatusName: ''
});
@@ -204,7 +472,8 @@
// 表单数据
const formData = reactive({
result: 1, // 验收结果 1.通过 2.不通过
verifyRemark: '' // 验收备注
verifyRemark: '', // 验收备注
quickApproveRadio: 'yes' // 是否快速审批 yes/no仅通过时有效
});
const fileList1 = ref([]);
@@ -285,8 +554,11 @@
getPayload: () => ({
formData: {
result: formData.result,
verifyRemark: formData.verifyRemark
verifyRemark: formData.verifyRemark,
quickApproveRadio: formData.quickApproveRadio
},
selectedAssigneeIdentityId: selectedAssigneeIdentityId.value,
selectedAssigneeName: selectedAssigneeName.value,
fileList1: fileList1.value,
signatureServerPath: signatureServerPath.value,
signatureUrl: signatureUrl.value,
@@ -300,6 +572,9 @@
const form = data.formData || {};
formData.result = form.result !== undefined ? form.result : 1;
formData.verifyRemark = form.verifyRemark || '';
formData.quickApproveRadio = form.quickApproveRadio === 'no' ? 'no' : 'yes';
selectedAssigneeIdentityId.value = data.selectedAssigneeIdentityId || '';
selectedAssigneeName.value = data.selectedAssigneeName || '';
fileList1.value = data.fileList1 || [];
signaturePaths.value = data.signaturePaths || [];
if (data.signatureServerPath || data.signatureUrl) {
@@ -318,6 +593,9 @@
clearForm: () => {
formData.result = 1;
formData.verifyRemark = '';
formData.quickApproveRadio = 'yes';
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
fileList1.value = [];
signatureServerPath.value = '';
signatureUrl.value = '';
@@ -332,6 +610,7 @@
canSave: () => !!rectifyId.value,
requireInitialized: true,
onAfterRestore: (data) => {
fetchNextStep();
if (data.signatureServerPath || data.signatureUrl || data.signatureLocalPath) {
return;
}
@@ -348,6 +627,9 @@
bindAutoSave(() => [
formData.result,
formData.verifyRemark,
formData.quickApproveRadio,
selectedAssigneeIdentityId.value,
selectedAssigneeName.value,
fileList1.value,
signatureServerPath.value,
signatureUrl.value,
@@ -375,22 +657,26 @@
});
};
// 从 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;
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);
}
if (assignId.value) {
const byAssignId = assigns.find(
(item) => String(item.assignId) === String(assignId.value) && item.rectify
);
if (byAssignId) return byAssignId;
return rectify.managerNames || [];
};
const resolveMemberNames = (rectify) => {
if (Array.isArray(rectify.members) && rectify.members.length > 0) {
return mapPersonListToNickNames(rectify.members);
}
return assigns.find((item) => item.rectify) || null;
return rectify.memberNames || [];
};
const applyRectifyData = (rectify) => {
@@ -401,13 +687,41 @@
rectifyData.rectifyResult = rectify.rectifyResult || '';
rectifyData.planCost = rectify.planCost ?? null;
rectifyData.actualCost = rectify.actualCost ?? null;
rectifyData.managerNames = rectify.managerNames || [];
rectifyData.memberNames = rectify.memberNames || [];
rectifyData.rectifyStatusName = rectify.rectifyStatusName || '';
rectifyData.managerNames = resolveManagerNames(rectify);
rectifyData.memberNames = resolveMemberNames(rectify);
rectifyData.rectifierName = rectify.rectifierName || '';
rectifyData.rectifyStatusName = rectify.rectifyStatusName || rectify.statusName || '';
rectifyAttachments.value = rectify.attachments || [];
};
// 获取隐患详情
// 获取整改详情(有 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;
@@ -420,8 +734,15 @@
const assign = resolveAssignWithRectify(res.data.assigns);
if (assign?.rectify) {
applyRectifyData(assign.rectify);
console.log('整改记录:', rectifyData);
console.log('整改附件:', rectifyAttachments.value);
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' });
@@ -432,6 +753,27 @@
}
};
const loadPageData = async () => {
if (rectifyId.value) {
await fetchRectifyDetail();
} else if (hazardId.value) {
await fetchDetail();
}
await fetchNextStep();
};
const validateFormBeforeSubmit = () => {
if (formData.result === 1 && !formData.quickApproveRadio) {
uni.showToast({ title: '请选择是否快速审批', icon: 'none' });
return false;
}
if (formData.result === 1 && formData.quickApproveRadio === 'no' && !selectedAssigneeIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return false;
}
return true;
};
// 取消
const handleCancel = () => {
uni.navigateBack();
@@ -447,6 +789,10 @@
return;
}
if (!validateFormBeforeSubmit()) {
return;
}
// 电子签名验证与处理
if (showCanvas.value) {
if (!signatureRef.value || isSignatureEmpty.value) {
@@ -502,6 +848,13 @@
signPath: signatureServerPath.value || '' // 电子签名路径
// sendMsgFlag: sendMsgFlag.value
};
if (formData.result === 1) {
params.quickApprove = formData.quickApproveRadio === 'yes';
if (formData.quickApproveRadio === 'no') {
params.assigneeIdentityId = selectedAssigneeIdentityId.value;
}
}
console.log('提交验收参数:', params);
@@ -664,10 +1017,12 @@
if (options.assignId) {
assignId.value = options.assignId;
}
console.log('验收页面参数:', { rectifyId: rectifyId.value, hazardId: hazardId.value, assignId: assignId.value });
if (options.taskId) {
taskId.value = options.taskId;
}
console.log('验收页面参数:', { rectifyId: rectifyId.value, hazardId: hazardId.value, assignId: assignId.value, taskId: taskId.value });
// 获取隐患详情
fetchDetail();
loadPageData();
// 页面打开时自动恢复草稿
restoreDraft();
@@ -710,6 +1065,117 @@
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;
}
}
.user-popup {
background: #fff;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
color: #333;
}
.popup-close {
font-size: 40rpx;
color: #999;
line-height: 1;
}
}
.user-list-scroll {
max-height: 600rpx;
padding: 0 30rpx;
box-sizing: border-box;
}
.empty-tip {
padding: 80rpx 20rpx;
text-align: center;
color: #909399;
font-size: 26rpx;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&.active {
.user-item-text {
color: #2667E9;
font-weight: 600;
}
}
.user-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
.popup-footer {
display: flex;
gap: 24rpx;
padding: 24rpx 30rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
background: #fff;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
margin: 0;
padding: 0;
&::after {
border: none;
}
}
.btn-cancel {
background: #fff;
color: #2667E9;
border: 2rpx solid #2667E9;
}
.btn-confirm {
color: #fff;
border: none;
}
}
}
// 签名相关样式
.signature-box {
width: 100%;