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

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

@@ -0,0 +1,228 @@
<template>
<u-popup :show="show" mode="bottom" round="20" @close="handleCancel">
<view class="user-popup">
<view class="popup-header">
<view class="popup-title text-bold">选择下一步处理人</view>
<view class="popup-close" @click="handleCancel">×</view>
</view>
<view v-if="pickerIdentityId" class="selected-summary">
<text class="summary-label">已选</text>
<text class="summary-text">{{ pickerDisplayName }}</text>
</view>
<scroll-view class="user-list-scroll" scroll-y>
<view v-if="loading" class="empty-tip">加载中...</view>
<view v-else-if="!taskId" class="empty-tip">缺少任务ID无法加载处理人</view>
<view v-else-if="deptTree.length === 0" class="empty-tip">暂无人员数据</view>
<FlowAssigneeTree
v-else
:depts="deptTree"
:selected-identity-id="pickerIdentityId"
@select="handleUserSelect"
/>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button class="btn-confirm bg-blue" @click="handleConfirm">确定</button>
</view>
</view>
</u-popup>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { getFlowApproverCandidates } from '@/request/api.js';
import FlowAssigneeTree from './FlowAssigneeTree.vue';
import {
findAssigneeUserInDeptTree,
formatAssigneeDisplayName,
normalizeApproverDeptTree,
resolveAssigneeIdentityId
} from './flowAssigneeUtils.js';
const props = defineProps({
show: {
type: Boolean,
default: false
},
taskId: {
type: String,
default: ''
},
pickerIdentityId: {
type: String,
default: ''
},
pickerName: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:pickerIdentityId', 'update:pickerName', 'cancel', 'confirm']);
const loading = ref(false);
const deptTree = ref([]);
const localPickerIdentityId = ref('');
const localPickerName = ref('');
const pickerDisplayName = computed(() => {
const user = findAssigneeUserInDeptTree(deptTree.value, localPickerIdentityId.value);
return user ? formatAssigneeDisplayName(user) : localPickerName.value;
});
const syncLocalPicker = () => {
localPickerIdentityId.value = props.pickerIdentityId || '';
localPickerName.value = props.pickerName || '';
};
const fetchAssigneeTree = async () => {
if (!props.taskId) {
deptTree.value = [];
return;
}
loading.value = true;
try {
const res = await getFlowApproverCandidates(props.taskId);
if (res.code === 0) {
deptTree.value = normalizeApproverDeptTree(res.data);
} else {
deptTree.value = [];
uni.showToast({ title: res.msg || '获取处理人失败', icon: 'none' });
}
} catch (error) {
console.error('获取处理人失败:', error);
deptTree.value = [];
uni.showToast({ title: '获取处理人失败', icon: 'none' });
} finally {
loading.value = false;
}
};
watch(
() => props.show,
async (visible) => {
if (!visible) return;
syncLocalPicker();
await fetchAssigneeTree();
}
);
const handleUserSelect = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (!identityId) return;
localPickerIdentityId.value = identityId;
localPickerName.value = formatAssigneeDisplayName(user);
emit('update:pickerIdentityId', identityId);
emit('update:pickerName', localPickerName.value);
};
const handleCancel = () => {
emit('cancel');
};
const handleConfirm = () => {
if (!localPickerIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return;
}
const user = findAssigneeUserInDeptTree(deptTree.value, localPickerIdentityId.value);
if (!user) {
uni.showToast({ title: '所选身份无效,请重新选择', icon: 'none' });
return;
}
const name = formatAssigneeDisplayName(user);
emit('update:pickerIdentityId', localPickerIdentityId.value);
emit('update:pickerName', name);
emit('confirm', {
identityId: localPickerIdentityId.value,
name,
user
});
};
</script>
<style lang="scss" scoped>
.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;
}
}
.selected-summary {
padding: 16rpx 30rpx;
background: #f5f7fa;
border-bottom: 1rpx solid #eee;
font-size: 24rpx;
line-height: 1.5;
.summary-label {
color: #909399;
}
.summary-text {
color: #333;
}
}
.user-list-scroll {
max-height: 600rpx;
box-sizing: border-box;
}
.empty-tip {
padding: 80rpx 20rpx;
text-align: center;
color: #909399;
font-size: 26rpx;
}
.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;
}
}
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<view class="assignee-tree">
<template v-for="row in flatRows" :key="row.key">
<view
v-if="row.type === 'dept'"
class="dept-node"
:style="{ paddingLeft: `${row.level * 24 + 20}rpx` }"
>
<text class="dept-name">{{ row.deptName }}</text>
</view>
<view
v-else
class="user-item"
:class="{ active: String(selectedIdentityId) === String(resolveAssigneeIdentityId(row.user)) }"
:style="{ paddingLeft: `${row.level * 24 + 20}rpx` }"
@click="handleSelect(row.user)"
>
<text class="user-item-text">{{ formatAssigneeDisplayName(row.user) }}</text>
<text
v-if="String(selectedIdentityId) === String(resolveAssigneeIdentityId(row.user))"
class="cuIcon-check text-blue"
></text>
</view>
</template>
</view>
</template>
<script setup>
import { computed } from 'vue';
import {
flattenApproverDeptTree,
formatAssigneeDisplayName,
resolveAssigneeIdentityId
} from './flowAssigneeUtils.js';
const props = defineProps({
depts: {
type: Array,
default: () => []
},
selectedIdentityId: {
type: String,
default: ''
}
});
const emit = defineEmits(['select']);
const flatRows = computed(() => flattenApproverDeptTree(props.depts));
const handleSelect = (user) => {
emit('select', user);
};
</script>
<style lang="scss" scoped>
.dept-node {
padding: 20rpx 20rpx 12rpx;
font-size: 26rpx;
color: #909399;
line-height: 1.4;
}
.dept-name {
font-weight: 600;
}
.user-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
&.active {
.user-item-text {
color: #2667E9;
font-weight: 600;
}
}
.user-item-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
</style>

View File

@@ -0,0 +1,71 @@
export const resolveAssigneeIdentityId = (user) => {
if (!user) return '';
const id = user.identityId ?? user.userIdentityId ?? user.userId ?? '';
return id === '' || id == null ? '' : String(id);
};
export const getAssigneeItemKey = (user) => {
const identityId = resolveAssigneeIdentityId(user);
if (identityId) return `identity-${identityId}`;
return `user-${user.userId || user.nickName || ''}`;
};
export const formatAssigneeDisplayName = (user) => {
if (!user) return '';
if (user.identityName) {
return `${user.nickName || user.userName || ''}_${user.identityName}`;
}
if (user.postName) {
return `${user.nickName || user.userName || ''}_${user.postName}`;
}
return user.nickName || user.userName || user.name || '未知人员';
};
export const normalizeApproverDeptTree = (data) => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
return [];
};
export const findAssigneeUserInDeptTree = (depts, identityId) => {
if (!identityId || !Array.isArray(depts)) return null;
for (const dept of depts) {
const user = (dept.users || []).find(
(item) => String(resolveAssigneeIdentityId(item)) === String(identityId)
);
if (user) return user;
if (dept.children?.length) {
const found = findAssigneeUserInDeptTree(dept.children, identityId);
if (found) return found;
}
}
return null;
};
/** 将部门树拍平为可渲染行(部门标题 + 人员),避免小程序递归组件不展示子部门 */
export const flattenApproverDeptTree = (depts, level = 0) => {
const rows = [];
if (!Array.isArray(depts)) return rows;
for (const dept of depts) {
rows.push({
type: 'dept',
key: `dept-${dept.deptId ?? dept.deptName ?? level}`,
deptName: dept.deptName || '',
level
});
for (const user of dept.users || []) {
rows.push({
type: 'user',
key: getAssigneeItemKey(user),
user,
level: level + 1
});
}
if (dept.children?.length) {
rows.push(...flattenApproverDeptTree(dept.children, level + 1));
}
}
return rows;
};

View File

@@ -173,7 +173,7 @@
<view class="text-gray">下一步处理人</view> <view class="text-gray">下一步处理人</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="static-field">部门企业管理员企业成</view> <view class="static-field">管理执行人</view>
</scroll-view> </scroll-view>
<view :class="pageMode ? 'page-footer' : 'popup-footer'"> <view :class="pageMode ? 'page-footer' : 'popup-footer'">
<button class="btn-cancel" @click="handleClose">取消</button> <button class="btn-cancel" @click="handleClose">取消</button>

View File

@@ -62,6 +62,10 @@
class="node-section" class="node-section"
:class="{ 'node-section--last': index === historyList.length - 1 }" :class="{ 'node-section--last': index === historyList.length - 1 }"
> >
<view
class="detail-card-shell"
:class="{ 'detail-card-shell--active': activeIndex === index }"
>
<view class="detail-card"> <view class="detail-card">
<view class="card-header"> <view class="card-header">
<!-- <text class="card-node-name">{{ item.nodeName }}</text> --> <!-- <text class="card-node-name">{{ item.nodeName }}</text> -->
@@ -320,6 +324,7 @@
</block> </block>
</view> </view>
</view> </view>
</view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -624,6 +629,23 @@ const previewImages = (attachments, index) => {
margin-bottom: 0; margin-bottom: 0;
} }
.detail-card-shell {
border-radius: 22rpx;
padding: 2rpx;
background: transparent;
transition: background 0.3s ease, box-shadow 0.3s ease;
}
.detail-card-shell--active {
background: linear-gradient(
145deg,
rgba(4, 108, 234, 0.48) 0%,
rgba(38, 103, 233, 0.3) 50%,
rgba(33, 88, 200, 0.16) 100%
);
box-shadow: 0 8rpx 24rpx rgba(38, 103, 233, 0.12);
}
.detail-card { .detail-card {
background: #fff; background: #fff;
border-radius: 16rpx; border-radius: 16rpx;

View File

@@ -62,6 +62,10 @@
class="node-section" class="node-section"
:class="{ 'node-section--last': index === historyList.length - 1 }" :class="{ 'node-section--last': index === historyList.length - 1 }"
> >
<view
class="detail-card-shell"
:class="{ 'detail-card-shell--active': activeIndex === index }"
>
<view class="detail-card"> <view class="detail-card">
<view class="card-header-v2"> <view class="card-header-v2">
<view class="card-header-main"> <view class="card-header-main">
@@ -328,6 +332,7 @@
</block> </block>
</view> </view>
</view> </view>
</view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -552,6 +557,23 @@ const previewImages = (attachments, index) => {
margin-bottom: 0; margin-bottom: 0;
} }
.detail-card-shell {
border-radius: 22rpx;
padding: 2rpx;
background: transparent;
transition: background 0.3s ease, box-shadow 0.3s ease;
}
.detail-card-shell--active {
background: linear-gradient(
145deg,
rgba(4, 108, 234, 0.48) 0%,
rgba(38, 103, 233, 0.3) 50%,
rgba(33, 88, 200, 0.16) 100%
);
box-shadow: 0 8rpx 24rpx rgba(38, 103, 233, 0.12);
}
.detail-card { .detail-card {
background: #fff; background: #fff;
border-radius: 20rpx; border-radius: 20rpx;

View File

@@ -0,0 +1,787 @@
<template>
<view class="hazard-process-chain-panel">
<view v-if="loading" class="loading-wrap">
<text class="loading-text">{{ labels.loading }}</text>
</view>
<view v-else-if="historyList.length === 0" class="empty-wrap">
<text class="empty-text">{{ labels.empty }}</text>
</view>
<view v-else class="detail-body" :style="detailBodyStyle">
<scroll-view
class="steps-card"
scroll-y
:style="scrollAreaStyle"
:show-scrollbar="false"
:scroll-into-view="stepScrollIntoView"
:scroll-with-animation="true"
>
<view
v-for="(item, index) in historyList"
:key="'step-' + index"
:id="'process-step-' + index"
class="step-item"
:class="{ 'step-item--active': activeIndex === index }"
@tap="scrollToNode(index)"
>
<view class="step-track">
<view
class="step-dot"
:class="{ 'step-dot--active': activeIndex === index }"
>
<image
class="step-icon"
:src="getProcessStepIconPath(item, activeIndex === index)"
mode="aspectFit"
/>
</view>
<text
class="step-label"
:class="{ 'step-label--active': activeIndex === index }"
>{{ item.nodeName }}</text>
<view v-if="index < historyList.length - 1" class="step-line"></view>
</view>
</view>
</scroll-view>
<scroll-view
class="content-column content-scroll"
scroll-y
:style="scrollAreaStyle"
:show-scrollbar="false"
:scroll-into-view="contentScrollIntoView"
:scroll-with-animation="scrollWithAnimation"
@scroll="onContentScroll"
@scrolltolower="onScrollToLower"
>
<view
v-for="(item, index) in historyList"
:key="'node-' + index"
:id="'process-node-' + index"
class="node-section"
:class="{ 'node-section--last': index === historyList.length - 1 }"
>
<view
class="detail-card-shell"
:class="{ 'detail-card-shell--active': activeIndex === index }"
>
<view class="detail-card">
<view class="card-header-v2">
<view class="card-header-main">
<text class="operator">{{ item.titlePrefix }}{{ labels.personnelSuffix }}{{ fieldText(item, item.operator) }}</text>
<text class="time">{{ fieldText(item, item.time) }}</text>
</view>
<view
v-if="item.type === 'add' && item.content.levelName"
:class="['level-badge', getLevelTagClass(item.content)]"
>
{{ item.content.levelName }}
</view>
</view>
<view class="card-divider"></view>
<view class="card-body">
<block v-if="item.type === 'add'">
<view class="detail-row-v2">
<text class="label">{{ labels.hazardCode }}</text>
<text class="value">{{ fieldText(item, item.content.code) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.hazardTitle }}</text>
<text class="value">{{ fieldText(item, item.content.title) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.checkSource }}</text>
<text class="value">{{ fieldText(item, item.content.source) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.hazardSource }}</text>
<text class="value">{{ fieldText(item, item.content.hazardSourceName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.hazardArea }}</text>
<text class="value">{{ fieldText(item, item.content.areaName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.address }}</text>
<text class="value">{{ fieldText(item, item.content.address) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.hazardLevel }}</text>
<view class="value">
<view :class="['level-tag', getLevelTagClass(item.content)]">
{{ fieldText(item, item.content.levelName) }}
</view>
</view>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.hazardTag }}</text>
<text class="value">{{ fieldText(item, item.content.tagName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.description }}</text>
<text class="value">{{ fieldText(item, item.content.description) }}</text>
</view>
<view
v-if="(item.content.attachments && item.content.attachments.length) || !item.completed"
class="detail-row-v2 detail-row-v2--block"
>
<text class="label">{{ labels.hazardAttachments }}</text>
<view v-if="item.content.attachments && item.content.attachments.length" class="attachment-list">
<image
v-for="(file, idx) in item.content.attachments"
:key="idx"
class="attachment-img"
:src="resolveFileUrl(file.filePath)"
mode="aspectFill"
@tap="previewImages(item.content.attachments, idx)"
@load="scheduleMeasureLayout"
/>
</view>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.legalBasis }}</text>
<text class="value">{{ fieldText(item, item.content.legalBasis) }}</text>
</view>
</block>
<block v-else-if="item.type === 'assign'">
<view class="detail-row-v2">
<text class="label">{{ labels.assigneeName }}</text>
<text class="value">{{ fieldText(item, item.content.assigneeName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.assignDeadline }}</text>
<text class="value">{{ fieldText(item, item.content.deadline) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.assignStatus }}</text>
<text class="value">{{ fieldText(item, item.content.assignStatusName) }}</text>
</view>
</block>
<block v-else-if="item.type === 'rectify'">
<view class="detail-row-v2">
<text class="label">{{ labels.rectifyStatus }}</text>
<text class="value">{{ fieldText(item, item.content.rectifyStatusName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.rectifyPlan }}</text>
<text class="value">{{ fieldText(item, item.content.rectifyPlan) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.rectifyResult }}</text>
<text class="value">{{ fieldText(item, item.content.rectifyResult) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.rectifyMeasures }}</text>
<text class="value">{{ fieldText(item, item.content.rectificationMeasures) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.controlMeasures }}</text>
<text class="value">{{ fieldText(item, item.content.controlMeasures) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.rectifierName }}</text>
<text class="value">{{ fieldText(item, item.content.rectifierName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.managerNames }}</text>
<text class="value">{{ fieldText(item, item.content.managerNames) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.memberNames }}</text>
<text class="value">{{ fieldText(item, item.content.memberNames) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.planCost }}</text>
<text class="value">{{ formatCost(item.content.planCost, item.completed) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.actualCost }}</text>
<text class="value">{{ formatCost(item.content.actualCost, item.completed) }}</text>
</view>
<view
v-if="(item.content.attachments && item.content.attachments.length) || !item.completed"
class="detail-row-v2 detail-row-v2--block"
>
<text class="label">{{ labels.rectifyAttachments }}</text>
<view v-if="item.content.attachments && item.content.attachments.length" class="attachment-list">
<template v-for="(file, idx) in item.content.attachments" :key="idx">
<image
v-if="file.fileType === 'image' || !file.fileType"
class="attachment-img"
:src="resolveFileUrl(file.filePath)"
mode="aspectFill"
@tap="previewImages(item.content.attachments, idx)"
@load="scheduleMeasureLayout"
/>
<text v-else class="file-link">{{ file.fileName || labels.attachmentFallback }}</text>
</template>
</view>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
<view v-if="item.content.signPath || !item.completed" class="detail-row-v2 detail-row-v2--block">
<text class="label">{{ labels.rectifySign }}</text>
<image
v-if="item.content.signPath"
class="sign-img"
:src="resolveFileUrl(item.content.signPath)"
mode="aspectFit"
@tap="previewSingle(item.content.signPath)"
@load="scheduleMeasureLayout"
/>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
</block>
<block v-else-if="item.type === 'verify' || item.type === 'writeoff_approve'">
<view class="detail-row-v2">
<text class="label">{{ labels.verifyResult }}</text>
<text
v-if="item.completed && item.content.resultName"
class="result-tag"
:class="item.content.resultName === labels.pass ? 'result-tag--pass' : 'result-tag--fail'"
>
{{ item.content.resultName }}
</text>
<text v-else class="value value--pending">{{ fieldText(item, item.content.resultName) }}</text>
</view>
<view v-if="item.content.remark || !item.completed" class="detail-row-v2">
<text class="label">{{ labels.verifyRemark }}</text>
<text class="value">{{ fieldText(item, item.content.remark) }}</text>
</view>
<view
v-if="(item.content.attachments && item.content.attachments.length) || !item.completed"
class="detail-row-v2 detail-row-v2--block"
>
<text class="label">{{ labels.verifyAttachments }}</text>
<view v-if="item.content.attachments && item.content.attachments.length" class="attachment-list">
<template v-for="(file, idx) in item.content.attachments" :key="idx">
<image
v-if="file.fileType === 'image' || !file.fileType"
class="attachment-img"
:src="resolveFileUrl(file.filePath)"
mode="aspectFill"
@tap="previewImages(item.content.attachments, idx)"
@load="scheduleMeasureLayout"
/>
<text v-else class="file-link">{{ file.fileName || labels.attachmentFallback }}</text>
</template>
</view>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
<view v-if="item.content.signPath || !item.completed" class="detail-row-v2 detail-row-v2--block">
<text class="label">{{ labels.verifySign }}</text>
<image
v-if="item.content.signPath"
class="sign-img"
:src="resolveFileUrl(item.content.signPath)"
mode="aspectFit"
@tap="previewSingle(item.content.signPath)"
@load="scheduleMeasureLayout"
/>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
</block>
<block v-else-if="item.type === 'writeoff_apply'">
<view class="detail-row-v2">
<text class="label">{{ labels.writeoffDeadline }}</text>
<text class="value">{{ fieldText(item, item.content.rectifyDeadline) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.responsibleDept }}</text>
<text class="value">{{ fieldText(item, item.content.responsibleDeptName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.responsiblePerson }}</text>
<text class="value">{{ fieldText(item, item.content.responsiblePerson) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.mainTreatment }}</text>
<text class="value">{{ fieldText(item, item.content.mainTreatmentContent) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.treatmentResult }}</text>
<text class="value">{{ fieldText(item, item.content.treatmentResult) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.selfVerify }}</text>
<text class="value">{{ fieldText(item, item.content.selfVerifyContent) }}</text>
</view>
<view v-if="item.content.signPath || !item.completed" class="detail-row-v2 detail-row-v2--block">
<text class="label">{{ labels.applySign }}</text>
<image
v-if="item.content.signPath"
class="sign-img"
:src="resolveFileUrl(item.content.signPath)"
mode="aspectFit"
@tap="previewSingle(item.content.signPath)"
@load="scheduleMeasureLayout"
/>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
</block>
<block v-else-if="item.type === 'approval'">
<view class="detail-row-v2">
<text class="label">{{ labels.approvalOpinion }}</text>
<text
v-if="item.completed && item.content.approveTypeName"
class="result-tag"
:class="item.content.pass ? 'result-tag--pass' : 'result-tag--fail'"
>
{{ item.content.approveTypeName }}
</text>
<text v-else class="value value--pending">{{ fieldText(item, item.content.approveTypeName) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.approvalComment }}</text>
<text class="value" :class="{ 'value--pending': isPendingText(item, item.content.comment) }">{{ fieldText(item, item.content.comment) }}</text>
</view>
<view class="detail-row-v2">
<text class="label">{{ labels.smsReminder }}</text>
<text class="value">
<template v-if="item.content.sendMsgFlag != null">{{ item.content.sendMsgFlag ? labels.yes : labels.no }}</template>
<text v-else class="value--pending">{{ fieldText(item, '') }}</text>
</text>
</view>
<view v-if="item.content.signPath || !item.completed" class="detail-row-v2 detail-row-v2--block">
<text class="label">{{ labels.approvalSign }}</text>
<image
v-if="item.content.signPath"
class="sign-img"
:src="resolveFileUrl(item.content.signPath)"
mode="aspectFit"
@tap="previewSingle(item.content.signPath)"
@load="scheduleMeasureLayout"
/>
<text v-else class="value value--pending">{{ fieldText(item, '') }}</text>
</view>
</block>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { computed, toRefs, watch } from 'vue';
import { toImageUrl } from '@/request/request.js';
import { getProcessStepIconPath, PENDING_LABEL, resolveFieldDisplay } from './processChain.js';
import { CHAIN_LABELS, LEVEL_NAME_CLASS_MAP } from './processChainLabels.js';
import { useProcessChainScroll } from './useProcessChainScroll.js';
const labels = CHAIN_LABELS;
const props = defineProps({
chainData: {
type: Object,
default: () => ({})
},
loading: {
type: Boolean,
default: false
},
bodyHeight: {
type: Number,
default: 0
}
});
const { chainData, loading } = toRefs(props);
const {
historyList,
activeIndex,
contentScrollIntoView,
stepScrollIntoView,
scrollWithAnimation,
onContentScroll,
onScrollToLower,
scrollToNode,
scheduleMeasureLayout
} = useProcessChainScroll(chainData, loading);
const scrollAreaStyle = computed(() => {
const height = props.bodyHeight > 0 ? props.bodyHeight : 400;
return { height: `${height}px` };
});
const detailBodyStyle = computed(() => {
const height = props.bodyHeight > 0 ? props.bodyHeight : 400;
return { height: `${height}px` };
});
watch(
() => props.bodyHeight,
(height) => {
if (height > 0) {
scheduleMeasureLayout();
setTimeout(scheduleMeasureLayout, 100);
}
}
);
const LEVEL_CLASS_MAP = {
2: 'level-normal',
3: 'level-major'
};
const getLevelTagClass = (content) => {
const level = content?.level;
const levelName = content?.levelName;
if (level != null && LEVEL_CLASS_MAP[level]) {
return LEVEL_CLASS_MAP[level];
}
if (levelName && LEVEL_NAME_CLASS_MAP[levelName]) {
return LEVEL_NAME_CLASS_MAP[levelName];
}
return '';
};
const resolveFileUrl = (path) => toImageUrl(path);
const fieldText = (item, value) => resolveFieldDisplay(value, item.completed);
const isPendingText = (item, value) => fieldText(item, value) === PENDING_LABEL;
const formatCost = (cost, completed = true) => {
if (cost == null || cost === '') return resolveFieldDisplay('', completed);
const num = Number(cost);
if (Number.isNaN(num)) return resolveFieldDisplay('', completed);
return `${num} ${labels.yuan}`;
};
const previewSingle = (path) => {
const url = resolveFileUrl(path);
if (!url) return;
uni.previewImage({ current: url, urls: [url] });
};
const previewImages = (attachments, index) => {
const urls = (attachments || [])
.filter((f) => f.fileType === 'image' || !f.fileType)
.map((f) => resolveFileUrl(f.filePath))
.filter(Boolean);
if (!urls.length) return;
uni.previewImage({ current: urls[index] || urls[0], urls });
};
</script>
<style lang="scss" scoped>
.hazard-process-chain-panel {
flex: 1;
min-height: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.loading-wrap,
.empty-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #909399;
}
.detail-body {
display: flex;
gap: 20rpx;
align-items: stretch;
overflow: hidden;
box-sizing: border-box;
}
.steps-card {
width: 148rpx;
flex-shrink: 0;
background: #fff;
border-radius: 20rpx;
box-sizing: border-box;
}
.step-item {
padding: 0 8rpx;
}
.step-track {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 0 0;
}
.step-dot {
width: 66rpx;
height: 66rpx;
border-radius: 50%;
background: #f3f3f3;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.step-dot--active {
background: #2667e9;
}
.step-icon {
width: 36rpx;
height: 36rpx;
}
.step-label {
margin-top: 10rpx;
font-size: 20rpx;
color: #999;
line-height: 1.3;
text-align: center;
word-break: break-all;
}
.step-label--active {
color: #2667e9;
font-weight: 600;
}
.step-line {
width: 0;
height: 36rpx;
margin: 8rpx 0;
border-left: 2rpx dashed #dcdfe6;
}
.content-column {
flex: 1;
width: 0;
min-height: 0;
box-sizing: border-box;
}
.content-scroll {
width: 100%;
box-sizing: border-box;
}
.node-section {
box-sizing: border-box;
padding: 0;
margin-bottom: 24rpx;
}
.node-section--last {
padding-bottom: 40rpx;
margin-bottom: 0;
}
.detail-card-shell {
border-radius: 22rpx;
padding: 2rpx;
background: transparent;
transition: background 0.3s ease, box-shadow 0.3s ease;
}
.detail-card-shell--active {
background: linear-gradient(
145deg,
rgba(4, 108, 234, 0.48) 0%,
rgba(38, 103, 233, 0.3) 50%,
rgba(33, 88, 200, 0.16) 100%
);
box-shadow: 0 8rpx 24rpx rgba(38, 103, 233, 0.12);
}
.detail-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
padding: 28rpx 38rpx;
box-sizing: border-box;
}
.card-header-v2 {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
padding: 0;
}
.card-header-main {
flex: 1;
min-width: 0;
}
.operator {
font-size: 28rpx;
font-weight: 600;
color: #303133;
line-height: 1.5;
word-break: break-all;
}
.time {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #999;
line-height: 1.4;
}
.level-badge {
flex-shrink: 0;
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
white-space: nowrap;
}
.level-badge.level-normal {
background: #fff7e6;
border: 2rpx solid #ffd591;
color: #fa8c16;
}
.level-badge.level-major {
background: #fff1f0;
border: 2rpx solid #ffa39e;
color: #f5222d;
}
.card-divider {
height: 0;
margin: 20rpx 0;
border-top: 2rpx dashed #eee;
}
.card-body {
padding: 0;
}
.detail-row-v2 {
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 24rpx;
padding: 18rpx 0;
}
.detail-row-v2--block {
flex-wrap: wrap;
}
.label {
flex-shrink: 0;
font-size: 26rpx;
color: #999;
line-height: 1.5;
}
.value {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #333;
line-height: 1.6;
text-align: left;
word-break: break-all;
}
.value--pending {
color: #e6a23c;
}
.level-tag {
display: inline-flex;
align-items: center;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
}
.level-normal {
background: #fff7e6;
border: 2rpx solid #ffd591;
color: #fa8c16;
}
.level-major {
background: #fff1f0;
border: 2rpx solid #ffa39e;
color: #f5222d;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
flex: 1;
min-width: 0;
}
.attachment-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f5f7fa;
}
.sign-img {
width: 300rpx;
height: 160rpx;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
background: #fafafa;
flex: 1;
min-width: 0;
max-width: 100%;
}
.file-link {
display: inline-block;
padding: 12rpx 20rpx;
background: #f5f7fa;
border: 1rpx solid #e4e7ed;
border-radius: 8rpx;
color: #2667e9;
font-size: 24rpx;
}
.result-tag {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
}
.result-tag--pass {
background: #f0f9eb;
color: #67c23a;
}
.result-tag--fail {
background: #fef0f0;
color: #f56c6c;
}
</style>

View File

@@ -0,0 +1,383 @@
import { formatNameList } from './hazardDetail.js';
export const PENDING_LABEL = '待处理';
/** completed 未传时视为已完成(兼容旧数据) */
export const isNodeCompleted = (node) => node?.completed !== false;
/** 空值:已完成显示 -,未完成显示待处理 */
export const resolveFieldDisplay = (value, completed = true) => {
if (value != null && value !== '') return value;
return completed ? '-' : PENDING_LABEL;
};
export const PROCESS_NODE_TYPES = {
ADD: 'add',
ASSIGN: 'assign',
RECTIFY: 'rectify',
RECTIFY_ASSIGN: 'rectifyAssign',
VERIFY: 'verify',
VERIFY_SUB: 'verify_sub',
WRITEOFF_APPLY: 'writeoff_apply',
WRITEOFF_APPROVE: 'writeoff_approve',
WRITEOFF_SUB: 'writeoff_sub',
APPROVAL: 'approval'
};
const SUB_TYPE_SUFFIX_MAP = {
verify: '验收',
writeoff: '销号'
};
const TITLE_PREFIX_MAP = {
add: '提交',
assign: '交办',
rectify: '整改',
rectifyAssign: '交办',
verify: '验收',
verify_sub: '审批',
writeoff_apply: '销号申请',
writeoff_approve: '销号审核',
writeoff_sub: '审批'
};
const APPROVAL_TASK_KEY_ICON = {
department_review: 'bumenshenpi',
section_chief_review: 'fenguanshenpi',
supervising_executive_review: 'fenguanshenpi',
supervising_leader_review_1: 'zhuguanshenpi',
supervising_leader_review_2: 'zhuguanshenpi'
};
const NODE_TYPE_ICON = {
add: 'tijiao',
assign: 'jiaoban',
rectifyAssign: 'jiaoban',
rectify: 'zhenggai',
verify: 'yanshou',
writeoff_apply: 'xiaohao',
writeoff_approve: 'xiaohao'
};
const resolveTaskKey = (node) => {
return node?.taskKey || node?.subProcessApprovalInfo?.taskKey || '';
};
/** null/undefined 等非对象值统一转为 {},避免默认参数对 null 不生效 */
const toSafeObject = (value) => (value && typeof value === 'object' ? value : {});
/** null/undefined/空字符串展示为空 */
const toDisplayText = (value) => {
if (value == null || value === '') return '';
return value;
};
export const getProcessStepDisplayName = (node) => {
if (!node) return '';
const nodeType = node.nodeType;
if (nodeType === 'verify_sub' || nodeType === 'writeoff_sub') {
const subType = node.subProcessApprovalInfo?.subType;
const suffix = SUB_TYPE_SUFFIX_MAP[subType];
return suffix ? `${node.nodeName}${suffix}` : node.nodeName;
}
return node.nodeName || '';
};
export const getProcessStepIconPath = (item, active = false) => {
const type = item?.type;
const taskKey = item?.taskKey || '';
const state = active ? 'selected' : 'unselected';
if (type === PROCESS_NODE_TYPES.APPROVAL) {
const iconBase = APPROVAL_TASK_KEY_ICON[taskKey] || 'bumenshenpi';
return `/static/yinhuan_detail/${iconBase}_${state}.png`;
}
const base = NODE_TYPE_ICON[item?.rawType] || NODE_TYPE_ICON[type] || 'tijiao';
if (base === 'zhenggai') {
return active
? '/static/yinhuan_detail/zhenggai_selected.png'
: '/static/yinhuan_detail/zhenggai__unselected.png';
}
return `/static/yinhuan_detail/${base}__${state}.png`;
};
const resolveOperator = (node, nodeType) => {
const completed = isNodeCompleted(node);
const fallback = completed ? '-' : '';
if (nodeType === PROCESS_NODE_TYPES.ADD) {
return node.hazardInfo?.reporterName || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.ASSIGN) {
return node.assignInfo?.assignerName || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.RECTIFY) {
return node.rectifyInfo?.rectifierName || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.VERIFY || nodeType === PROCESS_NODE_TYPES.WRITEOFF_APPROVE) {
return node.verifyInfo?.verifierName || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.WRITEOFF_APPLY) {
return node.writeOffApplyInfo?.applicantName || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.APPROVAL) {
const info = node.subProcessApprovalInfo || {};
return info.operatorName || info.assigneeName || fallback;
}
return fallback;
};
const resolveTime = (node, nodeType) => {
const completed = isNodeCompleted(node);
const fallback = completed ? '-' : '';
if (nodeType === PROCESS_NODE_TYPES.ADD) {
return node.hazardInfo?.createdAt || node.completedAt || node.occurredAt || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.ASSIGN) {
return node.assignInfo?.assignTime || node.completedAt || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.RECTIFY) {
return node.rectifyInfo?.rectifyTime || node.completedAt || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.VERIFY || nodeType === PROCESS_NODE_TYPES.WRITEOFF_APPROVE) {
return node.verifyInfo?.verifyTime || node.completedAt || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.WRITEOFF_APPLY) {
return node.writeOffApplyInfo?.applyTime || node.completedAt || fallback;
}
if (nodeType === PROCESS_NODE_TYPES.APPROVAL) {
return node.subProcessApprovalInfo?.endTime || node.completedAt || fallback;
}
return node.completedAt || node.occurredAt || fallback;
};
const mapAddContent = (info) => {
const data = toSafeObject(info);
return {
code: toDisplayText(data.code),
title: toDisplayText(data.title),
source: toDisplayText(data.source),
hazardSourceName: toDisplayText(data.hazardSourceName),
areaName: toDisplayText(data.areaName),
address: toDisplayText(data.address),
level: data.level ?? null,
levelName: toDisplayText(data.levelName),
tagName: toDisplayText(data.tagName),
description: toDisplayText(data.description),
attachments: Array.isArray(data.attachments) ? data.attachments : [],
legalBasis: toDisplayText(data.legalBasis)
};
};
const mapAssignContent = (info) => {
const data = toSafeObject(info);
return {
assigneeName: toDisplayText(data.assigneeName),
deadline: toDisplayText(data.deadline),
assignRemark: toDisplayText(data.assignRemark),
assignStatusName: toDisplayText(data.assignStatusName)
};
};
const mapRectifyContent = (info) => {
const data = toSafeObject(info);
return {
rectifyStatusName: toDisplayText(data.rectifyStatusName),
rectifyPlan: toDisplayText(data.rectifyPlan),
rectifyResult: toDisplayText(data.rectifyResult),
rectificationMeasures: toDisplayText(data.rectificationMeasures),
controlMeasures: toDisplayText(data.controlMeasures),
rectifierName: toDisplayText(data.rectifierName),
managerNames: formatNameList(data.managerNames, ''),
memberNames: formatNameList(data.memberNames, ''),
planCost: data.planCost ?? null,
actualCost: data.actualCost ?? null,
attachments: Array.isArray(data.attachments) ? data.attachments : [],
signPath: toDisplayText(data.signPath)
};
};
const mapVerifyContent = (info) => {
const data = toSafeObject(info);
return {
resultName: toDisplayText(data.resultName),
remark: toDisplayText(data.remark),
attachments: Array.isArray(data.attachments) ? data.attachments : [],
signPath: toDisplayText(data.signPath)
};
};
const mapApprovalContent = (info) => {
const data = toSafeObject(info);
return {
approveTypeName: toDisplayText(data.approveTypeName),
pass: data.pass ?? null,
comment: toDisplayText(data.comment),
nextStepName: toDisplayText(data.nextStepName),
nextAssigneeName: toDisplayText(data.nextAssigneeName),
sendMsgFlag: data.sendMsgFlag ?? null,
signPath: toDisplayText(data.signPath)
};
};
const mapWriteoffApplyContent = (info) => {
const data = toSafeObject(info);
return {
rectifyDeadline: toDisplayText(data.rectifyDeadline),
responsibleDeptName: toDisplayText(data.responsibleDeptName),
responsiblePerson: toDisplayText(data.responsiblePerson),
mainTreatmentContent: toDisplayText(data.mainTreatmentContent),
treatmentResult: toDisplayText(data.treatmentResult),
selfVerifyContent: toDisplayText(data.selfVerifyContent),
signPath: toDisplayText(data.signPath)
};
};
const resolveNodeType = (node) => {
const nodeType = node?.nodeType;
if (nodeType === 'verify_sub' || nodeType === 'writeoff_sub') {
return PROCESS_NODE_TYPES.APPROVAL;
}
return nodeType || PROCESS_NODE_TYPES.ADD;
};
const hasRectifyData = (rectifyInfo) => {
if (!rectifyInfo || typeof rectifyInfo !== 'object') {
return false;
}
return Boolean(
rectifyInfo.rectifyId
|| rectifyInfo.rectifyPlan
|| rectifyInfo.rectifyResult
|| rectifyInfo.rectifyTime
|| rectifyInfo.rectifierName
);
};
const hasAssignData = (assignInfo) => {
if (!assignInfo || typeof assignInfo !== 'object') {
return false;
}
return Boolean(
assignInfo.assignId
|| assignInfo.assigneeName
|| assignInfo.assignerName
|| assignInfo.assignTime
);
};
/** 交办 / 整改转派节点 */
const isAssignLikeNodeType = (nodeType) => (
nodeType === PROCESS_NODE_TYPES.ASSIGN
|| nodeType === PROCESS_NODE_TYPES.RECTIFY_ASSIGN
);
/** 交办/整改节点:有整改数据展示整改,否则有交办数据展示交办 */
export const resolveAssignRectifyDisplayType = (node) => {
if (hasRectifyData(node?.rectifyInfo)) {
return PROCESS_NODE_TYPES.RECTIFY;
}
if (hasAssignData(node?.assignInfo)) {
return PROCESS_NODE_TYPES.ASSIGN;
}
return null;
};
const resolveDisplayType = (node, nodeType) => {
// assign隐患交办 → assignInfo
// rectifyAssign整改转派 → assignInfo若同节点附带整改数据则展示整改
if (isAssignLikeNodeType(nodeType)) {
return resolveAssignRectifyDisplayType(node) || PROCESS_NODE_TYPES.ASSIGN;
}
// rectify隐患整改 → rectifyInfo未整改前可能只有转派信息
if (nodeType === PROCESS_NODE_TYPES.RECTIFY) {
return resolveAssignRectifyDisplayType(node) || PROCESS_NODE_TYPES.RECTIFY;
}
return nodeType;
};
const resolveTitlePrefix = (rawType, displayType, node) => {
if (displayType === PROCESS_NODE_TYPES.ASSIGN) {
return TITLE_PREFIX_MAP[rawType] || TITLE_PREFIX_MAP.assign;
}
if (displayType === PROCESS_NODE_TYPES.RECTIFY) {
return TITLE_PREFIX_MAP.rectify;
}
return TITLE_PREFIX_MAP[displayType] || TITLE_PREFIX_MAP[rawType] || node.nodeName || '处理';
};
const mapNodeContent = (node, displayType) => {
switch (displayType) {
case PROCESS_NODE_TYPES.ADD:
return mapAddContent(node.hazardInfo);
case PROCESS_NODE_TYPES.ASSIGN:
return mapAssignContent(node.assignInfo);
case PROCESS_NODE_TYPES.RECTIFY:
return mapRectifyContent(node.rectifyInfo);
case PROCESS_NODE_TYPES.VERIFY:
case PROCESS_NODE_TYPES.WRITEOFF_APPROVE:
return mapVerifyContent(node.verifyInfo);
case PROCESS_NODE_TYPES.WRITEOFF_APPLY:
return mapWriteoffApplyContent(node.writeOffApplyInfo);
case PROCESS_NODE_TYPES.APPROVAL:
return mapApprovalContent(node.subProcessApprovalInfo);
default:
return {};
}
};
export const mapProcessNodeToHistoryItem = (node) => {
if (!node || typeof node !== 'object') {
return {
type: PROCESS_NODE_TYPES.ADD,
rawType: '',
taskKey: '',
nodeName: '',
titlePrefix: '处理',
operator: '',
time: '',
content: {},
flowTaskId: '',
completed: true
};
}
const nodeType = resolveNodeType(node);
const rawType = node.nodeType || nodeType;
const displayType = resolveDisplayType(node, nodeType);
return {
type: displayType,
rawType,
taskKey: resolveTaskKey(node),
nodeName: getProcessStepDisplayName(node),
titlePrefix: resolveTitlePrefix(rawType, displayType, node),
operator: resolveOperator(node, displayType),
time: resolveTime(node, displayType),
content: mapNodeContent(node, displayType),
flowTaskId: node.flowTaskId || '',
completed: isNodeCompleted(node)
};
};
export const mapProcessChainNodes = (nodes = []) => {
if (!Array.isArray(nodes) || nodes.length === 0) return [];
return nodes.map((node) => mapProcessNodeToHistoryItem(node));
};
export const resolveProcessChainSummary = (data) => {
if (!data) {
return {
statusName: '-',
createdAt: '-'
};
}
const addNode = (data.nodes || []).find((item) => item.nodeType === 'add');
const createdAt = addNode?.hazardInfo?.createdAt || '-';
return {
statusName: data.statusName || '-',
createdAt
};
};

View File

@@ -0,0 +1,58 @@
export const CHAIN_LABELS = {
loading: '加载中...',
empty: '暂无流程记录',
personnelSuffix: '人员:',
hazardCode: '隐患编号',
hazardTitle: '隐患标题',
checkSource: '检查形式',
hazardSource: '隐患来源',
hazardArea: '隐患区域',
address: '位置描述',
hazardLevel: '隐患等级',
hazardTag: '隐患标签',
description: '问题描述',
hazardAttachments: '隐患附件',
legalBasis: '参考法规',
assigneeName: '指定整改责任人',
assignDeadline: '指定整改截至日期',
assignStatus: '交办状态',
rectifyStatus: '整改状态',
rectifyPlan: '整改方案',
rectifyResult: '整改结果',
rectifyMeasures: '整改措施',
controlMeasures: '管控措施',
rectifierName: '整改责任人',
managerNames: '管理人员',
memberNames: '整改成员',
planCost: '预计费用',
actualCost: '实际费用',
rectifyAttachments: '整改附件',
rectifySign: '整改签字',
verifyResult: '验收结果',
pass: '通过',
verifyRemark: '验收备注',
verifyAttachments: '验收附件',
verifySign: '验收签字',
writeoffDeadline: '整改时限',
responsibleDept: '治理责任单位',
responsiblePerson: '主要负责人',
mainTreatment: '主要治理内容',
treatmentResult: '治理完成内容',
selfVerify: '自行验收情况',
applySign: '申请签字',
approvalOpinion: '审批意见',
approvalComment: '意见说明',
smsReminder: '短信提醒',
yes: '是',
no: '否',
approvalSign: '审批签字',
attachmentFallback: '附件',
yuan: '元'
};
export const LEVEL_NAME_CLASS_MAP = {
一般: 'level-normal',
一般隐患: 'level-normal',
重大: 'level-major',
重大隐患: 'level-major'
};

View File

@@ -0,0 +1,163 @@
import { ref, watch, nextTick, getCurrentInstance } from 'vue';
import { onReady } from '@dcloudio/uni-app';
import { mapProcessChainNodes } from './processChain.js';
/**
* 流程链左右联动滚动(与 HazardDetailPanelV2 保持一致)
*/
export function useProcessChainScroll(chainSource, loadingSource) {
const instance = getCurrentInstance();
const queryScope = instance?.proxy || instance;
const historyList = ref([]);
const activeIndex = ref(0);
const sectionOffsets = ref([0]);
const contentViewHeight = ref(0);
const contentScrollIntoView = ref('');
const stepScrollIntoView = ref('');
const scrollWithAnimation = ref(true);
const isProgrammaticScroll = ref(false);
let measureLayoutTimer = null;
const measureLayout = () => {
if (!historyList.value.length) return;
nextTick(() => {
const query = uni.createSelectorQuery().in(queryScope);
query.select('.content-scroll').boundingClientRect();
query.select('.content-scroll').scrollOffset();
query.selectAll('.node-section').boundingClientRect();
query.exec((res) => {
const containerRect = res?.[0];
const scrollOffset = res?.[1];
const sections = res?.[2] || [];
if (!containerRect || !sections.length) return;
contentViewHeight.value = containerRect.height || 0;
const baseScrollTop = scrollOffset?.scrollTop || 0;
sectionOffsets.value = sections.map(
(section) => section.top - containerRect.top + baseScrollTop
);
});
});
};
const scheduleMeasureLayout = () => {
clearTimeout(measureLayoutTimer);
measureLayoutTimer = setTimeout(() => {
measureLayout();
}, 80);
};
const rebuildHistory = (data) => {
const nodes = data?.nodes || [];
historyList.value = mapProcessChainNodes(nodes);
activeIndex.value = 0;
contentScrollIntoView.value = '';
scheduleMeasureLayout();
setTimeout(scheduleMeasureLayout, 300);
};
const syncActiveIndex = (scrollTop, scrollHeight = 0) => {
const count = historyList.value.length;
if (!count) return;
if (scrollHeight > 0 && contentViewHeight.value > 0) {
if (scrollTop + contentViewHeight.value >= scrollHeight - 60) {
if (activeIndex.value !== count - 1) {
activeIndex.value = count - 1;
}
return;
}
}
const offsets = sectionOffsets.value;
if (!offsets.length) return;
let idx = 0;
for (let i = offsets.length - 1; i >= 0; i--) {
if (scrollTop >= offsets[i] - 80) {
idx = i;
break;
}
}
if (idx !== activeIndex.value) {
activeIndex.value = idx;
}
};
const onContentScroll = (e) => {
if (isProgrammaticScroll.value) return;
const { scrollTop = 0, scrollHeight = 0 } = e.detail || {};
syncActiveIndex(scrollTop, scrollHeight);
scheduleMeasureLayout();
};
const onScrollToLower = () => {
const count = historyList.value.length;
if (count > 0 && activeIndex.value !== count - 1) {
activeIndex.value = count - 1;
}
};
const scrollToNode = (index) => {
if (index < 0 || index >= historyList.value.length) return;
isProgrammaticScroll.value = true;
scrollWithAnimation.value = true;
activeIndex.value = index;
contentScrollIntoView.value = 'process-node-' + index;
setTimeout(() => {
contentScrollIntoView.value = '';
isProgrammaticScroll.value = false;
scheduleMeasureLayout();
}, 350);
};
watch(
() => (typeof loadingSource === 'function' ? loadingSource() : loadingSource?.value),
(loading) => {
if (!loading) {
scheduleMeasureLayout();
setTimeout(scheduleMeasureLayout, 300);
}
}
);
watch(
() => (typeof chainSource === 'function' ? chainSource() : chainSource?.value),
(val) => rebuildHistory(val),
{ immediate: true, deep: true }
);
watch(activeIndex, (index) => {
stepScrollIntoView.value = 'process-step-' + index;
setTimeout(() => {
stepScrollIntoView.value = '';
}, 300);
});
watch(
() => historyList.value.length,
() => scheduleMeasureLayout()
);
onReady(() => {
scheduleMeasureLayout();
setTimeout(scheduleMeasureLayout, 300);
});
return {
historyList,
activeIndex,
contentScrollIntoView,
stepScrollIntoView,
scrollWithAnimation,
onContentScroll,
onScrollToLower,
scrollToNode,
scheduleMeasureLayout
};
}

10
main.js
View File

@@ -69,10 +69,17 @@ uni.addInterceptor('uploadFile', {
} }
}); });
import uviewPlus, { setConfig } from '@/uni_modules/uview-plus'
setConfig({
config: {
loadFontOnce: true
}
})
// #ifndef VUE3 // #ifndef VUE3
import Vue from 'vue' import Vue from 'vue'
import './uni.promisify.adaptor' import './uni.promisify.adaptor'
import uviewPlus from '@/uni_modules/uview-plus'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(uviewPlus) Vue.use(uviewPlus)
App.mpType = 'app' App.mpType = 'app'
@@ -84,7 +91,6 @@ app.$mount()
// #ifdef VUE3 // #ifdef VUE3
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
import uviewPlus from '@/uni_modules/uview-plus'
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(uviewPlus) app.use(uviewPlus)

View File

@@ -129,6 +129,15 @@
"disableScroll": true "disableScroll": true
} }
}, },
{
"path": "pages/hiddendanger/process-chain",
"style": {
"navigationBarTitleText": "隐患详情",
"navigationStyle": "custom",
"navigationBarTextStyle": "white",
"disableScroll": true
}
},
{ {
"path":"pages/hiddendanger/rectification", "path":"pages/hiddendanger/rectification",
"style": { "style": {
@@ -141,6 +150,12 @@
"navigationBarTitleText": "隐患验收" "navigationBarTitleText": "隐患验收"
} }
}, },
{
"path":"pages/hiddendanger/acceptance-approval",
"style": {
"navigationBarTitleText": "验收审批"
}
},
{ {
"path":"pages/hiddendanger/assignment", "path":"pages/hiddendanger/assignment",
"style": { "style": {
@@ -153,6 +168,24 @@
"navigationBarTitleText": "销号申请" "navigationBarTitleText": "销号申请"
} }
}, },
{
"path":"pages/closeout/apply",
"style": {
"navigationBarTitleText": "新增销号申请"
}
},
{
"path":"pages/closeout/approval",
"style": {
"navigationBarTitleText": "销号审批"
}
},
{
"path":"pages/closeout/leader-approval",
"style": {
"navigationBarTitleText": "领导审批"
}
},
{ {
"path":"pages/closeout/editor", "path":"pages/closeout/editor",
"style": { "style": {
@@ -218,6 +251,12 @@
} }
}, },
{
"path": "pages/personalcenter/identity",
"style": {
"navigationBarTitleText": "切换身份"
}
},
{ {
"path" : "pages/login/login", "path" : "pages/login/login",
"style" : "style" :

View File

@@ -345,7 +345,7 @@ const viewHazardDetail = (item) => {
return; return;
} }
uni.navigateTo({ uni.navigateTo({
url: `/pages/hiddendanger/detail2?hazardId=${item.hazardId}&assignId=${item.assignId || ''}` url: `/pages/hiddendanger/process-chain?hazardId=${item.hazardId}`
}); });
}; };

View File

@@ -26,7 +26,7 @@
</view> </view>
<!-- 新增按钮 --> <!-- 新增按钮 -->
<button class="add-btn bg-blue round" @click="openAddPopup">新增公司区域</button> <button class="add-btn bg-blue round" @click="openAddPopup">新增区域</button>
<!-- 新增/编辑弹窗组件 --> <!-- 新增/编辑弹窗组件 -->
<AreaFormPopup <AreaFormPopup

View File

@@ -19,7 +19,6 @@
</view> </view>
<view class="flex margin-bottom"> <view class="flex margin-bottom">
<view class="text-gray">创建时间</view> <view class="text-gray">创建时间</view>
<view class="text-black">{{item.createdAt}}</view> <view class="text-black">{{item.createdAt}}</view>
</view> </view>
<view class="flex justify-between"> <view class="flex justify-between">
@@ -27,306 +26,32 @@
<view><button class="bg-blue round cu-btn lg" @click="editor(item)">查看详情</button></view> <view><button class="bg-blue round cu-btn lg" @click="editor(item)">查看详情</button></view>
</view> </view>
</view> </view>
<button class="cuIcon-add bg-blue round margin-top" @click="openAddPopup">新增</button> <button class="cuIcon-add bg-blue round margin-top" @click="goAddApply">新增</button>
<!-- 弹出框 -->
<u-popup :show="showAddPopup" mode="center" round="20" :safeAreaInsetBottom="false" @close="showAddPopup = false">
<view class="popup-content">
<view class="popup-header">
<view class="popup-title text-bold">新增销号申请</view>
<view class="popup-close" @click="showAddPopup = false">×</view>
</view>
<scroll-view class="popup-body" scroll-y :style="{ height: '60vh' }">
<view class="flex margin-bottom">
<view>隐患</view>
<view class="text-red">*</view>
</view>
<view class="picker-input" @click="showHazardPicker = true">
<text :class="selectedHazard ? '' : 'text-gray'">{{ selectedHazard || '请选择隐患' }}</text>
</view>
<up-picker
v-if="false" :show="showHazardPicker"
:columns="hazardColumns"
@confirm="onHazardConfirm"
@cancel="showHazardPicker = false"
@close="showHazardPicker = false"
></up-picker>
<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="请输入主要治理内容"></up-textarea>
<view class="margin-bottom margin-top">隐患治理完成内容</view>
<up-textarea v-model="formData.treatmentResult" placeholder="请输入隐患治理完成情况"></up-textarea>
<view class="margin-bottom margin-top">隐患治理责任单位自行验收的情况</view>
<up-textarea v-model="formData.selfVerifyContent" 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">隐患销号审批</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 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>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="showAddPopup = false">取消</button>
<button class="btn-confirm bg-blue" @click="handleAdd">确定</button>
</view>
</view>
</u-popup>
<!-- 移出到外层的选择器组件防止其定位样式被 scroll-view 剪裁并导致暗色遮罩溢出异常 -->
<up-picker
:show="showHazardPicker"
:columns="hazardColumns"
@confirm="onHazardConfirm"
@cancel="showHazardPicker = false"
@close="showHazardPicker = false"
></up-picker>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { ref } from 'vue';
import { getMyWriteOffList, applyDelete, getAcceptanceList, getHiddenDangerDetail, getRectifyDetail, generateWriteoffContent } from '@/request/api.js'; import { onShow } from '@dcloudio/uni-app';
import { getMyWriteOffList } from '@/request/api.js';
// 弹窗控制 const hazardList = ref([]);
const showAddPopup = ref(false);
const showHazardPicker = ref(false);
// 隐患选择
const selectedHazard = ref('');
const selectedHazardId = ref('');
const hazardColumns = ref([['暂无数据']]);
const acceptanceHazardList = ref([]); // 存储可申请销号的隐患数据
const hazardList = ref([]); // 存储销号申请列表
// 隐患关联的只读展示字段
const selectedDeptName = ref('');
// AI生成状态
const aiGenerating = ref(false);
// 短信提醒
const sendMsgFlagRadio = ref('yes');
const sendMsgFlag = computed(() => sendMsgFlagRadio.value === 'yes');
// 表单数据
const formData = reactive({
rectifyDeadline: '', // 整改时限
responsibleDeptId: '', // 隐患治理责任单位ID
responsiblePerson: '', // 主要负责人
mainTreatmentContent: '', // 主要治理内容
treatmentResult: '', // 隐患治理完成内容
selfVerifyContent: '' // 责任单位自行验收情况
});
// 获取销号申请列表(页面显示用)
const fetchWriteOffList = async () => { const fetchWriteOffList = async () => {
try { try {
const res = await getMyWriteOffList(); const res = await getMyWriteOffList();
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
hazardList.value = res.data; hazardList.value = res.data;
console.log('销号申请列表:', res.data);
} }
} catch (error) { } catch (error) {
console.error('获取销号申请列表失败:', error); console.error('获取销号申请列表失败:', error);
} }
}; };
// 获取可申请销号的隐患列表(弹窗选择用) const goAddApply = () => {
const fetchAcceptanceList = async () => { uni.navigateTo({ url: '/pages/closeout/apply' });
try {
const res = await getAcceptanceList();
if (res.code === 0 && res.data) {
const list = res.data.records || res.data || [];
acceptanceHazardList.value = list;
// 转换为 picker 需要的格式
if (list.length > 0) {
hazardColumns.value = [list.map(item => item.title || item.hazardTitle || `隐患${item.hazardId}`)];
} else {
hazardColumns.value = [['暂无可申请销号的隐患']];
}
console.log('可申请销号的隐患列表:', list);
}
} catch (error) {
console.error('获取可申请销号隐患列表失败:', error);
}
}; };
// 打开新增弹窗
const openAddPopup = () => {
resetForm();
fetchAcceptanceList(); // 获取可申请销号的隐患列表
showAddPopup.value = true;
};
// 根据选中的隐患回显只读字段
const fillHazardRelatedFields = (hazard) => {
formData.rectifyDeadline = hazard.deadline || '';
selectedDeptName.value = hazard.deptName || '';
formData.responsiblePerson = hazard.rectifierName || '';
formData.responsibleDeptId = hazard.deptId || '';
};
// 隐患选择确认
const onHazardConfirm = (e) => {
console.log('选择的隐患:', e);
if (e.value && e.value.length > 0) {
selectedHazard.value = e.value[0];
const index = e.indexs[0];
const hazard = acceptanceHazardList.value[index];
if (hazard) {
selectedHazardId.value = hazard.hazardId;
fillHazardRelatedFields(hazard);
}
}
showHazardPicker.value = false;
};
// 重置表单
const resetForm = () => {
selectedHazard.value = '';
selectedHazardId.value = '';
selectedDeptName.value = '';
formData.rectifyDeadline = '';
formData.responsibleDeptId = '';
formData.responsiblePerson = '';
formData.mainTreatmentContent = '';
formData.treatmentResult = '';
formData.selfVerifyContent = '';
sendMsgFlagRadio.value = 'yes';
};
// AI生成销号方案
const handleAiGenerate = async () => {
if (!selectedHazardId.value) {
uni.showToast({ title: '请先选择隐患', icon: 'none' });
return;
}
aiGenerating.value = true;
try {
// 1. 获取隐患详情
const hazardRes = await getHiddenDangerDetail({ hazardId: selectedHazardId.value });
if (hazardRes.code !== 0 || !hazardRes.data) {
uni.showToast({ title: '获取隐患详情失败', icon: 'none' });
return;
}
// 2. 从 assigns 中获取 rectifyId
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;
// 3. 获取整改详情
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;
}
// 4. 调用AI生成销号方案
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 handleAdd = async () => {
if (!selectedHazardId.value) {
uni.showToast({ title: '请选择隐患', icon: 'none' });
return;
}
// 构建请求参数(与接口文档对应)
const params = {
hazardId: Number(selectedHazardId.value), // 隐患ID必需
rectifyDeadline: formData.rectifyDeadline || '', // 整改时限
responsibleDeptId: Number(formData.responsibleDeptId) || 0, // 隐患治理责任单位ID
responsiblePerson: formData.responsiblePerson || '', // 主要负责人
mainTreatmentContent: formData.mainTreatmentContent || '', // 主要治理内容
treatmentResult: formData.treatmentResult || '', // 隐患治理完成内容
selfVerifyContent: formData.selfVerifyContent || '', // 责任单位自行验收情况
sendMsgFlag: sendMsgFlag.value // 是否短信提醒
};
console.log('提交数据:', params);
try {
const res = await applyDelete(params);
if (res.code === 0) {
uni.showToast({ title: '申请成功', icon: 'success' });
showAddPopup.value = false;
resetForm();
// 刷新销号申请列表
fetchWriteOffList();
} else {
uni.showToast({ title: res.msg || '申请失败', icon: 'none' });
}
} catch (error) {
console.error('申请失败:', error);
uni.showToast({ title: '请求失败', icon: 'none' });
}
};
const editor = (item) => { const editor = (item) => {
if (!item?.id) { if (!item?.id) {
uni.showToast({ title: '缺少申请ID', icon: 'none' }); uni.showToast({ title: '缺少申请ID', icon: 'none' });
@@ -336,8 +61,7 @@
url: `/pages/closeout/editor?applyId=${item.id}` url: `/pages/closeout/editor?applyId=${item.id}`
}); });
}; };
// 根据审核状态返回对应样式类
const getStatusClass = (item) => { const getStatusClass = (item) => {
const statusName = item?.verifyResultName || ''; const statusName = item?.verifyResultName || '';
if (statusName === '待审核') return 'status-pending'; if (statusName === '待审核') return 'status-pending';
@@ -348,9 +72,8 @@
if (item?.verifyResult === 2) return 'status-rejected'; if (item?.verifyResult === 2) return 'status-rejected';
return 'status-default'; return 'status-default';
}; };
// 页面加载时获取销号申请列表 onShow(() => {
onMounted(() => {
fetchWriteOffList(); fetchWriteOffList();
}); });
</script> </script>
@@ -397,120 +120,4 @@
background: #F5F5F5; background: #F5F5F5;
color: #8C8C8C; color: #8C8C8C;
} }
</style>
.popup-content {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.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;
}
}
.popup-body {
padding: 30rpx;
}
.popup-footer {
display: flex;
border-top: 1rpx solid #eee;
button {
flex: 1;
height: 90rpx;
line-height: 90rpx;
border-radius: 0;
margin: 0 !important;
padding: 0 !important;
font-size: 30rpx;
&::after {
border: none;
}
}
.btn-cancel {
background: #fff;
color: #666;
}
.btn-confirm {
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;
}
</style>

778
pages/closeout/apply.vue Normal file
View File

@@ -0,0 +1,778 @@
<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="请输入主要治理内容"></up-textarea>
<view class="margin-bottom margin-top">隐患治理完成内容</view>
<up-textarea v-model="formData.treatmentResult" placeholder="请输入隐患治理完成情况"></up-textarea>
<view class="margin-bottom margin-top">隐患治理责任单位自行验收的情况</view>
<up-textarea v-model="formData.selfVerifyContent" 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 class="flex margin-bottom margin-top">
<view class="text-gray">下一步处理人</view>
<view class="text-red">*</view>
</view>
<view 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 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>
<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>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import {
applyDelete,
getHiddenDangerDetail,
getHiddenDangerList,
getRectifyDetail,
generateWriteoffContent,
getFlowNextNodes,
getDeptUsers
} from '@/request/api.js';
import { buildWriteoffHazardFromOptions } from '@/utils/hazardNav.js';
const showHazardPicker = ref(false);
const hazardLocked = ref(false);
const selectedHazard = ref('');
const selectedHazardId = ref('');
const assignId = ref('');
const taskId = ref('');
const hazardColumns = ref([['暂无数据']]);
const selectableHazardList = ref([]);
const selectedDeptName = ref('');
const aiGenerating = ref(false);
const sendMsgFlagRadio = ref('yes');
const sendMsgFlag = computed(() => sendMsgFlagRadio.value === 'yes');
const nextStepName = ref('');
const nextStepLoading = ref(false);
const nextStepDisplay = computed(() => {
if (nextStepLoading.value) return '加载中...';
return nextStepName.value || '暂无下一步流程';
});
const selectedAssigneeIdentityId = ref('');
const selectedAssigneeName = ref('');
const pickerAssigneeIdentityId = ref('');
const pickerAssigneeName = ref('');
const showAssigneePopup = ref(false);
const assigneeList = ref([]);
const assigneeLoading = ref(false);
const formData = reactive({
rectifyDeadline: '',
responsibleDeptId: '',
responsiblePerson: '',
mainTreatmentContent: '',
treatmentResult: '',
selfVerifyContent: ''
});
const findHazardInList = (hazardId) => {
return selectableHazardList.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 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 buildPreviewVariables = () => ({
quickApprove: true
});
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 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 = () => 2;
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 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 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 = '';
};
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) : '';
if (hazard.taskId) {
taskId.value = String(hazard.taskId);
}
fillHazardRelatedFields(hazard);
resetFlowSelection();
await resolveTaskIdForHazard(hazard);
await fetchNextStep();
};
const resolveSelectableRecords = (data) => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.records)) return data.records;
return [];
};
const canApplyWriteoff = (item) => (
item?.statusName === '待销号' && (item?.applyFlag === true || item?.applyFlag === 1 || item?.applyFlag === '1')
);
/** 非锁定入口:从隐患列表拉取可申请销号的隐患(替代 verified/list */
const fetchSelectableHazardList = async () => {
try {
const res = await getHiddenDangerList({ pageNum: 1, pageSize: 100, status: 4 });
if (res.code === 0 && res.data) {
const list = resolveSelectableRecords(res.data).filter(canApplyWriteoff);
selectableHazardList.value = list;
if (list.length > 0) {
hazardColumns.value = [list.map((item) => item.title || item.hazardTitle || `隐患${item.hazardId}`)];
} else {
hazardColumns.value = [['暂无可申请销号的隐患']];
}
}
} catch (error) {
console.error('获取可申请销号隐患列表失败:', error);
}
};
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 = selectableHazardList.value[index];
if (hazard) {
await applySelectedHazard(hazard);
}
}
showHazardPicker.value = false;
};
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 handleSubmit = async () => {
if (!selectedHazardId.value) {
uni.showToast({ title: '请选择隐患', icon: 'none' });
return;
}
if (!selectedAssigneeIdentityId.value) {
uni.showToast({ title: '请选择下一步处理人', icon: 'none' });
return;
}
const params = {
hazardId: Number(selectedHazardId.value),
rectifyDeadline: formData.rectifyDeadline || '',
responsibleDeptId: Number(formData.responsibleDeptId) || 0,
responsiblePerson: formData.responsiblePerson || '',
mainTreatmentContent: formData.mainTreatmentContent || '',
treatmentResult: formData.treatmentResult || '',
selfVerifyContent: formData.selfVerifyContent || '',
assigneeIdentityId: selectedAssigneeIdentityId.value,
sendMsgFlag: sendMsgFlag.value
};
try {
const res = await applyDelete(params);
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' });
}
};
onLoad(async (options) => {
hazardLocked.value = options?.locked === '1';
if (options?.assignId) {
assignId.value = String(options.assignId);
}
if (options?.taskId) {
taskId.value = String(options.taskId);
}
const hazardFromOptions = buildWriteoffHazardFromOptions(options);
if (hazardFromOptions?.hazardId) {
await applySelectedHazard(hazardFromOptions);
return;
}
if (!hazardLocked.value) {
await fetchSelectableHazardList();
}
if (taskId.value) {
await fetchNextStep();
}
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #EBF2FC;
}
.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;
}
.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;
}
}
}
</style>

1232
pages/closeout/approval.vue Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,21 +16,12 @@
<text>分派单位</text> <text>分派单位</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<view class="picker-input" @click="showDeptPicker = true"> <view class="picker-input" @click="openDeptPopup">
<text :class="formData.deptName ? 'picker-value' : 'picker-placeholder'"> <text :class="formData.deptName ? 'picker-value' : 'picker-placeholder'">
{{ formData.deptName || '请选择分派单位' }} {{ formData.deptName || '请选择分派单位' }}
</text> </text>
<text class="cuIcon-unfold picker-arrow"></text> <text class="cuIcon-unfold picker-arrow"></text>
</view> </view>
<up-picker
:show="showDeptPicker"
:columns="deptCascaderColumns"
:defaultIndex="deptCascaderIndexs"
@confirm="onDeptConfirm"
@change="onDeptCascaderChange"
@cancel="showDeptPicker = false"
@close="showDeptPicker = false"
></up-picker>
</view> </view>
<!-- 补充说明 --> <!-- 补充说明 -->
@@ -377,6 +368,42 @@
</view> </view>
</u-popup> </u-popup>
<!-- 分派单位选择弹窗 -->
<u-popup :show="showDeptPicker" mode="center" round="20" :safeAreaInsetBottom="false" @close="cancelDeptSelect">
<view class="dept-popup">
<view class="popup-header">
<view class="popup-title">选择分派单位</view>
<view class="popup-close" @click="cancelDeptSelect">×</view>
</view>
<view v-if="showDeptPicker" class="dept-tree-body">
<xq-tree
v-if="deptTree.length"
ref="deptTreeRef"
:data="deptTree"
node-key="deptId"
label-key="deptName"
children-key="children"
show-checkbox
check-strictly
default-expand-all
:height="500"
:width="560"
:default-checked-keys="deptDefaultCheckedKeys"
empty-text="暂无部门数据"
@check="onDeptCheck"
@node-click="onDeptNodeClick"
/>
<view v-else class="empty-tip">暂无部门数据</view>
</view>
<view class="popup-footer">
<button class="btn-cancel" @click="cancelDeptSelect">取消</button>
<button class="btn-confirm" @click="confirmDeptSelect">确定</button>
</view>
</view>
</u-popup>
<!-- 执行人员选择弹窗 --> <!-- 执行人员选择弹窗 -->
<u-popup :show="showExecutorPopup" mode="center" round="20" @close="showExecutorPopup = false"> <u-popup :show="showExecutorPopup" mode="center" round="20" @close="showExecutorPopup = false">
<view class="executor-popup"> <view class="executor-popup">
@@ -413,9 +440,8 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed } from 'vue'; import { ref, reactive, computed, onMounted, nextTick } from 'vue';
import { getRegulationList, addCheckPoint, detailcheckPoint, deleteCheckPoint, getCheckItemList, getCheckItemListDetail, getDeptUsers, getDeptChildren, addCheckTable } from '@/request/api.js'; import { getRegulationList, addCheckPoint, detailcheckPoint, deleteCheckPoint, getCheckItemList, getCheckItemListDetail, getDeptUsers, getDeptChildren, addCheckTable } from '@/request/api.js';
import { onMounted } from 'vue';
// 表单数据 // 表单数据
const formData = reactive({ const formData = reactive({
@@ -560,13 +586,11 @@ const endDateValue = ref(getTodayTimestamp());
const showDeptPicker = ref(false); const showDeptPicker = ref(false);
const showTypePicker = ref(false); const showTypePicker = ref(false);
const showModePicker = ref(false); const showModePicker = ref(false);
const showDeptSelectPicker = ref(false);
const showCyclePicker = ref(false); const showCyclePicker = ref(false);
const showStartDatePicker = ref(false); const showStartDatePicker = ref(false);
const showEndDatePicker = ref(false); const showEndDatePicker = ref(false);
// 选择器数据 // 选择器数据
const deptColumns = ref([['湘西自治州和谐网络科技有限公司', '湘西自治州和谐云大数据科技有限公司', '湘西网络有限公司']]);
const typeColumns = ref([['日常检查', '专项检查', '设备检查']]); const typeColumns = ref([['日常检查', '专项检查', '设备检查']]);
const modeColumns = ref([['单人完成', '全员']]); const modeColumns = ref([['单人完成', '全员']]);
const cycleColumns = ref([['每天一次', '每周一次', '每月一次', '每季度一次']]); const cycleColumns = ref([['每天一次', '每周一次', '每月一次', '每季度一次']]);
@@ -576,23 +600,84 @@ const showExecutorPopup = ref(false);
const executorList = ref([]); const executorList = ref([]);
const selectedExecutorId = ref(null); const selectedExecutorId = ref(null);
// 分派单位级联选择器 // 分派单位树形选择器
const deptTree = ref([]); const deptTree = ref([]);
const deptCascaderColumns = ref([]); const deptTreeRef = ref(null);
const deptCascaderIndexs = ref([0]); const selectedDeptId = ref(null);
const selectedDeptPath = ref([]); // 选中的路径 const deptDefaultCheckedKeys = ref([]);
// 选择器确认回调 const getDeptNodeId = (node) => node?.deptId ?? node?.raw?.deptId ?? null;
const onDeptConfirm = () => {
const lastSelected = selectedDeptPath.value[selectedDeptPath.value.length - 1] const syncDeptTreeChecked = (deptId) => {
if (lastSelected) { nextTick(() => {
formData.deptId = lastSelected.deptId if (deptId) {
formData.deptName = selectedDeptPath.value.map(d => d.deptName).join(' / ') deptTreeRef.value?.setCheckedKeys?.([deptId]);
clearExecutorSelection() } else {
fetchDeptUsers(lastSelected.deptId) deptTreeRef.value?.clearChecked?.();
}
});
};
const findDeptPath = (tree, targetId, path = []) => {
for (const node of tree) {
const currentPath = [...path, node];
if (String(node.deptId) === String(targetId)) {
return currentPath;
}
if (node.children?.length) {
const found = findDeptPath(node.children, targetId, currentPath);
if (found) return found;
}
} }
showDeptPicker.value = false return null;
} };
const openDeptPopup = () => {
selectedDeptId.value = formData.deptId || null;
deptDefaultCheckedKeys.value = formData.deptId ? [formData.deptId] : [];
showDeptPicker.value = true;
syncDeptTreeChecked(formData.deptId);
};
const onDeptNodeClick = (node) => {
const deptId = getDeptNodeId(node);
selectedDeptId.value = deptId;
syncDeptTreeChecked(deptId);
};
const onDeptCheck = (node, { checkedKeys = [] } = {}) => {
const deptId = getDeptNodeId(node);
const isChecked = checkedKeys.some((key) => String(key) === String(deptId));
if (isChecked) {
selectedDeptId.value = deptId;
syncDeptTreeChecked(deptId);
return;
}
selectedDeptId.value = null;
syncDeptTreeChecked(null);
};
const cancelDeptSelect = () => {
showDeptPicker.value = false;
};
const confirmDeptSelect = () => {
if (!selectedDeptId.value) {
uni.showToast({ title: '请选择分派单位', icon: 'none' });
return;
}
const path = findDeptPath(deptTree.value, selectedDeptId.value);
if (!path?.length) {
uni.showToast({ title: '请选择分派单位', icon: 'none' });
return;
}
const selected = path[path.length - 1];
formData.deptId = selected.deptId;
formData.deptName = path.map((d) => d.deptName).join(' / ');
clearExecutorSelection();
fetchDeptUsers(selected.deptId);
showDeptPicker.value = false;
};
// 检查表类型映射:日常检查->1, 专项检查->2, 设备检查->3 // 检查表类型映射:日常检查->1, 专项检查->2, 设备检查->3
const typeMap = { const typeMap = {
@@ -674,72 +759,12 @@ const fetchDeptChildren = async () => {
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
const list = Array.isArray(res.data) ? res.data : [] const list = Array.isArray(res.data) ? res.data : []
deptTree.value = handleTree(list, 'deptId') deptTree.value = handleTree(list, 'deptId')
initDeptCascader(deptTree.value)
} }
} catch (error) { } catch (error) {
console.error('获取部门树失败:', error) console.error('获取部门树失败:', error)
} }
} }
// 初始化部门级联选择器
const initDeptCascader = (treeData) => {
const roots = Array.isArray(treeData) ? treeData : (treeData ? [treeData] : [])
if (roots.length === 0) return
const firstColumn = roots.map(item => ({ text: item.deptName, ...item }))
deptCascaderColumns.value = [firstColumn]
deptCascaderIndexs.value = [0]
selectedDeptPath.value = [roots[0]]
let current = roots[0]
while (current.children && current.children.length > 0) {
const nextColumn = current.children.map(item => ({ text: item.deptName, ...item }))
deptCascaderColumns.value.push(nextColumn)
deptCascaderIndexs.value.push(0)
selectedDeptPath.value.push(current.children[0])
current = current.children[0]
}
}
// 部门级联选择器列改变
const onDeptCascaderChange = (e) => {
const { columnIndex, index, value } = e;
// 更新当前列的选中索引
deptCascaderIndexs.value[columnIndex] = index;
// 更新选中路径
selectedDeptPath.value[columnIndex] = value;
// 删除后续列
deptCascaderColumns.value = deptCascaderColumns.value.slice(0, columnIndex + 1);
deptCascaderIndexs.value = deptCascaderIndexs.value.slice(0, columnIndex + 1);
selectedDeptPath.value = selectedDeptPath.value.slice(0, columnIndex + 1);
// 如果有子级,添加新列
if (value.children && value.children.length > 0) {
const nextColumn = value.children.map(item => ({ text: item.deptName, ...item }));
deptCascaderColumns.value.push(nextColumn);
deptCascaderIndexs.value.push(0);
selectedDeptPath.value.push(value.children[0]);
// 继续检查是否有下下级
if (value.children[0].children && value.children[0].children.length > 0) {
const thirdColumn = value.children[0].children.map(item => ({ text: item.deptName, ...item }));
deptCascaderColumns.value.push(thirdColumn);
deptCascaderIndexs.value.push(0);
selectedDeptPath.value.push(value.children[0].children[0]);
}
}
};
const onDeptSelectConfirm = (e) => {
if (e.value && e.value.length > 0) {
formData.selectDeptName = e.value[0];
}
showDeptSelectPicker.value = false;
};
const onCycleConfirm = (e) => { const onCycleConfirm = (e) => {
if (e.value && e.value.length > 0) { if (e.value && e.value.length > 0) {
formData.cycleName = e.value[0]; formData.cycleName = e.value[0];
@@ -1857,6 +1882,31 @@ onMounted(() => {
} }
} }
// 分派单位弹窗样式
.dept-popup {
width: 600rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
.popup-footer {
button {
margin: 0;
}
}
}
.dept-tree-body {
height: 500rpx;
overflow: hidden;
:deep(.checkbox-custom.checked),
:deep(.checkbox-custom.indeterminate) {
background-color: #2667E9;
border-color: #2667E9;
}
}
// 执行人员弹窗样式 // 执行人员弹窗样式
.executor-popup { .executor-popup {
width: 600rpx; width: 600rpx;

View File

@@ -4,16 +4,16 @@
<scroll-view class="status-tabs" scroll-x :show-scrollbar="false"> <scroll-view class="status-tabs" scroll-x :show-scrollbar="false">
<view class="status-tabs-inner"> <view class="status-tabs-inner">
<view v-for="(tab, index) in statusTabs" :key="index" class="status-tab-item" <view v-for="(tab, index) in statusTabs" :key="index" class="status-tab-item"
:class="{ 'status-tab-active': activeTab === index }" @click="activeTab = index"> :class="{ 'status-tab-active': activeTab === index }" @click="switchStatusTab(index)">
<text class="status-tab-text">{{ tab.label }}</text> <text class="status-tab-text">{{ tab.label }}</text>
<view v-if="activeTab === index" class="status-tab-bar"></view> <view v-if="activeTab === index" class="status-tab-bar"></view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<view v-if="filteredList.length === 0" class="empty-tip text-gray text-center padding">暂无数据</view> <view v-if="hiddenDangerList.length === 0 && !listLoading" class="empty-tip text-gray text-center padding">暂无数据</view>
<view class="padding radius bg-white list-list margin-bottom" v-for="item in filteredList" <view class="padding radius bg-white list-list margin-bottom" v-for="item in hiddenDangerList"
:key="item.hazardId"> :key="item.hazardId">
<view class="flex justify-between margin-bottom"> <view class="flex justify-between margin-bottom">
<view class="text-bold text-black" style="word-break: break-all; flex: 1;">{{item.title}}</view> <view class="text-bold text-black" style="word-break: break-all; flex: 1;">{{item.title}}</view>
@@ -43,12 +43,22 @@
class="round cu-btn bg-blue" @click="Rectification(item)">立即整改</button> class="round cu-btn bg-blue" @click="Rectification(item)">立即整改</button>
<button v-if="item.statusName === '待验收' && item.canEdit" <button v-if="item.statusName === '待验收' && item.canEdit"
class="round cu-btn light bg-blue" @click="editRectification(item)">编辑整改信息</button> class="round cu-btn light bg-blue" @click="editRectification(item)">编辑整改信息</button>
<button v-if="item.statusName === '待验收' && canAcceptance" <button v-if="canShowAcceptanceButton(item, userRole)"
class="round cu-btn bg-blue" @click="acceptance(item)">立即验收</button> class="round cu-btn bg-blue" @click="acceptance(item)">立即验收</button>
<button v-if="item.statusName === '待交办'" <button v-if="item.statusName === '待交办'"
class="round cu-btn bg-blue" @click="assignHazard(item)">隐患交办</button> class="round cu-btn bg-blue" @click="assignHazard(item)">隐患交办</button>
<button v-if="canShowWriteoffApplyButton(item)"
class="round cu-btn bg-blue" @click="goWriteoffApply(item)">销号申请</button>
<button v-if="canShowWriteoffApprovalButton(item, userRole)"
class="round cu-btn bg-blue" @click="goWriteoffApproval(item)">销号审批</button>
</view> </view>
</view> </view>
<u-loadmore
v-if="hiddenDangerList.length > 0"
:status="loadStatus"
style="margin-top: 20rpx; margin-bottom: 140rpx;"
/>
<view class="fixed-add-btn" @click="goToAdd"> <view class="fixed-add-btn" @click="goToAdd">
<text class="cuIcon-add"></text> <text class="cuIcon-add"></text>
@@ -58,35 +68,42 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app'
import { import {
enterCheckPlan, enterCheckPlan,
getCheckTaskDetail, getCheckTaskDetail,
getMyHiddenDangerList getMyHiddenDangerList
} from '@/request/api.js' } from '@/request/api.js'
import {
buildRectificationUrl,
buildEditRectificationUrl,
buildAssignmentUrl,
buildAcceptanceUrl,
buildWriteoffApplyUrl,
buildWriteoffApprovalUrl,
canShowAcceptanceButton,
canShowWriteoffApplyButton,
canShowWriteoffApprovalButton
} from '@/utils/hazardNav.js'
import { resolveUserRoleKey } from '@/utils/userInfo.js'
const taskId = ref('') const taskId = ref('')
const checkPointId = ref('') const checkPointId = ref('')
const oneTableId = ref('') const oneTableId = ref('')
const userRole = ref('') const userRole = ref('')
const canAcceptance = computed(() => {
return userRole.value === 'admin' || userRole.value === 'manage'
})
const getUserRole = () => { const getUserRole = () => {
try { try {
const userInfoStr = uni.getStorageSync('userInfo') const userInfoStr = uni.getStorageSync('userInfo')
if (userInfoStr) { if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr) userRole.value = resolveUserRoleKey(JSON.parse(userInfoStr))
userRole.value = userInfo.role || ''
} }
} catch (error) { } catch (error) {
console.error('获取用户信息失败:', error) console.error('获取用户信息失败:', error)
} }
} }
getUserRole()
const fetchTaskInfo = async (oneTableId) => { const fetchTaskInfo = async (oneTableId) => {
try { try {
@@ -105,19 +122,82 @@
} }
onLoad((options) => { onLoad((options) => {
getUserRole()
if (options.id) { if (options.id) {
oneTableId.value = options.id oneTableId.value = options.id
fetchTaskInfo(options.id) fetchTaskInfo(options.id)
} }
}) })
const HAZARD_PAGE_SIZE = 10
const hiddenDangerList = ref([]) const hiddenDangerList = ref([])
const pageNum = ref(1)
const listLoading = ref(false)
const loadStatus = ref('loadmore')
const statusTabs = ref([
{ label: '全部', value: null },
{ label: '待交办', value: 1 },
{ label: '待整改', value: 2 },
{ label: '待验收', value: 3 },
{ label: '待销号', value: 4 },
{ label: '已完成', value: 5 }
])
const activeTab = ref(0)
const buildListParams = () => {
const params = {
pageNum: pageNum.value,
pageSize: HAZARD_PAGE_SIZE
}
const currentTab = statusTabs.value[activeTab.value]
if (currentTab?.value != null) {
params.status = currentTab.value
}
return params
}
const resetHiddenDangerList = () => {
pageNum.value = 1
hiddenDangerList.value = []
loadStatus.value = 'loadmore'
}
const switchStatusTab = (index) => {
if (activeTab.value === index) return
activeTab.value = index
resetHiddenDangerList()
fetchHiddenDangerList()
}
const fetchHiddenDangerList = async () => { const fetchHiddenDangerList = async () => {
if (listLoading.value) return
if (pageNum.value > 1 && loadStatus.value === 'nomore') return
listLoading.value = true
if (pageNum.value > 1) {
loadStatus.value = 'loading'
}
try { try {
const res = await getMyHiddenDangerList() const res = await getMyHiddenDangerList(buildListParams())
if (res.code === 0) { if (res.code === 0) {
hiddenDangerList.value = res.data.records const records = res.data?.records || []
const total = Number(res.data?.total ?? 0)
if (pageNum.value === 1) {
hiddenDangerList.value = records
} else {
hiddenDangerList.value = [...hiddenDangerList.value, ...records]
}
if (hiddenDangerList.value.length >= total || records.length < HAZARD_PAGE_SIZE) {
loadStatus.value = 'nomore'
} else {
loadStatus.value = 'loadmore'
}
} else { } else {
if (pageNum.value === 1) {
hiddenDangerList.value = []
}
loadStatus.value = 'nomore'
uni.showToast({ uni.showToast({
title: res.msg || '获取隐患列表失败', title: res.msg || '获取隐患列表失败',
icon: 'none' icon: 'none'
@@ -125,15 +205,33 @@
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
if (pageNum.value > 1) {
pageNum.value--
}
loadStatus.value = 'loadmore'
uni.showToast({ uni.showToast({
title: '请求失败', title: '请求失败',
icon: 'none' icon: 'none'
}) })
} finally {
listLoading.value = false
} }
} }
onShow(() => { const loadMoreHiddenDangerList = () => {
if (loadStatus.value !== 'loadmore' || listLoading.value) return
pageNum.value++
fetchHiddenDangerList() fetchHiddenDangerList()
}
onShow(() => {
getUserRole()
resetHiddenDangerList()
fetchHiddenDangerList()
})
onReachBottom(() => {
loadMoreHiddenDangerList()
}) })
const goToAdd = () => { const goToAdd = () => {
@@ -156,59 +254,33 @@
const details = (item) => { const details = (item) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/hiddendanger/detail2?hazardId=${item.hazardId}&assignId=${item.assignId || ''}` url: `/pages/hiddendanger/process-chain?hazardId=${item.hazardId}`
}) })
} }
const Rectification = (item) => { const Rectification = (item) => {
let url = `/pages/hiddendanger/rectification?hazardId=${item.hazardId}&assignId=${item.assignId}` uni.navigateTo({ url: buildRectificationUrl(item) });
if (item.deadline) {
url += `&deadline=${encodeURIComponent(item.deadline)}`
}
if (item.assigneeId) {
url += `&assigneeId=${item.assigneeId}`
}
if (item.assigneeName) {
url += `&assigneeName=${encodeURIComponent(item.assigneeName)}`
}
uni.navigateTo({ url })
} }
const editRectification = (item) => { const editRectification = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildEditRectificationUrl(item) });
url: `/pages/hiddendanger/rectification?rectifyId=${item.rectifyId}&isEdit=1`
})
} }
const acceptance = (item) => { const acceptance = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildAcceptanceUrl(item) });
url: `/pages/hiddendanger/acceptance?hazardId=${item.hazardId}&assignId=${item.assignId}&rectifyId=${item.rectifyId}`
})
} }
const assignHazard = (item) => { const assignHazard = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildAssignmentUrl(item) });
url: `/pages/hiddendanger/assignment?hazardId=${item.hazardId}&assignId=${item.assignId}`
})
} }
const statusTabs = ref([ const goWriteoffApply = (item) => {
{ label: '全部', value: null }, uni.navigateTo({ url: buildWriteoffApplyUrl(item) });
{ label: '待交办', value: 1 }, }
{ label: '待整改', value: 2 },
{ label: '待验收', value: 3 },
{ label: '待销号', value: 4 },
{ label: '已完成', value: 5 }
])
const activeTab = ref(0)
const filteredList = computed(() => { const goWriteoffApproval = (item) => {
const currentTab = statusTabs.value[activeTab.value] uni.navigateTo({ url: buildWriteoffApprovalUrl(item) });
if (!currentTab || currentTab.value === null) { }
return hiddenDangerList.value
}
return hiddenDangerList.value.filter(item => item.status === currentTab.value)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,312 @@
# 隐患验收页逻辑说明
> 对应文件:`pages/hiddendanger/acceptance.vue`
> 最后整理2026-07-15
---
## 1. 页面职责
验收页用于对**已提交的整改记录**进行验收,主要能力:
1. 只读展示整改记录(方案、措施、人员、附件等)
2. 填写验收表单(结果、备注、验收附件、签名)
3. 根据验收选择**预览下一步流程**
4. 选择或通过只读展示**下一步处理人**
5. 提交到 `POST /frontend/hazard/verify`
---
## 2. 页面入参URL Query
从首页 / 巡检列表跳转,由 `utils/hazardNav.js``buildAcceptanceUrl` 构建:
| 参数 | 是否必需 | 说明 |
|------|----------|------|
| `rectifyId` | **推荐必带** | 整改记录 ID有则只调整改详情接口 |
| `hazardId` | 列表通常会带 | 隐患 ID`rectifyId` 时用于兜底拉取 |
| `assignId` | 可选 | 指派 ID用于从隐患详情中定位正确 assign |
| `taskId` | **流程预览依赖** | 工作流任务 ID用于「下一步流程」接口 |
示例:
```
/pages/hiddendanger/acceptance?hazardId=184&assignId=158&rectifyId=150&taskId=xxx
```
> **注意**`rectify/detail` 响应里通常**没有** `taskId`,下一步流程主要依赖 URL 传入的 `taskId`。若列表未带且详情也解析不到,「下一步流程」会显示「暂无下一步流程」。
---
## 3. 页面加载流程
```
onLoad
├─ 解析 URL 参数rectifyId / hazardId / assignId / taskId
├─ loadPageData()
│ ├─ 有 rectifyId → fetchRectifyDetail() // 只调一次
│ ├─ 无 rectifyId、有 hazardId → fetchDetail()
│ └─ fetchNextStep()
└─ restoreDraft() // 恢复本地草稿(若有)
```
### 3.1 数据接口选择(重要)
**不要两个详情接口都调**,当前规则:
| 条件 | 调用接口 | 说明 |
|------|----------|------|
| 有 `rectifyId` | `GET /frontend/hazard/rectify/detail` | **唯一数据源** |
| 无 `rectifyId`、有 `hazardId` | `GET /frontend/hazard/detail` | 从 `assigns[].rectify` 取整改记录 |
### 3.2 两个详情接口的区别
| 对比项 | `rectify/detail` | `hazard/detail` 内嵌 `rectify` |
|--------|------------------|-------------------------------|
| 数据范围 | 单条整改记录 | 整条隐患 + 指派 + 整改 |
| 人员结构 | `members` / `managers` 对象数组 | `memberNames` / `managerNames` 字符串数组 |
| 整改人 | 有 `rectifierName` | 有 `rectifierName` |
| 状态字段 | `statusName` | `rectifyStatusName` |
| 适用场景 | 有 `rectifyId` 时优先 | 仅无 `rectifyId` 时兜底 |
---
## 4. 整改记录字段映射(`applyRectifyData`
接口返回结构不统一,统一在 `applyRectifyData` 中做映射:
| 页面展示字段 | 映射规则 |
|--------------|----------|
| 整改方案/措施/管控/完成情况/费用 | 同名字段直取 |
| 安全管理人员 | 有 `managers[]` → 取 `nickName` 去重;否则用 `managerNames` |
| 整改责任人 | 有 `members[]` → 取 `nickName` 去重;否则用 `memberNames` |
| 完成情况 | `rectifyStatusName``statusName` |
| 整改附件 | `attachments` |
| 整改人(不通过时展示) | `rectifierName` |
### 人员名显示规则(易踩坑)
`rectify/detail` **同时可能返回**
- `memberNames` / `managerNames`身份名阎勇、user1
- `members` / `managers`(对象,含 `nickName`xiaomi、duoduo
**当前规则:有 `members` / `managers` 数组时,优先用其中的 `nickName`,不用 `memberNames`。**
---
## 5. 验收表单交互
### 5.1 验收结果 `formData.result`
| 值 | 含义 |
|----|------|
| `1` | 通过(默认) |
| `2` | 不通过 |
切换时触发 `onResultChange` → 重新请求下一步流程 `fetchNextStep()`
- 切到「不通过」:清空已选下一步处理人
- 切到「通过」:若未选快速审批,默认 `quickApproveRadio = 'yes'`
### 5.2 是否快速审批 `formData.quickApproveRadio`
- **仅验收通过时显示**,必选
- `yes` = 快速审批;`no` = 不快速审批
- 切换时触发 `onQuickApproveChange``fetchNextStep()`
### 5.3 下一步流程(只读预览)
接口:`POST /flow/task/next-nodes`
**三种情况都必须传:**
```json
{
"taskId": "当前任务ID",
"includeSubProcess": true,
"previewVariables": { ... }
}
```
`previewVariables` 按验收选择变化:
| 场景 | previewVariables |
|------|------------------|
| 不通过 | `{ "pass": false }` |
| 通过 + 快速审批 | `{ "pass": true, "quickApprove": true }` |
| 通过 + 不快速审批 | `{ "pass": true, "quickApprove": false }` |
展示逻辑:取返回 `branches``matched === true` 的分支(没有则取第一个)的 `nextNode.taskName`
### 5.4 下一步处理人
| 验收结果 | UI | 数据来源 |
|----------|-----|----------|
| 不通过 | 只读文本 | `rectify/detail`**`rectifierName`**(整改人) |
| 通过 | 底部弹窗单选 | `GET /admin/user/dept/users/{deptId}` |
通过时选人说明:
- `deptId` 来自本地 `userInfo.userIdentity.deptId`(无则取 `userInfo.deptId`
- 部门人员接口返回 `{ userId, nickName }`,可能没有 `identityId`
- 选人 ID 解析:`identityId``userIdentityId``userId`(兜底)
- 提交字段名是 `assigneeIdentityId`,无 `identityId` 时实际传的是 `userId`
### 5.5 电子签名
- 必填,提交前先上传云端得到 `signPath`
- 打开「选择下一步处理人」弹窗时,**卸载签名 Canvas**`v-if="showCanvas && !showAssigneePopup"`),避免微信小程序原生 canvas 层级穿透盖住弹窗
### 5.6 验收图片/视频
- 使用 `up-upload` + 水印 canvas
- 提交时只取 `status === 'success'` 的文件,经 `buildAttachmentItem` 转成附件对象
---
## 6. 提交验收
接口:`POST /frontend/hazard/verify``acceptanceRectification`
### 6.1 提交前校验
1. 必须有 `rectifyId`
2. 通过时:必须选「是否快速审批」
3. 通过 + 不快速审批:必须选下一步处理人
4. 必须有电子签名
### 6.2 请求体
**通用字段(通过/不通过都传):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `rectifyId` | number | 整改记录 ID |
| `result` | number | `1` 通过 / `2` 不通过 |
| `verifyRemark` | string | 验收备注,可为空 |
| `attachments` | array | 验收附件 `[{ fileName, filePath, fileType, fileSize }]` |
| `signPath` | string | 电子签名服务器路径 |
**仅通过时额外传:**
| 字段 | 条件 | 说明 |
|------|------|------|
| `quickApprove` | `result === 1` | boolean |
| `assigneeIdentityId` | `quickApprove === false` | 下一步处理人 ID |
**不提交的内容:**
- 整改记录只读区所有字段
- 下一步流程名称(仅预览)
- 不通过时的 `rectifierName`(仅展示,后端按流程自行处理)
- 快速审批为「是」时的下一步处理人
### 6.3 提交示例
**不通过:**
```json
{
"rectifyId": 150,
"result": 2,
"verifyRemark": "整改不到位",
"attachments": [],
"signPath": "https://oss.../sign.png"
}
```
**通过 + 快速审批:**
```json
{
"rectifyId": 150,
"result": 1,
"verifyRemark": "",
"attachments": [],
"signPath": "https://oss.../sign.png",
"quickApprove": true
}
```
**通过 + 不快速审批:**
```json
{
"rectifyId": 150,
"result": 1,
"verifyRemark": "",
"attachments": [],
"signPath": "https://oss.../sign.png",
"quickApprove": false,
"assigneeIdentityId": "44"
}
```
---
## 7. 草稿缓存
使用 `useDraftCache`,命名空间 `DRAFT_NS.ACCEPT`key 基于 `rectifyId`
**会缓存:**
- 验收结果、备注、快速审批选择
- 下一步处理人选择
- 验收上传附件列表
- 签名相关状态
**不会缓存:**
- 整改记录只读区(每次进页重新拉接口)
恢复草稿后会再次调用 `fetchNextStep()`,保证下一步流程与当前验收选择一致。
---
## 8. 关键函数索引
| 函数 | 作用 |
|------|------|
| `loadPageData` | 页面数据加载入口 |
| `fetchRectifyDetail` | 调整改详情,有 `rectifyId` 时用 |
| `fetchDetail` | 调隐患详情,无 `rectifyId` 时兜底 |
| `applyRectifyData` | 统一映射整改记录到页面 |
| `resolveManagerNames` / `resolveMemberNames` | 人员名映射(优先 members/managers 的 nickName |
| `buildPreviewVariables` | 构建下一步流程预览变量 |
| `fetchNextStep` | 请求下一步流程名称 |
| `onResultChange` / `onQuickApproveChange` | 切换验收选项后刷新流程预览 |
| `openAssigneePopup` / `fetchAssigneeList` | 通过时选择下一步处理人 |
| `validateFormBeforeSubmit` | 提交前表单校验 |
| `handleSubmit` / `executeSubmit` | 签名处理 + 提交验收 |
---
## 9. 关联文件
| 文件 | 关系 |
|------|------|
| `utils/hazardNav.js` | 构建跳转 URL含 taskId |
| `request/api.js` | 接口定义 |
| `utils/upload.js` | 附件上传与 `buildAttachmentItem` |
| `utils/draftCache.js` / `utils/useDraftCache.js` | 草稿 |
| `pages/hiddendanger/rectification.vue` | 签名 canvas 穿透弹窗的同类处理参考 |
---
## 10. 常见问题速查
| 现象 | 可能原因 |
|------|----------|
| 下一步流程显示「暂无」 | URL 未带 `taskId`,且详情接口也解析不到 |
| 人员显示身份名而非 nickName | `members` 数组为空,走了 `memberNames` 兜底 |
| 不通过时处理人不对 | 应检查 `rectify/detail``rectifierName` 是否有值 |
| 选人弹窗被白块挡住 | 签名 canvas 未在弹窗打开时卸载 |
| 选人列表为空 | `userInfo.userIdentity.deptId` 缺失,或部门无人员 |
| 提交了但后端报处理人错误 | `assigneeIdentityId` 传的是 `userId`,需确认后端是否接受 |
---
## 11. 维护建议
1. **有 `rectifyId` 就不要再调 `hazard/detail` 做展示**,避免重复请求和数据覆盖混乱。
2. 改人员展示逻辑时,优先看 `resolveManagerNames` / `resolveMemberNames`,不要直接改模板。
3. 改流程预览时,确认 `taskId``previewVariables``includeSubProcess: true` 三者始终齐全。
4. 新增提交字段时,同步更新本文档第 6 节。

View File

@@ -52,8 +52,8 @@
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="flex" style="gap: 20rpx;"> <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 === 1 ? 'active' : '']" @click="onResultChange(1)">通过</button>
<button :class="['result-btn', formData.result === 2 ? 'active' : '']" @click="formData.result = 2">不通过</button> <button :class="['result-btn', formData.result === 2 ? 'active' : '']" @click="onResultChange(2)">不通过</button>
</view> </view>
<view class="flex margin-bottom margin-top"> <view class="flex margin-bottom margin-top">
<view class="text-gray">验收备注</view> <view class="text-gray">验收备注</view>
@@ -63,6 +63,15 @@
<view class="text-gray">验收图片/视频</view> <view class="text-gray">验收图片/视频</view>
</view> </view>
<up-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple imageMode="aspectFill" :maxCount="10"></up-upload> <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 canvas-id="watermarkCanvas" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'fixed', left: '-9999px', top: '-9999px' }"></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-gray">下一步流程</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="static-field">申请隐患销号</view> <view class="static-field">{{ nextStepDisplay }}</view>
<view class="flex margin-bottom margin-top"> <view class="flex margin-bottom margin-top">
<view class="text-gray">下一步处理人</view> <view class="text-gray">下一步处理人</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</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"> <view class="flex margin-bottom margin-top">
@@ -115,7 +159,8 @@
<text v-else class="signature-placeholder">暂无签名请点击重新签名</text> <text v-else class="signature-placeholder">暂无签名请点击重新签名</text>
</view> </view>
<!-- 改为 v-if 解决小程序原生 canvas 真机渲染与生命周期挂载残留问题 --> <!-- 改为 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 <wd-signature
ref="signatureRef" ref="signatureRef"
:width="signatureWidth" :width="signatureWidth"
@@ -144,9 +189,9 @@
</template> </template>
<script setup> <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 { 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 { toImageUrl } from '@/request/request.js';
import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js'; import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js';
import { useDraftCache } from '@/utils/useDraftCache.js'; import { useDraftCache } from '@/utils/useDraftCache.js';
@@ -161,6 +206,228 @@
const rectifyId = ref(''); const rectifyId = ref('');
const hazardId = ref(''); const hazardId = ref('');
const assignId = 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({ const rectifyData = reactive({
@@ -172,6 +439,7 @@
actualCost: null, actualCost: null,
managerNames: [], managerNames: [],
memberNames: [], memberNames: [],
rectifierName: '',
rectifyStatusName: '' rectifyStatusName: ''
}); });
@@ -204,7 +472,8 @@
// 表单数据 // 表单数据
const formData = reactive({ const formData = reactive({
result: 1, // 验收结果 1.通过 2.不通过 result: 1, // 验收结果 1.通过 2.不通过
verifyRemark: '' // 验收备注 verifyRemark: '', // 验收备注
quickApproveRadio: 'yes' // 是否快速审批 yes/no仅通过时有效
}); });
const fileList1 = ref([]); const fileList1 = ref([]);
@@ -285,8 +554,11 @@
getPayload: () => ({ getPayload: () => ({
formData: { formData: {
result: formData.result, result: formData.result,
verifyRemark: formData.verifyRemark verifyRemark: formData.verifyRemark,
quickApproveRadio: formData.quickApproveRadio
}, },
selectedAssigneeIdentityId: selectedAssigneeIdentityId.value,
selectedAssigneeName: selectedAssigneeName.value,
fileList1: fileList1.value, fileList1: fileList1.value,
signatureServerPath: signatureServerPath.value, signatureServerPath: signatureServerPath.value,
signatureUrl: signatureUrl.value, signatureUrl: signatureUrl.value,
@@ -300,6 +572,9 @@
const form = data.formData || {}; const form = data.formData || {};
formData.result = form.result !== undefined ? form.result : 1; formData.result = form.result !== undefined ? form.result : 1;
formData.verifyRemark = form.verifyRemark || ''; formData.verifyRemark = form.verifyRemark || '';
formData.quickApproveRadio = form.quickApproveRadio === 'no' ? 'no' : 'yes';
selectedAssigneeIdentityId.value = data.selectedAssigneeIdentityId || '';
selectedAssigneeName.value = data.selectedAssigneeName || '';
fileList1.value = data.fileList1 || []; fileList1.value = data.fileList1 || [];
signaturePaths.value = data.signaturePaths || []; signaturePaths.value = data.signaturePaths || [];
if (data.signatureServerPath || data.signatureUrl) { if (data.signatureServerPath || data.signatureUrl) {
@@ -318,6 +593,9 @@
clearForm: () => { clearForm: () => {
formData.result = 1; formData.result = 1;
formData.verifyRemark = ''; formData.verifyRemark = '';
formData.quickApproveRadio = 'yes';
selectedAssigneeIdentityId.value = '';
selectedAssigneeName.value = '';
fileList1.value = []; fileList1.value = [];
signatureServerPath.value = ''; signatureServerPath.value = '';
signatureUrl.value = ''; signatureUrl.value = '';
@@ -332,6 +610,7 @@
canSave: () => !!rectifyId.value, canSave: () => !!rectifyId.value,
requireInitialized: true, requireInitialized: true,
onAfterRestore: (data) => { onAfterRestore: (data) => {
fetchNextStep();
if (data.signatureServerPath || data.signatureUrl || data.signatureLocalPath) { if (data.signatureServerPath || data.signatureUrl || data.signatureLocalPath) {
return; return;
} }
@@ -348,6 +627,9 @@
bindAutoSave(() => [ bindAutoSave(() => [
formData.result, formData.result,
formData.verifyRemark, formData.verifyRemark,
formData.quickApproveRadio,
selectedAssigneeIdentityId.value,
selectedAssigneeName.value,
fileList1.value, fileList1.value,
signatureServerPath.value, signatureServerPath.value,
signatureUrl.value, signatureUrl.value,
@@ -375,22 +657,26 @@
}); });
}; };
// 从 assigns 中找到包含整改记录的那一项 const mapPersonListToNickNames = (people) => {
const resolveAssignWithRectify = (assigns) => { if (!Array.isArray(people) || people.length === 0) return [];
if (!assigns?.length) return null; const names = people
if (rectifyId.value) { .map((person) => person.nickName || person.userName || person.name || '')
const byRectifyId = assigns.find( .filter(Boolean);
(item) => item.rectify && String(item.rectify.rectifyId) === String(rectifyId.value) return [...new Set(names)];
); };
if (byRectifyId) return byRectifyId;
const resolveManagerNames = (rectify) => {
if (Array.isArray(rectify.managers) && rectify.managers.length > 0) {
return mapPersonListToNickNames(rectify.managers);
} }
if (assignId.value) { return rectify.managerNames || [];
const byAssignId = assigns.find( };
(item) => String(item.assignId) === String(assignId.value) && item.rectify
); const resolveMemberNames = (rectify) => {
if (byAssignId) return byAssignId; 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) => { const applyRectifyData = (rectify) => {
@@ -401,13 +687,41 @@
rectifyData.rectifyResult = rectify.rectifyResult || ''; rectifyData.rectifyResult = rectify.rectifyResult || '';
rectifyData.planCost = rectify.planCost ?? null; rectifyData.planCost = rectify.planCost ?? null;
rectifyData.actualCost = rectify.actualCost ?? null; rectifyData.actualCost = rectify.actualCost ?? null;
rectifyData.managerNames = rectify.managerNames || []; rectifyData.managerNames = resolveManagerNames(rectify);
rectifyData.memberNames = rectify.memberNames || []; rectifyData.memberNames = resolveMemberNames(rectify);
rectifyData.rectifyStatusName = rectify.rectifyStatusName || ''; rectifyData.rectifierName = rectify.rectifierName || '';
rectifyData.rectifyStatusName = rectify.rectifyStatusName || rectify.statusName || '';
rectifyAttachments.value = rectify.attachments || []; 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 () => { const fetchDetail = async () => {
if (!hazardId.value) return; if (!hazardId.value) return;
@@ -420,8 +734,15 @@
const assign = resolveAssignWithRectify(res.data.assigns); const assign = resolveAssignWithRectify(res.data.assigns);
if (assign?.rectify) { if (assign?.rectify) {
applyRectifyData(assign.rectify); applyRectifyData(assign.rectify);
console.log('整改记录:', rectifyData); if (!rectifyId.value && assign.rectify.rectifyId) {
console.log('整改附件:', rectifyAttachments.value); 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 { } else {
uni.showToast({ title: res.msg || '获取详情失败', icon: 'none' }); 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 = () => { const handleCancel = () => {
uni.navigateBack(); uni.navigateBack();
@@ -447,6 +789,10 @@
return; return;
} }
if (!validateFormBeforeSubmit()) {
return;
}
// 电子签名验证与处理 // 电子签名验证与处理
if (showCanvas.value) { if (showCanvas.value) {
if (!signatureRef.value || isSignatureEmpty.value) { if (!signatureRef.value || isSignatureEmpty.value) {
@@ -502,6 +848,13 @@
signPath: signatureServerPath.value || '' // 电子签名路径 signPath: signatureServerPath.value || '' // 电子签名路径
// sendMsgFlag: sendMsgFlag.value // sendMsgFlag: sendMsgFlag.value
}; };
if (formData.result === 1) {
params.quickApprove = formData.quickApproveRadio === 'yes';
if (formData.quickApproveRadio === 'no') {
params.assigneeIdentityId = selectedAssigneeIdentityId.value;
}
}
console.log('提交验收参数:', params); console.log('提交验收参数:', params);
@@ -664,10 +1017,12 @@
if (options.assignId) { if (options.assignId) {
assignId.value = 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 });
// 获取隐患详情 loadPageData();
fetchDetail();
// 页面打开时自动恢复草稿 // 页面打开时自动恢复草稿
restoreDraft(); restoreDraft();
@@ -710,6 +1065,117 @@
line-height: 1.5; 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 { .signature-box {
width: 100%; width: 100%;

View File

@@ -13,7 +13,7 @@
<view class="text-gray">下一步流程</view> <view class="text-gray">下一步流程</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="static-field">隐患整改</view> <view class="static-field">{{ nextStepDisplay }}</view>
<view class="flex margin-bottom margin-top"> <view class="flex margin-bottom margin-top">
<view class="text-gray">整改责任人</view> <view class="text-gray">整改责任人</view>
@@ -33,7 +33,7 @@
<view class="popup-title text-bold">选择整改责任人</view> <view class="popup-title text-bold">选择整改责任人</view>
<view class="popup-close" @click="cancelUserSelect">×</view> <view class="popup-close" @click="cancelUserSelect">×</view>
</view> </view>
<view v-if="userPickerSelectedId" class="selected-summary"> <view v-if="userPickerSelectedIdentityId" class="selected-summary">
<text class="summary-label">已选</text> <text class="summary-label">已选</text>
<text class="summary-text">{{ userPickerSelectedText }}</text> <text class="summary-text">{{ userPickerSelectedText }}</text>
</view> </view>
@@ -55,12 +55,12 @@
<view <view
class="user-item" class="user-item"
v-for="user in currentDeptUsers" v-for="user in currentDeptUsers"
:key="'user-' + user.userId" :key="'identity-' + user.identityId"
:class="{ active: String(userPickerSelectedId) === String(user.userId) }" :class="{ active: String(userPickerSelectedIdentityId) === String(user.identityId) }"
@click="onUserItemClick(user.userId)" @click="onUserItemClick(user.identityId)"
> >
<text class="user-item-text">{{ formatUserDisplayName(user) }}</text> <text class="user-item-text">{{ formatUserDisplayName(user) }}</text>
<text v-if="String(userPickerSelectedId) === String(user.userId)" class="cuIcon-check text-blue"></text> <text v-if="String(userPickerSelectedIdentityId) === String(user.identityId)" class="cuIcon-check text-blue"></text>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -120,16 +120,32 @@
// 页面参数 // 页面参数
const hazardId = ref(''); const hazardId = ref('');
const assignId = ref(''); const assignId = ref('');
const taskId = ref('');
// 整改人员选择
// 下一步流程(固定为隐患整改)
const nextStepDisplay = '隐患整改';
// 整改人员选择(按身份 identityId 单选)
const showUserPopup = ref(false); const showUserPopup = ref(false);
const selectedUser = ref(''); const selectedUser = ref('');
const selectedIdentityId = ref('');
const selectedUserId = ref(''); const selectedUserId = ref('');
const deptList = ref([]); const deptList = ref([]);
const activeDeptIndex = ref(0); const activeDeptIndex = ref(0);
const userPickerSelectedId = ref(''); const userPickerSelectedIdentityId = ref('');
const findUserByIdentityId = (identityId) => {
if (!identityId) return null;
for (const dept of deptList.value) {
const user = (dept.users || []).find((u) => String(u.identityId) === String(identityId));
if (user) return user;
}
return null;
};
const formatUserDisplayName = (user) => { const formatUserDisplayName = (user) => {
if (user.identityName) {
return `${user.nickName}_${user.identityName}`;
}
if (user.postName) { if (user.postName) {
return `${user.nickName}_${user.postName}`; return `${user.nickName}_${user.postName}`;
} }
@@ -142,27 +158,32 @@
}); });
const userPickerSelectedText = computed(() => { const userPickerSelectedText = computed(() => {
if (!userPickerSelectedId.value) return ''; const user = findUserByIdentityId(userPickerSelectedIdentityId.value);
for (const dept of deptList.value) { return user ? formatUserDisplayName(user) : '';
const user = (dept.users || []).find((u) => String(u.userId) === String(userPickerSelectedId.value));
if (user) return formatUserDisplayName(user);
}
return '';
}); });
const deptHasSelectedUser = (dept) => { const deptHasSelectedUser = (dept) => {
if (!userPickerSelectedId.value || !dept.users?.length) return false; if (!userPickerSelectedIdentityId.value || !dept.users?.length) return false;
return dept.users.some((user) => String(user.userId) === String(userPickerSelectedId.value)); return dept.users.some((user) => String(user.identityId) === String(userPickerSelectedIdentityId.value));
}; };
const onUserItemClick = (userId) => { const onUserItemClick = (identityId) => {
userPickerSelectedId.value = String(userId); userPickerSelectedIdentityId.value = String(identityId);
}; };
const openUserPopup = () => { const openUserPopup = () => {
userPickerSelectedId.value = selectedUserId.value; userPickerSelectedIdentityId.value = selectedIdentityId.value;
const firstDeptWithUsers = deptList.value.findIndex((dept) => dept.users?.length > 0); let targetDeptIndex = 0;
activeDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0; if (selectedIdentityId.value) {
const deptIndex = deptList.value.findIndex((dept) =>
(dept.users || []).some((user) => String(user.identityId) === String(selectedIdentityId.value))
);
if (deptIndex >= 0) targetDeptIndex = deptIndex;
} else {
const firstDeptWithUsers = deptList.value.findIndex((dept) => dept.users?.length > 0);
if (firstDeptWithUsers >= 0) targetDeptIndex = firstDeptWithUsers;
}
activeDeptIndex.value = targetDeptIndex;
showUserPopup.value = true; showUserPopup.value = true;
}; };
@@ -171,12 +192,18 @@
}; };
const confirmUserSelect = () => { const confirmUserSelect = () => {
if (!userPickerSelectedId.value) { if (!userPickerSelectedIdentityId.value) {
uni.showToast({ title: '请选择整改责任人', icon: 'none' }); uni.showToast({ title: '请选择整改责任人', icon: 'none' });
return; return;
} }
selectedUserId.value = String(userPickerSelectedId.value); const user = findUserByIdentityId(userPickerSelectedIdentityId.value);
selectedUser.value = userPickerSelectedText.value; if (!user) {
uni.showToast({ title: '所选身份无效,请重新选择', icon: 'none' });
return;
}
selectedIdentityId.value = String(user.identityId);
selectedUserId.value = String(user.userId);
selectedUser.value = formatUserDisplayName(user);
showUserPopup.value = false; showUserPopup.value = false;
}; };
@@ -251,7 +278,7 @@
// 确认提交 // 确认提交
const handleSubmit = async () => { const handleSubmit = async () => {
if (!selectedUserId.value) { if (!selectedIdentityId.value) {
uni.showToast({ title: '请选择整改人员', icon: 'none' }); uni.showToast({ title: '请选择整改人员', icon: 'none' });
return; return;
} }
@@ -259,11 +286,17 @@
uni.showToast({ title: '请选择整改期限', icon: 'none' }); uni.showToast({ title: '请选择整改期限', icon: 'none' });
return; return;
} }
if (!taskId.value) {
uni.showToast({ title: '缺少任务ID', icon: 'none' });
return;
}
// 构建请求参数 // 构建请求参数
const params = { const params = {
hazardId: Number(hazardId.value), // 隐患ID hazardId: Number(hazardId.value), // 隐患ID
assigneeId: Number(selectedUserId.value), // 被指派人ID taskId: taskId.value, // 流程任务ID列表传入
assigneeId: Number(selectedUserId.value), // 被指派人用户ID
assigneeIdentityId: Number(selectedIdentityId.value), // 被指派人身份ID
deadline: selectedDate.value, // 处理期限 deadline: selectedDate.value, // 处理期限
assignRemark: '', // 交办备注(可选) assignRemark: '', // 交办备注(可选)
sendMsgFlag: sendMsgFlag.value // 是否短信提醒 sendMsgFlag: sendMsgFlag.value // 是否短信提醒
@@ -291,6 +324,7 @@
onLoad((options) => { onLoad((options) => {
if (options.hazardId) hazardId.value = options.hazardId; if (options.hazardId) hazardId.value = options.hazardId;
if (options.assignId) assignId.value = options.assignId; if (options.assignId) assignId.value = options.assignId;
if (options.taskId) taskId.value = options.taskId;
fetchDeptUsers(); fetchDeptUsers();
// 恢复草稿 (不恢复任何人员选择器数据) // 恢复草稿 (不恢复任何人员选择器数据)

View File

@@ -0,0 +1,247 @@
<template>
<view class="page">
<view class="top-gradient-wrap">
<u-navbar
title="查看隐患"
:placeholder="true"
:safeAreaInsetTop="true"
bgColor="transparent"
titleColor="#ffffff"
leftIconColor="#ffffff"
:autoBack="true"
:border="false"
/>
<view class="summary-card">
<view class="summary-side summary-side--left">
<image class="summary-icon" src="/static/yinhuan_detail/status.png" mode="aspectFit" />
<view class="summary-icon-gap">
<view class="summary-text">
<view class="summary-label">隐患状态</view>
<view class="summary-status">{{ summary.statusName }}</view>
</view>
</view>
</view>
<view class="summary-divider"></view>
<view class="summary-side summary-side--right">
<image class="summary-icon" src="/static/yinhuan_detail/date.png" mode="aspectFit" />
<view class="summary-text">
<view class="summary-label">提交日期</view>
<view class="summary-date">{{ summary.createdAt }}</view>
</view>
</view>
</view>
</view>
<view class="panel-wrap">
<HazardProcessChainPanel
:chain-data="chainData"
:loading="loading"
:body-height="panelBodyHeight"
/>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick, getCurrentInstance } from 'vue';
import { onLoad, onReady } from '@dcloudio/uni-app';
import HazardProcessChainPanel from '@/components/hazardDetail/HazardProcessChainPanel.vue';
import { getHazardProcessChain } from '@/request/api.js';
import { resolveProcessChainSummary } from '@/components/hazardDetail/processChain.js';
const instance = getCurrentInstance();
const queryScope = instance?.proxy || instance;
const chainData = ref({});
const loading = ref(false);
const panelBodyHeight = ref(0);
const summary = computed(() => resolveProcessChainSummary(chainData.value));
const calcPanelBodyHeight = () => {
nextTick(() => {
const query = uni.createSelectorQuery().in(queryScope);
query.select('.panel-wrap').boundingClientRect();
query.exec((res) => {
const rect = res?.[0];
if (rect?.height > 0) {
panelBodyHeight.value = Math.floor(rect.height);
return;
}
const sys = uni.getSystemInfoSync();
panelBodyHeight.value = Math.floor(sys.windowHeight * 0.55);
});
});
};
const loadProcessChain = async (hazardId) => {
loading.value = true;
try {
const res = await getHazardProcessChain(hazardId);
if (res.code === 0 && res.data) {
chainData.value = res.data;
} else {
chainData.value = {};
uni.showToast({ title: res.msg || '获取流程链失败', icon: 'none' });
}
} catch (error) {
console.error('获取隐患流程链失败:', error);
chainData.value = {};
uni.showToast({ title: '获取流程链失败', icon: 'none' });
} finally {
loading.value = false;
calcPanelBodyHeight();
setTimeout(calcPanelBodyHeight, 100);
setTimeout(calcPanelBodyHeight, 400);
}
};
onReady(() => {
calcPanelBodyHeight();
setTimeout(calcPanelBodyHeight, 100);
setTimeout(calcPanelBodyHeight, 400);
});
watch(loading, (isLoading) => {
if (!isLoading) {
calcPanelBodyHeight();
setTimeout(calcPanelBodyHeight, 100);
setTimeout(calcPanelBodyHeight, 400);
}
});
onLoad((options) => {
if (options.hazardId) {
loadProcessChain(options.hazardId);
return;
}
uni.showToast({ title: '缺少隐患ID', icon: 'none' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
});
</script>
<style lang="scss" scoped>
.page {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #f5f7fa;
}
.top-gradient-wrap {
flex-shrink: 0;
background: linear-gradient(180deg, #046CEA 0%, #2158C8 28.44%, rgba(4, 107, 234, 0) 100%);
}
.summary-card {
margin: 32rpx 30rpx 0;
padding: 28rpx 30rpx 32rpx;
background: #fff;
border-radius: 20rpx;
display: flex;
align-items: flex-start;
box-sizing: border-box;
}
.summary-side {
display: flex;
align-items: flex-start;
}
.summary-side--left {
flex-shrink: 0;
align-items: center;
}
.summary-side--left .summary-text {
padding-top: 0;
padding-bottom: 0;
}
.summary-side--right {
flex: 1;
min-width: 0;
align-items: center;
}
.summary-side--right .summary-icon {
margin-right: 22rpx;
}
.summary-side--right .summary-text {
flex: 1;
min-width: 0;
padding-top: 0;
padding-bottom: 0;
}
.summary-icon-gap {
width: 149rpx;
flex-shrink: 0;
box-sizing: border-box;
}
.summary-icon {
width: 55rpx;
height: 65rpx;
flex-shrink: 0;
}
.summary-side--left .summary-icon {
margin-right: 22rpx;
}
.summary-text {
flex-shrink: 0;
box-sizing: border-box;
}
.summary-label {
font-size: 24rpx;
color: #8f9ca2;
line-height: 34rpx;
}
.summary-status {
margin-top: 8rpx;
font-size: 28rpx;
font-weight: 400;
color: #333333;
line-height: 40rpx;
}
.summary-date {
margin-top: 8rpx;
font-size: 28rpx;
font-weight: 400;
color: #333333;
line-height: 40rpx;
white-space: nowrap;
}
.summary-divider {
width: 2rpx;
height: 72rpx;
flex-shrink: 0;
background: #eee;
margin-right: 40rpx;
}
.panel-wrap {
flex: 1;
min-height: 0;
height: 0;
margin-top: 12rpx;
padding: 0 30rpx 30rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>

View File

@@ -100,8 +100,8 @@
<view class="popup-title text-bold">选择安全管理人员</view> <view class="popup-title text-bold">选择安全管理人员</view>
<view class="popup-close" @click="cancelManagerSelect">×</view> <view class="popup-close" @click="cancelManagerSelect">×</view>
</view> </view>
<view v-if="managerPickerSelectedIds.length > 0" class="selected-summary"> <view v-if="managerPickerSelectedIdentityIds.length > 0" class="selected-summary">
<text class="summary-label">已选 {{ managerPickerSelectedIds.length }} </text> <text class="summary-label">已选 {{ managerPickerSelectedIdentityIds.length }} </text>
<text class="summary-text">{{ managerPickerSelectedText }}</text> <text class="summary-text">{{ managerPickerSelectedText }}</text>
</view> </view>
<view class="cascader-body"> <view class="cascader-body">
@@ -119,14 +119,14 @@
<scroll-view class="cascader-col user-col" scroll-y :key="'manager-dept-users-' + activeManagerDeptIndex"> <scroll-view class="cascader-col user-col" scroll-y :key="'manager-dept-users-' + activeManagerDeptIndex">
<view v-if="currentManagerDeptUsers.length === 0" class="empty-tip">该部门暂无人员</view> <view v-if="currentManagerDeptUsers.length === 0" class="empty-tip">该部门暂无人员</view>
<view v-else> <view v-else>
<view class="user-item" v-for="user in currentManagerDeptUsers" :key="'manager-user-' + user.userId"> <view class="user-item" v-for="user in currentManagerDeptUsers" :key="'manager-identity-' + user.identityId">
<up-checkbox <up-checkbox
usedAlone usedAlone
:checked="managerPickerSelectedSet.has(String(user.userId))" :checked="managerPickerSelectedSet.has(String(user.identityId))"
:label="formatUserDisplayName(user)" :label="formatMemberDisplayName(user)"
activeColor="#2667E9" activeColor="#2667E9"
shape="square" shape="square"
@change="(checked) => onManagerCheckChange(user.userId, checked)" @change="(checked) => onManagerCheckChange(user.identityId, checked)"
></up-checkbox> ></up-checkbox>
</view> </view>
</view> </view>
@@ -146,8 +146,8 @@
<view class="popup-title text-bold">选择整改责任人</view> <view class="popup-title text-bold">选择整改责任人</view>
<view class="popup-close" @click="cancelUserSelect">×</view> <view class="popup-close" @click="cancelUserSelect">×</view>
</view> </view>
<view v-if="userPickerSelectedIds.length > 0" class="selected-summary"> <view v-if="userPickerSelectedIdentityIds.length > 0" class="selected-summary">
<text class="summary-label">已选 {{ userPickerSelectedIds.length }} </text> <text class="summary-label">已选 {{ userPickerSelectedIdentityIds.length }} </text>
<text class="summary-text">{{ userPickerSelectedText }}</text> <text class="summary-text">{{ userPickerSelectedText }}</text>
</view> </view>
<view class="cascader-body"> <view class="cascader-body">
@@ -165,15 +165,15 @@
<scroll-view class="cascader-col user-col" scroll-y :key="'dept-users-' + activeDeptIndex"> <scroll-view class="cascader-col user-col" scroll-y :key="'dept-users-' + activeDeptIndex">
<view v-if="currentDeptUsers.length === 0" class="empty-tip">该部门暂无人员</view> <view v-if="currentDeptUsers.length === 0" class="empty-tip">该部门暂无人员</view>
<view v-else> <view v-else>
<view class="user-item" v-for="user in currentDeptUsers" :key="'user-' + user.userId" :class="{ 'user-item-locked': isLockedUser(user.userId) }"> <view class="user-item" v-for="user in currentDeptUsers" :key="'identity-' + user.identityId" :class="{ 'user-item-locked': isLockedIdentity(user.identityId) }">
<up-checkbox <up-checkbox
usedAlone usedAlone
:checked="userPickerSelectedSet.has(String(user.userId))" :checked="userPickerSelectedSet.has(String(user.identityId))"
:label="formatUserPickerLabel(user)" :label="formatUserPickerLabel(user)"
:disabled="isLockedUser(user.userId)" :disabled="isLockedIdentity(user.identityId)"
activeColor="#2667E9" activeColor="#2667E9"
shape="square" shape="square"
@change="(checked) => onUserCheckChange(user.userId, checked)" @change="(checked) => onUserCheckChange(user.identityId, checked)"
></up-checkbox> ></up-checkbox>
</view> </view>
</view> </view>
@@ -212,13 +212,13 @@
<view class="text-gray">下一步流程</view> <view class="text-gray">下一步流程</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="static-field">隐患验收</view> <view class="static-field">{{ nextStepDisplay }}</view>
<view class="form-label margin-bottom margin-top"> <view class="form-label margin-bottom margin-top">
<view class="text-gray">下一步处理人</view> <view class="text-gray">下一步处理人</view>
<view class="text-red">*</view> <view class="text-red">*</view>
</view> </view>
<view class="static-field">企业管理员</view> <view class="static-field">管理</view>
<view class="form-label margin-bottom margin-top"> <view class="form-label margin-bottom margin-top">
<view class="text-gray">短信提醒</view> <view class="text-gray">短信提醒</view>
@@ -289,11 +289,13 @@
import {onLoad, onHide} from '@dcloudio/uni-app' import {onLoad, onHide} from '@dcloudio/uni-app'
import { import {
submitRectification, submitRectification,
updateRectification,
// getDepartmentPersonUsers, // getDepartmentPersonUsers,
// getDeptUsersWithSubordinates, // getDeptUsersWithSubordinates,
getRelatedDeptUsers, getRelatedDeptUsers,
getRectifyDetail, getRectifyDetail,
getHiddenDangerDetail, getHiddenDangerDetail,
getFlowNextNodes,
generateRectifyPlan generateRectifyPlan
} from '@/request/api.js' } from '@/request/api.js'
import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js'; import { buildDraftKey, buildDraftKeyCompact, DRAFT_NS } from '@/utils/draftCache.js';
@@ -309,9 +311,91 @@
// 从页面参数获取的ID // 从页面参数获取的ID
const hazardId = ref(''); const hazardId = ref('');
const assignId = ref(''); const assignId = ref('');
const taskId = ref('');
const rectifyId = ref(''); // 整改ID编辑模式时使用 const rectifyId = ref(''); // 整改ID编辑模式时使用
const isEdit = ref(false); // 是否为编辑模式 const isEdit = ref(false); // 是否为编辑模式
// 下一步流程(编辑模式写死展示,新建整改走接口)
const EDIT_NEXT_STEP_NAME = '隐患验收';
const nextStepName = ref('');
const nextStepLoading = ref(false);
const nextStepDisplay = computed(() => {
if (isEdit.value) return EDIT_NEXT_STEP_NAME;
if (nextStepLoading.value) return '加载中...';
return nextStepName.value || '暂无下一步流程';
});
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 || [];
for (const assign of assigns) {
if (assign?.taskId) return String(assign.taskId);
}
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 fetchNextStep = async () => {
nextStepLoading.value = true;
try {
let currentTaskId = taskId.value;
if (!currentTaskId && rectifyId.value) {
const rectifyRes = await getRectifyDetail({ rectifyId: rectifyId.value });
if (rectifyRes.code === 0 && rectifyRes.data) {
const rectifyData = rectifyRes.data;
currentTaskId = resolveTaskIdFromDetail(rectifyData);
if (!hazardId.value && rectifyData.hazardId) {
hazardId.value = rectifyData.hazardId;
}
if (!assignId.value && rectifyData.assignId) {
assignId.value = rectifyData.assignId;
}
if (currentTaskId) {
taskId.value = currentTaskId;
}
}
}
if (!currentTaskId && hazardId.value) {
const detailRes = await getHiddenDangerDetail({
hazardId: hazardId.value,
assignId: assignId.value
});
if (detailRes.code === 0 && detailRes.data) {
currentTaskId = resolveTaskIdFromDetail(detailRes.data);
if (currentTaskId) {
taskId.value = currentTaskId;
}
}
}
if (!currentTaskId) {
nextStepName.value = '';
return;
}
const res = await getFlowNextNodes({ taskId: currentTaskId });
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;
}
};
// 防作弊时间戳水印 Canvas 大小配置 // 防作弊时间戳水印 Canvas 大小配置
const canvasWidth = ref(300); const canvasWidth = ref(300);
const canvasHeight = ref(300); const canvasHeight = ref(300);
@@ -492,31 +576,32 @@
} }
}; };
const applyAssigneeFromOptions = (assigneeId, assigneeName) => { const applyAssigneeFromOptions = (assigneeId, assigneeName, assigneeIdentityId) => {
if (!assigneeId) return; if (!assigneeIdentityId) return;
const id = String(assigneeId); const identityId = String(assigneeIdentityId);
const name = assigneeName ? decodeURIComponent(String(assigneeName)).trim() : ''; const name = assigneeName ? decodeURIComponent(String(assigneeName)).trim() : '';
const exists = detailPersonPool.value.some((user) => String(user.userId) === id); const exists = detailPersonPool.value.some((user) => String(user.identityId) === identityId);
if (!exists) { if (!exists) {
detailPersonPool.value.push({ detailPersonPool.value.push({
userId: assigneeId, userId: assigneeId || '',
identityId: assigneeIdentityId,
nickName: name, nickName: name,
deptName: '' deptName: ''
}); });
} }
if (!lockedUserIds.value.includes(id)) { if (!lockedIdentityIds.value.includes(identityId)) {
lockedUserIds.value = [...lockedUserIds.value, id]; lockedIdentityIds.value = [...lockedIdentityIds.value, identityId];
} }
selectedUserIds.value = mergeLockedUserIds([id]); selectedIdentityIds.value = mergeLockedIdentityIds([identityId]);
syncSelectedUsersFromIds(selectedUserIds.value); syncSelectedMembersFromIdentityIds(selectedIdentityIds.value);
}; };
const setLockedDefaultUsers = (ids) => { const setLockedDefaultIdentities = (ids) => {
const normalized = parseIdList(ids); const normalized = parseIdList(ids);
if (!normalized.length) return; if (!normalized.length) return;
lockedUserIds.value = normalized; lockedIdentityIds.value = normalized;
selectedUserIds.value = mergeLockedUserIds(selectedUserIds.value); selectedIdentityIds.value = mergeLockedIdentityIds(selectedIdentityIds.value);
syncSelectedUsersFromIds(selectedUserIds.value); syncSelectedMembersFromIdentityIds(selectedIdentityIds.value);
}; };
const parseIdList = (raw) => { const parseIdList = (raw) => {
@@ -527,42 +612,40 @@
return String(raw).split(',').map((id) => String(id).trim()).filter(Boolean); return String(raw).split(',').map((id) => String(id).trim()).filter(Boolean);
}; };
const resolveManagerIdsFromDetail = (data) => { const resolveManagerIdentityIdsFromDetail = (data) => {
const ids = parseIdList(data.manageIds ?? data.managerIds); const ids = parseIdList(data.managerIds ?? data.manageIds ?? data.manageIdentityIds ?? data.managerIdentityIds);
if (ids.length > 0) return ids; if (ids.length > 0) return ids;
if (Array.isArray(data.managers) && data.managers.length > 0) { if (Array.isArray(data.managers) && data.managers.length > 0) {
return data.managers.map((item) => String(item.userId)).filter(Boolean); return data.managers
.map((item) => String(item.identityId ?? item.userId))
.filter(Boolean);
} }
return []; return [];
}; };
const buildUserItemFromDetail = (user) => ({ const getMembersByIdentityIdsFromTree = (ids, tree) => {
id: String(user.userId), const memberMap = new Map();
name: formatUserDisplayName(user),
deptName: user.deptName || ''
});
const getUsersByIdsFromTree = (ids, tree) => {
const userMap = new Map();
(tree || []).forEach((dept) => { (tree || []).forEach((dept) => {
(dept.users || []).forEach((user) => { (dept.users || []).forEach((user) => {
userMap.set(String(user.userId), buildUserItem(user, dept)); if (user.identityId != null && user.identityId !== '') {
memberMap.set(String(user.identityId), buildMemberItem(user, dept));
}
}); });
}); });
return ids.map((id) => userMap.get(String(id))).filter(Boolean); return ids.map((id) => memberMap.get(String(id))).filter(Boolean);
}; };
const mergeUsersFromDetailPool = (ids, resolvedUsers) => { const mergeMembersFromDetailPool = (ids, resolvedMembers) => {
const userMap = new Map(resolvedUsers.map((user) => [user.id, user])); const memberMap = new Map(resolvedMembers.map((member) => [member.id, member]));
ids.forEach((id) => { ids.forEach((id) => {
const key = String(id); const key = String(id);
if (userMap.has(key)) return; if (memberMap.has(key)) return;
const found = detailPersonPool.value.find((user) => String(user.userId) === key); const found = detailPersonPool.value.find((user) => String(user.identityId) === key);
if (found) { if (found) {
userMap.set(key, buildUserItemFromDetail(found)); memberMap.set(key, buildMemberItemFromDetail(found));
} }
}); });
return ids.map((id) => userMap.get(String(id))).filter(Boolean); return ids.map((id) => memberMap.get(String(id))).filter(Boolean);
}; };
const applyRectifyTimeValue = (timeStr) => { const applyRectifyTimeValue = (timeStr) => {
@@ -591,45 +674,55 @@
const deptList = ref([]) const deptList = ref([])
const detailPersonPool = ref([]) const detailPersonPool = ref([])
const showManagerPopup = ref(false) const showManagerPopup = ref(false)
const selectedManagerIds = ref([]) const selectedManagerIdentityIds = ref([])
const selectedManagers = ref([]) const selectedManagers = ref([])
const activeManagerDeptIndex = ref(0) const activeManagerDeptIndex = ref(0)
const managerPickerSelectedIds = ref([]) const managerPickerSelectedIdentityIds = ref([])
const showUserPopup = ref(false) const showUserPopup = ref(false)
const selectedUserIds = ref([]) const selectedIdentityIds = ref([])
const selectedUsers = ref([]) const selectedUsers = ref([])
const lockedUserIds = ref([]) const lockedIdentityIds = ref([])
const activeDeptIndex = ref(0) const activeDeptIndex = ref(0)
const userPickerSelectedIds = ref([]) const userPickerSelectedIdentityIds = ref([])
const isLockedUser = (userId) => lockedUserIds.value.includes(String(userId)); const isLockedIdentity = (identityId) => lockedIdentityIds.value.includes(String(identityId));
const mergeLockedUserIds = (ids) => { const mergeLockedIdentityIds = (ids) => {
const merged = new Set([ const merged = new Set([
...(ids || []).map((id) => String(id)), ...(ids || []).map((id) => String(id)),
...lockedUserIds.value.map((id) => String(id)) ...lockedIdentityIds.value.map((id) => String(id))
]); ]);
return [...merged]; return [...merged];
}; };
const formatUserPickerLabel = (user) => { const formatMemberDisplayName = (user) => {
const name = formatUserDisplayName(user); if (user.identityName) {
return isLockedUser(user.userId) ? `${name}(默认)` : name; return `${user.nickName}_${user.identityName}`;
}; }
const formatUserDisplayName = (user) => {
if (user.postName) { if (user.postName) {
return `${user.nickName}_${user.postName}`; return `${user.nickName}_${user.postName}`;
} }
return user.nickName || ''; return user.nickName || '';
}; };
const buildUserItem = (user, dept) => ({ const formatUserPickerLabel = (user) => {
id: String(user.userId), const name = formatMemberDisplayName(user);
name: formatUserDisplayName(user), return isLockedIdentity(user.identityId) ? `${name}(默认)` : name;
};
const buildMemberItem = (user, dept) => ({
id: String(user.identityId),
userId: user.userId,
name: formatMemberDisplayName(user),
deptName: dept.deptName deptName: dept.deptName
}); });
const buildMemberItemFromDetail = (user) => ({
id: String(user.identityId || user.userId),
name: formatMemberDisplayName(user),
deptName: user.deptName || ''
});
const buildSelectedPersonText = (users) => { const buildSelectedPersonText = (users) => {
if (users.length === 0) return ''; if (users.length === 0) return '';
if (users.length <= 2) { if (users.length <= 2) {
@@ -647,12 +740,12 @@
}); });
const managerPickerSelectedText = computed(() => { const managerPickerSelectedText = computed(() => {
const users = getManagerUsersByIds(managerPickerSelectedIds.value); const users = getManagersByIdentityIds(managerPickerSelectedIdentityIds.value);
return buildSelectedPersonText(users); return buildSelectedPersonText(users);
}); });
const managerPickerSelectedSet = computed(() => { const managerPickerSelectedSet = computed(() => {
return new Set(managerPickerSelectedIds.value.map((id) => String(id))); return new Set(managerPickerSelectedIdentityIds.value.map((id) => String(id)));
}); });
const currentDeptUsers = computed(() => { const currentDeptUsers = computed(() => {
@@ -661,80 +754,80 @@
}); });
const userPickerSelectedText = computed(() => { const userPickerSelectedText = computed(() => {
const users = getUsersByIds(userPickerSelectedIds.value); const users = getMembersByIdentityIds(userPickerSelectedIdentityIds.value);
return buildSelectedPersonText(users); return buildSelectedPersonText(users);
}); });
const userPickerSelectedSet = computed(() => { const userPickerSelectedSet = computed(() => {
return new Set(userPickerSelectedIds.value.map((id) => String(id))); return new Set(userPickerSelectedIdentityIds.value.map((id) => String(id)));
}); });
const getManagerUsersByIds = (ids) => { const getManagersByIdentityIds = (ids) => {
let users = getUsersByIdsFromTree(ids, managerDeptList.value); let members = getMembersByIdentityIdsFromTree(ids, managerDeptList.value);
if (users.length < ids.length) { if (members.length < ids.length) {
const userDeptUsers = getUsersByIdsFromTree(ids, deptList.value); const deptMembers = getMembersByIdentityIdsFromTree(ids, deptList.value);
const userMap = new Map(users.map((user) => [user.id, user])); const memberMap = new Map(members.map((member) => [member.id, member]));
userDeptUsers.forEach((user) => { deptMembers.forEach((member) => {
if (!userMap.has(user.id)) userMap.set(user.id, user); if (!memberMap.has(member.id)) memberMap.set(member.id, member);
}); });
users = ids.map((id) => userMap.get(String(id))).filter(Boolean); members = ids.map((id) => memberMap.get(String(id))).filter(Boolean);
} }
return mergeUsersFromDetailPool(ids, users); return mergeMembersFromDetailPool(ids, members);
}; };
const getUsersByIds = (ids) => { const getMembersByIdentityIds = (ids) => {
let users = getUsersByIdsFromTree(ids, deptList.value); let members = getMembersByIdentityIdsFromTree(ids, deptList.value);
return mergeUsersFromDetailPool(ids, users); return mergeMembersFromDetailPool(ids, members);
}; };
const syncSelectedManagersFromIds = (ids) => { const syncSelectedManagersFromIdentityIds = (ids) => {
selectedManagers.value = getManagerUsersByIds(ids); selectedManagers.value = getManagersByIdentityIds(ids);
}; };
const syncSelectedUsersFromIds = (ids) => { const syncSelectedMembersFromIdentityIds = (ids) => {
selectedUsers.value = getUsersByIds(ids); selectedUsers.value = getMembersByIdentityIds(ids);
}; };
const getManagerDeptSelectedCount = (dept) => { const getManagerDeptSelectedCount = (dept) => {
if (!dept.users?.length) return 0; if (!dept.users?.length) return 0;
const selectedSet = new Set(managerPickerSelectedIds.value.map(String)); const selectedSet = new Set(managerPickerSelectedIdentityIds.value.map(String));
return dept.users.filter((user) => selectedSet.has(String(user.userId))).length; return dept.users.filter((user) => selectedSet.has(String(user.identityId))).length;
}; };
const getDeptSelectedCount = (dept) => { const getDeptSelectedCount = (dept) => {
if (!dept.users?.length) return 0; if (!dept.users?.length) return 0;
const selectedSet = new Set(userPickerSelectedIds.value.map(String)); const selectedSet = new Set(userPickerSelectedIdentityIds.value.map(String));
return dept.users.filter((user) => selectedSet.has(String(user.userId))).length; return dept.users.filter((user) => selectedSet.has(String(user.identityId))).length;
}; };
function onManagerCheckChange(userId, checked) { function onManagerCheckChange(identityId, checked) {
const id = String(userId); const id = String(identityId);
if (checked) { if (checked) {
if (!managerPickerSelectedSet.value.has(id)) { if (!managerPickerSelectedSet.value.has(id)) {
managerPickerSelectedIds.value = [...managerPickerSelectedIds.value, id]; managerPickerSelectedIdentityIds.value = [...managerPickerSelectedIdentityIds.value, id];
} }
return; return;
} }
managerPickerSelectedIds.value = managerPickerSelectedIds.value.filter((item) => String(item) !== id); managerPickerSelectedIdentityIds.value = managerPickerSelectedIdentityIds.value.filter((item) => String(item) !== id);
} }
function onUserCheckChange(userId, checked) { function onUserCheckChange(identityId, checked) {
const id = String(userId); const id = String(identityId);
if (!checked && isLockedUser(id)) { if (!checked && isLockedIdentity(id)) {
uni.showToast({ title: '默认整改责任人不可取消', icon: 'none' }); uni.showToast({ title: '默认整改责任人不可取消', icon: 'none' });
return; return;
} }
if (checked) { if (checked) {
if (!userPickerSelectedSet.value.has(id)) { if (!userPickerSelectedSet.value.has(id)) {
userPickerSelectedIds.value = [...userPickerSelectedIds.value, id]; userPickerSelectedIdentityIds.value = [...userPickerSelectedIdentityIds.value, id];
} }
return; return;
} }
userPickerSelectedIds.value = userPickerSelectedIds.value.filter((item) => String(item) !== id); userPickerSelectedIdentityIds.value = userPickerSelectedIdentityIds.value.filter((item) => String(item) !== id);
} }
const openManagerPopup = () => { const openManagerPopup = () => {
managerPickerSelectedIds.value = [...selectedManagerIds.value]; managerPickerSelectedIdentityIds.value = [...selectedManagerIdentityIds.value];
const firstDeptWithUsers = managerDeptList.value.findIndex((dept) => dept.users?.length > 0); const firstDeptWithUsers = managerDeptList.value.findIndex((dept) => dept.users?.length > 0);
activeManagerDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0; activeManagerDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0;
showManagerPopup.value = true; showManagerPopup.value = true;
@@ -745,7 +838,7 @@
}; };
const openUserPopup = () => { const openUserPopup = () => {
userPickerSelectedIds.value = mergeLockedUserIds(selectedUserIds.value); userPickerSelectedIdentityIds.value = mergeLockedIdentityIds(selectedIdentityIds.value);
const firstDeptWithUsers = deptList.value.findIndex((dept) => dept.users?.length > 0); const firstDeptWithUsers = deptList.value.findIndex((dept) => dept.users?.length > 0);
activeDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0; activeDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0;
showUserPopup.value = true; showUserPopup.value = true;
@@ -757,16 +850,16 @@
// 确认选择安全管理人员 // 确认选择安全管理人员
const confirmManagerSelect = () => { const confirmManagerSelect = () => {
selectedManagerIds.value = managerPickerSelectedIds.value.map((id) => String(id)); selectedManagerIdentityIds.value = managerPickerSelectedIdentityIds.value.map((id) => String(id));
syncSelectedManagersFromIds(selectedManagerIds.value); syncSelectedManagersFromIdentityIds(selectedManagerIdentityIds.value);
showManagerPopup.value = false; showManagerPopup.value = false;
}; };
// 确认选择整改责任人 // 确认选择整改责任人
const confirmUserSelect = () => { const confirmUserSelect = () => {
userPickerSelectedIds.value = mergeLockedUserIds(userPickerSelectedIds.value); userPickerSelectedIdentityIds.value = mergeLockedIdentityIds(userPickerSelectedIdentityIds.value);
selectedUserIds.value = userPickerSelectedIds.value.map((id) => String(id)); selectedIdentityIds.value = userPickerSelectedIdentityIds.value.map((id) => String(id));
syncSelectedUsersFromIds(selectedUserIds.value); syncSelectedMembersFromIdentityIds(selectedIdentityIds.value);
showUserPopup.value = false; showUserPopup.value = false;
}; };
@@ -777,11 +870,11 @@
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
managerDeptList.value = res.data; managerDeptList.value = res.data;
deptList.value = res.data; deptList.value = res.data;
if (selectedManagerIds.value.length > 0) { if (selectedManagerIdentityIds.value.length > 0) {
syncSelectedManagersFromIds(selectedManagerIds.value); syncSelectedManagersFromIdentityIds(selectedManagerIdentityIds.value);
} }
if (selectedUserIds.value.length > 0) { if (selectedIdentityIds.value.length > 0) {
syncSelectedUsersFromIds(selectedUserIds.value); syncSelectedMembersFromIdentityIds(selectedIdentityIds.value);
} }
} }
} catch (error) { } catch (error) {
@@ -823,8 +916,8 @@
const fetchPersonnelLists = async () => { const fetchPersonnelLists = async () => {
await fetchRelatedDeptUsers(); await fetchRelatedDeptUsers();
if (selectedManagerIds.value.length > 0) { if (selectedManagerIdentityIds.value.length > 0) {
syncSelectedManagersFromIds(selectedManagerIds.value); syncSelectedManagersFromIdentityIds(selectedManagerIdentityIds.value);
} }
}; };
@@ -839,38 +932,53 @@
} }
}); });
// 提交整改 // 构建提交/保存共用的整改业务字段
// 真正的提交接口请求 const buildSharedRectifyParams = (attachments) => {
const executeSubmit = async () => {
// 构建附件列表
const attachments = fileList1.value
.filter((f) => f.status === 'success')
.map((file) => buildAttachmentItem(file));
const params = { const params = {
hazardId: hazardId.value,
assignId: assignId.value,
rectifyPlan: formData.rectifyPlan, rectifyPlan: formData.rectifyPlan,
rectificationMeasures: formData.rectificationMeasures, rectificationMeasures: formData.rectificationMeasures,
controlMeasures: formData.controlMeasures, controlMeasures: formData.controlMeasures,
rectifyResult: formData.rectifyResult, rectifyResult: formData.rectifyResult,
planCost: Number(formData.planCost) || 0, planCost: Number(formData.planCost) || 0,
actualCost: Number(formData.actualCost) || 0, actualCost: Number(formData.actualCost) || 0,
attachments: attachments, attachments,
manageIds: selectedManagerIds.value.map((id) => Number(id)), managerIds: selectedManagerIdentityIds.value.map((id) => Number(id)),
memberIds: selectedUserIds.value.map((id) => Number(id)), memberIds: selectedIdentityIds.value.map((id) => Number(id)),
rectifyTime: selectedRectifyTime.value || formatDateValue(rectifyTimeValue.value), rectifyTime: selectedRectifyTime.value || formatDateValue(rectifyTimeValue.value),
signPath: signatureServerPath.value || '', signPath: signatureServerPath.value || '',
sendMsgFlag: sendMsgFlag.value sendMsgFlag: sendMsgFlag.value
}; };
if (selectedDeadlineDate.value) {
// 编辑模式需要传递rectifyId params.deadline = selectedDeadlineDate.value;
if (rectifyId.value) {
params.rectifyId = rectifyId.value;
} }
return params;
};
// 真正的提交/保存接口请求
const executeSubmit = async () => {
const attachments = fileList1.value
.filter((f) => f.status === 'success')
.map((file) => buildAttachmentItem(file));
try { try {
const res = await submitRectification(params); let res;
if (isEdit.value) {
const updateParams = {
rectifyId: Number(rectifyId.value),
hazardId: hazardId.value,
assignId: assignId.value,
...buildSharedRectifyParams(attachments)
};
res = await updateRectification(updateParams);
} else {
const params = {
hazardId: hazardId.value,
assignId: assignId.value,
...buildSharedRectifyParams(attachments)
};
res = await submitRectification(params);
}
uni.hideLoading(); uni.hideLoading();
if (res.code === 0) { if (res.code === 0) {
clearDraft(false); clearDraft(false);
@@ -889,7 +997,7 @@
} }
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
console.error('提交整改失败:', error); console.error(isEdit.value ? '保存整改失败:' : '提交整改失败:', error);
uni.showToast({ uni.showToast({
title: '操作失败', title: '操作失败',
icon: 'none' icon: 'none'
@@ -1051,7 +1159,7 @@
planCost: Number(formData.planCost) || 0, planCost: Number(formData.planCost) || 0,
actualCost: Number(formData.actualCost) || 0, actualCost: Number(formData.actualCost) || 0,
attachments: attachments, attachments: attachments,
memberIds: selectedUserIds.value.map(id => Number(id)) memberIds: selectedIdentityIds.value.map(id => Number(id))
}; };
// 编辑模式需要传递rectifyId // 编辑模式需要传递rectifyId
@@ -1119,26 +1227,30 @@
// 保存hazardId和assignId // 保存hazardId和assignId
hazardId.value = data.hazardId || ''; hazardId.value = data.hazardId || '';
assignId.value = data.assignId || ''; assignId.value = data.assignId || '';
const resolvedTaskId = resolveTaskIdFromDetail(data);
if (resolvedTaskId) {
taskId.value = resolvedTaskId;
}
detailPersonPool.value = [ detailPersonPool.value = [
...(Array.isArray(data.managers) ? data.managers : []), ...(Array.isArray(data.managers) ? data.managers : []),
...(Array.isArray(data.members) ? data.members : []) ...(Array.isArray(data.members) ? data.members : [])
]; ];
// 先解析人员 ID再拉取候选列表并回显 // 先解析人员 ID再拉取候选列表并回显
const managerIdArr = resolveManagerIdsFromDetail(data); const managerIdentityIdArr = resolveManagerIdentityIdsFromDetail(data);
const memberIdArr = parseIdList(data.memberIds); if (managerIdentityIdArr.length > 0) {
if (managerIdArr.length > 0) { selectedManagerIdentityIds.value = managerIdentityIdArr;
selectedManagerIds.value = managerIdArr;
} }
if (memberIdArr.length > 0) { const memberIdentityIdArr = parseIdList(data.memberIds ?? data.memberIdentityIds);
selectedUserIds.value = memberIdArr; if (memberIdentityIdArr.length > 0) {
} else if (data.rectifierId) { selectedIdentityIds.value = memberIdentityIdArr;
selectedUserIds.value = [String(data.rectifierId)]; } else if (data.rectifierIdentityId) {
selectedIdentityIds.value = [String(data.rectifierIdentityId)];
} }
if (data.assigneeId) { if (data.assigneeIdentityId) {
setLockedDefaultUsers(data.assigneeId); setLockedDefaultIdentities(data.assigneeIdentityId);
} else if (data.rectifierId) { } else if (data.rectifierIdentityId) {
setLockedDefaultUsers(data.rectifierId); setLockedDefaultIdentities(data.rectifierIdentityId);
} }
await fetchPersonnelLists(); await fetchPersonnelLists();
@@ -1335,9 +1447,12 @@
if (options.assignId) { if (options.assignId) {
assignId.value = options.assignId; assignId.value = options.assignId;
} }
if (options.taskId) {
taskId.value = options.taskId;
}
if (!options.rectifyId && options.assigneeId) { if (!options.rectifyId && options.assigneeIdentityId) {
applyAssigneeFromOptions(options.assigneeId, options.assigneeName); applyAssigneeFromOptions(options.assigneeId, options.assigneeName, options.assigneeIdentityId);
} }
// 在hazardId赋值后调用确保有值 // 在hazardId赋值后调用确保有值
@@ -1358,6 +1473,10 @@
if (options.deadline) { if (options.deadline) {
applyDeadlineFromOptions(options.deadline); applyDeadlineFromOptions(options.deadline);
} }
if (!options.rectifyId) {
fetchNextStep();
}
}); });
</script> </script>

View File

@@ -25,17 +25,18 @@
<view class="user-info"> <view class="user-info">
<view class="user-dept text-bold">{{ userInfo.deptName || '未知部门' }}</view> <view class="user-dept text-bold">{{ userInfo.deptName || '未知部门' }}</view>
<view class="user-phone">手机号{{ userInfo.phone || '未绑定' }}</view> <view class="user-phone">手机号{{ userInfo.phone || '未绑定' }}</view>
<view v-if="displayIdentityName" class="user-identity">当前身份{{ displayIdentityName }}</view>
</view> </view>
<!-- <view class="switch-btn"> <view class="switch-btn" @click="goSwitchIdentity">
<text>切换</text> <text>切换</text>
<u-icon name="list" color="#285CE9" size="14"></u-icon> <u-icon name="list" color="#285CE9" size="14"></u-icon>
</view> --> </view>
</view> </view>
</view> </view>
<view class="padding page-content"> <view class="padding page-content">
<!-- 功能菜单 --> <!-- 功能菜单approval 角色不展示 -->
<view class="menu-card"> <view v-if="!isApprovalRole" class="menu-card">
<view class="menu-grid"> <view class="menu-grid">
<view class="menu-item" v-for="(item, index) in infoList" :key="index" @click="handleMenuClick(item)"> <view class="menu-item" v-for="(item, index) in infoList" :key="index" @click="handleMenuClick(item)">
<image class="menu-icon" :src="item.src"></image> <image class="menu-icon" :src="item.src"></image>
@@ -44,7 +45,7 @@
</view> </view>
</view> </view>
<!-- 我的检查计划 --> <!-- 我的检查计划 -->
<view class="bg-white margin-top radius" style="padding: 40rpx; margin-left: -30rpx; margin-right: -30rpx;"> <view v-if="!isApprovalRole" class="bg-white margin-top radius" style="padding: 40rpx; margin-left: -30rpx; margin-right: -30rpx;">
<view class="flex margin-bottom-xl align-center justify-between"> <view class="flex margin-bottom-xl align-center justify-between">
<view class="text-bold margin-left-xs" style="font-size: 32rpx;">我的检查计划</view> <view class="text-bold margin-left-xs" style="font-size: 32rpx;">我的检查计划</view>
<!-- <button class="cu-btn round sm line-blue inspect-list-btn" @click="goInspectList">检查列表</button> --> <!-- <button class="cu-btn round sm line-blue inspect-list-btn" @click="goInspectList">检查列表</button> -->
@@ -121,8 +122,8 @@
加载更多 加载更多
</view> </view>
</view> </view>
<!-- 我的隐患 --> <!-- 我的隐患排查 -->
<view class="bg-white margin-top radius" style="padding: 40rpx; margin-left: -40rpx; margin-right: -40rpx;"> <view v-if="!isApprovalRole" class="bg-white margin-top radius" style="padding: 40rpx; margin-left: -40rpx; margin-right: -40rpx;">
<view class="flex margin-bottom"> <view class="flex margin-bottom">
<!-- <view class="border-tite"></view> --> <!-- <view class="border-tite"></view> -->
<view class="text-bold margin-left-xs" style="font-size: 32rpx;">我的隐患排查</view> <view class="text-bold margin-left-xs" style="font-size: 32rpx;">我的隐患排查</view>
@@ -140,11 +141,11 @@
</view> </view>
</scroll-view> </scroll-view>
<!-- 无数据提示 --> <!-- 无数据提示 -->
<view v-if="filteredDangerData.length === 0" class="text-center text-gray padding"> <view v-if="hiddenDangerData.length === 0 && !hiddenDangerLoading" class="text-center text-gray padding">
暂无隐患数据 暂无隐患数据
</view> </view>
<!-- 隐患卡片列表 --> <!-- 隐患卡片列表 -->
<view class="danger-card margin-top" v-for="(item, index) in filteredDangerData" :key="item.hazardId"> <view class="danger-card margin-top" v-for="(item, index) in hiddenDangerData" :key="item.hazardId">
<!-- 标题行图标+标题 + 等级标签 --> <!-- 标题行图标+标题 + 等级标签 -->
<view class="flex justify-between align-center"> <view class="flex justify-between align-center">
<view class="flex align-center" style="flex: 1; min-width: 0; margin-right: 24rpx;"> <view class="flex align-center" style="flex: 1; min-width: 0; margin-right: 24rpx;">
@@ -183,10 +184,74 @@
class="round cu-btn bg-blue" @click.stop="goRectification(item)">立即整改</button> class="round cu-btn bg-blue" @click.stop="goRectification(item)">立即整改</button>
<button v-if="item.statusName === '待验收' && item.canEdit" <button v-if="item.statusName === '待验收' && item.canEdit"
class="round cu-btn light bg-blue" @click.stop="editRectification(item)">编辑整改信息</button> class="round cu-btn light bg-blue" @click.stop="editRectification(item)">编辑整改信息</button>
<button v-if="item.statusName === '待验收' && canAcceptance" <button v-if="canShowAcceptanceButton(item, currentRoleKey)"
class="round cu-btn bg-blue" @click.stop="goAcceptance(item)">立即验收</button> class="round cu-btn bg-blue" @click.stop="goAcceptance(item)">立即验收</button>
<button v-if="item.statusName === '待交办'" <button v-if="item.statusName === '待交办'"
class="round cu-btn bg-blue" @click.stop="assignHazard(item)">隐患交办</button> class="round cu-btn bg-blue" @click.stop="assignHazard(item)">隐患交办</button>
<button v-if="canShowWriteoffApplyButton(item)"
class="round cu-btn bg-blue" @click.stop="goWriteoffApply(item)">销号申请</button>
<button v-if="canShowWriteoffApprovalButton(item, currentRoleKey)"
class="round cu-btn bg-blue" @click.stop="goWriteoffApproval(item)">销号审批</button>
</view>
</view>
<u-loadmore
v-if="hiddenDangerData.length > 0"
:status="hiddenDangerLoadStatus"
style="margin-top: 20rpx; margin-bottom: 20rpx;"
/>
</view>
<!-- 我的工作台approval 角色专属 -->
<view v-if="isApprovalRole" class="bg-white margin-top radius" style="padding: 40rpx; margin-left: -40rpx; margin-right: -40rpx;">
<view class="flex margin-bottom">
<view class="text-bold margin-left-xs" style="font-size: 32rpx;">我的工作台</view>
</view>
<view class="danger-tab-list workbench-tab-list">
<view
class="danger-tab-item"
:class="{ 'danger-tab-active': activeWorkbenchTab === index }"
v-for="(tab, index) in workbenchTabs"
:key="tab.value"
@click="switchWorkbenchTab(index)"
>{{ tab.label }}</view>
</view>
<view v-if="workbenchLoading" class="text-center text-gray padding">加载中...</view>
<view v-else-if="workbenchList.length === 0" class="text-center text-gray padding">
暂无数据
</view>
<view class="danger-card margin-top" v-for="item in workbenchList" :key="getWorkbenchItemKey(item)">
<view class="flex justify-between align-center">
<view class="flex align-center" style="flex: 1; min-width: 0; margin-right: 24rpx;">
<text class="cuIcon-infofill text-blue" style="font-size: 36rpx; margin-right: 12rpx; flex-shrink: 0;"></text>
<view class="text-bold" style="font-size: 30rpx; word-break: break-all; flex: 1;">{{ item.title }}</view>
</view>
<view class="level-tag" :class="{
'level-minor': item.levelName === '轻微隐患',
'level-normal': item.levelName === '一般隐患',
'level-major': item.levelName === '重大隐患'
}">{{ item.levelName }}</view>
</view>
<view class="text-gray margin-top-sm" style="font-size: 26rpx; padding-left: 48rpx;">{{ item.address }}</view>
<view style="height: 1rpx; background: #EEEEEE; margin: 20rpx 0;"></view>
<view class="danger-info-row">
<text class="text-gray">隐患来源</text>
<text style="color: #333333;">{{ item.source }}</text>
</view>
<view class="danger-info-row">
<text class="text-gray">隐患状态</text>
<text style="color: #333333;">{{ item.statusName }}</text>
</view>
<view class="danger-info-row">
<text class="text-gray">发现时间</text>
<text style="color: #333333;">{{ item.createdAt }}</text>
</view>
<view class="flex justify-end danger-card-actions">
<button class="round cu-btn light bg-blue" @click.stop="viewHazardDetail(item)">查看详情</button>
<button
v-if="activeWorkbenchTab === 0"
class="round cu-btn bg-blue"
@click.stop="goLeaderApproval(item)"
>领导批示</button>
</view> </view>
</view> </view>
</view> </view>
@@ -197,10 +262,24 @@
<script setup> <script setup>
import { ref, reactive, computed } from 'vue'; import { ref, reactive, computed } from 'vue';
// import { onLoad } from '@dcloudio/uni-app'; // import { onLoad } from '@dcloudio/uni-app';
import {getCheckPlanList, getHiddenDangerList} from '@/request/api.js' import {getCheckPlanList, getHiddenDangerList, getFlowTodoList, getFlowDoneList} from '@/request/api.js'
import { getProfileDetail } from '@/request/three_one_api/info.js'; import { getProfileDetail } from '@/request/three_one_api/info.js';
import { onLoad, onShow } from '@dcloudio/uni-app'; import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app';
import { toImageUrl } from '@/request/request.js'; import { toImageUrl } from '@/request/request.js';
import { applyProfileToUserInfo, applyStoredUserInfo, resolveIdentityName, resolveUserRoleKey } from '@/utils/userInfo.js';
import {
buildRectificationUrl,
buildEditRectificationUrl,
buildAssignmentUrl,
buildAcceptanceUrl,
buildAcceptanceApprovalUrl,
buildWriteoffApplyUrl,
buildWriteoffApprovalUrl,
buildLeaderWriteoffApprovalUrl,
canShowAcceptanceButton,
canShowWriteoffApplyButton,
canShowWriteoffApprovalButton
} from '@/utils/hazardNav.js';
const loading = ref(true); const loading = ref(true);
const defaultAvatar = '/static/my/default_avater.png'; const defaultAvatar = '/static/my/default_avater.png';
@@ -214,13 +293,31 @@
deptName: '', deptName: '',
role: '', role: '',
avatar: '', avatar: '',
phone: '' phone: '',
identityName: '',
userIdentity: null
}); });
// 获取用户角色判断是否有验收权限admin或manage才能验收 // 当前身份角色(优先 userIdentity.roleKey
const canAcceptance = computed(() => { const currentRoleKey = computed(() => resolveUserRoleKey(userInfo));
return userInfo.role === 'admin' || userInfo.role === 'manage';
const isApprovalRole = computed(() => currentRoleKey.value === 'approval');
// 展示用身份名称(兼容接口字段与本地缓存)
const displayIdentityName = computed(() => {
return resolveIdentityName(userInfo, userInfo.userIdentity);
}); });
const loadUserInfoFromStorage = () => {
try {
const storedUserInfo = uni.getStorageSync('userInfo');
if (storedUserInfo) {
applyStoredUserInfo(userInfo, JSON.parse(storedUserInfo));
}
} catch (storageError) {
console.error('从本地存储获取用户信息失败:', storageError);
}
};
// 获取图片完整URL用于显示 // 获取图片完整URL用于显示
const getImageUrl = (path) => { const getImageUrl = (path) => {
@@ -233,37 +330,11 @@
try { try {
const res = await getProfileDetail(); const res = await getProfileDetail();
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
userInfo.userId = res.data.userId || ''; applyProfileToUserInfo(userInfo, res.data);
userInfo.username = res.data.userName || '';
userInfo.nickName = res.data.nickName || '';
userInfo.deptId = res.data.deptId || '';
userInfo.deptName = res.data.deptName || '';
userInfo.avatar = res.data.avatar || '';
userInfo.phone = res.data.phonenumber || res.data.phone || '';
// 获取角色信息
if (res.data.roles && res.data.roles.length > 0) {
userInfo.role = res.data.roles[0].roleKey || '';
}
} }
} catch (e) { } catch (e) {
console.error('获取用户信息失败:', e); console.error('获取用户信息失败:', e);
// 如果接口失败,尝试从本地存储获取 loadUserInfoFromStorage();
try {
const storedUserInfo = uni.getStorageSync('userInfo');
if (storedUserInfo) {
const info = JSON.parse(storedUserInfo);
userInfo.userId = info.userId || '';
userInfo.username = info.username || '';
userInfo.nickName = info.nickName || '';
userInfo.deptId = info.deptId || '';
userInfo.deptName = info.deptName || '';
userInfo.role = info.role || '';
userInfo.avatar = info.avatar || '';
userInfo.phone = info.phone || '';
}
} catch (storageError) {
console.error('从本地存储获取用户信息失败:', storageError);
}
} }
}; };
// 全部菜单项 // 全部菜单项
@@ -307,7 +378,10 @@
// 根据角色动态展示菜单 // 根据角色动态展示菜单
const infoList = computed(() => { const infoList = computed(() => {
if (userInfo.role === 'common') { if (currentRoleKey.value === 'approval') {
return [];
}
if (currentRoleKey.value === 'common') {
return allMenuList.filter(item => commonMenuNames.includes(item.name)); return allMenuList.filter(item => commonMenuNames.includes(item.name));
} }
// admin、manage 及其他角色展示全部菜单 // admin、manage 及其他角色展示全部菜单
@@ -473,16 +547,22 @@
// }); // });
// 页面每次显示时都会加载数据 // 页面每次显示时都会加载数据
onShow(() => { onShow(() => {
getUserInfo(); loadUserInfoFromStorage();
getCheckPlanLists(); getUserInfo().then(() => {
getHiddenDangerLists(); if (isApprovalRole.value) {
fetchWorkbenchList();
} else {
getCheckPlanLists();
resetHiddenDangerList();
fetchHiddenDangerList();
}
});
}); });
//我的隐患排查 //我的隐患排查
const hiddenDangerParams = ref({ const HAZARD_PAGE_SIZE = 10;
pageNum: 1, const hiddenDangerPageNum = ref(1);
pageSize: 10, const hiddenDangerLoading = ref(false);
name: '' const hiddenDangerLoadStatus = ref('loadmore');
});
const hiddenDangerData = ref([]); const hiddenDangerData = ref([]);
// 隐患排查 Tab 筛选 // 隐患排查 Tab 筛选
@@ -496,92 +576,224 @@
]); ]);
const activeDangerTab = ref(0); const activeDangerTab = ref(0);
// 切换 Tab const buildHiddenDangerListParams = () => {
const params = {
pageNum: hiddenDangerPageNum.value,
pageSize: HAZARD_PAGE_SIZE
};
const activeTab = dangerTabs.value[activeDangerTab.value];
if (activeTab?.value != null) {
params.status = activeTab.value;
}
return params;
};
const resetHiddenDangerList = () => {
hiddenDangerPageNum.value = 1;
hiddenDangerData.value = [];
hiddenDangerLoadStatus.value = 'loadmore';
};
// 切换 Tab带 status 重新请求接口
const switchDangerTab = (index) => { const switchDangerTab = (index) => {
if (activeDangerTab.value === index) return;
activeDangerTab.value = index; activeDangerTab.value = index;
resetHiddenDangerList();
fetchHiddenDangerList();
}; };
// 根据 Tab 过滤隐患数据 const fetchHiddenDangerList = async () => {
const filteredDangerData = computed(() => { if (hiddenDangerLoading.value) return;
const activeTab = dangerTabs.value[activeDangerTab.value]; if (hiddenDangerPageNum.value > 1 && hiddenDangerLoadStatus.value === 'nomore') return;
if (!activeTab || activeTab.value === null) {
return hiddenDangerData.value; hiddenDangerLoading.value = true;
if (hiddenDangerPageNum.value > 1) {
hiddenDangerLoadStatus.value = 'loading';
} }
return hiddenDangerData.value.filter(item => item.status === activeTab.value);
});
const getHiddenDangerLists = async () => {
try { try {
const res = await getHiddenDangerList(hiddenDangerParams.value); const res = await getHiddenDangerList(buildHiddenDangerListParams());
console.log(res);
if (res.code === 0) { if (res.code === 0) {
hiddenDangerData.value = res.data.records; const records = res.data?.records || [];
console.log(hiddenDangerData.value,1111); const total = Number(res.data?.total ?? 0);
if (hiddenDangerPageNum.value === 1) {
hiddenDangerData.value = records;
} else {
hiddenDangerData.value = [...hiddenDangerData.value, ...records];
}
if (hiddenDangerData.value.length >= total || records.length < HAZARD_PAGE_SIZE) {
hiddenDangerLoadStatus.value = 'nomore';
} else {
hiddenDangerLoadStatus.value = 'loadmore';
}
} else {
if (hiddenDangerPageNum.value === 1) {
hiddenDangerData.value = [];
}
hiddenDangerLoadStatus.value = 'nomore';
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (hiddenDangerPageNum.value > 1) {
hiddenDangerPageNum.value--;
}
hiddenDangerLoadStatus.value = 'loadmore';
} finally { } finally {
hiddenDangerLoading.value = false;
loading.value = false; loading.value = false;
} }
}; };
const loadMoreHiddenDangerList = () => {
if (hiddenDangerLoadStatus.value !== 'loadmore' || hiddenDangerLoading.value) return;
hiddenDangerPageNum.value++;
fetchHiddenDangerList();
};
// 页面加载时调用接口 // 页面加载时调用接口
onLoad(() => { onLoad(() => {
getHiddenDangerLists(); if (!isApprovalRole.value) {
resetHiddenDangerList();
fetchHiddenDangerList();
}
}); });
onReachBottom(() => {
if (isApprovalRole.value) return;
loadMoreHiddenDangerList();
});
// ========== 我的工作台approval 角色) ==========
const workbenchTabs = [
{ label: '待办', value: 'todo' },
{ label: '已办', value: 'done' }
];
const activeWorkbenchTab = ref(0);
const workbenchList = ref([]);
const workbenchLoading = ref(false);
const normalizeWorkbenchItem = (item) => {
if (!item) return {};
const hazard = item.hazard || item.hazardInfo || {};
return {
...item,
hazardId: item.hazardId ?? hazard.hazardId ?? '',
assignId: item.assignId ?? hazard.assignId ?? '',
rectifyId: item.rectifyId ?? hazard.rectifyId ?? '',
taskId: item.taskId ?? item.flowTaskId ?? item.currentTaskId ?? '',
taskKey: item.taskKey ?? item.flowTaskKey ?? item.currentTaskKey ?? '',
title: item.title ?? hazard.title ?? item.hazardTitle ?? '',
address: item.address ?? hazard.address ?? '',
source: item.source ?? hazard.source ?? item.hazardSourceName ?? '',
statusName: item.statusName ?? hazard.statusName ?? item.hazardStatusName ?? hazard.hazardStatusName ?? item.taskName ?? '',
hazardStatus: item.hazardStatus ?? hazard.hazardStatus ?? item.status ?? hazard.status ?? '',
createdAt: item.discoveredAt ?? hazard.discoveredAt ?? '',
levelName: item.levelName ?? hazard.levelName ?? ''
};
};
const resolveWorkbenchRecords = (data) => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.records)) return data.records;
if (Array.isArray(data.list)) return data.list;
return [];
};
const getWorkbenchItemKey = (item) => {
const taskId = item.taskId || item.flowTaskId || '';
const hazardId = item.hazardId || '';
if (taskId && hazardId) return `${taskId}-${hazardId}`;
return taskId || hazardId || item.rectifyId || JSON.stringify(item);
};
const switchWorkbenchTab = (index) => {
if (activeWorkbenchTab.value === index) return;
activeWorkbenchTab.value = index;
fetchWorkbenchList();
};
const fetchWorkbenchList = async () => {
workbenchLoading.value = true;
try {
const isTodo = activeWorkbenchTab.value === 0;
const res = isTodo ? await getFlowTodoList() : await getFlowDoneList();
if (res.code === 0) {
workbenchList.value = resolveWorkbenchRecords(res.data).map(normalizeWorkbenchItem);
} else {
workbenchList.value = [];
uni.showToast({ title: res.msg || '获取工作台数据失败', icon: 'none' });
}
} catch (error) {
console.error('获取工作台数据失败:', error);
workbenchList.value = [];
uni.showToast({ title: '获取工作台数据失败', icon: 'none' });
} finally {
workbenchLoading.value = false;
loading.value = false;
}
};
const goLeaderApproval = (item) => {
const hazardStatus = Number(item.hazardStatus);
const statusName = item.statusName || item.hazardStatusName || '';
if (hazardStatus === 4 || statusName === '待销号') {
uni.navigateTo({ url: buildLeaderWriteoffApprovalUrl(item) });
return;
}
if (hazardStatus === 3 || statusName === '待验收') {
uni.navigateTo({ url: buildAcceptanceApprovalUrl(item) });
return;
}
uni.showToast({ title: '当前状态不支持领导审批', icon: 'none' });
};
// ========== 隐患排查相关跳转函数 ========== // ========== 隐患排查相关跳转函数 ==========
// 查看隐患详情 // 查看隐患详情(流程链)
const viewHazardDetail = (item) => { const viewHazardDetail = (item) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/hiddendanger/detail2?hazardId=${item.hazardId}&assignId=${item.assignId || ''}` url: `/pages/hiddendanger/process-chain?hazardId=${item.hazardId}`
}) })
} }
// 立即整改(待整改状态) // 立即整改(待整改状态)
const goRectification = (item) => { const goRectification = (item) => {
let url = `/pages/hiddendanger/rectification?hazardId=${item.hazardId}&assignId=${item.assignId}` uni.navigateTo({ url: buildRectificationUrl(item) });
if (item.deadline) {
url += `&deadline=${encodeURIComponent(item.deadline)}`
}
if (item.assigneeId) {
url += `&assigneeId=${item.assigneeId}`
}
if (item.assigneeName) {
url += `&assigneeName=${encodeURIComponent(item.assigneeName)}`
}
uni.navigateTo({ url })
} }
// 编辑整改信息(待验收状态) // 编辑整改信息(待验收状态)
const editRectification = (item) => { const editRectification = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildEditRectificationUrl(item) });
url: `/pages/hiddendanger/rectification?rectifyId=${item.rectifyId}&isEdit=1`
})
} }
// 立即验收 // 立即验收
const goAcceptance = (item) => { const goAcceptance = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildAcceptanceUrl(item) });
url: `/pages/hiddendanger/acceptance?hazardId=${item.hazardId}&assignId=${item.assignId}&rectifyId=${item.rectifyId}`
})
} }
// 隐患交办 // 隐患交办
const assignHazard = (item) => { const assignHazard = (item) => {
uni.navigateTo({ uni.navigateTo({ url: buildAssignmentUrl(item) });
url: `/pages/hiddendanger/assignment?hazardId=${item.hazardId}&assignId=${item.assignId}`
})
} }
// 销号申请
const goWriteoffApply = (item) => {
uni.navigateTo({ url: buildWriteoffApplyUrl(item) });
};
// 销号审批
const goWriteoffApproval = (item) => {
uni.navigateTo({ url: buildWriteoffApprovalUrl(item) });
};
// 切换账户 // 跳转身份切换页
const switchAccount = () => { const goSwitchIdentity = () => {
uni.showToast({ uni.navigateTo({
title: '切换账户功能开发中', url: '/pages/personalcenter/identity'
icon: 'none'
}); });
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -642,13 +854,22 @@
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
margin-top: 10rpx; margin-top: 10rpx;
} }
.user-identity {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
margin-top: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
.switch-btn { .switch-btn {
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: #fff;
padding: 16rpx 24rpx; padding: 8rpx 24rpx;
border-radius: 30rpx; border-radius: 30rpx;
color: #285CE9; color: #285CE9;
font-size: 26rpx; font-size: 26rpx;
@@ -896,6 +1117,10 @@
margin-left: 10rpx; margin-left: 10rpx;
} }
.workbench-tab-list {
margin-bottom: 20rpx;
}
.danger-tab-item { .danger-tab-item {
font-size: 28rpx; font-size: 28rpx;
color: #666; color: #666;

View File

@@ -31,9 +31,9 @@
<text>手机{{ item.phonenumber || '未设置' }}</text> <text>手机{{ item.phonenumber || '未设置' }}</text>
</view> </view>
</view> </view>
<button class="btn-lock bg-blue" @click="Lock(item)"> <!-- <button class="btn-lock bg-blue" @click="Lock(item)">
{{ item.status === '1' ? '解锁' : '锁定' }} {{ item.status === '1' ? '解锁' : '锁定' }}
</button> </button> -->
</view> </view>
</view> </view>

View File

@@ -0,0 +1,321 @@
<template>
<view class="page">
<!-- 当前生效身份 -->
<view v-if="currentIdentity" class="current-banner">
<view class="banner-label">当前生效身份</view>
<view class="banner-name">{{ getIdentityTitle(currentIdentity) }}</view>
<view class="banner-sub">{{ getIdentitySubtitle(currentIdentity) }}</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="state-box">
<u-loading-icon mode="circle" color="#007aff"></u-loading-icon>
<text class="state-text">加载中...</text>
</view>
<!-- 空状态 -->
<view v-else-if="!identityList.length" class="state-box">
<text class="state-text">暂无可用身份</text>
</view>
<!-- 身份列表 -->
<view v-else class="identity-list">
<view
v-for="item in identityList"
:key="item.identityId"
class="identity-card"
:class="{
'is-current': isCurrentIdentity(item),
'is-default': item.isDefault
}"
>
<view class="card-header">
<view class="card-title">{{ getIdentityTitle(item) }}</view>
<view class="card-tags">
<view v-if="isCurrentIdentity(item)" class="tag tag-current">当前使用</view>
<view v-if="item.isDefault" class="tag tag-default">默认</view>
</view>
</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">所属部门</text>
<text class="info-value">{{ item.deptName || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">所属角色</text>
<text class="info-value">{{ item.roleName || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">角色标识</text>
<text class="info-value">{{ item.roleKey || '-' }}</text>
</view>
<view v-if="item.postName" class="info-row">
<text class="info-label">所属岗位</text>
<text class="info-value">{{ item.postName }}</text>
</view>
</view>
<button
class="switch-btn"
:class="{ 'switch-btn--disabled': isCurrentIdentity(item) }"
:loading="switchingId === item.identityId"
:disabled="isCurrentIdentity(item) || !!switchingId"
@click="handleSwitch(item)"
>
{{ isCurrentIdentity(item) ? '当前使用中' : '切换到此身份' }}
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { getMyIdentity } from '@/request/identity.js';
import { performIdentitySwitch, reLaunchAfterSwitch } from '@/utils/identitySwitch.js';
const loading = ref(false);
const switchingId = ref(null);
const identityList = ref([]);
const currentIdentity = ref(null);
function unwrapIdentityData(res) {
if (!res) return { identities: [], currentIdentity: null };
const data = res.data && typeof res.data === 'object' ? res.data : res;
return {
identities: data.identities || [],
currentIdentity: data.currentIdentity || null
};
}
function getIdentityTitle(item) {
return item.identityName || item.roleName || '未命名身份';
}
function getIdentitySubtitle(item) {
const parts = [];
if (item.deptName) parts.push(item.deptName);
if (item.roleName && item.roleName !== item.identityName) parts.push(item.roleName);
return parts.join(' · ') || '-';
}
function isCurrentIdentity(item) {
if (item.current) return true;
return currentIdentity.value && currentIdentity.value.identityId === item.identityId;
}
async function fetchIdentityList() {
loading.value = true;
try {
const res = await getMyIdentity();
if (res.code === 0 || res.code === 200) {
const { identities, currentIdentity: current } = unwrapIdentityData(res);
identityList.value = identities;
currentIdentity.value = current;
} else {
identityList.value = [];
currentIdentity.value = null;
uni.showToast({ title: res.msg || '获取身份列表失败', icon: 'none' });
}
} catch (e) {
console.error('获取身份列表失败:', e);
identityList.value = [];
currentIdentity.value = null;
} finally {
loading.value = false;
}
}
async function handleSwitch(item) {
if (isCurrentIdentity(item) || switchingId.value) return;
const identityName = getIdentityTitle(item);
const confirmMsg = `确认切换到「${identityName}」吗?切换后数据将按新身份刷新。`;
uni.showModal({
title: '切换身份',
content: confirmMsg,
confirmText: '确认切换',
success: async (modalRes) => {
if (!modalRes.confirm) return;
switchingId.value = item.identityId;
try {
await performIdentitySwitch(item.identityId);
reLaunchAfterSwitch();
} catch (e) {
console.error('身份切换失败:', e);
} finally {
switchingId.value = null;
}
}
});
}
onShow(() => {
fetchIdentityList();
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f4f7fb;
padding: 24rpx;
padding-bottom: 48rpx;
}
.current-banner {
background: linear-gradient(135deg, #3e95f1 0%, #4269f5 100%);
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
color: #fff;
.banner-label {
font-size: 24rpx;
opacity: 0.85;
}
.banner-name {
font-size: 34rpx;
font-weight: bold;
margin-top: 12rpx;
}
.banner-sub {
font-size: 26rpx;
opacity: 0.9;
margin-top: 8rpx;
}
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.state-text {
margin-top: 24rpx;
font-size: 28rpx;
color: #999;
}
}
.identity-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.identity-card {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
border: 2rpx solid transparent;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
&.is-current {
border-color: #67c23a;
}
&.is-default {
border-color: #e6a23c;
}
&.is-current.is-default {
border-color: #67c23a;
}
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 24rpx;
}
.card-title {
flex: 1;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
flex-shrink: 0;
}
.tag {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.tag-current {
background: #e8f8ef;
color: #67c23a;
}
.tag-default {
background: #fdf6ec;
color: #e6a23c;
}
.info-list {
margin-bottom: 28rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx dashed #eee;
font-size: 28rpx;
&:last-child {
border-bottom: none;
}
}
.info-label {
color: #999;
flex-shrink: 0;
margin-right: 24rpx;
}
.info-value {
color: #333;
text-align: right;
word-break: break-all;
}
.switch-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(90deg, #3e95f1 0%, #4269f5 100%);
color: #fff;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
&::after {
border: none;
}
&--disabled {
background: #e8e8e8;
color: #999;
}
}
</style>

View File

@@ -118,6 +118,15 @@ export function getHazardDetail(hazardId) {
method: 'GET' method: 'GET'
}); });
} }
/** 隐患流程链(动态 nodes 时间线) */
export function getHazardProcessChain(hazardId) {
return requestAPI({
url: '/frontend/hazard/process-chain',
method: 'GET',
data: { hazardId }
});
}
//获取隐患排查列表 //获取隐患排查列表
export function getHiddenDangerList(params) { export function getHiddenDangerList(params) {
return requestAPI({ return requestAPI({
@@ -267,6 +276,33 @@ export function getWriteOffApplyDetail(applyId) {
method: 'GET' method: 'GET'
}); });
} }
//获取销号审批表单数据按隐患ID
export function getWriteoffForm(hazardId) {
return requestAPI({
url: '/frontend/hazard/writeoff/form',
method: 'GET',
data: { hazardId }
});
}
//销号审核通过
export function writeoffApprove(params) {
return requestAPI({
url: '/admin/hazard/writeoff/approve',
method: 'POST',
data: {
...params,
type: 'agree'
}
});
}
//销号驳回
export function writeoffReject(params) {
return requestAPI({
url: '/admin/hazard/writeoff/reject',
method: 'POST',
data: params
});
}
//验收整改 //验收整改
export function acceptanceRectification(params) { export function acceptanceRectification(params) {
@@ -426,14 +462,32 @@ export function getCheckItemListDetail(params) {
}); });
} }
// 根据部门id获取用户列表 // 根据部门id获取用户列表;可选 params.type1=部门全部用户默认2=部门 manage 角色用户3=非 manage 角色用户(验收页非快速审批用)
export function getDeptUsers(deptId) { export function getDeptUsers(deptId, params = {}) {
return requestAPI({ return requestAPI({
url: deptId ? `/admin/user/dept/users/${deptId}` : '/admin/user/dept/users', url: deptId ? `/admin/user/dept/users/${deptId}` : '/admin/user/dept/users',
method: 'GET',
data: params
});
}
/** 上报时获取上级范围内可选处理人deptId 取当前身份 userIdentity.deptId */
export function getDeptSuperiorScope(deptId) {
return requestAPI({
url: `/admin/user/dept/${deptId}/superior-scope`,
method: 'GET' method: 'GET'
}); });
} }
/** 流程审批:获取下一步处理人候选(部门树 + users */
export function getFlowApproverCandidates(taskId) {
return requestAPI({
url: '/admin/user/flow/approver-candidates',
method: 'GET',
data: { taskId }
});
}
// 获取子级部门列表(从当前到最后一层) // 获取子级部门列表(从当前到最后一层)
export function getDeptChildren() { export function getDeptChildren() {
return requestAPI({ return requestAPI({
@@ -487,4 +541,42 @@ export function generateWriteoffContent(params) {
data: params, data: params,
loadingText: 'AI生成销号方案中' loadingText: 'AI生成销号方案中'
}); });
}
// 预览流程下一节点
export function getFlowNextNodes(params) {
return requestAPI({
url: '/flow/task/next-nodes',
method: 'POST',
data: params,
loadingText: false
});
}
// 流程任务审批(同意/驳回/上报/重新整改等)
export function flowTaskApprove(params) {
return requestAPI({
url: '/flow/task/approve',
method: 'POST',
data: params,
loadingText: false
});
}
// 查询当前身份的 Flowable 待办(候选组 + assignee 并集)
export function getFlowTodoList(params) {
return requestAPI({
url: '/frontend/flow/todo',
method: 'GET',
data: params
});
}
// 查询当前用户/身份的已办(办结人匹配用户 ID 或身份 ID
export function getFlowDoneList(params) {
return requestAPI({
url: '/frontend/flow/done',
method: 'GET',
data: params
});
} }

26
request/identity.js Normal file
View File

@@ -0,0 +1,26 @@
import { requestAPI } from './request.js';
/** 获取当前用户身份列表及当前生效身份 */
export function getMyIdentity() {
return requestAPI({
url: '/system/identity/my',
method: 'GET'
});
}
/** 切换当前用户身份 */
export function switchIdentity(identityId) {
return requestAPI({
url: '/system/identity/switch',
method: 'POST',
data: { identityId }
});
}
/** 设置默认身份 */
export function setDefaultIdentity(identityId) {
return requestAPI({
url: `/system/identity/setDefault/${identityId}`,
method: 'PUT'
});
}

View File

@@ -2,10 +2,10 @@ import Request from './luch-request/index.js';
// 基础的url // 基础的url
// const baseUrl = 'https://yingji.hexieapi.com/prod-api'; const baseUrl = 'https://yingji.hexieapi.com/prod-api';
// const baseUrl = 'http://192.168.1.168:5004'; //廖哥本地 // const baseUrl = 'http://192.168.1.168:5004'; //廖哥本地
const baseUrl = 'http://192.168.1.140:5004'; //超哥本地 // const baseUrl = 'http://192.168.1.140:5004'; //超哥本地
// const baseUrl = 'http://192.168.1.158:7003/prod-api'; //测试环境 // const baseUrl = 'http://192.168.1.158:7003/prod-api'; //测试环境
// 图片/文件资源域名:去掉 /prod-api便于 <image> / previewImage / downloadFile 直接访问 // 图片/文件资源域名:去掉 /prod-api便于 <image> / previewImage / downloadFile 直接访问

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,27 +1,35 @@
<template> <template>
<view class="u-tree-node" :style="{ paddingLeft: depth * 20 + 'px' }"> <view class="u-tree-node">
<view class="u-tree-node-content" @click="toggle"> <view class="u-tree-node-content" :style="{ paddingLeft: depth * indent + 'rpx' }" @click="toggle">
<!-- <text v-if="hasChildren" class="u-tree-node-toggle"> <up-icon
{{ node.expanded ? '' : '' }} v-if="hasChildren"
</text> --> class="u-tree-node-toggle"
<up-icon v-if="hasChildren" class="u-tree-node-toggle" :name="isExpanded ? collapseIcon : expandIcon"
:name="node.expanded ? 'arrow-down-fill' : 'play-right-fill'" size="12" /> :size="iconSize"
/>
<view v-else class="u-tree-node-toggle u-tree-node-toggle--placeholder"></view>
<up-checkbox <up-checkbox
v-if="showCheckbox" v-if="showCheckbox"
usedAlone usedAlone
:size="12" :size="checkboxSize"
:checked="node.checked" :checked="node.checked"
@change="toggleCheck" @change="toggleCheck"
style="margin-right: 10px;" style="margin-right: 10px;"
/> />
<slot :nodeData="node" :level="depth + 1"> <!-- #ifdef MP-WEIXIN -->
{{ node[props.label] }} <text class="u-tree-node-label" :class="labelClass">{{ node[props.label] }}</text>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<slot :node="node" :data="node" :level="depth + 1" :expanded="isExpanded" :checked="!!node.checked" :disabled="isDisabled">
<text class="u-tree-node-label" :class="labelClass">{{ node[props.label] }}</text>
</slot> </slot>
<!-- #endif -->
</view> </view>
<view v-if="hasChildren && (node.expanded === undefined ? true : node.expanded)" <view
v-if="hasChildren && isExpanded"
class="u-tree-node-children" class="u-tree-node-children"
:style="{ paddingLeft: (depth + 1) * 20 + 'px' }"> >
<tree-node <TreeNode
v-for="child in node[props.children]" v-for="child in node[props.children]"
:key="child[props.nodeKey]" :key="child[props.nodeKey]"
:node="child" :node="child"
@@ -29,20 +37,31 @@
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:check-strictly="checkStrictly" :check-strictly="checkStrictly"
:expand-on-click-node="expandOnClickNode" :expand-on-click-node="expandOnClickNode"
:highlight-current="highlightCurrent"
:current-node-key="currentNodeKey"
:indent="indent"
:icon-size="iconSize"
:checkbox-size="checkboxSize"
:expand-icon="expandIcon"
:collapse-icon="collapseIcon"
:depth="depth + 1" :depth="depth + 1"
@node-click="$emit('node-click', $event)" @node-click="$emit('node-click', $event)"
@check-change="$emit('check-change', $event)"> @check-change="$emit('check-change', $event)"
<template #default="{ nodeData, level }"> >
<slot name="default" :nodeData="nodeData" :level="level"></slot> <template #default="slotProps">
<slot name="default" v-bind="slotProps"></slot>
</template> </template>
</tree-node> </TreeNode>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import TreeNode from './tree-node.vue';
export default { export default {
name: 'tree-node', name: 'tree-node',
components: { TreeNode },
props: { props: {
node: { node: {
type: Object, type: Object,
@@ -64,9 +83,37 @@ export default {
type: Boolean, type: Boolean,
default: true default: true
}, },
highlightCurrent: {
type: Boolean,
default: false
},
currentNodeKey: {
type: [String, Number],
default: ''
},
indent: {
type: [String, Number],
default: 32
},
iconSize: {
type: [String, Number],
default: 14
},
checkboxSize: {
type: [String, Number],
default: 16
},
expandIcon: {
type: String,
default: 'play-right-fill'
},
collapseIcon: {
type: String,
default: 'arrow-down-fill'
},
depth: { depth: {
type: Number, type: Number,
default: 0 default: 0
} }
}, },
computed: { computed: {
@@ -74,12 +121,29 @@ export default {
return this.node[this.props.children] && this.node[this.props.children].length > 0; return this.node[this.props.children] && this.node[this.props.children].length > 0;
}, },
isExpanded() { isExpanded() {
return this.node.expanded === undefined ? false : this.node.expanded; return this.node.expanded === undefined ? false : this.node.expanded;
},
isDisabled() {
const disabledKey = this.props.disabled || 'disabled';
return !!this.node[disabledKey];
},
isCurrent() {
if (!this.highlightCurrent || this.currentNodeKey === '' || this.currentNodeKey === null || this.currentNodeKey === undefined) {
return false;
}
return String(this.node[this.props.nodeKey]) === String(this.currentNodeKey);
},
labelClass() {
return {
'u-tree-node-label--current': this.isCurrent,
'u-tree-node-label--disabled': this.isDisabled
};
} }
}, },
emits: ['node-click', 'check-change'], emits: ['node-click', 'check-change'],
methods: { methods: {
toggle() { toggle() {
if (this.isDisabled) return;
if (this.expandOnClickNode && this.hasChildren) { if (this.expandOnClickNode && this.hasChildren) {
this.node.expanded = !this.node.expanded; this.node.expanded = !this.node.expanded;
} }
@@ -91,11 +155,11 @@ export default {
this.updateChildCheckStatus(this.node, checked); this.updateChildCheckStatus(this.node, checked);
this.updateParentCheckStatus(this.node); this.updateParentCheckStatus(this.node);
} }
this.$emit('check-change', this.node); this.$emit('check-change', this.node, checked);
}, },
updateChildCheckStatus(node, checked) { updateChildCheckStatus(node, checked) {
if (node[this.props.children]) { if (node[this.props.children]) {
node[this.props.children].forEach(child => { node[this.props.children].forEach((child) => {
child.checked = checked; child.checked = checked;
this.updateChildCheckStatus(child, checked); this.updateChildCheckStatus(child, checked);
}); });
@@ -104,9 +168,7 @@ export default {
updateParentCheckStatus(node) { updateParentCheckStatus(node) {
let parent = this.$parent; let parent = this.$parent;
while (parent && parent.node) { while (parent && parent.node) {
const allChecked = parent.node[this.props.children].every( const allChecked = parent.node[this.props.children].every((child) => child.checked);
child => child.checked
);
parent.node.checked = allChecked; parent.node.checked = allChecked;
parent = parent.$parent; parent = parent.$parent;
} }
@@ -120,9 +182,31 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: 20px; min-height: 72rpx;
padding-right: 20rpx;
} }
.u-tree-node-toggle { .u-tree-node-toggle {
margin-right: 5px; margin-right: 8rpx;
flex-shrink: 0;
} }
</style>
.u-tree-node-toggle--placeholder {
width: 14px;
}
.u-tree-node-label {
flex: 1;
font-size: 28rpx;
color: #333;
}
.u-tree-node-label--current {
color: #2667E9;
font-weight: 600;
}
.u-tree-node-label--disabled {
color: #c0c4cc;
}
</style>

View File

@@ -1,19 +1,27 @@
<template> <template>
<view class="u-tree"> <view class="u-tree">
<tree-node <TreeNode
v-for="node in treeData" v-for="node in treeData"
:key="node[props.nodeKey]" :key="node[nodeKeyName]"
:node="node" :node="node"
:props="props" :props="props"
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:check-strictly="checkStrictly" :check-strictly="checkStrictly"
:expand-on-click-node="expandOnClickNode" :expand-on-click-node="expandOnClickNode"
:highlight-current="highlightCurrent"
:current-node-key="innerCurrentNodeKey"
:indent="indent"
:icon-size="iconSize"
:checkbox-size="checkboxSize"
:expand-icon="expandIcon"
:collapse-icon="collapseIcon"
@node-click="handleNodeClick" @node-click="handleNodeClick"
@check-change="$emit('check-change', $event)"> @check-change="handleCheckChange"
<template #default="{ nodeData, level }"> >
<slot :node="nodeData" :level="level"></slot> <template #default="slotProps">
<slot v-bind="slotProps"></slot>
</template> </template>
</tree-node> </TreeNode>
</view> </view>
</template> </template>
@@ -26,16 +34,21 @@ export default {
props: { props: {
data: { data: {
type: Array, type: Array,
required: true default: () => []
}, },
props: { props: {
type: Object, type: Object,
default: () => ({ default: () => ({
label: 'label', label: 'label',
children: 'children', children: 'children',
nodeKey: 'id' nodeKey: 'id',
disabled: 'disabled'
}) })
}, },
nodeKey: {
type: String,
default: ''
},
showCheckbox: { showCheckbox: {
type: Boolean, type: Boolean,
default: false default: false
@@ -44,67 +57,167 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
defaultExpandedKeys: {
type: Array,
default: () => []
},
defaultCheckedKeys: {
type: Array,
default: () => []
},
expandOnClickNode: { expandOnClickNode: {
type: Boolean, type: Boolean,
default: true default: true
}, },
checkOnClickNode: {
type: Boolean,
default: false
},
checkStrictly: { checkStrictly: {
type: Boolean, type: Boolean,
default: false default: false
},
accordion: {
type: Boolean,
default: false
},
highlightCurrent: {
type: Boolean,
default: false
},
currentNodeKey: {
type: [String, Number],
default: ''
},
indent: {
type: [String, Number],
default: 32
},
iconSize: {
type: [String, Number],
default: 14
},
checkboxSize: {
type: [String, Number],
default: 16
},
expandIcon: {
type: String,
default: 'play-right-fill'
},
collapseIcon: {
type: String,
default: 'arrow-down-fill'
} }
}, },
data() { data() {
return { return {
treeData: [] treeData: [],
innerCurrentNodeKey: ''
}; };
}, },
created() { computed: {
this.initTree(); nodeKeyName() {
return this.nodeKey || this.props.nodeKey || 'id';
}
}, },
watch: { watch: {
data: { data: {
handler(newVal) { handler(newVal) {
this.treeData = JSON.parse(JSON.stringify(newVal)); this.initTree(newVal);
this.initExpandedState(this.treeData, this.defaultExpandAll);
}, },
deep: true, deep: true,
immediate: true immediate: true
},
currentNodeKey: {
handler(val) {
this.innerCurrentNodeKey = val;
},
immediate: true
} }
}, },
emits: ['node-click', 'check-change'], emits: ['node-click', 'check-change', 'check', 'node-expand', 'node-collapse', 'current-change'],
methods: { methods: {
initTree() { initTree(data = this.data) {
this.treeData = JSON.parse(JSON.stringify(this.data)); this.treeData = JSON.parse(JSON.stringify(data || []));
this.initExpandedState(this.treeData, this.defaultExpandAll); this.applyDefaultState(this.treeData);
}, },
initExpandedState(nodes, expanded) { applyDefaultState(nodes, parent = null) {
nodes.forEach(node => { nodes.forEach((node) => {
node.expanded = expanded; const key = node[this.nodeKeyName];
if (node[this.props.children]) { if (this.defaultExpandAll) {
this.initExpandedState(node[this.props.children], expanded); node.expanded = true;
} else if (this.defaultExpandedKeys.length) {
node.expanded = this.defaultExpandedKeys.map(String).includes(String(key));
} else {
node.expanded = false;
}
if (this.defaultCheckedKeys.length) {
node.checked = this.defaultCheckedKeys.map(String).includes(String(key));
}
const children = node[this.props.children];
if (children && children.length) {
this.applyDefaultState(children, node);
} }
}); });
}, },
handleNodeClick(node) { handleNodeClick(node) {
if (this.highlightCurrent) {
const oldKey = this.innerCurrentNodeKey;
this.innerCurrentNodeKey = node[this.nodeKeyName];
this.$emit('current-change', node, oldKey);
}
if (this.checkOnClickNode && this.showCheckbox) {
node.checked = !node.checked;
this.handleCheckChange(node, node.checked);
}
this.$emit('node-click', node); this.$emit('node-click', node);
}, },
/** handleCheckChange(node, checked) {
* 直接递归 treeData 获取所有 checked 的节点 this.$emit('check-change', node, checked);
*/ this.$emit('check', node, {
getCheckedNodes() { checkedNodes: this.getCheckedNodes(),
const traverse = (nodes) => { checkedKeys: this.getCheckedKeys(),
let result = []; halfCheckedNodes: [],
nodes.forEach(node => { halfCheckedKeys: []
if (node.checked) { });
result.push(node); },
} traverse(nodes, callback) {
if (node[this.props.children] && node[this.props.children].length > 0) { nodes.forEach((node) => {
result = result.concat(traverse(node[this.props.children])); callback(node);
} const children = node[this.props.children];
}); if (children && children.length) {
return result; this.traverse(children, callback);
}; }
return traverse(this.treeData); });
},
getCheckedNodes(leafOnly = false) {
const result = [];
this.traverse(this.treeData, (node) => {
if (!node.checked) return;
const children = node[this.props.children] || [];
if (leafOnly && children.length) return;
result.push(node);
});
return result;
},
getCheckedKeys(leafOnly = false) {
return this.getCheckedNodes(leafOnly).map((node) => node[this.nodeKeyName]);
},
setCurrentKey(key) {
this.innerCurrentNodeKey = key;
},
getCurrentKey() {
return this.innerCurrentNodeKey;
},
getCurrentNode() {
let current = null;
this.traverse(this.treeData, (node) => {
if (String(node[this.nodeKeyName]) === String(this.innerCurrentNodeKey)) {
current = node;
}
});
return current;
} }
} }
}; };
@@ -114,4 +227,4 @@ export default {
.u-tree { .u-tree {
font-size: 28rpx; font-size: 28rpx;
} }
</style> </style>

View File

@@ -0,0 +1,21 @@
## 1.0.22026-04-28
插槽去掉data字段
## 1.0.12026-04-26
预览图片
## 1.0.02026-04-26
## [1.0.0] - 2026-04-26
### 🚀 首次发布
-**树形展示**:基础树形列表渲染,支持无限层级数据展示
-**展开/折叠**:节点可展开折叠,支持深层递归展开所有节点
-**手风琴模式**:同级节点只保留一个展开,保持界面整洁
-**多选复选框**:显示复选框,支持严格模式(父子独立)和非严格模式(父子联动),父节点显示半选状态
-**单选模式**`multiple` 属性控制,关闭后仅允许选中一个节点
-**交错动画**:展开/折叠时子节点有平滑的下落和收起动画,视觉流畅
-**自定义节点内容**:通过 `#node` 插槽自定义节点内容,图标也可通过 `#icon` 插槽替换
-**固定宽高滚动**:通过 `height``width` 属性限制容器,内容溢出时自动出现滚动条
-**丰富的方法暴露**:提供全选、清空选中、指定节点选中、获取选中节点等方法
-**灵活字段映射**:支持自定义 `nodeKey``labelKey``childrenKey` 等字段名
-**微信小程序兼容**:小程序端使用纯文本箭头图标,性能优化,插槽可选择性开启
-**依赖声明**:插件根目录 `package.json` 中声明依赖 `@dcloudio/uni-ui`,导入时自动安装

View File

@@ -0,0 +1,848 @@
<script setup>
import { computed, nextTick, provide, reactive, ref, watch } from 'vue'
const props = defineProps({
data: { type: Array, default: () => [] },
nodeKey: { type: String, default: 'id' },
labelKey: { type: String, default: 'label' },
childrenKey: { type: String, default: 'children' },
indent: { type: Number, default: 20 },
showCheckbox: { type: Boolean, default: false },
defaultExpandAll: { type: Boolean, default: false },
defaultExpandedKeys: { type: Array, default: () => [] },
defaultCheckedKeys: { type: Array, default: () => [] },
checkStrictly: { type: Boolean, default: true }, // 是否严格模式(父子互不关联)
emptyText: { type: String, default: '暂无数据' },
checkedKey: { // 节点的 checked 状态字段名
type: String,
default: '_checked',
},
accordion: { // 手风琴
type: Boolean,
default: false,
},
multiple: { // 多选
type: Boolean,
default: false,
},
isDeepExpand: { // 方法是否深层展开
type: Boolean,
default: true,
},
height: { type: Number, default: 500 },
width: { type: Number, default: 300 },
labelSlot: { // 微信小程序不适用插槽的时候就不要打开了,真的无语了
type: Boolean,
default: false,
},
})
const emit = defineEmits(['node-click', 'node-expand', 'node-collapse', 'check-change', 'check'])
// 辅助函数
const getNodeKey = node => node[props.nodeKey]
function hasChildren(node) {
const children = node[props.childrenKey]
return children && Array.isArray(children) && children.length > 0
}
// 展开状态 (reactive 对象,避免克隆原始数据)
const expandedState = reactive({})
// 初始化展开状态
function initExpanded() {
Object.keys(expandedState).forEach(k => delete expandedState[k])
if (props.defaultExpandAll) {
const setAll = (list) => {
list.forEach((item) => {
expandedState[getNodeKey(item)] = true
const children = item[props.childrenKey]
if (children)
setAll(children)
})
}
setAll(props.data)
}
else if (props.defaultExpandedKeys.length) {
props.defaultExpandedKeys.forEach((key) => { expandedState[key] = true })
}
}
initExpanded()
watch(() => props.data, initExpanded, { deep: false })
// 扁平的节点列表 (递归遍历,根据展开状态决定是否包含子节点)
const flatData = computed(() => {
const result = []
const flatten = (list, level = 0, parent = null) => {
if (!list?.length)
return
list.forEach((node, idx) => {
const key = getNodeKey(node)
const expanded = !!expandedState[key]
const _node = reactive({
...node,
_level: level,
_expanded: expanded,
[props.checkedKey]: false,
_indeterminate: false,
_parent: parent,
_siblingIndex: idx, // 关键:同级索引
raw: node,
})
result.push(_node)
if (expanded && hasChildren(node)) {
flatten(node[props.childrenKey], level + 1, _node)
}
})
}
flatten(props.data)
return result
})
// 多选逻辑
const checkedKeys = reactive(new Set())
// 初始化默认勾选
watch(() => props.defaultCheckedKeys, (keys) => {
checkedKeys.clear()
keys.forEach(k => checkedKeys.add(k))
}, { immediate: true })
/**
* 递归计算并设置节点及其子树的 checked/indeterminate 状态
* @param {object} node 原始数据节点
* @param {object} flatNode 扁平化节点映射key -> reactive node用于写入状态
*/
function syncNodeState(node, flatNodeMap) {
if (!node)
return { checked: false, indeterminate: false }
const nodeKey = getNodeKey(node)
const children = node[props.childrenKey] || []
if (props.checkStrictly) {
// 严格模式:状态完全由 checkedKeys 决定,不计算父子关系
const isChecked = checkedKeys.has(nodeKey)
if (flatNodeMap.has(nodeKey)) {
const flatNode = flatNodeMap.get(nodeKey)
flatNode[props.checkedKey] = isChecked
flatNode._indeterminate = false
}
// 递归子节点以刷新它们的视图(但不会反向修改父节点)
children.forEach(child => syncNodeState(child, flatNodeMap))
return { checked: isChecked, indeterminate: false }
}
// 非严格模式:需要父子联动
let allChildrenChecked = true
let hasCheckedChild = false
let hasIndeterminateChild = false
for (const child of children) {
const childResult = syncNodeState(child, flatNodeMap)
if (!childResult.checked)
allChildrenChecked = false
if (childResult.checked)
hasCheckedChild = true
if (childResult.indeterminate)
hasIndeterminateChild = true
}
let finalChecked, finalIndeterminate
if (children.length === 0) {
// 叶子节点:状态完全由 checkedKeys 决定
finalChecked = checkedKeys.has(nodeKey)
finalIndeterminate = false
}
else {
// 父节点:状态仅取决于子节点,自身不自动被勾选
if (allChildrenChecked && children.length > 0) {
// 所有子节点全选 → 父节点为半选(不自动加入 checkedKeys
checkedKeys.delete(nodeKey)
finalChecked = false
finalIndeterminate = true
}
else if (hasCheckedChild || hasIndeterminateChild) {
// 部分子节点选中或存在半选 → 父节点为半选
checkedKeys.delete(nodeKey)
finalChecked = false
finalIndeterminate = true
}
else {
// 没有子节点被选中 → 父节点未选中
checkedKeys.delete(nodeKey)
finalChecked = false
finalIndeterminate = false
}
}
// 将状态写入当前可见的扁平节点(如果该节点已展开)
if (flatNodeMap.has(nodeKey)) {
const flatNode = flatNodeMap.get(nodeKey)
flatNode[props.checkedKey] = finalChecked
flatNode._indeterminate = finalIndeterminate
}
return { checked: finalChecked, indeterminate: finalIndeterminate }
}
// function syncNodeState(node, flatNodeMap) {
// if (!node)
// return { checked: false, indeterminate: false }
// const nodeKey = getNodeKey(node)
// const children = node[props.childrenKey] || []
// if (props.checkStrictly) {
// // 严格模式:状态完全由 checkedKeys 决定,不计算父子关系
// const isChecked = checkedKeys.has(nodeKey)
// if (flatNodeMap.has(nodeKey)) {
// const flatNode = flatNodeMap.get(nodeKey)
// flatNode[props.checkedKey] = isChecked
// flatNode._indeterminate = false // 严格模式没有半选
// }
// // 仍需递归子节点以刷新它们的视图(但不会反向修改父节点)
// children.forEach(child => syncNodeState(child, flatNodeMap))
// return { checked: isChecked, indeterminate: false }
// }
// // 非严格模式:需要父子联动
// let allChildrenChecked = true
// let hasCheckedChild = false
// let hasIndeterminateChild = false
// for (const child of children) {
// const childResult = syncNodeState(child, flatNodeMap)
// if (!childResult.checked)
// allChildrenChecked = false
// if (childResult.checked)
// hasCheckedChild = true
// if (childResult.indeterminate)
// hasIndeterminateChild = true
// }
// let finalChecked, finalIndeterminate
// if (children.length === 0) {
// finalChecked = checkedKeys.has(nodeKey)
// finalIndeterminate = false
// }
// else {
// if (allChildrenChecked && children.length > 0) {
// checkedKeys.add(nodeKey)
// finalChecked = true
// finalIndeterminate = false
// }
// else if (hasCheckedChild || hasIndeterminateChild) {
// checkedKeys.delete(nodeKey)
// finalChecked = false
// finalIndeterminate = true
// }
// else {
// checkedKeys.delete(nodeKey)
// finalChecked = false
// finalIndeterminate = false
// }
// }
// if (flatNodeMap.has(nodeKey)) {
// const flatNode = flatNodeMap.get(nodeKey)
// flatNode[props.checkedKey] = finalChecked
// flatNode._indeterminate = finalIndeterminate
// }
// return { checked: finalChecked, indeterminate: finalIndeterminate }
// }
// 递归更新节点勾选状态 (由外部调用或响应勾选变化)
function updateCheckedState() {
if (props.checkStrictly) {
// 严格模式:直接同步,无需父子计算
flatData.value.forEach((node) => {
const key = getNodeKey(node)
node[props.checkedKey] = checkedKeys.has(key)
node._indeterminate = false
})
return
}
// 非严格模式:需要根据子节点状态计算父节点的 checked 和 indeterminate
// 构建 key -> reactive node 映射
const flatNodeMap = new Map()
flatData.value.forEach(n => flatNodeMap.set(getNodeKey(n), n))
// 递归同步整棵树的状态(处理父子联动)
const syncRecursive = (node) => {
if (!node)
return { checked: false, indeterminate: false }
const children = node[props.childrenKey] || []
let allChildrenChecked = true
let hasCheckedChild = false
let hasIndeterminateChild = false
for (const child of children) {
const childResult = syncRecursive(child)
if (!childResult.checked)
allChildrenChecked = false
if (childResult.checked)
hasCheckedChild = true
if (childResult.indeterminate)
hasIndeterminateChild = true
}
const nodeKey = getNodeKey(node)
let finalChecked, finalIndeterminate
if (children.length === 0) {
finalChecked = checkedKeys.has(nodeKey)
finalIndeterminate = false
}
else {
if (allChildrenChecked && children.length > 0) {
checkedKeys.add(nodeKey)
finalChecked = true
finalIndeterminate = false
}
else if (hasCheckedChild || hasIndeterminateChild) {
checkedKeys.delete(nodeKey)
finalChecked = false
finalIndeterminate = true
}
else {
checkedKeys.delete(nodeKey)
finalChecked = false
finalIndeterminate = false
}
}
if (flatNodeMap.has(nodeKey)) {
const flatNode = flatNodeMap.get(nodeKey)
flatNode[props.checkedKey] = finalChecked
flatNode._indeterminate = finalIndeterminate
}
return { checked: finalChecked, indeterminate: finalIndeterminate }
}
props.data.forEach(root => syncRecursive(root))
}
// 勾选处理
function handleCheck(node) {
const key = getNodeKey(node)
if (props.checkStrictly) {
// 严格模式:只切换当前节点
if (checkedKeys.has(key)) {
checkedKeys.delete(key)
}
else {
checkedKeys.add(key)
}
updateCheckedState()
emit('check-change', node, checkedKeys.has(key))
emit('check', node, { checkedKeys: [...checkedKeys] })
return
}
// 非严格模式:处理当前节点及子节点
const shouldCheck = !checkedKeys.has(key)
const toggleRecursive = (n) => {
const nKey = getNodeKey(n)
if (shouldCheck) {
checkedKeys.add(nKey)
}
else {
checkedKeys.delete(nKey)
}
const children = n[props.childrenKey] || []
children.forEach(child => toggleRecursive(child))
}
toggleRecursive(node)
updateCheckedState()
emit('check-change', node, shouldCheck)
emit('check', node, { checkedKeys: [...checkedKeys] })
}
// 展开/折叠
function toggleExpand(node) {
const key = getNodeKey(node)
const wasExpanded = expandedState[key]
if (wasExpanded) {
delete expandedState[key]
emit('node-collapse', node)
}
else {
if (props.accordion) {
const siblings = node._parent
? (node._parent[props.childrenKey] || [])
: props.data
siblings.forEach((sib) => {
const sibKey = getNodeKey(sib)
if (sibKey !== key && expandedState[sibKey]) {
delete expandedState[sibKey]
}
})
}
expandedState[key] = true
emit('node-expand', node)
}
nextTick(() => updateCheckedState())
}
// 节点点击
function handleNodeClick(node) {
emit('node-click', node)
}
// 暴露方法
const getCheckedKeys = () => [...checkedKeys]
const getCheckedNodes = () => flatData.value.filter(n => n[props.checkedKey]).map(n => n.raw)
function setCheckedKeys(keys) {
checkedKeys.clear()
keys.forEach(k => checkedKeys.add(k))
updateCheckedState()
}
function expandAll() {
if (!props.isDeepExpand) {
// 展开浅层
flatData.value.forEach((node) => {
const key = getNodeKey(node)
if (hasChildren(node))
expandedState[key] = true
})
}
else {
// 递归展开所有
const expandRecursively = (list) => {
if (!list?.length)
return
list.forEach((item) => {
if (hasChildren(item)) {
expandedState[getNodeKey(item)] = true
expandRecursively(item[props.childrenKey])
}
})
}
expandRecursively(props.data)
}
nextTick(() => updateCheckedState())
}
function collapseAll() {
Object.keys(expandedState).forEach(k => delete expandedState[k])
// 折叠后同步选中状态
nextTick(() => updateCheckedState())
}
// 清空所有勾选
function clearChecked() {
checkedKeys.clear()
updateCheckedState()
}
// 解析 key/节点,返回 { key, node },保证 key 类型与节点真实 nodeKey 一致
function resolveKeyAndNode(keyOrNode) {
let key, node
if (typeof keyOrNode === 'object' && keyOrNode !== null) {
node = keyOrNode
key = getNodeKey(node) // 直接取真实 key
}
else {
// 传入的是原始 key可能是数字或字符串
node = findNodeByKey(keyOrNode, props.data)
if (node) {
key = getNodeKey(node) // 使用节点的真实 key保证类型一致
}
else {
key = keyOrNode // 找不到节点则保持原样(后续会过滤掉)
}
}
return { key, node }
}
// 递归查找原始节点
function findNodeByKey(key, list) {
if (!list)
return null
for (const item of list) {
const itemKey = getNodeKey(item)
// 首先严格匹配
if (itemKey === key)
return item
// 如果严格匹配失败,尝试类型转换后匹配
if (typeof itemKey === 'number' && itemKey === Number(key))
return item
if (typeof itemKey === 'string' && itemKey === String(key))
return item
// 递归子节点
const found = findNodeByKey(key, item[props.childrenKey])
if (found)
return found
}
return null
}
// 设置指定节点的勾选状态
function setNodeChecked(keyOrNode, checked) {
const { key, node } = resolveKeyAndNode(keyOrNode)
if (!key)
return // 找不到节点或 key 无效
if (checked) {
checkedKeys.add(key)
}
else {
checkedKeys.delete(key)
}
// 非严格模式且是父节点时,处理所有子节点
if (!props.checkStrictly && node && hasChildren(node)) {
const toggleChildren = (children) => {
if (!children)
return
children.forEach((child) => {
const childKey = getNodeKey(child) // 子节点的真实 key类型一致
if (checked) {
checkedKeys.add(childKey)
}
else {
checkedKeys.delete(childKey)
}
toggleChildren(child[props.childrenKey])
})
}
toggleChildren(node[props.childrenKey])
}
updateCheckedState()
emit('check-change', node || key, checked)
emit('check', node || key, { checkedKeys: [...checkedKeys] })
}
// 全选(选中所有叶子节点)
function checkAll() {
const selectAllNodes = (list) => {
if (!list?.length)
return
list.forEach((item) => {
// 将当前节点加入选中(严格模式下每个节点独立)
checkedKeys.add(getNodeKey(item))
// 递归处理子节点
if (hasChildren(item)) {
selectAllNodes(item[props.childrenKey])
}
})
}
selectAllNodes(props.data)
updateCheckedState()
emit('check', null, { checkedKeys: [...checkedKeys] })
}
defineExpose({ getCheckedKeys, getCheckedNodes, setCheckedKeys, expandAll, collapseAll, clearChecked, setNodeChecked, checkAll })
// 初始化勾选状态
updateCheckedState()
</script>
<template>
<scroll-view scroll-x scroll-y enable-flex class="tree-wrapper">
<view class="xq-tree-scroll">
<view
class="xq-tree" :style="{
minWidth: `${width}rpx`,
minHeight: `${height}rpx`,
}"
>
<!-- 空数据提示 -->
<view v-if="flatData.length === 0" class="xq-tree-empty">
{{ emptyText }}
</view>
<TransitionGroup v-else name="tree-node" tag="view" class="xq-tree-list">
<view
v-for="(node, index) in flatData" :key="node[nodeKey] || index" class="xq-tree-node" :class="[
{ 'is-expanded': node._expanded, 'is-leaf': !hasChildren(node) },
]" :style="{
paddingLeft: `${(node._level || 0) * indent}px`,
animationDelay: `${(node._siblingIndex || 0) * 0.03}s`,
}"
>
<view class="xq-tree-node-content" @click="handleNodeClick(node)">
<!-- 展开/折叠图标 -->
<view class="xq-tree-node-icon" @click.stop="toggleExpand(node)">
<!-- #ifndef MP-WEIXIN -->
<text v-if="hasChildren(node)" class="uni-icon-warp">
<slot name="icon" :node="node">
<!-- <uni-icons type="right" size="18" class="icon-arrow" :class="{ 'is-expanded': node._expanded }" /> -->
<text class="icon-arrow" :class="{ 'is-expanded': node._expanded }">
</text>
</slot>
</text>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<text v-if="hasChildren(node)" class="icon-arrow" :class="{ 'is-expanded': node._expanded }">
</text>
<!-- #endif -->
<view v-else class="leaf-icon-placeholder"></view>
</view>
<!-- 复选框如果开启多选 -->
<view v-if="showCheckbox" class="xq-tree-node-checkbox" @click.stop="handleCheck(node)">
<view
class="checkbox-custom"
:class="[{ checked: node[checkedKey], indeterminate: node._indeterminate }]"
>
<text v-if="node[checkedKey]">
</text>
<text v-else-if="node._indeterminate">
</text>
</view>
</view>
<!-- 节点内容插槽 -->
<!-- #ifndef MP-WEIXIN -->
<slot
name="node" :node="node" :level="node._level" :expanded="node._expanded"
:checked="node[checkedKey]" :indeterminate="node._indeterminate" :is-leaf="!hasChildren(node)"
>
<text class="xq-tree-node-label">
{{ node[labelKey] }}
</text>
</slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<!-- 微信的插槽真的浪费性能 -->
<slot
v-if="labelSlot" name="node" :node="node" :data="node" :level="node._level"
:expanded="node._expanded" :checked="node[checkedKey]" :indeterminate="node._indeterminate"
/>
<text v-else class="xq-tree-node-label">
{{ node[labelKey] }}
</text>
<!-- #endif -->
</view>
</view>
</TransitionGroup>
</view>
</view>
</scroll-view>
</template>
<style scoped lang="scss">
.xq-tree {
display: inline-block;
/* ⚠️ 关键:宽度由内容撑开,不继承父容器宽度 */
/* 内容不换行 */
/* 至少撑满容器 */
box-sizing: border-box;
width: max-content;
/* 宽度由内容决定,不换行 */
white-space: nowrap;
}
.tree-wrapper {
width: 100%;
height: 100%;
// padding: 20rpx;
/* 确保节点不换行 */
}
.xq-tree-scroll {
display: inline-block;
/* ⚠️ 关键:宽度由内容撑开,不继承父容器宽度 */
white-space: nowrap;
/* 内容不换行 */
min-width: 100%;
/* 至少撑满容器 */
box-sizing: border-box;
background-color: #fff;
padding: 16rpx;
}
/* 小程序下 scroll-view 的滚动条样式由原生控制通常无法自定义H5 端同样可用 ::-webkit-scrollbar */
/* 下面的样式仅对 H5 端生效 */
.tree-wrapper::-webkit-scrollbar {
height: 8rpx;
width: 8rpx;
}
.tree-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
}
.tree-wrapper::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4rpx;
}
// 进入动画(直接使用之前的 keyframes但通过 TransitionGroup 自动触发)
.tree-node-enter-active {
animation: treeExpandIn 0.3s ease-out both;
// animation-delay 已通过内联 style 设置,不需要在这里写死
}
// 离开动画:使用 transition 让 max-height、padding、opacity 平滑过渡
.tree-node-leave-active {
animation: treeCollapseOut 0.3s ease-in both;
overflow: hidden; // 确保高度变化时内容不会溢出
}
.tree-node-leave-to {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}
// 需要的关键帧 remain unchanged (可以保留)
@keyframes treeExpandIn {
0% {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
100% {
max-height: 500px;
opacity: 1;
padding-top: 8rpx;
padding-bottom: 8rpx;
}
}
// 新增离开关键帧
@keyframes treeCollapseOut {
0% {
max-height: 500px; // 开始时的实际高度,取一个足够大的值
opacity: 1;
padding-top: 8rpx;
padding-bottom: 8rpx;
}
100% {
max-height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
}
// 节点基本样式微调
.xq-tree-node {
padding: 8rpx 0;
overflow: hidden;
transform-origin: top;
}
.xq-tree {
width: 100%;
}
.xq-tree-empty {
padding: 40rpx;
text-align: center;
color: #999;
}
.xq-tree-node {
padding: 8rpx 0;
}
.xq-tree-node-content {
display: flex;
align-items: center;
}
.xq-tree-node-icon {
width: 40rpx;
text-align: center;
flex-shrink: 0;
}
.xq-tree-node-checkbox {
margin-right: 12rpx;
flex-shrink: 0;
}
.xq-tree-node-label {
flex: 1;
white-space: nowrap;
}
.checkbox-custom {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4rpx;
}
.checkbox-custom.checked {
background-color: #007aff;
border-color: #007aff;
color: #fff;
}
.checkbox-custom.indeterminate {
background-color: #007aff;
border-color: #007aff;
color: #fff;
}
.leaf-icon-placeholder {
width: 100%;
height: 100%;
}
.uni-icon-warp {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
}
.icon-arrow.is-expanded {
transform: rotate(90deg);
}
.icon-arrow {
display: inline-block;
transition: transform 0.3s ease;
}
// #ifdef H5
*,
*::before,
*::after {
box-sizing: border-box;
}
// #endif
// #ifdef MP-WEIXIN
page {
box-sizing: border-box;
}
view,
text,
image,
button,
input,
textarea,
navigator,
scroll-view {
box-sizing: border-box;
}
// #endif
</style>

View File

@@ -0,0 +1,102 @@
{
"id": "xq-tree",
"displayName": "xq-tree 可选择的树形组件",
"version": "1.0.2",
"description": "一款基于 uni-app 的高性能树形组件,支持手风琴模式、多选/单选、父子联动、严格模式、自定义插槽及流畅的展开折叠动画完美兼容微信小程序、H5 和 App。",
"keywords": [
"xq-tree",
"tree",
"树形",
"展开/折叠",
"选中"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0",
"uni-app": "^4.0",
"uni-app-x": "^4.44"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "√"
},
"web": {
"safari": "-",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": "√",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "√",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,145 @@
# xq-tree 树形控件
基于 uni-app 的高性能树形控件,支持**手风琴模式**、**多选/单选**、**父子联动**、**严格模式**、**自定义节点插槽**、**交错展开折叠动画**及**固定宽高滚动**完美兼容微信小程序、H5 及 App。
---
## ✨ 核心特性
-**手风琴模式**:同级节点自动折叠,保持界面整洁
-**多选支持**:复选框半选/全选状态,父子联动或严格模式自由切换
-**单选模式**`multiple` 为 false 时仅允许选中一个节点
-**自定义节点内容**:通过插槽完全控制节点渲染,包括展开图标
-**交错动画**:展开/折叠时子节点有平滑的下落/收起动画,无空白闪烁
-**固定宽高滚动**:通过 `height`/`width` 属性限制容器,溢出自动出现滚动条
-**灵活字段映射**:自定义 `nodeKey``labelKey``childrenKey` 等字段名
-**丰富的方法**:全选、清空、指定节点选中、获取选中节点等
-**深层递归操作**`expandAll` 等方法支持递归展开整棵树
---
## 📦 安装与引入
将组件文件夹放入 `uni_modules/xq-tree/``components/` 目录,即可通过 easycom 自动引入,无需手动 import。
---
## 🚀 快速上手
```vue
<script setup>
import { ref } from 'vue'
const treeData = ref([
{
id: 1,
name: '一级 1',
children: [
{ id: 2, name: '二级 1-1' },
{ id: 3, name: '二级 1-2' }
]
}
])
function handleCheck(node, checkedState) {
console.log('当前节点:', node)
console.log('选中状态:', checkedState)
}
</script>
<template>
<xq-tree
:data="treeData"
node-key="id"
label-key="name"
show-checkbox
default-expand-all
@check="handleCheck"
/>
</template>
```
---
## ⚙️ Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| data | Array | `[]` | 树形数据源 |
| nodeKey | String | `'id'` | 节点唯一标识字段 |
| labelKey | String | `'label'` | 节点显示文本字段 |
| childrenKey | String | `'children'` | 子节点字段名 |
| indent | Number | `20` | 每级缩进像素 |
| showCheckbox | Boolean | `false` | 是否显示复选框 |
| defaultExpandAll | Boolean | `false` | 是否默认展开所有节点 |
| defaultExpandedKeys | Array | `[]` | 默认展开的节点 key 数组 |
| defaultCheckedKeys | Array | `[]` | 默认勾选的节点 key 数组 |
| checkStrictly | Boolean | `true` | 是否严格模式(父子互不关联) |
| accordion | Boolean | `false` | 是否开启手风琴模式 |
| multiple | Boolean | `false` | 是否开启多选false 时为单选模式)本期并未实现 |
| isDeepExpand | Boolean | `true` | 调用 expandAll 时是否递归展开所有子节点 |
| emptyText | String | `'暂无数据'` | 空数据时显示的文本 |
| checkedKey | String | `'_checked'` | 节点数据中存储勾选状态的字段名 |
| height | Number | `500` | 容器最小高度rpx溢出时垂直滚动 |
| width | Number | `300` | 容器最小宽度rpx溢出时水平滚动 |
| labelSlot | Boolean | `false` | 微信小程序是否启用自定义节点插槽 |
---
## 📤 Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| node-click | `(node)` | 节点被点击时触发 |
| node-expand | `(node)` | 节点展开时触发 |
| node-collapse | `(node)` | 节点折叠时触发 |
| check-change | `(node, checked)` | 复选框状态改变时触发 |
| check | `(node, { checkedKeys })` | 点击复选框后触发,返回当前所有选中 keys |
---
## 🧩 Slots
| 插槽名 | 作用域 | 说明 |
|--------|--------|------|
| node | `{ node, level, expanded, checked, indeterminate, isLeaf }` | 自定义节点内容(需设置 `labelSlot` 为 true 在微信小程序中启用) |
| icon | `{ node }` | 自定义展开/折叠图标(微信小程序不支持) |
> 微信小程序中,由于性能问题,`node` 插槽需手动设置 `labelSlot` 为 `true` 才会启用,否则默认渲染纯文本。
---
## 🛠️ 方法 (通过 ref 调用)
| 方法名 | 参数 | 说明 |
|--------|------|------|
| getCheckedKeys() | - | 返回当前所有勾选的节点 key 数组 |
| getCheckedNodes() | - | 返回当前所有勾选的节点原始数据数组 |
| setCheckedKeys(keys) | `keys: Array` | 设置当前勾选的节点 key 数组 |
| expandAll() | - | 展开所有节点(根据 `isDeepExpand` 决定是否递归) |
| collapseAll() | - | 折叠所有节点 |
| clearChecked() | - | 清空所有勾选 |
| setNodeChecked(keyOrNode, checked) | `keyOrNode`:节点的 key 或节点对象<br>`checked`:布尔值 | 设置指定节点的勾选状态 |
| checkAll() | - | 全选所有节点 |
---
## 🎨 动画说明
组件内置了基于 CSS 关键帧的进入/离开动画,通过 `TransitionGroup` 实现:
- **展开动画**:子节点从上方逐渐撑开并淡入,同级子节点有 0.03s 的错开延迟
- **折叠动画**:节点向上收起并淡出,动画完成后自动移除
可通过覆盖 `.tree-node-enter-active``.tree-node-leave-active``@keyframes treeExpandIn``treeCollapseOut` 样式来调整动画时长或效果。
---
## 📄 License
MIT
---
**文档版本**v1.0.0
**最后更新**2026-04-26

View File

@@ -1 +0,0 @@
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./common/vendor.js"),o=require("./uni_modules/uview-plus/index.js");Math;const t={onLaunch:function(){console.log("App Launch")},onShow:function(){console.log("App Show")},onHide:function(){console.log("App Hide")}};e.index.addInterceptor("chooseImage",{success(o){const t=["bmp","gif","jpg","jpeg","png"],n=[],p=[];let i=!1,s="";o.tempFiles.forEach(((e,c)=>{const u=(e.path||o.tempFilePaths[c]).split("?")[0].split(".").pop().toLowerCase();t.includes(u)?(p.push(e),n.push(o.tempFilePaths[c])):(i=!0,s=u)})),i&&e.index.showToast({title:`已过滤不支持的 .${s} 格式图片,请上传 png/jpg/jpeg/gif/bmp`,icon:"none",duration:3500}),o.tempFilePaths=n,o.tempFiles=p}});const n=["bmp","gif","jpg","jpeg","png","doc","docx","xls","xlsx","ppt","pptx","html","htm","txt","rar","zip","gz","bz2","mp4","avi","rmvb","pdf"],p=["qiniup.com","qbox.me"];function i(o){if(!o)return!0;const t=o.split("?")[0].split(".").pop().toLowerCase();return!!n.includes(t)||(e.index.showToast({title:`不支持 .${t} 格式,请上传合规的文件或图片`,icon:"none",duration:3e3}),!1)}function s(){const n=e.createSSRApp(t);return n.use(o.uviewPlus),{app:n}}e.index.addInterceptor("uploadFile",{invoke(e){const o=e.url||"";return p.some((e=>o.includes(e)))||o.includes("/frontend/attachment/upload"),!!i(e.filePath)&&e}}),s().app.mount("#app"),exports.createApp=s;

View File

@@ -1,85 +0,0 @@
{
"pages": [
"pages/index/index",
"pages/map/map",
"pages/plandetail/plandetail",
"pages/Inspectionresult/Inspectionresult",
"pages/Inspectionresult/list",
"pages/Inspectionresult/detail",
"pages/membermanagemen/membermanagemen",
"pages/corporateInformation/corporateInformation",
"pages/editcompanInformation/editcompanInformation",
"pages/checklist/checklist",
"pages/checklist/detail",
"pages/editchecklist/editchecklist",
"pages/Inspectionlog/Inspectionlog",
"pages/Inspectionchecklist/Inspectionchecklist",
"pages/Idphotomanagement/Idphotomanagement",
"pages/hiddendanger/Inspection",
"pages/hiddendanger/add",
"pages/hiddendanger/view",
"pages/hiddendanger/detail2",
"pages/hiddendanger/rectification",
"pages/hiddendanger/acceptance",
"pages/hiddendanger/assignment",
"pages/closeout/application",
"pages/closeout/editor",
"pages/equipmentregistration/equipmentregistration",
"pages/area/management",
"pages/Inspectionwarning/Inspectionwarning",
"pages/personalcenter/my",
"pages/personalcenter/helpcenter",
"pages/personalcenter/notification",
"pages/personalcenter/settings",
"pages/personalcenter/account",
"pages/personalcenter/edit",
"pages/login/login",
"pages/login/reg",
"pages/login/enterprise",
"pages/login/success",
"pages/login/forget",
"pages/login/agreement"
],
"window": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#007aff",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#007aff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabbar_icon/home_icon.png",
"selectedIconPath": "static/tabbar_icon/home_selectedIcon.png"
},
{
"pagePath": "pages/Inspectionwarning/Inspectionwarning",
"text": "预警",
"iconPath": "static/tabbar_icon/yujing_icon.png",
"selectedIconPath": "static/tabbar_icon/yujing_selectedIcon.png"
},
{
"pagePath": "pages/personalcenter/my",
"text": "我的",
"iconPath": "static/tabbar_icon/mine_icon.png",
"selectedIconPath": "static/tabbar_icon/mine_selectedIcon.png"
}
]
},
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于选择隐患位置"
}
},
"requiredPrivateInfos": [
"chooseLocation",
"getLocation"
],
"usingComponents": {}
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";exports._imports_0="/static/home_icon/jianbianbeijing.png",exports._imports_0$1="/static/jianchabiao/biaodan.svg",exports._imports_0$2="/static/yinhuan_detail/status.png",exports._imports_0$3="/static/yujin/yujin_sousuo.png",exports._imports_0$4="/static/my/edit.png",exports._imports_0$5="/static/my/Customer service.png",exports._imports_0$6="/static/index/index_bg.png",exports._imports_0$7="/static/index/phone.png",exports._imports_0$8="/static/index/蒙版组 260.png",exports._imports_1="/static/yinhuan_detail/date.png",exports._imports_1$1="/static/yujin/yujin_tongji.png",exports._imports_1$2="/static/my/Notification.png",exports._imports_1$3="/static/my/Phone.png",exports._imports_1$4="/static/index/lock.png",exports._imports_2="/static/my/Account.png";

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";const e=require("../common/vendor.js"),o={__name:"AreaFormPopup",props:{visible:{type:Boolean,default:!1},isEdit:{type:Boolean,default:!1},editData:{type:Object,default:()=>({})},loading:{type:Boolean,default:!1}},emits:["update:visible","submit","close"],setup(o,{emit:a}){const t=o,l=a,n=e.reactive({name:"",color:"#D92121"}),i=[{name:"红色",value:"#D92121"},{name:"橙色",value:"#FF8822"},{name:"黄色",value:"#FFCC00"},{name:"蓝色",value:"#165DFF"}],r=i.map((e=>e.value)),c=e.computed((()=>{const e=i.find((e=>e.value===n.color));return e?`${e.name} ${e.value}`:n.color}));e.watch((()=>t.editData),(e=>{e&&Object.keys(e).length>0&&(n.name=e.name||"",n.color=(e=>{if(!e)return i[0].value;const o=String(e).toUpperCase();return r.find((e=>e.toUpperCase()===o))||i[0].value})(e.color))}),{immediate:!0,deep:!0}),e.watch((()=>t.visible),(e=>{e||u()}));const u=()=>{n.name="",n.color="#D92121"},s=()=>{l("update:visible",!1),l("close")},m=()=>{n.name?r.includes(n.color)?l("submit",{name:n.name,color:n.color}):e.index.showToast({title:"请从预设颜色中选择",icon:"none"}):e.index.showToast({title:"请输入区域名称",icon:"none"})};return(a,t)=>e.e({a:o.visible},o.visible?{b:e.t(o.isEdit?"编辑区域":"新增区域"),c:e.o(s),d:n.name,e:e.o((e=>n.name=e.detail.value)),f:n.color,g:e.t(c.value),h:e.f(i,((o,a,t)=>({a:n.color===o.value?1:"",b:o.value,c:e.t(o.name),d:o.value,e:e.o((e=>{return a=o.value,void(n.color=a);var a}),o.value)}))),i:e.o(s),j:e.o(m),k:o.loading,l:e.o((()=>{})),m:e.o(s),n:e.gei(a,"")}:{})}},a=e._export_sfc(o,[["__scopeId","data-v-737ed489"]]);wx.createComponent(a);

View File

@@ -1 +0,0 @@
<view wx:if="{{a}}" bindtap="{{m}}" class="{{['popup-mask', 'data-v-737ed489', virtualHostClass]}}" style="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{n}}"><view class="popup-content data-v-737ed489" catchtap="{{l}}"><view class="popup-header data-v-737ed489"><view class="popup-title text-bold data-v-737ed489">{{b}}</view><view class="popup-close data-v-737ed489" bindtap="{{c}}">×</view></view><view class="popup-body data-v-737ed489"><view class="flex margin-bottom-sm data-v-737ed489"><view class="data-v-737ed489">区域名称</view><view class="text-red data-v-737ed489">*</view></view><input class="form-input data-v-737ed489" placeholder="请输入区域名称" value="{{d}}" bindinput="{{e}}"/><view class="flex margin-bottom-sm margin-top data-v-737ed489"><view class="data-v-737ed489">区域颜色</view><view class="text-red data-v-737ed489">*</view></view><view class="flex align-center margin-bottom-sm data-v-737ed489"><view class="color-preview data-v-737ed489" style="{{'background-color:' + f}}"></view><text class="margin-left-sm text-gray data-v-737ed489">{{g}}</text></view><view class="margin-bottom-sm text-gray data-v-737ed489">请选择颜色</view><view class="color-grid data-v-737ed489"><view wx:for="{{h}}" wx:for-item="item" wx:key="d" class="color-option data-v-737ed489" bindtap="{{item.e}}"><view class="{{['color-item', 'data-v-737ed489', item.a && 'color-item-active']}}" style="{{'background-color:' + item.b}}"></view><text class="color-label data-v-737ed489">{{item.c}}</text></view></view></view><view class="popup-footer data-v-737ed489"><button class="btn-cancel data-v-737ed489" bindtap="{{i}}">取消</button><button class="btn-confirm bg-blue data-v-737ed489" bindtap="{{j}}" loading="{{k}}">确定</button></view></view></view>

View File

@@ -1 +0,0 @@
.popup-mask.data-v-737ed489{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:999}.popup-content.data-v-737ed489{width:600rpx;background:#fff;border-radius:20rpx;overflow:hidden}.popup-header.data-v-737ed489{display:flex;justify-content:space-between;align-items:center;padding:30rpx;border-bottom:1rpx solid #eee}.popup-title.data-v-737ed489{font-size:32rpx}.popup-close.data-v-737ed489{font-size:40rpx;color:#999}.popup-body.data-v-737ed489{padding:30rpx}.popup-footer.data-v-737ed489{display:flex;padding:20rpx 30rpx 30rpx}.popup-footer button.data-v-737ed489{flex:1;height:80rpx;line-height:80rpx;border-radius:40rpx;font-size:30rpx;margin:0 10rpx}.popup-footer button.data-v-737ed489:after{border:none}.popup-footer .btn-cancel.data-v-737ed489{background:#f5f5f5;color:#666}.popup-footer .btn-confirm.data-v-737ed489{color:#fff}.form-input.data-v-737ed489{width:100%;height:70rpx;padding:0 20rpx;border:2rpx solid #dadbde;border-radius:8rpx;font-size:28rpx;box-sizing:border-box}.color-preview.data-v-737ed489{width:70rpx;height:70rpx;border-radius:8rpx;flex-shrink:0;border:2rpx solid #e5e5e5}.color-grid.data-v-737ed489{display:flex;flex-wrap:wrap;justify-content:space-between;gap:24rpx 0}.color-option.data-v-737ed489{width:25%;display:flex;flex-direction:column;align-items:center}.color-item.data-v-737ed489{width:80rpx;height:80rpx;border-radius:12rpx;border:4rpx solid transparent;box-sizing:border-box}.color-label.data-v-737ed489{margin-top:12rpx;font-size:24rpx;color:#666}.color-item-active.data-v-737ed489{border-color:#333;box-shadow:0 0 0 4rpx rgba(0,0,0,.08)}

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
{
"component": true,
"usingComponents": {
"up-upload": "../../uni_modules/uview-plus/components/u-upload/u-upload",
"up-input": "../../uni_modules/uview-plus/components/u-input/u-input",
"up-textarea": "../../uni_modules/uview-plus/components/u-textarea/u-textarea",
"u-popup": "../../uni_modules/uview-plus/components/u-popup/u-popup"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../common/vendor.js");if(!Array){e.resolveComponent("u-popup")()}Math||(a+(()=>"../../uni_modules/uview-plus/components/u-popup/u-popup.js"))();const a=()=>"./HazardFormPanel.js",o={__name:"HazardFormPopup",props:{show:{type:Boolean,default:!1},title:{type:String,default:"填写隐患信息"},mode:{type:String,default:"collect"},modelValue:{type:Object,default:null},extraParams:{type:Object,default:()=>({})},showDraftBanner:{type:Boolean,default:!1},bodyHeight:{type:String,default:""},canvasId:{type:String,default:"hazardFormWatermarkCanvas"}},emits:["update:show","update:modelValue","success","confirm","clear-draft"],setup(a,{expose:o,emit:l}){const t=a,r=l,u=e.computed({get:()=>t.show,set:e=>r("update:show",e)}),d=e.computed((()=>({title:t.title,mode:t.mode,modelValue:t.modelValue,extraParams:t.extraParams,showDraftBanner:t.showDraftBanner,bodyHeight:t.bodyHeight,canvasId:t.canvasId}))),n=()=>{u.value=!1},s=e.ref(null);return o({getPayload:(...e)=>{var a,o;return null==(o=null==(a=s.value)?void 0:a.getPayload)?void 0:o.call(a,...e)},applyPayload:(...e)=>{var a,o;return null==(o=null==(a=s.value)?void 0:a.applyPayload)?void 0:o.call(a,...e)},resetForm:(...e)=>{var a,o;return null==(o=null==(a=s.value)?void 0:a.resetForm)?void 0:o.call(a,...e)},validateForm:(...e)=>{var a,o;return null==(o=null==(a=s.value)?void 0:a.validateForm)?void 0:o.call(a,...e)}}),(a,o)=>({a:e.sr(s,"52d033ec-1,52d033ec-0",{k:"panelRef"}),b:e.o((e=>u.value=e)),c:e.o((e=>r("success",e))),d:e.o((e=>r("confirm",e))),e:e.o((e=>r("clear-draft"))),f:e.o((e=>u.value=!1)),g:e.p({...d.value,show:u.value}),h:e.o(n),i:e.o((e=>u.value=e)),j:e.p({mode:"center",round:"20","safe-area-inset-bottom":!1,show:u.value}),k:e.gei(a,"")})}};wx.createComponent(o);

View File

@@ -1 +0,0 @@
<u-popup wx:if="{{j}}" u-s="{{['d']}}" bindclose="{{h}}" u-i="52d033ec-0" bind:__l="__l" bindupdateShow="{{i}}" u-p="{{j}}" class="{{[virtualHostClass]}}" virtualHostClass="{{[virtualHostClass]}}" style="{{virtualHostStyle}}" virtualHostStyle="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" virtualHostHidden="{{virtualHostHidden || false}}" id="{{k}}" virtualHostId="{{k}}"><hazard-form-panel wx:if="{{g}}" class="r" virtualHostClass="r" u-r="panelRef" bindupdateShow="{{b}}" bindsuccess="{{c}}" bindconfirm="{{d}}" bindclearDraft="{{e}}" bindclose="{{f}}" u-i="52d033ec-1,52d033ec-0" bind:__l="__l" u-p="{{g}}"/></u-popup>

View File

@@ -1 +0,0 @@
"use strict";const t=require("../../utils/upload.js"),e=[{id:2,title:"一般隐患"},{id:3,title:"重大隐患"}],a=[{id:1,title:"部门检查"},{id:2,title:"督导检查"},{id:3,title:"企业自查"},{id:4,title:"行业互查"}];exports.AI_LEVEL_MAP={"轻微":0,"轻微隐患":0,"一般":0,"一般隐患":0,"重大":1,"重大隐患":1},exports.LEVEL_OPTIONS=e,exports.SOURCE_OPTIONS=a,exports.buildHiddenDangerParams=function(l,i={}){var r,n,d;const{formData:s,address:o,lng:u,lat:c,areaId:g,fileList:I}=l,f=null==(r=l.tagOptions)?void 0:r[s.tagIndex],m=s.tagId??(f?f.id:null);return{title:s.title,level:(null==(n=e[s.level])?void 0:n.id)||2,lng:u||0,lat:c||0,address:o||"",areaId:g||null,description:s.description||"",source:(null==(d=a[s.source])?void 0:d.title)||"",tagId:m,attachments:(I||[]).filter((t=>"success"===t.status)).map((e=>t.buildAttachmentItem(e))),regulationId:s.regulationId||null,...i}},exports.createEmptyHazardPayload=function(){return{formData:{title:"",level:0,source:0,description:"",tagIndex:0,tagId:null,regulationId:null,regulationName:""},address:"",lng:0,lat:0,areaId:"",areaName:"",fileList:[]}},exports.hasHazardFormContent=function(t){if(!t)return!1;const e=t.formData||{},a=t.fileList||[];return!!(e.title||e.description||e.regulationName||e.regulationId||t.address||t.areaId||t.areaName||a.length>0)},exports.hasValidHazardPayload=function(t){if(!t)return!1;const{formData:e,fileList:a}=t;return!!((null==e?void 0:e.title)&&(null==a?void 0:a.length)>0)};

View File

@@ -1 +0,0 @@
"use strict";require("../../common/vendor.js"),require("../../request/request.js");

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.hazard-detail-panel-v2.data-v-1547c16c{flex:1;min-height:0;width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.loading-wrap.data-v-1547c16c,.empty-wrap.data-v-1547c16c{flex:1;display:flex;align-items:center;justify-content:center;min-height:0}.loading-text.data-v-1547c16c,.empty-text.data-v-1547c16c{font-size:28rpx;color:#909399}.detail-body.data-v-1547c16c{display:flex;gap:20rpx;align-items:stretch;overflow:hidden;box-sizing:border-box}.steps-card.data-v-1547c16c{width:140rpx;flex-shrink:0;background:#fff;border-radius:20rpx;box-sizing:border-box}.step-item.data-v-1547c16c{padding:0 12rpx}.step-track.data-v-1547c16c{display:flex;flex-direction:column;align-items:center;padding:28rpx 0 0}.step-dot.data-v-1547c16c{width:66rpx;height:66rpx;border-radius:50%;background:#f3f3f3;display:flex;align-items:center;justify-content:center;transition:background .2s}.step-dot--active.data-v-1547c16c{background:#2667e9}.step-icon.data-v-1547c16c{width:36rpx;height:36rpx}.step-label.data-v-1547c16c{margin-top:12rpx;font-size:24rpx;color:#999;line-height:1.2}.step-label--active.data-v-1547c16c{color:#2667e9;font-weight:600}.step-line.data-v-1547c16c{width:0;height:40rpx;margin:10rpx 0;border-left:2rpx dashed #dcdfe6}.content-column.data-v-1547c16c{flex:1;width:0;min-height:0;box-sizing:border-box}.content-scroll.data-v-1547c16c{width:100%;box-sizing:border-box}.node-section.data-v-1547c16c{box-sizing:border-box;padding:0;margin-bottom:24rpx}.node-section--last.data-v-1547c16c{padding-bottom:40rpx;margin-bottom:0}.detail-card.data-v-1547c16c{background:#fff;border-radius:20rpx;overflow:hidden;padding:28rpx 38rpx;box-sizing:border-box}.card-header-v2.data-v-1547c16c{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx;padding:0}.card-header-main.data-v-1547c16c{flex:1;min-width:0}.operator.data-v-1547c16c{font-size:28rpx;font-weight:600;color:#303133;line-height:1.5;word-break:break-all}.time.data-v-1547c16c{display:block;margin-top:8rpx;font-size:24rpx;color:#999;line-height:1.4}.level-badge.data-v-1547c16c{flex-shrink:0;padding:6rpx 16rpx;border-radius:8rpx;font-size:22rpx;font-weight:500;white-space:nowrap}.level-badge.level-normal.data-v-1547c16c{background:#fff7e6;border:2rpx solid #ffd591;color:#fa8c16}.level-badge.level-major.data-v-1547c16c{background:#fff1f0;border:2rpx solid #ffa39e;color:#f5222d}.card-divider.data-v-1547c16c{height:0;margin:20rpx 0;border-top:2rpx dashed #eee}.card-body.data-v-1547c16c{padding:0}.detail-row-v2.data-v-1547c16c{display:flex;align-items:flex-start;justify-content:flex-start;gap:24rpx;padding:18rpx 0}.detail-row-v2--block.data-v-1547c16c{flex-wrap:wrap}.label.data-v-1547c16c{flex-shrink:0;font-size:26rpx;color:#999;line-height:1.5}.value.data-v-1547c16c{flex:1;min-width:0;font-size:26rpx;color:#333;line-height:1.6;text-align:left;word-break:break-all}.value--inline.data-v-1547c16c{display:flex;align-items:center}.area-dot.data-v-1547c16c{width:20rpx;height:20rpx;border-radius:50%;margin-right:12rpx;flex-shrink:0}.level-tag.data-v-1547c16c{display:inline-flex;align-items:center;padding:4rpx 16rpx;border-radius:8rpx;font-size:24rpx;font-weight:500;line-height:1.4;white-space:nowrap}.level-normal.data-v-1547c16c{background:#fff7e6;border:2rpx solid #ffd591;color:#fa8c16}.level-major.data-v-1547c16c{background:#fff1f0;border:2rpx solid #ffa39e;color:#f5222d}.tag-badge.data-v-1547c16c{display:inline-flex;align-items:center;padding:4rpx 16rpx;border-radius:8rpx;font-size:24rpx;background:#eef3ff;border:2rpx solid #aac5fc;color:#2667e9}.attachment-list.data-v-1547c16c{display:flex;flex-wrap:wrap;gap:16rpx;flex:1;min-width:0}.attachment-img.data-v-1547c16c{width:160rpx;height:160rpx;border-radius:12rpx;background:#f5f7fa}.sign-img.data-v-1547c16c{width:300rpx;height:160rpx;border:1rpx solid #e4e7ed;border-radius:8rpx;background:#fafafa;flex:1;min-width:0;max-width:100%}.file-link.data-v-1547c16c{display:inline-block;padding:12rpx 20rpx;background:#f5f7fa;border:1rpx solid #e4e7ed;border-radius:8rpx;color:#2667e9;font-size:24rpx}.result-tag.data-v-1547c16c{padding:6rpx 16rpx;border-radius:6rpx;font-size:24rpx}.result-tag--pass.data-v-1547c16c{background:#f0f9eb;color:#67c23a}.result-tag--fail.data-v-1547c16c{background:#fef0f0;color:#f56c6c}

View File

@@ -1 +0,0 @@
"use strict";const e="submit",t="assign",a="rectify",r="verify",i="writeoff",s={1:"status-blue",2:"status-orange",3:"status-red",4:"status-yellow",5:"status-green"},n={"待交办":"status-blue","待整改":"status-orange","整改中":"status-orange","待验收":"status-red","待销号":"status-yellow","已完成":"status-green","已销号":"status-green"},o={2:"一般隐患",3:"重大隐患"};function m(e,t="-"){if(Array.isArray(e)&&e.length)return e.join("、");if("string"==typeof e&&e.trim()){const t=e.split(/[,,、]/).map((e=>e.trim())).filter(Boolean);return t.length?t.join("、"):e}return t}function c(e,t,a){(t||[]).forEach((t=>{if(!t)return;const s=t.verifyId??t.id;if(null!=s){if(a.has(s))return;a.add(s)}e.push(function(e){return!!e&&(2===e.type||"销号"===e.typeName)}(t)?function(e){return{type:i,nodeName:"销号",titlePrefix:"销号",operator:e.verifierName||"-",time:e.verifyTime||"-",content:{resultName:e.resultName||"-",remark:e.remark||"",attachments:e.attachments||[],signPath:e.signPath||"",writeoffDeptName:e.writeoffDeptName||"-"}}}(t):function(e){return{type:r,nodeName:"验收",titlePrefix:"验收",operator:e.verifierName||"-",time:e.verifyTime||"-",content:{resultName:e.resultName||"-",remark:e.remark||"",attachments:e.attachments||[],signPath:e.signPath||"",verifyDeptName:e.verifyDeptName||"-"}}}(t))}))}exports.generateHazardHistory=function(r){if(!r)return[];const i=[];var s;return i.push({type:e,nodeName:"提交",titlePrefix:"提交",operator:r.reporterName||"-",time:r.createdAt||"-",content:{title:r.title||"-",source:r.source||"-",areaName:r.areaName||"-",areaColor:r.areaColor||"",address:r.address||"-",level:r.level,levelName:r.levelName||(s=r.level,o[s]||"未知"),tagName:r.tagName||"-",description:r.description||"-",attachments:r.attachments||[],legalBasis:r.legalBasis||r.regulationName||"-",reporterPhone:r.reporterPhone||"-",reportDeptName:r.reportDeptName||"-"}}),r.assignFlows&&r.assignFlows.length>0&&r.assignFlows.forEach((e=>{var r;e.assignId&&i.push({type:t,nodeName:"交办",titlePrefix:"交办",operator:e.assignerName||"-",time:e.assignTime||"-",content:{assigneeName:e.assigneeName||"-",deadline:e.deadline||"-",assignDeptName:e.assignDeptName||"-",assignRemark:e.assignRemark||"-",priorityName:e.priorityName||"-"}}),e.rectify&&i.push({type:a,nodeName:"整改",titlePrefix:"整改",operator:e.rectify.rectifierName||"-",time:e.rectify.rectifyTime||"-",content:{rectifyPlan:e.rectify.rectifyPlan||"-",rectifyResult:e.rectify.rectifyResult||"-",rectificationMeasures:e.rectify.rectificationMeasures||"-",controlMeasures:e.rectify.controlMeasures||"-",deadline:e.deadline||"-",rectifierNames:(r=e.rectify,r?Array.isArray(r.rectifierNames)&&r.rectifierNames.length?r.rectifierNames.join("、"):Array.isArray(r.memberNames)&&r.memberNames.length?r.memberNames.join("、"):m(r.rectifierName,"-"):"-"),managerNames:m(e.rectify.managerNames,"-"),planCost:e.rectify.planCost,actualCost:e.rectify.actualCost,attachments:e.rectify.attachments||[],signPath:e.rectify.signPath||"",rectifyStatusName:e.rectify.rectifyStatusName||"-"}});const s=new Set;c(i,e.verifies||[],s),c(i,function(e){return e.writeOffs||e.writeoffs||[]}(e),s)})),i},exports.getStatusClass=function(e,t){return t&&n[t]?n[t]:s[e]||""},exports.getStepIconPath=function(e,t=!1){const a={submit:"tijiao",assign:"jiaoban",rectify:"zhenggai",verify:"yanshou",writeoff:"xiaohao"}[e]||"tijiao";return"zhenggai"===a?t?"/static/yinhuan_detail/zhenggai_selected.png":"/static/yinhuan_detail/zhenggai__unselected.png":`/static/yinhuan_detail/${a}__${t?"selected":"unselected"}.png`};

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../common/vendor.js"),l=require("./hazardDetail.js");exports.useHazardDetailScroll=function(t,o){const n=e.getCurrentInstance(),u=(null==n?void 0:n.proxy)||n,r=e.ref([]),a=e.ref(0),i=e.ref([0]),c=e.ref(0),s=e.ref(""),v=e.ref(""),d=e.ref(!0),f=e.ref(!1);let h=null;const g=()=>{clearTimeout(h),h=setTimeout((()=>{r.value.length&&e.nextTick$1((()=>{const l=e.index.createSelectorQuery().in(u);l.select(".content-scroll").boundingClientRect(),l.select(".content-scroll").scrollOffset(),l.selectAll(".node-section").boundingClientRect(),l.exec((e=>{const l=null==e?void 0:e[0],t=null==e?void 0:e[1],o=(null==e?void 0:e[2])||[];if(!l||!o.length)return;c.value=l.height||0;const n=(null==t?void 0:t.scrollTop)||0;i.value=o.map((e=>e.top-l.top+n))}))}))}),80)};return e.watch((()=>"function"==typeof o?o():null==o?void 0:o.value),(e=>{e||(g(),setTimeout(g,300))})),e.watch((()=>"function"==typeof t?t():null==t?void 0:t.value),(e=>{return t=e,r.value=l.generateHazardHistory(t),a.value=0,s.value="",g(),void setTimeout(g,300);var t}),{immediate:!0,deep:!0}),e.watch(a,(e=>{v.value="hazard-step-"+e,setTimeout((()=>{v.value=""}),300)})),e.watch((()=>r.value.length),(()=>g())),e.onReady((()=>{g(),setTimeout(g,300)})),{historyList:r,activeIndex:a,contentScrollIntoView:s,stepScrollIntoView:v,scrollWithAnimation:d,onContentScroll:e=>{if(f.value)return;const{scrollTop:l=0,scrollHeight:t=0}=e.detail||{};((e,l=0)=>{const t=r.value.length;if(!t)return;if(l>0&&c.value>0&&e+c.value>=l-60)return void(a.value!==t-1&&(a.value=t-1));const o=i.value;if(!o.length)return;let n=0;for(let u=o.length-1;u>=0;u--)if(e>=o[u]-80){n=u;break}n!==a.value&&(a.value=n)})(l,t),g()},onScrollToLower:()=>{const e=r.value.length;e>0&&a.value!==e-1&&(a.value=e-1)},scrollToNode:e=>{e<0||e>=r.value.length||(f.value=!0,d.value=!0,a.value=e,s.value="hazard-node-"+e,setTimeout((()=>{s.value="",f.value=!1,g()}),350))},scheduleMeasureLayout:g}};

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../../../common/vendor.js");Math||o();const o=()=>"../wd-icon/wd-icon.js",n=e.defineComponent({name:"wd-button",options:{addGlobalClass:!0,virtualHost:!0,styleIsolation:"shared"},props:e.buttonProps,emits:["click","getuserinfo","contact","getphonenumber","getrealtimephonenumber","error","launchapp","opensetting","chooseavatar","agreeprivacyauthorization"],setup(o,{emit:n}){const a=o,t=n,i=e.ref(20),s=e.ref(70),r=e.ref(""),c=e.computed((()=>`background-image: url(${r.value});`)),l=e.computed((()=>a.disabled||a.loading?void 0:a.openType));function d(e){a.disabled||a.loading||t("click",e)}function u(e){"phoneNumber"===a.scope?g(e):"userInfo"===a.scope&&p(e)}function p(e){t("getuserinfo",e.detail)}function f(e){t("contact",e.detail)}function g(e){t("getphonenumber",e.detail)}function m(e){t("getrealtimephonenumber",e.detail)}function b(e){t("error",e.detail)}function h(e){t("launchapp",e.detail)}function v(e){t("opensetting",e.detail)}function w(e){t("chooseavatar",e.detail)}function k(e){t("agreeprivacyauthorization",e.detail)}return e.watch((()=>a.loading),(()=>{!function(){const{loadingColor:o,type:n,plain:t}=a;let i=o;if(!i)switch(n){case"primary":i="#4D80F0";break;case"success":i="#34d19d";break;case"info":case"default":i="#333";break;case"warning":i="#f0883a";break;case"error":i="#fa4350"}const s=((e="#4D80F0",o=!0)=>`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42 42"><defs><linearGradient x1="100%" y1="0%" x2="0%" y2="0%" id="a"><stop stop-color="${o?e:"#fff"}" offset="0%" stop-opacity="0"/><stop stop-color="${o?e:"#fff"}" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path d="M21 1c11.046 0 20 8.954 20 20s-8.954 20-20 20S1 32.046 1 21 9.954 1 21 1zm0 7C13.82 8 8 13.82 8 21s5.82 13 13 13 13-5.82 13-13S28.18 8 21 8z" fill="${o?"#fff":e}"/><path d="M4.599 21c0 9.044 7.332 16.376 16.376 16.376 9.045 0 16.376-7.332 16.376-16.376" stroke="url(#a)" stroke-width="3.5" stroke-linecap="round"/></g></svg>`)(i,!t);r.value=`"data:image/svg+xml;base64,${e.encode(s)}"`}()}),{deep:!0,immediate:!0}),(o,n)=>e.e({a:o.loading},o.loading?{b:e.s(c.value)}:o.icon?{d:e.p({"custom-class":"wd-button__icon",name:o.icon,classPrefix:o.classPrefix})}:{},{c:o.icon,e:e.gei(o,o.buttonId),f:""+(o.disabled||o.loading?"":"wd-button--active"),g:e.s(o.customStyle),h:e.n("is-"+o.type),i:e.n("is-"+o.size),j:e.n(o.round?"is-round":""),k:e.n(o.hairline?"is-hairline":""),l:e.n(o.plain?"is-plain":""),m:e.n(o.disabled?"is-disabled":""),n:e.n(o.block?"is-block":""),o:e.n(o.loading?"is-loading":""),p:e.n(o.customClass),q:i.value,r:s.value,s:l.value,t:o.sendMessageTitle,v:o.sendMessagePath,w:o.sendMessageImg,x:o.appParameter,y:o.showMessageCard,z:o.sessionFrom,A:o.lang,B:o.hoverStopPropagation,C:o.scope,D:e.o(d),E:e.o(u),F:e.o(p),G:e.o(f),H:e.o(g),I:e.o(m),J:e.o(b),K:e.o(h),L:e.o(v),M:e.o(w),N:e.o(k)})}}),a=e._export_sfc(n,[["__scopeId","data-v-161f130c"]]);wx.createComponent(a);

View File

@@ -1,6 +0,0 @@
{
"component": true,
"usingComponents": {
"wd-icon": "../wd-icon/wd-icon"
}
}

View File

@@ -1 +0,0 @@
<button id="{{e}}" hover-class="{{f}}" style="{{g + ';' + virtualHostStyle}}" class="{{['data-v-161f130c', 'wd-button', h, i, j, k, l, m, n, o, p, virtualHostClass]}}" hover-start-time="{{q}}" hover-stay-time="{{r}}" open-type="{{s}}" send-message-title="{{t}}" send-message-path="{{v}}" send-message-img="{{w}}" app-parameter="{{x}}" show-message-card="{{y}}" session-from="{{z}}" lang="{{A}}" hover-stop-propagation="{{B}}" scope="{{C}}" bindtap="{{D}}" bindgetAuthorize="{{E}}" bindgetuserinfo="{{F}}" bindcontact="{{G}}" bindgetphonenumber="{{H}}" bindgetrealtimephonenumber="{{I}}" binderror="{{J}}" bindlaunchapp="{{K}}" bindopensetting="{{L}}" bindchooseavatar="{{M}}" bindagreeprivacyauthorization="{{N}}" hidden="{{virtualHostHidden || false}}"><view class="wd-button__content data-v-161f130c"><view wx:if="{{a}}" class="wd-button__loading data-v-161f130c"><view class="wd-button__loading-svg data-v-161f130c" style="{{b}}"></view></view><wd-icon wx:elif="{{c}}" class="data-v-161f130c" virtualHostClass="data-v-161f130c" u-i="161f130c-0" bind:__l="__l" u-p="{{d}}"></wd-icon><view class="wd-button__text data-v-161f130c"><slot/></view></view></button>

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../../../common/vendor.js"),o=e.defineComponent({name:"wd-icon",options:{virtualHost:!0,addGlobalClass:!0,styleIsolation:"shared"},props:e.iconProps,emits:["click","touch"],setup(o,{emit:t}){const s=o,c=t,n=e.computed((()=>e.isDef(s.name)&&s.name.includes("/"))),a=e.computed((()=>{const e=s.classPrefix;return`${e} ${s.customClass} ${n.value?"wd-icon--image":e+"-"+s.name}`})),i=e.computed((()=>{const o={};return s.color&&(o.color=s.color),s.size&&(o["font-size"]=e.addUnit(s.size)),`${e.objToStyle(o)} ${s.customStyle}`}));function l(e){c("click",e)}return(o,t)=>e.e({a:n.value},n.value?{b:o.name}:{},{c:e.o(l),d:e.n(a.value),e:e.s(i.value),f:e.gei(o,"")})}}),t=e._export_sfc(o,[["__scopeId","data-v-bef80b7c"]]);wx.createComponent(t);

View File

@@ -1,4 +0,0 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -1 +0,0 @@
<view bindtap="{{c}}" class="{{['data-v-bef80b7c', d, virtualHostClass]}}" style="{{e + ';' + virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{f}}"><image wx:if="{{a}}" class="wd-icon__image data-v-bef80b7c" src="{{b}}"></image></view>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
{
"component": true,
"usingComponents": {
"wd-button": "../wd-button/wd-button"
}
}

View File

@@ -1 +0,0 @@
<view class="{{['wd-signature', 'data-v-5e53ec40', virtualHostClass]}}" style="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{y}}"><view class="wd-signature__content data-v-5e53ec40"><block wx:if="{{r0}}"><canvas class="wd-signature__content-canvas data-v-5e53ec40" style="{{a}}" width="{{b}}" height="{{c}}" canvas-id="{{d}}" id="{{e}}" disable-scroll="{{f}}" bindtouchstart="{{g}}" bindtouchend="{{h}}" bindtouchmove="{{i}}" type="2d"/></block></view><view class="wd-signature__footer data-v-5e53ec40"><block wx:if="{{$slots.footer}}"><slot name="footer"></slot></block><block wx:else><block wx:if="{{j}}"><wd-button wx:if="{{m}}" class="data-v-5e53ec40" virtualHostClass="data-v-5e53ec40" u-s="{{['d']}}" bindclick="{{l}}" u-i="5e53ec40-0" bind:__l="__l" u-p="{{m}}">{{k}}</wd-button><wd-button wx:if="{{p}}" class="data-v-5e53ec40" virtualHostClass="data-v-5e53ec40" u-s="{{['d']}}" bindclick="{{o}}" u-i="5e53ec40-1" bind:__l="__l" u-p="{{p}}">{{n}}</wd-button></block><wd-button wx:if="{{s}}" class="data-v-5e53ec40" virtualHostClass="data-v-5e53ec40" u-s="{{['d']}}" bindclick="{{r}}" u-i="5e53ec40-2" bind:__l="__l" u-p="{{s}}">{{q}}</wd-button><wd-button wx:if="{{w}}" class="data-v-5e53ec40" virtualHostClass="data-v-5e53ec40" u-s="{{['d']}}" bindclick="{{v}}" u-i="5e53ec40-3" bind:__l="__l" u-p="{{w}}">{{t}}</wd-button></block></view></view>

View File

@@ -1 +0,0 @@
.wd-signature__content.data-v-5e53ec40{justify-content:center;align-items:center;display:flex;overflow:hidden;background:var(--wot-signature-bg, var(--wot-color-white, white));border-radius:var(--wot-signature-radius, 4px);border:var(--wot-signature-border, 1px solid var(--wot-color-gray-5, #c8c9cc))}.wd-signature__content-canvas.data-v-5e53ec40{width:100%}.wd-signature__footer.data-v-5e53ec40{margin-top:var(--wot-signature-footer-margin-top, 8px);justify-content:flex-end;display:flex}.wd-signature__footer.data-v-5e53ec40 .wd-button{margin-left:var(--wot-signature-button-margin-left, 8px)}

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
{
"navigationBarTitleText": "证件照管理",
"usingComponents": {
"u-popup": "../../uni_modules/uview-plus/components/u-popup/u-popup",
"u-datetime-picker": "../../uni_modules/uview-plus/components/u-datetime-picker/u-datetime-picker",
"u-modal": "../../uni_modules/uview-plus/components/u-modal/u-modal"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page.data-v-5f64e04d{min-height:100vh;background:#ebf2fc;padding-bottom:120rpx}.license-list.data-v-5f64e04d{padding-bottom:20rpx}.license-item.data-v-5f64e04d{background:#fff;border-radius:16rpx;padding:24rpx;margin-bottom:20rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,.05)}.license-header.data-v-5f64e04d{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx;padding-bottom:16rpx;border-bottom:1rpx solid #f0f0f0}.license-type.data-v-5f64e04d{font-size:32rpx;font-weight:700;color:#333}.license-actions.data-v-5f64e04d{display:flex;gap:20rpx}.action-btn.data-v-5f64e04d{font-size:28rpx;padding:8rpx 16rpx}.license-detail.data-v-5f64e04d{margin-bottom:16rpx}.detail-row.data-v-5f64e04d{display:flex;margin-bottom:12rpx;font-size:28rpx}.detail-row .label.data-v-5f64e04d{color:#999;width:160rpx;flex-shrink:0}.detail-row .value.data-v-5f64e04d{color:#333;flex:1}.license-photo.data-v-5f64e04d{width:200rpx;height:150rpx;border-radius:8rpx;overflow:hidden}.license-photo image.data-v-5f64e04d{width:100%;height:100%}.empty-state.data-v-5f64e04d{padding:200rpx 0;text-align:center}.add-btn.data-v-5f64e04d{position:fixed;bottom:40rpx;left:30rpx;right:30rpx;height:88rpx;line-height:88rpx;border-radius:44rpx;font-size:32rpx}.popup-content.data-v-5f64e04d{width:600rpx;background:#fff;border-radius:20rpx;padding:30rpx}.popup-header.data-v-5f64e04d{display:flex;justify-content:space-between;align-items:center;margin-bottom:30rpx}.popup-title.data-v-5f64e04d{font-size:34rpx;color:#333}.popup-close.data-v-5f64e04d{font-size:48rpx;color:#999;line-height:1}.popup-body.data-v-5f64e04d{max-height:700rpx;overflow-y:auto}.form-item.data-v-5f64e04d{margin-bottom:24rpx}.form-label.data-v-5f64e04d{font-size:28rpx;color:#333;margin-bottom:12rpx}.form-input.data-v-5f64e04d{width:100%;height:80rpx;border:2rpx solid #E5E5E5;border-radius:12rpx;padding:0 24rpx;font-size:28rpx;box-sizing:border-box}.form-select.data-v-5f64e04d{display:flex;align-items:center;line-height:80rpx}.upload-box.data-v-5f64e04d{width:200rpx;height:200rpx;border:2rpx dashed #ccc;border-radius:12rpx;display:flex;align-items:center;justify-content:center;position:relative}.upload-add.data-v-5f64e04d{display:flex;flex-direction:column;align-items:center}.upload-icon.data-v-5f64e04d{font-size:60rpx;color:#999}.upload-text.data-v-5f64e04d{font-size:24rpx;color:#999;margin-top:8rpx}.upload-preview.data-v-5f64e04d{width:100%;height:100%;position:relative}.upload-img.data-v-5f64e04d{width:100%;height:100%;border-radius:12rpx}.upload-delete.data-v-5f64e04d{position:absolute;top:-16rpx;right:-16rpx;width:40rpx;height:40rpx;background:#ff4d4f;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:28rpx}.popup-footer.data-v-5f64e04d{display:flex;justify-content:center;gap:30rpx;margin-top:40rpx}.btn-cancel.data-v-5f64e04d{flex:1;height:80rpx;line-height:80rpx;border:2rpx solid #E5E5E5;border-radius:40rpx;background:#fff;color:#333;font-size:30rpx}.btn-confirm.data-v-5f64e04d{flex:1;height:80rpx;line-height:80rpx;border-radius:40rpx;color:#fff;font-size:30rpx}.dept-popup.data-v-5f64e04d{width:600rpx;background:#fff;border-radius:20rpx;padding:30rpx}.dept-list.data-v-5f64e04d{max-height:400rpx;overflow-y:auto;margin-bottom:30rpx}.dept-item.data-v-5f64e04d{display:flex;align-items:center;padding:24rpx;border:2rpx solid #E5E5E5;border-radius:12rpx;margin-bottom:16rpx}.dept-checkbox.data-v-5f64e04d{width:36rpx;height:36rpx;border:2rpx solid #ccc;border-radius:6rpx;margin-right:20rpx;display:flex;align-items:center;justify-content:center;flex-shrink:0}.dept-checkbox-active.data-v-5f64e04d{background:#2667e9;border-color:#2667e9;color:#fff}.dept-name.data-v-5f64e04d{font-size:28rpx;color:#333}.btn-dept-confirm.data-v-5f64e04d{width:100%;height:80rpx;line-height:80rpx;border-radius:40rpx;color:#fff;font-size:30rpx}

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../common/vendor.js"),r={};const n=e._export_sfc(r,[["render",function(r,n){return{a:e.gei(r,"")}}]]);wx.createPage(n);

View File

@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "检查清单",
"usingComponents": {}
}

View File

@@ -1 +0,0 @@
<view class="{{['padding', virtualHostClass]}}" style="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{a}}"><view class="text-bold text-black">检查清单预览</view><view class="flex margin-bottom"><view class="text-gray">计划名称:</view><view>和谐矿业每日巡检</view></view><view class="flex margin-bottom"><view class="text-gray">检查时间:</view><view>2025-11-19 10:18:40</view></view><view class="flex margin-bottom"><view class="text-gray">检查人员:</view><view>18174379303</view></view><image></image><view class="flex margin-bottom"><view>被检查单位:</view><view></view></view><view class="flex margin-bottom"><view>检查人员:</view><view></view></view><view class="flex margin-bottom"><view>上次检查情况:</view><view></view></view><view class="flex margin-bottom"><view>本次检查情况:</view><view></view></view><view class="flex margin-bottom"><view>检查日期:</view><view>2025-11-19 10:18:40</view></view><view class="flex justify-between"><view class="flex text-center align-center"><button class="bg-blue">缩小</button><view>50%</view><button class="bg-blue">放大</button></view><button class="lg cu-btn">重置</button></view></view>

View File

@@ -1 +0,0 @@
"use strict";const e=require("../../common/vendor.js"),c={__name:"Inspectionlog",setup:c=>(c,s)=>({a:e.o((c=>{e.index.navigateTo({url:"/pages/Inspectionchecklist/Inspectionchecklist"})})),b:e.gei(c,"")})},s=e._export_sfc(c,[["__scopeId","data-v-fa142cd8"]]);wx.createPage(s);

View File

@@ -1,4 +0,0 @@
{
"navigationBarTitleText": "检查记录",
"usingComponents": {}
}

View File

@@ -1 +0,0 @@
<view class="{{['page', 'padding', 'data-v-fa142cd8', virtualHostClass]}}" style="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{b}}"><view class="padding bg-white radius list data-v-fa142cd8"><view class="text-bold margin-bottom text-black data-v-fa142cd8">和谐矿业每日巡检</view><view class="flex margin-bottom data-v-fa142cd8"><view class="text-gray data-v-fa142cd8">检查时间:</view><view class="data-v-fa142cd8">2025-11-19 10:18:40</view></view><view class="flex margin-bottom data-v-fa142cd8"><view class="text-gray data-v-fa142cd8">检查人员:</view><view class="data-v-fa142cd8">18174379303</view></view><view class="flex margin-bottom data-v-fa142cd8"><view class="text-gray data-v-fa142cd8">隐患数量:</view><view class="data-v-fa142cd8">1</view></view><view class="flex margin-bottom data-v-fa142cd8"><view class="text-gray data-v-fa142cd8">备注:</view><view class="data-v-fa142cd8">可以</view></view><view class="flex justify-between data-v-fa142cd8"><view class="data-v-fa142cd8"></view><button class="bg-blue round cu-btn lg data-v-fa142cd8" bindtap="{{a}}">预览清单</button></view></view></view>

View File

@@ -1 +0,0 @@
.page.data-v-fa142cd8{min-height:100vh;background:#ebf2fc}.list.data-v-fa142cd8{background:#fff;box-shadow:0 2rpx 6rpx 2rpx rgba(0,0,0,.08);border-left:5px solid #2667E9;border-radius:20rpx;padding:20rpx}

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
{
"navigationBarTitleText": "检查结果",
"usingComponents": {
"u-radio": "../../uni_modules/uview-plus/components/u-radio/u-radio",
"u-radio-group": "../../uni_modules/uview-plus/components/u-radio-group/u-radio-group",
"up-textarea": "../../uni_modules/uview-plus/components/u-textarea/u-textarea",
"hazard-form-popup": "../../components/hazard/HazardFormPopup"
}
}

View File

@@ -1 +0,0 @@
<view class="{{['page', 'padding', 'data-v-8e75359a', virtualHostClass]}}" style="{{virtualHostStyle}}" hidden="{{virtualHostHidden || false}}" id="{{J}}"><view wx:if="{{a}}" class="progress-bar data-v-8e75359a"><view class="progress-text data-v-8e75359a"><text class="current-index data-v-8e75359a">第 {{b}} 个问题</text><text class="total-count data-v-8e75359a"> / 共 {{c}} 个</text></view><view class="progress-line data-v-8e75359a"><view class="progress-inner data-v-8e75359a" style="{{'width:' + d}}"></view></view></view><view class="padding bg-white radius data-v-8e75359a"><view class="text-bold data-v-8e75359a">{{e}}</view><view class="margin-top data-v-8e75359a"><rich-text class="data-v-8e75359a" nodes="{{f}}"></rich-text></view><view class="margin-top data-v-8e75359a"><u-radio-group wx:if="{{j}}" class="data-v-8e75359a" virtualHostClass="data-v-8e75359a" u-s="{{['d']}}" bindchange="{{h}}" u-i="8e75359a-0" bind:__l="__l" bindupdateModelValue="{{i}}" u-p="{{j}}"><u-radio wx:for="{{g}}" wx:for-item="item" wx:key="a" class="data-v-8e75359a" virtualHostClass="data-v-8e75359a" bindchange="{{item.b}}" u-i="{{item.c}}" bind:__l="__l" u-p="{{item.d}}"></u-radio></u-radio-group></view><view wx:if="{{k}}" class="hazard-section margin-top data-v-8e75359a"><view class="hazard-tip data-v-8e75359a"><text class="cuIcon-warn text-yellow margin-right-xs data-v-8e75359a"></text><text class="text-orange data-v-8e75359a">检查结果为异常,需填写隐患信息</text></view><view wx:if="{{l}}" class="hazard-btn data-v-8e75359a" bindtap="{{m}}"><text class="text-blue data-v-8e75359a">填写隐患信息</text></view><view wx:else class="hazard-card data-v-8e75359a"><view class="card-header data-v-8e75359a"><view class="text-bold text-black data-v-8e75359a">{{n}}</view><view class="{{['level-tag', 'data-v-8e75359a', p && 'level-minor', q && 'level-normal', r && 'level-major']}}">{{o}}</view></view><view class="card-body data-v-8e75359a"><view class="info-row data-v-8e75359a"><text class="text-gray data-v-8e75359a">检查形式:</text><text class="data-v-8e75359a">{{s}}</text></view><view class="info-row data-v-8e75359a"><text class="text-gray data-v-8e75359a">隐患位置:</text><text class="data-v-8e75359a">{{t}}</text></view><view class="info-row data-v-8e75359a"><text class="text-gray data-v-8e75359a">隐患描述:</text><text class="description-text data-v-8e75359a">{{v}}</text></view><view wx:if="{{w}}" class="info-row data-v-8e75359a"><text class="text-gray data-v-8e75359a">附件:</text><text class="data-v-8e75359a">{{x}}个文件</text></view></view><view class="card-footer data-v-8e75359a"><button class="btn-edit data-v-8e75359a" bindtap="{{y}}">修改</button><button class="btn-clear data-v-8e75359a" bindtap="{{z}}">清除</button></view></view></view><view class="margin-top data-v-8e75359a"><up-textarea wx:if="{{B}}" class="data-v-8e75359a" virtualHostClass="data-v-8e75359a" u-i="8e75359a-2" bind:__l="__l" bindupdateModelValue="{{A}}" u-p="{{B}}"></up-textarea></view></view><button class="bg-blue round margin-top-xl data-v-8e75359a" bindtap="{{C}}">提交</button><hazard-form-popup wx:if="{{I}}" class="r data-v-8e75359a" virtualHostClass="r data-v-8e75359a" u-r="hazardFormRef" bindconfirm="{{E}}" bindclearDraft="{{F}}" u-i="8e75359a-3" bind:__l="__l" bindupdateShow="{{G}}" bindupdateModelValue="{{H}}" u-p="{{I}}"/></view>

View File

@@ -1 +0,0 @@
.page.data-v-8e75359a{min-height:100vh;background:#ebf2fc}.progress-bar.data-v-8e75359a{background:#fff;border-radius:12rpx;padding:24rpx;margin-bottom:20rpx;box-shadow:0 2rpx 12rpx rgba(0,0,0,.05)}.progress-text.data-v-8e75359a{display:flex;align-items:baseline;margin-bottom:16rpx}.progress-text .current-index.data-v-8e75359a{font-size:32rpx;font-weight:700;color:#2667e9}.progress-text .total-count.data-v-8e75359a{font-size:28rpx;color:#999}.progress-line.data-v-8e75359a{height:12rpx;background:#e5e5e5;border-radius:6rpx;overflow:hidden}.progress-inner.data-v-8e75359a{height:100%;background:linear-gradient(90deg,#2667e9,#5b9bff);border-radius:6rpx;transition:width .3s ease}.hazard-section.data-v-8e75359a{border-top:1rpx solid #eee;padding-top:20rpx}.hazard-tip.data-v-8e75359a{display:flex;align-items:center;padding:16rpx 20rpx;background:#fff7e6;border:1rpx solid #FFE7BA;border-radius:8rpx;margin-bottom:20rpx}.hazard-tip .text-orange.data-v-8e75359a{color:#fa8c16;font-size:26rpx}.hazard-btn.data-v-8e75359a{display:flex;align-items:center;justify-content:center;height:88rpx;border:2rpx dashed #2667E9;border-radius:12rpx;background:#f5f9ff}.hazard-btn .text-blue.data-v-8e75359a{color:#2667e9;font-size:28rpx}.hazard-card.data-v-8e75359a{background:#f5f9ff;border:1rpx solid #D6E4FF;border-radius:12rpx;overflow:hidden}.hazard-card .card-header.data-v-8e75359a{display:flex;justify-content:space-between;align-items:center;padding:20rpx;border-bottom:1rpx solid #E8E8E8;background:#fff}.hazard-card .card-body.data-v-8e75359a{padding:20rpx}.hazard-card .card-body .info-row.data-v-8e75359a{display:flex;margin-bottom:12rpx;font-size:26rpx}.hazard-card .card-body .info-row.data-v-8e75359a:last-child{margin-bottom:0}.hazard-card .card-body .info-row .text-gray.data-v-8e75359a{flex-shrink:0;color:#999}.hazard-card .card-body .info-row .description-text.data-v-8e75359a{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.hazard-card .card-footer.data-v-8e75359a{display:flex;border-top:1rpx solid #E8E8E8;background:#fff}.hazard-card .card-footer button.data-v-8e75359a{flex:1;height:80rpx;line-height:80rpx;font-size:28rpx;border-radius:0}.hazard-card .card-footer button.data-v-8e75359a:after{border:none}.hazard-card .card-footer .btn-edit.data-v-8e75359a{background:#fff;color:#2667e9;border-right:1rpx solid #E8E8E8}.hazard-card .card-footer .btn-clear.data-v-8e75359a{background:#fff;color:#f56c6c}.level-tag.data-v-8e75359a{padding:4rpx 16rpx;border-radius:8rpx;font-size:24rpx}.level-minor.data-v-8e75359a{background:#f6ffed;border:2rpx solid #B7EB8F;color:#52c41a}.level-normal.data-v-8e75359a{background:#fff7e6;border:2rpx solid #FFD591;color:#fa8c16}.level-major.data-v-8e75359a{background:#fff1f0;border:2rpx solid #FFA39E;color:#f5222d}

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
{
"navigationBarTitleText": "排查详情",
"usingComponents": {
"up-icon": "../../uni_modules/uview-plus/components/u-icon/u-icon",
"u-radio": "../../uni_modules/uview-plus/components/u-radio/u-radio",
"u-radio-group": "../../uni_modules/uview-plus/components/u-radio-group/u-radio-group",
"up-textarea": "../../uni_modules/uview-plus/components/u-textarea/u-textarea",
"u-popup": "../../uni_modules/uview-plus/components/u-popup/u-popup",
"hazard-form-popup": "../../components/hazard/HazardFormPopup"
}
}

Some files were not shown because too many files have changed in this diff Show More