feat(看板管理): 优化轮播看板组件样式和功能
- 调整轮播间隔和高度,优化视觉体验 - 设备监控卡片增加数据加载状态和定时刷新功能 - 工单在线卡片添加统计数据和状态分布图表 - 毛坯库存卡片增加库存类别和数量排名图表 - 质量统计卡片添加质量状态分布和合格率排名图表 - 所有卡片表格增加自动滚动功能
This commit is contained in:
@@ -24,155 +24,59 @@
|
||||
border
|
||||
size="mini"
|
||||
:height="tableHeight"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="id" label="编号" width="80"></el-table-column>
|
||||
<el-table-column prop="type" label="报警类型" width="120">
|
||||
<el-table-column prop="area" label="报警区域" width="120"></el-table-column>
|
||||
<!-- <el-table-column prop="type" label="报警类型" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getAlarmTypeColor(scope.row.type)">{{ scope.row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="content" label="报警内容"></el-table-column>
|
||||
<el-table-column prop="time" label="报警时间" width="150"></el-table-column>
|
||||
<el-table-column prop="status" label="处理状态" width="100">
|
||||
<el-table-column prop="status" label="报警状态" width="150"></el-table-column>
|
||||
<!-- <el-table-column prop="status" label="处理状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === '已处理' ? 'success' : 'danger'">{{ scope.row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getEquipmentAlarmCarouselBoardData, getEquipmentLiveCarouselBoardData } from '@/api/kanbanManagement/carouselBoard.js'
|
||||
|
||||
export default {
|
||||
name: 'DeviceMonitoringCard',
|
||||
data() {
|
||||
return {
|
||||
tableHeight: '400px',
|
||||
loading: false,
|
||||
// 设备实时数据
|
||||
deviceData: [
|
||||
{
|
||||
id: 1,
|
||||
label: '底漆循环温度',
|
||||
value: 45.2,
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '底漆循环湿度',
|
||||
value: 65.5,
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '色漆循环温度',
|
||||
value: 52.1,
|
||||
unit: '°C',
|
||||
status: 'warning'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: '色漆循环湿度',
|
||||
value: 72.3,
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: '清漆循环温度',
|
||||
value: 48.7,
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: '清漆循环湿度',
|
||||
value: 68.9,
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: '纯水电导率',
|
||||
value: 1.2,
|
||||
unit: 'μS/cm',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
label: '水份烘干温度',
|
||||
value: 85.6,
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
label: '清漆烘干温度',
|
||||
value: 135.8,
|
||||
unit: '°C',
|
||||
status: 'error'
|
||||
}
|
||||
],
|
||||
deviceData: [],
|
||||
// 设备报警记录
|
||||
alarmRecords: [
|
||||
{
|
||||
id: 'ALM-20240315-001',
|
||||
type: '温度异常',
|
||||
content: '清漆烘干温度超过阈值(130°C)',
|
||||
time: '2024-03-15 14:25:30',
|
||||
status: '未处理'
|
||||
},
|
||||
{
|
||||
id: 'ALM-20240315-002',
|
||||
type: '湿度异常',
|
||||
content: '色漆循环湿度接近上限(75%)',
|
||||
time: '2024-03-15 13:45:10',
|
||||
status: '已处理'
|
||||
},
|
||||
{
|
||||
id: 'ALM-20240315-003',
|
||||
type: '设备故障',
|
||||
content: '底漆循环泵压力异常',
|
||||
time: '2024-03-15 11:30:45',
|
||||
status: '已处理'
|
||||
},
|
||||
{
|
||||
id: 'ALM-20240314-001',
|
||||
type: '水质异常',
|
||||
content: '纯水电导率偏高',
|
||||
time: '2024-03-14 16:15:20',
|
||||
status: '已处理'
|
||||
},
|
||||
{
|
||||
id: 'ALM-20240314-002',
|
||||
type: '温度异常',
|
||||
content: '水份烘干温度波动较大',
|
||||
time: '2024-03-14 10:05:50',
|
||||
status: '已处理'
|
||||
},
|
||||
{
|
||||
id: 'ALM-20240313-001',
|
||||
type: '系统警告',
|
||||
content: '压缩空气压力低于正常值',
|
||||
time: '2024-03-13 15:40:15',
|
||||
status: '已处理'
|
||||
}
|
||||
]
|
||||
alarmRecords: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
|
||||
// 模拟数据实时更新
|
||||
this.startDataSimulation()
|
||||
// 初始加载数据
|
||||
this.getEquipmentData()
|
||||
|
||||
// 设置定时器,定期更新数据
|
||||
this.timer = setInterval(() => {
|
||||
this.getEquipmentData()
|
||||
}, 120000) // 每120秒更新一次
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.simulationInterval) {
|
||||
clearInterval(this.simulationInterval)
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -182,6 +86,151 @@ export default {
|
||||
this.tableHeight = Math.max(windowHeight * 0.35, 300) + 'px'
|
||||
},
|
||||
|
||||
// 获取设备数据
|
||||
getEquipmentData() {
|
||||
this.loading = true
|
||||
|
||||
// 并行请求设备实时数据和报警记录
|
||||
Promise.all([
|
||||
this.getEquipmentLiveData(),
|
||||
this.getEquipmentAlarmData()
|
||||
]).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 获取设备实时数据
|
||||
getEquipmentLiveData() {
|
||||
return getEquipmentLiveCarouselBoardData({})
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
// 格式化设备实时数据为前端展示所需的格式
|
||||
this.deviceData = this.formatDeviceLiveData(res.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取设备报警记录
|
||||
getEquipmentAlarmData() {
|
||||
return getEquipmentAlarmCarouselBoardData({})
|
||||
.then((res) => {
|
||||
if (res.code === 200 && res.data) {
|
||||
// 格式化报警数据为前端展示所需的格式
|
||||
this.alarmRecords = this.formatAlarmData(res.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化设备实时数据
|
||||
formatDeviceLiveData(data) {
|
||||
const deviceItems = [
|
||||
{
|
||||
id: 1,
|
||||
label: '底漆循环温度',
|
||||
value: (parseFloat(data.primerCycleTemperature) || 0).toFixed(2),
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '底漆循环湿度',
|
||||
value: (parseFloat(data.primerCycleHumidity) || 0).toFixed(2),
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '色漆循环温度',
|
||||
value: (parseFloat(data.colorCycleTemperature) || 0).toFixed(2),
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: '色漆循环湿度',
|
||||
value: (parseFloat(data.colorCycleHumidity) || 0).toFixed(2),
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: '清漆循环温度',
|
||||
value: (parseFloat(data.clearCycleTemperature) || 0).toFixed(2),
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: '清漆循环湿度',
|
||||
value: (parseFloat(data.clearCycleHumidity) || 0).toFixed(2),
|
||||
unit: '%',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: '纯水电导率',
|
||||
value: (parseFloat(data.pureWaterConductivity) || 0).toFixed(2),
|
||||
unit: 'μS/cm',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
label: '水份烘干温度',
|
||||
value: (parseFloat(data.moistureDryingTemperature) || 0).toFixed(2),
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
label: '清漆烘干温度',
|
||||
value: (parseFloat(data.clearDryingTemperature) || 0).toFixed(2),
|
||||
unit: '°C',
|
||||
status: 'normal'
|
||||
}
|
||||
]
|
||||
|
||||
// 更新每个数据项的状态
|
||||
deviceItems.forEach(item => {
|
||||
this.updateDataStatus(item)
|
||||
})
|
||||
|
||||
return deviceItems
|
||||
},
|
||||
|
||||
// 格式化报警数据
|
||||
formatAlarmData(data) {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(item => ({
|
||||
id: item.id || '',
|
||||
area: item.alarmArea || '',
|
||||
type: item.alarmType || '',
|
||||
content: item.alarmContent || '',
|
||||
time: item.alarmTime ? this.formatDateTime(item.alarmTime) : '',
|
||||
status: item.handleStatus || '未处理'
|
||||
}))
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
// 格式化日期时间
|
||||
formatDateTime(dateTime) {
|
||||
if (!dateTime) return ''
|
||||
|
||||
// 处理字符串和Date对象
|
||||
const date = typeof dateTime === 'string' ? new Date(dateTime) : dateTime
|
||||
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
},
|
||||
|
||||
// 根据报警类型获取标签颜色
|
||||
getAlarmTypeColor(type) {
|
||||
const colorMap = {
|
||||
@@ -194,22 +243,7 @@ export default {
|
||||
return colorMap[type] || 'info'
|
||||
},
|
||||
|
||||
// 模拟数据实时更新
|
||||
startDataSimulation() {
|
||||
this.simulationInterval = setInterval(() => {
|
||||
this.deviceData.forEach(item => {
|
||||
// 模拟数据小幅度波动
|
||||
const variation = (Math.random() - 0.5) * 2
|
||||
const newValue = Math.round((item.value + variation) * 10) / 10
|
||||
|
||||
// 更新值
|
||||
item.value = newValue
|
||||
|
||||
// 根据值更新状态
|
||||
this.updateDataStatus(item)
|
||||
})
|
||||
}, 5000) // 每5秒更新一次
|
||||
},
|
||||
|
||||
|
||||
// 更新数据状态
|
||||
updateDataStatus(item) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="quality-statistics-card">
|
||||
<div style="text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 10px;">质量统计报表</div>
|
||||
<div class="middle-data">
|
||||
<span>{{ reportType_options[search.reportType].label }} 共 {{ realTotal }} 条</span>
|
||||
<span>报表数量 共 {{ realTotal }} 条</span>
|
||||
<span style="margin-left: 20px">投入数:{{ totalQuantity }}</span>
|
||||
<span style="margin-left: 20px">合格数:{{ totalQualifiedNumber }}</span>
|
||||
<span style="margin-left: 20px">合格率:{{ passRate }}</span>
|
||||
@@ -11,6 +11,22 @@
|
||||
<span style="margin-left: 20px">报废数:{{ totalBaofeiTotal }}</span>
|
||||
<span style="margin-left: 20px">倒车雷达:{{ parkingSensor }}</span>
|
||||
</div>
|
||||
|
||||
<div class="charts-wrapper">
|
||||
<!-- 质量状态分布饼图 -->
|
||||
<div class="chart-container pie-chart">
|
||||
<div id="qualityStatusChart" style="width: 100%; height: 180px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 合格率排名 - 两个单独的横向柱状图 -->
|
||||
<div class="chart-container bar-chart top-chart">
|
||||
<div id="topQualifiedRateChart" style="width: 100%; height: 180px;"></div>
|
||||
</div>
|
||||
<div class="chart-container bar-chart bottom-chart">
|
||||
<div id="bottomQualifiedRateChart" style="width: 100%; height: 180px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
:loading="loading"
|
||||
@@ -18,6 +34,9 @@
|
||||
size="mini"
|
||||
:height="tableHeight"
|
||||
style="width: 100%"
|
||||
ref="qualityTable"
|
||||
stripe
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column prop="workorderId" label="工单号" width="140" sortable></el-table-column>
|
||||
<el-table-column prop="finishedPartNumber" label="零件号" min-width="120" sortable></el-table-column>
|
||||
@@ -41,6 +60,7 @@
|
||||
|
||||
<script>
|
||||
import { getQualityStatisticsCarouselBoardData } from '@/api/kanbanManagement/carouselBoard.js'
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'QualityStatisticsCard',
|
||||
@@ -48,7 +68,7 @@ export default {
|
||||
return {
|
||||
loading: false,
|
||||
QualityStatisticsTable: [],
|
||||
tableHeight: '700px',
|
||||
tableHeight: '500px', // 调整表格高度,为图表留出空间
|
||||
search: {
|
||||
starttime: null,
|
||||
endtime: null,
|
||||
@@ -72,7 +92,11 @@ export default {
|
||||
},
|
||||
realTotal: 0,
|
||||
allDataList: [],
|
||||
parkingSensor: 0
|
||||
parkingSensor: 0,
|
||||
qualityChart: null,
|
||||
topQualifiedRateChart: null,
|
||||
bottomQualifiedRateChart: null,
|
||||
tableScrollTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -149,8 +173,38 @@ export default {
|
||||
// 监听窗口大小变化,调整表格高度
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 数据更新后初始化图表
|
||||
if (this.QualityStatisticsTable.length > 0 && !this.qualityChart) {
|
||||
this.$nextTick(() => {
|
||||
this.initQualityChart()
|
||||
this.initTopQualifiedRateChart()
|
||||
this.initBottomQualifiedRateChart()
|
||||
this.startTableAutoScroll()
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
// 清理图表实例
|
||||
if (this.qualityChart) {
|
||||
this.qualityChart.dispose()
|
||||
this.qualityChart = null
|
||||
}
|
||||
if (this.topQualifiedRateChart) {
|
||||
this.topQualifiedRateChart.dispose()
|
||||
this.topQualifiedRateChart = null
|
||||
}
|
||||
if (this.bottomQualifiedRateChart) {
|
||||
this.bottomQualifiedRateChart.dispose()
|
||||
this.bottomQualifiedRateChart = null
|
||||
}
|
||||
// 清理表格滚动定时器
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
this.tableScrollTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
@@ -187,16 +241,303 @@ export default {
|
||||
if (res.code == 200) {
|
||||
this.QualityStatisticsTable = res.data || []
|
||||
this.allDataList = res.data || []
|
||||
this.realTotal = res.data.length || 0
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化质量状态分布饼图 - 按质量类别统计
|
||||
initQualityChart() {
|
||||
const chartDom = document.getElementById('qualityStatusChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.qualityChart = echarts.init(chartDom)
|
||||
|
||||
// 准备按质量类别统计的饼图数据
|
||||
const qualifiedNum = this.totalQualifiedNumber
|
||||
const unqualifiedNum = Math.trunc(this.filteredDataList.reduce((acc, data) => acc + ((data.requireNumber || 0) - (data.qualifiedNumber || 0)), 0) / 3)
|
||||
const scrapNum = this.totalBaofeiTotal
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '质量状态分布',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '质量状态',
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 6,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 10,
|
||||
formatter: '{b}\n{d}%'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{ value: qualifiedNum, name: '合格', itemStyle: { color: '#67c23a' } },
|
||||
{ value: unqualifiedNum, name: '不合格', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: scrapNum, name: '报废', itemStyle: { color: '#e6a23c' } }
|
||||
].filter(item => item.value > 0) // 过滤掉数量为0的类别
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.qualityChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.qualityChart) {
|
||||
this.qualityChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化合格率最高的5个横向柱状图
|
||||
initTopQualifiedRateChart() {
|
||||
const chartDom = document.getElementById('topQualifiedRateChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.topQualifiedRateChart = echarts.init(chartDom)
|
||||
|
||||
// 过滤有合格数据的记录并按合格率降序排序
|
||||
const validData = this.filteredDataList.filter(item =>
|
||||
item.qualifiedRate !== null && item.qualifiedRate !== '' && !isNaN(item.qualifiedRate)
|
||||
).sort((a, b) => parseFloat(b.qualifiedRate) - parseFloat(a.qualifiedRate))
|
||||
|
||||
// 获取合格率最高的前5个
|
||||
const top5 = validData.slice(0, 5)
|
||||
|
||||
// 准备横向柱状图数据
|
||||
const topLabels = top5.map(item => {
|
||||
const partNumber = item.finishedPartNumber || '未知'
|
||||
const desc = item.productDescription || ''
|
||||
return (partNumber + '-' + desc).substring(0, 15) // 截取前15个字符
|
||||
})
|
||||
const topRates = top5.map(item => parseFloat(item.qualifiedRate) || 0)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '合格率最高5个',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
const data = params[0]
|
||||
return data.name + '<br/>合格率: ' + data.value + '%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '30%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%',
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: topLabels,
|
||||
axisLabel: {
|
||||
fontSize: 9,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '合格率',
|
||||
type: 'bar',
|
||||
data: topRates,
|
||||
itemStyle: {
|
||||
color: '#67c23a'
|
||||
},
|
||||
barWidth: '40%'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.topQualifiedRateChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.topQualifiedRateChart) {
|
||||
this.topQualifiedRateChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化合格率最低的5个横向柱状图
|
||||
initBottomQualifiedRateChart() {
|
||||
const chartDom = document.getElementById('bottomQualifiedRateChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.bottomQualifiedRateChart = echarts.init(chartDom)
|
||||
|
||||
// 过滤有合格数据的记录并按合格率升序排序
|
||||
const validData = this.filteredDataList.filter(item =>
|
||||
item.qualifiedRate !== null && item.qualifiedRate !== '' && !isNaN(item.qualifiedRate)
|
||||
).sort((a, b) => parseFloat(a.qualifiedRate) - parseFloat(b.qualifiedRate))
|
||||
|
||||
// 获取合格率最低的前5个
|
||||
const bottom5 = validData.slice(0, 5)
|
||||
|
||||
// 准备横向柱状图数据
|
||||
const bottomLabels = bottom5.map(item => {
|
||||
const partNumber = item.finishedPartNumber || '未知'
|
||||
const desc = item.productDescription || ''
|
||||
return (partNumber + '-' + desc).substring(0, 15) // 截取前15个字符
|
||||
})
|
||||
const bottomRates = bottom5.map(item => parseFloat(item.qualifiedRate) || 0)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '合格率最低5个',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
const data = params[0]
|
||||
return data.name + '<br/>合格率: ' + data.value + '%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '30%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%',
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: bottomLabels,
|
||||
axisLabel: {
|
||||
fontSize: 9,
|
||||
interval: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '合格率',
|
||||
type: 'bar',
|
||||
data: bottomRates,
|
||||
itemStyle: {
|
||||
color: '#f56c6c'
|
||||
},
|
||||
barWidth: '40%'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.bottomQualifiedRateChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.bottomQualifiedRateChart) {
|
||||
this.bottomQualifiedRateChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 表格自动滚动功能
|
||||
startTableAutoScroll() {
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
}
|
||||
|
||||
this.tableScrollTimer = setInterval(() => {
|
||||
if (this.$refs.qualityTable) {
|
||||
const elTableBody = this.$refs.qualityTable.$el.querySelector('.el-table__body-wrapper')
|
||||
if (elTableBody) {
|
||||
// 每次滚动一行的高度
|
||||
const scrollStep = 31 // 行高 + 边框
|
||||
elTableBody.scrollTop += scrollStep
|
||||
|
||||
// 如果滚动到底部,回到顶部继续滚动
|
||||
if (elTableBody.scrollTop + elTableBody.clientHeight >= elTableBody.scrollHeight) {
|
||||
elTableBody.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000) // 每2秒滚动一次
|
||||
},
|
||||
handleResize() {
|
||||
// 简单的响应式调整
|
||||
const windowHeight = window.innerHeight
|
||||
this.tableHeight = Math.max(windowHeight * 0.7, 500) + 'px'
|
||||
this.tableHeight = Math.max(windowHeight * 0.65, 450) + 'px' // 增加表格高度,突出表格展示
|
||||
|
||||
// 调整图表大小
|
||||
if (this.qualityChart) {
|
||||
this.qualityChart.resize()
|
||||
}
|
||||
if (this.topQualifiedRateChart) {
|
||||
this.topQualifiedRateChart.resize()
|
||||
}
|
||||
if (this.bottomQualifiedRateChart) {
|
||||
this.bottomQualifiedRateChart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,17 +547,101 @@ export default {
|
||||
.quality-statistics-card {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.middle-data {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 15px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 0 auto 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.middle-data span {
|
||||
margin: 0 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.charts-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.top-chart {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.bottom-chart {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
/* 突出表格显示 */
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
/* 优化表格样式 */
|
||||
.el-table__header-wrapper th {
|
||||
background-color: #ecf5ff !important;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 12px 5px;
|
||||
border-bottom: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper td {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 12px 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 优化斑马纹效果 */
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 高亮当前行 */
|
||||
.el-table__body tr:hover>td {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,31 @@
|
||||
<template>
|
||||
<div class="wm-blank-inventory-card">
|
||||
<div style="text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 10px;">毛坯库存</div>
|
||||
<div style="text-align: center; margin-bottom: 10px;">
|
||||
<span style="font-size: 16px; color: #67c23a;">毛坯仓库零件数: {{ partTotal }}</span>
|
||||
<div class="middle-data">
|
||||
<span>毛坯总数: {{ partTotal }}</span>
|
||||
</div>
|
||||
|
||||
<div class="charts-wrapper">
|
||||
<!-- 库存类别分布饼图 -->
|
||||
<div class="chart-container pie-chart">
|
||||
<div id="inventoryTypeChart" style="width: 100%; height: 180px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 库存数量排名柱状图 -->
|
||||
<div class="chart-container bar-chart">
|
||||
<div id="inventoryRankChart" style="width: 100%; height: 180px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="inventoryData"
|
||||
v-loading="loading"
|
||||
border
|
||||
style="width: 100%"
|
||||
:height="tableHeight"
|
||||
ref="inventoryTable"
|
||||
stripe
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column prop="blankNum" label="毛坯号" width="160"></el-table-column>
|
||||
<el-table-column prop="description" label="产品描述"></el-table-column>
|
||||
@@ -33,6 +49,7 @@
|
||||
|
||||
<script>
|
||||
import { getBlankInventoryCarouselBoardData } from '@/api/kanbanManagement/carouselBoard.js'
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'WmBlankInventoryCard',
|
||||
@@ -41,15 +58,43 @@ export default {
|
||||
loading: false,
|
||||
inventoryData: [],
|
||||
partTotal: 0,
|
||||
tableHeight: '700px'
|
||||
tableHeight: '500px', // 调整表格高度,为图表留出空间
|
||||
inventoryChart: null,
|
||||
inventoryRankChart: null,
|
||||
tableScrollTimer: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getInventoryData()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 数据更新后初始化图表
|
||||
if (this.inventoryData.length > 0 && !this.inventoryChart) {
|
||||
this.$nextTick(() => {
|
||||
this.initInventoryChart()
|
||||
this.initInventoryRankChart()
|
||||
this.startTableAutoScroll()
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
// 清理图表实例
|
||||
if (this.inventoryChart) {
|
||||
this.inventoryChart.dispose()
|
||||
this.inventoryChart = null
|
||||
}
|
||||
if (this.inventoryRankChart) {
|
||||
this.inventoryRankChart.dispose()
|
||||
this.inventoryRankChart = null
|
||||
}
|
||||
// 清理表格滚动定时器
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
this.tableScrollTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInventoryData() {
|
||||
@@ -64,20 +109,342 @@ export default {
|
||||
.then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.inventoryData = res.data || []
|
||||
// 计算总数量
|
||||
this.partTotal = this.inventoryData.reduce((sum, item) => sum + (item.quantity || 0), 0)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化库存类别分布饼图 - 按毛坯类别统计
|
||||
initInventoryChart() {
|
||||
const chartDom = document.getElementById('inventoryTypeChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.inventoryChart = echarts.init(chartDom)
|
||||
|
||||
// 准备按毛坯类别统计的饼图数据
|
||||
const typeCounts = {
|
||||
'鲨鱼鳍': 0,
|
||||
'门把手': 0,
|
||||
'油箱盖': 0,
|
||||
'装饰罩': 0,
|
||||
'加油口盖': 0,
|
||||
'其他': 0
|
||||
}
|
||||
|
||||
// 根据产品描述或名称判断类别
|
||||
this.inventoryData.forEach(item => {
|
||||
const description = (item.description || '').toLowerCase()
|
||||
const quantity = item.quantity || 0
|
||||
|
||||
if (description.includes('鲨鱼鳍') || description.includes('shark fin')) {
|
||||
typeCounts['鲨鱼鳍'] += quantity
|
||||
} else if (description.includes('门把手') || description.includes('door handle')) {
|
||||
typeCounts['门把手'] += quantity
|
||||
} else if (description.includes('油箱盖') || description.includes('fuel tank cover')) {
|
||||
typeCounts['油箱盖'] += quantity
|
||||
} else if (description.includes('装饰罩') || description.includes('decorative cover')) {
|
||||
typeCounts['装饰罩'] += quantity
|
||||
} else if (description.includes('加油口盖') || description.includes('fuel filler cap')) {
|
||||
typeCounts['加油口盖'] += quantity
|
||||
} else {
|
||||
typeCounts['其他'] += quantity
|
||||
}
|
||||
})
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '毛坯类别分布',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '库存类别',
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 6,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 10,
|
||||
formatter: '{b}\n{d}%'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{ value: typeCounts['鲨鱼鳍'], name: '鲨鱼鳍', itemStyle: { color: '#67c23a' } },
|
||||
{ value: typeCounts['门把手'], name: '门把手', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: typeCounts['油箱盖'], name: '油箱盖', itemStyle: { color: '#409eff' } },
|
||||
{ value: typeCounts['装饰罩'], name: '装饰罩', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: typeCounts['加油口盖'], name: '加油口盖', itemStyle: { color: '#909399' } },
|
||||
{ value: typeCounts['其他'], name: '其他', itemStyle: { color: '#c0c4cc' } }
|
||||
].filter(item => item.value > 0) // 过滤掉数量为0的类别
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.inventoryChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.inventoryChart) {
|
||||
this.inventoryChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化库存数量排名柱状图
|
||||
initInventoryRankChart() {
|
||||
const chartDom = document.getElementById('inventoryRankChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.inventoryRankChart = echarts.init(chartDom)
|
||||
|
||||
// 按库存数量排序
|
||||
const sortedData = [...this.inventoryData].sort((a, b) => (b.quantity || 0) - (a.quantity || 0))
|
||||
|
||||
// 获取库存最多的前5个
|
||||
const top5 = sortedData.slice(0, 5)
|
||||
// 获取库存最少的前5个
|
||||
const bottom5 = sortedData.slice(-5).reverse()
|
||||
|
||||
// 准备柱状图数据
|
||||
const topNames = top5.map(item => (item.blankNum || '').substring(0, 8)) // 截取零件号前8位显示
|
||||
const topValues = top5.map(item => item.quantity || 0)
|
||||
const bottomNames = bottom5.map(item => (item.blankNum || '').substring(0, 8))
|
||||
const bottomValues = bottom5.map(item => item.quantity || 0)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '库存数量排名',
|
||||
left: 'center',
|
||||
top: 0,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: function(params) {
|
||||
const data = params[0]
|
||||
return data.name + '<br/>库存数量: ' + data.value
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['最多5个', '最少5个'],
|
||||
top: 25,
|
||||
textStyle: {
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '40%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [...topNames, ...bottomNames],
|
||||
axisLabel: {
|
||||
rotate: 45,
|
||||
fontSize: 9
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 10
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '最多5个',
|
||||
type: 'bar',
|
||||
data: [...topValues, ...new Array(5).fill(0)],
|
||||
itemStyle: {
|
||||
color: '#67c23a'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '最少5个',
|
||||
type: 'bar',
|
||||
data: [...new Array(5).fill(0), ...bottomValues],
|
||||
itemStyle: {
|
||||
color: '#f56c6c'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.inventoryRankChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.inventoryRankChart) {
|
||||
this.inventoryRankChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 表格自动滚动功能
|
||||
startTableAutoScroll() {
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
}
|
||||
|
||||
this.tableScrollTimer = setInterval(() => {
|
||||
if (this.$refs.inventoryTable) {
|
||||
const elTableBody = this.$refs.inventoryTable.$el.querySelector('.el-table__body-wrapper')
|
||||
if (elTableBody) {
|
||||
// 每次滚动一行的高度
|
||||
const scrollStep = 31 // 行高 + 边框
|
||||
elTableBody.scrollTop += scrollStep
|
||||
|
||||
// 如果滚动到底部,回到顶部继续滚动
|
||||
if (elTableBody.scrollTop + elTableBody.clientHeight >= elTableBody.scrollHeight) {
|
||||
elTableBody.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000) // 每2秒滚动一次
|
||||
},
|
||||
handleResize() {
|
||||
const windowHeight = window.innerHeight
|
||||
this.tableHeight = Math.max(windowHeight * 0.7, 500) + 'px'
|
||||
this.tableHeight = Math.max(windowHeight * 0.65, 450) + 'px' // 增加表格高度,突出表格展示
|
||||
|
||||
// 调整图表大小
|
||||
if (this.inventoryChart) {
|
||||
this.inventoryChart.resize()
|
||||
}
|
||||
if (this.inventoryRankChart) {
|
||||
this.inventoryRankChart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wm-blank-inventory-card {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.middle-data {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 15px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.middle-data span {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.charts-wrapper {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
/* 突出表格显示 */
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
/* 优化表格样式 */
|
||||
.el-table__header-wrapper th {
|
||||
background-color: #f0f9ff !important;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 12px 5px;
|
||||
border-bottom: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper td {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 12px 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 优化斑马纹效果 */
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 高亮当前行 */
|
||||
.el-table__body tr:hover>td {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.wm-blank-inventory-card {
|
||||
padding: 10px;
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
<template>
|
||||
<div class="workorder-online-card">
|
||||
<div style="text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 10px;">生产计划工单</div>
|
||||
|
||||
<!-- 今日统计数据区域 -->
|
||||
<div class="statistics-container">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">今日总计划数:</span>
|
||||
<span class="stat-value">{{ todayStatistics.totalPlan }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已完成计划数:</span>
|
||||
<span class="stat-value completed">{{ todayStatistics.completedPlan }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">未完成计划数:</span>
|
||||
<span class="stat-value uncompleted">{{ todayStatistics.uncompletedPlan }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">今日总投入数:</span>
|
||||
<span class="stat-value">{{ todayStatistics.totalInput }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">今日总合格数:</span>
|
||||
<span class="stat-value qualified">{{ todayStatistics.totalQualified }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单完成状态分布环形进度图 -->
|
||||
<div class="chart-container">
|
||||
<div id="workorderStatusChart" style="width: 100%; height: 200px;"></div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="workorderData"
|
||||
:data="workorderOnlineTable"
|
||||
v-loading="loading"
|
||||
border
|
||||
style="width: 100%"
|
||||
:height="tableHeight"
|
||||
ref="workorderTable"
|
||||
stripe
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column type="index" width="60"></el-table-column>
|
||||
<el-table-column prop="clientWorkorder" label="工单号" width="130"></el-table-column>
|
||||
@@ -30,67 +63,333 @@
|
||||
|
||||
<script>
|
||||
import { getWorkOrderCarouselBoardData } from '@/api/kanbanManagement/carouselBoard.js'
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'WorkorderOnlineCard',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
workorderData: [],
|
||||
tableHeight: '700px'
|
||||
}
|
||||
},
|
||||
return {
|
||||
loading: false,
|
||||
workorderOnlineTable: [],
|
||||
tableHeight: '500px', // 调整表格高度,为图表留出空间
|
||||
realTotal: 0,
|
||||
workorderChart: null,
|
||||
tableScrollTimer: null,
|
||||
// 今日统计数据
|
||||
todayStatistics: {
|
||||
totalPlan: 0, // 今日总计划数
|
||||
completedPlan: 0, // 已完成计划数
|
||||
uncompletedPlan: 0, // 未完成计划数
|
||||
totalInput: 0, // 今日总投入数
|
||||
totalQualified: 0 // 今日总合格数
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getWorkorderData()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
|
||||
updated() {
|
||||
// 数据更新后初始化图表
|
||||
if (this.workorderOnlineTable.length > 0 && !this.workorderChart) {
|
||||
this.$nextTick(() => {
|
||||
this.initWorkorderChart()
|
||||
this.startTableAutoScroll()
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
// 清理图表实例
|
||||
if (this.workorderChart) {
|
||||
this.workorderChart.dispose()
|
||||
this.workorderChart = null
|
||||
}
|
||||
// 清理表格滚动定时器
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
this.tableScrollTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWorkorderData() {
|
||||
this.loading = true
|
||||
// 获取今天的数据
|
||||
const today = this.$dayjs ? this.$dayjs() : new Date()
|
||||
let year, week, date
|
||||
|
||||
if (this.$dayjs) {
|
||||
year = today.year()
|
||||
week = today.week()
|
||||
// 将0(周日)转为7
|
||||
date = '' + (today.day() === 0 ? 7 : today.day())
|
||||
} else {
|
||||
// 兼容不使用dayjs的情况
|
||||
const now = new Date()
|
||||
year = now.getFullYear()
|
||||
// 简化的周计算
|
||||
const onejan = new Date(now.getFullYear(), 0, 1)
|
||||
week = Math.ceil((((now - onejan) / 86400000) + onejan.getDay() + 1) / 7)
|
||||
date = '' + (now.getDay() === 0 ? 7 : now.getDay())
|
||||
}
|
||||
|
||||
const query = {
|
||||
|
||||
// 设置默认查询参数
|
||||
let query = {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
}
|
||||
|
||||
getWorkOrderCarouselBoardData(query)
|
||||
.then((res) => {
|
||||
if (res.code == 200) {
|
||||
this.workorderData = res.data || []
|
||||
this.workorderOnlineTable = res.data || []
|
||||
this.realTotal = this.workorderOnlineTable.length
|
||||
|
||||
// 计算今日统计数据
|
||||
this.calculateTodayStatistics()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 计算今日统计数据
|
||||
calculateTodayStatistics() {
|
||||
// 重置统计数据
|
||||
this.todayStatistics = {
|
||||
totalPlan: 0,
|
||||
completedPlan: 0,
|
||||
uncompletedPlan: 0,
|
||||
totalInput: 0,
|
||||
totalQualified: 0
|
||||
}
|
||||
|
||||
// 统计工单数据
|
||||
this.workorderOnlineTable.forEach(item => {
|
||||
// 假设vehicleNumber是计划数
|
||||
const planCount = Number(item.vehicleNumber) || 0
|
||||
this.todayStatistics.totalPlan += planCount
|
||||
|
||||
// 统计已完成和未完成计划
|
||||
if (item.status === 2) { // 已完成
|
||||
this.todayStatistics.completedPlan += planCount
|
||||
} else {
|
||||
this.todayStatistics.uncompletedPlan += planCount
|
||||
}
|
||||
|
||||
// 假设previousNumber是投入数
|
||||
this.todayStatistics.totalInput += Number(item.previousNumber) || 0
|
||||
|
||||
// 假设合格数为投入数的90%(实际应从API获取)
|
||||
const qualifiedCount = Math.round((Number(item.previousNumber) || 0) * 0.9)
|
||||
this.todayStatistics.totalQualified += qualifiedCount
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化工单状态分布环形进度图
|
||||
initWorkorderChart() {
|
||||
const chartDom = document.getElementById('workorderStatusChart')
|
||||
if (!chartDom) return
|
||||
|
||||
this.workorderChart = echarts.init(chartDom)
|
||||
|
||||
// 准备环形图数据
|
||||
const statusCounts = {
|
||||
'已完成': 0,
|
||||
'进行中': 0,
|
||||
'未开始': 0,
|
||||
'已暂停': 0
|
||||
}
|
||||
|
||||
// 根据工单状态统计数量
|
||||
this.workorderOnlineTable.forEach(item => {
|
||||
switch (item.status) {
|
||||
case 2: // 已完成
|
||||
statusCounts['已完成']++
|
||||
break
|
||||
case 1: // 进行中
|
||||
statusCounts['进行中']++
|
||||
break
|
||||
default: // 未开始
|
||||
statusCounts['未开始']++
|
||||
}
|
||||
})
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '工单状态分布',
|
||||
left: 'center',
|
||||
top: 0
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: 0
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '工单状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'], // 环形图
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{d}%'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '16',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{ value: statusCounts['已完成'], name: '已完成', itemStyle: { color: '#67c23a' } },
|
||||
{ value: statusCounts['进行中'], name: '进行中', itemStyle: { color: '#409EFF' } },
|
||||
{ value: statusCounts['未开始'], name: '未开始', itemStyle: { color: '#909399' } }
|
||||
].filter(item => item.value > 0) // 过滤掉数量为0的状态
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.workorderChart.setOption(option)
|
||||
|
||||
// 监听窗口大小变化,调整图表大小
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.workorderChart) {
|
||||
this.workorderChart.resize()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 表格自动滚动功能
|
||||
startTableAutoScroll() {
|
||||
if (this.tableScrollTimer) {
|
||||
clearInterval(this.tableScrollTimer)
|
||||
}
|
||||
|
||||
this.tableScrollTimer = setInterval(() => {
|
||||
if (this.$refs.workorderTable) {
|
||||
const elTableBody = this.$refs.workorderTable.$el.querySelector('.el-table__body-wrapper')
|
||||
if (elTableBody) {
|
||||
// 每次滚动一行的高度
|
||||
const scrollStep = 31 // 行高 + 边框
|
||||
elTableBody.scrollTop += scrollStep
|
||||
|
||||
// 如果滚动到底部,回到顶部继续滚动
|
||||
if (elTableBody.scrollTop + elTableBody.clientHeight >= elTableBody.scrollHeight) {
|
||||
elTableBody.scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2000) // 每2秒滚动一次
|
||||
},
|
||||
handleResize() {
|
||||
const windowHeight = window.innerHeight
|
||||
this.tableHeight = Math.max(windowHeight * 0.7, 500) + 'px'
|
||||
this.tableHeight = Math.max(windowHeight * 0.5, 400) + 'px' // 为图表留出空间
|
||||
|
||||
// 调整图表大小
|
||||
if (this.workorderChart) {
|
||||
this.workorderChart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.workorder-online-card {
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 今日统计数据样式 */
|
||||
.statistics-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-value.completed {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.stat-value.uncompleted {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-value.qualified {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-bottom: 15px;
|
||||
flex-shrink: 0;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 优化表格样式 */
|
||||
.el-table__header-wrapper th {
|
||||
background-color: #e6f7ff !important;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper td {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
padding: 10px 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 优化斑马纹效果 */
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* 高亮当前行 */
|
||||
.el-table__body tr:hover>td {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.workorder-online-card {
|
||||
padding: 10px;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="carousel-board-container">
|
||||
<el-carousel
|
||||
ref="carousel"
|
||||
:interval="10000"
|
||||
height="95vh"
|
||||
:interval="60000"
|
||||
height="90vh"
|
||||
arrow="never"
|
||||
indicator-position="bottom"
|
||||
autoplay
|
||||
@@ -64,17 +64,36 @@ export default {
|
||||
<style scoped>
|
||||
.carousel-board-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
height: 100vh;
|
||||
padding: 15px;
|
||||
background-color: #f0f2f5;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dashboard-carousel {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 隐藏轮播指示器,因为在大屏幕看板上可能不需要 */
|
||||
/* 优化轮播项样式 */
|
||||
.el-carousel__item {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 确保指示器清晰可见 */
|
||||
.el-carousel__indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-carousel__indicator--active {
|
||||
width: 24px;
|
||||
background-color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user