refactor(首页): 展示模块改为跟随菜单权限自动隐藏
两套首页(/analytics 与 MES 首页)原先用 canNavigate 判断模块开关,它只看路由
是否注册过;而后端菜单 visible=0 只是被前端转成 meta.hideInMenu,路由依旧注册,
只在 generateMenus 剪枝时连同子树从侧边栏移除,所以菜单已关闭的模块照旧显示。
- 新增共享判据 canSeeMenu:路由存在且从根到该路由的祖先链上无一级被隐藏,与
侧边栏剪枝口径一致;use-home-access.ts 移到 src/utils 供两个首页共用
- /analytics:MES 生产 KPI、AI 智能分析、生产趋势、工单状态分布、经营 KPI 各卡
按各自菜单显隐,列宽随可见性自适应(欢迎栏、快捷操作保持常驻)
- MES 首页:KPI 卡、生产趋势、订单状态分布、待办异常、快捷入口同判据显隐
- 修正对不上任何菜单的路由名(MesQcRecord→MesQcIqc、MesProRepair→MesDvRepair、
BpmTaskTodo→BpmTodoTask、WlsMaterialStock→MesWmMaterialStock 等),
并删除无对应页面的「数据报表」入口
Co-Authored-By: Claude Code <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | /** é¦é¡µå¸¸é©»ï¼ä¸å¡æ°æ®åå
¥å£ä»ééµå¾ªå½åç¨æ·çèåæéã */ |
| | | export function useHomeAccess() { |
| | | const accessStore = useAccessStore(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | function canQuery(permission: string) { |
| | | return hasAccessByCodes([permission, '*:*:*']); |
| | | } |
| | | |
| | | function canNavigate(name: string) { |
| | | function contains(routes: RouteRecordRaw[]): boolean { |
| | | return routes.some((route) => { |
| | | if (route.meta?.menuVisibleWithForbidden) return false; |
| | | return route.name === name || contains(route.children ?? []); |
| | | }); |
| | | } |
| | | return contains(accessStore.accessRoutes); |
| | | } |
| | | |
| | | /** |
| | | * 该èå对å½åç¨æ·æ¯å¦ãå¼çãââ ç¨äºé¦é¡µæ¨¡åå¼å
³ã |
| | | * |
| | | * å端èå `visible = 0` ä¼è¢«å端转æè·¯ç±ç `meta.hideInMenu`ï¼è¿ç±»è·¯ç±ä»ç¶æ³¨åå¨ |
| | | * router éï¼`canNavigate` 为 trueï¼é¡µé¢å¯ä»¥ç´æ¥è®¿é®ï¼ï¼åªæ¯æ´æ£µåæ ä¸ä¼åºç°å¨ä¾§è¾¹æ ã |
| | | * æä»¥å¤æãè¿ä¸ªæ¨¡åææ²¡æå¼ãä¸è½åªçè·¯ç±æ¯å¦åå¨ï¼è¿è¦çå®åå®çæ¯ä¸çº§ç¥å
æ¯å¦å¯è§ã |
| | | */ |
| | | function canSeeMenu(name: string) { |
| | | function find( |
| | | routes: RouteRecordRaw[], |
| | | ancestorHidden: boolean, |
| | | ): boolean | undefined { |
| | | for (const route of routes) { |
| | | const hidden = |
| | | ancestorHidden || |
| | | route.meta?.hideInMenu === true || |
| | | route.meta?.menuVisibleWithForbidden === true; |
| | | if (route.name === name) return !hidden; |
| | | const found = find(route.children ?? [], hidden); |
| | | if (found !== undefined) return found; |
| | | } |
| | | return undefined; |
| | | } |
| | | return find(accessStore.accessRoutes, false) === true; |
| | | } |
| | | |
| | | const canViewHome = computed(() => canQuery('mes:home:query')); |
| | | |
| | | return { canNavigate, canQuery, canSeeMenu, canViewHome }; |
| | | } |
| | |
| | | |
| | | import { getHomeSummary } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { defaultSummary } from './data'; |
| | | import AiInsights from './modules/ai-insights.vue'; |
| | | import AlertPanel from './modules/alert-panel.vue'; |
| | |
| | | import KpiCards from './modules/kpi-cards.vue'; |
| | | import ProductionTrend from './modules/production-trend.vue'; |
| | | import QuickActions from './modules/quick-actions.vue'; |
| | | import { useHomeAccess } from './use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardAnalytics' }); |
| | | |
| | | const router = useRouter(); |
| | | const summary = ref(defaultSummary); |
| | | const { canNavigate, canViewHome } = useHomeAccess(); |
| | | const { canNavigate, canSeeMenu, canViewHome } = useHomeAccess(); |
| | | |
| | | // é¦é¡µæ¨¡åè·éä¾§è¾¹æ èåï¼èåæ²¡å¼çæ¨¡åæ´åä¸å±ç¤º |
| | | /** MES ç产 KPIï¼ä»»æä¸å¼ å¡çèåå¼çå°±æ¸²ææ´è¡ */ |
| | | const showKpi = computed( |
| | | () => |
| | | canViewHome.value && |
| | | ['MesProWorkOrder', 'MesProFeedback', 'MesQcIqc', 'MesDvMachinery'].some( |
| | | (name) => canSeeMenu(name), |
| | | ), |
| | | ); |
| | | /** AI æºè½åæï¼AI 模åçèåå¼çææ¸²æ */ |
| | | const showAiInsights = computed( |
| | | () => canViewHome.value && canSeeMenu('AiChat'), |
| | | ); |
| | | /** ç产è¶å¿ï¼æ°æ®æ¥èªç产æ¥å·¥ */ |
| | | const showTrend = computed( |
| | | () => canViewHome.value && canSeeMenu('MesProFeedback'), |
| | | ); |
| | | /** å·¥åç¶æåå¸ï¼æ°æ®æ¥èªç产订å */ |
| | | const showWorkOrderStatus = computed( |
| | | () => canViewHome.value && canSeeMenu('MesProWorkOrder'), |
| | | ); |
| | | |
| | | const greeting = computed(() => { |
| | | const hour = new Date().getHours(); |
| | |
| | | </div> |
| | | |
| | | <!-- MES ç产 KPI --> |
| | | <KpiCards v-if="canViewHome" :summary="summary" @navigate="handleNavigate" /> |
| | | <KpiCards v-if="showKpi" :summary="summary" @navigate="handleNavigate" /> |
| | | |
| | | <!-- ERP/WMS/BPM ç»è¥ KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | v-if="canViewHome && canNavigate('MesProWorkOrder')" |
| | | v-if="showAiInsights" |
| | | :active-order-count="summary.workOrderActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | | |
| | | <!-- ç产è¶å¿ + é¢è¦é¢æ¿ --> |
| | | <Row v-if="canViewHome" :gutter="16" class="mb-4"> |
| | | <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4"> |
| | | <Row v-if="showTrend || showWorkOrderStatus" :gutter="16" class="mb-4"> |
| | | <Col |
| | | v-if="showTrend" |
| | | :lg="showWorkOrderStatus ? 14 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showWorkOrderStatus ? 14 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100"> |
| | | ç产è¶å¿ï¼è¿7å¤©ï¼ |
| | |
| | | <ProductionTrend /> |
| | | </div> |
| | | </Col> |
| | | <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showWorkOrderStatus" |
| | | :lg="showTrend ? 10 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showTrend ? 10 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <AlertPanel |
| | | :repair-active-count="summary.repairActiveCount" |
| | | @navigate="handleNavigate" |
| | |
| | | |
| | | import { getWorkOrderStatusDistribution } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardAlertPanel' }); |
| | | |
| | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const loading = ref(false); |
| | | const { canNavigate, canViewHome } = useHomeAccess(); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | async function loadStatus() { |
| | | if (!canViewHome.value) return; |
| | | // å·¥åç¶æåå¸åèªç产订åï¼è®¢åèåæ²¡å¼å°±ä¸è¯·æ± |
| | | if (!canSeeMenu('MesProWorkOrder')) return; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getWorkOrderStatusDistribution(); |
| | |
| | | <!-- é¢è¦å¡çè¡ --> |
| | | <div class="grid grid-cols-1 gap-3"> |
| | | <div |
| | | v-if="canNavigate('MesProRepair')" |
| | | v-if="canSeeMenu('MesDvRepair')" |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProRepair')" |
| | | @click="emit('navigate', 'MesDvRepair')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-3"> |
| | | <div class="flex size-10 items-center justify-center rounded-lg bg-amber-50 text-amber-600 shadow-sm dark:bg-amber-500/15 dark:text-amber-400"> |
| | |
| | | </div> |
| | | |
| | | <!-- å·¥åç¶æå¾ --> |
| | | <div class="glass-tilt rounded-2xl p-5"> |
| | | <div v-if="canSeeMenu('MesProWorkOrder')" class="glass-tilt rounded-2xl p-5"> |
| | | <h4 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-100"> |
| | | å·¥åç¶æåå¸ |
| | | </h4> |
| | |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardBizCards' }); |
| | | |
| | |
| | | const saleCount = ref(0); |
| | | const stockCount = ref(0); |
| | | const bpmTodoCount = ref(0); |
| | | const { canNavigate, canQuery } = useHomeAccess(); |
| | | const canViewPurchase = computed(() => canQuery('erp:purchase-order:query') && canNavigate('ErpPurchaseOrder')); |
| | | const canViewSale = computed(() => canQuery('erp:sale-order:query') && canNavigate('ErpSaleOrder')); |
| | | const canViewStock = computed(() => canQuery('mes:wm-material-stock:query') && canNavigate('MesWmMaterialStock')); |
| | | const canViewBpmTodo = computed(() => canQuery('bpm:task:query') && canNavigate('BpmTaskTodo')); |
| | | const { canSeeMenu, canQuery } = useHomeAccess(); |
| | | const canViewPurchase = computed(() => canQuery('erp:purchase-order:query') && canSeeMenu('ErpPurchaseOrder')); |
| | | const canViewSale = computed(() => canQuery('erp:sale-order:query') && canSeeMenu('ErpSaleOrder')); |
| | | const canViewStock = computed(() => canQuery('mes:wm-material-stock:query') && canSeeMenu('MesWmMaterialStock')); |
| | | const canViewBpmTodo = computed(() => canQuery('bpm:task:query') && canSeeMenu('BpmTodoTask')); |
| | | |
| | | async function loadPurchase() { |
| | | if (!canViewPurchase.value) return; |
| | |
| | | :style="tiltStyle('bpm')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'bpm')" |
| | | @mouseleave="onMouseLeave('bpm')" |
| | | @click="emit('navigate', 'BpmTaskTodo')" |
| | | @click="emit('navigate', 'BpmTodoTask')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-red-500 to-rose-400 shadow-lg shadow-red-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardKpiCards' }); |
| | | |
| | | const { canNavigate } = useHomeAccess(); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | const props = defineProps<{ |
| | | summary: MesHomeApi.Summary; |
| | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- ç产订å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canSeeMenu('MesProWorkOrder')" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProWorkOrder') }" |
| | | :class="{ 'cursor-pointer': canSeeMenu('MesProWorkOrder') }" |
| | | :style="tiltStyle('order')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')" |
| | | @mouseleave="onMouseLeave('order')" |
| | | @click="canNavigate('MesProWorkOrder') && emit('navigate', 'MesProWorkOrder')" |
| | | @click="canSeeMenu('MesProWorkOrder') && emit('navigate', 'MesProWorkOrder')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | </Col> |
| | | |
| | | <!-- 仿¥äº§é --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canSeeMenu('MesProFeedback')" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProFeedback') }" |
| | | :class="{ 'cursor-pointer': canSeeMenu('MesProFeedback') }" |
| | | :style="tiltStyle('output')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')" |
| | | @mouseleave="onMouseLeave('output')" |
| | | @click="canNavigate('MesProFeedback') && emit('navigate', 'MesProFeedback')" |
| | | @click="canSeeMenu('MesProFeedback') && emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-green-400 shadow-lg shadow-emerald-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | </Col> |
| | | |
| | | <!-- è´¨éåæ ¼ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canSeeMenu('MesProFeedback')" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesQcRecord') }" |
| | | :class="{ 'cursor-pointer': canSeeMenu('MesProFeedback') }" |
| | | :style="tiltStyle('quality')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')" |
| | | @mouseleave="onMouseLeave('quality')" |
| | | @click="canNavigate('MesQcRecord') && emit('navigate', 'MesQcRecord')" |
| | | @click="canSeeMenu('MesProFeedback') && emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | </Col> |
| | | |
| | | <!-- 设å¤è¿è¡ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canSeeMenu('MesDvMachinery')" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesDvMachinery') }" |
| | | :class="{ 'cursor-pointer': canSeeMenu('MesDvMachinery') }" |
| | | :style="tiltStyle('equip')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')" |
| | | @mouseleave="onMouseLeave('equip')" |
| | | @click="canNavigate('MesDvMachinery') && emit('navigate', 'MesDvMachinery')" |
| | | @click="canSeeMenu('MesDvMachinery') && emit('navigate', 'MesDvMachinery')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-purple-400 shadow-lg shadow-violet-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | |
| | | |
| | | import { getProductionTrend } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardProductionTrend' }); |
| | | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const { canViewHome } = useHomeAccess(); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | async function loadTrend() { |
| | | if (!canViewHome.value) return; |
| | | // ç产è¶å¿åèªç产æ¥å·¥ï¼æ¥å·¥èåæ²¡å¼å°±ä¸è¯·æ± |
| | | if (!canSeeMenu('MesProFeedback')) return; |
| | | const data = await getProductionTrend(7); |
| | | const dates = data.map((d: MesHomeApi.ProductionTrend) => d.date.slice(5)); |
| | | const quantities = data.map((d: MesHomeApi.ProductionTrend) => d.quantity); |
| | |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { useHomeAccess } from '../use-home-access'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardQuickActions' }); |
| | | |
| | |
| | | const actions: QuickAction[] = [ |
| | | { title: 'ç产工å', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' }, |
| | | { title: 'ç产æ¥å·¥', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' }, |
| | | { title: 'è´¨æ£ç®¡ç', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' }, |
| | | { title: 'è´¨æ£ç®¡ç', icon: 'lucide:clipboard-check', route: 'MesQcIqc', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' }, |
| | | { title: '设å¤ç¹æ£', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' }, |
| | | { title: 'ç产æç¨', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' }, |
| | | { title: 'ç©æåºå', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, |
| | | { title: 'å·¥èºè·¯çº¿', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' }, |
| | | { title: 'æ°æ®æ¥è¡¨', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' }, |
| | | { title: 'ç产æç¨', icon: 'lucide:calendar-range', route: 'MesProTask', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' }, |
| | | { title: 'ç©æåºå', icon: 'lucide:package-search', route: 'MesWmMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, |
| | | { title: 'å·¥èºè·¯çº¿', icon: 'lucide:route', route: 'MesProcessDesignRoute', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' }, |
| | | ]; |
| | | |
| | | const { canNavigate } = useHomeAccess(); |
| | | const accessibleActions = computed(() => actions.filter((action) => canNavigate(action.route))); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | const accessibleActions = computed(() => |
| | | actions.filter((action) => canSeeMenu(action.route)), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | |
| | | import { MesProWorkOrderStatusEnum } from '@vben/constants'; |
| | | |
| | | /** é¦é¡µå模åä¾èµçèåï¼route.name = å端èåç component_nameï¼ï¼èåæ²¡å¼çæ¨¡åæ´åä¸å±ç¤º */ |
| | | export const HOME_BLOCK_MENUS = { |
| | | /** å®ç¯å¼å« */ |
| | | andon: 'MesProAndonRecord', |
| | | /** å°è´§éç¥ */ |
| | | arrivalNotice: 'MesWmArrivalNotice', |
| | | /** ç产æ¥å·¥ */ |
| | | feedback: 'MesProFeedback', |
| | | /** 设å¤å°è´¦ */ |
| | | machinery: 'MesDvMachinery', |
| | | /** åºåç°æé */ |
| | | materialStock: 'MesWmMaterialStock', |
| | | /** å·¥åºæµè½¬ï¼æµè½¬å¡ï¼ */ |
| | | processCard: 'MesProCard', |
| | | /** ç产æäº§ï¼ç产任å¡ï¼ */ |
| | | proTask: 'MesProTask', |
| | | /** æ¥ææ£éª */ |
| | | qcIqc: 'MesQcIqc', |
| | | /** ç»´ä¿®å */ |
| | | repair: 'MesDvRepair', |
| | | /** ç产订å */ |
| | | workOrder: 'MesProWorkOrder', |
| | | } as const; |
| | | |
| | | /** ç产 KPI å¡çä¾èµçèå */ |
| | | export const KPI_MENUS: string[] = [ |
| | | HOME_BLOCK_MENUS.workOrder, |
| | | HOME_BLOCK_MENUS.feedback, |
| | | HOME_BLOCK_MENUS.machinery, |
| | | ]; |
| | | |
| | | /** å¾
åä¸å¼å¸¸æ¡ç®ä¾èµçèå */ |
| | | export const ALERT_MENUS: string[] = [ |
| | | HOME_BLOCK_MENUS.andon, |
| | | HOME_BLOCK_MENUS.repair, |
| | | HOME_BLOCK_MENUS.workOrder, |
| | | ]; |
| | | |
| | | /** å¿«æ·å
¥å£ï¼åç§°ã徿 ãåºè²ä¸ä¾èµçèå */ |
| | | export const HOME_SHORTCUTS: { |
| | | bgColor: string; |
| | | icon: string; |
| | | menu: string; |
| | | name: string; |
| | | }[] = [ |
| | | { |
| | | bgColor: '#409EFF', |
| | | icon: 'lucide:file-text', |
| | | menu: HOME_BLOCK_MENUS.workOrder, |
| | | name: 'ç产订å', |
| | | }, |
| | | { |
| | | bgColor: '#67C23A', |
| | | icon: 'lucide:edit', |
| | | menu: HOME_BLOCK_MENUS.feedback, |
| | | name: 'ç产æ¥å·¥', |
| | | }, |
| | | { |
| | | bgColor: '#E6A23C', |
| | | icon: 'lucide:search', |
| | | menu: HOME_BLOCK_MENUS.qcIqc, |
| | | name: 'è´¨éæ£éª', |
| | | }, |
| | | { |
| | | bgColor: '#F56C6C', |
| | | icon: 'lucide:box', |
| | | menu: HOME_BLOCK_MENUS.materialStock, |
| | | name: 'åºåæ¥è¯¢', |
| | | }, |
| | | { |
| | | bgColor: '#7c3aed', |
| | | icon: 'lucide:cpu', |
| | | menu: HOME_BLOCK_MENUS.machinery, |
| | | name: '设å¤ç®¡ç', |
| | | }, |
| | | { |
| | | bgColor: '#0ea5e9', |
| | | icon: 'lucide:list', |
| | | menu: HOME_BLOCK_MENUS.proTask, |
| | | name: 'ç产任å¡', |
| | | }, |
| | | { |
| | | bgColor: '#14b8a6', |
| | | icon: 'lucide:truck', |
| | | menu: HOME_BLOCK_MENUS.arrivalNotice, |
| | | name: 'å°è´§éç¥', |
| | | }, |
| | | { |
| | | bgColor: '#f59e0b', |
| | | icon: 'lucide:settings-2', |
| | | menu: HOME_BLOCK_MENUS.repair, |
| | | name: '设å¤ç»´ä¿®', |
| | | }, |
| | | { |
| | | bgColor: '#ec4899', |
| | | icon: 'lucide:tickets', |
| | | menu: HOME_BLOCK_MENUS.processCard, |
| | | name: 'æµè½¬å¡', |
| | | }, |
| | | ]; |
| | | |
| | | /** é¦é¡µæ±æ»ç»è®¡é»è®¤å¼ */ |
| | | export const defaultSummary: MesHomeApi.Summary = { |
| | | andonActiveCount: 0, |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesHomeApi } from '#/api/mes/home'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { getHomeSummary } from '#/api/mes/home'; |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { defaultSummary } from './data'; |
| | | import { |
| | | ALERT_MENUS, |
| | | defaultSummary, |
| | | HOME_BLOCK_MENUS, |
| | | HOME_SHORTCUTS, |
| | | } from './data'; |
| | | import AlertPanel from './modules/alert-panel.vue'; |
| | | import KpiCards from './modules/kpi-cards.vue'; |
| | | import ProductionTrend from './modules/production-trend.vue'; |
| | |
| | | |
| | | const router = useRouter(); |
| | | const summary = ref<MesHomeApi.Summary>(defaultSummary); // é¦é¡µæ±æ»ç»è®¡ |
| | | |
| | | // é¦é¡µæ¨¡åè·éä¾§è¾¹æ èåï¼èåæ²¡å¼çæ¨¡åæ´åä¸å±ç¤º |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | /** ç产è¶å¿ï¼åèªç产æ¥å·¥ï¼ */ |
| | | const showTrend = computed(() => canSeeMenu(HOME_BLOCK_MENUS.feedback)); |
| | | /** å¾
åä¸å¼å¸¸ï¼ä»»æä¸æ¡çèåå¼ç就渲æè¿ä¸å */ |
| | | const showAlert = computed(() => ALERT_MENUS.some((menu) => canSeeMenu(menu))); |
| | | /** 订åç¶æåå¸ï¼åèªç产订åï¼ */ |
| | | const showChart = computed(() => canSeeMenu(HOME_BLOCK_MENUS.workOrder)); |
| | | /** å¿«æ·å
¥å£ï¼ä»»æä¸ä¸ªå
¥å£çèåå¼ç就渲æè¿ä¸å */ |
| | | const showShortcuts = computed(() => |
| | | HOME_SHORTCUTS.some((item) => canSeeMenu(item.menu)), |
| | | ); |
| | | |
| | | /** 跳转å°ç®æ 页é¢ï¼æè·¯ç± nameï¼ */ |
| | | function handleNavigate(name: string) { |
| | |
| | | <KpiCards :summary="summary" class="mb-4" @navigate="handleNavigate" /> |
| | | |
| | | <!-- 第äºè¡ï¼ç产è¶å¿ + å¾
åå¼å¸¸ --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :lg="16" :md="24" :sm="24" :xl="16" :xs="24" class="mb-4"> |
| | | <Row v-if="showTrend || showAlert" :gutter="16" class="mb-4"> |
| | | <Col |
| | | v-if="showTrend" |
| | | :lg="showAlert ? 16 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showAlert ? 16 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <ProductionTrend /> |
| | | </Col> |
| | | <Col :lg="8" :md="24" :sm="24" :xl="8" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showAlert" |
| | | :lg="showTrend ? 8 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showTrend ? 8 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <AlertPanel :summary="summary" @navigate="handleNavigate" /> |
| | | </Col> |
| | | </Row> |
| | | |
| | | <!-- 第ä¸è¡ï¼è®¢ååå¸ + å¿«æ·å
¥å£ --> |
| | | <Row :gutter="16"> |
| | | <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4"> |
| | | <Row v-if="showChart || showShortcuts" :gutter="16"> |
| | | <Col |
| | | v-if="showChart" |
| | | :lg="showShortcuts ? 12 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showShortcuts ? 12 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <WorkOrderChart /> |
| | | </Col> |
| | | <Col :lg="12" :md="24" :sm="24" :xl="12" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showShortcuts" |
| | | :lg="showChart ? 12 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showChart ? 12 : 24" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Shortcuts @navigate="handleNavigate" /> |
| | | </Col> |
| | | </Row> |
| | |
| | | |
| | | import { Badge, Card } from 'ant-design-vue'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { HOME_BLOCK_MENUS } from '../data'; |
| | | |
| | | defineOptions({ name: 'MesHomeAlertPanel' }); |
| | | |
| | | const props = defineProps<{ |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | /** å¾
åæéåè¡¨ï¼æ ç¾ãæè¿°ã徿 ãç®æ è·¯ç±åç§°ãæ°é */ |
| | | const alertItems = computed(() => [ |
| | | { |
| | | count: props.summary.andonActiveCount, |
| | | desc: 'æªå¤ç½®çå®ç¯å¼å«', |
| | | icon: 'lucide:bell-ring', |
| | | iconClass: 'bg-red-50 text-red-500', |
| | | label: 'å®ç¯æ¥è¦', |
| | | routeName: 'MesProAndonRecord', |
| | | }, |
| | | { |
| | | count: props.summary.repairActiveCount, |
| | | desc: 'å¾
å¤çç维修订å', |
| | | icon: 'lucide:wrench', |
| | | iconClass: 'bg-orange-50 text-orange-500', |
| | | label: '设å¤ç»´ä¿®', |
| | | routeName: 'MesDvRepair', |
| | | }, |
| | | { |
| | | count: props.summary.workOrderPrepareCount, |
| | | desc: 'èç¨¿ç¶æçç产订å', |
| | | icon: 'lucide:clipboard-list', |
| | | iconClass: 'bg-blue-50 text-blue-500', |
| | | label: 'å¾
æäº§è®¢å', |
| | | routeName: 'MesProWorkOrder', |
| | | }, |
| | | ]); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** å¾
åæéåè¡¨ï¼æ ç¾ãæè¿°ã徿 ãç®æ è·¯ç±åç§°ãæ°éï¼èåæ²¡å¼çæ¡ç®ä¸å±ç¤º */ |
| | | const alertItems = computed(() => |
| | | [ |
| | | { |
| | | count: props.summary.andonActiveCount, |
| | | desc: 'æªå¤ç½®çå®ç¯å¼å«', |
| | | icon: 'lucide:bell-ring', |
| | | iconClass: 'bg-red-50 text-red-500', |
| | | label: 'å®ç¯æ¥è¦', |
| | | routeName: HOME_BLOCK_MENUS.andon, |
| | | }, |
| | | { |
| | | count: props.summary.repairActiveCount, |
| | | desc: 'å¾
å¤çç维修订å', |
| | | icon: 'lucide:wrench', |
| | | iconClass: 'bg-orange-50 text-orange-500', |
| | | label: '设å¤ç»´ä¿®', |
| | | routeName: HOME_BLOCK_MENUS.repair, |
| | | }, |
| | | { |
| | | count: props.summary.workOrderPrepareCount, |
| | | desc: 'èç¨¿ç¶æçç产订å', |
| | | icon: 'lucide:clipboard-list', |
| | | iconClass: 'bg-blue-50 text-blue-500', |
| | | label: 'å¾
æäº§è®¢å', |
| | | routeName: HOME_BLOCK_MENUS.workOrder, |
| | | }, |
| | | ].filter((item) => canSeeMenu(item.routeName)), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Card title="å¾
åä¸å¼å¸¸" class="h-full"> |
| | | <Card v-if="alertItems.length" title="å¾
åä¸å¼å¸¸" class="h-full"> |
| | | <div class="flex flex-col"> |
| | | <div |
| | | v-for="item in alertItems" |
| | |
| | | |
| | | import { Card, Col, Divider, Row } from 'ant-design-vue'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { HOME_BLOCK_MENUS } from '../data'; |
| | | |
| | | defineOptions({ name: 'MesHomeKpiCards' }); |
| | | |
| | | const props = defineProps<{ |
| | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | /** èåæ²¡å¼ç模åï¼å¯¹åº KPI å¡ä¸å±ç¤º */ |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** æ¯å¦æè´¨éæ°æ®ï¼åæ ¼å + ä¸è¯å > 0ï¼ */ |
| | | const hasQualityData = computed( |
| | |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canSeeMenu(HOME_BLOCK_MENUS.workOrder)" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="6" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProWorkOrder')" |
| | | @click="emit('navigate', HOME_BLOCK_MENUS.workOrder)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canSeeMenu(HOME_BLOCK_MENUS.feedback)" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="6" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | @click="emit('navigate', HOME_BLOCK_MENUS.feedback)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canSeeMenu(HOME_BLOCK_MENUS.feedback)" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="6" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | @click="emit('navigate', HOME_BLOCK_MENUS.feedback)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canSeeMenu(HOME_BLOCK_MENUS.machinery)" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="6" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesDvMachinery')" |
| | | @click="emit('navigate', HOME_BLOCK_MENUS.machinery)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | |
| | | |
| | | import { getProductionTrend } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { getProductionTrendChartOptions } from '../chart-options'; |
| | | import { HOME_BLOCK_MENUS } from '../data'; |
| | | |
| | | defineOptions({ name: 'MesHomeProductionTrend' }); |
| | | |
| | | const trendDays = ref(7); // å½åéä¸ç天æ°èå´ |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** ç产è¶å¿åèªç产æ¥å·¥ï¼æ¥å·¥èåæ²¡å¼å°±ä¸å±ç¤ºãä¸è¯·æ± */ |
| | | const showTrend = computed(() => canSeeMenu(HOME_BLOCK_MENUS.feedback)); |
| | | |
| | | /** å è½½ç产è¶å¿æ°æ®å¹¶æ¸²æå¾è¡¨ */ |
| | | async function loadData() { |
| | | if (!showTrend.value) return; |
| | | const data = await getProductionTrend(trendDays.value); |
| | | const dates = data.map((d) => d.date.slice(5)); |
| | | const quantities = data.map((d) => d.quantity); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Card title="ç产è¶å¿" class="h-full"> |
| | | <Card v-if="showTrend" title="ç产è¶å¿" class="h-full"> |
| | | <template #extra> |
| | | <RadioGroup |
| | | v-model:value="trendDays" |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Card, Col, Row } from 'ant-design-vue'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { HOME_SHORTCUTS } from '../data'; |
| | | |
| | | defineOptions({ name: 'MesHomeShortcuts' }); |
| | | |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | /** å¿«æ·å
¥å£å表ï¼3Ã3 ç½æ ¼å¸å±ä¸è®¢åç¶æåå¸é¢æ¿çé« */ |
| | | const shortcuts = [ |
| | | { |
| | | bgColor: '#409EFF', |
| | | icon: 'lucide:file-text', |
| | | name: 'ç产订å', |
| | | routeName: 'MesProWorkOrder', |
| | | }, |
| | | { |
| | | bgColor: '#67C23A', |
| | | icon: 'lucide:edit', |
| | | name: 'ç产æ¥å·¥', |
| | | routeName: 'MesProFeedback', |
| | | }, |
| | | { |
| | | bgColor: '#E6A23C', |
| | | icon: 'lucide:search', |
| | | name: 'è´¨éæ£éª', |
| | | routeName: 'MesQcIqc', |
| | | }, |
| | | { |
| | | bgColor: '#F56C6C', |
| | | icon: 'lucide:box', |
| | | name: 'åºåæ¥è¯¢', |
| | | routeName: 'MesWmMaterialStock', |
| | | }, |
| | | { |
| | | bgColor: '#7c3aed', |
| | | icon: 'lucide:cpu', |
| | | name: '设å¤ç®¡ç', |
| | | routeName: 'MesDvMachinery', |
| | | }, |
| | | { |
| | | bgColor: '#0ea5e9', |
| | | icon: 'lucide:list', |
| | | name: 'ç产任å¡', |
| | | routeName: 'MesProTask', |
| | | }, |
| | | { |
| | | bgColor: '#14b8a6', |
| | | icon: 'lucide:truck', |
| | | name: 'å°è´§éç¥', |
| | | routeName: 'MesWmArrivalNotice', |
| | | }, |
| | | { |
| | | bgColor: '#f59e0b', |
| | | icon: 'lucide:settings-2', |
| | | name: '设å¤ç»´ä¿®', |
| | | routeName: 'MesDvRepair', |
| | | }, |
| | | { |
| | | bgColor: '#ec4899', |
| | | icon: 'lucide:tickets', |
| | | name: 'æµè½¬å¡', |
| | | routeName: 'MesProCard', |
| | | }, |
| | | ]; |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** å¿«æ·å
¥å£å表ï¼3Ã3 ç½æ ¼å¸å±ä¸è®¢åç¶æåå¸é¢æ¿çé«ï¼èåæ²¡å¼çä¸å±ç¤º */ |
| | | const shortcuts = computed(() => |
| | | HOME_SHORTCUTS.filter((item) => canSeeMenu(item.menu)), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Card title="å¿«æ·å
¥å£" class="h-full"> |
| | | <Card v-if="shortcuts.length" title="å¿«æ·å
¥å£" class="h-full"> |
| | | <Row :gutter="16"> |
| | | <Col v-for="item in shortcuts" :key="item.name" :span="8" class="mb-4"> |
| | | <div |
| | | class="hover:bg-accent flex cursor-pointer flex-col items-center gap-2 rounded-lg py-3 transition-all hover:-translate-y-0.5" |
| | | @click="emit('navigate', item.routeName)" |
| | | @click="emit('navigate', item.menu)" |
| | | > |
| | | <div |
| | | class="flex size-12 items-center justify-center rounded-xl text-white" |
| | |
| | | <script lang="ts" setup> |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | |
| | | |
| | | import { getWorkOrderStatusDistribution } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | import { getWorkOrderStatusChartOptions } from '../chart-options'; |
| | | import { WORK_ORDER_STATUS_COLOR_MAP } from '../data'; |
| | | import { HOME_BLOCK_MENUS, WORK_ORDER_STATUS_COLOR_MAP } from '../data'; |
| | | |
| | | defineOptions({ name: 'MesHomeWorkOrderChart' }); |
| | | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** èåæ²¡å¼å°±ä¸å±ç¤ºãä¸è¯·æ± */ |
| | | const showChart = computed(() => canSeeMenu(HOME_BLOCK_MENUS.workOrder)); |
| | | |
| | | /** å 载订åç¶æå叿°æ®å¹¶æ¸²æé¥¼å¾ */ |
| | | async function loadData() { |
| | | if (!showChart.value) return; |
| | | const data = await getWorkOrderStatusDistribution(); |
| | | const chartData = data.map((d) => ({ |
| | | itemStyle: { color: WORK_ORDER_STATUS_COLOR_MAP[d.status] || '#409EFF' }, |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Card title="订åç¶æåå¸" class="h-full"> |
| | | <Card v-if="showChart" title="订åç¶æåå¸" class="h-full"> |
| | | <EchartsUI ref="chartRef" class="h-[280px] w-full" /> |
| | | </Card> |
| | | </template> |