| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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, inject, watch } 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 stopProgressTableScroll = () => { |
| | | if (progressTableScrollTimer.value) { |
| | | cancelAnimationFrame(progressTableScrollTimer.value) |
| | | progressTableScrollTimer.value = null |
| | | } |
| | | const tableContainer = progressTableRef.value |
| | | if (tableContainer?._pauseTimer) { |
| | | clearInterval(tableContainer._pauseTimer) |
| | | tableContainer._pauseTimer = null |
| | | } |
| | | } |
| | | |
| | | const initProgressTableScroll = () => { |
| | | const tableContainer = progressTableRef.value |
| | | if (!tableContainer) return |
| | | stopProgressTableScroll() |
| | | 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) => { |
| | | stopProgressTableScroll() |
| | | 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) |
| | | }) |
| | | } |
| | | |
| | | const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null) |
| | | if (dataDashboardRefreshTick) { |
| | | watch(dataDashboardRefreshTick, () => { |
| | | progressStatisticsInfo() |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | progressStatisticsInfo() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | stopProgressTableScroll() |
| | | 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: 320px; |
| | | 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> |