feat(看板管理): 优化轮播看板组件样式和功能

- 调整轮播间隔和高度,优化视觉体验
- 设备监控卡片增加数据加载状态和定时刷新功能
- 工单在线卡片添加统计数据和状态分布图表
- 毛坯库存卡片增加库存类别和数量排名图表
- 质量统计卡片添加质量状态分布和合格率排名图表
- 所有卡片表格增加自动滚动功能
This commit is contained in:
2025-10-24 20:20:57 +08:00
parent 450e3911ec
commit 89e07ce7e2
5 changed files with 1327 additions and 183 deletions

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>