feat(ai): 集成AI智能分析功能到MPS表单和仪表板
- 在MPS表单中添加AI预测组件(物料、时长、风险、交付)
- 修改AI API接口支持MPS ID参数
- 添加AI智能分析概览模块到仪表板
- 为AI预测组件实现加载进度动画和错误处理
- 重构AI预测组件props支持可选参数传递
| | |
| | | const BASE = '/mes/pro/ai'; |
| | | |
| | | /** AI ç©æçç¼ºé¢æµ */ |
| | | export function predictMaterial(workOrderId: number) { |
| | | export function predictMaterial(workOrderId?: number, mpsId?: number) { |
| | | return requestClient.post<MesProAiApi.PredictMaterialRespVO>( |
| | | `${BASE}/predict-material`, |
| | | { workOrderId }, |
| | | { workOrderId, mpsId }, |
| | | ); |
| | | } |
| | | |
| | | /** AI ç产æ¶é¿é¢æµ */ |
| | | export function predictDuration(workOrderId: number) { |
| | | export function predictDuration(workOrderId?: number, mpsId?: number) { |
| | | return requestClient.post<MesProAiApi.PredictDurationRespVO>( |
| | | `${BASE}/predict-duration`, |
| | | { workOrderId }, |
| | | { workOrderId, mpsId }, |
| | | ); |
| | | } |
| | | |
| | | /** AI ç产é£é©é¢æµ */ |
| | | export function predictRisk(workOrderId: number) { |
| | | export function predictRisk(workOrderId?: number, mpsId?: number) { |
| | | return requestClient.post<MesProAiApi.PredictRiskRespVO>( |
| | | `${BASE}/predict-risk`, |
| | | { workOrderId }, |
| | | { workOrderId, mpsId }, |
| | | ); |
| | | } |
| | | |
| | | /** AI ææ¶äº¤ä»é¢æµ */ |
| | | export function predictDelivery(workOrderId: number) { |
| | | export function predictDelivery(workOrderId?: number, mpsId?: number) { |
| | | return requestClient.post<MesProAiApi.PredictDeliveryRespVO>( |
| | | `${BASE}/predict-delivery`, |
| | | { workOrderId }, |
| | | { workOrderId, mpsId }, |
| | | ); |
| | | } |
| | |
| | | import { getHomeSummary } from '#/api/mes/home'; |
| | | |
| | | import { defaultSummary } from './data'; |
| | | import AiInsights from './modules/ai-insights.vue'; |
| | | import AlertPanel from './modules/alert-panel.vue'; |
| | | import BizCards from './modules/biz-cards.vue'; |
| | | import KpiCards from './modules/kpi-cards.vue'; |
| | |
| | | <!-- ERP/WMS/BPM ç»è¥ KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- AI æºè½åæ --> |
| | | <AiInsights |
| | | :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"> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'DashboardAiInsights' }); |
| | | |
| | | const props = defineProps<{ |
| | | activeOrderCount: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | interface AiCapability { |
| | | key: string; |
| | | title: string; |
| | | desc: string; |
| | | icon: string; |
| | | gradient: string; |
| | | route: string; |
| | | } |
| | | |
| | | const capabilities: AiCapability[] = [ |
| | | { |
| | | key: 'material', |
| | | title: 'AI ç©æé¢æµ', |
| | | desc: '颿µç©æéæ±ä¸ç缺é£é©', |
| | | icon: 'lucide:package-open', |
| | | gradient: 'from-violet-500 to-purple-500', |
| | | route: 'MesProWorkOrder', |
| | | }, |
| | | { |
| | | key: 'duration', |
| | | title: 'AI æ¶é¿é¢æµ', |
| | | desc: '颿µåå·¥åºçäº§èæ¶', |
| | | icon: 'lucide:clock', |
| | | gradient: 'from-sky-500 to-blue-500', |
| | | route: 'MesProWorkOrder', |
| | | }, |
| | | { |
| | | key: 'risk', |
| | | title: 'AI é£é©é¢æµ', |
| | | desc: 'è¯å«æ½å¨ç产é£é©å¹¶ç»åºå»ºè®®', |
| | | icon: 'lucide:shield-alert', |
| | | gradient: 'from-orange-500 to-red-500', |
| | | route: 'MesProWorkOrder', |
| | | }, |
| | | { |
| | | key: 'delivery', |
| | | title: 'AI 交ä»è¯ä¼°', |
| | | desc: 'è¯ä¼°è®¢å交ä»åæ¶çä¸å»¶æå ç´ ', |
| | | icon: 'lucide:truck', |
| | | gradient: 'from-emerald-500 to-teal-500', |
| | | route: 'MesProWorkOrder', |
| | | }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="glass-card mb-4 rounded-2xl p-5"> |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <h3 class="text-base font-semibold text-gray-800 dark:text-gray-100"> |
| | | AI æºè½åæ |
| | | </h3> |
| | | <span class="rounded-full bg-violet-50 px-3 py-0.5 text-xs font-medium text-violet-600 dark:bg-violet-500/15 dark:text-violet-400"> |
| | | {{ activeOrderCount }} 个工åå¯åæ |
| | | </span> |
| | | </div> |
| | | <Row :gutter="16"> |
| | | <Col v-for="cap in capabilities" :key="cap.key" :lg="6" :md="12" :sm="12" :xl="6" :xs="12" class="mb-3"> |
| | | <div |
| | | class="group flex cursor-pointer items-center gap-3 rounded-xl p-3 transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg" |
| | | @click="emit('navigate', cap.route)" |
| | | > |
| | | <div |
| | | class="flex size-10 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br shadow-md transition-transform duration-300 group-hover:scale-110" |
| | | :class="cap.gradient" |
| | | > |
| | | <IconifyIcon :icon="cap.icon" class="size-5 text-white" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="text-sm font-medium text-gray-800 dark:text-gray-100"> |
| | | {{ cap.title }} |
| | | </div> |
| | | <div class="text-xs text-gray-400">{{ cap.desc }}</div> |
| | | </div> |
| | | <IconifyIcon icon="lucide:chevron-right" class="size-4 text-gray-300 transition-transform duration-200 group-hover:translate-x-0.5" /> |
| | | </div> |
| | | </Col> |
| | | </Row> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .glass-card { |
| | | background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55)); |
| | | backdrop-filter: blur(20px); |
| | | -webkit-backdrop-filter: blur(20px); |
| | | border: 1px solid rgba(255,255,255,0.6); |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.04), |
| | | 0 1px 4px rgba(0,0,0,0.02), |
| | | inset 0 1px 0 rgba(255,255,255,0.8); |
| | | } |
| | | |
| | | .dark .glass-card { |
| | | background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5)); |
| | | border: 1px solid rgba(255,255,255,0.08); |
| | | box-shadow: |
| | | 0 4px 24px rgba(0,0,0,0.3), |
| | | 0 1px 4px rgba(0,0,0,0.2), |
| | | inset 0 1px 0 rgba(255,255,255,0.04); |
| | | } |
| | | </style> |
| | |
| | | import { createMps, getMps, updateMps } from '#/api/mes/pro/mps'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import PredictDelivery from '../../workorder/modules/ai/predict-delivery.vue'; |
| | | import PredictDuration from '../../workorder/modules/ai/predict-duration.vue'; |
| | | import PredictMaterial from '../../workorder/modules/ai/predict-material.vue'; |
| | | import PredictRisk from '../../workorder/modules/ai/predict-risk.vue'; |
| | | import { MpsStatusEnum, useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | |
| | | const formType = ref<FormType>('detail'); |
| | | |
| | | const isEditable = computed(() => ['create', 'edit'].includes(formType.value)); |
| | | |
| | | const canUseAi = computed(() => !!formData.value?.id); |
| | | |
| | | const getTitle = computed(() => { |
| | | switch (formType.value) { |
| | |
| | | </Descriptions.Item> |
| | | </Descriptions> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <div v-if="canUseAi" class="flex items-center gap-2"> |
| | | <PredictMaterial :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictDuration :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictRisk :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictDelivery :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProAiApi } from '#/api/mes/pro/ai'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { onUnmounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, message, Progress, Tag } from 'ant-design-vue'; |
| | | import { Alert, Button, Progress, Tag } from 'ant-design-vue'; |
| | | |
| | | import { predictDelivery } from '#/api/mes/pro/ai'; |
| | | |
| | | defineOptions({ name: 'MesProAiPredictDelivery' }); |
| | | |
| | | const props = defineProps<{ workOrderId: number }>(); |
| | | const props = defineProps<{ workOrderId?: number; mpsId?: number }>(); |
| | | |
| | | const loading = ref(false); |
| | | const progress = ref(0); |
| | | const result = ref<MesProAiApi.PredictDeliveryRespVO>(); |
| | | const errorMsg = ref(''); |
| | | |
| | | let progressTimer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | function startProgress() { |
| | | progress.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progress.value < 30) { |
| | | progress.value += 3; |
| | | } else if (progress.value < 70) { |
| | | progress.value += 2; |
| | | } else if (progress.value < 88) { |
| | | progress.value += 0.5; |
| | | } |
| | | }, 100); |
| | | } |
| | | |
| | | function stopProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = null; |
| | | } |
| | | } |
| | | |
| | | function finishProgress() { |
| | | stopProgress(); |
| | | progress.value = 100; |
| | | } |
| | | |
| | | onUnmounted(() => stopProgress()); |
| | | |
| | | async function handlePredict() { |
| | | loading.value = true; |
| | | result.value = undefined; |
| | | errorMsg.value = ''; |
| | | modalApi.open(); |
| | | startProgress(); |
| | | try { |
| | | result.value = await predictDelivery(props.workOrderId); |
| | | modalApi.open(); |
| | | result.value = await predictDelivery(props.workOrderId, props.mpsId); |
| | | finishProgress(); |
| | | } catch { |
| | | message.error('AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'); |
| | | errorMsg.value = 'AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'; |
| | | stopProgress(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const [ResultModal, modalApi] = useVbenModal({ footer: false }); |
| | | const [ResultModal, modalApi] = useVbenModal({ |
| | | footer: false, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | stopProgress(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </Button> |
| | | |
| | | <ResultModal title="AI ææ¶äº¤ä»é¢æµ" class="w-1/2"> |
| | | <template v-if="result"> |
| | | <!-- å è½½ä¸ --> |
| | | <div v-if="loading" class="flex flex-col items-center py-10"> |
| | | <div class="relative mb-6"> |
| | | <div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-20" style="width: 80px; height: 80px" /> |
| | | <div class="relative flex h-20 w-20 items-center justify-center rounded-full bg-green-50"> |
| | | <IconifyIcon icon="ant-design:check-circle-outlined" class="text-3xl text-green-500" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-3 text-base font-medium text-gray-700"> |
| | | AI æ£å¨è¯ä¼°äº¤ä»è½å... |
| | | </div> |
| | | <div class="w-2/3"> |
| | | <Progress |
| | | :percent="Math.round(progress)" |
| | | :stroke-color="{ '0%': '#52c41a', '100%': '#b7eb8f' }" |
| | | :show-info="true" |
| | | status="active" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 text-sm text-gray-400">æ£å¨åæç产è¿åº¦ãå©ä½å·¥æ¶ä¸äº¤ä»é£é©</div> |
| | | </div> |
| | | |
| | | <!-- é误 --> |
| | | <div v-else-if="errorMsg" class="flex flex-col items-center py-10"> |
| | | <IconifyIcon icon="ant-design:close-circle-filled" class="mb-4 text-5xl text-red-400" /> |
| | | <span class="text-base text-gray-500">{{ errorMsg }}</span> |
| | | </div> |
| | | |
| | | <!-- ç»æ --> |
| | | <template v-else-if="result"> |
| | | <div class="mb-4 flex items-center gap-4"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="text-base font-medium">è½å¦ææ¶äº¤ä»ï¼</span> |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProAiApi } from '#/api/mes/pro/ai'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { onUnmounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, message, Table, Tag } from 'ant-design-vue'; |
| | | import { Alert, Button, Progress, Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { predictDuration } from '#/api/mes/pro/ai'; |
| | | |
| | | defineOptions({ name: 'MesProAiPredictDuration' }); |
| | | |
| | | const props = defineProps<{ workOrderId: number }>(); |
| | | const props = defineProps<{ workOrderId?: number; mpsId?: number }>(); |
| | | |
| | | const loading = ref(false); |
| | | const progress = ref(0); |
| | | const result = ref<MesProAiApi.PredictDurationRespVO>(); |
| | | const errorMsg = ref(''); |
| | | |
| | | let progressTimer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | const columns = [ |
| | | { title: 'å·¥åºåç§°', dataIndex: 'processName', width: 150 }, |
| | |
| | | { title: '颿µç产æ¶é´(æ¶)', dataIndex: 'predictedProductionTime', width: 140 }, |
| | | ]; |
| | | |
| | | function startProgress() { |
| | | progress.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progress.value < 30) { |
| | | progress.value += 3; |
| | | } else if (progress.value < 70) { |
| | | progress.value += 2; |
| | | } else if (progress.value < 88) { |
| | | progress.value += 0.5; |
| | | } |
| | | }, 100); |
| | | } |
| | | |
| | | function stopProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = null; |
| | | } |
| | | } |
| | | |
| | | function finishProgress() { |
| | | stopProgress(); |
| | | progress.value = 100; |
| | | } |
| | | |
| | | onUnmounted(() => stopProgress()); |
| | | |
| | | async function handlePredict() { |
| | | loading.value = true; |
| | | result.value = undefined; |
| | | errorMsg.value = ''; |
| | | modalApi.open(); |
| | | startProgress(); |
| | | try { |
| | | result.value = await predictDuration(props.workOrderId); |
| | | modalApi.open(); |
| | | result.value = await predictDuration(props.workOrderId, props.mpsId); |
| | | finishProgress(); |
| | | } catch { |
| | | message.error('AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'); |
| | | errorMsg.value = 'AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'; |
| | | stopProgress(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const [ResultModal, modalApi] = useVbenModal({ footer: false }); |
| | | const [ResultModal, modalApi] = useVbenModal({ |
| | | footer: false, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | stopProgress(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </Button> |
| | | |
| | | <ResultModal title="AI ç产æ¶é¿é¢æµ" class="w-3/5"> |
| | | <template v-if="result"> |
| | | <!-- å è½½ä¸ --> |
| | | <div v-if="loading" class="flex flex-col items-center py-10"> |
| | | <div class="relative mb-6"> |
| | | <div class="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-20" style="width: 80px; height: 80px" /> |
| | | <div class="relative flex h-20 w-20 items-center justify-center rounded-full bg-blue-50"> |
| | | <IconifyIcon icon="ant-design:clock-circle-outlined" class="text-3xl text-blue-500" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-3 text-base font-medium text-gray-700"> |
| | | AI æ£å¨åæç产工åºä¸æ¶é¿... |
| | | </div> |
| | | <div class="w-2/3"> |
| | | <Progress |
| | | :percent="Math.round(progress)" |
| | | :stroke-color="{ '0%': '#1677ff', '100%': '#69b1ff' }" |
| | | :show-info="true" |
| | | status="active" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 text-sm text-gray-400">æ£å¨åæåå·¥åºæ åå·¥æ¶ä¸é¢æµåå·®</div> |
| | | </div> |
| | | |
| | | <!-- é误 --> |
| | | <div v-else-if="errorMsg" class="flex flex-col items-center py-10"> |
| | | <IconifyIcon icon="ant-design:close-circle-filled" class="mb-4 text-5xl text-red-400" /> |
| | | <span class="text-base text-gray-500">{{ errorMsg }}</span> |
| | | </div> |
| | | |
| | | <!-- ç»æ --> |
| | | <template v-else-if="result"> |
| | | <div class="mb-4 flex items-center gap-4"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="text-base font-medium">颿µæ»æ¶é¿ï¼</span> |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProAiApi } from '#/api/mes/pro/ai'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { onUnmounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, message, Table, Tag } from 'ant-design-vue'; |
| | | import { Button, Progress, Table, Tag } from 'ant-design-vue'; |
| | | |
| | | import { predictMaterial } from '#/api/mes/pro/ai'; |
| | | |
| | | defineOptions({ name: 'MesProAiPredictMaterial' }); |
| | | |
| | | const props = defineProps<{ workOrderId: number }>(); |
| | | const props = defineProps<{ workOrderId?: number; mpsId?: number }>(); |
| | | |
| | | const loading = ref(false); |
| | | const progress = ref(0); |
| | | const result = ref<MesProAiApi.PredictMaterialRespVO>(); |
| | | const errorMsg = ref(''); |
| | | |
| | | let progressTimer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | const riskLevelColor: Record<number, string> = { 1: 'green', 2: 'orange', 3: 'red' }; |
| | | const riskLevelText: Record<number, string> = { 1: 'ä½é£é©', 2: 'ä¸é£é©', 3: 'é«é£é©' }; |
| | |
| | | { title: 'å»ºè®®æªæ½', dataIndex: 'suggestion' }, |
| | | ]; |
| | | |
| | | function startProgress() { |
| | | progress.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progress.value < 30) { |
| | | progress.value += 3; |
| | | } else if (progress.value < 70) { |
| | | progress.value += 2; |
| | | } else if (progress.value < 88) { |
| | | progress.value += 0.5; |
| | | } |
| | | }, 100); |
| | | } |
| | | |
| | | function stopProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = null; |
| | | } |
| | | } |
| | | |
| | | function finishProgress() { |
| | | stopProgress(); |
| | | progress.value = 100; |
| | | } |
| | | |
| | | onUnmounted(() => stopProgress()); |
| | | |
| | | async function handlePredict() { |
| | | loading.value = true; |
| | | result.value = undefined; |
| | | errorMsg.value = ''; |
| | | modalApi.open(); |
| | | startProgress(); |
| | | try { |
| | | result.value = await predictMaterial(props.workOrderId); |
| | | modalApi.open(); |
| | | result.value = await predictMaterial(props.workOrderId, props.mpsId); |
| | | finishProgress(); |
| | | } catch { |
| | | message.error('AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'); |
| | | errorMsg.value = 'AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'; |
| | | stopProgress(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const [ResultModal, modalApi] = useVbenModal({ footer: false }); |
| | | const [ResultModal, modalApi] = useVbenModal({ |
| | | footer: false, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | stopProgress(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </Button> |
| | | |
| | | <ResultModal title="AI ç©æçç¼ºé¢æµ" class="w-3/5"> |
| | | <template v-if="result"> |
| | | <!-- å è½½ä¸ --> |
| | | <div v-if="loading" class="flex flex-col items-center py-10"> |
| | | <div class="relative mb-6"> |
| | | <div class="absolute inset-0 animate-ping rounded-full bg-blue-400 opacity-20" style="width: 80px; height: 80px" /> |
| | | <div class="relative flex h-20 w-20 items-center justify-center rounded-full bg-blue-50"> |
| | | <IconifyIcon icon="ant-design:alert-outlined" class="text-3xl text-blue-500" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-3 text-base font-medium text-gray-700"> |
| | | AI æ£å¨åæç©æåºåä¸éæ±... |
| | | </div> |
| | | <div class="w-2/3"> |
| | | <Progress |
| | | :percent="Math.round(progress)" |
| | | :stroke-color="{ '0%': '#1677ff', '100%': '#69b1ff' }" |
| | | :show-info="true" |
| | | status="active" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 text-sm text-gray-400">æ£å¨æ¥è¯¢åºåæ°æ®ãè¯ä¼°ç©æç缺é£é©</div> |
| | | </div> |
| | | |
| | | <!-- é误 --> |
| | | <div v-else-if="errorMsg" class="flex flex-col items-center py-10"> |
| | | <IconifyIcon icon="ant-design:close-circle-filled" class="mb-4 text-5xl text-red-400" /> |
| | | <span class="text-base text-gray-500">{{ errorMsg }}</span> |
| | | </div> |
| | | |
| | | <!-- ç»æ --> |
| | | <template v-else-if="result"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="text-base font-medium">æ´ä½é£é©ç级ï¼</span> |
| | | <Tag :color="riskLevelColor[result.riskLevel]"> |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProAiApi } from '#/api/mes/pro/ai'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { onUnmounted, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, message, Tag } from 'ant-design-vue'; |
| | | import { Alert, Button, Progress, Tag } from 'ant-design-vue'; |
| | | |
| | | import { predictRisk } from '#/api/mes/pro/ai'; |
| | | |
| | | defineOptions({ name: 'MesProAiPredictRisk' }); |
| | | |
| | | const props = defineProps<{ workOrderId: number }>(); |
| | | const props = defineProps<{ workOrderId?: number; mpsId?: number }>(); |
| | | |
| | | const loading = ref(false); |
| | | const progress = ref(0); |
| | | const result = ref<MesProAiApi.PredictRiskRespVO>(); |
| | | const errorMsg = ref(''); |
| | | |
| | | let progressTimer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | const riskLevelColor: Record<number, string> = { 1: 'green', 2: 'orange', 3: 'red' }; |
| | | const riskLevelText: Record<number, string> = { 1: 'ä½é£é©', 2: 'ä¸é£é©', 3: 'é«é£é©' }; |
| | | const severityColor: Record<string, string> = { 'é«': 'red', 'ä¸': 'orange', 'ä½': 'green' }; |
| | | |
| | | function startProgress() { |
| | | progress.value = 0; |
| | | progressTimer = setInterval(() => { |
| | | if (progress.value < 30) { |
| | | progress.value += 3; |
| | | } else if (progress.value < 70) { |
| | | progress.value += 2; |
| | | } else if (progress.value < 88) { |
| | | progress.value += 0.5; |
| | | } |
| | | }, 100); |
| | | } |
| | | |
| | | function stopProgress() { |
| | | if (progressTimer) { |
| | | clearInterval(progressTimer); |
| | | progressTimer = null; |
| | | } |
| | | } |
| | | |
| | | function finishProgress() { |
| | | stopProgress(); |
| | | progress.value = 100; |
| | | } |
| | | |
| | | onUnmounted(() => stopProgress()); |
| | | |
| | | async function handlePredict() { |
| | | loading.value = true; |
| | | result.value = undefined; |
| | | errorMsg.value = ''; |
| | | modalApi.open(); |
| | | startProgress(); |
| | | try { |
| | | result.value = await predictRisk(props.workOrderId); |
| | | modalApi.open(); |
| | | result.value = await predictRisk(props.workOrderId, props.mpsId); |
| | | finishProgress(); |
| | | } catch { |
| | | message.error('AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'); |
| | | errorMsg.value = 'AI åæææ¶ä¸å¯ç¨ï¼è¯·ç¨åéè¯'; |
| | | stopProgress(); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | const [ResultModal, modalApi] = useVbenModal({ footer: false }); |
| | | const [ResultModal, modalApi] = useVbenModal({ |
| | | footer: false, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | stopProgress(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | </Button> |
| | | |
| | | <ResultModal title="AI ç产é£é©é¢æµ" class="w-1/2"> |
| | | <template v-if="result"> |
| | | <!-- å è½½ä¸ --> |
| | | <div v-if="loading" class="flex flex-col items-center py-10"> |
| | | <div class="relative mb-6"> |
| | | <div class="absolute inset-0 animate-ping rounded-full bg-orange-400 opacity-20" style="width: 80px; height: 80px" /> |
| | | <div class="relative flex h-20 w-20 items-center justify-center rounded-full bg-orange-50"> |
| | | <IconifyIcon icon="ant-design:warning-outlined" class="text-3xl text-orange-500" /> |
| | | </div> |
| | | </div> |
| | | <div class="mb-3 text-base font-medium text-gray-700"> |
| | | AI æ£å¨è¯ä¼°ç产é£é©... |
| | | </div> |
| | | <div class="w-2/3"> |
| | | <Progress |
| | | :percent="Math.round(progress)" |
| | | :stroke-color="{ '0%': '#fa8c16', '100%': '#ffd591' }" |
| | | :show-info="true" |
| | | status="active" |
| | | /> |
| | | </div> |
| | | <div class="mt-2 text-sm text-gray-400">æ£å¨ç»¼åè¯ä¼°ç©æã设å¤ãè´¨éçå¤ç»´åº¦é£é©</div> |
| | | </div> |
| | | |
| | | <!-- é误 --> |
| | | <div v-else-if="errorMsg" class="flex flex-col items-center py-10"> |
| | | <IconifyIcon icon="ant-design:close-circle-filled" class="mb-4 text-5xl text-red-400" /> |
| | | <span class="text-base text-gray-500">{{ errorMsg }}</span> |
| | | </div> |
| | | |
| | | <!-- ç»æ --> |
| | | <template v-else-if="result"> |
| | | <div class="mb-4 flex items-center gap-2"> |
| | | <span class="text-base font-medium">æ´ä½é£é©ç级ï¼</span> |
| | | <Tag :color="riskLevelColor[result.overallRiskLevel]"> |