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