Files
threeonecheck_web/pages/Inspectionresult/Inspectionresult.vue
2026-02-08 09:30:43 +08:00

971 lines
24 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="page padding">
<!-- 进度显示 -->
<view class="progress-bar" v-if="checkData">
<view class="progress-text">
<text class="current-index"> {{ checkData.currentIndex || 1 }} 个问题</text>
<text class="total-count"> / {{ checkData.totalCount || 1 }} </text>
</view>
<view class="progress-line">
<view class="progress-inner" :style="{ width: progressPercent + '%' }"></view>
</view>
</view>
<view class="padding bg-white radius">
<view class="text-bold">{{ checkData?.name || '加载中...' }}</view>
<view class="margin-top">
<rich-text :nodes="checkData?.point || ''"></rich-text>
</view>
<view class="margin-top">
<u-radio-group
v-model="radiovalue1"
placement="row"
@change="groupChange"
>
<u-radio
:customStyle="{marginBottom: '8px'}"
v-for="(item, index) in radiolist1"
:key="index"
:label="item.name"
:name="item.name"
@change="radioChange"
>
</u-radio>
</u-radio-group>
</view>
<!-- 异常时显示隐患信息区域 -->
<view v-if="radiovalue1 === '异常'" class="hazard-section margin-top">
<view class="hazard-tip">
<text class="cuIcon-warn text-yellow margin-right-xs"></text>
<text class="text-orange">检查结果为异常需填写隐患信息</text>
</view>
<!-- 未填写时显示按钮 -->
<view v-if="!hasHazardData" class="hazard-btn" @click="openHazardPopup">
<text class="text-blue">填写隐患信息</text>
</view>
<!-- 已填写时显示卡片 -->
<view v-else class="hazard-card">
<view class="card-header">
<view class="text-bold text-black">{{ hazardFormData.title }}</view>
<view class="level-tag" :class="{
'level-minor': hazardFormData.level === 0,
'level-normal': hazardFormData.level === 1,
'level-major': hazardFormData.level === 2
}">{{ levelOptions[hazardFormData.level]?.title }}</view>
</view>
<view class="card-body">
<view class="info-row">
<text class="text-gray">隐患来源</text>
<text>{{ sourceOptions[hazardFormData.source]?.title || '-' }}</text>
</view>
<view class="info-row">
<text class="text-gray">隐患位置</text>
<text>{{ hazardAddress || '-' }}</text>
</view>
<view class="info-row">
<text class="text-gray">隐患描述</text>
<text class="description-text">{{ hazardFormData.description || '-' }}</text>
</view>
<view class="info-row" v-if="hazardFileList.length > 0">
<text class="text-gray">附件</text>
<text>{{ hazardFileList.length }}个文件</text>
</view>
</view>
<view class="card-footer">
<button class="btn-edit" @click="editHazard">修改</button>
<button class="btn-clear" @click="clearHazard">清除</button>
</view>
</view>
</view>
<view class="margin-top">
<up-textarea v-model="value1" placeholder="请输入备注信息" ></up-textarea>
</view>
</view>
<button class="bg-blue round margin-top-xl" @click="handleSubmit">提交</button>
<!-- 新增隐患弹窗 -->
<u-popup :show="showHazardPopup" mode="center" round="20" @close="showHazardPopup = false">
<view class="popup-content">
<view class="popup-header">
<view class="popup-title text-bold">填写隐患信息</view>
<view class="popup-close" @click="showHazardPopup = false">×</view>
</view>
<scroll-view class="popup-body" scroll-y :style="{ height: '60vh' }">
<view class="flex margin-bottom">
<view class="text-gray">隐患图片/视频</view>
<view class="text-red">*</view>
</view>
<up-upload :fileList="hazardFileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple
:maxCount="10"></up-upload>
<view class="text-gray text-sm">必填请上传现场照片或者视频作为隐患证据</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患标题</view>
<view class="text-red">*</view>
</view>
<up-input placeholder="请输入内容" border="surround" v-model="hazardFormData.title"></up-input>
<view class="text-sm text-gray margin-top-xs">请用简洁的语言概括隐患要点</view>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患等级</view>
<view class="text-red">*</view>
</view>
<up-choose v-model="hazardFormData.level" :options="levelOptions" :wrap="false" item-width="183rpx"
item-height="72rpx"></up-choose>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患来源</view>
<view class="text-red">*</view>
</view>
<up-choose v-model="hazardFormData.source" :options="sourceOptions" :wrap="false" item-width="183rpx"
item-height="72rpx"></up-choose>
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患位置</view>
<view class="text-red">*</view>
</view>
<view class="address-box">
<up-input class="address-input-wrapper" v-model="hazardAddress" placeholder="请输入地址" border="surround"></up-input>
<button class="btn-address bg-blue" @tap.stop="chooseLocation">选择地址</button>
</view>
<view class="text-gray text-sm margin-top-xs">办公楼3层东侧消防通道生产车间A区设备旁等</view>
<!-- 隐患区域选择 -->
<view class="flex margin-bottom margin-top">
<view class="text-gray">隐患区域</view>
</view>
<view class="select-trigger" @click="showAreaPicker = true">
<view class="select-value" :class="{ 'placeholder': !selectedAreaName }">
{{ selectedAreaName || '请选择隐患区域' }}
</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-textarea v-model="hazardFormData.description" placeholder="请输入内容"></up-textarea>
<view class="text-gray text-sm margin-top-xs">请详细说明隐患现状潜在风险及影响范围</view>
<view class="text-gray margin-bottom margin-top">隐患标签</view>
<up-choose v-model="hazardFormData.tagIndex" :options="tagOptions"></up-choose>
<view class="text-gray text-sm">可选择多个相关标签对隐患进行分类</view>
</scroll-view>
<view class="popup-footer">
<button class="btn-cancel" @click="showHazardPopup = false">取消</button>
<button class="btn-confirm bg-blue" @click="confirmHazard">确定</button>
</view>
</view>
</u-popup>
<!-- 区域选择弹窗 -->
<u-popup :show="showAreaPicker" mode="bottom" round="20" @close="showAreaPicker = false">
<view class="picker-popup">
<view class="picker-header">
<view class="picker-cancel" @click="showAreaPicker = false">取消</view>
<view class="picker-title">选择隐患区域</view>
<view class="picker-confirm" @click="confirmAreaSelect">确定</view>
</view>
<scroll-view class="picker-body" scroll-y>
<view
v-for="item in areaList"
:key="item.id"
class="picker-item"
:class="{ 'picker-item-active': tempAreaId === item.id }"
@click="tempAreaId = item.id"
>
<view class="flex align-center">
<view class="area-color-dot" :style="{ backgroundColor: item.color }"></view>
<text>{{ item.name }}</text>
</view>
<text v-if="tempAreaId === item.id" class="cuIcon-check text-blue"></text>
</view>
<view v-if="areaList.length === 0" class="text-gray text-center padding">
暂无区域数据
</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { enterCheckPlan, submitCheckResult, addHiddenDanger, getHiddenDangerLabelList } from '@/request/api.js';
import { baseUrl, getToken } from '@/request/request.js';
import { getAreaList } from '@/request/three_one_api/area.js';
// 页面参数
const oneTableId = ref('');
// 检查项数据
const checkData = ref(null);
// 单选框 result: 1.正常 2.异常 3.不涉及
const radiolist1 = reactive([
{
name: '正常',
value: 1,
disabled: false,
},
{
name: '异常',
value: 2,
disabled: false,
},
{
name: '不涉及',
value: 3,
disabled: false,
},
]);
// 选中的结果值
const radiovalue1 = ref('');
const groupChange = (n) => {
console.log('groupChange', n);
// 切换到非异常时清除隐患数据
if (n !== '异常') {
clearHazard();
}
};
const radioChange = (n) => {
console.log('radioChange', n);
};
// 备注
const value1 = ref('');
// ==================== 隐患信息相关 ====================
// 弹窗控制
const showHazardPopup = ref(false);
// 隐患表单数据(暂存)
const hazardFormData = reactive({
title: '',
level: 0,
source: 0,
description: '',
tagIndex: 0
});
// 隐患地址和经纬度
const hazardAddress = ref('');
const hazardLng = ref(0);
const hazardLat = ref(0);
// 区域选择相关
const showAreaPicker = ref(false);
const areaList = ref([]);
const selectedAreaId = ref('');
const selectedAreaName = ref('');
const tempAreaId = ref('');
// 获取区域列表
const fetchAreaList = async () => {
try {
const res = await getAreaList();
if (res.code === 0 && res.data && res.data.records) {
areaList.value = res.data.records;
}
} catch (error) {
console.error('获取区域列表失败:', error);
}
};
// 确认区域选择
const confirmAreaSelect = () => {
if (tempAreaId.value) {
selectedAreaId.value = tempAreaId.value;
const selected = areaList.value.find(item => item.id === tempAreaId.value);
selectedAreaName.value = selected ? selected.name : '';
}
showAreaPicker.value = false;
};
// 隐患附件列表
const hazardFileList = ref([]);
// 是否已填写隐患数据
const hasHazardData = computed(() => {
return hazardFormData.title && hazardFileList.value.length > 0;
});
// 进度百分比
const progressPercent = computed(() => {
if (!checkData.value) return 0;
const current = checkData.value.currentIndex || 1;
const total = checkData.value.totalCount || 1;
return Math.round((current / total) * 100);
});
// 隐患等级选项
const levelOptions = ref([
{ id: 1, title: '轻微隐患' },
{ id: 2, title: '一般隐患' },
{ id: 3, title: '重大隐患' }
]);
// 隐患来源选项
const sourceOptions = ref([
{ id: 1, title: '随手拍' },
{ id: 2, title: '企业自查' },
{ id: 3, title: '行业互查' },
{ id: 4, title: '专家诊查' }
]);
// 隐患标签选项
const tagOptions = ref([]);
// 获取隐患标签列表
const fetchTagOptions = async () => {
try {
const res = await getHiddenDangerLabelList();
if (res.code === 0) {
tagOptions.value = res.data.map(item => ({
id: item.id,
title: item.name
}));
}
} catch (error) {
console.error('获取标签列表失败:', error);
}
};
// 打开隐患信息弹窗
const openHazardPopup = () => {
showHazardPopup.value = true;
};
// 编辑隐患信息(回显数据)
const editHazard = () => {
showHazardPopup.value = true;
};
// 清除隐患信息
const clearHazard = () => {
hazardFormData.title = '';
hazardFormData.level = 0;
hazardFormData.source = 0;
hazardFormData.description = '';
hazardFormData.tagIndex = 0;
hazardAddress.value = '';
hazardLng.value = 0;
hazardLat.value = 0;
hazardFileList.value = [];
selectedAreaId.value = '';
selectedAreaName.value = '';
};
// 确认隐患信息(只暂存,不调接口)
const confirmHazard = () => {
// 表单验证
if (hazardFileList.value.length === 0) {
uni.showToast({ title: '请上传隐患图片/视频', icon: 'none' });
return;
}
if (!hazardFormData.title) {
uni.showToast({ title: '请输入隐患标题', icon: 'none' });
return;
}
if (!hazardAddress.value) {
uni.showToast({ title: '请输入隐患位置', icon: 'none' });
return;
}
if (!hazardFormData.description) {
uni.showToast({ title: '请输入隐患描述', icon: 'none' });
return;
}
// 验证通过,关闭弹窗(数据已在 reactive 中暂存)
showHazardPopup.value = false;
uni.showToast({ title: '隐患信息已暂存', icon: 'success' });
};
// 选择地址
const chooseLocation = () => {
showHazardPopup.value = false;
setTimeout(() => {
uni.chooseLocation({
success: (res) => {
hazardAddress.value = res.address + (res.name ? `(${res.name})` : '');
hazardLng.value = res.longitude;
hazardLat.value = res.latitude;
showHazardPopup.value = true;
},
fail: (err) => {
showHazardPopup.value = true;
if (err.errMsg && err.errMsg.indexOf('cancel') === -1) {
uni.showToast({ title: '选择位置失败', icon: 'none' });
}
}
});
}, 300);
};
// 删除图片
const deletePic = (event) => {
hazardFileList.value.splice(event.index, 1);
};
// 新增图片
const afterRead = async (event) => {
let lists = [].concat(event.file);
let fileListLen = hazardFileList.value.length;
lists.map((item) => {
hazardFileList.value.push({
...item,
status: 'uploading',
message: '上传中',
});
});
for (let i = 0; i < lists.length; i++) {
const result = await uploadFilePromise(lists[i].url);
let item = hazardFileList.value[fileListLen];
hazardFileList.value.splice(fileListLen, 1, {
...item,
status: 'success',
message: '',
url: result,
});
fileListLen++;
}
};
// 上传文件
const uploadFilePromise = (filePath) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + '/frontend/attachment/upload',
filePath: filePath,
name: 'file',
header: {
'Authorization': getToken()
},
success: (res) => {
const data = JSON.parse(res.data);
if (data.code === 0) {
resolve(data.data);
} else {
reject(data.msg || '上传失败');
}
},
fail: (err) => {
console.error('上传失败:', err);
reject(err);
}
});
});
};
// ==================== 提交逻辑 ====================
// 提交检查结果
const handleSubmit = async () => {
// 验证是否选择了检查结果
if (!radiovalue1.value) {
uni.showToast({ title: '请选择检查结果', icon: 'none' });
return;
}
// 获取选中项对应的 value 值
const selectedItem = radiolist1.find(item => item.name === radiovalue1.value);
const resultValue = selectedItem ? selectedItem.value : null;
if (!resultValue) {
uni.showToast({ title: '请选择检查结果', icon: 'none' });
return;
}
// 如果选择异常,验证是否填写了隐患信息
if (radiovalue1.value === '异常' && !hasHazardData.value) {
uni.showToast({ title: '请填写隐患信息', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '提交中...' });
// 如果选择异常,先调用新增隐患接口
if (radiovalue1.value === '异常' && hasHazardData.value) {
// 构建附件列表
const attachments = hazardFileList.value.map(file => {
let url = '';
if (typeof file.url === 'string') {
url = file.url;
} else if (file.url && typeof file.url === 'object') {
url = file.url.url || file.url.path || '';
}
const fileName = (typeof url === 'string' && url) ? url.split('/').pop() : (file.name || '');
return {
fileName: fileName || '',
filePath: url || '',
fileType: file.type || 'image/png',
fileSize: file.size || 0
};
});
// 获取隐患标签ID
const selectedTag = tagOptions.value[hazardFormData.tagIndex];
const tagId = selectedTag ? selectedTag.id : null;
// 构建隐患参数
const hazardParams = {
taskId: checkData.value?.taskId,
checkPointId: checkData.value?.checkPointId,
title: hazardFormData.title,
level: hazardFormData.level + 1,
lng: hazardLng.value || 0,
lat: hazardLat.value || 0,
address: hazardAddress.value || '',
areaId: selectedAreaId.value || null, // 隐患区域ID
description: hazardFormData.description || '',
source: sourceOptions.value[hazardFormData.source]?.title || '',
tagId: tagId,
attachments: attachments
};
console.log('隐患参数:', hazardParams);
// 调用新增隐患接口
const hazardRes = await addHiddenDanger(hazardParams);
if (hazardRes.code !== 0) {
uni.hideLoading();
uni.showToast({ title: hazardRes.msg || '新增隐患失败', icon: 'none' });
return;
}
console.log('新增隐患成功');
}
// 调用提交巡检结果接口
const submitParams = {
taskId: checkData.value?.taskId,
result: resultValue,
remark: value1.value
};
console.log('提交参数:', submitParams);
const res = await submitCheckResult(submitParams);
uni.hideLoading();
if (res.code === 0) {
// 判断是否全部完成
if (res.data && res.data.allFinished === true) {
// 全部完成,退出页面
uni.showToast({ title: '全部检查已完成', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else if (res.data && res.data.nextTask) {
// 还有下一个问题,更新数据继续
uni.showToast({ title: '提交成功,进入下一题', icon: 'success' });
// 重置表单状态
resetFormState();
// 更新为下一个检查项的数据
checkData.value = res.data.nextTask;
} else {
// 兜底处理:没有 allFinished 也没有 nextTask直接返回
uni.showToast({ title: '提交成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' });
}
} catch (error) {
uni.hideLoading();
console.error('提交失败:', error);
uni.showToast({ title: '提交失败', icon: 'none' });
}
};
// 重置表单状态(进入下一题时调用)
const resetFormState = () => {
// 重置检查结果选择
radiovalue1.value = '';
// 重置备注
value1.value = '';
// 清除隐患信息
clearHazard();
};
// 获取检查项数据
const getCheckData = async () => {
try {
const res = await enterCheckPlan(oneTableId.value);
console.log('检查项数据:', res);
if (res.code === 0) {
checkData.value = res.data;
}
} catch (error) {
console.error(error);
}
};
// 页面加载时获取参数并调用接口
onLoad((options) => {
console.log('接收到的参数:', options);
if (options.id) {
oneTableId.value = options.id;
getCheckData();
}
// 获取隐患标签列表
fetchTagOptions();
// 获取区域列表
fetchAreaList();
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #EBF2FC;
}
// 进度显示
.progress-bar {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.progress-text {
display: flex;
align-items: baseline;
margin-bottom: 16rpx;
.current-index {
font-size: 32rpx;
font-weight: bold;
color: #2667E9;
}
.total-count {
font-size: 28rpx;
color: #999;
}
}
.progress-line {
height: 12rpx;
background: #E5E5E5;
border-radius: 6rpx;
overflow: hidden;
}
.progress-inner {
height: 100%;
background: linear-gradient(90deg, #2667E9, #5B9BFF);
border-radius: 6rpx;
transition: width 0.3s ease;
}
// 隐患信息区域
.hazard-section {
border-top: 1rpx solid #eee;
padding-top: 20rpx;
}
// 提示文字
.hazard-tip {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
background: #FFF7E6;
border: 1rpx solid #FFE7BA;
border-radius: 8rpx;
margin-bottom: 20rpx;
.text-orange {
color: #FA8C16;
font-size: 26rpx;
}
}
// 填写隐患信息按钮
.hazard-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border: 2rpx dashed #2667E9;
border-radius: 12rpx;
background: #F5F9FF;
.text-blue {
color: #2667E9;
font-size: 28rpx;
}
}
// 隐患信息卡片
.hazard-card {
background: #F5F9FF;
border: 1rpx solid #D6E4FF;
border-radius: 12rpx;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #E8E8E8;
background: #fff;
}
.card-body {
padding: 20rpx;
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 26rpx;
&:last-child {
margin-bottom: 0;
}
.text-gray {
flex-shrink: 0;
color: #999;
}
.description-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.card-footer {
display: flex;
border-top: 1rpx solid #E8E8E8;
background: #fff;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
border-radius: 0;
&::after {
border: none;
}
}
.btn-edit {
background: #fff;
color: #2667E9;
border-right: 1rpx solid #E8E8E8;
}
.btn-clear {
background: #fff;
color: #F56C6C;
}
}
}
// 隐患等级标签
.level-tag {
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
}
.level-minor {
background: #F6FFED;
border: 2rpx solid #B7EB8F;
color: #52C41A;
}
.level-normal {
background: #FFF7E6;
border: 2rpx solid #FFD591;
color: #FA8C16;
}
.level-major {
background: #FFF1F0;
border: 2rpx solid #FFA39E;
color: #F5222D;
}
// 弹窗样式
.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;
font-size: 30rpx;
&::after {
border: none;
}
}
.btn-cancel {
background: #fff;
color: #666;
}
.btn-confirm {
color: #fff;
}
}
.address-box {
display: flex;
align-items: center;
gap: 20rpx;
.address-input-wrapper {
flex: 1;
}
.btn-address {
flex-shrink: 0;
height: 70rpx;
line-height: 70rpx;
padding: 0 30rpx;
font-size: 26rpx;
border-radius: 8rpx;
color: #fff;
&::after {
border: none;
}
}
}
// 选择器触发器样式
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
padding: 20rpx 24rpx;
.select-value {
flex: 1;
font-size: 28rpx;
color: #333;
&.placeholder {
color: #c0c4cc;
}
}
}
// 选择弹窗样式
.picker-popup {
background: #fff;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.picker-cancel {
font-size: 28rpx;
color: #999;
}
.picker-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.picker-confirm {
font-size: 28rpx;
color: #2667E9;
}
}
.picker-body {
max-height: 600rpx;
padding: 0 30rpx;
}
.picker-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
font-size: 30rpx;
color: #333;
&:last-child {
border-bottom: none;
}
&.picker-item-active {
color: #2667E9;
}
}
}
// 区域颜色圆点
.area-color-dot {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
margin-right: 16rpx;
}
</style>