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