Files
threeonecheck_web/pages/hiddendanger/assignment.vue

563 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="padding page">
<view class="padding radius bg-white">
<!-- 草稿恢复提示 -->
<view v-if="showRestoreBanner" class="bg-orange-light text-orange padding-sm radius margin-bottom flex justify-between align-center" style="font-size: 24rpx; background-color: #FFF7EB; border: 1rpx solid #FFE4CC; width: 100%; box-sizing: border-box; display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 20rpx;">
<view class="flex align-center" style="display: flex; flex-direction: row; align-items: center;">
<text class="cuIcon-info margin-right-xs" style="margin-right: 10rpx;"></text>
<text>已自动恢复您上次未提交的内容</text>
</view>
<text class="text-blue text-bold" style="cursor: pointer; padding: 0 10rpx; color: #2667E9; font-weight: bold;" @click="clearDraft(true)">清空草稿</text>
</view>
<view class="flex margin-bottom">
<view class="text-gray">整改责任人</view>
<view class="text-red">*</view>
</view>
<view class="select-trigger" @click="openUserPopup">
<view class="select-content" :class="{ 'text-gray': !selectedUser }">
{{ selectedUser || '请选择整改责任人' }}
</view>
<text class="cuIcon-unfold"></text>
</view>
<!-- 整改责任人部门-人员级联单选弹窗 -->
<u-popup :show="showUserPopup" mode="bottom" round="20" @close="cancelUserSelect">
<view class="user-popup cascader-user-popup">
<view class="popup-header">
<view class="popup-title text-bold">选择整改责任人</view>
<view class="popup-close" @click="cancelUserSelect">×</view>
</view>
<view v-if="userPickerSelectedId" class="selected-summary">
<text class="summary-label">已选</text>
<text class="summary-text">{{ userPickerSelectedText }}</text>
</view>
<view class="cascader-body">
<scroll-view class="cascader-col dept-col" scroll-y>
<view
v-for="(dept, index) in deptList"
:key="dept.deptId"
:class="['cascader-item', { active: activeDeptIndex === index }]"
@click="activeDeptIndex = index"
>
<text class="cascader-item-text">{{ dept.deptName }}</text>
<text v-if="deptHasSelectedUser(dept)" class="dept-dot"></text>
</view>
</scroll-view>
<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-else>
<view
class="user-item"
v-for="user in currentDeptUsers"
:key="'user-' + user.userId"
:class="{ active: String(userPickerSelectedId) === String(user.userId) }"
@click="onUserItemClick(user.userId)"
>
<text class="user-item-text">{{ formatUserDisplayName(user) }}</text>
<text v-if="String(userPickerSelectedId) === String(user.userId)" class="cuIcon-check text-blue"></text>
</view>
</view>
</scroll-view>
</view>
<view class="popup-footer">
<button class="btn-cancel" @click="cancelUserSelect">取消</button>
<button class="btn-confirm bg-blue" @click="confirmUserSelect">确定</button>
</view>
</view>
</u-popup>
<view class="flex margin-bottom margin-top">
<view class="text-gray">整改期限</view>
<view class="text-red">*</view>
</view>
<view class="picker-input" @click="showDatePicker = true">
<text :class="selectedDate ? '' : 'text-gray'">{{ selectedDate || '请选择整改期限' }}</text>
</view>
<up-datetime-picker
:show="showDatePicker"
v-model="dateValue"
mode="datetime"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
@close="showDatePicker = false"
></up-datetime-picker>
<view class="btn-group margin-top-xl">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button class="btn-confirm bg-blue" @click="handleSubmit">确认</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getDepartmentPersonUsers,assignHiddenDanger } from '@/request/api.js';
// 页面参数
const hazardId = ref('');
const assignId = ref('');
// 整改人员选择
const showUserPopup = ref(false);
const selectedUser = ref('');
const selectedUserId = ref('');
const deptList = ref([]);
const activeDeptIndex = ref(0);
const userPickerSelectedId = ref('');
const formatUserDisplayName = (user) => {
if (user.postName) {
return `${user.nickName}_${user.postName}`;
}
return user.nickName || '';
};
const currentDeptUsers = computed(() => {
const dept = deptList.value[activeDeptIndex.value];
return dept?.users || [];
});
const userPickerSelectedText = computed(() => {
if (!userPickerSelectedId.value) return '';
for (const dept of deptList.value) {
const user = (dept.users || []).find((u) => String(u.userId) === String(userPickerSelectedId.value));
if (user) return formatUserDisplayName(user);
}
return '';
});
const deptHasSelectedUser = (dept) => {
if (!userPickerSelectedId.value || !dept.users?.length) return false;
return dept.users.some((user) => String(user.userId) === String(userPickerSelectedId.value));
};
const onUserItemClick = (userId) => {
userPickerSelectedId.value = String(userId);
};
const openUserPopup = () => {
userPickerSelectedId.value = selectedUserId.value;
const firstDeptWithUsers = deptList.value.findIndex((dept) => dept.users?.length > 0);
activeDeptIndex.value = firstDeptWithUsers >= 0 ? firstDeptWithUsers : 0;
showUserPopup.value = true;
};
const cancelUserSelect = () => {
showUserPopup.value = false;
};
const confirmUserSelect = () => {
if (!userPickerSelectedId.value) {
uni.showToast({ title: '请选择整改责任人', icon: 'none' });
return;
}
selectedUserId.value = String(userPickerSelectedId.value);
selectedUser.value = userPickerSelectedText.value;
showUserPopup.value = false;
};
// 整改期限选择
const showDatePicker = ref(false);
const dateValue = ref(Date.now());
const selectedDate = ref('');
// 获取部门人员列表
const fetchDeptUsers = async () => {
try {
const res = await getDepartmentPersonUsers();
if (res.code === 0 && res.data) {
deptList.value = res.data;
console.log('部门人员树:', deptList.value);
}
} catch (error) {
console.error('获取部门人员失败:', error);
}
};
// 日期时间选择确认
const onDateConfirm = (e) => {
console.log('选择的日期时间:', e);
const date = new Date(e.value);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
selectedDate.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
showDatePicker.value = false;
};
// 取消
const handleCancel = () => {
uni.navigateBack();
};
// 确认提交
const handleSubmit = async () => {
if (!selectedUserId.value) {
uni.showToast({ title: '请选择整改人员', icon: 'none' });
return;
}
if (!selectedDate.value) {
uni.showToast({ title: '请选择整改期限', icon: 'none' });
return;
}
// 构建请求参数
const params = {
hazardId: Number(hazardId.value), // 隐患ID
assigneeId: Number(selectedUserId.value), // 被指派人ID
deadline: selectedDate.value, // 处理期限
assignRemark: '' // 交办备注(可选)
};
console.log('提交数据:', params);
try {
const res = await assignHiddenDanger(params);
if (res.code === 0) {
clearDraft(false);
uni.showToast({ title: '交办成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
uni.showToast({ title: res.msg || '交办失败', icon: 'none' });
}
} catch (error) {
console.error('交办失败:', error);
uni.showToast({ title: '请求失败', icon: 'none' });
}
};
// 草稿缓存与恢复逻辑 (移至底部以确保 formData 等响应式状态已被正常定义)
const hasDraft = ref(false);
const showRestoreBanner = ref(false); // 独立控制提示 Banner仅在初次确实从本地恢复了内容时才显示
const isRestoring = ref(false); // 正在恢复标志避免触发冗余watch
const getDraftKey = () => `draft_assign_${hazardId.value || ''}`;
// 保存草稿 (排除选择器人员缓存,仅缓存整改期限日期值)
const saveDraft = () => {
if (isRestoring.value) return;
const key = getDraftKey();
const hasContent = selectedDate.value;
if (!hasContent) {
uni.removeStorageSync(key);
hasDraft.value = false;
return;
}
const data = {
selectedDate: selectedDate.value,
dateValue: dateValue.value
};
uni.setStorageSync(key, JSON.stringify(data));
hasDraft.value = true;
};
// 清空草稿
const clearDraft = (showToast = true) => {
const key = getDraftKey();
uni.removeStorageSync(key);
hasDraft.value = false;
showRestoreBanner.value = false;
isRestoring.value = true;
selectedDate.value = '';
dateValue.value = Date.now();
nextTick(() => {
isRestoring.value = false;
});
if (showToast) {
uni.showToast({ title: '草稿已清空', icon: 'none' });
}
};
// 恢复草稿
const restoreDraft = () => {
const key = getDraftKey();
const cached = uni.getStorageSync(key);
if (cached) {
try {
const data = JSON.parse(cached);
const hasContent = data.selectedDate;
if (!hasContent) return;
isRestoring.value = true;
selectedDate.value = data.selectedDate || '';
dateValue.value = data.dateValue || Date.now();
hasDraft.value = true;
showRestoreBanner.value = true; // 确实存在内容并恢复了,才亮起提示 Banner
nextTick(() => {
isRestoring.value = false;
});
uni.showToast({
title: '已自动恢复您上次未提交的内容',
icon: 'none',
duration: 2500
});
} catch (e) {
console.error('解析草稿失败:', e);
isRestoring.value = false;
}
}
};
// 监听变量变化,自动保存草稿
watch(
() => [selectedDate.value],
() => {
if (hazardId.value) {
saveDraft();
}
}
);
onLoad((options) => {
if (options.hazardId) hazardId.value = options.hazardId;
if (options.assignId) assignId.value = options.assignId;
fetchDeptUsers();
// 恢复草稿 (不恢复任何人员选择器数据)
restoreDraft();
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #EBF2FC;
}
.picker-input {
background: #fff;
border-radius: 8rpx;
padding: 24rpx 20rpx;
margin-bottom: 20rpx;
border: 1rpx solid #F6F6F6;
text {
font-size: 28rpx;
color: #333;
}
}
.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;
}
}
&.cascader-user-popup {
.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;
}
}
.cascader-body {
display: flex;
height: 600rpx;
}
.cascader-col {
height: 600rpx;
box-sizing: border-box;
}
.dept-col {
width: 38%;
background: #f7f8fa;
border-right: 1rpx solid #eee;
}
.user-col {
width: 62%;
padding: 10rpx 20rpx;
box-sizing: border-box;
}
.cascader-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
font-size: 28rpx;
color: #333;
border-bottom: 1rpx solid #eef0f3;
&.active {
background: #fff;
color: #2667E9;
font-weight: 600;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: #2667E9;
}
}
}
.cascader-item-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dept-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #2667E9;
margin-left: 8rpx;
flex-shrink: 0;
}
.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;
}
}
}
.btn-group {
display: flex;
gap: 30rpx;
}
.btn-cancel {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border: 2rpx solid #2667E9;
border-radius: 40rpx;
background: #fff;
color: #2667E9;
font-size: 30rpx;
}
.btn-confirm {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
color: #fff;
font-size: 30rpx;
}
</style>