From 75242bbba28ae8902ddf86d33febb497d79e5fc3 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 03 八月 2026 10:45:27 +0800
Subject: [PATCH] 银川 1.首页修改 2.ai辅助功能

---
 src/locales/langs/en-US/page.json                                     |    4 
 src/api/crm/saleQuotation/ai/index.ts                                 |   32 +
 src/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue |    2 
 src/views/basicData/mdm/components/select.vue                         |    4 
 src/views/crm/saleQuotation/index.vue                                 |   26 +
 src/views/mes/pro/workorder/modules/ai/predict-material.vue           |   85 +++
 src/views/mes/pro/workorder/modules/ai/predict-risk.vue               |   90 ++++
 .env                                                                  |    2 
 src/router/routes/modules/dashboard.ts                                |   43 -
 src/views/crm/saleQuotation/modules/ocr-upload.vue                    |  185 ++++++++
 src/views/crm/saleQuotation/modules/form.vue                          |   56 ++
 /dev/null                                                             |  260 -----------
 vite.config.ts                                                        |    2 
 src/locales/langs/zh-CN/page.json                                     |    4 
 src/views/crm/saleQuotation/modules/item-form.vue                     |    1 
 src/views/mes/pro/feedback/index.vue                                  |   14 
 src/views/mes/pro/workorder/modules/form.vue                          |   31 +
 src/views/mes/pro/workorder/modules/ai/predict-duration.vue           |   91 ++++
 src/views/mes/pd/archive/modules/detail.vue                           |  164 +++---
 src/views/mes/pro/workorder/modules/ai/predict-delivery.vue           |   86 +++
 src/api/mes/pro/ai/index.ts                                           |   97 ++++
 21 files changed, 884 insertions(+), 395 deletions(-)

diff --git a/.env b/.env
index 625866c..bba56ab 100644
--- a/.env
+++ b/.env
@@ -1,5 +1,5 @@
 # 搴旂敤鏍囬
-VITE_APP_TITLE=宸ュ巶鏁板瓧鍖朚OM绯荤粺
+VITE_APP_TITLE=宸ヤ笟AI鏅洪�犲钩鍙�
 
 # 搴旂敤鍛藉悕绌洪棿锛岀敤浜庣紦瀛樸�乻tore绛夊姛鑳界殑鍓嶇紑锛岀‘淇濋殧绂�
 VITE_APP_NAMESPACE=yudao-vben-antd
diff --git a/src/api/crm/saleQuotation/ai/index.ts b/src/api/crm/saleQuotation/ai/index.ts
new file mode 100644
index 0000000..a9b18fb
--- /dev/null
+++ b/src/api/crm/saleQuotation/ai/index.ts
@@ -0,0 +1,32 @@
+import { requestClient } from '#/api/request';
+
+export namespace CrmSaleQuotationAiApi {
+  /** OCR 璇嗗埆鍚庣殑鎶ヤ环鐗╂枡鏄庣粏 */
+  export interface OcrItemVO {
+    itemName?: string;
+    itemSpec?: string;
+    count?: number;
+    quotationPrice?: number;
+  }
+
+  /** AI OCR 璇嗗埆鎶ヤ环鏂囦欢鍝嶅簲 */
+  export interface OcrResultVO {
+    name?: string;
+    customerName?: string;
+    quotationTime?: string;
+    validUntil?: string;
+    taxRate?: number;
+    discountPercent?: number;
+    remark?: string;
+    items?: OcrItemVO[];
+    rawText?: string;
+  }
+}
+
+/** AI OCR 璇嗗埆鎶ヤ环鏂囦欢 */
+export function ocrSaleQuotation(blobId: number) {
+  return requestClient.post<CrmSaleQuotationAiApi.OcrResultVO>(
+    '/crm/sale-quotation/ai/ocr',
+    { blobId },
+  );
+}
diff --git a/src/api/mes/pro/ai/index.ts b/src/api/mes/pro/ai/index.ts
new file mode 100644
index 0000000..d632e91
--- /dev/null
+++ b/src/api/mes/pro/ai/index.ts
@@ -0,0 +1,97 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesProAiApi {
+  // ========== 鐗╂枡棰勬祴 ==========
+  export interface MaterialRiskItem {
+    itemCode: string;
+    itemName: string;
+    requiredQuantity: number;
+    availableQuantity: number;
+    shortageQuantity: number;
+    riskLevel: string;
+    suggestion: string;
+  }
+
+  export interface PredictMaterialRespVO {
+    riskLevel: number;
+    reasoning: string;
+    riskItems: MaterialRiskItem[];
+  }
+
+  // ========== 鏃堕暱棰勬祴 ==========
+  export interface ProcessTimeBreakdown {
+    processName: string;
+    sort: number;
+    standardPrepareTime: number;
+    standardWaitTime: number;
+    standardProductionTime: number;
+    predictedProductionTime: number;
+  }
+
+  export interface PredictDurationRespVO {
+    predictedTotalHours: number;
+    standardTotalHours: number;
+    reasoning: string;
+    keyPoints: string[];
+    processBreakdown: ProcessTimeBreakdown[];
+  }
+
+  // ========== 椋庨櫓棰勬祴 ==========
+  export interface RiskItem {
+    category: string;
+    description: string;
+    severity: string;
+    suggestion: string;
+  }
+
+  export interface PredictRiskRespVO {
+    overallRiskLevel: number;
+    reasoning: string;
+    keyPoints: string[];
+    risks: RiskItem[];
+  }
+
+  // ========== 浜や粯棰勬祴 ==========
+  export interface PredictDeliveryRespVO {
+    onTime: boolean;
+    confidence: number;
+    reasoning: string;
+    delayFactors: string[];
+    progressPercent: number;
+    remainingDays: number;
+  }
+}
+
+const BASE = '/mes/pro/ai';
+
+/** AI 鐗╂枡鐭己棰勬祴 */
+export function predictMaterial(workOrderId: number) {
+  return requestClient.post<MesProAiApi.PredictMaterialRespVO>(
+    `${BASE}/predict-material`,
+    { workOrderId },
+  );
+}
+
+/** AI 鐢熶骇鏃堕暱棰勬祴 */
+export function predictDuration(workOrderId: number) {
+  return requestClient.post<MesProAiApi.PredictDurationRespVO>(
+    `${BASE}/predict-duration`,
+    { workOrderId },
+  );
+}
+
+/** AI 鐢熶骇椋庨櫓棰勬祴 */
+export function predictRisk(workOrderId: number) {
+  return requestClient.post<MesProAiApi.PredictRiskRespVO>(
+    `${BASE}/predict-risk`,
+    { workOrderId },
+  );
+}
+
+/** AI 鎸夋椂浜や粯棰勬祴 */
+export function predictDelivery(workOrderId: number) {
+  return requestClient.post<MesProAiApi.PredictDeliveryRespVO>(
+    `${BASE}/predict-delivery`,
+    { workOrderId },
+  );
+}
diff --git a/src/locales/langs/en-US/page.json b/src/locales/langs/en-US/page.json
index 2d972fe..a98832c 100644
--- a/src/locales/langs/en-US/page.json
+++ b/src/locales/langs/en-US/page.json
@@ -8,8 +8,8 @@
     "profile": "Profile"
   },
   "dashboard": {
-    "title": "Dashboard",
-    "analytics": "Analytics",
+    "title": "Home",
+    "analytics": "Home",
     "workspace": "Workspace"
   },
   "action": {
diff --git a/src/locales/langs/zh-CN/page.json b/src/locales/langs/zh-CN/page.json
index d7ef79e..a1d4bd5 100644
--- a/src/locales/langs/zh-CN/page.json
+++ b/src/locales/langs/zh-CN/page.json
@@ -8,8 +8,8 @@
     "profile": "涓汉涓績"
   },
   "dashboard": {
-    "title": "姒傝",
-    "analytics": "鍒嗘瀽椤�",
+    "title": "棣栭〉",
+    "analytics": "棣栭〉",
     "workspace": "宸ヤ綔鍙�"
   },
   "action": {
diff --git a/src/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue b/src/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue
index 9cd46d0..e59b9f6 100644
--- a/src/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue
+++ b/src/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue
@@ -56,7 +56,7 @@
 </script>
 
 <template>
-  <main ref="contentElement" :style="style" class="relative bg-background-deep">
+  <main ref="contentElement" :style="style" class="relative overflow-auto bg-background-deep">
     <Slot :style="overlayStyle">
       <slot name="overlay"></slot>
     </Slot>
diff --git a/src/router/routes/modules/dashboard.ts b/src/router/routes/modules/dashboard.ts
index 2cccc81..1df827e 100644
--- a/src/router/routes/modules/dashboard.ts
+++ b/src/router/routes/modules/dashboard.ts
@@ -1,46 +1,15 @@
 import type { RouteRecordRaw } from 'vue-router';
 
-import { $t } from '#/locales';
-
 const routes: RouteRecordRaw[] = [
   {
+    name: 'Analytics',
+    path: '/analytics',
+    component: () => import('#/views/dashboard/analytics/index.vue'),
     meta: {
-      icon: 'lucide:layout-dashboard',
+      affixTab: true,
+      icon: 'lucide:area-chart',
       order: -1,
-      title: $t('page.dashboard.title'),
-    },
-    name: 'Dashboard',
-    path: '/dashboard',
-    children: [
-      {
-        name: 'Workspace',
-        path: '/workspace',
-        component: () => import('#/views/dashboard/workspace/index.vue'),
-        meta: {
-          icon: 'carbon:workspace',
-          title: $t('page.dashboard.workspace'),
-        },
-      },
-      {
-        name: 'Analytics',
-        path: '/analytics',
-        component: () => import('#/views/dashboard/analytics/index.vue'),
-        meta: {
-          affixTab: true,
-          icon: 'lucide:area-chart',
-          title: $t('page.dashboard.analytics'),
-        },
-      },
-    ],
-  },
-  {
-    name: 'Profile',
-    path: '/profile',
-    component: () => import('#/views/_core/profile/index.vue'),
-    meta: {
-      icon: 'ant-design:profile-outlined',
-      title: $t('ui.widgets.profile'),
-      hideInMenu: true,
+      title: '棣栭〉',
     },
   },
 ];
diff --git a/src/views/basicData/mdm/components/select.vue b/src/views/basicData/mdm/components/select.vue
index ad10d58..6149196 100644
--- a/src/views/basicData/mdm/components/select.vue
+++ b/src/views/basicData/mdm/components/select.vue
@@ -19,12 +19,14 @@
     disabled?: boolean;
     modelValue?: number;
     placeholder?: string;
+    label?: string;
   }>(),
   {
     allowClear: true,
     disabled: false,
     modelValue: undefined,
     placeholder: '璇烽�夋嫨鐗╂枡',
+    label: undefined,
   },
 );
 const emit = defineEmits<{
@@ -36,7 +38,7 @@
 const hovering = ref(false); // 鏄惁鎮仠
 const selectedItem = ref<MdmItemApi.Item>(); // 褰撳墠閫変腑鐗╂枡
 
-const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const displayLabel = computed(() => selectedItem.value?.name ?? props.label ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
 const showClear = computed(
   () =>
     props.allowClear &&
diff --git a/src/views/crm/saleQuotation/index.vue b/src/views/crm/saleQuotation/index.vue
index abd5c66..13297ae 100644
--- a/src/views/crm/saleQuotation/index.vue
+++ b/src/views/crm/saleQuotation/index.vue
@@ -16,8 +16,13 @@
 } from '#/api/crm/saleQuotation';
 import { $t } from '#/locales';
 
+import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai';
+
+import { nextTick, ref } from 'vue';
+
 import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data';
 import Form from './modules/form.vue';
+import OcrUpload from './modules/ocr-upload.vue';
 
 const { push } = useRouter();
 
@@ -25,6 +30,8 @@
   connectedComponent: Form,
   destroyOnClose: true,
 });
+
+const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>();
 
 /** 鍒锋柊琛ㄦ牸 */
 function handleRefresh() {
@@ -34,6 +41,18 @@
 /** 鍒涘缓鎶ヤ环鍗� */
 function handleCreate() {
   formModalApi.setData(null).open();
+}
+
+/** AI 璇嗗埆褰曞叆 */
+function handleOcrCreate() {
+  ocrUploadRef.value?.open();
+}
+
+/** OCR 璇嗗埆缁撴灉纭 */
+function handleOcrSuccess(data: CrmSaleQuotationAiApi.OcrResultVO) {
+  nextTick(() => {
+    formModalApi.setData({ ocrData: data } as any).open();
+  });
 }
 
 /** 缂栬緫鎶ヤ环鍗� */
@@ -127,6 +146,7 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" />
     <Grid>
       <template #toolbar-tools>
         <TableAction
@@ -138,6 +158,12 @@
               auth: ['crm:sale-quotation:create'],
               onClick: handleCreate,
             },
+            {
+              label: 'AI 璇嗗埆褰曞叆',
+              type: 'default',
+              auth: ['crm:sale-quotation:create'],
+              onClick: handleOcrCreate,
+            },
           ]"
         />
       </template>
diff --git a/src/views/crm/saleQuotation/modules/form.vue b/src/views/crm/saleQuotation/modules/form.vue
index 8fa938f..a52beb5 100644
--- a/src/views/crm/saleQuotation/modules/form.vue
+++ b/src/views/crm/saleQuotation/modules/form.vue
@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai';
 import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
 
 import { computed, ref, watch } from 'vue';
@@ -6,7 +7,7 @@
 import { useVbenModal } from '#/packages/effects/common-ui/src';
 import { erpPriceMultiply } from '#/packages/utils/src';
 
-import { message } from 'ant-design-vue';
+import { message, Alert } from 'ant-design-vue';
 
 import { useVbenForm } from '#/adapter/form';
 import {
@@ -22,6 +23,7 @@
 const emit = defineEmits(['success']);
 const formData = ref<CrmSaleQuotationApi.SaleQuotation>();
 const itemFormRef = ref();
+const ocrCustomerName = ref('');
 
 const getTitle = computed(() => {
   return formData.value?.id
@@ -85,14 +87,52 @@
   async onOpenChange(isOpen: boolean) {
     if (!isOpen) {
       formData.value = undefined;
+      ocrCustomerName.value = '';
       return;
     }
-    const data = modalApi.getData<CrmSaleQuotationApi.SaleQuotation>();
+    const data = modalApi.getData<CrmSaleQuotationApi.SaleQuotation & { ocrData?: CrmSaleQuotationAiApi.OcrResultVO }>();
     if (!data || !data.id) {
       formData.value = {} as CrmSaleQuotationApi.SaleQuotation;
       await formApi.resetForm();
-      // 鏂板鏃讹紝閲嶇疆骞舵坊鍔犱竴琛岀┖鏁版嵁
-      itemFormRef.value?.resetData();
+      // OCR 璇嗗埆鏁版嵁棰勫~
+      if (data?.ocrData) {
+        const ocr = data.ocrData;
+        await formApi.setValues({
+          name: ocr.name,
+          quotationTime: ocr.quotationTime,
+          validUntil: ocr.validUntil,
+          taxRate: ocr.taxRate,
+          discountPercent: ocr.discountPercent,
+          remark: ocr.remark,
+        });
+        if (ocr.items?.length) {
+          itemFormRef.value?.setData(ocr.items.map((item) => ({
+            id: undefined as unknown as number,
+            itemId: undefined as unknown as number,
+            itemName: item.itemName || '',
+            itemCode: '',
+            itemBarCode: undefined,
+            itemSpecification: item.itemSpec || '',
+            itemUnitName: '',
+            itemUnitName2: undefined,
+            itemUnitName3: undefined,
+            itemPrice: undefined as unknown as number,
+            quotationPrice: item.quotationPrice || 0,
+            count: item.count || 1,
+            totalPrice: erpPriceMultiply(item.quotationPrice || 0, item.count || 1) ?? 0,
+            taxPercent: undefined,
+            taxPrice: undefined,
+            remark: undefined,
+          })));
+        } else {
+          itemFormRef.value?.resetData();
+        }
+        // 瀛樺偍 OCR 璇嗗埆鐨勫鎴峰悕绉帮紝鐢ㄤ簬鐣岄潰鎻愮ず
+        ocrCustomerName.value = ocr.customerName || '';
+      } else {
+        // 鏂板鏃讹紝閲嶇疆骞舵坊鍔犱竴琛岀┖鏁版嵁
+        itemFormRef.value?.resetData();
+      }
       return;
     }
     modalApi.lock();
@@ -110,6 +150,14 @@
 
 <template>
   <Modal :title="getTitle" class="w-[80%]">
+    <Alert
+      v-if="ocrCustomerName"
+      type="info"
+      show-icon
+      class="mx-4 mb-4"
+      message="AI 璇嗗埆鍒板鎴峰悕绉帮紝璇峰湪瀹㈡埛閫夋嫨鍣ㄤ腑鎵嬪姩鍖归厤 CRM 瀹㈡埛"
+      :description="`璇嗗埆缁撴灉锛�${ocrCustomerName}`"
+    />
     <Form class="mx-4">
       <template #items>
         <ItemForm
diff --git a/src/views/crm/saleQuotation/modules/item-form.vue b/src/views/crm/saleQuotation/modules/item-form.vue
index 62d90fc..0de5bb2 100644
--- a/src/views/crm/saleQuotation/modules/item-form.vue
+++ b/src/views/crm/saleQuotation/modules/item-form.vue
@@ -191,6 +191,7 @@
       <MdmItemSelect
         v-if="!disabled"
         v-model:model-value="row.itemId"
+        :label="row.itemName"
         placeholder="璇烽�夋嫨浜у搧"
         @change="(item) => handleItemChange(item, row)"
       />
diff --git a/src/views/crm/saleQuotation/modules/ocr-upload.vue b/src/views/crm/saleQuotation/modules/ocr-upload.vue
new file mode 100644
index 0000000..db35cb6
--- /dev/null
+++ b/src/views/crm/saleQuotation/modules/ocr-upload.vue
@@ -0,0 +1,185 @@
+<script lang="ts" setup>
+import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai';
+
+import { ref } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Modal, Button, Upload, Descriptions, Table, Tag, message, Alert } from 'ant-design-vue';
+
+import { ocrSaleQuotation } from '#/api/crm/saleQuotation/ai';
+import { uploadFile } from '#/api/system/storage';
+
+defineOptions({ name: 'CrmSaleQuotationOcrUpload' });
+
+const emit = defineEmits<{
+  success: [data: CrmSaleQuotationAiApi.OcrResultVO];
+}>();
+
+const open = ref(false);
+const step = ref(0); // 0=upload, 1=processing, 2=preview
+const fileList = ref<any[]>([]);
+const ocrResult = ref<CrmSaleQuotationAiApi.OcrResultVO>();
+const processingTip = ref('');
+
+const itemColumns = [
+  { title: '鐗╂枡鍚嶇О', dataIndex: 'itemName', key: 'itemName' },
+  { title: '瑙勬牸鍨嬪彿', dataIndex: 'itemSpec', key: 'itemSpec' },
+  { title: '鏁伴噺', dataIndex: 'count', key: 'count' },
+  { title: '鍗曚环', dataIndex: 'quotationPrice', key: 'quotationPrice' },
+];
+
+function handleOpen() {
+  open.value = true;
+  step.value = 0;
+  fileList.value = [];
+  ocrResult.value = undefined;
+}
+
+/** 鑷畾涔変笂浼� */
+async function handleUpload(options: any) {
+  const { file, onSuccess, onError } = options;
+  try {
+    const result = await uploadFile([file as File]);
+    const blobId = result?.[0]?.id;
+    if (!blobId) {
+      onError(new Error('涓婁紶澶辫触'));
+      return;
+    }
+    onSuccess({ blobId }, file);
+    // 寮�濮� OCR 璇嗗埆
+    step.value = 1;
+    processingTip.value = '姝e湪杩涜 AI OCR 璇嗗埆锛岃绋嶅��...';
+    const ocrData = await ocrSaleQuotation(blobId);
+    // 妫�鏌ラ敊璇�
+    if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) {
+      // 鎵�鏈夊瓧娈典负 null锛宺awText 鍖呭惈閿欒鎻愮ず
+      processingTip.value = '';
+      step.value = 0;
+      message.error(ocrData.rawText);
+      return;
+    }
+    // 妫�鏌ユ槸鍚﹀叏閮ㄤ负绌�
+    const hasData = ocrData.name || ocrData.customerName || ocrData.quotationTime
+      || ocrData.validUntil || ocrData.taxRate || ocrData.discountPercent
+      || ocrData.remark || (ocrData.items && ocrData.items.length > 0);
+    if (!hasData) {
+      processingTip.value = '';
+      step.value = 0;
+      message.warning('鏈瘑鍒埌鎶ヤ环鏁版嵁锛岃鎵嬪姩褰曞叆鎴栭噸鏂颁笂浼�');
+      return;
+    }
+    ocrResult.value = ocrData;
+    step.value = 2;
+  } catch {
+    processingTip.value = '';
+    step.value = 0;
+    onError(new Error('涓婁紶澶辫触'));
+  }
+}
+
+function handleConfirm() {
+  if (ocrResult.value) {
+    emit('success', ocrResult.value);
+  }
+  handleClose();
+}
+
+function handleReset() {
+  step.value = 0;
+  fileList.value = [];
+  ocrResult.value = undefined;
+  processingTip.value = '';
+}
+
+function handleClose() {
+  open.value = false;
+  handleReset();
+}
+
+defineExpose({ open: handleOpen });
+</script>
+
+<template>
+  <Modal v-model:open="open" title="AI 璇嗗埆褰曞叆鎶ヤ环鍗�" width="680px" :footer="null" @cancel="handleClose">
+    <!-- Step 0: 涓婁紶 -->
+    <template v-if="step === 0">
+      <Upload.Dragger
+        v-model:file-list="fileList"
+        :max-count="1"
+        :custom-request="handleUpload"
+        accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.bmp,.txt,.csv"
+        @remove="handleReset"
+      >
+        <p class="text-4xl text-gray-400">
+          <IconifyIcon icon="ant-design:inbox-outlined" />
+        </p>
+        <p class="text-base text-gray-500">鐐瑰嚮鎴栨嫋鎷芥姤浠锋枃浠跺埌姝ゅ尯鍩熶笂浼�</p>
+        <p class="text-sm text-gray-400">
+          鏀寔 PDF銆乄ord銆丒xcel銆佸浘鐗囥�佺函鏂囨湰鏂囦欢
+        </p>
+      </Upload.Dragger>
+    </template>
+
+    <!-- Step 1: 璇嗗埆涓� -->
+    <template v-else-if="step === 1">
+      <div class="flex flex-col items-center gap-4 py-12">
+        <a-spin size="large" />
+        <span class="text-base text-gray-500">{{ processingTip }}</span>
+      </div>
+    </template>
+
+    <!-- Step 2: 棰勮缁撴灉 -->
+    <template v-else-if="step === 2 && ocrResult">
+      <div class="mb-4">
+        <Descriptions :column="2" bordered size="small">
+          <Descriptions.Item v-if="ocrResult.name" label="鎶ヤ环鍗曞悕绉�">
+            {{ ocrResult.name }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.customerName" label="瀹㈡埛鍚嶇О锛堥渶鎵嬪姩鍖归厤锛�">
+            <Tag color="orange">{{ ocrResult.customerName }}</Tag>
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.quotationTime" label="鎶ヤ环鏃ユ湡">
+            {{ ocrResult.quotationTime }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.validUntil" label="鏈夋晥鏈熻嚦">
+            {{ ocrResult.validUntil }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.taxRate != null" label="绋庣巼(%)">
+            {{ ocrResult.taxRate }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.discountPercent != null" label="鎶樻墸鐜�(%)">
+            {{ ocrResult.discountPercent }}
+          </Descriptions.Item>
+          <Descriptions.Item v-if="ocrResult.remark" label="澶囨敞" :span="2">
+            {{ ocrResult.remark }}
+          </Descriptions.Item>
+        </Descriptions>
+      </div>
+
+      <!-- 鐗╂枡鏄庣粏 -->
+      <div v-if="ocrResult.items?.length" class="mb-4">
+        <div class="mb-2 text-sm font-medium">鐗╂枡鏄庣粏锛堥渶鎵嬪姩鍖归厤瀹為檯鐗╂枡锛�</div>
+        <Table
+          :columns="itemColumns"
+          :data-source="ocrResult.items"
+          :pagination="false"
+          bordered
+          size="small"
+        />
+      </div>
+
+      <Alert
+        message="瀹㈡埛鍚嶇О鍜岀墿鏂欏悕绉颁粎涓烘枃鏈瘑鍒粨鏋滐紝闇�鍦ㄨ〃鍗曚腑鎵嬪姩閫夋嫨鍖归厤鐨� CRM 瀹㈡埛鍜屽疄闄呯墿鏂欍��"
+        type="info"
+        show-icon
+        class="mb-4"
+      />
+
+      <div class="flex justify-end gap-2">
+        <Button @click="handleReset">閲嶆柊涓婁紶</Button>
+        <Button type="primary" @click="handleConfirm">纭骞跺~鍏ヨ〃鍗�</Button>
+      </div>
+    </template>
+  </Modal>
+</template>
diff --git a/src/views/dashboard/workspace/index.vue b/src/views/dashboard/workspace/index.vue
deleted file mode 100644
index 1cfbef1..0000000
--- a/src/views/dashboard/workspace/index.vue
+++ /dev/null
@@ -1,260 +0,0 @@
-<script lang="ts" setup>
-import type {
-  WorkbenchProjectItem,
-  WorkbenchQuickNavItem,
-  WorkbenchTodoItem,
-  WorkbenchTrendItem,
-} from '@vben/common-ui';
-
-import { ref } from 'vue';
-import { useRouter } from 'vue-router';
-
-import {
-  AnalysisChartCard,
-  WorkbenchHeader,
-  WorkbenchProject,
-  WorkbenchQuickNav,
-  WorkbenchTodo,
-  WorkbenchTrends,
-} from '@vben/common-ui';
-import { preferences } from '@vben/preferences';
-import { useUserStore } from '@vben/stores';
-import { openWindow } from '@vben/utils';
-
-import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
-
-const userStore = useUserStore();
-
-// 杩欐槸涓�涓ず渚嬫暟鎹紝瀹為檯椤圭洰涓渶瑕佹牴鎹疄闄呮儏鍐佃繘琛岃皟鏁�
-// url 涔熷彲浠ユ槸鍐呴儴璺敱锛屽湪 navTo 鏂规硶涓瘑鍒鐞嗭紝杩涜鍐呴儴璺宠浆
-// 渚嬪锛歶rl: /dashboard/workspace
-const projectItems: WorkbenchProjectItem[] = [
-  {
-    color: '#6DB33F',
-    content: 'github.com/YunaiV/ruoyi-vue-pro',
-    date: '2025-01-02',
-    group: 'Spring Boot 鍗曚綋鏋舵瀯',
-    icon: 'simple-icons:springboot',
-    title: 'ruoyi-vue-pro',
-    url: 'https://github.com/YunaiV/ruoyi-vue-pro',
-  },
-  {
-    color: '#409EFF',
-    content: 'github.com/yudaocode/yudao-ui-admin-vue3',
-    date: '2025-02-03',
-    group: 'Vue3 + element-plus 绠$悊鍚庡彴',
-    icon: 'ep:element-plus',
-    title: 'yudao-ui-admin-vue3',
-    url: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
-  },
-  {
-    color: '#ff4d4f',
-    content: 'github.com/yudaocode/yudao-mall-uniapp',
-    date: '2025-03-04',
-    group: 'Vue3 + uniapp 鍟嗗煄鎵嬫満绔�',
-    icon: 'icon-park-outline:mall-bag',
-    title: 'yudao-mall-uniapp',
-    url: 'https://github.com/yudaocode/yudao-mall-uniapp',
-  },
-  {
-    color: '#1890ff',
-    content: 'github.com/YunaiV/yudao-cloud',
-    date: '2025-04-05',
-    group: 'Spring Cloud 寰湇鍔℃灦鏋�',
-    icon: 'material-symbols:cloud-outline',
-    title: 'yudao-cloud',
-    url: 'https://github.com/YunaiV/yudao-cloud',
-  },
-  {
-    color: '#e18525',
-    content: 'github.com/yudaocode/yudao-ui-admin-vben',
-    date: '2025-05-06',
-    group: 'Vue3 + vben5(antd) 绠$悊鍚庡彴',
-    icon: 'devicon:antdesign',
-    title: 'yudao-ui-admin-vben',
-    url: 'https://github.com/yudaocode/yudao-ui-admin-vben',
-  },
-  {
-    color: '#2979ff',
-    content: 'github.com/yudaocode/yudao-ui-admin-uniapp',
-    date: '2025-06-01',
-    group: 'Vue3 + uniapp 绠$悊鎵嬫満绔�',
-    icon: 'ant-design:mobile',
-    title: 'yudao-ui-admin-uniapp',
-    url: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
-  },
-];
-
-// 鍚屾牱锛岃繖閲岀殑 url 涔熷彲浠ヤ娇鐢ㄤ互 http 寮�澶寸殑澶栭儴閾炬帴
-const quickNavItems: WorkbenchQuickNavItem[] = [
-  {
-    color: '#1fdaca',
-    icon: 'ion:home-outline',
-    title: '棣栭〉',
-    url: '/',
-  },
-  {
-    color: '#ff6b6b',
-    icon: 'lucide:shopping-bag',
-    title: '鍟嗗煄涓績',
-    url: '/mall',
-  },
-  {
-    color: '#7c3aed',
-    icon: 'tabler:ai',
-    title: 'AI 澶фā鍨�',
-    url: '/ai',
-  },
-  {
-    color: '#3fb27f',
-    icon: 'simple-icons:erpnext',
-    title: 'ERP 绯荤粺',
-    url: '/erp',
-  },
-  {
-    color: '#4daf1bc9',
-    icon: 'simple-icons:civicrm',
-    title: 'CRM 绯荤粺',
-    url: '/crm',
-  },
-  {
-    color: '#1a73e8',
-    icon: 'fa-solid:hdd',
-    title: 'IoT 鐗╄仈缃�',
-    url: '/iot',
-  },
-];
-
-const todoItems = ref<WorkbenchTodoItem[]>([
-  {
-    completed: false,
-    content: `绯荤粺鏀寔 JDK 8/17/21锛孷ue 2/3`,
-    date: '2024-07-15 09:30:00',
-    title: '鎶�鏈吋瀹规��',
-  },
-  {
-    completed: false,
-    content: `鍚庣鎻愪緵 Spring Boot 2.7/3.2 + Cloud 鍙屾灦鏋刞,
-    date: '2024-08-30 14:20:00',
-    title: '鏋舵瀯鐏垫椿鎬�',
-  },
-  {
-    completed: false,
-    content: `鍏ㄩ儴寮�婧愶紝涓汉涓庝紒涓氬彲 100% 鐩存帴浣跨敤锛屾棤闇�鎺堟潈`,
-    date: '2024-07-25 16:45:00',
-    title: '寮�婧愬厤鎺堟潈',
-  },
-  {
-    completed: false,
-    content: `鍥藉唴浣跨敤鏈�骞挎硾鐨勫揩閫熷紑鍙戝钩鍙帮紝杩滆秴 10w+ 浼佷笟浣跨敤`,
-    date: '2024-07-10 11:15:00',
-    title: '骞挎硾浼佷笟璁ゅ彲',
-  },
-]);
-const trendItems: WorkbenchTrendItem[] = [
-  {
-    avatar: 'svg:avatar-1',
-    content: `鍦� <a>寮�婧愮粍</a> 鍒涘缓浜嗛」鐩� <a>Vue</a>`,
-    date: '鍒氬垰',
-    title: '濞佸粔',
-  },
-  {
-    avatar: 'svg:avatar-2',
-    content: `鍏虫敞浜� <a>濞佸粔</a> `,
-    date: '1涓皬鏃跺墠',
-    title: '鑹炬枃',
-  },
-  {
-    avatar: 'svg:avatar-3',
-    content: `鍙戝竷浜� <a>涓汉鍔ㄦ��</a> `,
-    date: '1澶╁墠',
-    title: '鍏嬮噷鏂�',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `鍙戣〃鏂囩珷 <a>濡備綍缂栧啓涓�涓猇ite鎻掍欢</a> `,
-    date: '2澶╁墠',
-    title: 'Vben',
-  },
-  {
-    avatar: 'svg:avatar-1',
-    content: `鍥炲浜� <a>鏉板厠</a> 鐨勯棶棰� <a>濡備綍杩涜椤圭洰浼樺寲锛�</a>`,
-    date: '3澶╁墠',
-    title: '鐨壒',
-  },
-  {
-    avatar: 'svg:avatar-2',
-    content: `鍏抽棴浜嗛棶棰� <a>濡備綍杩愯椤圭洰</a> `,
-    date: '1鍛ㄥ墠',
-    title: '鏉板厠',
-  },
-  {
-    avatar: 'svg:avatar-3',
-    content: `鍙戝竷浜� <a>涓汉鍔ㄦ��</a> `,
-    date: '1鍛ㄥ墠',
-    title: '濞佸粔',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `鎺ㄩ�佷簡浠g爜鍒� <a>Github</a>`,
-    date: '2021-04-01 20:00',
-    title: '濞佸粔',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `鍙戣〃鏂囩珷 <a>濡備綍缂栧啓浣跨敤 Admin Vben</a> `,
-    date: '2021-03-01 20:00',
-    title: 'Vben',
-  },
-];
-
-const router = useRouter();
-
-// 杩欐槸涓�涓ず渚嬫柟娉曪紝瀹為檯椤圭洰涓渶瑕佹牴鎹疄闄呮儏鍐佃繘琛岃皟鏁�
-// This is a sample method, adjust according to the actual project requirements
-function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
-  if (nav.url?.startsWith('http')) {
-    openWindow(nav.url);
-    return;
-  }
-  if (nav.url?.startsWith('/')) {
-    router.push(nav.url).catch((error) => {
-      console.error('Navigation failed:', error);
-    });
-  } else {
-    console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
-  }
-}
-</script>
-
-<template>
-  <div class="p-5">
-    <WorkbenchHeader
-      :avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
-    >
-      <template #title>
-        鏃╁畨, {{ userStore.userInfo?.nickname }}, 寮�濮嬫偍涓�澶╃殑宸ヤ綔鍚э紒
-      </template>
-      <template #description> 浠婃棩鏅达紝20鈩� - 32鈩冿紒 </template>
-    </WorkbenchHeader>
-
-    <div class="flex flex-col lg:flex-row">
-      <div class="mr-4 w-full lg:w-3/5">
-        <WorkbenchProject :items="projectItems" title="椤圭洰" @click="navTo" />
-        <WorkbenchTrends :items="trendItems" class="mt-5" title="鏈�鏂板姩鎬�" />
-      </div>
-      <div class="w-full lg:w-2/5">
-        <WorkbenchQuickNav
-          :items="quickNavItems"
-          class="lg:mt-0"
-          title="蹇嵎瀵艰埅"
-          @click="navTo"
-        />
-        <WorkbenchTodo :items="todoItems" class="mt-5" title="寰呭姙浜嬮」" />
-        <AnalysisChartCard class="mt-5" title="璁块棶鏉ユ簮">
-          <AnalyticsVisitsSource />
-        </AnalysisChartCard>
-      </div>
-    </div>
-  </div>
-</template>
diff --git a/src/views/mes/pd/archive/modules/detail.vue b/src/views/mes/pd/archive/modules/detail.vue
index 9eeee80..adb24db 100644
--- a/src/views/mes/pd/archive/modules/detail.vue
+++ b/src/views/mes/pd/archive/modules/detail.vue
@@ -3,13 +3,11 @@
 
 import { ref } from 'vue';
 
-import { useVbenModal } from '#/packages/effects/common-ui/src';
-import { DICT_TYPE } from '#/packages/constants/src';
-
-import { Descriptions, Table, Tabs } from 'ant-design-vue';
+import { Modal, Descriptions, Table, Tabs, message } from 'ant-design-vue';
 
 import { getPdArchiveDetailByProduct } from '#/api/mes/pd/archive';
 
+const open = ref(false);
 const loading = ref(false);
 const detailList = ref<MesPdArchiveApi.DetailItem[]>([]);
 const activeProject = ref('0');
@@ -36,85 +34,93 @@
   },
 ];
 
-const [Modal, modalApi] = useVbenModal({
-  async onOpenChange(isOpen: boolean) {
-    if (!isOpen) {
-      detailList.value = [];
-      activeProject.value = '0';
-      return;
-    }
-    const data = modalApi.getData<{ productCode: number }>();
-    loading.value = true;
-    try {
-      detailList.value = await getPdArchiveDetailByProduct(data.productCode);
-      activeProject.value = '0';
-    } finally {
-      loading.value = false;
-    }
-  },
-});
+async function handleOpen(productCode?: string) {
+  if (!productCode) {
+    message.warning('浜у搧缂栫爜涓虹┖锛屾棤娉曟煡鐪嬪綊妗�');
+    return;
+  }
+  open.value = true;
+  detailList.value = [];
+  activeProject.value = '0';
+  loading.value = true;
+  try {
+    detailList.value = await getPdArchiveDetailByProduct(productCode);
+  } catch {
+    message.error('鑾峰彇褰掓。璇︽儏澶辫触');
+  } finally {
+    loading.value = false;
+  }
+}
 
-defineExpose({
-  open: (productCode: number) => modalApi.setData({ productCode }).open(),
-});
+function handleClose() {
+  open.value = false;
+  detailList.value = [];
+}
+
+defineExpose({ open: handleOpen });
 </script>
 
 <template>
-  <Modal class="w-4/5" title="褰掓。璇︽儏" :loading="loading">
-    <Tabs v-if="detailList.length > 0" v-model:active-key="activeProject" type="card">
-      <Tabs.TabPane
-        v-for="(detail, index) in detailList"
-        :key="String(index)"
-        :tab="detail.project.taskName || '椤圭洰'"
-      >
-        <!-- 椤圭洰淇℃伅 -->
-        <Descriptions :column="3" bordered size="small" class="mb-4">
-          <Descriptions.Item label="浠诲姟缂栫爜">{{ detail.project.taskCode }}</Descriptions.Item>
-          <Descriptions.Item label="浠诲姟鍚嶇О">{{ detail.project.taskName }}</Descriptions.Item>
-          <Descriptions.Item label="浜у搧缂栫爜">{{ detail.project.productCode }}</Descriptions.Item>
-          <Descriptions.Item label="浜у搧鍚嶇О">{{ detail.project.productName }}</Descriptions.Item>
-          <Descriptions.Item label="鐗堟湰鍙�">{{ detail.project.version }}</Descriptions.Item>
-          <Descriptions.Item label="涓昏璁″笀">{{ detail.project.chiefDesignerNickname }}</Descriptions.Item>
-          <Descriptions.Item label="瀹℃牳浜�">{{ detail.project.reviewerNickname }}</Descriptions.Item>
-          <Descriptions.Item label="璁″垝瀹屾垚">{{ detail.project.planFinishTime }}</Descriptions.Item>
-        </Descriptions>
-
-        <!-- 褰掓。璁板綍 -->
-        <div class="mb-4">
-          <h4 class="mb-2 text-sm font-medium">褰掓。璁板綍</h4>
-          <Table
-            :columns="archiveColumns"
-            :data-source="detail.archives"
-            :pagination="false"
-            bordered
-            row-key="id"
-            size="small"
-          />
-        </div>
-
-        <!-- 璁捐璧勬枡 -->
-        <div>
-          <h4 class="mb-2 text-sm font-medium">璁捐璧勬枡</h4>
-          <Table
-            :columns="documentColumns"
-            :data-source="detail.documents"
-            :pagination="false"
-            bordered
-            row-key="id"
-            size="small"
-          >
-            <template #bodyCell="{ column, record }">
-              <template v-if="column.key === 'action'">
-                <a v-if="record.fileUrl" :href="record.fileUrl" target="_blank" class="text-primary">涓嬭浇</a>
-                <span v-else class="text-gray-400">-</span>
-              </template>
-            </template>
-          </Table>
-        </div>
-      </Tabs.TabPane>
-    </Tabs>
-    <div v-else class="py-8 text-center text-gray-400">
-      鏆傛棤褰掓。璁板綍
+  <Modal v-model:open="open" title="褰掓。璇︽儏" width="80%" :footer="null" @cancel="handleClose">
+    <div v-if="loading" class="flex justify-center py-8">
+      <a-spin />
     </div>
+    <template v-else>
+      <Tabs v-if="detailList.length > 0" v-model:active-key="activeProject" type="card">
+        <Tabs.TabPane
+          v-for="(detail, index) in detailList"
+          :key="String(index)"
+          :tab="detail.project.taskName || '椤圭洰'"
+        >
+          <!-- 椤圭洰淇℃伅 -->
+          <Descriptions :column="3" bordered size="small" class="mb-4">
+            <Descriptions.Item label="浠诲姟缂栫爜">{{ detail.project.taskCode }}</Descriptions.Item>
+            <Descriptions.Item label="浠诲姟鍚嶇О">{{ detail.project.taskName }}</Descriptions.Item>
+            <Descriptions.Item label="浜у搧缂栫爜">{{ detail.project.productCode }}</Descriptions.Item>
+            <Descriptions.Item label="浜у搧鍚嶇О">{{ detail.project.productName }}</Descriptions.Item>
+            <Descriptions.Item label="鐗堟湰鍙�">{{ detail.project.version }}</Descriptions.Item>
+            <Descriptions.Item label="涓昏璁″笀">{{ detail.project.chiefDesignerNickname }}</Descriptions.Item>
+            <Descriptions.Item label="瀹℃牳浜�">{{ detail.project.reviewerNickname }}</Descriptions.Item>
+            <Descriptions.Item label="璁″垝瀹屾垚">{{ detail.project.planFinishTime }}</Descriptions.Item>
+          </Descriptions>
+
+          <!-- 褰掓。璁板綍 -->
+          <div class="mb-4">
+            <h4 class="mb-2 text-sm font-medium">褰掓。璁板綍</h4>
+            <Table
+              :columns="archiveColumns"
+              :data-source="detail.archives"
+              :pagination="false"
+              bordered
+              row-key="id"
+              size="small"
+            />
+          </div>
+
+          <!-- 璁捐璧勬枡 -->
+          <div>
+            <h4 class="mb-2 text-sm font-medium">璁捐璧勬枡</h4>
+            <Table
+              :columns="documentColumns"
+              :data-source="detail.documents"
+              :pagination="false"
+              bordered
+              row-key="id"
+              size="small"
+            >
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'action'">
+                  <a v-if="record.fileUrl" :href="record.fileUrl" target="_blank" class="text-primary">涓嬭浇</a>
+                  <span v-else class="text-gray-400">-</span>
+                </template>
+              </template>
+            </Table>
+          </div>
+        </Tabs.TabPane>
+      </Tabs>
+      <div v-else class="py-8 text-center text-gray-400">
+        鏆傛棤褰掓。璁板綍
+      </div>
+    </template>
   </Modal>
 </template>
diff --git a/src/views/mes/pro/feedback/index.vue b/src/views/mes/pro/feedback/index.vue
index 1793ebb..b60997e 100644
--- a/src/views/mes/pro/feedback/index.vue
+++ b/src/views/mes/pro/feedback/index.vue
@@ -15,6 +15,7 @@
   exportFeedback,
   getFeedbackPage,
 } from '#/api/mes/pro/feedback';
+import { getWorkOrder } from '#/api/mes/pro/workorder';
 import { $t } from '#/locales';
 
 import { ref } from 'vue';
@@ -94,6 +95,17 @@
 async function handleExport() {
   const data = await exportFeedback(await gridApi.formApi.getValues());
   downloadFileFromBlobPart({ fileName: '鐢熶骇鎶ュ伐.xls', source: data });
+}
+
+/** 鏌ョ湅褰掓。 */
+async function handleArchive(row: MesProFeedbackApi.Feedback) {
+  if (!row.workOrderId) return;
+  try {
+    const workOrder = await getWorkOrder(row.workOrderId);
+    archiveDetailRef.value?.open(workOrder.productCode);
+  } catch {
+    message.error('鑾峰彇宸ュ崟淇℃伅澶辫触');
+  }
 }
 
 /** 蹇�熸姤宸� */
@@ -213,7 +225,7 @@
             {
               label: '褰掓。',
               type: 'link',
-              onClick: () => archiveDetailRef.value?.open(row.itemCode),
+              onClick: handleArchive.bind(null, row),
             },
           ]"
         />
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue b/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue
new file mode 100644
index 0000000..09e4da0
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/ai/predict-delivery.vue
@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import type { MesProAiApi } from '#/api/mes/pro/ai';
+
+import { 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 { predictDelivery } from '#/api/mes/pro/ai';
+
+defineOptions({ name: 'MesProAiPredictDelivery' });
+
+const props = defineProps<{ workOrderId: number }>();
+
+const loading = ref(false);
+const result = ref<MesProAiApi.PredictDeliveryRespVO>();
+
+async function handlePredict() {
+  loading.value = true;
+  result.value = undefined;
+  try {
+    result.value = await predictDelivery(props.workOrderId);
+    modalApi.open();
+  } catch {
+    message.error('AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�');
+  } finally {
+    loading.value = false;
+  }
+}
+
+const [ResultModal, modalApi] = useVbenModal({ footer: false });
+</script>
+
+<template>
+  <Button :loading="loading" @click="handlePredict">
+    <template #icon><IconifyIcon icon="ant-design:check-circle-outlined" /></template>
+    AI 浜や粯璇勪及
+  </Button>
+
+  <ResultModal title="AI 鎸夋椂浜や粯棰勬祴" class="w-1/2">
+    <template v-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>
+          <Tag :color="result.onTime ? 'green' : 'red'">
+            {{ result.onTime ? '鍙寜鏃朵氦浠�' : '鏃犳硶鎸夋椂浜や粯' }}
+          </Tag>
+        </div>
+        <div class="flex items-center gap-2">
+          <span class="text-base">缃俊搴︼細</span>
+          <span class="font-medium">{{ result.confidence }}%</span>
+        </div>
+      </div>
+
+      <div class="mb-4 flex items-center gap-4">
+        <div class="flex-1">
+          <div class="mb-1 text-sm text-gray-500">
+            鐢熶骇杩涘害 {{ result.progressPercent }}%
+          </div>
+          <Progress :percent="result.progressPercent" :stroke-color="{ '0%': '#108ee9', '100%': '#87d068' }" />
+        </div>
+        <div class="text-sm text-gray-500">
+          鍓╀綑 {{ result.remainingDays }} 澶�
+        </div>
+      </div>
+
+      <div class="mb-4 rounded bg-gray-50 p-3 text-sm leading-relaxed">
+        {{ result.reasoning }}
+      </div>
+
+      <div v-if="!result.onTime && result.delayFactors?.length">
+        <div class="mb-2 text-base font-medium">寤舵湡鍥犵礌锛�</div>
+        <Alert
+          v-for="(factor, index) in result.delayFactors"
+          :key="index"
+          type="error"
+          show-icon
+          class="mb-2"
+          :message="factor"
+        />
+      </div>
+    </template>
+  </ResultModal>
+</template>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-duration.vue b/src/views/mes/pro/workorder/modules/ai/predict-duration.vue
new file mode 100644
index 0000000..5464c96
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/ai/predict-duration.vue
@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+import type { MesProAiApi } from '#/api/mes/pro/ai';
+
+import { 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 { predictDuration } from '#/api/mes/pro/ai';
+
+defineOptions({ name: 'MesProAiPredictDuration' });
+
+const props = defineProps<{ workOrderId: number }>();
+
+const loading = ref(false);
+const result = ref<MesProAiApi.PredictDurationRespVO>();
+
+const columns = [
+  { title: '宸ュ簭鍚嶇О', dataIndex: 'processName', width: 150 },
+  { title: '椤哄簭', dataIndex: 'sort', width: 60 },
+  { title: '鏍囧噯鍑嗗鏃堕棿(鍒�)', dataIndex: 'standardPrepareTime', width: 140 },
+  { title: '鏍囧噯绛夊緟鏃堕棿(鍒�)', dataIndex: 'standardWaitTime', width: 140 },
+  { title: '鏍囧噯鐢熶骇鏃堕棿(鏃�)', dataIndex: 'standardProductionTime', width: 140 },
+  { title: '棰勬祴鐢熶骇鏃堕棿(鏃�)', dataIndex: 'predictedProductionTime', width: 140 },
+];
+
+async function handlePredict() {
+  loading.value = true;
+  result.value = undefined;
+  try {
+    result.value = await predictDuration(props.workOrderId);
+    modalApi.open();
+  } catch {
+    message.error('AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�');
+  } finally {
+    loading.value = false;
+  }
+}
+
+const [ResultModal, modalApi] = useVbenModal({ footer: false });
+</script>
+
+<template>
+  <Button :loading="loading" @click="handlePredict">
+    <template #icon><IconifyIcon icon="ant-design:clock-circle-outlined" /></template>
+    AI 鏃堕暱棰勬祴
+  </Button>
+
+  <ResultModal title="AI 鐢熶骇鏃堕暱棰勬祴" class="w-3/5">
+    <template v-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>
+          <Tag color="blue">{{ result.predictedTotalHours }} 灏忔椂</Tag>
+        </div>
+        <div class="flex items-center gap-2">
+          <span class="text-base">鏍囧噯鎬绘椂闀匡細</span>
+          <span class="text-gray-600">{{ result.standardTotalHours }} 灏忔椂</span>
+        </div>
+      </div>
+
+      <div class="mb-4 rounded bg-gray-50 p-3 text-sm leading-relaxed">
+        {{ result.reasoning }}
+      </div>
+
+      <Table
+        v-if="result.processBreakdown?.length"
+        :columns="columns"
+        :data-source="result.processBreakdown"
+        :pagination="false"
+        row-key="sort"
+        size="small"
+        bordered
+      />
+
+      <div v-if="result.keyPoints?.length" class="mt-4">
+        <div class="mb-2 text-base font-medium">鍏虫敞鐐癸細</div>
+        <Alert
+          v-for="(point, index) in result.keyPoints"
+          :key="index"
+          type="info"
+          show-icon
+          class="mb-2"
+          :message="point"
+        />
+      </div>
+    </template>
+  </ResultModal>
+</template>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-material.vue b/src/views/mes/pro/workorder/modules/ai/predict-material.vue
new file mode 100644
index 0000000..919fc43
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/ai/predict-material.vue
@@ -0,0 +1,85 @@
+<script lang="ts" setup>
+import type { MesProAiApi } from '#/api/mes/pro/ai';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Button, message, Table, Tag } from 'ant-design-vue';
+
+import { predictMaterial } from '#/api/mes/pro/ai';
+
+defineOptions({ name: 'MesProAiPredictMaterial' });
+
+const props = defineProps<{ workOrderId: number }>();
+
+const loading = ref(false);
+const result = ref<MesProAiApi.PredictMaterialRespVO>();
+
+const riskLevelColor: Record<number, string> = { 1: 'green', 2: 'orange', 3: 'red' };
+const riskLevelText: Record<number, string> = { 1: '浣庨闄�', 2: '涓闄�', 3: '楂橀闄�' };
+const itemRiskColor: Record<string, string> = { '鍏呰冻': 'green', '绱у紶': 'orange', '鐭己': 'red' };
+
+const columns = [
+  { title: '鐗╂枡缂栫爜', dataIndex: 'itemCode', width: 120 },
+  { title: '鐗╂枡鍚嶇О', dataIndex: 'itemName', width: 150 },
+  { title: '闇�姹傞噺', dataIndex: 'requiredQuantity', width: 100 },
+  { title: '搴撳瓨鍙敤閲�', dataIndex: 'availableQuantity', width: 100 },
+  { title: '鐭己鏁伴噺', dataIndex: 'shortageQuantity', width: 100 },
+  { title: '椋庨櫓绛夌骇', dataIndex: 'riskLevel', width: 100, key: 'riskLevel' },
+  { title: '寤鸿鎺柦', dataIndex: 'suggestion' },
+];
+
+async function handlePredict() {
+  loading.value = true;
+  result.value = undefined;
+  try {
+    result.value = await predictMaterial(props.workOrderId);
+    modalApi.open();
+  } catch {
+    message.error('AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�');
+  } finally {
+    loading.value = false;
+  }
+}
+
+const [ResultModal, modalApi] = useVbenModal({ footer: false });
+</script>
+
+<template>
+  <Button :loading="loading" @click="handlePredict">
+    <template #icon><IconifyIcon icon="ant-design:alert-outlined" /></template>
+    AI 鐗╂枡棰勬祴
+  </Button>
+
+  <ResultModal title="AI 鐗╂枡鐭己棰勬祴" class="w-3/5">
+    <template v-if="result">
+      <div class="mb-4 flex items-center gap-2">
+        <span class="text-base font-medium">鏁翠綋椋庨櫓绛夌骇锛�</span>
+        <Tag :color="riskLevelColor[result.riskLevel]">
+          {{ riskLevelText[result.riskLevel] || '-' }}
+        </Tag>
+      </div>
+      <div class="mb-4 rounded bg-gray-50 p-3 text-sm leading-relaxed">
+        {{ result.reasoning }}
+      </div>
+      <Table
+        v-if="result.riskItems?.length"
+        :columns="columns"
+        :data-source="result.riskItems"
+        :pagination="false"
+        row-key="itemCode"
+        size="small"
+        bordered
+      >
+        <template #bodyCell="{ column, text }">
+          <template v-if="column.key === 'riskLevel'">
+            <Tag :color="itemRiskColor[text] || 'default'">{{ text }}</Tag>
+          </template>
+        </template>
+      </Table>
+      <div v-else class="py-4 text-center text-gray-400">鏃犵墿鏂欓闄╅」</div>
+    </template>
+  </ResultModal>
+</template>
diff --git a/src/views/mes/pro/workorder/modules/ai/predict-risk.vue b/src/views/mes/pro/workorder/modules/ai/predict-risk.vue
new file mode 100644
index 0000000..d21d638
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/ai/predict-risk.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { MesProAiApi } from '#/api/mes/pro/ai';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { IconifyIcon } from '@vben/icons';
+
+import { Alert, Button, message, Tag } from 'ant-design-vue';
+
+import { predictRisk } from '#/api/mes/pro/ai';
+
+defineOptions({ name: 'MesProAiPredictRisk' });
+
+const props = defineProps<{ workOrderId: number }>();
+
+const loading = ref(false);
+const result = ref<MesProAiApi.PredictRiskRespVO>();
+
+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' };
+
+async function handlePredict() {
+  loading.value = true;
+  result.value = undefined;
+  try {
+    result.value = await predictRisk(props.workOrderId);
+    modalApi.open();
+  } catch {
+    message.error('AI 鍒嗘瀽鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�');
+  } finally {
+    loading.value = false;
+  }
+}
+
+const [ResultModal, modalApi] = useVbenModal({ footer: false });
+</script>
+
+<template>
+  <Button :loading="loading" @click="handlePredict">
+    <template #icon><IconifyIcon icon="ant-design:warning-outlined" /></template>
+    AI 椋庨櫓棰勬祴
+  </Button>
+
+  <ResultModal title="AI 鐢熶骇椋庨櫓棰勬祴" class="w-1/2">
+    <template v-if="result">
+      <div class="mb-4 flex items-center gap-2">
+        <span class="text-base font-medium">鏁翠綋椋庨櫓绛夌骇锛�</span>
+        <Tag :color="riskLevelColor[result.overallRiskLevel]">
+          {{ riskLevelText[result.overallRiskLevel] || '-' }}
+        </Tag>
+      </div>
+
+      <div class="mb-4 rounded bg-gray-50 p-3 text-sm leading-relaxed">
+        {{ result.reasoning }}
+      </div>
+
+      <div v-if="result.keyPoints?.length" class="mb-4">
+        <div class="mb-2 text-base font-medium">鍏抽敭鍏虫敞鐐癸細</div>
+        <Alert
+          v-for="(point, index) in result.keyPoints"
+          :key="index"
+          type="warning"
+          show-icon
+          class="mb-2"
+          :message="point"
+        />
+      </div>
+
+      <div v-if="result.risks?.length">
+        <div class="mb-2 text-base font-medium">椋庨櫓鏄庣粏锛�</div>
+        <div
+          v-for="(risk, index) in result.risks"
+          :key="index"
+          class="mb-2 rounded border border-gray-200 p-3"
+        >
+          <div class="mb-1 flex items-center gap-2">
+            <Tag>{{ risk.category }}</Tag>
+            <Tag :color="severityColor[risk.severity] || 'default'">
+              涓ラ噸绋嬪害锛歿{ risk.severity }}
+            </Tag>
+          </div>
+          <div class="text-sm text-gray-700">{{ risk.description }}</div>
+          <div class="mt-1 text-sm text-blue-600">{{ risk.suggestion }}</div>
+        </div>
+      </div>
+    </template>
+  </ResultModal>
+</template>
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 1181216..5da9d79 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -27,6 +27,10 @@
   import { BarcodeDetail } from "#/views/wls/barcode/components";
 
   import { useFormSchema } from "../data";
+  import PredictDelivery from "./ai/predict-delivery.vue";
+  import PredictDuration from "./ai/predict-duration.vue";
+  import PredictMaterial from "./ai/predict-material.vue";
+  import PredictRisk from "./ai/predict-risk.vue";
   import ItemList from "./item-list.vue";
   import ProcessList from "./process-list.vue";
 
@@ -50,6 +54,12 @@
     () =>
       formType.value === "update" &&
       formData.value?.status === MesProWorkOrderStatusEnum.PREPARE
+  );
+  const canUseAi = computed(
+    () =>
+      formData.value?.id &&
+      (formData.value?.status === MesProWorkOrderStatusEnum.PREPARE ||
+        formData.value?.status === MesProWorkOrderStatusEnum.CONFIRMED)
   );
   const getTitle = computed(() => {
     const isChild = !!formData.value?.parentId;
@@ -351,12 +361,21 @@
       </Tabs>
     </template>
     <template #prepend-footer>
-      <div class="flex flex-auto items-center">
-        <Button v-if="showMainBtn"
-                type="primary"
-                @click="modalApi.onConfirm()">
-          {{ mainActionText }}
-        </Button>
+      <div class="flex flex-auto flex-col gap-2">
+        <div v-if="canUseAi" class="flex items-center gap-2">
+          <PredictMaterial :work-order-id="formData!.id" />
+          <PredictDuration :work-order-id="formData!.id" />
+          <PredictRisk :work-order-id="formData!.id" />
+          <PredictDelivery :work-order-id="formData!.id" />
+        </div>
+        <div v-if="canUseAi && showMainBtn" class="border-t border-gray-200" />
+        <div class="flex items-center">
+          <Button v-if="showMainBtn"
+                  type="primary"
+                  @click="modalApi.onConfirm()">
+            {{ mainActionText }}
+          </Button>
+        </div>
       </div>
     </template>
     <template #append-footer>
diff --git a/vite.config.ts b/vite.config.ts
index dcf9f5b..149f3a6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -55,7 +55,7 @@
           "/admin-api": {
             changeOrigin: true,
             rewrite: path => path.replace(/^\/admin-api/, ""),
-            target: "http://192.168.0.226:48080/admin-api",
+            target: "http://192.168.0.10:48080/admin-api",
             ws: true,
           },
         },

--
Gitblit v1.9.3