| | |
| | | /> |
| | | </div> |
| | | <div class="category-cards"> |
| | | <div v-for="(it, idx) in categoryPieData" :key="it.name" class="category-card"> |
| | | <div class="category-name">产品大类{{ idx + 1 }}</div> |
| | | <div v-for="(it, idx) in categoryPieData" :key="idx" class="category-card"> |
| | | <div class="category-name">{{ it.name }}</div> |
| | | <div class="category-val">{{ it.value }}</div> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="bottom-row"> |
| | | <div class="panel panel-full"> |
| | | <div class="panel-header"> |
| | | <div class="panel-title">工序不良原因分析</div> |
| | | <div class="panel-title">工序不良率分析</div> |
| | | <div class="panel-actions"> |
| | | <div class="panel-action">年份</div> |
| | | <div class="panel-action">筛选时间范围</div> |
| | | <el-date-picker |
| | | v-model="currentYear" |
| | | type="year" |
| | | format="YYYY年" |
| | | value-format="YYYY" |
| | | :clearable="false" |
| | | class="panel-year-picker" |
| | | v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | :clearable="true" |
| | | class="panel-date-picker" |
| | | size="small" |
| | | @change="handleDateChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <div class="panel-body"> |
| | | <div class="bottom-chart-wrap"> |
| | | <div class="bottom-chart-wrap" v-loading="isLoading" element-loading-text="加载中..."> |
| | | <div class="chart-unit">单位:%</div> |
| | | <Echarts |
| | | :chartStyle="{ width: '100%', height: '260px' }" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from 'vue' |
| | | import { |
| | | findProductQualityStatistics, |
| | | findProductWorkOrderCountStatistics, |
| | | findProductProductionStatistics, |
| | | findProductOutputCategoryPieData, |
| | | findProductDefectReasonAnalysis, |
| | | findProductProcessDefectRateAnalysis |
| | | } from '@/api/productionManagement/productionStatistic.js' |
| | | |
| | | import {computed, onMounted, ref} from 'vue' |
| | | import * as echarts from 'echarts' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import { ElSelect, ElOption, ElDatePicker } from 'element-plus' |
| | |
| | | } |
| | | |
| | | const kpi = ref({ |
| | | workOrderTotal: 378, |
| | | workOrderDoing: 108, |
| | | workOrderDone: 238, |
| | | passRate: 98, |
| | | ngRate: 8, |
| | | scrapTotal: 38, |
| | | productionAmount: 989878, |
| | | productionAmountTrend: 16, |
| | | productionQty: 19878, |
| | | productionQtyTrend: 16, |
| | | productionCost: 69878, |
| | | productionCostTrend: -16, |
| | | supplierCount: 48, |
| | | supplierCountTrend: 16, |
| | | totalOutput: 1000, |
| | | workOrderTotal: 0, |
| | | workOrderDoing: 0, |
| | | workOrderDone: 0, |
| | | passRate: 0, |
| | | ngRate: 0, |
| | | scrapTotal: 0, |
| | | productionAmount: 0, // 总生产总量 |
| | | productionAmountTrend: 0, // 总生产总量较上月趋势 |
| | | productionCost: 0, // 生产总消耗 |
| | | productionCostTrend: 0, // 生产总消耗较上月趋势 |
| | | supplierCount: 0, // 总供应商 |
| | | supplierCountTrend: 0, // 总供应商较上月趋势 |
| | | }) |
| | | |
| | | const currentYear = ref('2025') |
| | | const dateRange = ref([]) |
| | | |
| | | function formatMoney(n) { |
| | | const num = Number(n || 0) |
| | |
| | | const axisTooltip = { |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | confine: true, // 限制在图表区域内显示 |
| | | position: 'top', // 固定显示在顶部 |
| | | enterable: true, // 允许鼠标进入提示框区域 |
| | | extraCssText: 'max-height: 200px; overflow-y: auto; padding: 10px;', // 添加最大高度和滚动条 |
| | | formatter: (params) => { |
| | | const first = params?.[0] |
| | | const x = first?.axisValueLabel || '' |
| | |
| | | |
| | | // 中部左侧:饼状图(按产品大类统计产出数量) |
| | | const categoryPieColors = ['#2D5BFF', '#4E8AFF', '#00A4ED', '#26C6DA', '#7C3AED', '#F59E0B', '#EF4444', '#10B981'] |
| | | const categoryPieData = ref([ |
| | | { name: '产品大类1', value: 600 }, |
| | | { name: '产品大类2', value: 200 }, |
| | | { name: '产品大类3', value: 200 }, |
| | | { name: '产品大类4', value: 200 }, |
| | | { name: '产品大类5', value: 200 }, |
| | | ]) |
| | | const categoryPieData = ref([]) |
| | | |
| | | const categoryPieLegend = computed(() => { |
| | | // 设计图中饼图本身显示占比标签,不额外展示图例 |
| | |
| | | // 中部右侧:环形饼图 |
| | | const ngColors = ['#2D5BFF', '#26C6DA', '#F59E0B', '#7C3AED', '#60A5FA', '#10B981'] |
| | | |
| | | const ngReasonData = ref([ |
| | | { name: '不良原因1', value: 300 }, |
| | | { name: '不良原因3', value: 200 }, |
| | | { name: '不良原因4', value: 100 }, |
| | | { name: '不良原因5', value: 100 }, |
| | | { name: '不良原因6', value: 100 }, |
| | | ]) |
| | | const ngReasonData = ref([]) |
| | | const ngRateData = ref([]) |
| | | const isLoading = ref(false) |
| | | |
| | | const ngTotal = computed(() => ngReasonData.value.reduce((s, it) => s + Number(it.value || 0), 0)) |
| | | const ngRows = computed(() => { |
| | |
| | | // 底部:折线图(年份联动的每个工序不良率) |
| | | const lineGrid = { left: '4%', right: '4%', top: 60, bottom: 30, containLabel: true } |
| | | const lineLegend = computed(() => { |
| | | const processes = chartData.value.processes || [] |
| | | const processNames = processes.map(p => p.name) |
| | | return { |
| | | show: true, |
| | | top: 8, |
| | | type: 'scroll', |
| | | orient: 'horizontal', |
| | | textStyle: { color: '#666' }, |
| | | data: ['平均不良率', '工序A', '工序B', '工序C'], |
| | | data: ['平均不良率', ...processNames], |
| | | pageIconSize: 10, |
| | | pageTextStyle: { color: '#666' }, |
| | | pageButtonItemGap: 5, |
| | | pageButtonGap: 10 |
| | | } |
| | | }) |
| | | |
| | | const chartDataByYear = { |
| | | '2023': { |
| | | x: ['2023/02/21', '2023/03/02', '2023/03/13', '2023/03/24', '2023/04/04', '2023/04/15', '2023/04/28'], |
| | | bar: [30, 32, 35, 40, 38, 42, 39], |
| | | lineA: [45, 48, 52, 60, 55, 62, 58], |
| | | lineB: [30, 32, 38, 45, 40, 48, 42], |
| | | lineC: [20, 24, 30, 35, 32, 38, 33], |
| | | }, |
| | | '2024': { |
| | | x: ['2024/02/21', '2024/03/02', '2024/03/13', '2024/03/24', '2024/04/04', '2024/04/15', '2024/04/28'], |
| | | bar: [28, 30, 33, 37, 35, 39, 36], |
| | | lineA: [42, 46, 50, 58, 53, 60, 56], |
| | | lineB: [28, 31, 36, 43, 38, 46, 40], |
| | | lineC: [18, 21, 27, 33, 29, 35, 30], |
| | | }, |
| | | '2025': { |
| | | x: ['2025/02/21', '2025/03/02', '2025/03/13', '2025/03/24', '2025/04/04', '2025/04/15', '2025/04/28'], |
| | | bar: [32, 34, 37, 42, 40, 45, 41], |
| | | lineA: [45, 49, 54, 61, 57, 64, 60], |
| | | lineB: [31, 33, 39, 47, 41, 50, 44], |
| | | lineC: [21, 25, 31, 37, 34, 40, 35], |
| | | }, |
| | | } |
| | | |
| | | const chartData = computed(() => { |
| | | return chartDataByYear[currentYear.value] || chartDataByYear['2025'] |
| | | const data = ngRateData.value |
| | | if (!data || data.length === 0) { |
| | | return { |
| | | x: [], |
| | | bar: [], |
| | | processes: [] |
| | | } |
| | | } |
| | | |
| | | // Extract data from the API response |
| | | const x = data.map(item => item.date) |
| | | const bar = data.map(item => item.averageDefectRate) // Convert to percentage |
| | | |
| | | // Extract process names and their data |
| | | const processNames = new Set() |
| | | data.forEach(item => { |
| | | item.processes.forEach(process => { |
| | | Object.keys(process).forEach(name => processNames.add(name)) |
| | | }) |
| | | }) |
| | | |
| | | const processes = Array.from(processNames).map(name => { |
| | | return { |
| | | name, |
| | | data: data.map(item => { |
| | | const process = item.processes.find(p => p[name] !== undefined) |
| | | return process ? process[name] : 0 // Convert to percentage |
| | | }) |
| | | } |
| | | }) |
| | | |
| | | return { |
| | | x, |
| | | bar, |
| | | processes |
| | | } |
| | | }) |
| | | |
| | | const lineXAxis = computed(() => { |
| | |
| | | ] |
| | | }) |
| | | |
| | | const lineSeries = computed(() => [ |
| | | { |
| | | name: '平均不良率', |
| | | type: 'bar', |
| | | barWidth: 18, |
| | | data: chartData.value.bar, |
| | | itemStyle: { |
| | | color: 'rgba(59, 130, 246, 0.15)', |
| | | borderRadius: [4, 4, 0, 0], |
| | | const lineSeries = computed(() => { |
| | | const series = [ |
| | | { |
| | | name: '平均不良率', |
| | | type: 'bar', |
| | | barWidth: 18, |
| | | data: chartData.value.bar, |
| | | itemStyle: { |
| | | color: 'rgba(59, 130, 246, 0.15)', |
| | | borderRadius: [4, 4, 0, 0], |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | name: '工序A', |
| | | type: 'line', |
| | | smooth: true, |
| | | data: chartData.value.lineA, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | lineStyle: { width: 2, color: '#3b82f6' }, |
| | | itemStyle: { color: '#3b82f6' }, |
| | | }, |
| | | { |
| | | name: '工序B', |
| | | type: 'line', |
| | | smooth: true, |
| | | data: chartData.value.lineB, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | lineStyle: { width: 2, color: '#f59e0b' }, |
| | | itemStyle: { color: '#f59e0b' }, |
| | | }, |
| | | { |
| | | name: '工序C', |
| | | type: 'line', |
| | | smooth: true, |
| | | data: chartData.value.lineC, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | lineStyle: { width: 2, color: '#10b981' }, |
| | | itemStyle: { color: '#10b981' }, |
| | | }, |
| | | ]) |
| | | ] |
| | | |
| | | // Add process lines with different colors |
| | | const colors = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'] |
| | | chartData.value.processes.forEach((process, index) => { |
| | | series.push({ |
| | | name: process.name, |
| | | type: 'line', |
| | | smooth: true, |
| | | data: process.data, |
| | | symbol: 'circle', |
| | | symbolSize: 6, |
| | | lineStyle: { width: 2, color: colors[index % colors.length] }, |
| | | itemStyle: { color: colors[index % colors.length] }, |
| | | }) |
| | | }) |
| | | |
| | | return series |
| | | }) |
| | | |
| | | // 获取生产工单数量统计数据 |
| | | const fetchProductWorkOrderCountStatistic = () => { |
| | | findProductWorkOrderCountStatistics().then((res) => { |
| | | const data = res.data |
| | | kpi.value.workOrderTotal = data.totalCount |
| | | kpi.value.workOrderDoing = data.inProgressCount |
| | | kpi.value.workOrderDone = data.completedCount |
| | | }) |
| | | } |
| | | |
| | | // 获取生产质量统计数据 |
| | | const fetchProductQualityStatistics = () => { |
| | | findProductQualityStatistics().then((res) => { |
| | | const data = res.data |
| | | kpi.value.passRate = data.qualifiedRate |
| | | kpi.value.ngRate = data.defectRate |
| | | kpi.value.scrapTotal = data.scrapCount |
| | | }) |
| | | } |
| | | |
| | | // 获取生产数量统计数据 |
| | | const fetchProductProductionStatistics = () => { |
| | | findProductProductionStatistics().then((res) => { |
| | | const data = res.data |
| | | kpi.value.productionAmount = data.productionOutput |
| | | kpi.value.productionAmountTrend = data.productionOutputMonthlyChange |
| | | kpi.value.productionCost = data.productionConsumption |
| | | kpi.value.productionCostTrend = data.productionConsumptionMonthlyChange |
| | | kpi.value.supplierCount = data.supplierCount |
| | | kpi.value.supplierCountTrend = data.supplierCountMonthlyChange |
| | | }) |
| | | } |
| | | |
| | | // 获取产品产出分析数据 |
| | | const fetchProductOutputCategoryPieData = () => { |
| | | findProductOutputCategoryPieData().then((res) => { |
| | | categoryPieData.value = res.data |
| | | }) |
| | | } |
| | | // 获取不良原因分析统计数据 |
| | | const fetchProductDefectReasonAnalysis = () => { |
| | | findProductDefectReasonAnalysis().then((res) => { |
| | | ngReasonData.value = res.data |
| | | }) |
| | | } |
| | | |
| | | // 获取工序不良率分析 |
| | | const fetchProductProcessDefectRateAnalysis = (dateRange) => { |
| | | isLoading.value = true |
| | | const params = dateRange ? { |
| | | startDate: dateRange[0], |
| | | endDate: dateRange[1] |
| | | } : {} |
| | | findProductProcessDefectRateAnalysis(params).then((res) => { |
| | | ngRateData.value = res.data |
| | | }).finally(() => { |
| | | isLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | // 处理日期选择变化 |
| | | const handleDateChange = (range) => { |
| | | fetchProductProcessDefectRateAnalysis(range) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | // 初始化时获取生产工单数量统计数据 |
| | | fetchProductWorkOrderCountStatistic() |
| | | // 初始化时获取生产质量统计数据 |
| | | fetchProductQualityStatistics() |
| | | // 初始化时获取生产数量统计数据 |
| | | fetchProductProductionStatistics() |
| | | // 初始化时获取产品产出分析数据 |
| | | fetchProductOutputCategoryPieData() |
| | | // 初始化时获取不良原因分析统计数据 |
| | | fetchProductDefectReasonAnalysis() |
| | | // 初始化时获取工序不良率分析 |
| | | fetchProductProcessDefectRateAnalysis() |
| | | }) |
| | | |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .panel-date-picker { |
| | | width: 240px; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .panel-date-picker { |
| | | width: 240px; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .panel-title { |
| | | font-weight: 800; |
| | | color: #030303; |
| | |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | </style> |
| | | </style> |