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.
280 lines
6.2 KiB
Vue
280 lines
6.2 KiB
Vue
<template>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title">
|
|
<span class="card-title-icon">
|
|
<Icon icon="fa-solid:sun" />
|
|
</span>
|
|
<span>日产能达成情况</span>
|
|
</div>
|
|
<span class="tag">当日维度</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="card-body-row">
|
|
<div class="card-body-col" style="flex: 0 0 52%;">
|
|
<div ref="chartRef" class="chart"></div>
|
|
</div>
|
|
<div class="card-body-col">
|
|
<div class="metrics-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-label">排产单数量</div>
|
|
<div class="metric-value" ref="dayOrderEl"></div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">已排产数量</div>
|
|
<div class="metric-value" ref="dayPlanEl"></div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">已生产数量</div>
|
|
<div class="metric-value ok" ref="dayPendingEl"></div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">产能合格率</div>
|
|
<div class="metric-value accent" ref="dayRateEl"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import * as echarts from 'echarts'
|
|
import { colors, animateCount, style } from '../utils'
|
|
import { PlanApi } from '@/api/mes/plan'
|
|
|
|
const route = useRoute()
|
|
const orgId = route.query.orgId
|
|
|
|
type CapacityData = {
|
|
orders: number
|
|
scheduled: number
|
|
produced: number
|
|
rate: number
|
|
}
|
|
|
|
const day: CapacityData = {
|
|
orders: 0,
|
|
scheduled: 0,
|
|
produced: 0,
|
|
rate: 0
|
|
}
|
|
|
|
const chartRef = ref<HTMLElement | null>(null)
|
|
const dayOrderEl = ref<HTMLElement | null>(null)
|
|
const dayPlanEl = ref<HTMLElement | null>(null)
|
|
const dayPendingEl = ref<HTMLElement | null>(null)
|
|
const dayRateEl = ref<HTMLElement | null>(null)
|
|
|
|
let chart: echarts.ECharts | null = null
|
|
|
|
const mapCapacity = (raw: any): CapacityData => {
|
|
const orderCount = Number(raw?.orders ?? raw?.order ?? 0)
|
|
const scheduled = Number(raw?.plan ?? 0)
|
|
const produced = Number(raw?.pending ?? 0)
|
|
const rate = Number(raw?.rate ?? raw?.passRate ?? raw?.qualifiedRate ?? 0)
|
|
return { orders: orderCount, scheduled, produced, rate }
|
|
}
|
|
|
|
const loadDayCapacity = async () => {
|
|
try {
|
|
const raw = await PlanApi.getPlanCapacity(1, orgId)
|
|
const mapped = mapCapacity(raw || {})
|
|
day.orders = mapped.orders
|
|
day.scheduled = mapped.scheduled
|
|
day.produced = mapped.produced
|
|
day.rate = mapped.rate
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
const initChart = () => {
|
|
if (!chartRef.value) return
|
|
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
|
|
chart.setOption({
|
|
backgroundColor: 'transparent',
|
|
tooltip: { trigger: 'item' },
|
|
legend: { bottom: '0%', left: 'center', textStyle: style.legendText },
|
|
series: [
|
|
{
|
|
type: 'pie',
|
|
radius: ['60%', '82%'],
|
|
center: ['50%', '55%'],
|
|
label: { show: false },
|
|
emphasis: { scale: false },
|
|
data: [
|
|
{ value: day.scheduled, name: '已排产', itemStyle: { color: colors.cyan } },
|
|
{ value: day.produced, name: '已生产', itemStyle: { color: colors.green } }
|
|
]
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
const resizeHandler = () => {
|
|
chart?.resize()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadDayCapacity()
|
|
initChart()
|
|
animateCount(dayOrderEl.value, day.orders, '', 900)
|
|
animateCount(dayPlanEl.value, day.scheduled, '', 900)
|
|
animateCount(dayPendingEl.value, day.produced, '', 900)
|
|
animateCount(dayRateEl.value, day.rate, '%', 900)
|
|
window.addEventListener('resize', resizeHandler)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', resizeHandler)
|
|
chart?.dispose()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card {
|
|
position: relative;
|
|
display: flex;
|
|
padding: 10px;
|
|
overflow: hidden;
|
|
background: linear-gradient(135deg, rgb(15 23 42 / 96%), rgb(15 23 42 / 88%));
|
|
border: 1px solid rgb(30 64 175 / 85%);
|
|
border-radius: 10px;
|
|
box-shadow:
|
|
0 18px 45px rgb(15 23 42 / 95%),
|
|
0 0 0 1px rgb(15 23 42 / 100%),
|
|
inset 0 0 0 1px rgb(56 189 248 / 5%);
|
|
flex-direction: column;
|
|
}
|
|
|
|
.card::before,
|
|
.card::after {
|
|
position: absolute;
|
|
width: 13px;
|
|
height: 13px;
|
|
pointer-events: none;
|
|
border: 1px solid rgb(56 189 248 / 75%);
|
|
border-radius: 2px;
|
|
content: "";
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.card::before {
|
|
top: -1px;
|
|
left: -1px;
|
|
border-right: none;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.card::after {
|
|
right: -1px;
|
|
bottom: -1px;
|
|
border-top: none;
|
|
border-left: none;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
padding-bottom: 4px;
|
|
margin-bottom: 8px;
|
|
border-bottom: 1px solid rgb(41 54 95 / 90%);
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.card-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #e5f0ff;
|
|
}
|
|
|
|
.card-title-icon {
|
|
color: #22d3ee;
|
|
}
|
|
|
|
.card-body {
|
|
display: flex;
|
|
min-height: 0;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.card-body-row {
|
|
flex: 1;
|
|
display: flex;
|
|
gap: 8px;
|
|
min-height: 0;
|
|
}
|
|
|
|
.card-body-col {
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.tag {
|
|
padding: 2px 6px;
|
|
font-size: 10px;
|
|
color: #94a3b8;
|
|
border: 1px solid rgb(148 163 184 / 40%);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
grid-auto-rows: 1fr;
|
|
gap: 6px;
|
|
}
|
|
|
|
.metric-card {
|
|
display: flex;
|
|
padding: 6px 8px;
|
|
background: radial-gradient(circle at 0 0, rgb(56 189 248 / 16%), transparent 70%);
|
|
border: 1px solid rgb(30 64 175 / 90%);
|
|
border-radius: 8px;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 20px;
|
|
font-weight: 800;
|
|
color: #e5f0ff;
|
|
}
|
|
|
|
.metric-value.accent { color: #22d3ee; }
|
|
|
|
.metric-value.warn { color: #f59e0b; }
|
|
|
|
.metric-value.ok { color: #22c55e; }
|
|
|
|
.metric-extra {
|
|
display: flex;
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.chart {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 180px;
|
|
}
|
|
|
|
@media (width <= 1600px) {
|
|
.chart { min-height: 160px; }
|
|
}
|
|
</style>
|