You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
7.3 KiB
Vue
297 lines
7.3 KiB
Vue
<template>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title">
|
|
<span class="card-title-icon">
|
|
<Icon icon="fa-solid:table-list" />
|
|
</span>
|
|
<span>产线任务看板</span>
|
|
</div>
|
|
<div class="legend-inline">
|
|
<span class="tag">实时刷新 · 滚动展示</span>
|
|
<span class="dot" style="background: var(--green);"></span> 已完成
|
|
<span class="dot" style="background: var(--warn);"></span> 进度偏低
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-shell">
|
|
<table class="task-table">
|
|
<thead>
|
|
<tr>
|
|
<th>产线名称</th>
|
|
<th>排产单</th>
|
|
<th>产品名称</th>
|
|
<th>计划数量</th>
|
|
<th>完工数量</th>
|
|
<th>完工率</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody ref="tbodyRef">
|
|
<tr v-for="(r, index) in tasks" :key="index">
|
|
<td>{{ r.line }}</td>
|
|
<td :style="{ color: colors.cyan }">{{ r.order }}</td>
|
|
<td :title="r.product">{{ r.product }}</td>
|
|
<td>{{ r.plan }}</td>
|
|
<td>{{ r.done }}</td>
|
|
<td>
|
|
<div style="display:flex; flex-direction:column; gap:4px; align-items:center;">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" :style="{ width: getRate(r) + '%', background: getRateColor(r) }"></div>
|
|
</div>
|
|
<span :style="{ color: getRateColor(r), fontWeight: 700 }">{{ getRate(r) }}%</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
const colors = {
|
|
blue: '#1e90ff',
|
|
cyan: '#22d3ee',
|
|
green: '#22c55e',
|
|
purple: '#8b5cf6',
|
|
warn: '#f59e0b',
|
|
danger: '#ef4444'
|
|
}
|
|
|
|
const tasks = [
|
|
{ line: '产线A', order: 'ORD-2025121701', product: '智能模组X1', plan: 1200, done: 1080 },
|
|
{ line: '产线B', order: 'ORD-2025121702', product: '高密电芯M2', plan: 1500, done: 1120 },
|
|
{ line: '产线C', order: 'ORD-2025121703', product: '组装组件A5', plan: 800, done: 760 },
|
|
{ line: '产线D', order: 'ORD-2025121704', product: '精密外壳S7', plan: 600, done: 480 },
|
|
{ line: '产线E', order: 'ORD-2025121705', product: '控制板K9', plan: 900, done: 855 },
|
|
{ line: '产线F', order: 'ORD-2025121706', product: '电源模组P3', plan: 1100, done: 990 },
|
|
{ line: '产线G', order: 'ORD-2025121707', product: '线束套件W4', plan: 700, done: 560 },
|
|
{ line: '产线H', order: 'ORD-2025121708', product: '散热组件H2', plan: 650, done: 559 },
|
|
{ line: '产线I', order: 'ORD-2025121709', product: '传感器C8', plan: 500, done: 450 },
|
|
{ line: '产线J', order: 'ORD-2025121710', product: '整机Z3', plan: 1000, done: 800 }
|
|
]
|
|
|
|
const getRate = (r: any) => Math.round((r.done / r.plan) * 100)
|
|
|
|
const getRateColor = (r: any) => {
|
|
const rate = getRate(r)
|
|
if (rate < 50) return colors.danger
|
|
else if (rate < 90) return colors.warn
|
|
return colors.green
|
|
}
|
|
|
|
const tbodyRef = ref<HTMLElement | null>(null)
|
|
let scrollTimer: number | undefined
|
|
|
|
const setBodyHeight = () => {
|
|
if (!tbodyRef.value) return
|
|
// Wait for render
|
|
setTimeout(() => {
|
|
if (!tbodyRef.value) return
|
|
const sample = tbodyRef.value.querySelector('tr') as HTMLElement | null
|
|
const rh = sample ? sample.getBoundingClientRect().height : 40
|
|
tbodyRef.value.style.height = `${Math.round(rh * 6)}px`
|
|
}, 100)
|
|
}
|
|
|
|
const startScroll = () => {
|
|
scrollTimer = window.setInterval(() => {
|
|
if (!tbodyRef.value) return
|
|
const first = tbodyRef.value.firstElementChild as HTMLElement | null
|
|
if (!first) return
|
|
first.style.transition = 'all 0.35s'
|
|
first.style.transform = 'translateY(-48px)'
|
|
first.style.opacity = '0'
|
|
window.setTimeout(() => {
|
|
if (!tbodyRef.value) return
|
|
first.style.transition = 'none'
|
|
first.style.transform = 'translateY(0)'
|
|
first.style.opacity = '1'
|
|
tbodyRef.value.appendChild(first)
|
|
}, 360)
|
|
}, 5000)
|
|
}
|
|
|
|
onMounted(() => {
|
|
setBodyHeight()
|
|
startScroll()
|
|
window.addEventListener('resize', setBodyHeight)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (scrollTimer) clearInterval(scrollTimer)
|
|
window.removeEventListener('resize', setBodyHeight)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card {
|
|
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(30,64,175,0.85);
|
|
box-shadow:
|
|
0 18px 45px rgba(15,23,42,0.95),
|
|
0 0 0 1px rgba(15,23,42,1),
|
|
inset 0 0 0 1px rgba(56,189,248,0.05);
|
|
padding: 10px 10px 10px 10px;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card::before,
|
|
.card::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 13px;
|
|
height: 13px;
|
|
border-radius: 2px;
|
|
border: 1px solid rgba(56,189,248,0.75);
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card::before {
|
|
top: -1px;
|
|
left: -1px;
|
|
border-right: none;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.card::after {
|
|
right: -1px;
|
|
bottom: -1px;
|
|
border-left: none;
|
|
border-top: none;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
padding-bottom: 4px;
|
|
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
|
|
}
|
|
|
|
.card-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #e5f0ff;
|
|
}
|
|
|
|
.card-title-icon {
|
|
color: #22d3ee;
|
|
}
|
|
|
|
.card-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.legend-inline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 10px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.tag {
|
|
border-radius: 999px;
|
|
padding: 2px 6px;
|
|
font-size: 10px;
|
|
border: 1px solid rgba(148,163,184,0.4);
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Scoped styles specific to this component but we expect common styles from parent */
|
|
.table-shell {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.task-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0 5px;
|
|
table-layout: fixed;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.task-table thead {
|
|
background: radial-gradient(circle at 0 0, rgba(56, 189, 248, 0.18), transparent 70%);
|
|
}
|
|
|
|
.task-table thead th {
|
|
padding: 6px 4px;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
text-align: center;
|
|
border-bottom: 1px solid rgba(51, 65, 85, 0.9);
|
|
}
|
|
|
|
.task-table tbody {
|
|
display: block;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.task-table thead tr,
|
|
.task-table tbody tr {
|
|
display: table;
|
|
width: 100%;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.task-table tbody td {
|
|
padding: 6px 4px;
|
|
text-align: center;
|
|
color: var(--text);
|
|
background: rgba(15, 23, 42, 0.88);
|
|
border-radius: 4px;
|
|
border: 1px solid rgba(30, 64, 175, 0.7);
|
|
}
|
|
|
|
.task-table tbody tr:nth-child(even) td {
|
|
background: rgba(15, 23, 42, 0.96);
|
|
}
|
|
|
|
.task-table tbody tr:hover td {
|
|
border-color: rgba(56, 189, 248, 0.95);
|
|
box-shadow: 0 0 14px rgba(56, 189, 248, 0.45);
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 6px;
|
|
background: rgba(15, 23, 42, 1);
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(148, 163, 184, 0.45);
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
transition: width 0.4s ease-out;
|
|
}
|
|
</style>
|