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.

606 lines
19 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>
<div class="app-container">
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button
type="primary"
plain
@click="handleSummary"
:loading="loading"
v-hasPermi="['warehouse:WmsImportResult:query']"
>获取维修汇总</el-button>
</el-col> -->
</el-row>
<el-table
v-loading="loading"
:data="repairSummaryList"
element-loading-text="正在加载数据..."
border
stripe
style="width: 100%"
>
<el-table-column prop="equipment_code" label="设备编号" align="center" width="120">
<template #default="scope">
<span class="equipment-code-link" @click="showRepairRecords(scope.row.equipment_code)">
{{ scope.row.equipment_code }}
</span>
</template>
</el-table-column>
<el-table-column prop="equipment_name" label="设备名称" align="center" min-width="200" />
<el-table-column prop="record_count" label="维修次数" align="center" width="100" />
<el-table-column prop="first_apply_time" label="首次维修时间" align="center" min-width="180" />
<el-table-column prop="last_apply_time" label="最近维修时间" align="center" min-width="180" />
<el-table-column label="距离上次维修天数" align="center" width="150">
<template #default="scope">
<el-tag
:type="getDaysTagType(scope.row.days_since_last_repair)"
effect="dark"
>
{{ scope.row.days_since_last_repair }} 天
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 设备维修记录对话框 -->
<el-dialog
v-model="repairRecordsDialogVisible"
:title="`${selectedEquipmentCode} 维修记录`"
width="90%"
:before-close="handleRepairRecordsClose">
<div class="repair-records-dialog-container">
<!-- 左侧:数据展示区域 -->
<div class="left-panel">
<!-- 故障名称筛选 -->
<div class="breakdown-filter-container">
<span class="filter-label">故障名称:</span>
<el-select
v-model="selectedBreakdownName"
placeholder="全部故障"
clearable
@change="filterRecordsByBreakdownName"
@clear="filterRecordsByBreakdownName"
>
<el-option
v-for="name in uniqueBreakdownNames"
:key="name"
:label="name"
:value="name"
/>
</el-select>
</div>
<div v-loading="repairRecordsLoading" class="repair-records-container">
<div v-if="selectedRepairRecords.length > 0">
<el-table
:data="filteredRepairRecords"
stripe
border
style="width: 100%"
max-height="60vh">
<el-table-column prop="id" label="维修编号" width="80" align="center" />
<el-table-column prop="breakdown_name" label="故障名称" width="120" align="center" />
<el-table-column prop="breakdown_description" label="故障描述" min-width="150" show-overflow-tooltip />
<el-table-column prop="repair_method" label="维修方法" min-width="150" show-overflow-tooltip />
<el-table-column prop="apply_time" label="报修时间" width="200" align="center">
<template #default="scope">
<span :class="{ 'recent': isRecent(scope.row.repair_time) }">
{{ scope.row.apply_time || '无' }}
</span>
</template>
</el-table-column>
<el-table-column prop="repair_time" label="维修时间" width="200" align="center">
<template #default="scope">
<span :class="{ 'recent': isRecent(scope.row.repair_time) }">
{{ scope.row.repair_time || '无' }}
</span>
</template>
</el-table-column>
<el-table-column label="维修时长" width="160" align="center">
<template #default="scope">
{{ calculateRepairDuration(scope.row.apply_time, scope.row.repair_time) }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="!repairRecordsLoading" class="no-records">
暂无维修记录
</div>
</div>
</div>
<!-- 右侧:分析结果区域 -->
<div class="right-panel">
<!-- 分析按钮 -->
<div class="analysis-button-container">
<el-button
type="primary"
@click="analyzeRepairRecords"
:loading="analyzing"
>分析维修记录</el-button>
</div>
<!-- 分析结果显示区域 -->
<div class="analysis-result-container">
<el-input
v-model="analysisResult"
type="textarea"
:rows="20"
placeholder="点击分析维修记录按钮后,分析结果将显示在这里"
readonly
/>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="repairRecordsDialogVisible = false"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { API_CONFIG } from "@/config/api"
// 接收父组件传递的props
const props = defineProps({
selectedEquipmentType: {
type: String,
default: '漆包机'
},
dateRange: {
type: Object,
default: () => ({
start_date: '',
end_date: ''
})
}
})
const loading = ref(false)
const repairSummaryList = ref([])
// 维修记录对话框相关状态
const repairRecordsDialogVisible = ref(false)
const repairRecordsLoading = ref(false)
const selectedRepairRecords = ref([])
const filteredRepairRecords = ref([])
const selectedEquipmentCode = ref('')
const selectedBreakdownName = ref('')
const uniqueBreakdownNames = ref([])
// 分析相关状态
const analyzing = ref(false)
const analysisResult = ref('')
// 监听设备类型变化
watch(() => props.selectedEquipmentType, (newVal, oldVal) => {
console.log('设备类型从', oldVal, '变化为', newVal);
// 如果当前是维修汇总组件,则重新获取数据
if (newVal) {
handleSummary();
}
});
// 监听日期范围变化
watch(() => props.dateRange, (newVal, oldVal) => {
console.log('日期范围从', oldVal, '变化为', newVal);
// 如果日期范围发生变化,则重新获取数据
if (newVal && newVal.start_date && newVal.end_date) {
handleSummary();
}
}, { deep: true });
// 监听故障名称变化,确保筛选能正常工作
watch(selectedBreakdownName, (newVal, oldVal) => {
console.log('故障名称从', oldVal, '变化为', newVal);
// 当故障名称变化时,执行筛选
filterRecordsByBreakdownName();
});
// 组件加载时自动获取数据
onMounted(() => {
console.log('组件加载,设备类型:', props.selectedEquipmentType);
handleSummary();
})
// 根据天数获取标签类型
const getDaysTagType = (days) => {
if (days > 300) return 'danger'
if (days > 180) return 'warning'
if (days > 90) return 'success'
return 'info'
}
// 获取维修汇总数据
function handleSummary() {
console.log('设备类型:', props.selectedEquipmentType);
console.log('日期范围:', props.dateRange);
loading.value = true
// 构建API URL包含设备类型和日期范围参数
let apiUrl = `${API_CONFIG.BASE_URL}/api/eq-repair/enamelling-repair-summary?equipment_type=${props.selectedEquipmentType}`;
// 如果有日期范围则添加到URL中
if (props.dateRange && props.dateRange.start_date && props.dateRange.end_date) {
apiUrl += `&start_date=${props.dateRange.start_date}&end_date=${props.dateRange.end_date}`;
}
fetch(apiUrl)
.then(response => response.json())
.then(data => {
if (data.code === 200 && data.data) {
repairSummaryList.value = data.data
}
})
.catch(error => {
console.error('维修汇总API调用失败:', error)
})
.finally(() => {
loading.value = false
})
}
// 显示设备维修记录
const showRepairRecords = (equipmentCode) => {
selectedEquipmentCode.value = equipmentCode
repairRecordsDialogVisible.value = true
fetchRepairRecords(equipmentCode)
}
// 获取设备维修记录
const fetchRepairRecords = (equipmentCode) => {
repairRecordsLoading.value = true
// 构建API URL
let apiUrl = `${API_CONFIG.BASE_URL}/api/eq-repair/repair-records/${equipmentCode}`;
fetch(apiUrl)
.then(response => response.json())
.then(data => {
if (data.code === 200 && data.data) {
selectedRepairRecords.value = data.data
filteredRepairRecords.value = data.data
// 提取所有唯一的故障名称
const breakdownNames = [...new Set(data.data.map(record => record.breakdown_name).filter(Boolean))]
uniqueBreakdownNames.value = breakdownNames
// 确保筛选器初始化正确
selectedBreakdownName.value = ''
filteredRepairRecords.value = [...selectedRepairRecords.value]
} else {
selectedRepairRecords.value = []
filteredRepairRecords.value = []
uniqueBreakdownNames.value = []
}
})
.catch(error => {
console.error('维修记录API调用失败:', error)
selectedRepairRecords.value = []
filteredRepairRecords.value = []
uniqueBreakdownNames.value = []
})
.finally(() => {
repairRecordsLoading.value = false
})
}
// 根据故障名称筛选维修记录
const filterRecordsByBreakdownName = (value) => {
console.log('筛选故障名称:', value, 'selectedBreakdownName.value:', selectedBreakdownName.value)
// 如果有传入的值,更新 selectedBreakdownName
if (value !== undefined) {
selectedBreakdownName.value = value
}
// 获取筛选值,处理空值情况
const filterValue = selectedBreakdownName.value || ''
if (!filterValue) {
// 如果没有选择故障名称,显示所有记录
filteredRepairRecords.value = [...selectedRepairRecords.value]
} else {
// 根据选择的故障名称筛选
filteredRepairRecords.value = selectedRepairRecords.value.filter(
record => record.breakdown_name === filterValue
)
}
console.log('筛选后记录数:', filteredRepairRecords.value.length)
}
// 关闭维修记录对话框
const handleRepairRecordsClose = () => {
repairRecordsDialogVisible.value = false
}
// 检查维修日期是否是最近的30天内
const isRecent = (repairDate) => {
if (!repairDate) return false
try {
const repairDateObj = new Date(repairDate)
const currentDate = new Date()
const timeDiff = currentDate - repairDateObj
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24))
return daysDiff <= 30
} catch (e) {
return false
}
}
// 计算维修时长
const calculateRepairDuration = (applyTime, repairTime) => {
if (!applyTime || !repairTime) return '无'
try {
const applyDate = new Date(applyTime)
const repairDate = new Date(repairTime)
const timeDiff = repairDate - applyDate
if (timeDiff < 0) return '无效时间'
const hours = Math.floor(timeDiff / (1000 * 60 * 60))
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60))
if (hours > 24) {
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return `${days}${remainingHours}小时`
} else if (hours > 0) {
return `${hours}小时${minutes}分钟`
} else {
return `${minutes}分钟`
}
} catch (e) {
return '计算错误'
}
}
// 分析维修记录
const analyzeRepairRecords = () => {
analyzing.value = true
// 模拟分析过程
setTimeout(() => {
try {
if (filteredRepairRecords.value.length === 0) {
analysisResult.value = '暂无数据可供分析。'
analyzing.value = false
return
}
// 基本统计数据
const totalRecords = filteredRepairRecords.value.length
const breakdownStats = {}
const repairMethods = {}
const repairDurations = []
const recentRecords = []
const oldestRecords = []
let totalRepairTime = 0
let completedRepairs = 0
// 统计各项数据
filteredRepairRecords.value.forEach(record => {
// 故障类型统计
const breakdownName = record.breakdown_name || '未知故障'
breakdownStats[breakdownName] = (breakdownStats[breakdownName] || 0) + 1
// 维修方法统计
const repairMethod = record.repair_method || '未知方法'
repairMethods[repairMethod] = (repairMethods[repairMethod] || 0) + 1
// 计算维修时长
if (record.apply_time && record.repair_time) {
const applyDate = new Date(record.apply_time)
const repairDate = new Date(record.repair_time)
if (!isNaN(applyDate.getTime()) && !isNaN(repairDate.getTime()) && repairDate > applyDate) {
const duration = (repairDate - applyDate) / (1000 * 60 * 60) // 小时
repairDurations.push(duration)
totalRepairTime += duration
completedRepairs++
}
}
})
// 分析结果
let result = `=== 设备 ${selectedEquipmentCode.value} 维修记录分析报告 ===\n\n`
// 1. 基本统计
result += `【基本统计】\n`
result += `- 总维修记录数:${totalRecords}\n`
result += `- 完成维修数:${completedRepairs}\n`
if (completedRepairs > 0) {
const avgRepairTime = (totalRepairTime / completedRepairs).toFixed(2)
result += `- 平均维修时长:${avgRepairTime} 小时\n`
}
result += `\n`
// 2. 故障类型分析
result += `【故障类型分析】\n`
const sortedBreakdowns = Object.entries(breakdownStats).sort((a, b) => b[1] - a[1])
sortedBreakdowns.forEach(([name, count]) => {
const percentage = ((count / totalRecords) * 100).toFixed(1)
result += `- ${name}${count} 次 (${percentage}%)\n`
})
result += `\n`
// 3. 维修方法分析
result += `【维修方法分析】\n`
const sortedMethods = Object.entries(repairMethods).sort((a, b) => b[1] - a[1])
if (sortedMethods.length > 0) {
sortedMethods.slice(0, 5).forEach(([method, count]) => {
const percentage = ((count / totalRecords) * 100).toFixed(1)
result += `- ${method}${count} 次 (${percentage}%)\n`
})
} else {
result += `- 暂无维修方法记录\n`
}
result += `\n`
// 4. 维修效率分析
if (repairDurations.length > 0) {
result += `【维修效率分析】\n`
repairDurations.sort((a, b) => a - b)
const minTime = repairDurations[0].toFixed(2)
const maxTime = repairDurations[repairDurations.length - 1].toFixed(2)
const medianTime = repairDurations[Math.floor(repairDurations.length / 2)].toFixed(2)
result += `- 最短维修时间:${minTime} 小时\n`
result += `- 最长维修时间:${maxTime} 小时\n`
result += `- 中位数维修时间:${medianTime} 小时\n`
// 维修效率分类
let fastRepairs = 0, normalRepairs = 0, slowRepairs = 0
repairDurations.forEach(duration => {
if (duration <= 2) fastRepairs++
else if (duration <= 8) normalRepairs++
else slowRepairs++
})
result += `- 快速维修≤2小时${fastRepairs} 次 (${((fastRepairs / repairDurations.length) * 100).toFixed(1)}%)\n`
result += `- 常规维修2-8小时${normalRepairs} 次 (${((normalRepairs / repairDurations.length) * 100).toFixed(1)}%)\n`
result += `- 耗时维修(>8小时${slowRepairs} 次 (${((slowRepairs / repairDurations.length) * 100).toFixed(1)}%)\n`
} else {
result += `【维修效率分析】\n`
result += `- 暂无有效维修时间记录\n`
}
result += `\n`
// 5. 建议和结论
result += `【建议与结论】\n`
// 根据最常见的故障类型给出建议
if (sortedBreakdowns.length > 0) {
const topBreakdown = sortedBreakdowns[0]
const percentage = ((topBreakdown[1] / totalRecords) * 100).toFixed(1)
result += `- 最常见的故障是"${topBreakdown[0]}",占比${percentage}%,建议针对性加强预防措施\n`
}
// 根据维修效率给出建议
if (repairDurations.length > 0) {
const avgTime = totalRepairTime / completedRepairs
if (avgTime > 8) {
result += `- 平均维修时间较长,建议优化维修流程或增加维修人员\n`
} else if (avgTime < 2) {
result += `- 维修效率较高,维修团队表现良好\n`
}
}
// 如果筛选了特定故障类型,给出针对性建议
if (selectedBreakdownName.value) {
const filteredCount = filteredRepairRecords.value.length
const totalCount = selectedRepairRecords.value.length
result += `- 针对"${selectedBreakdownName.value}"故障的专项分析:共${filteredCount}条记录,占总故障的${((filteredCount / totalCount) * 100).toFixed(1)}%\n`
}
analysisResult.value = result
} catch (error) {
console.error('分析过程中出错:', error)
analysisResult.value = '分析过程中发生错误,请稍后重试。'
} finally {
analyzing.value = false
}
}, 1500) // 模拟分析过程耗时
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.mb8 {
margin-bottom: 8px;
}
/* 设备编号链接样式 */
.equipment-code-link {
color: #409EFF;
cursor: pointer;
text-decoration: underline;
}
.equipment-code-link:hover {
color: #66b1ff;
}
/* 故障名称筛选区域样式 */
.breakdown-filter-container {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 0 10px;
}
.filter-label {
margin-right: 10px;
white-space: nowrap;
}
/* 维修记录对话框样式 */
.repair-records-container {
padding: 10px 0;
}
.no-records {
text-align: center;
color: #909399;
padding: 20px 0;
}
/* 最近维修记录高亮样式 */
.recent {
color: #F56C6C;
font-weight: bold;
}
/* 维修记录对话框左右布局样式 */
.repair-records-dialog-container {
display: flex;
height: 60vh;
}
.left-panel {
flex: 3;
padding-right: 15px;
display: flex;
flex-direction: column;
}
.right-panel {
flex: 2;
padding-left: 15px;
border-left: 1px solid #ebeef5;
display: flex;
flex-direction: column;
}
.analysis-button-container {
margin-bottom: 15px;
text-align: right;
}
.analysis-result-container {
flex: 1;
display: flex;
flex-direction: column;
}
.analysis-result-container .el-textarea {
flex: 1;
}
.analysis-result-container .el-textarea__inner {
height: 100%;
font-family: monospace;
white-space: pre-wrap;
word-break: break-word;
}
</style>