<template>
|
<view class="inspection-detail">
|
<!-- 使用通用页面头部组件 -->
|
<PageHeader title="设备巡检详情"
|
@back="goBack" />
|
<!-- 设备信息卡片 -->
|
<view class="device-card">
|
<view class="device-header">
|
<view class="device-icon">
|
<up-icon name="settings"
|
size="24"
|
color="#1890ff"></up-icon>
|
</view>
|
<view class="device-info">
|
<text class="device-name">{{ deviceInfo.deviceName }}</text>
|
<text class="device-code">{{ deviceInfo.deviceCode }}</text>
|
</view>
|
<view class="qr-scan"
|
@click="scanDeviceQR">
|
<up-icon name="scan"
|
size="20"
|
color="#1890ff"></up-icon>
|
<text class="scan-text">扫码</text>
|
</view>
|
</view>
|
<view class="device-details">
|
<view class="detail-item">
|
<text class="label">位置:</text>
|
<text class="value">{{ deviceInfo.location }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="label">巡检时间:</text>
|
<text class="value">{{ deviceInfo.inspectionTime }}</text>
|
</view>
|
<view class="detail-item">
|
<text class="label">负责人:</text>
|
<text class="value">{{ deviceInfo.inspector }}</text>
|
</view>
|
</view>
|
</view>
|
<!-- 巡检项目清单 -->
|
<view class="inspection-items">
|
<view class="section-title">
|
<up-icon name="list"
|
size="18"
|
color="#333"></up-icon>
|
<text class="title-text">巡检项目清单</text>
|
<text class="progress-text">({{ completedItems }}/{{ totalItems }})</text>
|
</view>
|
<view class="items-list">
|
<view v-for="(item, index) in inspectionItems"
|
:key="index"
|
class="inspection-item"
|
:class="{ 'completed': item.completed, 'abnormal': item.isAbnormal }">
|
<view class="item-header"
|
@click="toggleItem(index)">
|
<view class="item-left">
|
<view class="checkbox"
|
:class="{ 'checked': item.completed }">
|
<up-icon v-if="item.completed"
|
name="checkmark"
|
size="14"
|
color="#ffffff"></up-icon>
|
</view>
|
<text class="item-name">{{ item.name }}</text>
|
</view>
|
<view class="item-status">
|
<u-tag v-if="item.isAbnormal"
|
type="error"
|
size="mini">异常</u-tag>
|
<u-tag v-else-if="item.completed"
|
type="success"
|
size="mini">正常</u-tag>
|
<u-tag v-else
|
type="info"
|
size="mini">待检</u-tag>
|
</view>
|
</view>
|
<!-- 展开的详情内容 -->
|
<view v-if="item.expanded"
|
class="item-content">
|
<view class="item-description">
|
<text class="desc-text">{{ item.description }}</text>
|
</view>
|
<!-- 巡检结果选择 -->
|
<view class="result-section">
|
<text class="section-label">巡检结果:</text>
|
<view class="result-options">
|
<u-radio-group v-model="item.result"
|
@change="onResultChange(index, $event)">
|
<u-radio v-for="option in resultOptions"
|
:key="option.value"
|
:label="option.value"
|
:name="option.label"
|
size="small">
|
{{ option.label }}
|
</u-radio>
|
</u-radio-group>
|
</view>
|
</view>
|
<!-- 异常情况描述 -->
|
<view v-if="item.result === 'abnormal'"
|
class="abnormal-section">
|
<text class="section-label">异常描述:</text>
|
<up-textarea v-model="item.abnormalDesc"
|
placeholder="请详细描述异常情况"
|
:maxlength="200"
|
count
|
height="80"></up-textarea>
|
</view>
|
<!-- 图片上传 -->
|
<view class="upload-section">
|
<text class="section-label">现场照片:</text>
|
<up-upload :fileList="item.images"
|
@afterRead="(event) => afterRead(event, index, 'images')"
|
@delete="(event) => deleteFile(event, index, 'images')"
|
name="images"
|
multiple
|
:maxCount="5"
|
:previewImage="true">
|
<view class="upload-btn">
|
<up-icon name="camera"
|
size="20"
|
color="#999"></up-icon>
|
<text class="upload-text">添加照片</text>
|
</view>
|
</up-upload>
|
</view>
|
<!-- 视频上传 -->
|
<view class="upload-section">
|
<text class="section-label">现场视频:</text>
|
<up-upload :fileList="item.videos"
|
@afterRead="(event) => afterRead(event, index, 'videos')"
|
@delete="(event) => deleteFile(event, index, 'videos')"
|
name="videos"
|
:maxCount="2"
|
accept="video">
|
<view class="upload-btn">
|
<up-icon name="play-circle"
|
size="20"
|
color="#999"></up-icon>
|
<text class="upload-text">添加视频</text>
|
</view>
|
</up-upload>
|
</view>
|
<!-- 备注 -->
|
<view class="remark-section">
|
<text class="section-label">备注:</text>
|
<up-textarea v-model="item.remark"
|
placeholder="请输入备注信息(可选)"
|
:maxlength="100"
|
count
|
height="60"></up-textarea>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
<!-- 底部操作按钮 -->
|
<view class="bottom-actions">
|
<u-button type="primary"
|
size="large"
|
:disabled="!canSubmit"
|
@click="submitInspection"
|
:loading="submitting">
|
{{ allCompleted ? '提交巡检记录' : `继续巡检 (${completedItems}/${totalItems})` }}
|
</u-button>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted } from "vue";
|
import { onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { submitInspectionRecord } from "@/api/equipmentManagement/inspection";
|
import dayjs from "dayjs";
|
|
// 设备信息
|
const deviceInfo = ref({});
|
|
// 巡检项目列表
|
const inspectionItems = ref([]);
|
|
// 提交状态
|
const submitting = ref(false);
|
|
// 巡检结果选项
|
const resultOptions = [
|
{ label: "正常", value: "normal" },
|
{ label: "异常", value: "abnormal" },
|
];
|
|
// 显示提示信息
|
const showToast = message => {
|
uni.showToast({
|
title: message,
|
icon: "none",
|
});
|
};
|
|
// 计算属性
|
const totalItems = computed(() => inspectionItems.value.length);
|
const completedItems = computed(
|
() => inspectionItems.value.filter(item => item.completed).length
|
);
|
const allCompleted = computed(
|
() => completedItems.value === totalItems.value && totalItems.value > 0
|
);
|
const canSubmit = computed(() => completedItems.value > 0);
|
|
// 返回上一页
|
const goBack = () => {
|
if (completedItems.value > 0) {
|
uni.showModal({
|
title: "提示",
|
content: "当前有未保存的巡检记录,确定要离开吗?",
|
success: res => {
|
if (res.confirm) {
|
uni.navigateBack();
|
}
|
},
|
});
|
} else {
|
uni.navigateBack();
|
}
|
};
|
|
// 扫描设备二维码
|
const scanDeviceQR = () => {
|
uni.scanCode({
|
success: res => {
|
console.log("扫码结果:", res);
|
if (res.result.includes(deviceInfo.value.deviceCode)) {
|
showToast("设备确认成功");
|
// 记录扫码时间
|
deviceInfo.value.scanTime = new Date().toISOString();
|
} else {
|
showToast("设备二维码不匹配");
|
}
|
},
|
fail: err => {
|
console.log("扫码失败:", err);
|
showToast("扫码失败");
|
},
|
});
|
};
|
|
// 切换巡检项目
|
const toggleItem = index => {
|
inspectionItems.value[index].expanded =
|
!inspectionItems.value[index].expanded;
|
};
|
|
// 巡检结果改变
|
const onResultChange = (index, value) => {
|
const item = inspectionItems.value[index];
|
item.result = value;
|
item.completed = true;
|
item.isAbnormal = value === "abnormal";
|
|
// 如果选择正常,清空异常描述
|
if (value === "normal") {
|
item.abnormalDesc = "";
|
}
|
};
|
|
// 文件上传后处理
|
const afterRead = async (event, index, type) => {
|
const { file } = event;
|
const item = inspectionItems.value[index];
|
|
// 模拟上传过程
|
uni.showLoading({ title: "上传中..." });
|
|
try {
|
// 这里应该调用实际的上传API
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
// 添加到对应的文件列表
|
if (type === "images") {
|
item.images = item.images || [];
|
item.images.push({
|
url: file.url,
|
name: file.name,
|
size: file.size,
|
});
|
} else if (type === "videos") {
|
item.videos = item.videos || [];
|
item.videos.push({
|
url: file.url,
|
name: file.name,
|
size: file.size,
|
});
|
}
|
|
uni.hideLoading();
|
showToast("上传成功");
|
} catch (error) {
|
uni.hideLoading();
|
showToast("上传失败");
|
}
|
};
|
|
// 删除文件
|
const deleteFile = (event, index, type) => {
|
const item = inspectionItems.value[index];
|
if (type === "images") {
|
item.images.splice(event.index, 1);
|
} else if (type === "videos") {
|
item.videos.splice(event.index, 1);
|
}
|
};
|
|
// 提交巡检记录
|
const submitInspection = async () => {
|
if (!canSubmit.value) {
|
showToast("请至少完成一项巡检");
|
return;
|
}
|
|
// 检查异常项目是否填写了描述
|
const abnormalItems = inspectionItems.value.filter(item => item.isAbnormal);
|
for (const item of abnormalItems) {
|
if (!item.abnormalDesc || item.abnormalDesc.trim() === "") {
|
showToast(`请填写"${item.name}"的异常描述`);
|
return;
|
}
|
}
|
|
submitting.value = true;
|
|
try {
|
const recordData = {
|
deviceId: deviceInfo.value.id,
|
deviceCode: deviceInfo.value.deviceCode,
|
inspectionDate: dayjs().format("YYYY-MM-DD"),
|
inspector: deviceInfo.value.inspector,
|
scanTime: deviceInfo.value.scanTime,
|
items: inspectionItems.value.map(item => ({
|
name: item.name,
|
result: item.result,
|
completed: item.completed,
|
isAbnormal: item.isAbnormal,
|
abnormalDesc: item.abnormalDesc,
|
images: item.images || [],
|
videos: item.videos || [],
|
remark: item.remark,
|
})),
|
completedAt: new Date().toISOString(),
|
};
|
|
// 模拟API调用
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
// 实际API调用
|
// await submitInspectionRecord(recordData)
|
|
showToast("巡检记录提交成功");
|
|
// 返回列表页面
|
setTimeout(() => {
|
uni.navigateBack();
|
}, 1500);
|
} catch (error) {
|
showToast("提交失败,请重试");
|
} finally {
|
submitting.value = false;
|
}
|
};
|
|
// 初始化数据
|
const initData = () => {
|
// 从存储中获取当前巡检信息
|
const currentInspection = uni.getStorageSync("currentInspection");
|
if (currentInspection) {
|
deviceInfo.value = currentInspection;
|
}
|
|
// 模拟巡检项目数据
|
inspectionItems.value = [
|
{
|
name: "设备外观检查",
|
description: "检查设备外观是否有损坏、锈蚀、变形等异常情况",
|
completed: false,
|
expanded: false,
|
result: "",
|
isAbnormal: false,
|
abnormalDesc: "",
|
images: [],
|
videos: [],
|
remark: "",
|
},
|
{
|
name: "运行状态检查",
|
description: "检查设备运行是否正常,有无异常声音、振动等",
|
completed: false,
|
expanded: false,
|
result: "",
|
isAbnormal: false,
|
abnormalDesc: "",
|
images: [],
|
videos: [],
|
remark: "",
|
},
|
{
|
name: "安全装置检查",
|
description: "检查各类安全装置是否完好,安全标识是否清晰",
|
completed: false,
|
expanded: false,
|
result: "",
|
isAbnormal: false,
|
abnormalDesc: "",
|
images: [],
|
videos: [],
|
remark: "",
|
},
|
{
|
name: "环境条件检查",
|
description: "检查设备周围环境是否符合要求,通风、照明等是否正常",
|
completed: false,
|
expanded: false,
|
result: "",
|
isAbnormal: false,
|
abnormalDesc: "",
|
images: [],
|
videos: [],
|
remark: "",
|
},
|
{
|
name: "仪表读数记录",
|
description: "记录相关仪表的读数,检查是否在正常范围内",
|
completed: false,
|
expanded: false,
|
result: "",
|
isAbnormal: false,
|
abnormalDesc: "",
|
images: [],
|
videos: [],
|
remark: "",
|
},
|
];
|
};
|
|
onMounted(() => {
|
initData();
|
});
|
|
onShow(() => {
|
// 页面显示时刷新数据
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.inspection-detail {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
min-height: 100vh;
|
padding-bottom: 80px;
|
position: relative;
|
|
&::before {
|
content: "";
|
position: absolute;
|
top: 0;
|
left: 0;
|
right: 0;
|
height: 200px;
|
background: linear-gradient(
|
135deg,
|
rgba(102, 126, 234, 0.8) 0%,
|
rgba(118, 75, 162, 0.8) 100%
|
);
|
z-index: 0;
|
}
|
}
|
|
.device-card {
|
background: rgba(255, 255, 255, 0.95);
|
backdrop-filter: blur(15px);
|
margin: 10px 20px;
|
border-radius: 20px;
|
padding: 24px;
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
position: relative;
|
z-index: 1;
|
transition: all 0.3s ease;
|
|
&:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
}
|
}
|
|
.device-header {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
margin-bottom: 20px;
|
}
|
|
.device-icon {
|
width: 56px;
|
height: 56px;
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
border-radius: 16px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
|
transition: all 0.3s ease;
|
|
&:hover {
|
transform: scale(1.05);
|
}
|
}
|
|
.device-info {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.device-name {
|
font-size: 20px;
|
font-weight: 600;
|
color: #1a1a1a;
|
line-height: 1.3;
|
}
|
|
.device-code {
|
font-size: 13px;
|
color: #8c8c8c;
|
font-weight: 500;
|
padding: 4px 12px;
|
background: rgba(140, 140, 140, 0.1);
|
border-radius: 12px;
|
display: inline-block;
|
width: fit-content;
|
}
|
|
.qr-scan {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 6px;
|
padding: 12px 16px;
|
background: linear-gradient(135deg, #52c41a, #389e0d);
|
border-radius: 12px;
|
box-shadow: 0 4px 15px rgba(82, 196, 26, 0.3);
|
transition: all 0.3s ease;
|
|
&:hover {
|
transform: scale(1.05);
|
box-shadow: 0 6px 20px rgba(82, 196, 26, 0.4);
|
}
|
|
&:active {
|
transform: scale(0.98);
|
}
|
}
|
|
.scan-text {
|
font-size: 13px;
|
color: #ffffff;
|
font-weight: 600;
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
}
|
|
.device-details {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
background: rgba(248, 250, 252, 0.8);
|
border-radius: 16px;
|
padding: 16px;
|
backdrop-filter: blur(10px);
|
}
|
|
.detail-item {
|
display: flex;
|
align-items: center;
|
font-size: 14px;
|
padding: 8px 0;
|
transition: all 0.2s ease;
|
|
&:hover {
|
background: rgba(255, 255, 255, 0.5);
|
margin: 0 -8px;
|
padding-left: 8px;
|
padding-right: 8px;
|
border-radius: 8px;
|
}
|
}
|
|
.label {
|
color: #595959;
|
min-width: 80px;
|
font-weight: 500;
|
}
|
|
.value {
|
color: #262626;
|
flex: 1;
|
font-weight: 500;
|
}
|
|
.inspection-items {
|
margin: 10px 20px;
|
position: relative;
|
z-index: 1;
|
}
|
|
.section-title {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
padding: 20px 0;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.95);
|
backdrop-filter: blur(15px);
|
border-radius: 16px;
|
padding: 20px;
|
margin-bottom: 16px;
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
}
|
|
.title-text {
|
font-size: 18px;
|
font-weight: 600;
|
color: #1a1a1a;
|
flex: 1;
|
}
|
|
.progress-text {
|
font-size: 15px;
|
font-weight: 600;
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
background-clip: text;
|
}
|
|
.items-list {
|
background: rgba(255, 255, 255, 0.95);
|
backdrop-filter: blur(15px);
|
border-radius: 20px;
|
overflow: hidden;
|
margin-top: 0;
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
}
|
|
.inspection-item {
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
transition: all 0.3s ease;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
&.completed {
|
background: rgba(82, 196, 26, 0.05);
|
border-left: 4px solid #52c41a;
|
}
|
|
&.abnormal {
|
background: rgba(255, 77, 79, 0.05);
|
border-left: 4px solid #ff4d4f;
|
}
|
|
&:hover {
|
background: rgba(102, 126, 234, 0.05);
|
}
|
}
|
|
.item-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 15px;
|
cursor: pointer;
|
}
|
|
.item-left {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
flex: 1;
|
}
|
|
.checkbox {
|
width: 20px;
|
height: 20px;
|
border: 2px solid #d9d9d9;
|
border-radius: 4px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
transition: all 0.3s;
|
|
&.checked {
|
background: #52c41a;
|
border-color: #52c41a;
|
}
|
}
|
|
.item-name {
|
font-size: 15px;
|
color: #333;
|
font-weight: 500;
|
}
|
|
.item-status {
|
flex-shrink: 0;
|
}
|
|
.item-content {
|
padding: 0 15px 20px;
|
border-top: 1px solid #f5f5f5;
|
}
|
|
.item-description {
|
padding: 15px 0;
|
}
|
|
.desc-text {
|
font-size: 14px;
|
color: #666;
|
line-height: 1.5;
|
}
|
|
.result-section,
|
.abnormal-section,
|
.upload-section,
|
.remark-section {
|
margin-top: 15px;
|
}
|
|
.section-label {
|
display: block;
|
font-size: 14px;
|
color: #333;
|
margin-bottom: 8px;
|
font-weight: 500;
|
}
|
|
.result-options {
|
display: flex;
|
gap: 20px;
|
}
|
|
.upload-btn {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
width: 88px;
|
height: 88px;
|
border: 2px dashed rgba(102, 126, 234, 0.3);
|
border-radius: 16px;
|
background: rgba(102, 126, 234, 0.05);
|
gap: 8px;
|
transition: all 0.3s ease;
|
|
&:hover {
|
border-color: rgba(102, 126, 234, 0.5);
|
background: rgba(102, 126, 234, 0.1);
|
transform: translateY(-2px);
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
}
|
|
&:active {
|
transform: translateY(0);
|
}
|
}
|
|
.upload-text {
|
font-size: 13px;
|
color: #667eea;
|
font-weight: 500;
|
}
|
|
.bottom-actions {
|
position: fixed;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
background: rgba(255, 255, 255, 0.95);
|
backdrop-filter: blur(20px);
|
padding: 20px;
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.1);
|
z-index: 10;
|
|
button {
|
height: 48px;
|
border-radius: 16px;
|
font-weight: 600;
|
font-size: 16px;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
&:hover {
|
transform: translateY(-2px);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
}
|
|
&:active {
|
transform: translateY(0);
|
}
|
}
|
}
|
</style>
|