You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

766 lines
25 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<view class="header-section">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
<text class="back-text">返回</text>
</view>
<view class="header-content">
<text class="header-title">设备详情</text>
</view>
</view>
<view class="content-section">
<view class="info-card">
<view class="card-title">基本信息</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">设备名称</text>
<text class="info-value">{{ getDetailField('deviceName') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备编号</text>
<text class="info-value">{{ getDetailField('deviceCode') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备状态</text>
<u-tag :text="statusLabel" :type="statusTagType" size="mini" />
</view>
<view class="info-row">
<text class="info-label">设备型号</text>
<text class="info-value">{{ getDetailField('deviceModel') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备规格</text>
<text class="info-value">{{ getDetailField('deviceSpec') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备类型</text>
<text class="info-value">{{ getDetailField('deviceType') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备位置</text>
<text class="info-value">{{ getDetailField('deviceLocation') }}</text>
</view>
<view class="info-row">
<text class="info-label">设备负责人</text>
<text class="info-value">{{ getDetailField('deviceManagerName') }}</text>
</view>
<view class="info-row">
<text class="info-label">生产日期</text>
<text class="info-value">{{ productionDateLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">入厂日期</text>
<text class="info-value">{{ factoryEntryDateLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">创建人</text>
<text class="info-value">{{ getDetailField('creatorName') }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-value">{{ createTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{ updateTimeLabel }}</text>
</view>
<view class="info-row">
<text class="info-label">备注</text>
<text class="info-value">{{ getDetailField('remark') }}</text>
</view>
</view>
</view>
<view class="info-card">
<view class="card-title">履历</view>
<view class="tabs-box">
<u-tabs activeColor="#1a3a5c" :list="tabList" :current="currentTab" :is-scroll="false" @change="handleTabChange" />
</view>
<view>
<view v-if="currentTab === 0">
<view v-if="!inspectionGroups.length" class="empty">暂无点检数据</view>
<view v-for="group in inspectionGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">点检方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">点检时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else-if="currentTab === 1">
<view v-if="!maintainGroups.length" class="empty">暂无保养数据</view>
<view v-for="group in maintainGroups" :key="group.key" class="history-group">
<view class="history-group-head">
<text class="history-group-time">[{{ group.time }}]</text>
<text class="history-group-operator">操作人: {{ group.operator }}</text>
</view>
<view v-for="item in group.items" :key="item.key" class="history-item">
<view class="history-item-head">
<text class="result-badge" :class="'result-' + item.resultType">{{ item.resultLabel }}</text>
<text class="history-item-name">{{ item.name }}</text>
</view>
<view class="history-item-body">
<view class="history-row">
<text class="history-label">保养方式</text>
<text class="history-value">{{ detailValue(item.method) }}</text>
</view>
<view class="history-row">
<text class="history-label">判定标准</text>
<text class="history-value">{{ detailValue(item.criteria) }}</text>
</view>
<view class="history-row">
<text class="history-label">保养时间</text>
<text class="history-value">{{ detailValue(item.taskTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">创建时间</text>
<text class="history-value">{{ detailValue(item.createTimeLabel) }}</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(item.remark) }}</text>
</view>
<view v-if="item.images && item.images.length" class="history-images">
<image v-for="img in item.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(item.images, img)" />
</view>
</view>
</view>
</view>
</view>
<view v-else>
<view v-if="!repairGroups.length" class="empty">暂无维修数据</view>
<view v-for="group in repairGroups" :key="group.key" class="repair-group">
<view class="repair-group-head">
<text class="repair-group-name">{{ group.name }}</text>
<text class="repair-group-meta">共{{ group.items.length }}条</text>
</view>
<view v-for="row in group.items" :key="row.key" class="repair-item">
<view class="repair-item-head">
<text class="repair-tag">{{ detailValue(row.subjectCode) }}</text>
<text class="repair-title">{{ detailValue(row.subjectName) }}</text>
</view>
<view class="repair-item-body">
<view class="history-row">
<text class="history-label">项目内容</text>
<text class="history-value">{{ detailValue(row.subjectContent) }}</text>
</view>
<view class="history-row">
<text class="history-label">维修结果</text>
<text class="history-value">
<text class="result-badge" :class="'result-' + row.resultType">{{ row.resultLabel }}</text>
</text>
</view>
<view class="history-row">
<text class="history-label">备注</text>
<text class="history-value">{{ detailValue(row.remark) }}</text>
</view>
<view class="history-row">
<text class="history-label">完成日期</text>
<text class="history-value">{{ detailValue(row.finishDateLabel) }}</text>
</view>
<view v-if="row.images && row.images.length" class="history-images">
<image v-for="img in row.images" :key="img" class="history-image" :src="img" mode="aspectFill"
@click="previewImages(row.images, img)" />
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import {
getEquipmentDetail,
getEquipmentInspectionByDeviceId,
getEquipmentMaintenanceByDeviceId,
getEquipmentRepairListByDeviceId
} from '@/api/mes/equipment'
import { getDictLabel } from '@/utils/dict'
const loading = ref(false)
const deviceId = ref(undefined)
const detailData = ref(null)
const inspectionList = ref([])
const maintainList = ref([])
const repairList = ref([])
const tabList = ref([{ name: '点检履历' }, { name: '保养履历' }, { name: '维修履历' }])
const currentTab = ref(0)
const detailFieldCandidates = {
deviceName: ['deviceName', 'name'],
deviceCode: ['deviceCode', 'code', 'deviceNo'],
deviceStatus: ['deviceStatus', 'status'],
deviceModel: ['deviceModel', 'model'],
deviceSpec: ['deviceSpec', 'spec'],
deviceType: ['deviceTypeName', 'deviceType'],
deviceLocation: ['deviceLocation', 'location'],
deviceManagerName: ['deviceManagerName', 'managerName'],
productionDate: ['productionDate'],
factoryEntryDate: ['factoryEntryDate', 'inDate', 'entryDate'],
creatorName: ['creatorName', 'creator', 'createBy', 'createUserName'],
createTime: ['createTime'],
updateTime: ['updateTime'],
remark: ['remark', 'deviceRemark']
}
function getDetailField(field) {
const d = detailData.value
const candidates = detailFieldCandidates[field]
if (d && Array.isArray(candidates)) return detailValue(pickFirst(d, candidates))
return detailValue(d ? d[field] : undefined)
}
const statusMeta = computed(() => {
const d = detailData.value
const raw = d ? pickFirst(d, detailFieldCandidates.deviceStatus) : undefined
const label = getDictLabel('mes_tz_status', raw, detailValue(raw))
return formatStatus(label)
})
const statusLabel = computed(() => statusMeta.value.label)
const statusType = computed(() => statusMeta.value.type)
const statusTagType = computed(() => {
if (statusType.value === 'error') return 'error'
if (statusType.value === 'warning') return 'warning'
return 'success'
})
const productionDateLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? pickFirst(d, detailFieldCandidates.productionDate) : undefined)
})
const factoryEntryDateLabel = computed(() => {
const d = detailData.value
return formatDateOnly(d ? pickFirst(d, detailFieldCandidates.factoryEntryDate) : undefined)
})
const createTimeLabel = computed(() => {
const d = detailData.value
return formatHistoryTime(d ? pickFirst(d, detailFieldCandidates.createTime) : undefined) || '-'
})
const updateTimeLabel = computed(() => {
const d = detailData.value
return formatHistoryTime(d ? pickFirst(d, detailFieldCandidates.updateTime) : undefined) || '-'
})
const inspectionGroups = computed(() =>
buildStepGroups(inspectionList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const maintainGroups = computed(() =>
buildStepGroups(maintainList.value, {
timeFieldCandidates: ['taskTime', 'inspectionTime', 'createTime'],
nameFieldCandidates: ['maintainItemName', 'inspectionItemName', 'name', 'itemName'],
resultFieldCandidates: ['maintainResult', 'inspectionResult', 'result'],
methodFieldCandidates: ['inspectionMethod', 'method'],
criteriaFieldCandidates: ['judgmentCriteria', 'criteria'],
imagesFieldCandidates: ['images'],
remarkFieldCandidates: ['remark']
})
)
const repairGroups = computed(() => {
const groupsMap = new Map()
const rows = Array.isArray(repairList.value) ? repairList.value : []
for (const row of rows) {
const groupKey = String(
(row && (row.repairCode || row.repairId || row.subjectName || row.id)) ? (row.repairCode || row.repairId || row.subjectName || row.id) : '-'
)
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, {
key: groupKey,
name: String((row && (row.repairName || row.repairCode)) ? (row.repairName || row.repairCode) : groupKey),
items: []
})
}
const resultMeta = formatResult(row ? (row.repairResult !== undefined ? row.repairResult : row.result) : undefined)
groupsMap.get(groupKey).items.push({
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${Math.random()}`),
subjectCode: row ? row.subjectCode : undefined,
subjectName: row ? row.subjectName : undefined,
subjectContent: row ? row.subjectContent : undefined,
remark: row ? row.remark : undefined,
finishDateLabel: formatDateOnly(row ? row.finishDate : undefined),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
images: parseImages(row ? (row.malfunctionImages || row.malfunctionUrl || row.images) : undefined)
})
}
return Array.from(groupsMap.values())
})
function handleTabChange(e) {
const idx = e && typeof e === 'object' ? e.index : e
currentTab.value = Number(idx === 0 ? 0 : idx || 0)
}
function goBack() {
uni.navigateBack()
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage && currentPage.options ? currentPage.options : {}
const rawId = options.id !== undefined ? options.id : options.code
const decoded = rawId ? decodeURIComponent(String(rawId)) : ''
deviceId.value = decoded ? decoded : undefined
fetchAll()
})
async function fetchAll() {
if (!deviceId.value) {
uni.showToast({ title: '缺少设备ID', icon: 'none' })
return
}
loading.value = true
try {
const [detailRes, inspectionRes, maintainRes, repairRes] = await Promise.all([
getEquipmentDetail(deviceId.value),
getEquipmentInspectionByDeviceId(deviceId.value),
getEquipmentMaintenanceByDeviceId(deviceId.value),
getEquipmentRepairListByDeviceId(deviceId.value)
])
detailData.value = normalizeDetail(detailRes)
inspectionList.value = normalizeList(inspectionRes)
maintainList.value = normalizeList(maintainRes)
repairList.value = normalizeList(repairRes)
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function normalizeDetail(res) {
const data = res && res.data !== undefined ? res.data : res
if (data && !Array.isArray(data) && data.data && !Array.isArray(data.data)) return data.data
if (data && !Array.isArray(data)) return data
return null
}
function normalizeList(res) {
const data = res && res.data !== undefined ? res.data : res
if (Array.isArray(data)) return data
if (data && Array.isArray(data.data)) return data.data
if (data && data.data && Array.isArray(data.data.list)) return data.data.list
if (data && data.data && Array.isArray(data.data.rows)) return data.data.rows
if (data && data.data && Array.isArray(data.data.records)) return data.data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
if (data && Array.isArray(data.records)) return data.records
return []
}
function detailValue(v) {
if (v === 0) return '0'
if (v === false) return '否'
if (v === true) return '是'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s ? s : '-'
}
function formatStatus(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'normal' }
if (raw === '1' || raw === '0' || upper === 'OK' || raw.includes('正常') || raw.includes('运行')) return { label: raw, type: 'normal' }
if (raw === '2' || upper === 'NG' || raw.includes('停') || raw.includes('禁') || raw.includes('坏') || raw.includes('修'))
return { label: raw, type: 'error' }
return { label: raw, type: 'warning' }
}
function formatResult(v) {
const raw = v === null || v === undefined ? '' : String(v).trim()
const upper = raw.toUpperCase()
if (!raw) return { label: '-', type: 'info' }
if (raw === '0') return { label: '待检测', type: 'info' }
if (raw === '1' || upper === 'OK') return { label: '通过', type: 'success' }
if (raw === '2' || upper === 'NG') return { label: '不通过', type: 'danger' }
return { label: raw, type: 'info' }
}
function formatHistoryTime(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh, mm, ss] = value
const pad = (n) => String(n).padStart(2, '0')
if (hh !== undefined) return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
return `${y}-${pad(m)}-${pad(d)}`
}
const s = String(value).trim()
if (!s) return ''
const num = Number(s)
if (Number.isFinite(num)) {
const ms = s.length === 10 ? num * 1000 : num
const d = new Date(ms)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
const d = new Date(s)
if (!Number.isNaN(d.getTime())) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
return s
}
function formatDateOnly(value) {
const t = formatHistoryTime(value)
if (!t) return '-'
return String(t).split(' ')[0]
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
const cleaned = String(value).replace(/[`'"]/g, '').trim()
return cleaned
.split(',')
.map((v) => v.trim())
.filter(Boolean)
}
function pickFirst(obj, keys) {
for (const k of keys) {
if (obj && obj[k] !== undefined && obj[k] !== null && String(obj[k]).trim() !== '') return obj[k]
}
return undefined
}
function buildStepGroups(rows, options) {
const groupsMap = new Map()
const list = Array.isArray(rows) ? rows : []
for (const row of list) {
const time = formatHistoryTime(pickFirst(row, options.timeFieldCandidates) || (row ? row.createTime : undefined))
const operator = detailValue(row ? (row.operator || row.creatorName || row.creator) : undefined)
const managementId = row && row.managementId !== undefined && row.managementId !== null ? row.managementId : ''
const groupKey = `${managementId}__${time}__${operator}`
const name = pickFirst(row, options.nameFieldCandidates) || '-'
const resultMeta = formatResult(pickFirst(row, options.resultFieldCandidates))
const item = {
key: String(row && row.id !== undefined && row.id !== null ? row.id : `${groupKey}_${String(name)}`),
name: detailValue(name),
resultLabel: resultMeta.label,
resultType: resultMeta.type,
method: pickFirst(row, options.methodFieldCandidates),
criteria: pickFirst(row, options.criteriaFieldCandidates),
remark: pickFirst(row, options.remarkFieldCandidates),
images: parseImages(pickFirst(row, options.imagesFieldCandidates)),
taskTimeLabel: formatHistoryTime(row ? (row.taskTime || row.inspectionTime) : undefined),
createTimeLabel: formatHistoryTime(row ? row.createTime : undefined)
}
if (!groupsMap.has(groupKey)) {
groupsMap.set(groupKey, { key: groupKey, time: time || '-', operator, items: [item] })
} else {
groupsMap.get(groupKey).items.push(item)
}
}
return Array.from(groupsMap.values())
}
function previewImages(list, current) {
if (!list || !list.length) return
uni.previewImage({ urls: list, current })
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f0f2f5;
}
.header-section {
background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%);
padding: 40rpx 30rpx 80rpx;
position: relative;
}
.back-btn {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:active {
opacity: 0.7;
}
.back-icon {
font-size: 48rpx;
color: #ffffff;
margin-right: 8rpx;
}
.back-text {
font-size: 28rpx;
color: #ffffff;
}
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
.header-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 16rpx;
}
}
.content-section {
padding: 0 30rpx 30rpx;
}
.info-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-top: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #1a3a5c;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f2f5;
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 28rpx;
color: #999999;
}
.info-value {
font-size: 28rpx;
color: #333333;
}
.tabs-box {
margin-bottom: 24rpx;
}
.empty {
padding: 30rpx 0;
text-align: center;
color: #999;
font-size: 26rpx;
}
.history-group {
margin-bottom: 24rpx;
}
.history-group-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.history-group-time {
font-size: 24rpx;
color: #1a3a5c;
}
.history-group-operator {
font-size: 24rpx;
color: #666;
}
.history-item {
border: 1rpx solid #eef1f5;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 12rpx;
}
.history-item-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.history-item-name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.history-item-body {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.history-row {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.history-label {
color: #999;
font-size: 24rpx;
}
.history-value {
color: #333;
font-size: 24rpx;
text-align: right;
}
.history-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 10rpx;
}
.history-image {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx;
}
.result-badge {
font-size: 22rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
.result-info {
background: rgba(144, 147, 153, 0.15);
color: #606266;
}
.result-success {
background: rgba(24, 188, 55, 0.15);
color: #18bc37;
}
.result-danger {
background: rgba(255, 77, 79, 0.15);
color: #ff4d4f;
}
.repair-group {
margin-bottom: 20rpx;
}
.repair-group-head {
display: flex;
justify-content: space-between;
margin-bottom: 12rpx;
}
.repair-group-name {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 600;
}
.repair-group-meta {
font-size: 24rpx;
color: #999;
}
.repair-item {
border: 1rpx solid #eef1f5;
border-radius: 12rpx;
padding: 16rpx;
margin-bottom: 12rpx;
}
.repair-item-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.repair-tag {
font-size: 22rpx;
color: #666;
background: #f5f7fa;
border-radius: 8rpx;
padding: 4rpx 10rpx;
}
.repair-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
</style>