liu
2 天以前 5a322d24b59c4b70c08e792f21162be0f05d0199
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
 
import { computed, ref } from 'vue';
 
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
 
import { Button, message, Progress, Upload } from 'ant-design-vue';
 
import {
  importMaterialStock,
  importMaterialStockTemplate,
} from '#/api/mes/wm/materialstock';
 
defineOptions({ name: 'WmMaterialStockImportModal' });
 
const emit = defineEmits<{ success: [] }>();
 
const fileList = ref<FileType[]>([]);
const importResult = ref<MesWmMaterialStockApi.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 importMaterialStock(fileList.value[0] as File);
      // 完成:进度条走满
      stopMockProgress();
      progressPercent.value = 100;
      await delay(300);
      emit('success');
      const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
      if (failedCount > 0) {
        message.warning(
          `导入完成:成功 ${createCount.value} 条、覆盖 ${updateCount.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 importMaterialStockTemplate();
  downloadFileFromBlobPart({ fileName: '库存现有量导入模板.xls', source: data });
}
 
const createCount = computed(() => importResult.value?.createNames?.length ?? 0);
const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0);
const failureCount = computed(
  () => Object.keys(importResult.value?.failureNames ?? {}).length,
);
</script>
 
<template>
  <Modal title="导入库存现有量" class="w-[640px]">
    <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-3 rounded-md border border-gray-200 bg-gray-50 p-3 text-sm leading-6 text-gray-600"
      >
        模板列:物料编码、仓库、库区、库位、批次号、供应商、<b>操作类型</b>、<b>在库数量</b>。<br />
        操作类型必填:
        <b class="text-gray-800">增加</b> / <b class="text-gray-800">减少</b>
        按"在库数量"作为本次变动量加减库存;
        <b class="text-gray-800">覆盖</b>
        把该记录在库数量直接设为"在库数量"(期初建账推荐),已存在记录自动按差额调整。<br />
        仓库/库区/库位/供应商可填编码或名称;物料启用批次管理时批次号必填;每行独立执行,失败行不影响其它行;仅支持
        xls、xlsx 文件
      </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 flex-wrap 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="createCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-green-600">
          <div v-for="name in importResult.createNames" :key="`c-${name}`">
            {{ name }}
          </div>
        </div>
        <div v-if="updateCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-blue-600">
          <div v-for="name in importResult.updateNames" :key="`u-${name}`">
            {{ name }}
          </div>
        </div>
        <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
          <div
            v-for="(reason, name) in importResult.failureNames"
            :key="`f-${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>