<template>
|
<div>
|
<PanelHeader title="生产订单完成进度" />
|
<div class="main-panel">
|
<div class="panel-item-customers">
|
<CarouselCards :items="cardItems" :visible-count="4" />
|
<div
|
class="progress-table-container"
|
ref="progressTableRef"
|
style="margin-top: 0px;"
|
@scroll="handleTableScroll"
|
>
|
<table class="progress-table">
|
<thead>
|
<tr>
|
<th>生产订单号</th>
|
<th>产品名称</th>
|
<th>规格</th>
|
<th>需求数量</th>
|
<th>完成数量</th>
|
<th>完成进度</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr
|
v-for="(item, index) in progressTableData"
|
:key="index"
|
:ref="(el) => setRowRef(el, index)"
|
:class="{ 'row-under-header': isRowUnderHeader(index) }"
|
>
|
<td>{{ item.npsNo || '-' }}</td>
|
<td>{{ item.productCategory || '-' }}</td>
|
<td>{{ item.specificationModel || '-' }}</td>
|
<td>{{ item.quantity || 0 }}</td>
|
<td>{{ item.completeQuantity || 0 }}</td>
|
<td>
|
<el-progress
|
:percentage="calculateProgress(item)"
|
:color="progressColor(calculateProgress(item))"
|
:status="calculateProgress(item) >= 100 ? 'success' : ''"
|
:stroke-width="8"
|
/>
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import { getProgressStatistics } from '@/api/viewIndex.js'
|
import PanelHeader from './PanelHeader.vue'
|
import CarouselCards from './CarouselCards.vue'
|
|
const progressTableRef = ref(null)
|
const progressTableScrollTimer = ref(null)
|
const tableScrollTimeout = ref(null)
|
const tableRowRefs = ref([])
|
const rowsUnderHeader = ref(new Set())
|
|
// 订单统计对象
|
const orderStatisticsObject = ref({
|
totalOrderCount: 0,
|
uncompletedOrderCount: 0,
|
partialCompletedOrderCount: 0,
|
completedOrderCount: 0,
|
})
|
|
// 轮播卡片数据(由 orderStatisticsObject 同步)
|
const cardItems = ref([])
|
|
// 生产订单完成进度表格数据
|
const progressTableData = ref([])
|
|
// 计算完成进度百分比
|
const calculateProgress = (item) => {
|
if (!item) return 0
|
if (item.completionStatus !== undefined && item.completionStatus !== null) {
|
const percentage = Number(item.completionStatus)
|
if (isNaN(percentage)) return 0
|
return Math.min(Math.max(Math.round(percentage), 0), 100)
|
}
|
if (!item.quantity || item.quantity === 0) return 0
|
const percentage = ((item.completeQuantity || 0) / item.quantity) * 100
|
return Math.min(Math.max(Math.round(percentage), 0), 100)
|
}
|
|
// 根据进度百分比返回颜色
|
const progressColor = (percentage) => {
|
const p = percentage || 0
|
if (p < 30) return '#f56c6c'
|
if (p < 50) return '#e6a23c'
|
if (p < 80) return '#409eff'
|
return '#67c23a'
|
}
|
|
const setRowRef = (el, index) => {
|
if (el) {
|
tableRowRefs.value[index] = el
|
}
|
}
|
|
const isRowUnderHeader = (index) => rowsUnderHeader.value.has(index)
|
|
const handleTableScroll = () => {
|
const tableContainer = progressTableRef.value
|
if (!tableContainer) return
|
const thead = tableContainer.querySelector('thead')
|
if (!thead) return
|
const theadHeight = thead.offsetHeight
|
const containerRect = tableContainer.getBoundingClientRect()
|
const containerTop = containerRect.top
|
const theadBottom = containerTop + theadHeight
|
rowsUnderHeader.value.clear()
|
tableRowRefs.value.forEach((row, index) => {
|
if (row) {
|
const rowRect = row.getBoundingClientRect()
|
const rowTop = rowRect.top
|
const rowBottom = rowRect.bottom
|
if (rowTop < theadBottom && rowBottom > containerTop) {
|
rowsUnderHeader.value.add(index)
|
}
|
}
|
})
|
if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
|
tableScrollTimeout.value = setTimeout(() => {
|
rowsUnderHeader.value.clear()
|
}, 150)
|
}
|
|
const initProgressTableScroll = () => {
|
const tableContainer = progressTableRef.value
|
if (!tableContainer) return
|
if (progressTableScrollTimer.value) {
|
cancelAnimationFrame(progressTableScrollTimer.value)
|
progressTableScrollTimer.value = null
|
}
|
if (tableContainer._pauseTimer) {
|
clearInterval(tableContainer._pauseTimer)
|
tableContainer._pauseTimer = null
|
}
|
const tbody = tableContainer.querySelector('tbody')
|
if (!tbody) return
|
const originalCount = progressTableData.value.length
|
const allRows = Array.from(tbody.querySelectorAll('tr'))
|
if (allRows.length > originalCount) {
|
for (let i = originalCount; i < allRows.length; i++) {
|
allRows[i].remove()
|
}
|
}
|
const scrollItems = Array.from(tbody.querySelectorAll('tr'))
|
if (scrollItems.length === 0) return
|
const originalItemCount = scrollItems.length
|
const thead = tableContainer.querySelector('thead')
|
const theadHeight = thead ? thead.offsetHeight : 40
|
const containerHeight = tableContainer.clientHeight
|
const visibleHeight = containerHeight - theadHeight
|
const itemHeight = scrollItems[0]?.offsetHeight || 40
|
const totalContentHeight = itemHeight * originalItemCount
|
if (totalContentHeight <= visibleHeight) return
|
const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
|
for (let i = 0; i < cloneCount; i++) {
|
const clone = scrollItems[i % originalItemCount].cloneNode(true)
|
tbody.appendChild(clone)
|
}
|
let scrollPosition = 0
|
const scrollSpeed = 1.5
|
const pauseTime = 3000
|
let isPaused = false
|
let lastTimestamp = 0
|
function scrollAnimation(timestamp) {
|
if (!lastTimestamp) lastTimestamp = timestamp
|
const deltaTime = timestamp - lastTimestamp
|
lastTimestamp = timestamp
|
if (!isPaused) {
|
scrollPosition += scrollSpeed * (deltaTime / 16)
|
const maxScroll = itemHeight * originalItemCount
|
if (scrollPosition >= maxScroll) {
|
scrollPosition = 0
|
tableContainer.scrollTop = 0
|
} else {
|
tableContainer.scrollTop = scrollPosition
|
}
|
}
|
progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
|
}
|
progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
|
const pauseTimer = setInterval(() => {
|
isPaused = !isPaused
|
}, pauseTime)
|
tableContainer._pauseTimer = pauseTimer
|
}
|
|
const progressStatisticsInfo = () => {
|
getProgressStatistics()
|
.then((res) => {
|
if (!res || !res.data) return
|
const obj = {
|
totalOrderCount: res.data.totalOrderCount || 0,
|
uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
|
partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
|
completedOrderCount: res.data.completedOrderCount || 0,
|
}
|
orderStatisticsObject.value = obj
|
cardItems.value = [
|
{ label: '总订单数', value: obj.totalOrderCount, unit: '件' },
|
{ label: '未完成订单数', value: obj.uncompletedOrderCount, unit: '件' },
|
{ label: '部分完成订单数', value: obj.partialCompletedOrderCount, unit: '件' },
|
{ label: '已完成订单数', value: obj.completedOrderCount, unit: '件' },
|
]
|
progressTableData.value = res.data.completedOrderDetails || []
|
tableRowRefs.value = []
|
rowsUnderHeader.value.clear()
|
nextTick(() => {
|
initProgressTableScroll()
|
})
|
})
|
.catch((err) => {
|
console.error('获取生产订单完成进度统计失败:', err)
|
})
|
}
|
|
onMounted(() => {
|
progressStatisticsInfo()
|
})
|
|
onBeforeUnmount(() => {
|
if (progressTableScrollTimer.value) {
|
cancelAnimationFrame(progressTableScrollTimer.value)
|
}
|
if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
|
const tableContainer = progressTableRef.value
|
if (tableContainer?._pauseTimer) {
|
clearInterval(tableContainer._pauseTimer)
|
}
|
})
|
</script>
|
|
<style scoped>
|
.main-panel {
|
display: flex;
|
flex-direction: column;
|
gap: 20px;
|
}
|
|
.panel-item-customers {
|
border: 1px solid #1a58b0;
|
padding: 18px;
|
width: 100%;
|
height: 428px;
|
}
|
|
.progress-table-container {
|
height: 280px;
|
overflow-y: auto;
|
overflow-x: hidden;
|
margin-top: 10px;
|
scrollbar-width: none;
|
-ms-overflow-style: none;
|
}
|
|
.progress-table-container::-webkit-scrollbar {
|
display: none;
|
}
|
|
.progress-table {
|
width: 100%;
|
border-collapse: collapse;
|
color: #b8c8e0;
|
font-size: 12px;
|
table-layout: fixed;
|
}
|
|
.progress-table thead {
|
position: sticky;
|
top: 0;
|
background-color: rgba(26, 88, 176, 0.9);
|
z-index: 10;
|
}
|
|
.progress-table th {
|
padding: 8px 6px;
|
text-align: left;
|
font-weight: 500;
|
border-bottom: 1px solid rgba(184, 200, 224, 0.3);
|
color: #b8c8e0;
|
font-size: 12px;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.progress-table th:nth-child(1) {
|
width: 15%;
|
}
|
|
.progress-table th:nth-child(2) {
|
width: 15%;
|
}
|
|
.progress-table th:nth-child(3) {
|
width: 15%;
|
}
|
|
.progress-table th:nth-child(4) {
|
width: 12%;
|
}
|
|
.progress-table th:nth-child(5) {
|
width: 12%;
|
}
|
|
.progress-table th:nth-child(6) {
|
width: 31%;
|
}
|
|
.progress-table td {
|
padding: 8px 6px;
|
border-bottom: 1px solid rgba(184, 200, 224, 0.1);
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 12px;
|
transition: opacity 0.3s ease;
|
}
|
|
.progress-table tbody tr:hover {
|
background-color: rgba(184, 200, 224, 0.1);
|
}
|
|
.progress-table tbody tr.row-under-header {
|
opacity: 0.5;
|
}
|
|
.progress-table :deep(.el-progress) {
|
width: 100%;
|
}
|
|
.progress-table :deep(.el-progress-bar__outer) {
|
background-color: rgba(184, 200, 224, 0.2);
|
}
|
|
.progress-table :deep(.el-progress__text) {
|
color: #b8c8e0;
|
font-size: 11px;
|
}
|
</style>
|