perf: 模板优化

This commit is contained in:
刘引
2025-12-27 14:27:33 +08:00
parent 99e1c9b445
commit 7e6921c8df
16 changed files with 617 additions and 61 deletions

View File

@@ -1 +1 @@
# Vue 3 + Typescript + Vite + ElementUI-Plus + DayJs + Pinia
# Vue 3 + Typescript + Vite + TDdesign + DayJs + Pinia

2
auto-imports.d.ts vendored
View File

@@ -6,5 +6,5 @@
// biome-ignore lint: disable
export {}
declare global {
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

View File

@@ -82,7 +82,7 @@ define(['./workbox-86c9b217'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.t9v2fs5c6io"
"revision": "0.o5uo4dculfk"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@@ -2,12 +2,12 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/logo.png" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>Vue项目模板</title>
<title>Vue模板</title>
</head>
<body>
<div id="app"></div>

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,5 +1,5 @@
{
"version": "2025-10-15 09:50:32",
"buildTime": "2025-10-15T01:50:32.937Z",
"version": "2025-12-27 10:25:12",
"buildTime": "2025-12-27T02:25:12.719Z",
"environment": "production"
}
}

View File

@@ -36,7 +36,7 @@ onMounted(async () => {
z-index: 1000;
display: flex;
justify-content: center;
padding: 6px 8px;
padding: 16px 8px;
font-size: 12px;
color: #333;
background: rgba(255, 255, 255, 0.85);

View File

@@ -3,6 +3,37 @@
padding: 0;
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 {
list-style: none;
}

Binary file not shown.

View File

@@ -15,8 +15,8 @@ import TDesign from 'tdesign-vue-next'
import 'tdesign-vue-next/es/style/index.css'
import 'normalize.css/normalize.css'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
// 启动版本检测服务 (20秒检查一次)
startVersionCheck(1000 * 20)
// 启动版本检测服务 (两小时检查一次)
startVersionCheck(1000 * 60 * 2)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注入插件
// 挂载到app上

View File

@@ -12,6 +12,11 @@ const routes = [
{
path: '/',
component: () => import('@/views/index.vue')
},
{
path: '/excel-upload',
name: 'ExcelUpload',
component: () => import('@/views/excel-upload/index.vue')
}
]

View 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()
}
})
}

View File

@@ -7,41 +7,41 @@
* @LastEditTime: 2022-01-25 18:04:29
*/
import Axios from "../api/base-service"; // 导入配置好的axios文件
import Axios from '../utils/request/base-service' // 导入配置好的axios文件
// 封装axios请求函数并用export导出
export function getInfo(datas: unknown) {
return Axios({
url: "/api.php?key=free&appid=0&msg=鹅鹅鹅",
method: "GET",
url: '/api.php?key=free&appid=0&msg=鹅鹅鹅',
method: 'GET',
headers: {
"content-type": "application/json",
'content-type': 'application/json'
},
data: datas,
});
data: datas
})
}
export function getInfoA(datas: unknown) {
return Axios({
url: "/api/getbooks",
method: "get",
url: '/api/getbooks',
method: 'get',
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) {
return Axios({
url: "/api/getItem",
method: "post",
url: '/api/getItem',
method: 'post',
headers: {
"Content-Type": "application/json", //设置请求头请求格式为json
'Content-Type': 'application/json' //设置请求头请求格式为json
},
data: datas,
});
data: datas
})
}
export function getItemInfo(datas: unknown) {
return Axios({
url: "/api/getItemInfo" + datas,
method: "get",
});
url: '/api/getItemInfo' + datas,
method: 'get'
})
}

View File

@@ -1,5 +1,5 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { DialogPlugin } from 'tdesign-vue-next'
// 获取浏览器中的url地址和端口号并拼接
/* import { config } from 'public/config.js'
const getBaseUrl = () => {
@@ -13,8 +13,8 @@ const getBaseUrl = () => {
} */
// 创建一个新的axios实例
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 30000,
baseURL: '/api',
timeout: 3000000,
withCredentials: false,
headers: {
'Content-Type': 'application/json'
@@ -46,14 +46,31 @@ instance.interceptors.response.use(
const data = response.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则将请求视为失败
ElMessageBox.alert(JSON.stringify(data), '错误', {
confirmButtonText: '确定',
type: 'error',
dangerouslyUseHTMLString: false
const errorDia = DialogPlugin({
header: '错误',
body: `接口${data.config.url}报错!【${JSON.stringify(data) || '未知错误'}`,
confirmBtn: null,
cancelBtn: null,
destroyOnClose: true,
theme: 'danger',
zIndex: 10000,
onClose: () => {
errorDia.destroy()
}
})
return Promise.reject({
response: {
status: 200,
@@ -62,8 +79,8 @@ instance.interceptors.response.use(
}
})
}
// 如果是 Success则返回数据
return data
// 如果是 Success或者没有Message字段则返回数据
return response
},
(error: any) => {
// 对响应错误做点什么
@@ -81,19 +98,19 @@ instance.interceptors.response.use(
}
console.error('报错', error)
ElMessageBox.alert(
`接口:【${error.config.url}报错
${error.message}
${JSON.stringify(error?.response?.data)}
`,
'报错',
{
confirmButtonText: '确定',
type: 'error',
dangerouslyUseHTMLString: false
const errorDia = DialogPlugin({
header: '错误',
body: `接口${error.config.url}报错!【${JSON.stringify(error.message) || '未知错误'}`,
confirmBtn: null,
cancelBtn: null,
destroyOnClose: true,
theme: 'danger',
zIndex: 10000,
onClose: () => {
errorDia.destroy()
}
)
})
return Promise.reject(error)
}
)

View 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>

View File

@@ -26,8 +26,8 @@ export default defineConfig({
globPatterns: ['**/*.{js,css,html,svg,jpg,ico}']
},
manifest: {
name: 'vue模板',
short_name: 'vue模板',
name: 'Excel处理工具',
short_name: '处理工具',
theme_color: '#fff', // 浏览器状态栏主题
start_url: './',
display: 'standalone',
@@ -39,7 +39,6 @@ export default defineConfig({
type: 'image/png',
purpose: 'any'
},
{
src: 'logo.png',
sizes: '512x512',
@@ -67,17 +66,13 @@ export default defineConfig({
open: true,
proxy: {
'/api': {
// 7566
// target: 'http://192.168.39.120:7566',
target: 'http://127.0.0.1:8066',
target: 'http://10.2.0.32:5000',
changeOrigin: true,
rewrite: p => p.replace(/^\/api/, ''),
bypass: (req, res, options) => {
// @ts-ignore
const proxyURL = options.target + options.rewrite(req.url)
// console.log('proxyURL', proxyURL)
req.headers['x-req-proxyURL'] = proxyURL // 设置未生效
// @ts-ignore
res.setHeader('x-req-proxyURL', proxyURL) // 设置响应头可以看到
}
}