refactor(首页): 展示模块跟随菜单权限自动隐藏
权限少的账号打开首页会挨个弹「没有该操作权限」:卡片不看权限就发请求,
403 被全局拦截器弹成 toast,取不到数的卡片还留着 0 的假数字。
- 新增共享判据 src/utils/use-home-access.ts:canQuery(接口权限码,带超管
兜底,与后端 @ss.hasPermission 对超管放行的口径一致)、canSeeMenu(菜单
开着;后端菜单 visible=0 只是 meta.hideInMenu,路由照样注册,所以要连祖先
一起看,和侧边栏剪枝一个口径)
- /analytics:生产 KPI 四张卡、经营 KPI 四张卡、生产趋势、工单状态/维修预警、
AI 智能分析块、快捷入口各按自己的权限与菜单显隐;没权限的接口直接不发请求,
列宽随可见卡片数自适应
- 顺手修正对不上九鸿任何菜单的路由名:MesQcRecord→MesQcIqc、
MesProScheduling→MesProTask、WlsMaterialStock→MesWmMaterialStock、
MesPdRouting→MesProcessDesignRoute、MesProRepair→MesDvRepair;删掉指向
已不存在的 MesHomeIndex 的「数据报表」入口
同一套判据先在万通 8a2832d0 落地,九鸿按自己的首页布局(生产 KPI/生产趋势/
预警面板)套用。九鸿的菜单目录节点是停用状态,普通角色拿不到这些权限码,
卡片会跟着菜单一起隐藏。
Co-Authored-By: Claude Code <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { useAccessStore, useUserStore } from '@vben/stores'; |
| | | |
| | | /** é¦é¡µå¸¸é©»ï¼ä¸å¡æ°æ®åå
¥å£ä»ééµå¾ªå½åç¨æ·çèåæéã */ |
| | | export function useHomeAccess() { |
| | | const accessStore = useAccessStore(); |
| | | const userStore = useUserStore(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | /** |
| | | * æ¥å£æéï¼æ²¡æå¯¹åºæéç å°±åä¸å°æ°ï¼è¯·æ±ä¼ç´æ¥ 403ï¼æä»¥åæ°åå
é®ä¸å¥ã |
| | | * |
| | | * å端 `@ss.hasPermission` 对è¶
管æ¯ç´æ¥æ¾è¡çï¼ä¸çæéç ï¼ï¼å端è¿éä¿æä¸è´ï¼ |
| | | * å¦åè¶
管ä¼å 为æéç æ²¡å¨èåéé
èå°çå°å¡çã |
| | | */ |
| | | function canQuery(permission: string) { |
| | | return ( |
| | | userStore.userRoles.includes('super_admin') || |
| | | hasAccessByCodes([permission, '*:*:*']) |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 该èå对å½åç¨æ·æ¯å¦ãå¼çãââ ç¨äºé¦é¡µæ¨¡åå¼å
³ã |
| | | * |
| | | * å端èå `visible = 0` ä¼è¢«å端转æè·¯ç±ç `meta.hideInMenu`ï¼è¿ç±»è·¯ç±ä»ç¶æ³¨åå¨ |
| | | * router éï¼é¡µé¢å¯ä»¥ç´æ¥è®¿é®ï¼ï¼åªæ¯æ´æ£µåæ ä¸ä¼åºç°å¨ä¾§è¾¹æ ãæä»¥å¤æãè¿ä¸ªæ¨¡åæ |
| | | * 没æå¼ãä¸è½åªçè·¯ç±æ¯å¦åå¨ï¼è¿è¦çå®åå®çæ¯ä¸çº§ç¥å
æ¯å¦å¯è§ã |
| | | * |
| | | * @param name è·¯ç± nameï¼å³å端èåç component_name |
| | | */ |
| | | 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; |
| | | } |
| | | |
| | | return { canQuery, canSeeMenu }; |
| | | } |
| | |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Card, Col, Row } from 'ant-design-vue'; |
| | | 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 AiInsights from './modules/ai-insights.vue'; |
| | |
| | | |
| | | const router = useRouter(); |
| | | const summary = ref(defaultSummary); |
| | | |
| | | const { canQuery, canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** é¦é¡µå模åçæ¥å£æé齿¯ mes:home:queryï¼åä¸å°æ°å°±å
å«è¯·æ±ï¼å¦åå¼¹ãæ²¡æè¯¥æä½æéãï¼ */ |
| | | const canViewSummary = computed(() => canQuery('mes:home:query')); |
| | | |
| | | /** MES ç产 KPIï¼æ°æ®åèªé¦é¡µæ±æ»æ¥å£ï¼ä¸å¯¹åºèåå¼çææ¸²æ */ |
| | | const showKpi = computed( |
| | | () => |
| | | canViewSummary.value && |
| | | ['MesProWorkOrder', 'MesProFeedback', 'MesQcIqc', 'MesDvMachinery'].some( |
| | | (name) => canSeeMenu(name), |
| | | ), |
| | | ); |
| | | |
| | | /** ç产è¶å¿ï¼æ°æ®æ¥èªç产æ¥å·¥ */ |
| | | const showTrend = computed( |
| | | () => canViewSummary.value && canSeeMenu('MesProFeedback'), |
| | | ); |
| | | |
| | | /** å·¥åç¶æåå¸ + ç»´ä¿®é¢è¦ */ |
| | | const showAlert = computed( |
| | | () => |
| | | canViewSummary.value && |
| | | (canSeeMenu('MesProWorkOrder') || canSeeMenu('MesDvRepair')), |
| | | ); |
| | | |
| | | /** ç产è¶å¿åé¢è¦é¢æ¿åä¸è¡ï¼ä¸¤å齿²¡å¼å°±æ´è¡ä¸æ¸²æ */ |
| | | const showMonitorRow = computed(() => showTrend.value || showAlert.value); |
| | | |
| | | /** AI åæåéãN 个工åå¯åæãåç乿¯é¦é¡µæ±æ»æ¥å£ï¼åä¸å°æ°å°±å«æ¾ç¤ºè¿ä¸ªåæ°å */ |
| | | const showAiInsights = computed(() => canViewSummary.value); |
| | | |
| | | const greeting = computed(() => { |
| | | const hour = new Date().getHours(); |
| | |
| | | } |
| | | |
| | | async function loadSummary() { |
| | | if (!canViewSummary.value) return; |
| | | summary.value = await getHomeSummary(); |
| | | } |
| | | |
| | |
| | | </div> |
| | | |
| | | <!-- MES ç产 KPI --> |
| | | <KpiCards :summary="summary" @navigate="handleNavigate" /> |
| | | <KpiCards v-if="showKpi" :summary="summary" @navigate="handleNavigate" /> |
| | | |
| | | <!-- ERP/WMS/BPM ç»è¥ KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | v-if="showAiInsights" |
| | | :active-order-count="summary.workOrderActiveCount" |
| | | @navigate="handleNavigate" |
| | | /> |
| | | |
| | | <!-- ç产è¶å¿ + é¢è¦é¢æ¿ --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4"> |
| | | <Row v-if="showMonitorRow" :gutter="16" class="mb-4"> |
| | | <Col |
| | | v-if="showTrend" |
| | | :lg="showAlert ? 14 : 24" |
| | | :md="24" |
| | | :sm="24" |
| | | :xl="showAlert ? 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="showAlert" |
| | | :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 type { MesHomeApi } from '#/api/mes/home'; |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | import { Spin } from 'ant-design-vue'; |
| | | |
| | | import { getWorkOrderStatusDistribution } from '#/api/mes/home'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardAlertPanel' }); |
| | | |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const { canQuery, canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** å·¥åç¶æåå¸åèªç产订åï¼æ¥å£æéåèåé½è¦æ */ |
| | | const canViewStatus = computed( |
| | | () => canQuery('mes:home:query') && canSeeMenu('MesProWorkOrder'), |
| | | ); |
| | | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const loading = ref(false); |
| | | |
| | | async function loadStatus() { |
| | | if (!canViewStatus.value) return; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getWorkOrderStatusDistribution(); |
| | |
| | | <!-- é¢è¦å¡çè¡ --> |
| | | <div class="grid grid-cols-1 gap-3"> |
| | | <div |
| | | 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="canViewStatus" class="glass-tilt rounded-2xl p-5"> |
| | | <h4 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-100"> |
| | | å·¥åç¶æåå¸ |
| | | </h4> |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | | import { getTaskTodoPage } from '#/api/bpm/task'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardBizCards' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const { canQuery, canSeeMenu } = 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'), |
| | | ); |
| | | |
| | | /** å¯è§å¡çæ°éå³å®å宽ï¼é¿å
åªå©ä¸ä¸¤å¼ æ¶æ¤å¨å·¦ä¸è§ */ |
| | | const colSpan = computed(() => { |
| | | const count = [ |
| | | canViewPurchase.value, |
| | | canViewSale.value, |
| | | canViewStock.value, |
| | | canViewBpmTodo.value, |
| | | ].filter(Boolean).length; |
| | | if (count >= 4) return 6; |
| | | if (count === 3) return 8; |
| | | if (count === 2) return 12; |
| | | return 24; |
| | | }); |
| | | |
| | | const purchaseCount = ref(0); |
| | | const saleCount = ref(0); |
| | |
| | | const bpmTodoCount = ref(0); |
| | | |
| | | async function loadPurchase() { |
| | | if (!canViewPurchase.value) return; |
| | | try { |
| | | const result = await getPurchaseOrderPage({ pageNo: 1, pageSize: 1 }); |
| | | purchaseCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadSale() { |
| | | if (!canViewSale.value) return; |
| | | try { |
| | | const result = await getSaleOrderPage({ pageNo: 1, pageSize: 1 }); |
| | | saleCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadStock() { |
| | | if (!canViewStock.value) return; |
| | | try { |
| | | const result = await getMaterialStockPage({ pageNo: 1, pageSize: 1 }); |
| | | stockCount.value = result.total ?? 0; |
| | |
| | | } |
| | | |
| | | async function loadBpmTodo() { |
| | | if (!canViewBpmTodo.value) return; |
| | | try { |
| | | const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 }); |
| | | bpmTodoCount.value = result.total ?? 0; |
| | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- éè´è®¢å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canViewPurchase" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('purchase')" |
| | |
| | | </Col> |
| | | |
| | | <!-- éå®è®¢å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canViewSale" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('sale')" |
| | |
| | | </Col> |
| | | |
| | | <!-- ç©æåºå --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canViewStock" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('stock')" |
| | |
| | | </Col> |
| | | |
| | | <!-- BPM å¾
å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="canViewBpmTodo" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :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 '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardKpiCards' }); |
| | | |
| | | const props = defineProps<{ |
| | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const { canQuery, canSeeMenu } = useHomeAccess(); |
| | | |
| | | /** åå¼ å¡çæ°æ®é½æ¥èªé¦é¡µæ±æ»æ¥å£ï¼æ²¡æè¯¥æéæ¶åä¸çæ¯ç©ºå¼ */ |
| | | const canViewSummary = computed(() => canQuery('mes:home:query')); |
| | | const showOrder = computed( |
| | | () => canViewSummary.value && canSeeMenu('MesProWorkOrder'), |
| | | ); |
| | | const showOutput = computed( |
| | | () => canViewSummary.value && canSeeMenu('MesProFeedback'), |
| | | ); |
| | | const showQuality = computed( |
| | | () => canViewSummary.value && canSeeMenu('MesQcIqc'), |
| | | ); |
| | | const showEquipment = computed( |
| | | () => canViewSummary.value && canSeeMenu('MesDvMachinery'), |
| | | ); |
| | | |
| | | const visibleCards = computed( |
| | | () => |
| | | [showOrder, showOutput, showQuality, showEquipment].filter( |
| | | (card) => card.value, |
| | | ).length, |
| | | ); |
| | | |
| | | /** å¯è§å¡çæ°éå³å®å宽ï¼é¿å
åªå©ä¸ä¸¤å¼ æ¶æ¤å¨å·¦ä¸è§ */ |
| | | const colSpan = computed(() => { |
| | | if (visibleCards.value >= 4) return 6; |
| | | if (visibleCards.value === 3) return 8; |
| | | if (visibleCards.value === 2) return 12; |
| | | return 24; |
| | | }); |
| | | |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <Row v-if="visibleCards > 0" :gutter="16"> |
| | | <!-- ç产订å --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showOrder" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('order')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 仿¥äº§é --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showOutput" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('output')" |
| | |
| | | </Col> |
| | | |
| | | <!-- è´¨éåæ ¼ç --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-if="showQuality" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('quality')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')" |
| | | @mouseleave="onMouseLeave('quality')" |
| | | @click="emit('navigate', 'MesQcRecord')" |
| | | @click="emit('navigate', 'MesQcIqc')" |
| | | > |
| | | <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="showEquipment" |
| | | :lg="colSpan" |
| | | :md="colSpan" |
| | | :sm="colSpan" |
| | | :xl="colSpan" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('equip')" |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { useHomeAccess } from '#/utils/use-home-access'; |
| | | |
| | | defineOptions({ name: 'DashboardQuickActions' }); |
| | | |
| | |
| | | shadow: string; |
| | | } |
| | | |
| | | const { canSeeMenu } = useHomeAccess(); |
| | | |
| | | 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' }, |
| | | ]; |
| | | |
| | | /** èåæ²¡å¼çå
¥å£ä¸å±ç¤ºï¼é¿å
ç¹è¿å» 403 */ |
| | | const accessibleActions = computed(() => |
| | | actions.filter((action) => canSeeMenu(action.route)), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <div v-if="accessibleActions.length > 0" class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">å¿«æ·æä½</h3> |
| | | <div class="grid grid-cols-4 gap-3 sm:grid-cols-8"> |
| | | <div |
| | | v-for="action in actions" |
| | | v-for="action in accessibleActions" |
| | | :key="action.route" |
| | | class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1" |
| | | @click="emit('navigate', action.route)" |