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.

2110 lines
68 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.

<!--
TODO: 字典和统计接口待完善
需要引入的字典
1. active_flag - 激活标识字典
2. maint_type - 保养类型字典
3. alarm_level - 报警级别字典
4. handle_status - 处理状态字典
5. bills_status - 工单状态字典
6. device_status - 设备状态字典
需要创建的统计接口
1. getDmsDeviceStatistics() - 设备统计接口
返回{ totalCount, runningCount, maintenanceCount, alarmCount, faultCount }
2. getDmsDeviceMaintenanceStats(machineId) - 设备维护统计接口
返回{ plannedCount, temporaryCount, avgInterval, nextMaintenanceDate }
-->
<template>
<div class="dms-machine-ledger">
<!-- 工厂背景装饰 -->
<div class="factory-bg-decoration">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none">
<path d="M0,0 L0,60 L50,60 L50,30 L100,30 L100,50 L150,50 L150,20 L200,20 L200,60 L1200,60 L1200,0 Z"
fill="url(#factory-gradient)" opacity="0.1"/>
<defs>
<linearGradient id="factory-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#409EFF;stop-opacity:1" />
<stop offset="100%" style="stop-color:#667eea;stop-opacity:1" />
</linearGradient>
</defs>
</svg>
</div>
<!-- 顶部信息面板 -->
<div class="top-info-panel">
<div class="panel-left">
<h1 class="page-title">
<el-icon><Monitor /></el-icon>
设备台账管理
</h1>
<p class="page-subtitle">设备全生命周期管理与履历追踪</p>
</div>
<div class="panel-right">
<div class="current-time">
<el-icon><Clock /></el-icon>
{{ currentTime }}
</div>
</div>
</div>
<!-- 功能操作区 -->
<div class="action-toolbar">
<div class="toolbar-left">
<el-button-group>
<el-button :type="viewMode === 'grid' ? 'primary' : 'default'"
@click="viewMode = 'grid'">
<el-icon><Grid /></el-icon>
网格视图
</el-button>
<el-button :type="viewMode === 'list' ? 'primary' : 'default'"
@click="viewMode = 'list'">
<el-icon><List /></el-icon>
列表视图
</el-button>
</el-button-group>
</div>
<div class="toolbar-right">
<el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['dms:dmsBaseMachineInfo:add']">
新增设备
</el-button>
<el-button type="success" icon="Edit" :disabled="single" @click="handleUpdate()"
v-hasPermi="['dms:dmsBaseMachineInfo:edit']">
修改设备
</el-button>
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete()"
v-hasPermi="['dms:dmsBaseMachineInfo:remove']">
删除设备
</el-button>
<el-button type="warning" icon="Download" @click="handleExport"
v-hasPermi="['dms:dmsBaseMachineInfo:export']">
导出数据
</el-button>
<el-button text @click="showSearch = !showSearch">
<el-icon><Filter /></el-icon>
{{ showSearch ? '隐藏' : '显示' }}筛选
</el-button>
</div>
</div>
<!-- 查询过滤器 -->
<el-card class="search-filter-card" v-show="showSearch">
<template #header>
<div class="filter-header">
<div class="filter-title">
<el-icon><Filter /></el-icon>
筛选条件
</div>
<el-button text @click="showSearch = false">
<el-icon><ArrowUp /></el-icon>
</el-button>
</div>
</template>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item label="设备编号" prop="machineCode">
<el-input v-model="queryParams.machineCode" placeholder="请输入设备编号" clearable />
</el-form-item>
<el-form-item label="设备名称" prop="machineName">
<el-input v-model="queryParams.machineName" placeholder="请输入设备名称" clearable />
</el-form-item>
<el-form-item label="设备位置" prop="machineLocation">
<el-input v-model="queryParams.machineLocation" placeholder="请输入设备位置" clearable />
</el-form-item>
<el-form-item label="设备状态" prop="machineStatus">
<el-select v-model="queryParams.machineStatus" placeholder="请选择设备状态" clearable>
<el-option v-for="dict in machine_status" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="machineType">
<el-select v-model="queryParams.machineType" placeholder="请选择设备类型" clearable>
<el-option v-for="type in deviceTypes" :key="type.deviceTypeId"
:label="type.deviceTypeName" :value="type.deviceTypeId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" class="filter-btn-primary">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 主内容区 -->
<div class="main-content">
<!-- 网格视图 -->
<div v-if="viewMode === 'grid'" class="grid-view" v-loading="loading">
<transition-group name="fade-transform" tag="div" class="machine-grid">
<div v-for="machine in machineList" :key="machine.machineId"
class="machine-card-wrapper" @click="selectMachine(machine)">
<div :class="['machine-card', { selected: selectedMachine?.machineId === machine.machineId }]">
<!-- 状态指示灯 -->
<div :class="['status-indicator', `status-${machine.machineStatus}`]">
<span class="status-dot"></span>
</div>
<!-- 设备图标区 -->
<div class="machine-visual">
<div class="machine-icon-bg" v-if="!machine.photoAddress">
<el-icon :size="40">
<component :is="getMachineIcon(machine.machineType)" />
</el-icon>
</div>
<div class="machine-photo" v-else>
<ImagePreview
:width="40"
:height="40"
:src="machine.photoAddress"
:preview-src-list="[machine.photoAddress]"
/>
</div>
<div class="machine-number">{{ machine.machineCode }}</div>
</div>
<!-- 设备信息 -->
<div class="machine-details">
<h3 class="machine-name">{{ machine.machineName }}</h3>
<div class="machine-meta">
<div class="meta-item">
<el-icon><Location /></el-icon>
<span>{{ machine.machineLocation || '-' }}</span>
</div>
<div class="meta-item">
<el-icon><Memo /></el-icon>
<span>{{ getDeviceTypeName(machine.machineType) }}</span>
</div>
<div class="meta-item">
<el-icon><Timer /></el-icon>
<span>运行 {{ calculateRunDays(machine.createTime) }} 天</span>
</div>
</div>
</div>
<!-- 快捷操作 -->
<div class="quick-actions">
<el-button size="small" @click.stop="handleUpdate(machine)" v-hasPermi="['dms:dmsBaseMachineInfo:edit']">编辑</el-button>
<el-button size="small" @click.stop="viewLifecycle(machine)">生命周期</el-button>
<el-button size="small" @click.stop="viewDetails(machine)">详情</el-button>
</div>
</div>
</div>
</transition-group>
<!-- 网格视图分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
<!-- 列表视图 -->
<div v-else-if="viewMode === 'list'" class="list-view">
<el-table v-loading="loading" :data="machineList" :row-class-name="tableRowClassName"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备编号" align="center" prop="machineCode" />
<el-table-column label="设备名称" align="center" prop="machineName" />
<el-table-column label="设备类型" align="center" prop="machineType">
<template #default="scope">
{{ getDeviceTypeName(scope.row.machineType) }}
</template>
</el-table-column>
<el-table-column label="设备位置" align="center" prop="machineLocation" />
<el-table-column label="设备状态" align="center" prop="machineStatus">
<template #default="scope">
<div class="status-cell">
<span :class="['status-badge', `status-${scope.row.machineStatus}`]"></span>
<dict-tag :options="machine_status" :value="String(scope.row.machineStatus)" />
</div>
</template>
</el-table-column>
<el-table-column label="设备图片" align="center" prop="photoAddress" width="100">
<template #default="scope">
<ImagePreview
v-if="scope.row.photoAddress && checkFileSuffix(scope.row.photoAddress)"
:width="50"
:height="50"
:src="scope.row.photoAddress"
:preview-src-list="[scope.row.photoAddress]"
/>
<span v-else>无图片</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span class="date-text">{{ formatDate(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="240">
<template #default="scope">
<el-button type="primary" size="small" @click="handleUpdate(scope.row)" v-hasPermi="['dms:dmsBaseMachineInfo:edit']">编辑</el-button>
<el-button size="small" @click="viewLifecycle(scope.row)">生命周期</el-button>
<el-button size="small" @click="viewDetails(scope.row)">详情</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['dms:dmsBaseMachineInfo:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</div>
<!-- 添加或修改设备信息对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
<el-form ref="machineFormRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备编号" prop="machineCode">
<el-input v-model="form.machineCode" placeholder="请输入设备编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备名称" prop="machineName">
<el-input v-model="form.machineName" placeholder="请输入设备名称" />
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="资产编号" prop="assetNumber">
<el-input v-model="form.assetNumber" placeholder="请输入资产编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备位置" prop="machineLocation">
<el-input v-model="form.machineLocation" placeholder="请输入设备位置" />
</el-form-item>
</el-col>
</el-row> -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备类型" prop="machineType">
<el-select v-model="form.machineType" placeholder="请选择设备类型" style="width: 100%;">
<el-option
v-for="item in deviceTypes"
:key="item.deviceTypeId"
:label="item.deviceTypeName"
:value="item.deviceTypeId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备规格" prop="machineSpec">
<el-input v-model="form.machineSpec" placeholder="请输入设备规格" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商" prop="supplierId">
<el-input v-model="form.supplierId" placeholder="请输入供应商" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备状态" prop="machineStatus">
<el-select v-model="form.machineStatus" placeholder="请选择设备状态" style="width: 100%;">
<el-option
v-for="dict in machine_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属车间" prop="workshopId">
<el-select v-model="form.workshopId" placeholder="请选择所属车间" style="width: 100%;">
<el-option
v-for="item in workshopInfoList"
:key="item.workshopId"
:label="item.workshopName"
:value="item.workshopId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备模型" prop="deviceModeId">
<el-select v-model="form.deviceModeId" placeholder="请选择设备模型" style="width: 100%;">
<el-option
v-for="item in deviceModeList"
:key="item.deviceModeId"
:label="item.deviceModeName"
:value="item.deviceModeId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row> -->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备IP地址" prop="machineIp">
<el-input v-model="form.machineIp" placeholder="请输入设备IP地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备端口" prop="machinePort">
<el-input v-model="form.machinePort" placeholder="请输入设备端口" />
</el-form-item>
</el-col>
</el-row>-->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备协议" prop="accessProtocol">
<el-input v-model="form.accessProtocol" placeholder="请输入设备协议" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="寄存器地址" prop="registerAddress">
<el-input v-model="form.registerAddress" placeholder="请输入寄存器地址" />
</el-form-item>
</el-col>
</el-row>-->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="数据类型" prop="dataType">
<el-select v-model="form.dataType" placeholder="请选择数据类型" style="width: 100%;">
<el-option
v-for="dict in machine_data_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数据长度" prop="dataLength">
<el-input-number v-model="form.dataLength" placeholder="请输入数据长度" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>-->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="数据编码格式" prop="dataEncoding">
<el-select v-model="form.dataEncoding" placeholder="请选择数据编码格式" style="width: 100%;">
<el-option
v-for="dict in machine_data_encoding"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="请求间隔(毫秒)" prop="requestInterval">
<el-input-number v-model="form.requestInterval" placeholder="请输入请求间隔" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>-->
<!-- <el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班制类型" prop="classType">
<el-radio-group v-model="form.classType">
<el-radio
v-for="dict in mes_class_type"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入库类型" prop="instockType">
<el-radio-group v-model="form.instockType">
<el-radio
v-for="dict in mes_instock_type"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row> -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="设备图片" prop="photoAddress">
<imageUpload v-model="form.file" :limit="1" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 生命周期抽屉 -->
<el-drawer v-model="lifecycleDrawer" :title="`设备生命周期 - ${selectedMachine?.machineName || ''}`"
size="60%" class="lifecycle-drawer">
<div class="lifecycle-container" v-loading="lifecycleLoading">
<!-- 设备概览卡片 -->
<div class="device-overview-card" v-if="selectedMachine">
<div class="overview-header">
<div class="device-avatar">
<el-icon :size="40">
<component :is="getMachineIcon(selectedMachine.machineType)" />
</el-icon>
</div>
<div class="device-basic">
<h2>{{ selectedMachine.machineName }}</h2>
<p>编号:{{ selectedMachine.machineCode }} | 位置:{{ selectedMachine.machineLocation || '-' }}</p>
</div>
</div>
<el-divider />
<div class="overview-stats">
<div class="stat">
<span class="stat-label">运行天数</span>
<span class="stat-value">{{ calculateRunDays(selectedMachine.createTime) }}</span>
</div>
<div class="stat">
<span class="stat-label">故障次数</span>
<span class="stat-value">{{ lifecycleStats.faultCount }}</span>
</div>
<div class="stat">
<span class="stat-label">保养次数</span>
<span class="stat-value">{{ lifecycleStats.maintCount }}</span>
</div>
<div class="stat">
<span class="stat-label">报警次数</span>
<span class="stat-value">{{ lifecycleStats.alarmCount }}</span>
</div>
</div>
</div>
<!-- 生命周期类型选择 -->
<div class="lifecycle-type-selector">
<h3>选择查看的生命周期类型</h3>
<div class="type-cards">
<div v-for="type in lifecycleTypes" :key="type.value"
:class="['type-card', { active: selectedLifecycleTypes.includes(type.value) }]"
@click="toggleLifecycleType(type.value)">
<div class="type-icon" :style="{ background: type.gradient }">
<el-icon :size="24">
<component :is="type.icon" />
</el-icon>
</div>
<span class="type-name">{{ type.label }}</span>
<span v-if="type.count > 0" class="type-count">{{ type.count }}</span>
</div>
</div>
</div>
<!-- 生命周期时间轴 -->
<div class="lifecycle-timeline-container" v-if="lifecycleEvents.length > 0">
<h3>设备履历时间轴</h3>
<div class="timeline-wrapper">
<div v-for="event in lifecycleEvents" :key="event.id" class="timeline-item">
<div class="timeline-marker" :style="{ backgroundColor: event.color }">
<el-icon :size="20">
<component :is="event.icon" />
</el-icon>
</div>
<div class="timeline-content">
<div class="event-header">
<h4>{{ event.title }}</h4>
<span class="event-time">{{ formatDateTime(event.time) }}</span>
</div>
<p class="event-description">{{ event.description }}</p>
<div class="event-tags">
<el-tag v-for="tag in event.tags" :key="tag" size="small">{{ tag }}</el-tag>
</div>
<div class="event-details" v-if="event.details && event.details.length">
<el-collapse>
<el-collapse-item title="查看详情">
<el-descriptions :column="2" border>
<el-descriptions-item v-for="item in event.details"
:key="item.label" :label="item.label">
<template v-if="item.dictOptions">
<dict-tag :options="item.dictOptions" :value="String(item.value)" />
</template>
<template v-else>
{{ item.value ?? '-' }}
</template>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</div>
</div>
<div class="timeline-line"></div>
</div>
</div>
</div>
<!-- 空状态 -->
<el-empty v-else-if="selectedLifecycleTypes.length > 0"
description="暂无相关生命周期数据" />
</div>
</el-drawer>
<!-- 设备详情对话框 -->
<el-dialog v-model="detailDialog" :title="`设备详情 - ${selectedMachine?.machineName || ''}`"
width="80%" top="5vh">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<div class="detail-section">
<el-row :gutter="20">
<el-col :span="12">
<el-card>
<template #header>
<span class="card-header-title">设备基础信息</span>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="设备编号">{{ selectedMachine?.machineCode }}</el-descriptions-item>
<el-descriptions-item label="设备名称">{{ selectedMachine?.machineName }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ getDeviceTypeName(selectedMachine?.machineType) }}</el-descriptions-item>
<el-descriptions-item label="设备位置">{{ selectedMachine?.machineLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备状态">
<dict-tag :options="machine_status" :value="String(selectedMachine?.machineStatus ?? '')" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDateTime(selectedMachine?.createTime) }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<span class="card-header-title">技术参数</span>
</template>
<el-table :data="technicalParams" max-height="300">
<el-table-column prop="paramName" label="参数名称" />
<el-table-column prop="paramValue" label="参数值" />
<el-table-column prop="unit" label="单位" />
<el-table-column prop="remark" label="备注" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="维护信息" name="maintenance">
<div class="detail-section maintenance-section">
<el-row :gutter="20">
<el-col :span="24">
<el-card class="maintenance-summary">
<template #header>
<span class="card-header-title">维护统计</span>
</template>
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="计划保养次数" :value="maintenanceStats.planned" />
</el-col>
<el-col :span="6">
<el-statistic title="临时保养次数" :value="maintenanceStats.temporary" />
</el-col>
<el-col :span="6">
<el-statistic title="平均间隔(天)" :value="maintenanceStats.avgInterval" />
</el-col>
<el-col :span="6">
<el-statistic title="下次保养日期" :value="maintenanceStats.nextDate || '-'" />
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :span="24">
<el-card>
<template #header>
<span class="card-header-title">维护记录</span>
</template>
<el-table :data="maintenanceRecords" max-height="400">
<el-table-column prop="maintCode" label="保养单号" />
<el-table-column prop="maintType" label="保养类型">
<template #default="scope">
<dict-tag :options="maint_type" :value="String(scope.row.maintType)" />
</template>
</el-table-column>
<el-table-column prop="maintDate" label="保养日期" />
<el-table-column prop="maintPerson" label="保养人员" />
<el-table-column prop="maintContent" label="保养内容" show-overflow-tooltip />
<el-table-column prop="maintStatus" label="保养结果">
<template #default="scope">
<dict-tag :options="maint_status" :value="String(scope.row.maintStatus)" />
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script setup name="ProdBaseMachineInfo" lang="ts">
import { ref, reactive, toRefs, getCurrentInstance, onMounted, onUnmounted, shallowRef, markRaw, watch } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import {
listDmsBaseMachineInfo,
getDmsBaseMachineInfo,
addProdBaseMachineInfo,
updateProdBaseMachineInfo,
delDmsBaseMachineInfo
} from '@/api/dms/dmsBaseMachineInfo';
import { getBaseDeviceTypeList } from '@/api/dms/baseDeviceType';
import { getDmsBaseDevicePurchaseList, getPurchaseCount } from '@/api/dms/dmsBaseDevicePurchase';
import { getDmsBaseDeviceInstallList, getInstallCount } from '@/api/dms/dmsBaseDeviceInstall';
import { getDmsBaseDeviceDebuggingList, getDebuggingCount } from '@/api/dms/dmsBaseDeviceDebugging';
import { getDmsBillsFaultInstanceList, getFaultInstanceCount } from '@/api/dms/dmsBillsFaultInstance';
import { getDmsBillsMaintDetailList, getMaintInstancesByMachineId, countMaintInstancesByMachineId } from '@/api/dms/dmsBillsMaintDetail';
import { getDmsInspectInstanceDetailList, getInspectInstancesByMachineId, countInspectInstancesByMachineId } from '@/api/dms/dmsInspectInstanceDetail';
import { getBaseAlarmInfoList, getAlarmInfoCount } from '@/api/dms/baseAlarmInfo';
import { getDmsBaseSpecialdeviceParamList } from '@/api/dms/dmsBaseSpecialdeviceParam';
import { getWorkshopList } from '@/api/mes/baseWorkshopInfo';
import { getDmsDeviceModeList } from '@/api/dms/deviceMode';
import { ProdBaseMachineInfoVO, ProdBaseMachineInfoForm } from '@/api/dms/dmsBaseMachineInfo/types';
import imageUpload from '@/components/ImageUpload/index.vue';
import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request';
// 图标导入
import {
Monitor, Clock, Grid, List, Connection, Filter, ArrowUp, ArrowDown,
Location, Memo, Timer, InfoFilled, Calendar, DataAnalysis, TrendCharts,
Download, Search, Refresh, Document, Files, Box, Setting,
Tools, Notification, TurnOff, Warning, Money, Switch, View,
ShoppingCart, CircleCheck, Position, MagicStick, Plus, Edit, Delete
} from '@element-plus/icons-vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 设备台账页面实际使用到的核心字典集合
const {
machine_status,
debug_status,
fault_status,
maint_level,
maint_status,
handle_status,
bills_status,
active_flag,
machine_data_type,
machine_data_encoding,
mes_class_type,
mes_instock_type
} = toRefs<any>(proxy?.useDict(
'machine_status',
'debug_status',
'fault_status',
'maint_level',
'maint_status',
'handle_status',
'bills_status',
'active_flag',
'machine_data_type',
'machine_data_encoding',
'mes_class_type',
'mes_instock_type'
));
// 本页还需要的其它字典(用于详情/统计处标签显示)
const { maint_type, alarm_level } = toRefs<any>(proxy?.useDict('maint_type', 'alarm_level'));
// 响应式数据
const loading = ref(false);
const buttonLoading = ref(false);
const showSearch = ref(true);
const machineList = ref([]);
const total = ref(0);
const viewMode = ref('grid');
const selectedMachine = ref(null);
const lifecycleDrawer = ref(false);
const detailDialog = ref(false);
const activeTab = ref('basic');
const lifecycleLoading = ref(false);
const selectedLifecycleTypes = ref([]);
const lifecycleEvents = ref([]);
// 防抖/竞态控制:仅应用最近一次生命周期数据请求的结果
const lifecycleRequestId = ref(0);
const currentTime = ref('');
const deviceTypes = ref([]);
const workshopInfoList = ref([]);
const deviceModeList = ref([]);
// 表单相关
const queryFormRef = ref<ElFormInstance>();
const machineFormRef = ref<ElFormInstance>();
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
machineCode: undefined,
machineName: undefined,
machineLocation: undefined,
machineStatus: undefined,
machineType: undefined
});
// 表单数据
const initFormData: ProdBaseMachineInfoForm = {
machineId: undefined,
machineCode: undefined,
machineName: undefined,
assetNumber: undefined,
machineLocation: undefined,
machineType: undefined,
machineSpec: undefined,
supplierId: undefined,
machineStatus: '1',
remark: undefined,
photoAddress: undefined,
ossId: undefined,
// workshopId: undefined,
// deviceModeId: undefined,
// machineIp: undefined,
// machinePort: undefined,
// accessProtocol: undefined,
// registerAddress: undefined,
// dataType: undefined,
// dataLength: undefined,
// dataEncoding: undefined,
// requestInterval: undefined,
// classType: undefined,
// instockType: undefined,
file: undefined
};
const form = ref<ProdBaseMachineInfoForm>({ ...initFormData });
// 表单验证规则
const rules = reactive({
machineCode: [
{ required: true, message: '设备编号不能为空', trigger: 'blur' }
],
machineName: [
{ required: true, message: '设备名称不能为空', trigger: 'blur' }
],
machineStatus: [
{ required: true, message: '设备状态不能为空', trigger: 'change' }
]
});
// 生命周期类型配置
const lifecycleTypes = ref([
{
label: '采购信息',
value: 'purchase',
icon: shallowRef(ShoppingCart),
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
count: 0
},
{
label: '安装调试',
value: 'install',
icon: shallowRef(Setting),
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
count: 0
},
{
label: '维保记录',
value: 'maintenance',
icon: shallowRef(Tools),
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
count: 0
},
{
label: '故障维修',
value: 'fault',
icon: shallowRef(Warning),
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
count: 0
},
{
label: '巡检记录',
value: 'inspection',
icon: shallowRef(View),
gradient: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)',
count: 0
},
{
label: '报警信息',
value: 'alarm',
icon: shallowRef(Notification),
gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
count: 0
}
]);
// 生命周期统计
const lifecycleStats = ref({
faultCount: 0,
maintCount: 0,
alarmCount: 0
});
// 技术参数
const technicalParams = ref([]);
// 维护统计
const maintenanceStats = ref({
planned: 0,
temporary: 0,
avgInterval: 0,
nextDate: null
});
// 维护记录
const maintenanceRecords = ref([]);
// 检查文件后缀是否为图片
const checkFileSuffix = (fileSuffix: string | string[]) => {
if (!fileSuffix) return false;
const arr = ['.png', '.jpg', '.jpeg', '.gif'];
const suffix = typeof fileSuffix === 'string' ? fileSuffix.toLowerCase() : '';
return arr.some(item => suffix.endsWith(item));
};
// 更新时间
const updateCurrentTime = () => {
const now = new Date();
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
};
// 获取设备列表
const getList = async () => {
loading.value = true;
try {
const res = await listDmsBaseMachineInfo(queryParams.value);
machineList.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取设备列表失败:', error);
proxy?.$modal.msgError('获取设备列表失败');
machineList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 获取设备类型列表
const getDeviceTypes = async () => {
try {
const res = await getBaseDeviceTypeList(null);
deviceTypes.value = res.data;
} catch (error) {
console.error('获取设备类型失败:', error);
proxy?.$modal.msgError('获取设备类型失败');
}
};
// 获取车间列表
// const getWorkshopListSelect = async () => {
// try {
// const res = await getWorkshopList(null);
// workshopInfoList.value = res.data;
// } catch (error) {
// console.error('获取车间列表失败:', error);
// proxy?.$modal.msgError('获取车间列表失败');
// }
// };
// 获取设备模型列表
const getDmsDeviceModeListSelect = async () => {
try {
const res = await getDmsDeviceModeList(null);
deviceModeList.value = res.data;
} catch (error) {
console.error('获取设备模型列表失败:', error);
proxy?.$modal.msgError('获取设备模型列表失败');
}
};
// 获取设备图标
const getMachineIcon = (type) => {
const iconMap = {
'1': markRaw(Monitor),
'2': markRaw(Setting),
'3': markRaw(Tools),
'4': markRaw(Tools)
};
return iconMap[type] || markRaw(Monitor);
};
// 获取设备类型名称
const getDeviceTypeName = (typeId) => {
const type = deviceTypes.value.find(t => t.deviceTypeId === typeId);
return type?.deviceTypeName || '-';
};
// 获取保养级别标签
const getMaintLevelLabel = (level) => {
const dict = maint_level?.value?.find(d => d.value === level);
return dict?.label || level;
};
// 获取故障状态标签
const getFaultStatusLabel = (status) => {
const dict = bills_status?.value?.find(d => d.value === status);
return dict?.label || status;
};
// 表格行样式
const tableRowClassName = ({ row }) => {
if (row.machineStatus === '0') return 'danger-row';
if (row.machineStatus === '2') return 'warning-row';
return '';
};
// 格式化日期
const formatDate = (date) => {
if (!date) return null;
return proxy.parseTime(date, '{y}-{m}-{d}');
};
// 格式化日期时间
const formatDateTime = (date) => {
if (!date) return null;
return proxy.parseTime(date, '{y}-{m}-{d} {h}:{i}');
};
// 计算运行天数
const calculateRunDays = (startDate) => {
if (!startDate) return 0;
const start = new Date(startDate);
const now = new Date();
const diff = now.getTime() - start.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
};
// 搜索
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
// 重置
const resetQuery = () => {
(proxy as any)?.resetForm?.("queryFormRef");
handleQuery();
};
// 选择设备
const selectMachine = (machine) => {
selectedMachine.value = machine;
};
// 多选框选中数据
const handleSelectionChange = (selection: ProdBaseMachineInfoVO[]) => {
ids.value = selection.map(item => item.machineId);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
// 新增按钮操作
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加设备信息';
};
// 修改按钮操作
const handleUpdate = async (row?: ProdBaseMachineInfoVO) => {
reset();
const _machineId = row?.machineId || ids.value[0];
try {
const res = await getDmsBaseMachineInfo(_machineId);
Object.assign(form.value, res.data);
// 处理图片数据 - 只取第一张图片
if (res.data.ossId && res.data.photoAddress) {
const ossIdArray = res.data.ossId.split(',');
const urlArray = res.data.photoAddress.split(',');
if (ossIdArray.length > 0 && urlArray.length > 0) {
form.value.file = [{
ossId: ossIdArray[0].trim(),
url: urlArray[0].trim()
}];
}
}
dialog.visible = true;
dialog.title = '修改设备信息';
} catch (error) {
console.error('获取设备信息失败:', error);
proxy?.$modal.msgError('获取设备信息失败');
}
};
// 提交按钮
const submitForm = () => {
machineFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
// 处理图片上传 - 只处理第一张图片
if (form.value.file && form.value.file.length > 0) {
form.value.ossId = form.value.file[0].ossId;
form.value.photoAddress = form.value.file[0].url;
} else {
form.value.ossId = undefined;
form.value.photoAddress = undefined;
}
if (form.value.machineId) {
await updateProdBaseMachineInfo(form.value);
} else {
await addProdBaseMachineInfo(form.value);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
} catch (error) {
console.error('保存设备信息失败:', error);
proxy?.$modal.msgError('保存设备信息失败');
} finally {
buttonLoading.value = false;
}
}
});
};
// 删除按钮操作
const handleDelete = async (row?: ProdBaseMachineInfoVO) => {
const _machineIds = row?.machineId || ids.value;
try {
await proxy?.$modal.confirm('是否确认删除设备信息编号为"' + _machineIds + '"的数据项?');
await delDmsBaseMachineInfo(_machineIds);
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除设备信息失败:', error);
if (error !== 'cancel') {
proxy?.$modal.msgError('删除设备信息失败');
}
}
};
// 导出按钮操作
const handleExport = () => {
proxy?.download('dms/dmsBaseMachineInfo/export', {
...queryParams.value
}, `machineInfo_${new Date().getTime()}.xlsx`);
};
// 取消按钮
const cancel = () => {
reset();
dialog.visible = false;
};
// 表单重置
const reset = () => {
form.value = { ...initFormData };
machineFormRef.value?.resetFields();
};
// 切换生命周期类型
const toggleLifecycleType = (type) => {
const index = selectedLifecycleTypes.value.indexOf(type);
if (index > -1) {
selectedLifecycleTypes.value.splice(index, 1);
} else {
selectedLifecycleTypes.value.push(type);
}
};
// 查看生命周期
const viewLifecycle = async (machine) => {
selectedMachine.value = machine;
lifecycleDrawer.value = true;
selectedLifecycleTypes.value = [];
lifecycleEvents.value = [];
// 预加载统计数据
await loadLifecycleTypes();
};
// 加载生命周期数据
const loadLifecycleData = async () => {
if (!selectedMachine.value || selectedLifecycleTypes.value.length === 0) {
lifecycleEvents.value = [];
return;
}
// 标记本次请求序列,后续仅应用最后一次请求的结果
const reqId = ++lifecycleRequestId.value;
lifecycleLoading.value = true;
const events = [];
const machineId = selectedMachine.value.machineId;
// 兼容不同接口返回结构data 或 rows
const getListData = (res) => (res && (res.data ?? res.rows)) || [];
try {
const promises = selectedLifecycleTypes.value.map(async (type) => {
switch (type) {
case 'purchase':
try {
const purchaseRes = await getDmsBaseDevicePurchaseList({ machineId:machineId });
const purchases = getListData(purchaseRes);
if (purchases.length > 0) {
purchases.forEach(purchase => {
events.push({
id: `purchase-${purchase.devicePurchaseId}`,
type: 'purchase',
title: '设备采购',
description: `采购人: ${purchase.purchasePerson || '-'}`,
time: purchase.purchaseTime,
icon: markRaw(ShoppingCart),
color: '#667eea',
tags: ['采购'],
details: [
{ label: '计划编号', value: purchase.workOrder },
{ label: '采购地点', value: purchase.purchasePosition },
{ label: '采购价格', value: purchase.purchasePrice || '-' },
{ label: '备注', value: purchase.remark || '-' }
]
});
});
}
} catch (error) {
console.error('获取采购信息失败:', error);
proxy?.$modal.msgError('获取采购信息失败');
}
break;
case 'install':
try {
const installRes = await getDmsBaseDeviceInstallList({ machineId:machineId });
const installs = getListData(installRes);
if (installs.length > 0) {
installs.forEach(install => {
events.push({
id: `install-${install.deviceInstallId}`,
type: 'install',
title: '设备安装',
description: `安装人员: ${install.installPerson || '-'}`,
time: install.installTime,
icon: markRaw(Setting),
color: '#f093fb',
tags: ['安装'],
details: [
{ label: '安装地点', value: install.installPosition },
{ label: '状态', value: install.activeFlag, dictOptions: active_flag?.value }
]
});
});
}
const debugRes = await getDmsBaseDeviceDebuggingList({ machineId:machineId });
const debugs = getListData(debugRes);
if (debugs.length > 0) {
debugs.forEach(debug => {
events.push({
id: `debug-${debug.deviceDebuggingId}`,
type: 'debugging',
title: '设备调试',
description: `调试人员: ${debug.debugPerson || '-'}`,
time: debug.debugTime,
icon: markRaw(Tools),
color: '#f5576c',
tags: ['调试'],
details: [
{ label: '调试单号', value: debug.workOrder },
{ label: '调试状态', value: debug.status, dictOptions: debug_status?.value },
{ label: '备注', value: debug.remark || '-' }
]
});
});
}
} catch (error) {
console.error('获取安装调试信息失败:', error);
proxy?.$modal.msgError('获取安装调试信息失败');
}
break;
case 'maintenance':
try {
// 使用新的后端接口,直接获取维保工单列表
const res = await getMaintInstancesByMachineId(machineId);
const maintInstances = getListData(res);
maintInstances.forEach(maint => {
events.push({
id: `maint-${maint.maintInstanceId}`,
type: 'maintenance',
title: '保养记录',
description: `保养单号: ${maint.billsMaintCode}`,
time: maint.planBeginTime || maint.createTime,
icon: markRaw(Tools),
color: '#4facfe',
tags: ['保养', getMaintLevelLabel(maint.maintLevel)],
details: [
{ label: '保养单号', value: maint.billsMaintCode },
{ label: '保养级别', value: maint.maintLevel, dictOptions: maint_level?.value },
{ label: '保养状态', value: maint.maintStatus, dictOptions: maint_status?.value },
{ label: '计划开始时间', value: formatDateTime(maint.planBeginTime) },
// { label: '计划结束时间', value: formatDateTime(maint.planEndTime) }
]
});
});
} catch (error) {
console.error('获取保养记录失败:', error);
proxy?.$modal.msgError('获取保养记录失败');
}
break;
case 'fault':
try {
const res = await getDmsBillsFaultInstanceList({ machineId:machineId });
const faults = getListData(res);
if (faults.length > 0) {
faults.forEach(fault => {
events.push({
id: `fault-${fault.repairInstanceId}`,
type: 'fault',
title: '故障报修',
description: `故障单号: ${fault.billsFaultCode}`,
time: fault.applyTime,
icon: markRaw(Warning),
color: '#fa709a',
tags: ['故障', getFaultStatusLabel(fault.billsStatus)],
details: [
{ label: '申请人', value: fault.applyUser },
{ label: '工单状态', value: fault.billsStatus, dictOptions: bills_status?.value },
{ label: '故障描述', value: fault.faultDescription || '-' },
{ label: '备注', value: fault.remark || '-' }
]
});
});
}
} catch (error) {
console.error('获取故障记录失败:', error);
proxy?.$modal.msgError('获取故障记录失败');
}
break;
case 'inspection':
try {
// 使用新的后端接口,直接获取巡检工单列表
const res = await getInspectInstancesByMachineId(machineId);
const inspectInstances = getListData(res);
inspectInstances.forEach(inspect => {
events.push({
id: `inspect-${inspect.inspectInstanceId}`,
type: 'inspection',
title: '巡检记录',
description: `巡检单号: ${inspect.billsInspectCode}`,
time: inspect.planBeginTime || inspect.createTime,
icon: markRaw(View),
color: '#30cfd0',
tags: ['巡检'],
details: [
{ label: '巡检单号', value: inspect.billsInspectCode },
{ label: '巡检类型', value: inspect.inspectType === '1' ? '巡检' : '点检' },
{ label: '巡检状态', value: inspect.inspectStatus || '-' },
{ label: '计划开始时间', value: formatDateTime(inspect.planBeginTime) },
// { label: '计划结束时间', value: formatDateTime(inspect.planEndTime) }
]
});
});
} catch (error) {
console.error('获取巡检记录失败:', error);
proxy?.$modal.msgError('获取巡检记录失败');
}
break;
case 'alarm':
try {
const res = await getBaseAlarmInfoList({ machineId:machineId });
const alarms = getListData(res);
if (alarms.length > 0) {
alarms.slice(0, 10).forEach(alarm => {
events.push({
id: `alarm-${alarm.alarmId}`,
type: 'alarm',
title: '设备报警',
description: `报警类型: ${alarm.alarmTypeName || '-'}`,
time: alarm.alarmTime,
icon: markRaw(Notification),
color: '#a8edea',
tags: ['报警', alarm.alarmLevelName],
details: [
{ label: '报警级别', value: alarm.alarmLevel ?? alarm.alarmLevelName, dictOptions: alarm_level?.value },
{ label: '报警内容', value: alarm.alarmContent },
{ label: '处理状态', value: alarm.handleStatus, dictOptions: handle_status?.value }
]
});
});
}
} catch (error) {
console.error('获取报警信息失败:', error);
proxy?.$modal.msgError('获取报警信息失败');
}
break;
}
});
await Promise.all(promises);
// 仅当本次请求仍是最新请求时,应用结果
if (reqId === lifecycleRequestId.value) {
lifecycleEvents.value = events.sort((a, b) => {
const timeA = new Date(a.time).getTime();
const timeB = new Date(b.time).getTime();
return timeB - timeA;
});
lifecycleLoading.value = false;
}
} catch (e) {
if (reqId === lifecycleRequestId.value) {
lifecycleLoading.value = false;
}
proxy?.$modal.msgError('加载生命周期数据失败');
}
};
// 查看详情
const viewDetails = async (machine) => {
selectedMachine.value = machine;
detailDialog.value = true;
activeTab.value = 'basic';
// 加载详情数据
await Promise.all([
loadTechnicalParams(machine.machineId),
loadMaintenanceStats(machine.machineId)
]);
};
// 加载技术参数
const loadTechnicalParams = async (machineId) => {
try {
const paramRes = await getDmsBaseSpecialdeviceParamList({ machineId });
technicalParams.value = paramRes.data?.map(param => ({
paramName: param.paramName,
paramValue: param.paramValue,
unit: param.paramUnit || '-',
remark: param.remark || '-'
})) || [];
} catch (error) {
console.error('获取技术参数失败:', error);
technicalParams.value = [];
proxy?.$modal.msgError('获取技术参数失败');
}
};
// 加载维护统计
const loadMaintenanceStats = async (machineId) => {
try {
const statsRes = await getDmsDeviceMaintenanceStats({ machineId });
const statsData = statsRes.data || {};
maintenanceStats.value = {
planned: statsData.plannedCount || 0,
temporary: statsData.temporaryCount || 0,
avgInterval: statsData.avgInterval || 0,
nextDate: statsData.nextMaintenanceDate ? formatDate(statsData.nextMaintenanceDate) : null
};
// 修复:通过维保明细表查询维护记录
const maintDetailRes = await getDmsBillsMaintDetailList({ machineId, pageNum: 1, pageSize: 10 });
const maintDetails = (maintDetailRes.data || maintDetailRes.rows || []);
// 从维保明细中提取维护记录,按工单去重处理
const maintInstanceMap = new Map();
maintDetails.forEach(detail => {
const maintInstanceId = detail.maintInstanceId;
if (!maintInstanceMap.has(maintInstanceId)) {
maintInstanceMap.set(maintInstanceId, {
maintCode: detail.billsMaintCode || `MAINT-${detail.maintInstanceId}`,
maintType: detail.maintLevel || 1, // 保养级别对应保养类型
maintDate: formatDate(detail.beginTime || detail.createTime),
maintPerson: detail.maintSupervisor || '-',
maintContent: detail.operationDescription || '-',
maintStatus: detail.maintStatus || 1
});
}
});
maintenanceRecords.value = Array.from(maintInstanceMap.values());
} catch (error) {
console.error('获取维护统计失败:', error);
maintenanceStats.value = { planned: 0, temporary: 0, avgInterval: 0, nextDate: null };
maintenanceRecords.value = [];
proxy?.$modal.msgError('获取维护统计失败');
}
};
// 加载生命周期类型统计
const loadLifecycleTypes = async () => {
if (!selectedMachine.value) return;
const machineId = selectedMachine.value.machineId;
try {
const [
purchaseCount,
installCount,
debuggingCount,
maintCount, // 使用新的后端接口
faultCount,
inspectCount, // 使用新的后端接口
alarmCount
] = await Promise.all([
getPurchaseCount({ machineId }).catch(() => ({ data: 0 })),
getInstallCount({ machineId }).catch(() => ({ data: 0 })),
getDebuggingCount({ machineId }).catch(() => ({ data: 0 })),
countMaintInstancesByMachineId(machineId).catch(() => ({ data: 0 })),
getFaultInstanceCount({ machineId }).catch(() => ({ data: 0 })),
countInspectInstancesByMachineId(machineId).catch(() => ({ data: 0 })),
getAlarmInfoCount({ machineId }).catch(() => ({ data: 0 }))
]);
// 更新类型计数
lifecycleTypes.value.forEach(type => {
switch (type.value) {
case 'purchase':
type.count = purchaseCount.data || 0;
break;
case 'install':
type.count = (installCount.data || 0) + (debuggingCount.data || 0);
break;
case 'maintenance':
type.count = maintCount.data || 0;
break;
case 'fault':
type.count = faultCount.data || 0;
break;
case 'inspection':
type.count = inspectCount.data || 0;
break;
case 'alarm':
type.count = alarmCount.data || 0;
break;
default:
type.count = 0;
}
});
} catch (error) {
console.error('加载生命周期类型统计失败:', error);
proxy?.$modal.msgError('加载生命周期类型统计失败');
}
};
// 定时更新
let timer = null;
onMounted(() => {
getList();
getDeviceTypes();
// getWorkshopListSelect();
getDmsDeviceModeListSelect();
updateCurrentTime();
timer = setInterval(updateCurrentTime, 1000);
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
}
});
// 监听生命周期类型变化,自动加载数据
watch(selectedLifecycleTypes, (newTypes) => {
if (newTypes.length > 0 && selectedMachine.value) {
loadLifecycleData();
} else {
// 作废在途请求,防止已发起的请求在清空选择后回填数据
lifecycleRequestId.value++;
lifecycleEvents.value = [];
lifecycleLoading.value = false;
}
}, { deep: true });
// 监听选中设备变化,重置生命周期选择和数据
watch(selectedMachine, (newMachine) => {
if (newMachine) {
selectedLifecycleTypes.value = [];
lifecycleEvents.value = [];
loadLifecycleTypes();
}
});
</script>
<style lang="scss" scoped>
.dms-machine-ledger {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
padding: 6px;
}
.factory-bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60px;
z-index: 0;
svg {
width: 100%;
height: 100%;
}
}
.top-info-panel {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
margin-bottom: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.panel-left {
.page-title {
font-size: 18px;
font-weight: 600;
color: #2c3e50;
margin: 0;
display: flex;
align-items: center;
gap: 6px;
}
.page-subtitle {
font-size: 11px;
color: #7f8c8d;
margin: 1px 0 0 0;
}
}
.panel-right {
.current-time {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: #34495e;
font-weight: 500;
}
}
}
.action-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.toolbar-left, .toolbar-right {
display: flex;
gap: 6px;
}
}
.search-filter-card {
margin-bottom: 8px;
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
.filter-title {
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
}
}
:deep(.el-card__body) {
padding: 12px;
}
.el-form {
.el-form-item {
margin-bottom: 8px;
}
}
}
.main-content {
.grid-view {
.machine-grid {
display: grid;
grid-template-columns: repeat(5, 1fr); // 修改为一行5个
gap: 8px;
margin-bottom: 16px;
}
.machine-card-wrapper {
cursor: pointer;
.machine-card {
background: #fff;
border-radius: 4px;
padding: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
&.selected {
border: 2px solid #409EFF;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
.status-indicator {
position: absolute;
top: 6px;
right: 6px;
.status-dot {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
animation: pulse 2s infinite;
}
&.status-0 .status-dot { background: #f56c6c; }
&.status-1 .status-dot { background: #67c23a; }
&.status-2 .status-dot { background: #e6a23c; }
&.status-3 .status-dot { background: #909399; }
}
.machine-visual {
text-align: center;
margin-bottom: 6px;
.machine-icon-bg {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: linear-gradient(45deg, #667eea, #764ba2);
border-radius: 6px;
color: #fff;
margin-bottom: 4px;
}
.machine-photo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-bottom: 4px;
border-radius: 6px;
overflow: hidden;
}
.machine-number {
font-size: 11px;
font-weight: 600;
color: #2c3e50;
}
}
.machine-details {
margin-bottom: 6px;
.machine-name {
font-size: 13px;
font-weight: 600;
color: #2c3e50;
margin: 0 0 4px 0;
text-align: center;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.machine-meta {
.meta-item {
display: flex;
align-items: center;
gap: 2px;
font-size: 10px;
color: #7f8c8d;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:last-child {
margin-bottom: 0;
}
}
}
}
.quick-actions {
display: flex;
gap: 4px;
justify-content: center;
.el-button {
padding: 2px 6px;
font-size: 10px;
min-height: 20px;
}
}
}
}
}
.list-view {
.status-cell {
display: flex;
align-items: center;
gap: 4px;
.status-badge {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
&.status-0 { background: #f56c6c; }
&.status-1 { background: #67c23a; }
&.status-2 { background: #e6a23c; }
&.status-3 { background: #909399; }
}
}
.date-text {
font-size: 12px;
color: #606266;
}
:deep(.danger-row) {
background-color: rgba(245, 108, 108, 0.05);
}
:deep(.warning-row) {
background-color: rgba(230, 162, 60, 0.05);
}
}
}
.lifecycle-drawer {
:deep(.el-drawer__body) {
padding: 12px;
}
}
.lifecycle-container {
.device-overview-card {
background: #fff;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
.overview-header {
display: flex;
align-items: center;
gap: 10px;
.device-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
background: linear-gradient(45deg, #667eea, #764ba2);
border-radius: 10px;
color: #fff;
}
.device-basic {
h2 {
margin: 0 0 3px 0;
font-size: 16px;
color: #2c3e50;
}
p {
margin: 0;
font-size: 12px;
color: #7f8c8d;
}
}
}
.overview-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 10px;
.stat {
text-align: center;
.stat-label {
display: block;
font-size: 11px;
color: #7f8c8d;
margin-bottom: 3px;
}
.stat-value {
display: block;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
}
}
}
.lifecycle-type-selector {
margin-bottom: 12px;
h3 {
margin: 0 0 10px 0;
font-size: 15px;
color: #2c3e50;
}
.type-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 6px;
.type-card {
background: #fff;
border: 2px solid #e1e6f0;
border-radius: 6px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #409EFF;
}
&.active {
border-color: #409EFF;
background: #f0f9ff;
}
.type-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 6px;
margin-bottom: 5px;
color: #fff;
}
.type-name {
display: block;
font-size: 11px;
color: #2c3e50;
font-weight: 500;
}
.type-count {
position: absolute;
top: -3px;
right: -3px;
background: #409EFF;
color: #fff;
border-radius: 8px;
padding: 1px 5px;
font-size: 9px;
min-width: 14px;
text-align: center;
}
}
}
}
.lifecycle-timeline-container {
h3 {
margin: 0 0 10px 0;
font-size: 15px;
color: #2c3e50;
}
.timeline-wrapper {
position: relative;
.timeline-item {
position: relative;
padding-left: 35px;
padding-bottom: 16px;
&:last-child {
.timeline-line {
display: none;
}
}
.timeline-marker {
position: absolute;
left: 0;
top: 0;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
z-index: 2;
}
.timeline-content {
background: #fff;
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
h4 {
margin: 0;
font-size: 13px;
color: #2c3e50;
}
.event-time {
font-size: 11px;
color: #7f8c8d;
}
}
.event-description {
font-size: 12px;
color: #606266;
margin: 0 0 6px 0;
}
.event-tags {
margin-bottom: 6px;
.el-tag {
margin-right: 3px;
margin-bottom: 3px;
}
}
}
.timeline-line {
position: absolute;
left: 13px;
top: 28px;
width: 2px;
height: 100%;
background: #e1e6f0;
z-index: 1;
}
}
}
}
}
.detail-section {
.card-header-title {
font-weight: 600;
color: #2c3e50;
}
.maintenance-section {
.maintenance-summary {
margin-bottom: 12px;
}
}
}
.filter-btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
border: none;
&:hover {
background: linear-gradient(45deg, #5a6fd8, #6a4190);
}
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 currentColor;
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px transparent;
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 transparent;
}
}
.fade-transform-enter-active,
.fade-transform-leave-active {
transition: all 0.3s ease;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateY(20px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>