<template>
|
<HomePanel title="工序生产统计" subtitle="投入 · 报废 · 产出" :loading="loading">
|
<template #extra>
|
<el-button size="small" plain @click="openDialog">选择工序</el-button>
|
<el-button size="small" plain @click="resetFilter">重置</el-button>
|
<el-radio-group v-model="range" size="small" @change="refresh">
|
<el-radio-button :value="1">日</el-radio-button>
|
<el-radio-button :value="2">周</el-radio-button>
|
<el-radio-button :value="3">月</el-radio-button>
|
</el-radio-group>
|
</template>
|
|
<div class="process">
|
<div class="process__chart">
|
<Echarts
|
:chartStyle="{ width: '100%', height: '100%' }"
|
:grid="grid"
|
:series="series"
|
:tooltip="tooltip"
|
:xAxis="xAxis"
|
:yAxis="yAxis"
|
style="height: 100%"
|
@click="handleChartClick"
|
/>
|
</div>
|
|
<aside class="process__aside">
|
<div class="process__legend">
|
<span class="process__legend-item"><i class="dot" style="background: #2c51d9"></i>投入量</span>
|
<span class="process__legend-item"><i class="dot" style="background: #d97706"></i>报废量</span>
|
<span class="process__legend-item"><i class="dot" style="background: #0f9960"></i>产出量</span>
|
</div>
|
<div class="process__name">{{ aside.processName }}</div>
|
<div class="process__stat">
|
<span class="process__stat-label">累计总投入</span>
|
<span class="process__stat-value app-num">{{ formatAmount(aside.totalInput) }}</span>
|
</div>
|
<div class="process__stat">
|
<span class="process__stat-label">累计总报废</span>
|
<span class="process__stat-value app-num">{{ formatAmount(aside.totalScrap) }}</span>
|
</div>
|
<div class="process__stat">
|
<span class="process__stat-label">累计总产出</span>
|
<span class="process__stat-value app-num">{{ formatAmount(aside.totalOutput) }}</span>
|
</div>
|
</aside>
|
</div>
|
|
<el-dialog v-model="dialogVisible" title="选择工序" width="500px" append-to-body :close-on-click-modal="false">
|
<div class="dialog-body">
|
<el-checkbox-group v-model="tempIds">
|
<div class="process-grid">
|
<el-checkbox v-for="item in options" :key="item.id" :label="item.id" border>
|
{{ item.name }}
|
</el-checkbox>
|
</div>
|
</el-checkbox-group>
|
</div>
|
<template #footer>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="confirmDialog">确认</el-button>
|
</template>
|
</el-dialog>
|
</HomePanel>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, onMounted } from 'vue'
|
import Echarts from '@/components/Echarts/echarts.vue'
|
import HomePanel from '../HomePanel.vue'
|
import { processDataProductionStatistics } from '@/api/viewIndex'
|
import { list } from '@/api/productionManagement/productionProcess'
|
|
const loading = ref(true)
|
const range = ref(1)
|
const chartData = ref([])
|
const activeIndex = ref(0)
|
|
const options = ref([])
|
const selectedIds = ref([])
|
const tempIds = ref([])
|
const dialogVisible = ref(false)
|
|
const xAxis = ref([
|
{
|
nameTextStyle: { color: 'rgba(0,0,0,0.35)', fontSize: 12 },
|
axisLabel: { color: 'rgba(0,0,0,0.45)' },
|
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)', type: 'dashed' } }
|
}
|
])
|
|
const yAxis = ref([
|
{
|
type: 'category',
|
axisTick: { show: false },
|
axisLine: { show: false },
|
axisLabel: { color: 'rgba(0,0,0,0.55)' },
|
data: []
|
}
|
])
|
|
const grid = reactive({ left: 0, right: 60, top: 20, bottom: 20, containLabel: true })
|
|
const tooltip = reactive({
|
trigger: 'axis',
|
axisPointer: { type: 'shadow' },
|
formatter: (params) => {
|
const name = params?.[0]?.name ?? ''
|
const lines = (Array.isArray(params) ? params : [])
|
.map((p) => {
|
const box = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`
|
return `${box}${p.seriesName} <b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`
|
})
|
.join('<br/>')
|
return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>`
|
}
|
})
|
|
const series = computed(() => [
|
{
|
name: '投入量', type: 'bar', stack: 'total', barWidth: 18,
|
itemStyle: { color: '#2C51D9', borderRadius: [6, 0, 0, 6] },
|
data: chartData.value.map((i) => i.input)
|
},
|
{
|
name: '报废量', type: 'bar', stack: 'total', barWidth: 18,
|
itemStyle: { color: '#D97706' },
|
data: chartData.value.map((i) => i.scrap)
|
},
|
{
|
name: '产出量', type: 'bar', stack: 'total', barWidth: 18,
|
itemStyle: { color: '#0F9960', borderRadius: [0, 6, 6, 0] },
|
data: chartData.value.map((i) => i.output)
|
}
|
])
|
|
const aside = computed(() => {
|
const item = chartData.value[activeIndex.value] || {}
|
return {
|
processName: item.name || '暂无数据',
|
totalInput: item.input || 0,
|
totalScrap: item.scrap || 0,
|
totalOutput: item.output || 0
|
}
|
})
|
|
const formatAmount = (n) =>
|
Number(n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
const refresh = () => {
|
return processDataProductionStatistics({
|
type: range.value,
|
processIds: selectedIds.value.length > 0 ? selectedIds.value.join(',') : null
|
}).then((res) => {
|
chartData.value = (res.data || []).map((item) => ({
|
name: item.processName,
|
input: item.totalInput,
|
scrap: item.totalScrap,
|
output: item.totalOutput
|
}))
|
yAxis.value[0].data = chartData.value.map((i) => i.name)
|
activeIndex.value = 0
|
})
|
}
|
|
const openDialog = () => {
|
tempIds.value = [...selectedIds.value]
|
dialogVisible.value = true
|
}
|
|
const confirmDialog = () => {
|
selectedIds.value = [...tempIds.value]
|
dialogVisible.value = false
|
refresh()
|
}
|
|
const resetFilter = () => {
|
selectedIds.value = []
|
tempIds.value = []
|
refresh()
|
}
|
|
const handleChartClick = (params) => {
|
if (params && params.dataIndex !== undefined) activeIndex.value = params.dataIndex
|
}
|
|
onMounted(() => {
|
list().then((res) => { options.value = res.data || [] }).catch(() => {})
|
Promise.all([refresh()]).finally(() => { loading.value = false })
|
})
|
</script>
|
|
<style scoped>
|
.process {
|
display: flex;
|
gap: 16px;
|
height: 300px;
|
}
|
|
.process__chart {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.process__aside {
|
width: 190px;
|
flex-shrink: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
}
|
|
.process__legend {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 10px;
|
padding: 0 0 4px;
|
}
|
|
.process__legend-item {
|
display: inline-flex;
|
align-items: center;
|
gap: 5px;
|
font-size: 12px;
|
color: var(--app-text-tertiary);
|
}
|
|
.dot {
|
width: 8px;
|
height: 8px;
|
border-radius: 2px;
|
display: inline-block;
|
}
|
|
.process__name {
|
padding: 8px 12px;
|
border-radius: var(--app-radius-sm);
|
background: var(--app-primary-soft);
|
color: var(--app-primary);
|
font-size: 13px;
|
font-weight: 600;
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.process__stat {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 8px 12px;
|
border-radius: var(--app-radius-sm);
|
background: var(--app-surface-hover);
|
}
|
|
.process__stat-label {
|
font-size: 12px;
|
color: var(--app-text-tertiary);
|
}
|
|
.process__stat-value {
|
font-size: 14px;
|
font-weight: 600;
|
color: var(--app-text);
|
}
|
|
.dialog-body {
|
max-height: 380px;
|
overflow-y: auto;
|
}
|
|
.process-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
gap: 12px;
|
}
|
|
.process-grid :deep(.el-checkbox.is-bordered) {
|
margin-left: 0 !important;
|
width: 100%;
|
}
|
|
@media (max-width: 1200px) {
|
.process { flex-direction: column; height: auto; }
|
.process__chart { height: 280px; }
|
.process__aside { width: 100%; flex-direction: row; flex-wrap: wrap; }
|
.process__aside > * { flex: 1; min-width: 150px; }
|
}
|
</style>
|