<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>
|