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.

1339 lines
42 KiB
Vue

<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">
<i class="el-icon-monitor"></i>
设备台账管理
</h1>
<p class="page-subtitle">设备全生命周期管理与履历追踪</p>
</div>
<div class="panel-right">
<div class="current-time">
<i class="el-icon-time"></i>
{{ 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'">
<i class="el-icon-grid"></i>
网格视图
</el-button>
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
<i class="el-icon-menu"></i>
列表视图
</el-button>
</el-button-group>
</div>
<div class="toolbar-right">
<!-- <el-button type="primary" icon="el-icon-plus" @click="handleAdd" v-hasPermi="['dms:dmsBaseMachineInfo:add']">
新增设备
</el-button>
<el-button type="success" icon="el-icon-edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['dms:dmsBaseMachineInfo:edit']">
修改设备
</el-button>
<el-button type="danger" icon="el-icon-delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['dms:dmsBaseMachineInfo:remove']">
删除设备
</el-button>-->
<el-button text @click="showSearch = !showSearch">
<i class="el-icon-filter"></i>
{{ showSearch ? '隐藏' : '显示' }}筛选
</el-button>
</div>
</div>
<!-- 查询过滤器 -->
<el-card class="search-filter-card" v-show="showSearch">
<div slot="header" class="filter-header">
<div class="filter-title">
<i class="el-icon-filter"></i>
筛选条件
</div>
<el-button text @click="showSearch = false">
<i class="el-icon-arrow-up"></i>
</el-button>
</div>
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item label="设备编号" prop="deviceCode">
<el-input v-model="queryParams.deviceCode" placeholder="请输入设备编号" clearable />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<el-button icon="el-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.objId" class="machine-card-wrapper" @click="selectMachine(machine)">
<div :class="['machine-card', { selected: selectedMachine && selectedMachine.objId === machine.objId }]">
<div :class="['status-indicator', `status-${machine.deviceStatus}`]">
<span class="status-dot"></span>
</div>
<div class="machine-visual">
<div class="machine-icon-bg">
<i class="el-icon-monitor" style="font-size: 40px;"></i>
</div>
<div class="machine-number">{{ machine.deviceCode }}</div>
</div>
<div class="machine-details">
<h3 class="machine-name">{{ machine.deviceName }}</h3>
<div class="machine-meta">
<div class="meta-item">
<i class="el-icon-location"></i>
<span>{{ machine.productLineName || '-' }}</span>
</div>
<div class="meta-item">
<i class="el-icon-document"></i>
<span>{{ getDeviceTypeName(machine.deviceType) }}</span>
</div>
<div class="meta-item">
<i class="el-icon-time"></i>
<span>建立 {{ calculateRunDays(machine.createdTime) }} 天</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" :page.sync="queryParams.pageNum" :limit.sync="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" style="width: 100%">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备编号" align="center" prop="deviceCode" min-width="120" />
<el-table-column label="设备名称" align="center" prop="deviceName" min-width="150" />
<el-table-column label="设备类型" align="center" prop="deviceTypeName" min-width="120" />
<el-table-column label="所属产线" align="center" prop="productLineName" min-width="130" />
<el-table-column label="设备状态" align="center" prop="deviceStatus" width="120">
<template slot-scope="scope">
<dict-tag :options="machine_status" :value="String(scope.row.deviceStatus)" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createdTime" min-width="160">
<template slot-scope="scope">
<span>{{ formatDate(scope.row.createdTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="320" fixed="right">
<template slot-scope="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" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</div>
<!-- 添加或修改设备信息对话框 -->
<el-dialog :title="dialog.title" :visible.sync="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="deviceCode">
<el-input v-model="form.deviceCode" placeholder="请输入设备编号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="form.deviceType" placeholder="请选择设备类型" style="width: 100%;">
<el-option v-for="item in deviceTypes" :key="item.deviceTypeId" :label="item.deviceTypeName" :value="String(item.deviceTypeId)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备型号" prop="deviceModel">
<el-input v-model="form.deviceModel" placeholder="请输入设备型号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="form.supplierId" placeholder="请选择供应商" style="width: 100%;">
<el-option v-for="item in supplierInfoList" :key="item.supplierId" :label="item.supplierName" :value="String(item.supplierId)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设备状态" prop="deviceStatus">
<el-select v-model="form.deviceStatus" 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="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>
<span slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</span>
</el-dialog>
<!-- 生命周期抽屉 -->
<el-drawer :visible.sync="lifecycleDrawer" :title="`设备生命周期 - ${selectedMachine ? selectedMachine.deviceName : ''}`" 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">
<i class="el-icon-monitor" style="font-size: 40px;"></i>
</div>
<div class="device-basic">
<h2>{{ selectedMachine.deviceName }}</h2>
<p>编号:{{ selectedMachine.deviceCode }} | 产线:{{ selectedMachine.productLineName || '-' }}</p>
</div>
</div>
<el-divider />
<div class="overview-stats">
<div class="stat">
<span class="stat-label">建立天数</span>
<span class="stat-value">{{ calculateRunDays(selectedMachine.createdTime) }}</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.indexOf(type.value) > -1 }]"
@click="toggleLifecycleType(type.value)">
<div class="type-icon" :style="{ background: type.gradient }">
<i :class="type.iconClass" style="font-size: 24px;"></i>
</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 }">
<i :class="event.iconClass" style="font-size: 20px; color: #fff;"></i>
</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>
<div class="timeline-line"></div>
</div>
</div>
</div>
<!-- 空状态 -->
<el-empty v-else-if="selectedLifecycleTypes.length > 0" description="暂无相关生命周期数据" />
</div>
</el-drawer>
<!-- 设备详情对话框 -->
<el-dialog :visible.sync="detailDialog" :title="`设备详情 - ${selectedMachine ? selectedMachine.deviceName : ''}`" 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>
<div slot="header" class="card-header-title">
<span>设备基础信息</span>
</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="设备编号">{{ selectedMachine ? selectedMachine.deviceCode : '' }}</el-descriptions-item>
<el-descriptions-item label="设备名称">{{ selectedMachine ? selectedMachine.deviceName : '' }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ selectedMachine ? selectedMachine.deviceTypeName : '' }}</el-descriptions-item>
<el-descriptions-item label="所属产线">{{ selectedMachine ? (selectedMachine.productLineName || '-') : '' }}</el-descriptions-item>
<el-descriptions-item label="设备状态">
<dict-tag :options="machine_status" :value="String(selectedMachine ? (selectedMachine.deviceStatus || '') : '')" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ selectedMachine ? formatDateTime(selectedMachine.createdTime) : '' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script>
import { listDeviceLedger } from '@/api/base/deviceLedger';
import { listAllDmsBillsMaintInstance } from '@/api/dms/dmsBillsMaintInstance';
import { listAllDmsRecordInspect } from '@/api/dms/dmsRecordInspect';
import { listAllShutDown } from '@/api/dms/shutDown';
import { listAllDmsBillsFaultInstance } from '@/api/dms/dmsBillsFaultInstance';
import { listAllRepairRecord } from '@/api/dms/repairRecord';
import pagination from '@/components/Pagination';
export default {
name: 'ProdBaseMachineInfo',
components: {
pagination
},
data() {
return {
loading: false,
buttonLoading: false,
showSearch: true,
machineList: [],
total: 0,
viewMode: 'grid',
selectedMachine: null,
detailDialog: false,
activeTab: 'basic',
currentTime: '',
timer: null,
deviceTypes: [],
supplierInfoList: [],
ids: [],
single: true,
multiple: true,
// 生命周期相关
lifecycleDrawer: false,
lifecycleLoading: false,
selectedLifecycleTypes: [],
lifecycleEvents: [],
lifecycleRequestId: 0,
lifecycleTypes: [
{ label: '保养', value: 'maintenance', iconClass: 'el-icon-setting', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', count: 0 },
{ label: '点检', value: 'inspection', iconClass: 'el-icon-view', gradient: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)', count: 0 },
{ label: '停机', value: 'downtime', iconClass: 'el-icon-warning-outline', gradient: 'linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%)', count: 0 },
{ label: '故障', value: 'fault', iconClass: 'el-icon-circle-close', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', count: 0 },
{ label: '维修', value: 'repair', iconClass: 'el-icon-tools', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', count: 0 }
],
technicalParams: [],
dialog: {
visible: false,
title: ''
},
queryParams: {
pageNum: 1,
pageSize: 10,
deviceCode: undefined,
deviceName: undefined
},
form: {
objId: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceType: undefined,
deviceModel: undefined,
supplierId: undefined,
deviceStatus: '1',
remark: undefined
},
rules: {
deviceCode: [
{ required: true, message: '设备编号不能为空', trigger: 'blur' }
],
deviceName: [
{ required: true, message: '设备名称不能为空', trigger: 'blur' }
],
deviceStatus: [
{ required: true, message: '设备状态不能为空', trigger: 'change' }
]
},
machine_status: [
{ label: '正常', value: '1' },
{ label: '维修中', value: '2' },
{ label: '停用', value: '0' }
]
};
},
watch: {
selectedLifecycleTypes: {
handler(newTypes) {
if (newTypes.length > 0 && this.selectedMachine) {
this.loadLifecycleData();
} else {
this.lifecycleRequestId++;
this.lifecycleEvents = [];
this.lifecycleLoading = false;
}
},
deep: true
},
selectedMachine(newMachine) {
if (newMachine) {
this.selectedLifecycleTypes = [];
this.lifecycleEvents = [];
this.loadLifecycleTypes();
}
}
},
mounted() {
this.getList();
this.getDeviceTypes();
this.getSupplierInfoList();
this.updateCurrentTime();
this.timer = setInterval(this.updateCurrentTime, 1000);
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
updateCurrentTime() {
const now = new Date();
this.currentTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
},
async getList() {
this.loading = true;
try {
const res = await listDeviceLedger(this.queryParams);
this.machineList = res.rows || [];
this.total = res.total || 0;
} catch (error) {
console.error('获取设备列表失败:', error);
this.$modal.msgError('获取设备列表失败');
this.machineList = [];
this.total = 0;
} finally {
this.loading = false;
}
},
async getDeviceTypes() {
try {
// 使用假数据
this.deviceTypes = [
{ deviceTypeId: '1', deviceTypeName: '数控机床' },
{ deviceTypeId: '2', deviceTypeName: '冲床' },
{ deviceTypeId: '3', deviceTypeName: '焊接机' },
{ deviceTypeId: '4', deviceTypeName: '其他' }
];
} catch (error) {
console.error('获取设备类型失败:', error);
}
},
async getSupplierInfoList() {
try {
// 使用假数据
this.supplierInfoList = [
{ supplierId: '1', supplierName: '供应商A' },
{ supplierId: '2', supplierName: '供应商B' },
{ supplierId: '3', supplierName: '供应商C' }
];
} catch (error) {
console.error('获取供应商列表失败:', error);
}
},
getDeviceTypeName(typeId) {
const type = this.deviceTypes.find(t => String(t.deviceTypeId) === String(typeId));
return type ? type.deviceTypeName : '-';
},
tableRowClassName({ row }) {
if (row.deviceStatus === '0') return 'danger-row';
if (row.deviceStatus === '2') return 'warning-row';
return '';
},
formatDate(date) {
if (!date) return null;
return this.parseTime(date, '{y}-{m}-{d}');
},
formatDateTime(date) {
if (!date) return null;
return this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
},
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));
},
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
resetQuery() {
this.$refs.queryFormRef.resetFields();
this.handleQuery();
},
selectMachine(machine) {
this.selectedMachine = machine;
},
handleSelectionChange(selection) {
this.ids = selection.map(item => item.objId);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
handleAdd() {
this.reset();
this.dialog.visible = true;
this.dialog.title = '添加设备信息';
},
async handleUpdate(row) {
this.reset();
const objId = row ? row.objId : this.ids[0];
try {
const form = row || this.machineList.find(m => m.objId === objId);
if (form) {
Object.assign(this.form, form);
this.form.deviceType = form.deviceType ? String(form.deviceType) : undefined;
this.form.supplierId = form.supplierId ? String(form.supplierId) : undefined;
}
this.dialog.visible = true;
this.dialog.title = '修改设备信息';
} catch (error) {
console.error('获取设备信息失败:', error);
this.$modal.msgError('获取设备信息失败');
}
},
submitForm() {
this.$refs.machineFormRef.validate(async (valid) => {
if (valid) {
this.buttonLoading = true;
try {
this.$modal.msgSuccess('操作成功');
this.dialog.visible = false;
await this.getList();
} catch (error) {
console.error('保存设备信息失败:', error);
this.$modal.msgError('保存设备信息失败');
} finally {
this.buttonLoading = false;
}
}
});
},
async handleDelete(row) {
const objIds = row ? row.objId : this.ids;
try {
await this.$modal.confirm('是否确认删除设备信息编号为"' + objIds + '"的数据项?');
this.$modal.msgSuccess('删除成功');
await this.getList();
} catch (error) {
console.error('删除设备信息失败:', error);
if (error !== 'cancel') {
this.$modal.msgError('删除设备信息失败');
}
}
},
cancel() {
this.reset();
this.dialog.visible = false;
},
reset() {
this.form = {
objId: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceType: undefined,
deviceModel: undefined,
supplierId: undefined,
deviceStatus: '1',
remark: undefined
};
this.$refs.machineFormRef && this.$refs.machineFormRef.resetFields();
},
viewDetails(machine) {
this.selectedMachine = machine;
this.detailDialog = true;
this.activeTab = 'basic';
},
// 生命周期相关方法
viewLifecycle(machine) {
this.selectedMachine = machine;
this.lifecycleDrawer = true;
this.selectedLifecycleTypes = [];
this.lifecycleEvents = [];
this.loadLifecycleTypes();
},
toggleLifecycleType(type) {
const index = this.selectedLifecycleTypes.indexOf(type);
if (index > -1) {
this.selectedLifecycleTypes.splice(index, 1);
} else {
this.selectedLifecycleTypes.push(type);
}
},
async loadLifecycleTypes() {
if (!this.selectedMachine) return;
const deviceCode = this.selectedMachine.deviceCode;
try {
// 并行获取所有类型的数据数量
const [maintRes, inspectRes, shutdownRes, faultRes, repairRes] = await Promise.all([
listAllDmsBillsMaintInstance({ deviceCode }).catch(() => ({ data: [] })),
listAllDmsRecordInspect({ deviceCode }).catch(() => ({ data: [] })),
listAllShutDown({ deviceCode }).catch(() => ({ data: [] })),
listAllDmsBillsFaultInstance({ deviceCode }).catch(() => ({ data: [] })),
listAllRepairRecord({ deviceCode }).catch(() => ({ data: [] }))
]);
// 更新各类型数量
this.lifecycleTypes.forEach(type => {
switch (type.value) {
case 'maintenance':
type.count = (maintRes.data || []).length;
break;
case 'inspection':
type.count = (inspectRes.data || []).length;
break;
case 'downtime':
type.count = (shutdownRes.data || []).length;
break;
case 'fault':
type.count = (faultRes.data || []).length;
break;
case 'repair':
type.count = (repairRes.data || []).length;
break;
default:
type.count = 0;
}
});
} catch (error) {
console.error('加载生命周期类型统计失败:', error);
}
},
async loadLifecycleData() {
if (!this.selectedMachine || this.selectedLifecycleTypes.length === 0) {
this.lifecycleEvents = [];
return;
}
const reqId = ++this.lifecycleRequestId;
this.lifecycleLoading = true;
const events = [];
const deviceCode = this.selectedMachine.deviceCode;
try {
// 根据选中的类型加载对应数据
const promises = [];
this.selectedLifecycleTypes.forEach(type => {
switch (type) {
case 'maintenance':
promises.push(
listAllDmsBillsMaintInstance({ deviceCode }).then(res => {
(res.data || []).forEach((item, i) => {
const statusMap = { 1: '待保养', 2: '保养中', 3: '已完成' };
const statusText = statusMap[item.maintStatus] || '未知';
events.push({
id: `maint-${item.maintInstanceId || i}`,
type: 'maintenance',
title: item.maintLevelName || '设备保养',
description: `保养单号: ${item.billsMaintCode || '-'} | 保养人员: ${item.maintSupervisor || '-'} | 状态: ${statusText}`,
time: item.realEndTime || item.realBeginTime || item.planBeginTime || item.createTime,
iconClass: 'el-icon-setting',
color: '#4facfe',
tags: ['保养', statusText]
});
});
}).catch(() => {})
);
break;
case 'inspection':
promises.push(
listAllDmsRecordInspect({ deviceCode }).then(res => {
(res.data || []).forEach((item, i) => {
const statusMap = { 1: '待巡检', 2: '巡检中', 3: '完成' };
const statusText = statusMap[item.inspectStatus] || '未知';
const inspectTypeText = item.inspectType === '1' ? '巡检' : '点检';
events.push({
id: `inspect-${item.recordInspectId || i}`,
type: 'inspection',
title: inspectTypeText,
description: `巡检单号: ${item.billsInspectCode || '-'} | 执行人: ${item.performer || '-'} | 状态: ${statusText}`,
time: item.realEndTime || item.realBeginTime || item.planBeginTime || item.createTime,
iconClass: 'el-icon-view',
color: statusText === '完成' ? '#30cfd0' : '#ff7675',
tags: [inspectTypeText, statusText]
});
});
}).catch(() => {})
);
break;
case 'downtime':
promises.push(
listAllShutDown({ deviceCode }).then(res => {
(res.data || []).forEach((item, i) => {
const shutTimeText = item.shutTime ? `${item.shutTime}小时` : '-';
events.push({
id: `downtime-${item.recordShutDownId || i}`,
type: 'downtime',
title: '设备停机',
description: `原因: ${item.shutReason || '-'} | 时长: ${shutTimeText}`,
time: item.shutBeginTime || item.createTime,
iconClass: 'el-icon-warning-outline',
color: '#ffeaa7',
tags: ['停机', item.shutReason ? (item.shutReason.includes('计划') ? '计划' : '非计划') : '非计划']
});
});
}).catch(() => {})
);
break;
case 'fault':
promises.push(
listAllDmsBillsFaultInstance({ deviceCode }).then(res => {
(res.data || []).forEach((item, i) => {
const statusMap = { '0': '待维修', '1': '维修中', '2': '维修完成', '3': '待检修', '4': '检修中', '5': '检修完成' };
const statusText = statusMap[item.billsStatus] || '未知';
const faultLevelMap = { '1': '一般', '2': '紧急', '3': '严重' };
const levelText = faultLevelMap[item.faultLevel] || '一般';
events.push({
id: `fault-${item.repairInstanceId || i}`,
type: 'fault',
title: item.faultType || '设备故障',
description: `故障单号: ${item.billsFaultCode || '-'} | 等级: ${levelText} | 报修人: ${item.applyUser || '-'} | 状态: ${statusText}`,
time: item.applyTime || item.createTime,
iconClass: 'el-icon-circle-close',
color: '#fa709a',
tags: ['故障', levelText]
});
});
}).catch(() => {})
);
break;
case 'repair':
promises.push(
listAllRepairRecord({ deviceCode }).then(res => {
(res.data || []).forEach((item, i) => {
const resultMap = { '1': '成功', '2': '失败', '3': '部分完成' };
const resultText = resultMap[item.repairResult] || '未知';
events.push({
id: `repair-${item.recordId || i}`,
type: 'repair',
title: item.faultPhenomenon || '设备维修',
description: `工单编号: ${item.workOrderCode || '-'} | 维修人: ${item.repairerName || '-'} | 结果: ${resultText}`,
time: item.endTime || item.startTime || item.createTime,
iconClass: 'el-icon-tools',
color: '#667eea',
tags: ['维修', resultText]
});
});
}).catch(() => {})
);
break;
}
});
await Promise.all(promises);
if (reqId === this.lifecycleRequestId) {
this.lifecycleEvents = events.sort((a, b) => {
const timeA = a.time ? new Date(a.time).getTime() : 0;
const timeB = b.time ? new Date(b.time).getTime() : 0;
return timeB - timeA;
});
this.lifecycleLoading = false;
}
} catch (e) {
if (reqId === this.lifecycleRequestId) {
this.lifecycleLoading = false;
}
console.error('加载生命周期数据失败:', e);
}
}
}
};
</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;
width: 100%;
box-sizing: border-box;
}
.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: 6px;
}
}
.main-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 6px;
padding: 12px;
min-height: 500px;
}
.grid-view {
.machine-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
}
.machine-card-wrapper {
cursor: pointer;
}
.machine-card {
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
&.selected {
border-color: #409eff;
background: #f0f7ff;
}
}
.status-indicator {
position: absolute;
top: 8px;
right: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
&.status-1 {
background: #67c23a;
}
&.status-2 {
background: #e6a23c;
}
&.status-0 {
background: #f56c6c;
}
}
.machine-visual {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 12px;
.machine-icon-bg {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-bottom: 6px;
}
.machine-number {
font-size: 12px;
color: #909399;
font-weight: 500;
}
}
.machine-details {
margin-bottom: 12px;
.machine-name {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin: 0 0 8px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.machine-meta {
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.quick-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.list-view {
.el-table {
background: transparent;
}
}
.detail-section {
padding: 12px 0;
}
.card-header-title {
display: flex;
align-items: center;
font-weight: 600;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.fade-transform-enter-active, .fade-transform-leave-active {
transition: all 0.3s ease;
}
.fade-transform-enter, .fade-transform-leave-to {
opacity: 0;
transform: translateY(10px);
}
// 生命周期相关样式
.lifecycle-drawer {
::v-deep .el-drawer__body {
padding: 16px;
background: #f5f7fa;
}
}
.lifecycle-container {
min-height: 100%;
}
.device-overview-card {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
.overview-header {
display: flex;
align-items: center;
gap: 16px;
.device-avatar {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.device-basic {
flex: 1;
h2 {
margin: 0 0 6px 0;
font-size: 18px;
font-weight: 600;
color: #2c3e50;
}
p {
margin: 0;
font-size: 13px;
color: #909399;
line-height: 1.5;
}
}
}
.el-divider {
margin: 16px 0;
}
.overview-stats {
display: flex;
gap: 32px;
justify-content: space-around;
.stat {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
.stat-label {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #409eff;
font-family: 'Arial', sans-serif;
}
}
}
}
.lifecycle-type-selector {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
h3 {
margin: 0 0 16px 0;
font-size: 15px;
font-weight: 600;
color: #2c3e50;
}
.type-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 12px;
}
}
.type-card {
background: #f8f9fa;
border: 2px solid transparent;
border-radius: 10px;
padding: 14px 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
&:hover {
border-color: #409eff;
background: #fff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
transform: translateY(-2px);
}
&.active {
border-color: #409eff;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f4ff 100%);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.25);
.type-name {
color: #409eff;
font-weight: 600;
}
}
.type-icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.type-name {
display: block;
font-size: 13px;
color: #2c3e50;
margin-bottom: 0;
font-weight: 500;
transition: all 0.3s ease;
}
.type-count {
display: inline-block;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: #fff;
border-radius: 12px;
min-width: 22px;
height: 22px;
line-height: 22px;
padding: 0 6px;
font-size: 11px;
font-weight: 600;
position: absolute;
top: -6px;
right: -6px;
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.4);
}
}
.lifecycle-timeline-container {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
h3 {
margin: 0 0 20px 0;
font-size: 15px;
font-weight: 600;
color: #2c3e50;
padding-bottom: 12px;
border-bottom: 2px solid #f0f0f0;
}
}
.timeline-wrapper {
position: relative;
padding-left: 32px;
&::before {
content: '';
position: absolute;
left: 9px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(to bottom, #409eff 0%, #e0e0e0 100%);
border-radius: 2px;
}
}
.timeline-item {
position: relative;
margin-bottom: 24px;
padding-bottom: 8px;
&:last-child {
.timeline-line {
display: none;
}
}
.timeline-marker {
position: absolute;
left: -32px;
top: 0;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid #fff;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
z-index: 2;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
}
.timeline-content {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #2c3e50;
}
.event-time {
font-size: 12px;
color: #909399;
white-space: nowrap;
}
}
.event-description {
margin: 0 0 10px 0;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.event-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 8px;
.el-tag {
margin: 0;
}
}
.event-details {
margin-top: 12px;
.el-collapse {
border: none;
}
.el-descriptions {
margin-top: 8px;
}
}
}
.timeline-line {
position: absolute;
left: -17px;
top: 28px;
width: 2px;
height: calc(100% - 28px);
background: #e0e0e0;
z-index: 1;
}
}
</style>