| | |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产订单进度</div> |
| | | <el-radio-group v-model="orderFilter" size="small"> |
| | | <el-radio-button label="all">全部({{ orderProgressMeta.total }})</el-radio-button> |
| | | <el-radio-button label="inProgress">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button label="completed">已完成({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button label="paused">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | <el-radio-button :value="'all'">全部({{ orderProgressMeta.total }})</el-radio-button> |
| | | <el-radio-button :value="'waiting'">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button> |
| | | <el-radio-button :value="'inProgress'">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button> |
| | | <el-radio-button :value="'completed'">已完成({{ orderProgressMeta.completedCount }})</el-radio-button> |
| | | <el-radio-button :value="'paused'">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | |
| | | </ul> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <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> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}件</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}件</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}件</span></div> |
| | | </div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="barLegend" |
| | | :series="barSeries1" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 270px" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="hasRightPanels" class="right-column"> |
| | |
| | | </ul> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.receipt" class="cockpit-panel receipt-panel"> |
| | | <div class="panel-title">回款与开票分析</div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries" |
| | | :tooltip="tooltipLine" |
| | | :xAxis="xAxis2" |
| | | :yAxis="yAxis2" |
| | | style="height: 300px" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 质量统计 - 占满整行 --> |
| | | <div v-if="visiblePanels.quality" class="cockpit-panel quality-panel full-width"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">质量统计</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <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> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">原材料已检数量 <span>{{ qualityStatisticsObject.supplierNum }}吨</span></div> |
| | | <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}吨</span></div> |
| | | <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}吨</span></div> |
| | | </div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="barLegend" |
| | | :series="barSeries1" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 270px" |
| | | /> |
| | | </div> |
| | | </section> |
| | | |
| | |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | | <div class="process-grid"> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :value="item.id" border> |
| | | {{ item.name }} |
| | | </el-checkbox> |
| | | </div> |
| | |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { |
| | | analysisCustomerContractAmounts, |
| | | getAmountHalfYear, |
| | | getBusiness, |
| | | homeTodos, |
| | | processDataProductionStatistics, |
| | |
| | | productionOverview, |
| | | productionRealtimeBoard, |
| | | qualityInspectionStatistics, |
| | | statisticsReceivablePayable, |
| | | todayProductionPlan, |
| | | } from "@/api/viewIndex.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | |
| | | monthSaleHaveMoney: 0, |
| | | monthSaleMoney: 0, |
| | | todayInventoryNum: 0, |
| | | lastMonthSaleMoney: 0, |
| | | lastMonthPurchaseMoney: 0, |
| | | }); |
| | | |
| | | const qualityStatisticsObject = ref({ |
| | |
| | | }); |
| | | |
| | | const orderProgressMeta = ref({ |
| | | status: "all", |
| | | tab: "all", |
| | | bizDate: null, |
| | | total: 0, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | |
| | | itemStyle: { color: "#f59e0b", borderRadius: [6, 6, 0, 0] }, |
| | | emphasis: { focus: "series" }, |
| | | data: [], |
| | | }, |
| | | ]); |
| | | |
| | | const lineLegend = { |
| | | show: true, |
| | | textStyle: { color: axisTextColor }, |
| | | data: ["开票", "回款"], |
| | | }; |
| | | |
| | | const xAxis2 = ref([ |
| | | { |
| | | type: "category", |
| | | data: [], |
| | | axisLine: { lineStyle: { color: axisLineColor } }, |
| | | axisLabel: { |
| | | color: axisTextColor, |
| | | interval: 0, |
| | | formatter: (value) => value.replace(/~/g, "\n"), |
| | | }, |
| | | }, |
| | | ]); |
| | | |
| | | const yAxis2 = ref([ |
| | | { |
| | | type: "value", |
| | | splitLine: { lineStyle: { color: splitLineColor } }, |
| | | axisLine: { lineStyle: { color: axisLineColor } }, |
| | | axisLabel: { color: axisTextColor }, |
| | | }, |
| | | ]); |
| | | |
| | | const tooltipLine = { |
| | | trigger: "axis", |
| | | backgroundColor: "rgba(255, 255, 255, 0.97)", |
| | | borderColor: "rgba(148, 163, 184, 0.26)", |
| | | textStyle: { color: "#334155" }, |
| | | }; |
| | | |
| | | const lineSeries = ref([ |
| | | { |
| | | name: "开票", |
| | | type: "line", |
| | | data: [], |
| | | smooth: true, |
| | | itemStyle: { color: "#2563eb" }, |
| | | lineStyle: { width: 2, color: "#2563eb" }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(37, 99, 235, 0.24)" }, |
| | | { offset: 1, color: "rgba(37, 99, 235, 0.02)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | { |
| | | name: "回款", |
| | | type: "line", |
| | | data: [], |
| | | smooth: true, |
| | | itemStyle: { color: "#14b8a6" }, |
| | | lineStyle: { width: 2, color: "#14b8a6" }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(20, 184, 166, 0.2)" }, |
| | | { offset: 1, color: "rgba(20, 184, 166, 0.02)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | ]); |
| | | |
| | |
| | | }); |
| | | }); |
| | | |
| | | // 计算环比增长率 |
| | | const calculateGrowthRate = (current, last) => { |
| | | const currentVal = parseFloat(current) || 0; |
| | | const lastVal = parseFloat(last) || 0; |
| | | if (lastVal === 0) return currentVal > 0 ? 100 : 0; |
| | | return ((currentVal - lastVal) / lastVal * 100).toFixed(1); |
| | | }; |
| | | |
| | | const dashboardCards = computed(() => [ |
| | | { |
| | | key: "sales", |
| | | title: "销售数据", |
| | | desc: "本月销售额(元)", |
| | | value: formatNumber(businessInfo.value.monthSaleMoney), |
| | | subLabel: "未开票金额", |
| | | subValue: formatNumber(businessInfo.value.monthSaleHaveMoney), |
| | | trend: `占比 ${ratioText(businessInfo.value.monthSaleHaveMoney, businessInfo.value.monthSaleMoney)}`, |
| | | subLabel: "上月销售", |
| | | subValue: formatNumber(businessInfo.value.lastMonthSaleMoney), |
| | | trend: (() => { |
| | | const rate = calculateGrowthRate(businessInfo.value.monthSaleMoney, businessInfo.value.lastMonthSaleMoney); |
| | | const num = parseFloat(rate); |
| | | if (num > 0) return `环比 ↑ ${rate}%`; |
| | | if (num < 0) return `环比 ↓ ${Math.abs(num)}%`; |
| | | return "环比持平"; |
| | | })(), |
| | | icon: DataLine, |
| | | visible: visibleModules.value.sales, |
| | | }, |
| | |
| | | title: "采购数据", |
| | | desc: "本月采购额(元)", |
| | | value: formatNumber(businessInfo.value.monthPurchaseMoney), |
| | | subLabel: "待付款金额", |
| | | subValue: formatNumber(businessInfo.value.monthPurchaseHaveMoney), |
| | | trend: `占比 ${ratioText( |
| | | businessInfo.value.monthPurchaseHaveMoney, |
| | | businessInfo.value.monthPurchaseMoney |
| | | )}`, |
| | | subLabel: "上月采购", |
| | | subValue: formatNumber(businessInfo.value.lastMonthPurchaseMoney), |
| | | trend: (() => { |
| | | const rate = calculateGrowthRate(businessInfo.value.monthPurchaseMoney, businessInfo.value.lastMonthPurchaseMoney); |
| | | const num = parseFloat(rate); |
| | | if (num > 0) return `环比 ↑ ${rate}%`; |
| | | if (num < 0) return `环比 ↓ ${Math.abs(num)}%`; |
| | | return "环比持平"; |
| | | })(), |
| | | icon: ShoppingCartFull, |
| | | visible: visibleModules.value.procurement, |
| | | }, |
| | |
| | | icon: Box, |
| | | visible: visibleModules.value.inventory, |
| | | }, |
| | | { |
| | | key: "production", |
| | | title: "生产总览", |
| | | desc: "累计产出(件)", |
| | | value: formatNumber(productionOverviewData.value.totalOutput), |
| | | subLabel: "累计报废", |
| | | subValue: formatNumber(productionOverviewData.value.totalScrap), |
| | | trend: `良率 ${Number(productionOverviewData.value.yieldRate || 0).toFixed(2)}%`, |
| | | icon: Operation, |
| | | visible: visibleModules.value.production, |
| | | }, |
| | | ].filter((item) => item.visible)); |
| | | |
| | | const productionOrders = ref([]); |
| | | |
| | | const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "paused"]; |
| | | const orderFilterAliasMap = { |
| | | 1: "waiting", |
| | | 2: "inProgress", |
| | | 3: "completed", |
| | | 4: "paused", |
| | | }; |
| | | const orderFilter = ref("all"); |
| | | const filteredOrders = computed(() => productionOrders.value); |
| | | |
| | | const normalizeOrderFilter = (value, fallback = "all") => { |
| | | const safeFallback = orderFilterOptions.includes(fallback) ? fallback : "all"; |
| | | const text = String(value ?? "").trim(); |
| | | if (orderFilterAliasMap[text]) { |
| | | return orderFilterAliasMap[text]; |
| | | } |
| | | return orderFilterOptions.includes(text) ? text : safeFallback; |
| | | }; |
| | | |
| | | const parseCount = (value) => { |
| | | if (value === null || value === undefined || value === "") return null; |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : null; |
| | | }; |
| | | |
| | | const resolveProgressCount = (rawValue, currentStatus, targetStatus, total) => { |
| | | const count = parseCount(rawValue); |
| | | if (count !== null) return count; |
| | | return currentStatus === targetStatus ? total : 0; |
| | | }; |
| | | |
| | | const getCompareTrend = (value) => { |
| | | const num = Number(value || 0); |
| | |
| | | realtime: visibleModules.value.production, |
| | | quick: quickEntries.value.length > 0, |
| | | plan: visibleModules.value.production, |
| | | receipt: visibleModules.value.sales || visibleModules.value.finance, |
| | | |
| | | })); |
| | | |
| | | const hasLeftPanels = computed( |
| | | () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract || visiblePanels.value.quality |
| | | () => visiblePanels.value.process || visiblePanels.value.order || visiblePanels.value.contract |
| | | ); |
| | | const hasRightPanels = computed( |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan || visiblePanels.value.receipt |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan |
| | | ); |
| | | const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value); |
| | | const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value || visiblePanels.value.quality); |
| | | |
| | | const quickEntries = computed(() => |
| | | quickEntryConfigs |
| | |
| | | const refreshProductionOrderProgress = async () => { |
| | | try { |
| | | const res = await productionOrderProgress({ |
| | | status: orderFilter.value, |
| | | tab: orderFilter.value, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }); |
| | | const data = res?.data || {}; |
| | | const statusValue = normalizeOrderFilter(data.status, orderFilter.value); |
| | | const total = Number(data.total || 0); |
| | | orderProgressMeta.value = { |
| | | status: statusValue, |
| | | tab: data.tab || orderFilter.value, |
| | | total: Number(data.total || 0), |
| | | bizDate: data.bizDate || null, |
| | | total, |
| | | pageNum: Number(data.pageNum || 1), |
| | | pageSize: Number(data.pageSize || 10), |
| | | inProgressCount: Number(data.inProgressCount || 0), |
| | | completedCount: Number(data.completedCount || 0), |
| | | pausedCount: Number(data.pausedCount || 0), |
| | | waitingCount: resolveProgressCount(data.waitingCount, statusValue, "waiting", total), |
| | | inProgressCount: resolveProgressCount(data.inProgressCount, statusValue, "inProgress", total), |
| | | completedCount: resolveProgressCount(data.completedCount, statusValue, "completed", total), |
| | | pausedCount: resolveProgressCount(data.pausedCount, statusValue, "paused", total), |
| | | }; |
| | | productionOrders.value = (data.records || []).map(mapOrderProgressRecord); |
| | | } catch { |
| | | orderProgressMeta.value = { |
| | | status: orderFilter.value, |
| | | tab: orderFilter.value, |
| | | bizDate: null, |
| | | total: 0, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | |
| | | |
| | | const refreshTodayProductionPlan = async () => { |
| | | try { |
| | | const res = await todayProductionPlan({ limit: 4 }); |
| | | const res = await todayProductionPlan({ |
| | | limit: 4, |
| | | planDate: nowDate.value, |
| | | }); |
| | | const data = res?.data || {}; |
| | | todayPlanTotal.value = Number(data.total || 0); |
| | | todayPlanList.value = (data.records || []).map(mapTodayPlanRecord); |
| | |
| | | todoList.value = res.data || []; |
| | | }; |
| | | |
| | | const statisticsReceivable = async () => { |
| | | const res = await statisticsReceivablePayable({ type: 1 }); |
| | | barSeries.value[0].data = [ |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }, |
| | | ]; |
| | | }; |
| | | |
| | | const qualityStatisticsInfo = async () => { |
| | | const res = await qualityInspectionStatistics({ type: qualityRange.value }); |
| | | xAxis1.value[0].data = []; |
| | | barSeries1.value[0].data = []; |
| | | barSeries1.value[1].data = []; |
| | | barSeries1.value[2].data = []; |
| | | (res.data.item || []).forEach((item) => { |
| | | const data = res.data || {}; |
| | | (data.item || []).forEach((item) => { |
| | | xAxis1.value[0].data.push(item.date); |
| | | barSeries1.value[0].data.push(item.supplierNum); |
| | | barSeries1.value[1].data.push(item.processNum); |
| | | barSeries1.value[2].data.push(item.factoryNum); |
| | | }); |
| | | qualityStatisticsObject.value.supplierNum = res.data.supplierNum; |
| | | qualityStatisticsObject.value.processNum = res.data.processNum; |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum; |
| | | }; |
| | | |
| | | const getAmountHalfYearNum = async () => { |
| | | const res = await getAmountHalfYear(); |
| | | const monthName = []; |
| | | const receiptAmount = []; |
| | | const invoiceAmount = []; |
| | | (res.data || []).forEach((item) => { |
| | | monthName.push(item.month); |
| | | receiptAmount.push(item.receiptAmount); |
| | | invoiceAmount.push(item.invoiceAmount); |
| | | }); |
| | | |
| | | xAxis2.value[0].data = monthName.map((item) => item.replace(/~/g, "\n~")); |
| | | lineSeries.value[0].data = invoiceAmount; |
| | | lineSeries.value[1].data = receiptAmount; |
| | | qualityStatisticsObject.value.supplierNum = data.supplierNum || 0; |
| | | qualityStatisticsObject.value.processNum = data.processNum || 0; |
| | | qualityStatisticsObject.value.factoryNum = data.factoryNum || 0; |
| | | }; |
| | | |
| | | const getProcessList = async () => { |
| | |
| | | } |
| | | if (visiblePanels.value.quality) { |
| | | qualityStatisticsInfo(); |
| | | } |
| | | if (visiblePanels.value.receipt) { |
| | | statisticsReceivable(); |
| | | getAmountHalfYearNum(); |
| | | } |
| | | if (visiblePanels.value.process) { |
| | | getProcessList(); |
| | |
| | | |
| | | .stats-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 14px; |
| | | } |
| | | |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | max-height: 230px; |
| | | max-height: 380px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | .main-grid { |
| | | display: grid; |
| | | grid-template-columns: minmax(0, 1fr) 400px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .left-column, |
| | | .left-column { |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .right-column { |
| | | width: 400px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .full-width { |
| | | width: 100%; |
| | | flex-basis: 100%; |
| | | } |
| | | |
| | | .process-panel { |
| | |
| | | } |
| | | |
| | | .quick-panel { |
| | | min-height: 0; |
| | | min-height: 280px; |
| | | } |
| | | |
| | | .table-progress { |
| | |
| | | |
| | | .quality-panel { |
| | | min-height: 0; |
| | | } |
| | | |
| | | .receipt-panel { |
| | | min-height: 340px; |
| | | } |
| | | |
| | | .plan-list { |
| | |
| | | } |
| | | |
| | | @media (max-width: 1600px) { |
| | | .main-grid { |
| | | grid-template-columns: minmax(0, 1fr) 380px; |
| | | .right-column { |
| | | width: 380px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1366px) { |
| | | .main-grid { |
| | | grid-template-columns: minmax(0, 1fr) 340px; |
| | | .right-column { |
| | | width: 340px; |
| | | } |
| | | |
| | | .stats-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 992px) { |
| | | .stats-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .main-grid { |
| | | grid-template-columns: 1fr; |
| | | .left-column, |
| | | .right-column { |
| | | width: 100%; |
| | | } |
| | | |
| | | .right-column { |