| src/utils/use-home-access.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/dashboard/analytics/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/dashboard/analytics/modules/biz-cards.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/dashboard/analytics/modules/kpi-cards.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/dashboard/analytics/modules/quick-actions.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/utils/use-home-access.ts
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,54 @@ 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 }; } src/views/dashboard/analytics/index.vue
@@ -18,6 +18,8 @@ import SummaryCard from '#/views/erp/home/modules/summary-card.vue'; import { useHomeAccess } from '#/utils/use-home-access'; import { defaultSummary } from './data'; import BizCards from './modules/biz-cards.vue'; import KpiCards from './modules/kpi-cards.vue'; @@ -26,6 +28,26 @@ defineOptions({ name: 'DashboardAnalytics' }); const router = useRouter(); const { canQuery, canSeeMenu } = useHomeAccess(); /** MES ç产 KPIï¼æ°æ®åèªé¦é¡µæ±æ»æ¥å£ï¼æ²¡æè¯¥æéå°±åä¸å°æ° */ const canViewSummary = computed(() => canQuery('mes:home:query')); const showKpi = computed( () => canViewSummary.value && (canSeeMenu('MesQcIqc') || canSeeMenu('MesDvMachinery')), ); const canViewSale = computed( () => canQuery('erp:sale-order:query') && canSeeMenu('ErpSaleOrder'), ); const canViewPurchase = computed( () => canQuery('erp:purchase-order:query') && canSeeMenu('ErpPurchaseOrder'), ); /** é颿¦è§æ¯éå®/éè´çå¯¹ç §å£å¾ï¼åªæä¸è¾¹ææéæ¶ä¸æå¡ç */ const showAmountSummary = computed( () => canViewSale.value && canViewPurchase.value, ); const greeting = computed(() => { const hour = new Date().getHours(); @@ -116,6 +138,7 @@ } async function loadStatistics() { if (canViewSale.value) { try { const saleOrders = await fetchAllPages<ErpSaleOrderApi.SaleOrder>( getSaleOrderPage, @@ -124,8 +147,11 @@ } catch { /* æ¥å£å¼å¸¸ææ æé */ } } if (canViewPurchase.value) { try { const purchaseOrders = await fetchAllPages<ErpPurchaseOrderApi.PurchaseOrder>( const purchaseOrders = await fetchAllPages<ErpPurchaseOrderApi.PurchaseOrder>( getPurchaseOrderPage, ); purchaseSummary.value = aggregatePrice(purchaseOrders); @@ -133,8 +159,10 @@ /* æ¥å£å¼å¸¸ææ æé */ } } } async function loadHomeSummary() { if (!canViewSummary.value) return; try { homeSummary.value = await getHomeSummary(); } catch { @@ -170,13 +198,13 @@ </div> <!-- è´¨é / è®¾å¤ KPI --> <KpiCards :summary="homeSummary" @navigate="handleNavigate" /> <KpiCards v-if="showKpi" :summary="homeSummary" @navigate="handleNavigate" /> <!-- ERP/WMS/BPM ç»è¥ KPI --> <BizCards @navigate="handleNavigate" /> <!-- éå® / éè´ é颿¦è§ --> <div class="mb-4"> <div v-if="showAmountSummary" class="mb-4"> <SummaryCard :purchase-summary="purchaseSummary" :sale-summary="saleSummary" src/views/dashboard/analytics/modules/biz-cards.vue
@@ -1,5 +1,5 @@ <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'; @@ -11,11 +11,42 @@ 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); @@ -23,6 +54,7 @@ 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; @@ -30,6 +62,7 @@ } async function loadSale() { if (!canViewSale.value) return; try { const result = await getSaleOrderPage({ pageNo: 1, pageSize: 1 }); saleCount.value = result.total ?? 0; @@ -37,6 +70,7 @@ } async function loadStock() { if (!canViewStock.value) return; try { const result = await getMaterialStockPage({ pageNo: 1, pageSize: 1 }); stockCount.value = result.total ?? 0; @@ -44,6 +78,7 @@ } async function loadBpmTodo() { if (!canViewBpmTodo.value) return; try { const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 }); bpmTodoCount.value = result.total ?? 0; @@ -85,7 +120,15 @@ <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')" @@ -112,7 +155,15 @@ </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')" @@ -139,7 +190,15 @@ </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')" @@ -166,13 +225,21 @@ </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"> src/views/dashboard/analytics/modules/kpi-cards.vue
@@ -8,6 +8,8 @@ import { Col, Row } from 'ant-design-vue'; import { useHomeAccess } from '#/utils/use-home-access'; defineOptions({ name: 'DashboardKpiCards' }); const props = defineProps<{ @@ -17,6 +19,18 @@ const emit = defineEmits<{ navigate: [name: string]; }>(); const { canQuery, canSeeMenu } = useHomeAccess(); /** ä¸¤å¼ å¡çæ°æ®é½æ¥èªé¦é¡µæ±æ»æ¥å£ï¼æ²¡æè¯¥æéæ¶åä¸çæ¯ç©ºå¼ */ const canViewSummary = computed(() => canQuery('mes:home:query')); const showQuality = computed( () => canViewSummary.value && canSeeMenu('MesQcIqc'), ); const showEquipment = computed( () => canViewSummary.value && canSeeMenu('MesDvMachinery'), ); const bothVisible = computed(() => showQuality.value && showEquipment.value); const qualityRate = computed(() => { const total = @@ -59,13 +73,21 @@ <template> <Row :gutter="16"> <!-- è´¨éåæ ¼ç --> <Col :lg="12" :md="12" :sm="12" :xl="12" :xs="24" class="mb-4"> <Col v-if="showQuality" :lg="bothVisible ? 12 : 24" :md="bothVisible ? 12 : 24" :sm="bothVisible ? 12 : 24" :xl="bothVisible ? 12 : 24" :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"> @@ -92,7 +114,15 @@ </Col> <!-- 设å¤è¿è¡ç --> <Col :lg="12" :md="12" :sm="12" :xl="12" :xs="24" class="mb-4"> <Col v-if="showEquipment" :lg="bothVisible ? 12 : 24" :md="bothVisible ? 12 : 24" :sm="bothVisible ? 12 : 24" :xl="bothVisible ? 12 : 24" :xs="24" class="mb-4" > <div class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" :style="tiltStyle('equip')" src/views/dashboard/analytics/modules/quick-actions.vue
@@ -1,5 +1,9 @@ <script lang="ts" setup> import { computed } from 'vue'; import { IconifyIcon } from '@vben/icons'; import { useHomeAccess } from '#/utils/use-home-access'; defineOptions({ name: 'DashboardQuickActions' }); @@ -15,19 +19,25 @@ shadow: string; } const { canSeeMenu } = useHomeAccess(); const actions: QuickAction[] = [ { 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:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, { title: 'ç©æåºå', icon: 'lucide:package-search', route: 'MesWmMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, ]; 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)"