| src/api/crm/contract/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/erp/purchase/request/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/crm/contract/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/crm/contract/modules/import-form.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/erp/purchase/request/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/erp/purchase/request/modules/import-form.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/crm/contract/index.ts
@@ -160,3 +160,23 @@ export function generateSaleOrder(id: number) { return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } }); } /** ååå¯¼å ¥ç»æ */ export interface ImportResult { /** å建æåçååç¼å· */ createList?: string[]; /** æ´æ°æåçååç¼å· */ updateList?: string[]; /** å¯¼å ¥å¤±è´¥éåï¼key 为ååç¼å·æè¡å·ï¼value 为失败åå */ failureList?: Record<string, string>; } /** ä¸è½½ååå¯¼å ¥æ¨¡æ¿ */ export function importContractTemplate() { return requestClient.download('/crm/contract/get-import-template'); } /** å¯¼å ¥åå */ export function importContract(file: File) { return requestClient.upload<ImportResult>('/crm/contract/import', { file }); } src/api/erp/purchase/request/index.ts
@@ -46,6 +46,16 @@ } } /** éè´ç³è¯·å¯¼å ¥ç»æ */ export interface ImportResult { /** å建æåçéè´ç³è¯·åå· */ createList?: string[]; /** æ´æ°æåçéè´ç³è¯·åå· */ updateList?: string[]; /** å¯¼å ¥å¤±è´¥éåï¼key 为éè´ç³è¯·åå·æè¡å·ï¼value 为失败åå */ failureList?: Record<string, string>; } /** å建éè´ç³è¯· */ export function createPurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) { return requestClient.post('/erp/purchase-request/create', data); @@ -134,3 +144,13 @@ } } } /** ä¸è½½éè´ç³è¯·å¯¼å ¥æ¨¡æ¿ */ export function importPurchaseRequestTemplate() { return requestClient.download('/erp/purchase-request/get-import-template'); } /** å¯¼å ¥éè´ç³è¯· */ export function importPurchaseRequest(file: File) { return requestClient.upload<ImportResult>('/erp/purchase-request/import', { file }); } src/views/crm/contract/index.vue
@@ -23,12 +23,18 @@ import { useGridColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; import ImportForm from './modules/import-form.vue'; const { push } = useRouter(); const sceneType = ref('1'); const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true, }); const [ImportModal, importModalApi] = useVbenModal({ connectedComponent: ImportForm, destroyOnClose: true, }); @@ -51,6 +57,11 @@ ...formValues, }); downloadFileFromBlobPart({ fileName: 'åå.xls', source: data }); } /** å¯¼å ¥åå */ function handleImport() { importModalApi.open(); } /** å建åå */ @@ -205,6 +216,7 @@ <template> <Page auto-content-height> <FormModal @success="handleRefresh" /> <ImportModal @success="handleRefresh" /> <Grid> <template #toolbar-actions> <Tabs class="w-full" @change="handleChangeSceneType"> @@ -230,6 +242,13 @@ auth: ['crm:contract:export'], onClick: handleExport, }, { label: 'å¯¼å ¥', type: 'primary', icon: ACTION_ICON.UPLOAD, auth: ['crm:contract:import'], onClick: handleImport, }, ]" /> </template> src/views/crm/contract/modules/import-form.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,155 @@ <script lang="ts" setup> import type { FileType } from 'ant-design-vue/es/upload/interface'; import type { ImportResult } from '#/api/crm/contract'; import { computed, ref } from 'vue'; import { useVbenModal } from '#/packages/effects/common-ui/src'; import { downloadFileFromBlobPart } from '#/packages/utils/src'; import { Button, message, Progress, Upload } from 'ant-design-vue'; import { importContract, importContractTemplate } from '#/api/crm/contract'; const emit = defineEmits(['success']); const fileList = ref<FileType[]>([]); const importResult = ref<ImportResult>(); const importing = ref(false); const progressPercent = ref(0); let progressTimer: ReturnType<typeof setInterval> | undefined; const [Modal, modalApi] = useVbenModal({ async onConfirm() { let shouldClose = false; if (fileList.value.length === 0) { message.warning('è¯·éæ©è¦å¯¼å ¥çæä»¶'); return; } if (importing.value) return; modalApi.lock(); importing.value = true; startMockProgress(); try { importResult.value = await importContract(fileList.value[0] as File); // 宿ï¼è¿åº¦æ¡èµ°æ»¡ stopMockProgress(); progressPercent.value = 100; await delay(300); emit('success'); const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; if (failedCount > 0) { message.warning( `å¯¼å ¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, ); } else { message.success('å¯¼å ¥å®æ'); shouldClose = true; } } finally { stopMockProgress(); importing.value = false; modalApi.unlock(); if (shouldClose) { await modalApi.close(); } } }, }); /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ function startMockProgress() { progressPercent.value = 0; progressTimer = setInterval(() => { if (progressPercent.value < 90) { progressPercent.value = Math.min( 90, progressPercent.value + Math.random() * 12, ); } }, 300); } function stopMockProgress() { if (progressTimer) { clearInterval(progressTimer); progressTimer = undefined; } } function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function beforeUpload(file: FileType) { fileList.value = [file]; importResult.value = undefined; return false; } function handleRemove() { fileList.value = []; importResult.value = undefined; } async function handleDownloadTemplate() { const data = await importContractTemplate(); downloadFileFromBlobPart({ fileName: 'ååå¯¼å ¥æ¨¡æ¿.xls', source: data }); } const createCount = computed(() => importResult.value?.createList?.length ?? 0); const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); const failureCount = computed( () => Object.keys(importResult.value?.failureList ?? {}).length, ); </script> <template> <Modal title="å¯¼å ¥åå" class="w-[720px]"> <div class="mx-4"> <Upload :max-count="1" accept=".xls,.xlsx" :file-list="fileList" :before-upload="beforeUpload" @remove="handleRemove" > <Button type="primary">éæ© Excel æä»¶</Button> </Upload> <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> <p>ä» å è®¸å¯¼å ¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿ å¡«åï¼åååç§°ã客æ·åç§°ã产åç¼ç ãæ°éãâ客æ·åç§°âéä¸ CRM 客æ·ä¸è´ï¼â产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä» ä¾å¯¹ç §ï¼ï¼âåååä»·(å )âç空ååç©æéå®ä»·ã</p> <p>ä¸å¼ ååå¯å«å¤æ¡æç»ï¼ç¸åâååç¼å·âçå¤è¡ä¼å并为ä¸å¼ ååï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âååç¼å·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°ååãååç¼å·å·²å卿¶ä¼å¯¼å ¥å¤±è´¥ã</p> <p>ååé颿âæç»åä»·Ãæ°é â 伿 çâèªå¨è®¡ç®ï¼å®ééé¢å¯å¡«ï¼è´è´£äºº = å½åç»å½ç¨æ·ï¼ç¶æä¸ºâæªæäº¤âï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ææäº¤å®¡æ¹ã</p> </div> <!-- å¯¼å ¥è¿åº¦æ¡ --> <div v-if="importing" class="mt-4"> <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å ¥ååæ°æ®ï¼è¯·ç¨å...</div> <Progress :percent="progressPercent" status="active" /> </div> <!-- å¯¼å ¥ç»æ --> <div v-if="importResult" class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" > <div class="mb-2 font-medium">å¯¼å ¥ç»æ</div> <div class="mb-2 flex gap-4"> <span class="text-green-600">æ°å¢ï¼{{ createCount }} æ¡</span> <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} æ¡</span> <span class="text-red-600">失败ï¼{{ failureCount }} æ¡</span> </div> <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> <div v-for="(reason, name) in importResult.failureList" :key="name" class="text-sm leading-6 text-red-500" > {{ name }}ï¼{{ reason }} </div> </div> </div> </div> <template #prepend-footer> <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å ¥æ¨¡æ¿</Button> </template> </Modal> </template> src/views/erp/purchase/request/index.vue
@@ -23,6 +23,7 @@ import { useGridColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; import ImportForm from './modules/import-form.vue'; import OrderForm from './modules/order-form.vue'; import ProcessSelectModal from './modules/process-select-modal.vue'; @@ -33,6 +34,11 @@ const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true, }); const [ImportModal, importModalApi] = useVbenModal({ connectedComponent: ImportForm, destroyOnClose: true, }); @@ -55,6 +61,11 @@ async function handleExport() { const data = await exportPurchaseRequest(await gridApi.formApi.getValues()); downloadFileFromBlobPart({ fileName: 'éè´ç³è¯·.xls', source: data }); } /** å¯¼å ¥éè´ç³è¯· */ function handleImport() { importModalApi.open(); } /** æ°å¢éè´ç³è¯· */ @@ -180,6 +191,7 @@ <template> <Page auto-content-height> <FormModal @success="handleRefresh" /> <ImportModal @success="handleRefresh" /> <OrderFormModal @success="handleRefresh" /> <ProcessModal @success="handleRefresh" /> <Grid table-title="éè´ç³è¯·å表"> @@ -201,6 +213,13 @@ onClick: handleExport, }, { label: 'å¯¼å ¥', type: 'primary', icon: ACTION_ICON.UPLOAD, auth: ['erp:purchase-request:import'], onClick: handleImport, }, { label: 'æ¹éå é¤', type: 'primary', danger: true, src/views/erp/purchase/request/modules/import-form.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,158 @@ <script lang="ts" setup> import type { FileType } from 'ant-design-vue/es/upload/interface'; import type { ImportResult } from '#/api/erp/purchase/request'; import { computed, ref } from 'vue'; import { useVbenModal } from '#/packages/effects/common-ui/src'; import { downloadFileFromBlobPart } from '#/packages/utils/src'; import { Button, message, Progress, Upload } from 'ant-design-vue'; import { importPurchaseRequest, importPurchaseRequestTemplate, } from '#/api/erp/purchase/request'; const emit = defineEmits(['success']); const fileList = ref<FileType[]>([]); const importResult = ref<ImportResult>(); const importing = ref(false); const progressPercent = ref(0); let progressTimer: ReturnType<typeof setInterval> | undefined; const [Modal, modalApi] = useVbenModal({ async onConfirm() { let shouldClose = false; if (fileList.value.length === 0) { message.warning('è¯·éæ©è¦å¯¼å ¥çæä»¶'); return; } if (importing.value) return; modalApi.lock(); importing.value = true; startMockProgress(); try { importResult.value = await importPurchaseRequest(fileList.value[0] as File); // 宿ï¼è¿åº¦æ¡èµ°æ»¡ stopMockProgress(); progressPercent.value = 100; await delay(300); emit('success'); const failedCount = Object.keys(importResult.value?.failureList ?? {}).length; if (failedCount > 0) { message.warning( `å¯¼å ¥å®æï¼æ°å¢ ${createCount.value} æ¡ï¼å¤±è´¥ ${failedCount} æ¡ï¼è¯·æ¥ç䏿¹å¤±è´¥æç»`, ); } else { message.success('å¯¼å ¥å®æ'); shouldClose = true; } } finally { stopMockProgress(); importing.value = false; modalApi.unlock(); if (shouldClose) { await modalApi.close(); } } }, }); /** 模æè¿åº¦æ¡ï¼è¯·æ±æé´ä» 0 èµ°å° 90% */ function startMockProgress() { progressPercent.value = 0; progressTimer = setInterval(() => { if (progressPercent.value < 90) { progressPercent.value = Math.min( 90, progressPercent.value + Math.random() * 12, ); } }, 300); } function stopMockProgress() { if (progressTimer) { clearInterval(progressTimer); progressTimer = undefined; } } function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function beforeUpload(file: FileType) { fileList.value = [file]; importResult.value = undefined; return false; } function handleRemove() { fileList.value = []; importResult.value = undefined; } async function handleDownloadTemplate() { const data = await importPurchaseRequestTemplate(); downloadFileFromBlobPart({ fileName: 'éè´ç³è¯·å¯¼å ¥æ¨¡æ¿.xls', source: data }); } const createCount = computed(() => importResult.value?.createList?.length ?? 0); const updateCount = computed(() => importResult.value?.updateList?.length ?? 0); const failureCount = computed( () => Object.keys(importResult.value?.failureList ?? {}).length, ); </script> <template> <Modal title="å¯¼å ¥éè´ç³è¯·" class="w-[720px]"> <div class="mx-4"> <Upload :max-count="1" accept=".xls,.xlsx" :file-list="fileList" :before-upload="beforeUpload" @remove="handleRemove" > <Button type="primary">éæ© Excel æä»¶</Button> </Upload> <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500"> <p>ä» å è®¸å¯¼å ¥ xlsãxlsx æ ¼å¼æä»¶ï¼å¿ å¡«åï¼äº§åç¼ç ãæ°éãâ产åç¼ç âæç©æç¼ç å¡«åï¼â产ååç§°âåä» ä¾å¯¹ç §ï¼ï¼ä¾åºåå¯ç©ºï¼å¡«äºéä¸ç³»ç»ä¾åºåä¸è´ï¼ï¼âéæ±æ¥æâæ ¼å¼å¦ 2026-01-01ï¼âæ¯å¦æ¥ææ£éªâå¡«âæ¯/å¦âã</p> <p>ä¸å¼ éè´ç³è¯·å¯å«å¤æ¡æç»ï¼ç¸åâéè´ç³è¯·åå·âçå¤è¡ä¼å并为ä¸å¼ ç³è¯·ï¼ä¸»è¡¨å段å该ç»é¦è¡ï¼ï¼âéè´ç³è¯·åå·âçç©ºæ¶æ¯ä¸è¡åçæä¸å¼ æ°ç³è¯·ãéè´ç³è¯·åå·å·²å卿¶ä¼å¯¼å ¥å¤±è´¥ã</p> <p>å¯¼å ¥çç³è¯·ï¼å ¶ãç³è¯·äºº / ç³è¯·é¨é¨ãæå½åç»å½ç¨æ·èªå¨å¸¦åºï¼ç¶æä¸ºâæªæäº¤âï¼å¯å¨å表ä¸ç»§ç»ç¼è¾ææäº¤å®¡æ¹ã</p> </div> <!-- å¯¼å ¥è¿åº¦æ¡ --> <div v-if="importing" class="mt-4"> <div class="mb-2 text-sm text-gray-600">æ£å¨å¯¼å ¥éè´ç³è¯·æ°æ®ï¼è¯·ç¨å...</div> <Progress :percent="progressPercent" status="active" /> </div> <!-- å¯¼å ¥ç»æ --> <div v-if="importResult" class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3" > <div class="mb-2 font-medium">å¯¼å ¥ç»æ</div> <div class="mb-2 flex gap-4"> <span class="text-green-600">æ°å¢ï¼{{ createCount }} æ¡</span> <span class="text-blue-600">è¦çæ´æ°ï¼{{ updateCount }} æ¡</span> <span class="text-red-600">失败ï¼{{ failureCount }} æ¡</span> </div> <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto"> <div v-for="(reason, name) in importResult.failureList" :key="name" class="text-sm leading-6 text-red-500" > {{ name }}ï¼{{ reason }} </div> </div> </div> </div> <template #prepend-footer> <Button @click="handleDownloadTemplate">ä¸è½½å¯¼å ¥æ¨¡æ¿</Button> </template> </Modal> </template>