| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 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'; |
| | |
| | | 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(); |
| | |
| | | } |
| | | |
| | | async function loadStatistics() { |
| | | try { |
| | | const saleOrders = await fetchAllPages<ErpSaleOrderApi.SaleOrder>( |
| | | getSaleOrderPage, |
| | | ); |
| | | saleSummary.value = aggregatePrice(saleOrders); |
| | | } catch { |
| | | /* æ¥å£å¼å¸¸ææ æé */ |
| | | if (canViewSale.value) { |
| | | try { |
| | | const saleOrders = await fetchAllPages<ErpSaleOrderApi.SaleOrder>( |
| | | getSaleOrderPage, |
| | | ); |
| | | saleSummary.value = aggregatePrice(saleOrders); |
| | | } catch { |
| | | /* æ¥å£å¼å¸¸ææ æé */ |
| | | } |
| | | } |
| | | try { |
| | | const purchaseOrders = await fetchAllPages<ErpPurchaseOrderApi.PurchaseOrder>( |
| | | getPurchaseOrderPage, |
| | | ); |
| | | purchaseSummary.value = aggregatePrice(purchaseOrders); |
| | | } catch { |
| | | /* æ¥å£å¼å¸¸ææ æé */ |
| | | if (canViewPurchase.value) { |
| | | try { |
| | | const purchaseOrders = |
| | | await fetchAllPages<ErpPurchaseOrderApi.PurchaseOrder>( |
| | | getPurchaseOrderPage, |
| | | ); |
| | | purchaseSummary.value = aggregatePrice(purchaseOrders); |
| | | } catch { |
| | | /* æ¥å£å¼å¸¸ææ æé */ |
| | | } |
| | | } |
| | | } |
| | | |
| | | async function loadHomeSummary() { |
| | | if (!canViewSummary.value) return; |
| | | try { |
| | | homeSummary.value = await getHomeSummary(); |
| | | } catch { |
| | |
| | | </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" |
| | |
| | | <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 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 = |
| | |
| | | <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"> |
| | |
| | | </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')" |
| | |
| | | <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: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)" |
| | |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { DeptTreeSelect } from '#/views/system/dept/components'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | |
| | | /** NCR ç¶ææä¸¾ */ |
| | | export const NCR_STATUS = { |
| | |
| | | { |
| | | fieldName: 'itemId', |
| | | label: 'ä¸åæ ¼ç©æ', |
| | | component: markRaw(MdItemSelect), |
| | | component: markRaw(MdmItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ä¸åæ ¼ç©æ', |
| | | }, |
| | |
| | | { |
| | | fieldName: 'itemId', |
| | | label: 'ä¸åæ ¼ç©æ', |
| | | component: markRaw(MdItemSelect), |
| | | component: markRaw(MdmItemSelect), |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©ç©æ', |
| | | }, |