perf: 模板优化
This commit is contained in:
@@ -1 +1 @@
|
|||||||
# Vue 3 + Typescript + Vite + ElementUI-Plus + DayJs + Pinia
|
# Vue 3 + Typescript + Vite + TDdesign + DayJs + Pinia
|
||||||
|
|||||||
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -6,5 +6,5 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
|
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ define(['./workbox-86c9b217'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.t9v2fs5c6io"
|
"revision": "0.o5uo4dculfk"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/logo.png" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||||
/>
|
/>
|
||||||
<title>Vue项目模板</title>
|
<title>Vue模板</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2025-10-15 09:50:32",
|
"version": "2025-12-27 10:25:12",
|
||||||
"buildTime": "2025-10-15T01:50:32.937Z",
|
"buildTime": "2025-12-27T02:25:12.719Z",
|
||||||
"environment": "production"
|
"environment": "production"
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ onMounted(async () => {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 6px 8px;
|
padding: 16px 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #333;
|
color: #333;
|
||||||
background: rgba(255, 255, 255, 0.85);
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
|||||||
@@ -3,6 +3,37 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 禁用移动端弹性滚动 */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
-webkit-overflow-scrolling: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用iOS橡皮筋效果 */
|
||||||
|
body {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止下拉刷新和弹性滚动 */
|
||||||
|
html {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
li {
|
li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -15,8 +15,8 @@ import TDesign from 'tdesign-vue-next'
|
|||||||
import 'tdesign-vue-next/es/style/index.css'
|
import 'tdesign-vue-next/es/style/index.css'
|
||||||
import 'normalize.css/normalize.css'
|
import 'normalize.css/normalize.css'
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
// 启动版本检测服务 (20秒检查一次)
|
// 启动版本检测服务 (两小时检查一次)
|
||||||
startVersionCheck(1000 * 20)
|
startVersionCheck(1000 * 60 * 2)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPluginPersistedstate) // 注入插件
|
pinia.use(piniaPluginPersistedstate) // 注入插件
|
||||||
// 挂载到app上
|
// 挂载到app上
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: () => import('@/views/index.vue')
|
component: () => import('@/views/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/excel-upload',
|
||||||
|
name: 'ExcelUpload',
|
||||||
|
component: () => import('@/views/excel-upload/index.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
71
src/services/excel-upload.ts
Normal file
71
src/services/excel-upload.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Axios from '../utils/request/base-service' // 导入配置好的axios文件
|
||||||
|
import type { AxiosProgressEvent } from 'axios'
|
||||||
|
|
||||||
|
// 封装axios请求函数,并用export导出
|
||||||
|
export function uploadExcel(
|
||||||
|
file: FormData,
|
||||||
|
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
|
) {
|
||||||
|
return Axios({
|
||||||
|
url: '/upload',
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
data: file,
|
||||||
|
responseType: 'blob',
|
||||||
|
onUploadProgress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkHealth() {
|
||||||
|
return Axios({
|
||||||
|
url: '/health',
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Pragma: 'no-cache'
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
_t: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkDataSourceStatus() {
|
||||||
|
return Axios({
|
||||||
|
url: '/data-source-status',
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Pragma: 'no-cache'
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
_t: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testConnection() {
|
||||||
|
return Axios({
|
||||||
|
url: '/test',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
_t: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProcessingProgress() {
|
||||||
|
return Axios({
|
||||||
|
url: '/progress',
|
||||||
|
method: 'get',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Pragma: 'no-cache'
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
_t: Date.now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,41 +7,41 @@
|
|||||||
* @LastEditTime: 2022-01-25 18:04:29
|
* @LastEditTime: 2022-01-25 18:04:29
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Axios from "../api/base-service"; // 导入配置好的axios文件
|
import Axios from '../utils/request/base-service' // 导入配置好的axios文件
|
||||||
// 封装axios请求函数,并用export导出
|
// 封装axios请求函数,并用export导出
|
||||||
export function getInfo(datas: unknown) {
|
export function getInfo(datas: unknown) {
|
||||||
return Axios({
|
return Axios({
|
||||||
url: "/api.php?key=free&appid=0&msg=鹅鹅鹅",
|
url: '/api.php?key=free&appid=0&msg=鹅鹅鹅',
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
'content-type': 'application/json'
|
||||||
},
|
},
|
||||||
data: datas,
|
data: datas
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
export function getInfoA(datas: unknown) {
|
export function getInfoA(datas: unknown) {
|
||||||
return Axios({
|
return Axios({
|
||||||
url: "/api/getbooks",
|
url: '/api/getbooks',
|
||||||
method: "get",
|
method: 'get',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded", //设置请求头请求格式form
|
'Content-Type': 'application/x-www-form-urlencoded' //设置请求头请求格式form
|
||||||
},
|
},
|
||||||
data: datas,
|
data: datas
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
export function getItem(datas: unknown) {
|
export function getItem(datas: unknown) {
|
||||||
return Axios({
|
return Axios({
|
||||||
url: "/api/getItem",
|
url: '/api/getItem',
|
||||||
method: "post",
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json", //设置请求头请求格式为json
|
'Content-Type': 'application/json' //设置请求头请求格式为json
|
||||||
},
|
},
|
||||||
data: datas,
|
data: datas
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
export function getItemInfo(datas: unknown) {
|
export function getItemInfo(datas: unknown) {
|
||||||
return Axios({
|
return Axios({
|
||||||
url: "/api/getItemInfo" + datas,
|
url: '/api/getItemInfo' + datas,
|
||||||
method: "get",
|
method: 'get'
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { DialogPlugin } from 'tdesign-vue-next'
|
||||||
// 获取浏览器中的url地址和端口号并拼接
|
// 获取浏览器中的url地址和端口号并拼接
|
||||||
/* import { config } from 'public/config.js'
|
/* import { config } from 'public/config.js'
|
||||||
const getBaseUrl = () => {
|
const getBaseUrl = () => {
|
||||||
@@ -13,8 +13,8 @@ const getBaseUrl = () => {
|
|||||||
} */
|
} */
|
||||||
// 创建一个新的axios实例
|
// 创建一个新的axios实例
|
||||||
const instance: AxiosInstance = axios.create({
|
const instance: AxiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_BASE_API,
|
baseURL: '/api',
|
||||||
timeout: 30000,
|
timeout: 3000000,
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -46,14 +46,31 @@ instance.interceptors.response.use(
|
|||||||
|
|
||||||
const data = response.data
|
const data = response.data
|
||||||
console.log('接口返回', data)
|
console.log('接口返回', data)
|
||||||
// 判断接口返回的 Message 字段是否为 Success
|
|
||||||
if (!data?.Message.includes('Success')) {
|
// 对于健康检查和数据源状态检查接口,直接返回数据
|
||||||
|
if (
|
||||||
|
response.config.url?.includes('/health') ||
|
||||||
|
response.config.url?.includes('/data-source-status')
|
||||||
|
) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断接口返回的 Message 字段是否为 Success(对于其他接口)
|
||||||
|
if (data && data.Message && !data.Message.includes('Success')) {
|
||||||
// 如果不是 Success,则将请求视为失败
|
// 如果不是 Success,则将请求视为失败
|
||||||
ElMessageBox.alert(JSON.stringify(data), '错误', {
|
const errorDia = DialogPlugin({
|
||||||
confirmButtonText: '确定',
|
header: '错误',
|
||||||
type: 'error',
|
body: `接口${data.config.url}报错!【${JSON.stringify(data) || '未知错误'}】`,
|
||||||
dangerouslyUseHTMLString: false
|
confirmBtn: null,
|
||||||
|
cancelBtn: null,
|
||||||
|
destroyOnClose: true,
|
||||||
|
theme: 'danger',
|
||||||
|
zIndex: 10000,
|
||||||
|
onClose: () => {
|
||||||
|
errorDia.destroy()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
response: {
|
response: {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -62,8 +79,8 @@ instance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 如果是 Success,则返回数据
|
// 如果是 Success,或者没有Message字段,则返回数据
|
||||||
return data
|
return response
|
||||||
},
|
},
|
||||||
(error: any) => {
|
(error: any) => {
|
||||||
// 对响应错误做点什么
|
// 对响应错误做点什么
|
||||||
@@ -81,19 +98,19 @@ instance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.error('报错', error)
|
console.error('报错', error)
|
||||||
|
const errorDia = DialogPlugin({
|
||||||
ElMessageBox.alert(
|
header: '错误',
|
||||||
`接口:【${error.config.url}】报错
|
body: `接口${error.config.url}报错!【${JSON.stringify(error.message) || '未知错误'}】`,
|
||||||
${error.message}
|
confirmBtn: null,
|
||||||
${JSON.stringify(error?.response?.data)}
|
cancelBtn: null,
|
||||||
`,
|
destroyOnClose: true,
|
||||||
'报错',
|
theme: 'danger',
|
||||||
{
|
zIndex: 10000,
|
||||||
confirmButtonText: '确定',
|
onClose: () => {
|
||||||
type: 'error',
|
errorDia.destroy()
|
||||||
dangerouslyUseHTMLString: false
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
437
src/views/excel-upload/index.vue
Normal file
437
src/views/excel-upload/index.vue
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<template>
|
||||||
|
<div class="excel-upload-container">
|
||||||
|
<t-card title="Excel 地址信息补充" class="upload-card">
|
||||||
|
<!-- 服务状态检查 -->
|
||||||
|
<div class="status-section">
|
||||||
|
<t-space>
|
||||||
|
<t-tag variant="outline" :theme="serviceStatus === 'ok' ? 'success' : 'danger'">
|
||||||
|
服务器状态:
|
||||||
|
{{ serviceStatus === 'ok' ? '正常' : serviceStatus === 'error' ? '异常' : '检查中' }}
|
||||||
|
</t-tag>
|
||||||
|
<t-tag :theme="dataSourceStatus.both_exist ? 'success' : 'danger'" variant="outline">
|
||||||
|
数据源: {{ dataSourceStatus.both_exist ? '完整' : '缺失' }}
|
||||||
|
</t-tag>
|
||||||
|
</t-space>
|
||||||
|
</div>
|
||||||
|
<t-divider />
|
||||||
|
<!-- 文件上传区域 -->
|
||||||
|
<div class="upload-section" @click="openFileSelect($event)">
|
||||||
|
<t-upload
|
||||||
|
v-model="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
ref="uploadRef"
|
||||||
|
:cancelUploadButton="null"
|
||||||
|
:max="2"
|
||||||
|
upload-all-files-in-one-request
|
||||||
|
:disabled="uploading"
|
||||||
|
:request-method="customUpload"
|
||||||
|
:showUploadProgress="false"
|
||||||
|
:auto-upload="false"
|
||||||
|
theme="file-flow"
|
||||||
|
multiple
|
||||||
|
placeholder="最多支持2个Excel文件"
|
||||||
|
>
|
||||||
|
</t-upload>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
|
||||||
|
<div v-if="uploading" class="upload-progress">
|
||||||
|
<p class="progress-text">{{ uploadStatus }}</p>
|
||||||
|
<t-progress :percentage="uploadProgress" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t-divider />
|
||||||
|
|
||||||
|
<!-- 说明信息 -->
|
||||||
|
<div class="info-section">
|
||||||
|
<t-alert theme="info" message="使用说明" :close="false">
|
||||||
|
<template #default>
|
||||||
|
<li>1、上传需要处理的Excel文件(包含身份证号列)</li>
|
||||||
|
<li>2、系统将自动根据数据源补充乡镇街道和村社区信息</li>
|
||||||
|
<li>3、处理完成后会自动下载结果文件</li>
|
||||||
|
</template>
|
||||||
|
</t-alert>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||||
|
import {
|
||||||
|
uploadExcel,
|
||||||
|
checkHealth,
|
||||||
|
checkDataSourceStatus,
|
||||||
|
getProcessingProgress
|
||||||
|
} from '@/services/excel-upload'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const fileList = ref([])
|
||||||
|
const uploadRef = ref(null)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const uploadProgress = ref(0)
|
||||||
|
const uploadStatus = ref('')
|
||||||
|
const serviceStatus = ref('checking')
|
||||||
|
const dataSourceStatus = reactive({
|
||||||
|
urban_rural_exists: false,
|
||||||
|
worker_exists: false,
|
||||||
|
both_exist: false
|
||||||
|
})
|
||||||
|
const processingProgress = reactive({
|
||||||
|
status: 'idle',
|
||||||
|
current_step: '',
|
||||||
|
progress: 0,
|
||||||
|
total_files: 0,
|
||||||
|
processed_files: 0,
|
||||||
|
current_file: '',
|
||||||
|
error_message: ''
|
||||||
|
})
|
||||||
|
const progressTimer = ref<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// 文件上传前检查
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
// 检查文件类型
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||||
|
'application/vnd.ms-excel' // .xls
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
MessagePlugin.error('只支持Excel文件格式(.xlsx, .xls)')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小(50MB)
|
||||||
|
/* const maxSize = 50 * 1024 * 1024
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
MessagePlugin.error('文件大小不能超过50MB')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取处理进度
|
||||||
|
const fetchProcessingProgress = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getProcessingProgress()
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
Object.assign(processingProgress, response.data.data)
|
||||||
|
|
||||||
|
// 更新进度显示
|
||||||
|
if (processingProgress.status !== 'idle') {
|
||||||
|
uploadProgress.value = processingProgress.progress
|
||||||
|
// 直接从接口获取状态文本,增加更友好的状态显示
|
||||||
|
const step = processingProgress.current_step || '处理中...'
|
||||||
|
|
||||||
|
// 根据不同的处理状态提供更详细的提示
|
||||||
|
uploadStatus.value = `🚀 ${step}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果处理完成或出错,停止轮询
|
||||||
|
if (processingProgress.status === 'completed' || processingProgress.status === 'error') {
|
||||||
|
if (progressTimer.value) {
|
||||||
|
clearInterval(progressTimer.value)
|
||||||
|
progressTimer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processingProgress.status === 'error') {
|
||||||
|
throw new Error(processingProgress.error_message || '处理失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('进度显示', uploadProgress.value)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取进度失败:', error)
|
||||||
|
if (progressTimer.value) {
|
||||||
|
clearInterval(progressTimer.value)
|
||||||
|
progressTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始进度轮询
|
||||||
|
const startProgressPolling = () => {
|
||||||
|
// 重置进度状态
|
||||||
|
Object.assign(processingProgress, {
|
||||||
|
status: 'idle',
|
||||||
|
current_step: '',
|
||||||
|
progress: 0,
|
||||||
|
total_files: 0,
|
||||||
|
processed_files: 0,
|
||||||
|
current_file: '',
|
||||||
|
error_message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 立即获取一次进度
|
||||||
|
fetchProcessingProgress()
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
progressTimer.value = setInterval(() => {
|
||||||
|
fetchProcessingProgress()
|
||||||
|
}, 2000) // 每1秒查询一次
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止进度轮询
|
||||||
|
const stopProgressPolling = () => {
|
||||||
|
if (progressTimer.value) {
|
||||||
|
clearInterval(progressTimer.value)
|
||||||
|
progressTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义上传方法
|
||||||
|
const customUpload = async (options: any) => {
|
||||||
|
const files = options.map((item: any) => item.raw)
|
||||||
|
console.log('文件', files)
|
||||||
|
|
||||||
|
// 开始进度轮询
|
||||||
|
startProgressPolling()
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploading.value = true
|
||||||
|
// 创建FormData并添加文件
|
||||||
|
const formData = new FormData()
|
||||||
|
// 逐个添加文件到 FormData,添加索引避免覆盖
|
||||||
|
files.forEach((file: File, index: number) => {
|
||||||
|
formData.append(`file_${index}`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调用上传API,传递formData
|
||||||
|
const response = await uploadExcel(formData)
|
||||||
|
|
||||||
|
// 停止轮询,因为后端处理已完成
|
||||||
|
stopProgressPolling()
|
||||||
|
|
||||||
|
uploadStatus.value = '处理完成!'
|
||||||
|
uploadProgress.value = 100
|
||||||
|
// 处理响应,下载文件
|
||||||
|
if (response.data instanceof Blob) {
|
||||||
|
// 生成带时间戳的文件名
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(now.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
const fileName = `已添加地址信息_${year}-${month}-${day} ${hours}:${minutes}:${seconds}.zip`
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.setAttribute('download', fileName)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
link.remove()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
MessagePlugin.success('文件处理完成,已自动下载结果')
|
||||||
|
|
||||||
|
// 显示成功对话框
|
||||||
|
const tipDialog = DialogPlugin({
|
||||||
|
header: '处理完成',
|
||||||
|
theme: 'success',
|
||||||
|
body: 'Excel文件处理完成,已自动下载处理结果文件。',
|
||||||
|
confirmBtn: '确定',
|
||||||
|
cancelBtn: null,
|
||||||
|
onConfirm: () => {
|
||||||
|
fileList.value = []
|
||||||
|
uploadProgress.value = 0
|
||||||
|
uploading.value = false
|
||||||
|
tipDialog.destroy()
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
fileList.value = []
|
||||||
|
uploadProgress.value = 0
|
||||||
|
uploading.value = false
|
||||||
|
tipDialog.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('处理失败:', error)
|
||||||
|
|
||||||
|
// 如果响应是blob但包含错误信息,尝试读取
|
||||||
|
if (error.response && error.response.data instanceof Blob) {
|
||||||
|
try {
|
||||||
|
const errorText = await error.response.data.text()
|
||||||
|
const errorData = JSON.parse(errorText)
|
||||||
|
console.error('服务器错误详情:', errorData)
|
||||||
|
MessagePlugin.error(errorData.error || '处理失败,请重试')
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('解析错误响应失败:', parseError)
|
||||||
|
MessagePlugin.error('处理失败,请重试')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(error.message || '处理失败,请重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
stopProgressPolling()
|
||||||
|
uploading.value = false
|
||||||
|
uploadProgress.value = 0
|
||||||
|
uploadStatus.value = '处理失败'
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查服务状态
|
||||||
|
const checkServiceStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkHealth()
|
||||||
|
console.log('健康检查响应:', response.data)
|
||||||
|
if (response.data.success && response.data.data.status === 'ok') {
|
||||||
|
serviceStatus.value = 'ok'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
serviceStatus.value = 'error'
|
||||||
|
console.error('服务状态检查失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据源状态
|
||||||
|
const checkDataSourceStatusLocal = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkDataSourceStatus()
|
||||||
|
console.log('数据源状态响应:', response.data)
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
Object.assign(dataSourceStatus, response.data.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据源状态检查失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const openFileSelect = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (
|
||||||
|
target &&
|
||||||
|
uploadRef.value &&
|
||||||
|
['t-upload__flow-empty', 't-upload__flow-card-area'].includes(target.className)
|
||||||
|
) {
|
||||||
|
;(uploadRef.value as any).triggerUpload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 组件挂载时检查状态
|
||||||
|
onMounted(() => {
|
||||||
|
checkServiceStatus()
|
||||||
|
checkDataSourceStatusLocal()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopProgressPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.excel-upload-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
:deep(.t-upload__flow-table) {
|
||||||
|
th {
|
||||||
|
&:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
&:nth-child(3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.t-card__title) {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
:deep(.t-alert__content) {
|
||||||
|
// margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
.upload-card {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
.status-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
margin: 24px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
:deep(.t-upload__flow-empty) {
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
:deep(.t-upload__flow) {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 100px !important;
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
:deep(.t-upload__flow-card-area) {
|
||||||
|
border: 1px dashed var(--td-brand-color) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--td-bg-color-container-hover);
|
||||||
|
border-color: var(--td-brand-color-hover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drag-content {
|
||||||
|
text-align: center;
|
||||||
|
// padding: 40px 20px;
|
||||||
|
|
||||||
|
.t-icon {
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tips {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
.progress-text {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
|
ol {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.excel-upload-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,8 +26,8 @@ export default defineConfig({
|
|||||||
globPatterns: ['**/*.{js,css,html,svg,jpg,ico}']
|
globPatterns: ['**/*.{js,css,html,svg,jpg,ico}']
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'vue模板',
|
name: 'Excel处理工具',
|
||||||
short_name: 'vue模板',
|
short_name: '处理工具',
|
||||||
theme_color: '#fff', // 浏览器状态栏主题
|
theme_color: '#fff', // 浏览器状态栏主题
|
||||||
start_url: './',
|
start_url: './',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
@@ -39,7 +39,6 @@ export default defineConfig({
|
|||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'any'
|
purpose: 'any'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
src: 'logo.png',
|
src: 'logo.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
@@ -67,17 +66,13 @@ export default defineConfig({
|
|||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
// 7566
|
target: 'http://10.2.0.32:5000',
|
||||||
// target: 'http://192.168.39.120:7566',
|
|
||||||
target: 'http://127.0.0.1:8066',
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: p => p.replace(/^\/api/, ''),
|
rewrite: p => p.replace(/^\/api/, ''),
|
||||||
bypass: (req, res, options) => {
|
bypass: (req, res, options) => {
|
||||||
// @ts-ignore
|
|
||||||
const proxyURL = options.target + options.rewrite(req.url)
|
const proxyURL = options.target + options.rewrite(req.url)
|
||||||
// console.log('proxyURL', proxyURL)
|
// console.log('proxyURL', proxyURL)
|
||||||
req.headers['x-req-proxyURL'] = proxyURL // 设置未生效
|
req.headers['x-req-proxyURL'] = proxyURL // 设置未生效
|
||||||
// @ts-ignore
|
|
||||||
res.setHeader('x-req-proxyURL', proxyURL) // 设置响应头可以看到
|
res.setHeader('x-req-proxyURL', proxyURL) // 设置响应头可以看到
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user