| | |
| | | <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="'end'">已结束({{ orderProgressMeta.endCount }})</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <el-table :data="filteredOrders" stripe> |
| | | <el-table-column prop="orderNo" label="订单编号" min-width="150" /> |
| | | <el-table-column prop="productName" label="产品名称" min-width="120" /> |
| | | <el-table-column prop="planQty" label="计划数量" min-width="90" /> |
| | | <el-table-column prop="completedQty" label="已完成" min-width="90" /> |
| | | <el-table-column label="完成率" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deliveryDate" label="交期" min-width="110" /> |
| | | <el-table-column label="状态" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getOrderStatusType(row.status)" effect="light"> |
| | | {{ row.statusLabel || getOrderStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="order-table-wrap"> |
| | | <el-table :data="filteredOrders" stripe max-height="440"> |
| | | <el-table-column prop="orderNo" label="订单编号" min-width="150" /> |
| | | <el-table-column prop="productName" label="产品名称" min-width="120" /> |
| | | <el-table-column prop="planQty" label="计划数量" min-width="90" /> |
| | | <el-table-column prop="completedQty" label="已完成" min-width="90" /> |
| | | <el-table-column label="完成率" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <div class="table-progress"> |
| | | <el-progress |
| | | :stroke-width="8" |
| | | :percentage="row.completionRate" |
| | | :show-text="false" |
| | | status="success" |
| | | /> |
| | | <span>{{ row.completionRate }}%</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="deliveryDate" label="交期" min-width="110" /> |
| | | <el-table-column label="状态" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getOrderStatusType(row.status)" effect="light"> |
| | | {{ row.statusLabel || getOrderStatusText(row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | |
| | | <div v-if="visiblePanels.todo" class="cockpit-panel todo-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">待办事项</div> |
| | | <span class="panel-more">更多</span> |
| | | <span class="panel-more" @click="openTodoDialog">更多</span> |
| | | </div> |
| | | <ul class="todo-list" v-if="todoList.length > 0"> |
| | | <li v-for="item in todoList" :key="item.id" class="todo-item"> |
| | |
| | | <div v-if="visiblePanels.realtime" class="cockpit-panel realtime-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产实时看板</div> |
| | | <span class="panel-more">更多</span> |
| | | </div> |
| | | <div class="realtime-grid"> |
| | | <div class="realtime-item" v-for="item in realtimeBoard" :key="item.key"> |
| | |
| | | <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> |
| | |
| | | <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="todoDialogVisible" title="全部待办事项" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="todoDialogLoading" class="todo-dialog-body"> |
| | | <el-table :data="todoDialogList" stripe max-height="520"> |
| | | <el-table-column prop="approveId" label="待办编号" min-width="160" /> |
| | | <el-table-column prop="approveDeptName" label="部门/模板" min-width="160" /> |
| | | <el-table-column prop="approveReason" label="事由" min-width="240" show-overflow-tooltip /> |
| | | <el-table-column prop="approveUserName" label="发起人" min-width="120" /> |
| | | <el-table-column prop="approveTime" label="时间" min-width="170" /> |
| | | </el-table> |
| | | </div> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | |
| | | }); |
| | | |
| | | const orderProgressMeta = ref({ |
| | | status: "all", |
| | | tab: "all", |
| | | bizDate: null, |
| | | total: 0, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | waitingCount: 0, |
| | | inProgressCount: 0, |
| | | completedCount: 0, |
| | | pausedCount: 0, |
| | | endCount: 0, |
| | | }); |
| | | |
| | | const todayPlanList = ref([]); |
| | | const todayPlanTotal = ref(0); |
| | | const todoDialogVisible = ref(false); |
| | | const todoDialogLoading = ref(false); |
| | | const todoDialogList = ref([]); |
| | | |
| | | const sum = ref(0); |
| | | const yny = ref(0); |
| | |
| | | |
| | | const productionOrders = ref([]); |
| | | |
| | | const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "end"]; |
| | | const orderFilterAliasMap = { |
| | | 1: "waiting", |
| | | 2: "inProgress", |
| | | 3: "completed", |
| | | 5: "end", |
| | | }; |
| | | 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); |
| | |
| | | 1: "待开始", |
| | | 2: "进行中", |
| | | 3: "已完成", |
| | | 4: "已暂停", |
| | | 5: "已结束", |
| | | }; |
| | | return mapping[status] || "未知"; |
| | | }; |
| | |
| | | const refreshProductionOrderProgress = async () => { |
| | | try { |
| | | const res = await productionOrderProgress({ |
| | | status: orderFilter.value, |
| | | tab: orderFilter.value, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | pageNum: -1, |
| | | pageSize: -1, |
| | | }); |
| | | 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), |
| | | endCount: resolveProgressCount(data.endCount, statusValue, "end", 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, |
| | | endCount: 0, |
| | | }; |
| | | productionOrders.value = []; |
| | | } |
| | |
| | | |
| | | const refreshTodayProductionPlan = async () => { |
| | | try { |
| | | const res = await todayProductionPlan({ limit: 4 }); |
| | | const res = await todayProductionPlan({ |
| | | limit: 1000, |
| | | planDate: nowDate.value, |
| | | }); |
| | | const data = res?.data || {}; |
| | | todayPlanTotal.value = Number(data.total || 0); |
| | | todayPlanList.value = (data.records || []).map(mapTodayPlanRecord); |
| | |
| | | todoList.value = res.data || []; |
| | | }; |
| | | |
| | | const openTodoDialog = async () => { |
| | | todoDialogVisible.value = true; |
| | | todoDialogLoading.value = true; |
| | | try { |
| | | const res = await homeTodos(); |
| | | todoDialogList.value = res.data || []; |
| | | } catch { |
| | | todoDialogList.value = []; |
| | | } finally { |
| | | todoDialogLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const statisticsReceivable = async () => { |
| | | const res = await statisticsReceivablePayable({ type: 1 }); |
| | | const data = res?.data || {}; |
| | | const payableMoney = Number(data.payableMoney ?? 0); |
| | | const receivableMoney = Number(data.receivableMoney ?? 0); |
| | | barSeries.value[0].data = [ |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }, |
| | | { value: payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: receivableMoney, itemStyle: { color: barColors2[1] } }, |
| | | ]; |
| | | }; |
| | | |
| | |
| | | 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; |
| | | qualityStatisticsObject.value.supplierNum = data.supplierNum || 0; |
| | | qualityStatisticsObject.value.processNum = data.processNum || 0; |
| | | qualityStatisticsObject.value.factoryNum = data.factoryNum || 0; |
| | | }; |
| | | |
| | | const getAmountHalfYearNum = async () => { |
| | |
| | | font-size: 14px; |
| | | color: #2563eb; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .todo-dialog-body { |
| | | min-height: 220px; |
| | | } |
| | | |
| | | .plan-panel { |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .plan-list { |
| | | margin: 10px 0 0; |
| | | padding: 0 4px 0 0; |
| | | list-style: none; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | max-height: 300px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .plan-list::-webkit-scrollbar { |
| | | width: 6px; |
| | | } |
| | | |
| | | .plan-list::-webkit-scrollbar-thumb { |
| | | border-radius: 999px; |
| | | background: rgba(37, 99, 235, 0.28); |
| | | } |
| | | |
| | | .todo-list { |
| | |
| | | |
| | | .order-panel { |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .order-table-wrap { |
| | | flex: 1; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .quick-panel { |
| | |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; |
| | | } |
| | | |
| | | .plan-panel { |
| | | min-height: 236px; |
| | | } |
| | | |
| | | .quality-panel { |
| | | min-height: 0; |
| | | } |
| | | |
| | | .receipt-panel { |
| | | min-height: 340px; |
| | | } |
| | | |
| | | .plan-list { |
| | | margin: 10px 0 0; |
| | | padding: 0; |
| | | list-style: none; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .plan-item { |