| | |
| | | </el-avatar> |
| | | <div class="welcome-text"> |
| | | <div class="welcome-title"> |
| | | 晚上好,{{ userStore.nickName || userStore.name || "超级管理员" }} 👋 |
| | | {{ greetingText }},{{ userStore.nickName || userStore.name || "超级管理员" }} 👋 |
| | | </div> |
| | | <div class="welcome-subtitle">专注工业数字化,助力智造升级</div> |
| | | </div> |
| | | </div> |
| | | <div class="welcome-meta"> |
| | | <div class="meta-time digital-number">{{ nowTime }}</div> |
| | | <div class="meta-extra"> |
| | | <span class="meta-date">{{ nowDate }}</span> |
| | | <span class="meta-weather">{{ weatherText }}</span> |
| | | </div> |
| | | <div class="meta-tip">MES / MOM 生产运营驾驶舱</div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="top-row"> |
| | | <section v-if="dashboardCards.length > 0" class="top-row"> |
| | | <div class="stats-grid"> |
| | | <article |
| | | v-for="card in dashboardCards" |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="main-grid"> |
| | | <div class="left-column"> |
| | | <div class="cockpit-panel process-panel"> |
| | | <section v-if="hasVisiblePanels" class="main-grid"> |
| | | <div v-if="hasLeftPanels" class="left-column"> |
| | | <div v-if="visiblePanels.process" class="cockpit-panel process-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">工序数据生产统计明细</div> |
| | | <div class="panel-actions"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel order-panel"> |
| | | <div v-if="visiblePanels.order" class="cockpit-panel order-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">生产订单进度</div> |
| | | <el-radio-group v-model="orderFilter" size="small"> |
| | |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel contract-panel"> |
| | | <div v-if="visiblePanels.contract" class="cockpit-panel contract-panel"> |
| | | <div class="panel-title">客户合同金额分析</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-card"> |
| | |
| | | </ul> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel quality-panel"> |
| | | <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"> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="right-column"> |
| | | <div class="cockpit-panel todo-panel"> |
| | | <div v-if="hasRightPanels" class="right-column"> |
| | | <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> |
| | |
| | | <div v-else class="panel-empty">暂无数据</div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel realtime-panel"> |
| | | <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> |
| | | |
| | | <div class="cockpit-panel quick-panel"> |
| | | <div v-if="visiblePanels.quick" class="cockpit-panel quick-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">快捷功能</div> |
| | | </div> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel plan-panel"> |
| | | <div v-if="visiblePanels.plan" class="cockpit-panel plan-panel"> |
| | | <div class="panel-title-row"> |
| | | <div class="panel-title">今日生产计划</div> |
| | | <span class="panel-more">{{ todayPlanList.length }}项</span> |
| | |
| | | </ul> |
| | | </div> |
| | | |
| | | <div class="cockpit-panel receipt-panel"> |
| | | <div v-if="visiblePanels.receipt" class="cockpit-panel receipt-panel"> |
| | | <div class="panel-title">回款与开票分析</div> |
| | | <Echarts |
| | | :options="chartBaseOptions" |
| | |
| | | </div> |
| | | |
| | | </div> |
| | | </section> |
| | | |
| | | <section v-else class="cockpit-panel empty-home-panel"> |
| | | 当前账号没有可展示的首页模块 |
| | | </section> |
| | | |
| | | <el-dialog v-model="processDialogVisible" title="选择工序" width="500px" append-to-body> |
| | |
| | | UserFilled, |
| | | } from "@element-plus/icons-vue"; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { |
| | | analysisCustomerContractAmounts, |
| | |
| | | |
| | | const router = useRouter(); |
| | | const userStore = useUserStore(); |
| | | const permissionStore = usePermissionStore(); |
| | | const defaultWelcomeAvatar = new URL("../assets/images/profile.jpg", import.meta.url).href; |
| | | |
| | | const nowTime = ref(""); |
| | |
| | | const weatherText = "多云 28°C"; |
| | | |
| | | const nowDate = computed(() => (nowTime.value ? nowTime.value.slice(0, 10) : dayjs().format("YYYY-MM-DD"))); |
| | | |
| | | const greetingText = computed(() => { |
| | | const hour = dayjs().hour(); |
| | | if (hour < 6) return "凌晨好"; |
| | | if (hour < 9) return "早上好"; |
| | | if (hour < 12) return "上午好"; |
| | | if (hour < 14) return "中午好"; |
| | | if (hour < 18) return "下午好"; |
| | | return "晚上好"; |
| | | }); |
| | | |
| | | const welcomeAvatar = computed(() => |
| | | welcomeAvatarLoadFailed.value || !userStore.avatar ? defaultWelcomeAvatar : userStore.avatar |
| | |
| | | subValue: formatNumber(businessInfo.value.monthSaleHaveMoney), |
| | | trend: `占比 ${ratioText(businessInfo.value.monthSaleHaveMoney, businessInfo.value.monthSaleMoney)}`, |
| | | icon: DataLine, |
| | | visible: visibleModules.value.sales, |
| | | }, |
| | | { |
| | | key: "purchase", |
| | |
| | | businessInfo.value.monthPurchaseMoney |
| | | )}`, |
| | | icon: ShoppingCartFull, |
| | | visible: visibleModules.value.procurement, |
| | | }, |
| | | { |
| | | key: "inventory", |
| | |
| | | subValue: formatNumber(businessInfo.value.todayInventoryNum), |
| | | trend: "库存结构持续优化", |
| | | icon: Box, |
| | | visible: visibleModules.value.inventory, |
| | | }, |
| | | { |
| | | key: "production", |
| | |
| | | subValue: formatNumber(processTotals.value.scrap), |
| | | trend: `良率 ${ratioText(processTotals.value.output, processTotals.value.input)}`, |
| | | icon: Operation, |
| | | visible: visibleModules.value.production, |
| | | }, |
| | | ]); |
| | | ].filter((item) => item.visible)); |
| | | |
| | | const productionOrders = ref([ |
| | | { |
| | |
| | | }, |
| | | ]); |
| | | |
| | | const quickEntries = [ |
| | | { label: "生产排产", icon: Calendar, path: "/productionManagement/productionDispatching" }, |
| | | { label: "工序报工", icon: EditPen, path: "/productionManagement/productionReporting" }, |
| | | { label: "生产订单", icon: Tickets, path: "/productionManagement/productionOrder" }, |
| | | { label: "物料齐套", icon: Box, path: "/productionManagement/workOrderManagement" }, |
| | | { label: "质量检验", icon: Checked, path: "/qualityManagement/processInspection" }, |
| | | { label: "设备管理", icon: Tools, path: "/equipmentManagement/deviceInfo" }, |
| | | { label: "库存查询", icon: Search, path: "/inventoryManagement/stockManage" }, |
| | | { label: "数据报表", icon: DataAnalysis, path: "/reportAnalysis/reportManagement" }, |
| | | const quickEntryConfigs = [ |
| | | { label: "主生产计划", icon: Calendar, titles: ["主生产计划"], fallbackPath: "/productionPlan/productionPlan" }, |
| | | { label: "生产订单", icon: Tickets, titles: ["生产订单"], fallbackPath: "/productionManagement/productionOrder" }, |
| | | { label: "生产报工", icon: EditPen, titles: ["生产报工"], fallbackPath: "/productionManagement/productionReporting" }, |
| | | { label: "设备台账", icon: Tools, titles: ["设备台账"], fallbackPath: "/equipmentManagement/ledger" }, |
| | | { label: "销售台账", icon: DataLine, titles: ["销售台账"], fallbackPath: "/salesManagement/salesLedger" }, |
| | | { label: "采购台账", icon: ShoppingCartFull, titles: ["采购台账"], fallbackPath: "/procurementManagement/procurementLedger" }, |
| | | { label: "员工台账", icon: UserFilled, titles: ["员工台账", "在职员工台账"], fallbackPath: "/personnelManagement/employeeRecord" }, |
| | | { label: "库存管理", icon: Box, titles: ["库存管理"], fallbackPath: "/inventoryManagement/stockManage" }, |
| | | ]; |
| | | |
| | | const normalizeMenuTitle = (title) => String(title || "").replace(/\s+/g, "").trim(); |
| | | const normalizeRoutePath = (path) => |
| | | String(path || "") |
| | | .trim() |
| | | .replace(/\/+/g, "/") |
| | | .replace(/\/$/, "") |
| | | .toLowerCase(); |
| | | |
| | | const resolveRoutePath = (route, parentPath = "") => { |
| | | const currentPath = String(route?.path || "").trim(); |
| | | if (!currentPath) return parentPath || ""; |
| | | if (/^(https?:)?\/\//.test(currentPath)) return currentPath; |
| | | if (currentPath.startsWith("/")) return currentPath; |
| | | const basePath = parentPath && parentPath !== "/" ? parentPath.replace(/\/$/, "") : ""; |
| | | return `${basePath}/${currentPath}`.replace(/\/+/g, "/"); |
| | | }; |
| | | |
| | | const collectAccessibleRoutes = (routes = [], parentPath = "") => { |
| | | const items = []; |
| | | (routes || []).forEach((route) => { |
| | | if (!route) return; |
| | | const fullPath = resolveRoutePath(route, parentPath); |
| | | const title = route.meta?.title ?? route.title ?? ""; |
| | | if (title && fullPath && !String(route.redirect || "").includes("noRedirect")) { |
| | | items.push({ |
| | | title: normalizeMenuTitle(title), |
| | | path: normalizeRoutePath(fullPath), |
| | | }); |
| | | } |
| | | if (Array.isArray(route.children) && route.children.length > 0) { |
| | | items.push(...collectAccessibleRoutes(route.children, fullPath)); |
| | | } |
| | | }); |
| | | return items; |
| | | }; |
| | | |
| | | const accessibleMenuRoutes = computed(() => { |
| | | const routePool = |
| | | permissionStore.defaultRoutes?.length > 0 |
| | | ? permissionStore.defaultRoutes |
| | | : permissionStore.sidebarRouters?.length > 0 |
| | | ? permissionStore.sidebarRouters |
| | | : permissionStore.routes; |
| | | return collectAccessibleRoutes(routePool || []); |
| | | }); |
| | | |
| | | const moduleAccessConfig = { |
| | | sales: { |
| | | titles: ["销售管理", "销售台账"], |
| | | pathPrefixes: ["/salesmanagement"], |
| | | }, |
| | | procurement: { |
| | | titles: ["采购管理", "采购台账"], |
| | | pathPrefixes: ["/procurementmanagement"], |
| | | }, |
| | | inventory: { |
| | | titles: ["库存管理"], |
| | | pathPrefixes: ["/inventorymanagement"], |
| | | }, |
| | | production: { |
| | | titles: ["生产管理", "主生产计划", "生产订单", "生产报工"], |
| | | pathPrefixes: ["/productionmanagement", "/productionplan"], |
| | | }, |
| | | quality: { |
| | | titles: ["质量管理"], |
| | | pathPrefixes: ["/qualitymanagement"], |
| | | }, |
| | | equipment: { |
| | | titles: ["设备管理", "设备台账"], |
| | | pathPrefixes: ["/equipmentmanagement"], |
| | | }, |
| | | personnel: { |
| | | titles: ["人事管理", "员工台账", "在职员工台账"], |
| | | pathPrefixes: ["/personnelmanagement"], |
| | | }, |
| | | approval: { |
| | | titles: ["协同审批", "待办事项"], |
| | | pathPrefixes: ["/collaborativeapproval"], |
| | | }, |
| | | finance: { |
| | | titles: ["财务管理", "财务分析", "回款管理", "开票管理"], |
| | | pathPrefixes: ["/financesuite", "/financialmanagement"], |
| | | }, |
| | | }; |
| | | |
| | | const hasModuleAccess = (config) => |
| | | accessibleMenuRoutes.value.some((route) => { |
| | | const matchedTitle = (config.titles || []).some((title) => route.title === normalizeMenuTitle(title)); |
| | | const matchedPath = (config.pathPrefixes || []).some( |
| | | (prefix) => route.path === prefix || route.path.startsWith(`${prefix}/`) |
| | | ); |
| | | return matchedTitle || matchedPath; |
| | | }); |
| | | |
| | | const visibleModules = computed(() => ({ |
| | | sales: hasModuleAccess(moduleAccessConfig.sales), |
| | | procurement: hasModuleAccess(moduleAccessConfig.procurement), |
| | | inventory: hasModuleAccess(moduleAccessConfig.inventory), |
| | | production: hasModuleAccess(moduleAccessConfig.production), |
| | | quality: hasModuleAccess(moduleAccessConfig.quality), |
| | | equipment: hasModuleAccess(moduleAccessConfig.equipment), |
| | | personnel: hasModuleAccess(moduleAccessConfig.personnel), |
| | | approval: hasModuleAccess(moduleAccessConfig.approval), |
| | | finance: hasModuleAccess(moduleAccessConfig.finance), |
| | | })); |
| | | |
| | | const visiblePanels = computed(() => ({ |
| | | process: visibleModules.value.production, |
| | | order: visibleModules.value.production, |
| | | contract: visibleModules.value.sales, |
| | | quality: visibleModules.value.quality, |
| | | todo: visibleModules.value.approval, |
| | | 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 |
| | | ); |
| | | const hasRightPanels = computed( |
| | | () => visiblePanels.value.todo || visiblePanels.value.realtime || visiblePanels.value.quick || visiblePanels.value.plan || visiblePanels.value.receipt |
| | | ); |
| | | const hasVisiblePanels = computed(() => hasLeftPanels.value || hasRightPanels.value); |
| | | |
| | | const quickEntries = computed(() => |
| | | quickEntryConfigs |
| | | .map((item) => { |
| | | const targetRoute = accessibleMenuRoutes.value.find((route) => |
| | | item.titles.some((title) => route.title === normalizeMenuTitle(title)) |
| | | ); |
| | | const resolvedPath = targetRoute?.path || ""; |
| | | return resolvedPath |
| | | ? { |
| | | label: item.label, |
| | | icon: item.icon, |
| | | path: resolvedPath, |
| | | } |
| | | : null; |
| | | }) |
| | | .filter(Boolean) |
| | | ); |
| | | |
| | | const updateNowTime = () => { |
| | | nowTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | |
| | | }; |
| | | |
| | | const goToQuick = (path) => { |
| | | if (!path) return; |
| | | router.push(path).catch(() => {}); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | updateNowTime(); |
| | | clockTimer = setInterval(updateNowTime, 1000); |
| | | getBusinessData(); |
| | | analysisCustomer(); |
| | | todoInfoS(); |
| | | statisticsReceivable(); |
| | | qualityStatisticsInfo(); |
| | | getAmountHalfYearNum(); |
| | | getProcessList(); |
| | | refreshProcessStats(); |
| | | if (dashboardCards.value.length > 0) { |
| | | getBusinessData(); |
| | | } |
| | | if (visiblePanels.value.contract) { |
| | | analysisCustomer(); |
| | | } |
| | | if (visiblePanels.value.todo) { |
| | | todoInfoS(); |
| | | } |
| | | if (visiblePanels.value.quality) { |
| | | qualityStatisticsInfo(); |
| | | } |
| | | if (visiblePanels.value.receipt) { |
| | | statisticsReceivable(); |
| | | getAmountHalfYearNum(); |
| | | } |
| | | if (visiblePanels.value.process) { |
| | | getProcessList(); |
| | | refreshProcessStats(); |
| | | } |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | overflow-x: clip; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .digital-number { |
| | |
| | | color: var(--text-tertiary); |
| | | } |
| | | |
| | | .empty-home-panel { |
| | | min-height: 220px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #64748b; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .main-grid { |
| | | display: grid; |
| | | grid-template-columns: minmax(0, 1fr) 400px; |