From 7179a06d3a8c824ee711a701e99114205fce9bcb Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 06 五月 2026 13:31:24 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
---
src/api/productionManagement/workOrder.js | 9 +
src/views/productionManagement/processStatistics/index.vue | 268 ++++++++++++++++++++++++++++++++++++++
src/views/productionManagement/productionOrder/index.vue | 129 +++++++++++++++++
src/views/productionManagement/workOrderEdit/index.vue | 4
src/views/productionManagement/workOrderManagement/index.vue | 8
5 files changed, 409 insertions(+), 9 deletions(-)
diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
index 2e2fe2d..b3050c9 100644
--- a/src/api/productionManagement/workOrder.js
+++ b/src/api/productionManagement/workOrder.js
@@ -86,3 +86,12 @@
data,
});
}
+
+// 鑾峰彇宸ュ簭缁熻鏁版嵁
+export function getOperationStatistics(query) {
+ return request({
+ url: "/productionOperationTask/getOperation",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/views/productionManagement/processStatistics/index.vue b/src/views/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..25fa531
--- /dev/null
+++ b/src/views/productionManagement/processStatistics/index.vue
@@ -0,0 +1,268 @@
+<template>
+ <div class="app-container">
+ <div class="search-bar">
+ <el-form :model="searchForm"
+ inline>
+ <el-form-item label="鏃ユ湡鍖洪棿:">
+ <el-date-picker v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 240px" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ icon="Search"
+ @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="stats-grid"
+ v-loading="loading">
+ <el-row :gutter="16"
+ v-if="statsData.length > 0">
+ <el-col v-for="(item, index) in statsData"
+ :key="index"
+ :xs="24"
+ :sm="12"
+ :md="8"
+ :lg="4.8"
+ :xl="4.8"
+ class="mb-16">
+ <div class="stats-card">
+ <div class="card-header">
+ <span class="process-name">{{ item.name }}</span>
+ <div class="header-stats">
+ <div class="stat-row">
+ <span class="label">璁″垝鏁�</span>
+ <span class="value">{{ item.planned }}</span>
+ </div>
+ <div class="stat-row">
+ <span class="label">鑹搧鏁�</span>
+ <span class="value">{{ item.good }}</span>
+ </div>
+ <div class="stat-row">
+ <span class="label">涓嶈壇鍝佹暟</span>
+ <span class="value">{{ item.bad }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="main-stat">
+ <div class="big-number">{{ item.total }}</div>
+ <div class="sub-label">鐢熶骇浠诲姟鏁�</div>
+ </div>
+ </div>
+ <div class="card-footer">
+ <div class="progress-info">
+ <span class="progress-label">杩涘害:</span>
+ <el-progress :percentage="Math.min(item.percentage, 100)"
+ :color="getProgressColor(item.percentage)"
+ :stroke-width="10"
+ :show-text="false"
+ class="flex-1" />
+ <span class="percentage-text">{{ item.percentage }}%</span>
+ </div>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-empty v-else
+ description="鏆傛棤鏁版嵁" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { reactive, ref, onMounted } from "vue";
+ import dayjs from "dayjs";
+ import { getOperationStatistics } from "@/api/productionManagement/workOrder.js";
+
+ const loading = ref(false);
+ const searchForm = reactive({
+ dateRange: [],
+ });
+
+ const statsData = ref([]);
+
+ const getProgressColor = percentage => {
+ if (percentage >= 100) return "#67c23a";
+ if (percentage >= 50) return "#409eff";
+ if (percentage >= 25) return "#e6a23c";
+ return "red";
+ };
+
+ const getList = () => {
+ loading.value = true;
+ const params = {
+ startDate: searchForm.dateRange?.[0] || "",
+ endDate: searchForm.dateRange?.[1] || "",
+ };
+ getOperationStatistics(params)
+ .then(res => {
+ // 鏍规嵁瀹為檯鎺ュ彛杩斿洖鐨勫瓧娈佃繘琛屾槧灏�
+ statsData.value = (res.data || []).map(item => ({
+ name: item.operationName || "-",
+ total: item.productionTaskCount || 0,
+ planned: item.planQuantity || 0,
+ good: item.goodQuantity || 0,
+ bad: item.scrapQty || 0,
+ percentage: Number(item.completionStatus || 0),
+ }));
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ getList();
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .app-container {
+ padding: 20px;
+ background-color: #f0f2f5;
+ min-height: calc(100vh - 84px);
+ }
+
+ .search-bar {
+ background: #fff;
+ padding: 15px 20px 0;
+ border-radius: 4px;
+ margin-bottom: 20px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+ }
+
+ .mb-16 {
+ margin-bottom: 16px;
+ }
+
+ // 妯℃嫙 lg="4.8" 鍥犱负 element 涓嶆敮鎸� 24/5
+ @media only screen and (min-width: 1200px) {
+ .el-col-lg-4-8 {
+ width: 20%;
+ max-width: 20%;
+ flex: 0 0 20%;
+ }
+ }
+
+ .stats-card {
+ background: #fff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ transition: transform 0.3s;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+
+ .process-name {
+ background-color: #e6f7ff;
+ color: #1890ff;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .header-stats {
+ text-align: right;
+
+ .stat-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 2px;
+
+ .label {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .value {
+ font-size: 13px;
+ color: #303133;
+ font-weight: bold;
+ min-width: 24px;
+ }
+ }
+ }
+ }
+
+ .card-body {
+ padding: 10px 0;
+
+ .main-stat {
+ .big-number {
+ font-size: 28px;
+ font-weight: bold;
+ color: #303133;
+ line-height: 1;
+ }
+
+ .sub-label {
+ font-size: 14px;
+ color: #606266;
+ margin-top: 8px;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .card-footer {
+ margin-top: 16px;
+
+ .progress-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .progress-label {
+ font-size: 12px;
+ color: #909399;
+ white-space: nowrap;
+ }
+
+ .flex-1 {
+ flex: 1;
+ }
+
+ .percentage-text {
+ font-size: 12px;
+ color: #606266;
+ min-width: 45px;
+ text-align: right;
+ }
+ }
+ }
+ }
+
+ // 淇 el-col 甯冨眬閫傞厤 5 鍒�
+ :deep(.el-row) {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ @media only screen and (min-width: 1200px) {
+ .el-col-lg-4\.8 {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 93fc177..c37358b 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -75,6 +75,26 @@
:color="progressColor(toProgressPercentage(row?.completionStatus))"
:status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
</template>
+ <template #processRouteStatus="{ row }">
+ <div v-if="row.processRouteStatus && row.processRouteStatus.length"
+ class="process-progress-container">
+ <div v-for="(item, index) in row.processRouteStatus"
+ :key="index"
+ class="process-step">
+ <div class="step-content">
+ <div class="step-circle"
+ :class="{ 'is-completed': item.percentage >= 100 }">
+ <span class="step-percentage"
+ :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span>
+ </div>
+ <div class="step-name">{{ item.name }}</div>
+ </div>
+ <div v-if="index < row.processRouteStatus.length - 1"
+ class="step-line"></div>
+ </div>
+ </div>
+ <span v-else>-</span>
+ </template>
</PIMTable>
</div>
<el-dialog v-model="bindRouteDialogVisible"
@@ -215,6 +235,7 @@
getProductOrderSource,
updateProductOrder,
} from "@/api/productionManagement/productionOrder.js";
+ import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue";
import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue";
@@ -241,7 +262,17 @@
total: 0,
});
- const tableColumn = ref([
+ const processColumnWidth = computed(() => {
+ if (!tableData.value || tableData.value.length === 0) return "200px";
+ const maxProcesses = Math.max(
+ ...tableData.value.map(row => row.processRouteStatus?.length || 0)
+ );
+ if (maxProcesses === 0) return "100px";
+ // 姣忎釜宸ュ簭鍦嗗湀 36px + 绾挎潯 30px = 66px锛岄澶栧姞 60px 杈硅窛鍜屾枃瀛楃┖闂�
+ return `${maxProcesses * 66 + 60}px`;
+ });
+
+ const tableColumn = computed(() => [
{
label: "鐢熶骇璁㈠崟鍙�",
prop: "npsNo",
@@ -296,6 +327,13 @@
{
label: "瀹屾垚鏁伴噺",
prop: "completeQuantity",
+ },
+ {
+ label: "宸ュ簭鐢熶骇杩涘害",
+ prop: "processRouteStatus",
+ dataType: "slot",
+ slot: "processRouteStatus",
+ width: processColumnWidth.value,
},
{
dataType: "slot",
@@ -630,10 +668,35 @@
const params = { ...searchForm.value, ...page };
params.entryDate = undefined;
productOrderListPage(params)
- .then(res => {
- tableLoading.value = false;
- tableData.value = res.data.records;
+ .then(async res => {
+ const records = res.data.records || [];
+ // 涓烘瘡涓鍗曟煡璇㈠搴旂殑宸ュ簭杩涘害鏁版嵁
+ const processPromises = records.map(async item => {
+ if (item.npsNo) {
+ try {
+ const workOrderRes = await productWorkOrderPage({
+ npsNo: item.npsNo,
+ size: 100,
+ });
+ const workOrders = workOrderRes.data.records || [];
+ // 鎸夌収宸ュ簭椤哄簭鎺掑簭锛堝鏋滄湁椤哄簭瀛楁锛屽亣璁句负 orderNum 鎴栨寜杩斿洖椤哄簭锛�
+ // 杞崲涓� processRouteStatus 鏍煎紡
+ const processRouteStatus = workOrders.map(wo => ({
+ name: wo.operationName || "鏈煡宸ュ簭",
+ percentage: wo.completionStatus > 100 ? 100 : wo.completionStatus,
+ }));
+ return { ...item, processRouteStatus };
+ } catch (error) {
+ console.error(`鑾峰彇宸ュ崟 ${item.npsNo} 杩涘害澶辫触:`, error);
+ return { ...item, processRouteStatus: [] };
+ }
+ }
+ return { ...item, processRouteStatus: [] };
+ });
+
+ tableData.value = await Promise.all(processPromises);
page.total = res.data.total;
+ tableLoading.value = false;
})
.catch(() => {
tableLoading.value = false;
@@ -813,6 +876,64 @@
.table_list {
margin-top: unset;
}
+
+ .process-progress-container {
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 0;
+ white-space: nowrap;
+
+ .process-step {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ .step-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 1;
+
+ .step-circle {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 2px solid #409eff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #fff;
+ margin-bottom: 4px;
+
+ .step-percentage {
+ font-size: 11px;
+ font-weight: bold;
+ }
+
+ &.is-completed {
+ border-color: #67c23a;
+ .step-percentage {
+ color: #67c23a;
+ }
+ }
+ }
+
+ .step-name {
+ font-size: 12px;
+ color: #606266;
+ white-space: nowrap;
+ }
+ }
+
+ .step-line {
+ width: 30px;
+ height: 1px;
+ background-color: #dcdfe6;
+ margin: 0 -2px;
+ margin-top: -20px; // 鍚戜笂鍋忕Щ浠ュ榻愬渾蹇�
+ }
+ }
+ }
</style>
<style lang="scss">
.status-cell {
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 49c33bf..1cde5ea 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -13,7 +13,7 @@
</div>
<div class="search-item">
<span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
- <el-input v-model="searchForm.productOrderNpsNo"
+ <el-input v-model="searchForm.npsNo"
style="width: 240px"
placeholder="璇疯緭鍏�"
@change="handleQuery"
@@ -259,7 +259,7 @@
const data = reactive({
searchForm: {
workOrderNo: "",
- productOrderNpsNo: "",
+ npsNo: "",
},
});
const { searchForm } = toRefs(data);
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 8003890..600b274 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -13,7 +13,7 @@
</div>
<div class="search-item">
<span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
- <el-input v-model="searchForm.productOrderNpsNo"
+ <el-input v-model="searchForm.npsNo"
style="width: 240px"
placeholder="璇疯緭鍏�"
@change="handleQuery"
@@ -265,7 +265,9 @@
import QRCode from "qrcode";
import { getCurrentInstance, reactive, toRefs } from "vue";
import MaterialDialog from "./components/MaterialDialog.vue";
- const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
@@ -525,7 +527,7 @@
const data = reactive({
searchForm: {
workOrderNo: "",
- productOrderNpsNo: "",
+ npsNo: "",
},
});
const { searchForm } = toRefs(data);
--
Gitblit v1.9.3