| | |
| | | # åºç¨æ é¢ |
| | | VITE_APP_TITLE=工忰ååMOMç³»ç» |
| | | VITE_APP_TITLE=å·¥ä¸AIæºé å¹³å° |
| | | |
| | | # åºç¨å½å空é´ï¼ç¨äºç¼åãstoreçåè½çåç¼ï¼ç¡®ä¿é离 |
| | | VITE_APP_NAMESPACE=yudao-vben-antd |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 }, |
| | | ); |
| | | } |
| | |
| | | "profile": "Profile" |
| | | }, |
| | | "dashboard": { |
| | | "title": "Dashboard", |
| | | "analytics": "Analytics", |
| | | "title": "Home", |
| | | "analytics": "Home", |
| | | "workspace": "Workspace" |
| | | }, |
| | | "action": { |
| | |
| | | "profile": "个人ä¸å¿" |
| | | }, |
| | | "dashboard": { |
| | | "title": "æ¦è§", |
| | | "analytics": "åæé¡µ", |
| | | "title": "é¦é¡µ", |
| | | "analytics": "é¦é¡µ", |
| | | "workspace": "å·¥ä½å°" |
| | | }, |
| | | "action": { |
| | |
| | | </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> |
| | |
| | | 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: 'é¦é¡µ', |
| | | }, |
| | | }, |
| | | ]; |
| | |
| | | disabled?: boolean; |
| | | modelValue?: number; |
| | | placeholder?: string; |
| | | label?: string; |
| | | }>(), |
| | | { |
| | | allowClear: true, |
| | | disabled: false, |
| | | modelValue: undefined, |
| | | placeholder: 'è¯·éæ©ç©æ', |
| | | label: undefined, |
| | | }, |
| | | ); |
| | | const emit = defineEmits<{ |
| | |
| | | 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 && |
| | |
| | | } 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(); |
| | | |
| | |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>(); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | |
| | | /** å建æ¥ä»·å */ |
| | | 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(); |
| | | }); |
| | | } |
| | | |
| | | /** ç¼è¾æ¥ä»·å */ |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: 'AI è¯å«å½å
¥', |
| | | type: 'default', |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | <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'; |
| | |
| | | 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 { |
| | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<CrmSaleQuotationApi.SaleQuotation>(); |
| | | const itemFormRef = ref(); |
| | | const ocrCustomerName = ref(''); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | 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(); |
| | |
| | | |
| | | <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 |
| | |
| | | <MdmItemSelect |
| | | v-if="!disabled" |
| | | v-model:model-value="row.itemId" |
| | | :label="row.itemName" |
| | | placeholder="è¯·éæ©äº§å" |
| | | @change="(item) => handleItemChange(item, row)" |
| | | /> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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 = 'æ£å¨è¿è¡ AI OCR è¯å«ï¼è¯·ç¨å...'; |
| | | const ocrData = await ocrSaleQuotation(blobId); |
| | | // æ£æ¥é误 |
| | | if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) { |
| | | // ææå段为 nullï¼rawText å
å«é误æç¤º |
| | | 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ãWordãExcelãå¾çãçº¯ææ¬æä»¶ |
| | | </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> |
| | |
| | | |
| | | 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'); |
| | |
| | | }, |
| | | ]; |
| | | |
| | | 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> |
| | |
| | | exportFeedback, |
| | | getFeedbackPage, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { ref } from 'vue'; |
| | |
| | | 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('è·åå·¥åä¿¡æ¯å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** å¿«éæ¥å·¥ */ |
| | |
| | | { |
| | | label: '彿¡£', |
| | | type: 'link', |
| | | onClick: () => archiveDetailRef.value?.open(row.itemCode), |
| | | onClick: handleArchive.bind(null, row), |
| | | }, |
| | | ]" |
| | | /> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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"; |
| | | |
| | |
| | | () => |
| | | 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; |
| | |
| | | </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> |
| | |
| | | "/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, |
| | | }, |
| | | }, |