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

<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>