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.

293 lines
6.6 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.feedingPipelineName }}</td>
<td :style="{ color: colors.cyan }">{{ r.code }}</td>
<td :title="r.productName">{{ r.productName }}</td>
<td>{{ r.planNumber }}</td>
<td>{{ r.wangongNumber }}</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 }">{{ r.passRate }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { PlanApi, PlanVO } from '@/api/mes/plan'
const route = useRoute()
const orgId = route.query.orgId
const colors = {
blue: '#1e90ff',
cyan: '#22d3ee',
green: '#22c55e',
purple: '#8b5cf6',
warn: '#f59e0b',
danger: '#ef4444'
}
const tasks = ref<PlanVO[]>([])
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(async () => {
setBodyHeight()
startScroll()
window.addEventListener('resize', setBodyHeight)
tasks.value = await PlanApi.getProductPlans({ orgId })
// console.log(tasks.value)
})
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>