From a27f80b043bb9651fd0032dcd28b2fee6cf820b9 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 03 八月 2026 13:42:34 +0800
Subject: [PATCH] feat(ai): 集成AI智能分析功能到MPS表单和仪表板

---
 src/views/mes/pro/mps/modules/form.vue                      |   14 +
 src/views/dashboard/analytics/modules/ai-insights.vue       |  119 +++++++++++++
 src/views/mes/pro/workorder/modules/ai/predict-duration.vue |   87 ++++++++
 src/views/mes/pro/workorder/modules/ai/predict-material.vue |   87 ++++++++
 src/views/mes/pro/workorder/modules/ai/predict-risk.vue     |   87 ++++++++
 src/views/dashboard/analytics/index.vue                     |    7 
 src/views/mes/pro/workorder/modules/ai/predict-delivery.vue |   87 ++++++++
 src/api/mes/pro/ai/index.ts                                 |   16 
 8 files changed, 464 insertions(+), 40 deletions(-)

diff --git a/src/api/mes/pro/ai/index.ts b/src/api/mes/pro/ai/index.ts
index d632e91..2215dc4 100644
--- a/src/api/mes/pro/ai/index.ts
+++ b/src/api/mes/pro/ai/index.ts
@@ -65,33 +65,33 @@
 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 },
   );
 }
diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index 6085aef..3714991 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -9,6 +9,7 @@
 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';
@@ -76,6 +77,12 @@
     <!-- 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">
diff --git a/src/views/dashboard/analytics/modules/ai-insights.vue b/src/views/dashboard/analytics/modules/ai-insights.vue
new file mode 100644
index 0000000..93b9ada
--- /dev/null
+++ b/src/views/dashboard/analytics/modules/ai-insights.vue
@@ -0,0 +1,119 @@
+<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>
diff --git a/src/views/mes/pro/mps/modules/form.vue b/src/views/mes/pro/mps/modules/form.vue
index 4cc8308..7490a8d 100644
--- a/src/views/mes/pro/mps/modules/form.vue
+++ b/src/views/mes/pro/mps/modules/form.vue
@@ -13,6 +13,10 @@
 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']);
@@ -20,6 +24,8 @@
 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) {
@@ -171,5 +177,13 @@
         </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>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue b/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue
index 09e4da0..339aea9 100644
--- a/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue
+++ b/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue
@@ -1,36 +1,78 @@
 <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>
@@ -40,7 +82,36 @@
   </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 姝e湪璇勪及浜や粯鑳藉姏...
+      </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">姝e湪鍒嗘瀽鐢熶骇杩涘害銆佸墿浣欏伐鏃朵笌浜や粯椋庨櫓</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>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-duration.vue b/src/views/mes/pro/workorder/modules/ai/predict-duration.vue
index 5464c96..c91b08b 100644
--- a/src/views/mes/pro/workorder/modules/ai/predict-duration.vue
+++ b/src/views/mes/pro/workorder/modules/ai/predict-duration.vue
@@ -1,21 +1,25 @@
 <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 },
@@ -26,20 +30,58 @@
   { 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>
@@ -49,7 +91,36 @@
   </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 姝e湪鍒嗘瀽鐢熶骇宸ュ簭涓庢椂闀�...
+      </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">姝e湪鍒嗘瀽鍚勫伐搴忔爣鍑嗗伐鏃朵笌棰勬祴鍋忓樊</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>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-material.vue b/src/views/mes/pro/workorder/modules/ai/predict-material.vue
index 919fc43..c6c5673 100644
--- a/src/views/mes/pro/workorder/modules/ai/predict-material.vue
+++ b/src/views/mes/pro/workorder/modules/ai/predict-material.vue
@@ -1,21 +1,25 @@
 <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: '楂橀闄�' };
@@ -31,20 +35,58 @@
   { 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>
@@ -54,7 +96,36 @@
   </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 姝e湪鍒嗘瀽鐗╂枡搴撳瓨涓庨渶姹�...
+      </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">姝e湪鏌ヨ搴撳瓨鏁版嵁銆佽瘎浼扮墿鏂欑煭缂洪闄�</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]">
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-risk.vue b/src/views/mes/pro/workorder/modules/ai/predict-risk.vue
index d21d638..3a29bd1 100644
--- a/src/views/mes/pro/workorder/modules/ai/predict-risk.vue
+++ b/src/views/mes/pro/workorder/modules/ai/predict-risk.vue
@@ -1,40 +1,82 @@
 <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>
@@ -44,7 +86,36 @@
   </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 姝e湪璇勪及鐢熶骇椋庨櫓...
+      </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">姝e湪缁煎悎璇勪及鐗╂枡銆佽澶囥�佽川閲忕瓑澶氱淮搴﹂闄�</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]">

--
Gitblit v1.9.3