Przeglądaj źródła

解决跨域问题

胡北宽 1 tydzień temu
rodzic
commit
c3811201fd
5 zmienionych plików z 838 dodań i 1067 usunięć
  1. 370
    1061
      package-lock.json
  2. 16
    2
      src/api/contractApi.js
  3. 6
    0
      src/router/index.js
  4. 438
    0
      src/views/ImportSalesContractExcel.vue
  5. 8
    4
      vite.config.js

+ 370
- 1061
package-lock.json
Plik diff jest za duży
Wyświetl plik


+ 16
- 2
src/api/contractApi.js Wyświetl plik

@@ -6,7 +6,7 @@ const service = axios.create({
6 6
   timeout: 10000
7 7
 })
8 8
 
9
-// 导入销售合同Excel
9
+// 导入采购合同Excel
10 10
 export const importContractExcel = (file) => {
11 11
   const formData = new FormData()
12 12
   formData.append('file', file)
@@ -19,7 +19,21 @@ export const importContractExcel = (file) => {
19 19
     }
20 20
   })
21 21
 }
22
+// 导入销售合同Excel
23
+export const importSalesContractExcel = (file) => {
24
+  const formData = new FormData()
25
+  formData.append('file', file)
26
+  return service({
27
+    url: '/salesContract/importExcel',
28
+    method: 'post',
29
+    data: formData,
30
+    headers: {
31
+      'Content-Type': 'multipart/form-data'
32
+    }
33
+  })
34
+}
22 35
 
23 36
 export default {
24
-  importContractExcel
37
+  importContractExcel,
38
+  importSalesContractExcel
25 39
 }

+ 6
- 0
src/router/index.js Wyświetl plik

@@ -4,6 +4,7 @@ import PurchaseContractView from '../views/PurchaseContractView.vue'
4 4
 import UserList from '../views/UserList.vue'
5 5
 import UserAdd from '../views/UserAdd.vue'
6 6
 import UserEdit from '../views/UserEdit.vue'
7
+import ImportSalesContractExcel from "../views/ImportSalesContractExcel.vue";
7 8
 
8 9
 const routes = [
9 10
   {
@@ -11,6 +12,11 @@ const routes = [
11 12
     name: 'contract',
12 13
     component: SalesContractView
13 14
   },
15
+  {
16
+    path:'/sales',
17
+    name:'sales',
18
+    component: ImportSalesContractExcel
19
+  },
14 20
   {
15 21
     path: '/PC',
16 22
     name: 'PC',

+ 438
- 0
src/views/ImportSalesContractExcel.vue Wyświetl plik

@@ -0,0 +1,438 @@
1
+<template>
2
+  <div class="excel-import-simple">
3
+    <el-card class="import-card">
4
+      <template #header>
5
+        <div class="card-header">
6
+          <span class="title">销售合同Excel导入</span>
7
+        </div>
8
+      </template>
9
+
10
+      <!-- 文件上传区域 -->
11
+      <div class="upload-area">
12
+        <el-upload
13
+            ref="uploadRef"
14
+            class="upload-demo"
15
+            drag
16
+            action="#"
17
+            :auto-upload="false"
18
+            :on-change="handleFileChange"
19
+            :on-remove="handleFileRemove"
20
+            :file-list="fileList"
21
+            :limit="1"
22
+            accept=".xlsx,.xls"
23
+        >
24
+          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
25
+          <div class="el-upload__text">
26
+            将销售合同Excel文件拖到此处,或<em>点击上传</em>
27
+          </div>
28
+          <template #tip>
29
+            <div class="el-upload__tip">
30
+              只能上传 .xlsx 或 .xls 格式的Excel文件
31
+            </div>
32
+          </template>
33
+        </el-upload>
34
+      </div>
35
+
36
+      <!-- 导入按钮 -->
37
+      <div class="action-buttons">
38
+        <el-button
39
+            type="primary"
40
+            :loading="loading"
41
+            :disabled="!fileList.length"
42
+            @click="handleImport"
43
+        >
44
+          {{ loading ? '导入中...' : '开始导入' }}
45
+        </el-button>
46
+        <el-button @click="handleReset">重置</el-button>
47
+      </div>
48
+
49
+      <!-- 导入结果 -->
50
+      <div v-if="importResult" class="result-area">
51
+        <!-- 成功结果 -->
52
+        <div v-if="importResult.success" class="success-result">
53
+          <el-alert
54
+              :title="importResult.title"
55
+              type="success"
56
+              :description="importResult.message"
57
+              show-icon
58
+              :closable="false"
59
+          />
60
+
61
+          <!-- 成功统计信息 -->
62
+          <div class="statistics">
63
+            <el-descriptions :column="2" border size="small">
64
+              <el-descriptions-item label="总记录数">
65
+                <el-tag type="info">{{ importResult.totalRecords }}</el-tag>
66
+              </el-descriptions-item>
67
+              <el-descriptions-item label="成功记录数">
68
+                <el-tag type="success">{{ importResult.successRecords }}</el-tag>
69
+              </el-descriptions-item>
70
+              <el-descriptions-item label="失败记录数">
71
+                <el-tag type="danger">{{ importResult.totalRecords - importResult.successRecords }}</el-tag>
72
+              </el-descriptions-item>
73
+              <el-descriptions-item label="成功率">
74
+                <el-tag :type="getSuccessRateType(importResult.successRate)">
75
+                  {{ (importResult.successRate * 100).toFixed(2) }}%
76
+                </el-tag>
77
+              </el-descriptions-item>
78
+            </el-descriptions>
79
+          </div>
80
+        </div>
81
+
82
+        <!-- 失败结果 -->
83
+        <div v-else class="error-result">
84
+          <el-alert
85
+              :title="importResult.title"
86
+              type="error"
87
+              :description="importResult.message"
88
+              show-icon
89
+              :closable="false"
90
+          />
91
+
92
+          <!-- 错误列表 -->
93
+          <div v-if="importResult.errors && importResult.errors.length" class="error-list">
94
+            <div class="error-header">
95
+              <h4>
96
+                <el-icon color="#F56C6C"><Warning /></el-icon>
97
+                错误信息(共 {{ importResult.errors.length }} 条)
98
+              </h4>
99
+              <el-button
100
+                  size="small"
101
+                  @click="exportErrors"
102
+                  :disabled="!importResult.errors.length"
103
+              >
104
+                导出错误报告
105
+              </el-button>
106
+            </div>
107
+            <div class="error-scroll">
108
+              <div
109
+                  v-for="(error, index) in importResult.errors"
110
+                  :key="index"
111
+                  class="error-item"
112
+              >
113
+                <span class="error-index">{{ index + 1 }}.</span>
114
+                <span class="error-content">{{ error }}</span>
115
+              </div>
116
+            </div>
117
+          </div>
118
+        </div>
119
+      </div>
120
+
121
+      <!-- 导入说明 -->
122
+      <div class="import-tips">
123
+        <el-alert
124
+            title="导入说明"
125
+            type="info"
126
+            :closable="false"
127
+        >
128
+          <template #default>
129
+            <div class="tips-content">
130
+              <p>1. 请确保Excel文件包含以下Sheet:</p>
131
+              <ul>
132
+                <li>Sheet1: 销售合同主表</li>
133
+                <li>Sheet2: 产品明细</li>
134
+                <li>Sheet3: 收款计划</li>
135
+                <li>Sheet4: 责任中心</li>
136
+                <li>Sheet5: 费用明细</li>
137
+                <li>Sheet6: 服务费率</li>
138
+              </ul>
139
+              <p>2. 系统会自动根据合同号关联主表和子表数据</p>
140
+              <p>3. 如果合同已存在,系统会自动删除后重新导入</p>
141
+            </div>
142
+          </template>
143
+        </el-alert>
144
+      </div>
145
+    </el-card>
146
+  </div>
147
+</template>
148
+
149
+<script setup>
150
+import { ref, computed } from 'vue'
151
+import { ElMessage } from 'element-plus'
152
+import { UploadFilled, Warning } from '@element-plus/icons-vue'
153
+import axios from 'axios'
154
+
155
+// 响应式数据
156
+const uploadRef = ref()
157
+const fileList = ref([])
158
+const loading = ref(false)
159
+const importResult = ref(null)
160
+
161
+// API配置
162
+const API_BASE_URL = 'http://localhost:8080' // 确保与Postman中使用的地址一致
163
+
164
+// 计算属性 - 成功率类型
165
+const getSuccessRateType = (rate) => {
166
+  if (rate >= 0.9) return 'success'
167
+  if (rate >= 0.7) return 'warning'
168
+  return 'danger'
169
+}
170
+
171
+// 处理文件选择
172
+const handleFileChange = (file) => {
173
+  const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls')
174
+
175
+  if (!isExcel) {
176
+    ElMessage.error('只能上传Excel文件!')
177
+    uploadRef.value.handleRemove(file)
178
+    return false
179
+  }
180
+
181
+  const isLt50M = file.size / 1024 / 1024 < 50
182
+  if (!isLt50M) {
183
+    ElMessage.error('Excel文件大小不能超过50MB!')
184
+    uploadRef.value.handleRemove(file)
185
+    return false
186
+  }
187
+
188
+  fileList.value = [file]
189
+  importResult.value = null
190
+}
191
+
192
+// 处理文件移除
193
+const handleFileRemove = () => {
194
+  fileList.value = []
195
+  importResult.value = null
196
+}
197
+
198
+// 执行导入
199
+const handleImport = async () => {
200
+  if (!fileList.value.length) {
201
+    ElMessage.warning('请先选择Excel文件')
202
+    return
203
+  }
204
+
205
+  const file = fileList.value[0].raw
206
+  loading.value = true
207
+  importResult.value = null
208
+
209
+  try {
210
+    const formData = new FormData()
211
+    formData.append('file', file)
212
+
213
+    console.log('开始上传文件:', file.name)
214
+    console.log('文件大小:', file.size)
215
+
216
+    // 方案1:直接调用完整URL,关闭withCredentials
217
+    const response = await axios.post(
218
+        'http://localhost:8080/api/salesContract/import',
219
+        formData,
220
+        {
221
+          headers: {
222
+            'Content-Type': 'multipart/form-data'
223
+          },
224
+          timeout: 120000,
225
+          withCredentials: false  // 必须设为false
226
+        }
227
+    )
228
+
229
+    console.log('API响应成功:', response.status)
230
+    console.log('响应数据:', response.data)
231
+
232
+    // 处理响应数据
233
+    if (response.status === 200) {
234
+      const resultData = response.data
235
+
236
+      // 根据实际返回结构判断
237
+      if (resultData.code === 200 || resultData.success === true) {
238
+        importResult.value = {
239
+          success: true,
240
+          title: '导入成功',
241
+          message: resultData.message || '导入成功',
242
+          totalRecords: resultData.totalRecords || 0,
243
+          successRecords: resultData.successRecords || 0,
244
+          errors: resultData.errors || []
245
+        }
246
+        ElMessage.success('导入成功')
247
+      } else {
248
+        importResult.value = {
249
+          success: false,
250
+          title: '导入失败',
251
+          message: resultData.message || '导入失败',
252
+          errors: resultData.errors || []
253
+        }
254
+        ElMessage.error(resultData.message || '导入失败')
255
+      }
256
+    }
257
+
258
+  } catch (error) {
259
+    console.error('详细错误信息:', error)
260
+    console.error('错误配置:', error.config)
261
+
262
+    let errorMessage = '网络请求失败'
263
+    let errorDetails = []
264
+
265
+    if (error.code === 'ERR_NETWORK') {
266
+      errorMessage = '网络连接被阻止'
267
+      errorDetails = [
268
+        '可能的原因:',
269
+        '1. 后端服务未启动',
270
+        '2. 端口被占用',
271
+        '3. 防火墙阻止',
272
+        '4. 跨域限制'
273
+      ]
274
+    }
275
+
276
+    ElMessage.error(errorMessage)
277
+    importResult.value = {
278
+      success: false,
279
+      title: '导入失败',
280
+      message: errorMessage,
281
+      errors: errorDetails
282
+    }
283
+  } finally {
284
+    loading.value = false
285
+  }
286
+}
287
+
288
+// 导出错误报告
289
+const exportErrors = () => {
290
+  if (!importResult.value || !importResult.value.errors.length) return
291
+
292
+  const errorText = importResult.value.errors.join('\n')
293
+  const blob = new Blob([errorText], { type: 'text/plain;charset=utf-8' })
294
+  const url = URL.createObjectURL(blob)
295
+  const link = document.createElement('a')
296
+  link.href = url
297
+  link.download = `销售合同导入错误报告_${new Date().toISOString().split('T')[0]}.txt`
298
+  document.body.appendChild(link)
299
+  link.click()
300
+  document.body.removeChild(link)
301
+  URL.revokeObjectURL(url)
302
+
303
+  ElMessage.success('错误报告导出成功')
304
+}
305
+
306
+// 重置
307
+const handleReset = () => {
308
+  fileList.value = []
309
+  importResult.value = null
310
+  uploadRef.value?.clearFiles()
311
+}
312
+</script>
313
+
314
+<style scoped>
315
+.excel-import-simple {
316
+  padding: 20px;
317
+  max-width: 900px;
318
+  margin: 0 auto;
319
+}
320
+
321
+.import-card {
322
+  min-height: 500px;
323
+}
324
+
325
+.card-header {
326
+  display: flex;
327
+  justify-content: space-between;
328
+  align-items: center;
329
+}
330
+
331
+.title {
332
+  font-size: 18px;
333
+  font-weight: bold;
334
+}
335
+
336
+.upload-area {
337
+  margin: 20px 0;
338
+}
339
+
340
+.action-buttons {
341
+  text-align: center;
342
+  margin: 20px 0;
343
+}
344
+
345
+.result-area {
346
+  margin-top: 20px;
347
+}
348
+
349
+.success-result,
350
+.error-result {
351
+  margin-bottom: 20px;
352
+}
353
+
354
+.statistics {
355
+  margin-top: 15px;
356
+  padding: 15px;
357
+  background: #f0f9ff;
358
+  border-radius: 4px;
359
+  border: 1px solid #e1f3ff;
360
+}
361
+
362
+.error-list {
363
+  margin-top: 15px;
364
+  padding: 15px;
365
+  background: #fef0f0;
366
+  border-radius: 4px;
367
+  border: 1px solid #fde2e2;
368
+}
369
+
370
+.error-header {
371
+  display: flex;
372
+  justify-content: space-between;
373
+  align-items: center;
374
+  margin-bottom: 10px;
375
+}
376
+
377
+.error-header h4 {
378
+  margin: 0;
379
+  color: #f56c6c;
380
+  font-size: 14px;
381
+  display: flex;
382
+  align-items: center;
383
+  gap: 8px;
384
+}
385
+
386
+.error-scroll {
387
+  max-height: 300px;
388
+  overflow-y: auto;
389
+}
390
+
391
+.error-item {
392
+  display: flex;
393
+  align-items: flex-start;
394
+  padding: 8px 0;
395
+  border-bottom: 1px solid #fde2e2;
396
+  line-height: 1.4;
397
+}
398
+
399
+.error-item:last-child {
400
+  border-bottom: none;
401
+}
402
+
403
+.error-index {
404
+  color: #909399;
405
+  font-size: 12px;
406
+  margin-right: 8px;
407
+  flex-shrink: 0;
408
+  min-width: 20px;
409
+}
410
+
411
+.error-content {
412
+  font-size: 13px;
413
+  color: #f56c6c;
414
+  word-break: break-all;
415
+}
416
+
417
+.import-tips {
418
+  margin-top: 20px;
419
+}
420
+
421
+.tips-content {
422
+  font-size: 13px;
423
+  line-height: 1.6;
424
+}
425
+
426
+.tips-content p {
427
+  margin: 5px 0;
428
+}
429
+
430
+.tips-content ul {
431
+  margin: 5px 0;
432
+  padding-left: 20px;
433
+}
434
+
435
+.tips-content li {
436
+  margin: 2px 0;
437
+}
438
+</style>

+ 8
- 4
vite.config.js Wyświetl plik

@@ -1,18 +1,22 @@
1 1
 import { defineConfig } from 'vite'
2 2
 import vue from '@vitejs/plugin-vue'
3 3
 
4
-// https://vitejs.dev/config/
5 4
 export default defineConfig({
6 5
   plugins: [vue()],
7 6
   server: {
8 7
     port: 3000,
9
-    open: true,
8
+    host: 'localhost', // 明确指定host
10 9
     proxy: {
11 10
       '/api': {
12 11
         target: 'http://localhost:8080',
13 12
         changeOrigin: true,
14
-        rewrite: (path) => path.replace(/^\/api/, '')
13
+        secure: false,
14
+        configure: (proxy, options) => {
15
+          proxy.on('proxyReq', (proxyReq, req, res) => {
16
+            console.log('代理请求:', req.method, req.url)
17
+          })
18
+        }
15 19
       }
16 20
     }
17 21
   }
18
-})
22
+})

Ładowanie…
Anuluj
Zapisz