Compare commits
34 Commits
He-Chang-L
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e5753dcd | ||
|
|
7e6921c8df | ||
|
|
99e1c9b445 | ||
|
|
aff235bbf4 | ||
|
|
0f59cc6c87 | ||
|
|
eb7312fb23 | ||
|
|
20428e2ac8 | ||
|
|
fae1999a93 | ||
|
|
64e8813f35 | ||
|
|
4b99518c16 | ||
|
|
4fdf48893c | ||
|
|
55d51efd20 | ||
|
|
fc76632d4d | ||
|
|
2a38aed1f7 | ||
|
|
11e617adc9 | ||
|
|
32051eb13f | ||
|
|
2e61e255a2 | ||
|
|
499796afbe | ||
|
|
15617f45b9 | ||
|
|
ceac848fd2 | ||
|
|
9b35ac6474 | ||
|
|
4368d0a88d | ||
|
|
d5090f66e2 | ||
|
|
24a01c9bb0 | ||
|
|
38e65e9917 | ||
|
|
d6186e73ad | ||
|
|
690331f88c | ||
|
|
5e679bdde8 | ||
|
|
2732efbefb | ||
| 6efc8b908b | |||
| 935e7f61b3 | |||
| 26a6d313e3 | |||
| dfa6146840 | |||
| c76c4329f9 |
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AngularCliAddDependency" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
<inspection_tool class="AngularInaccessibleComponentMemberInAotMode" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
<inspection_tool class="AngularInsecureBindingToEvent" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
<inspection_tool class="AngularInvalidI18nAttribute" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
<inspection_tool class="AngularNgOptimizedImage" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/vue-frame.iml" filepath="$PROJECT_DIR$/.idea/vue-frame.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/vue-frame.iml
generated
Normal file
9
.idea/vue-frame.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
# Vue 3 + Typescript + Vite + ElementUI-Plus + DayJs + Pinia
|
||||
# Vue 3 + Typescript + Vite + TDdesign + DayJs + Pinia
|
||||
|
||||
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@@ -3,7 +3,8 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
}
|
||||
|
||||
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Foot: typeof import('./src/components/Foot.vue')['default']
|
||||
|
||||
1
dev-dist/registerSW.js
Normal file
1
dev-dist/registerSW.js
Normal file
@@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||
92
dev-dist/sw.js
Normal file
92
dev-dist/sw.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return registry[uri] || (
|
||||
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = depUri => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require
|
||||
};
|
||||
registry[uri] = Promise.all(depsNames.map(
|
||||
depName => specialDeps[depName] || require(depName)
|
||||
)).then(deps => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-86c9b217'], (function (workbox) { 'use strict';
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "registerSW.js",
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.o5uo4dculfk"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
|
||||
}));
|
||||
3391
dev-dist/workbox-86c9b217.js
Normal file
3391
dev-dist/workbox-86c9b217.js
Normal file
File diff suppressed because it is too large
Load Diff
11
index.html
11
index.html
@@ -1,10 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
3484
package-lock.json
generated
3484
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,29 +1,32 @@
|
||||
{
|
||||
"name": "vue-frame",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "tsx src/utils/update-version.ts && vite build --mode release",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.4.3",
|
||||
"axios": "^1.12.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qs": "^6.11.2",
|
||||
"vue": "^3.3.10",
|
||||
"vue-router": "^4.2.5"
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"qs": "^6.14.0",
|
||||
"tdesign-vue-next": "^1.17.1",
|
||||
"vite-plugin-pwa": "^1.1.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.17.1",
|
||||
"@vitejs/plugin-vue": "^4.5.1",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^4.9.5",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.6",
|
||||
"vue-tsc": "^0.39.5"
|
||||
"@types/node": "^24.7.2",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.3.0",
|
||||
"vue-tsc": "^3.2.1"
|
||||
}
|
||||
}
|
||||
6072
pnpm-lock.yaml
generated
6072
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
public/config.ts
Normal file
1
public/config.ts
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
5
public/version.json
Normal file
5
public/version.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "2025-12-27 10:25:12",
|
||||
"buildTime": "2025-12-27T02:25:12.719Z",
|
||||
"environment": "production"
|
||||
}
|
||||
48
src/App.vue
48
src/App.vue
@@ -1,18 +1,46 @@
|
||||
<!--
|
||||
* @Description: {{ByRuin}}
|
||||
* @Version: 2.0
|
||||
* @Author: Ruin 🍭
|
||||
* @Date: 2022-01-25 16:22:24
|
||||
* @LastEditors: 刘引 liu.yin.work@foxmail.com
|
||||
* @LastEditTime: 2023-08-01 15:25:13
|
||||
-->
|
||||
|
||||
<!-- 基于rspack打包github&gitee -->
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<footer class="app-version">
|
||||
<span>版本:{{ versionText }}</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const versionText = ref('未知')
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/version.json', { cache: 'no-store' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const v = typeof data?.version === 'string' && data.version.trim() ? data.version.trim() : ''
|
||||
versionText.value = v || '未知'
|
||||
} else {
|
||||
versionText.value = '未知'
|
||||
}
|
||||
} catch (e) {
|
||||
versionText.value = '未知'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import './assets/scss/index.scss';
|
||||
.app-version {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 8px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: saturate(180%) blur(6px);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* @Description: {{ByRuin}}
|
||||
* @Version: 2.0
|
||||
* @Author: Ruin 🍭
|
||||
* @Date: 2022-01-25 17:48:13
|
||||
* @LastEditors: 刘引
|
||||
* @LastEditTime: 2022-01-25 17:52:39
|
||||
*/
|
||||
/* 1.引入文件 */
|
||||
import axios from 'axios' //引入 axios库
|
||||
// @ts-ignore
|
||||
import qs from 'qs' //引入 node中自带的qs模块(数据格式转换)
|
||||
/* 2.全局默认配置 */
|
||||
let baseURL
|
||||
let process: any
|
||||
// 判断开发环境(一般用于本地代理)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 开发环境
|
||||
baseURL = 'http://www.liulongbin.top:3006' // 你设置的本地代理请求(跨域代理),下文会详细介绍怎么进行跨域代理
|
||||
} else {
|
||||
// 编译环境
|
||||
if (process.env.type === 'test') {
|
||||
// 测试环境
|
||||
baseURL = 'http://www.liulongbin.top:3006'
|
||||
} else {
|
||||
// 正式环境
|
||||
baseURL = 'http://www.liulongbin.top:3006'
|
||||
}
|
||||
}
|
||||
// 配置axios的属性
|
||||
axios.defaults.timeout = 6000 // 请求超时时间1分钟
|
||||
axios.defaults.baseURL = baseURL // 你的接口地址
|
||||
axios.defaults.responseType = 'json'
|
||||
axios.defaults.withCredentials = false //是否允许带cookie这些
|
||||
/*你也可以创建一个实例,然后在实例中配置相关属性,此方法和上面的方法一样,写法不同,怎么用随个人
|
||||
*喜好,我比较喜欢用这种方法,如下:
|
||||
*/
|
||||
const Axios = axios.create({
|
||||
baseURL: baseURL, // 后台服务地址
|
||||
timeout: 60000, // 请求超时时间1分钟
|
||||
responseType: 'json',
|
||||
withCredentials: false // 是否允许带cookie这些
|
||||
})
|
||||
/* 3.设置拦截器 */
|
||||
/*如果不是用创建实例的方式配置,那么下面的Axios都要换成axios,也就是文件开头你用import引入axios
|
||||
时定义的变量*/
|
||||
Axios.interceptors.request.use(
|
||||
(config) => {
|
||||
//发送请求前进行拦截
|
||||
// 可在此处配置请求头信息
|
||||
// config.headers["appkey"] = "...";
|
||||
// config.headers["token"] = "...";
|
||||
if (config.method == 'post') {
|
||||
/*数据转换: axios post方式默认是json格式提交数据,如果使用application/x-www-form-urlencoded数据格式提交,要用qs.stringify()进行转换,个人建议不在拦截器中全局配置,因为不够灵活,还有一点是,如果
|
||||
设置了重新请求的配置,那么重新请求时,请求体中的config里面的传参就会被再次进行qs.stringify()转
|
||||
换,会使得参数丢失,造成请求失败。*/
|
||||
config.data = qs.stringify(config.data)
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
//console.log("错误的传参", 'fail');
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
Axios.interceptors.response.use(
|
||||
(res) => {
|
||||
//请求响应后拦截
|
||||
if (res.status == 200) {
|
||||
// 对响应数据做些事
|
||||
//alert("提交成功")
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
return res
|
||||
},
|
||||
(error) => {
|
||||
//alert("网络异常!") 404等问题可以在这里处理
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
export default Axios
|
||||
@@ -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;
|
||||
}
|
||||
@@ -11,3 +42,14 @@ li {
|
||||
margin: 0 auto;
|
||||
}
|
||||
$fzt: 20px;
|
||||
.t-notification__detail {
|
||||
// padding: 0 20px;
|
||||
|
||||
.t-notification__detail-item {
|
||||
padding: 6px 12px;
|
||||
|
||||
&:nth-child(1) {
|
||||
color: #0052d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/main.ts
21
src/main.ts
@@ -10,17 +10,14 @@ import { createApp } from 'vue'
|
||||
import router from './router/index' //引入vue-router
|
||||
import App from './App.vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Foot from '@/components/Foot.vue'
|
||||
import Head from '@/components/Head.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import { startVersionCheck } from './utils/versionCheck'
|
||||
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'
|
||||
// 启动版本检测服务 (两小时检查一次)
|
||||
startVersionCheck(1000 * 60 * 2)
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate) // 注入插件
|
||||
// 挂载到app上
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(createPinia())
|
||||
.use(ElementPlus)
|
||||
.component('Foot', Foot)
|
||||
.component('Head', Head)
|
||||
.mount('#app')
|
||||
createApp(App).use(router).use(pinia).use(TDesign).mount('#app')
|
||||
|
||||
@@ -6,24 +6,17 @@
|
||||
* @LastEditors: 刘引
|
||||
* @LastEditTime: 2022-03-11 14:26:24
|
||||
*/
|
||||
import path from 'node:path/win32'
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/views/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
component: import('@/views/index.vue'),
|
||||
children: [{ path: '/', component: import('@/views/index.vue') }]
|
||||
}
|
||||
]
|
||||
// redirect: "/index",
|
||||
component: () => import('@/views/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: () => import('@/views/user/index.vue')
|
||||
path: '/excel-upload',
|
||||
name: 'ExcelUpload',
|
||||
component: () => import('@/views/excel-upload/index.vue')
|
||||
}
|
||||
]
|
||||
|
||||
@@ -32,4 +25,3 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
export default router
|
||||
// 我感觉
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
/*
|
||||
* @Author: 刘引 liu.yin.work@foxmail.com
|
||||
* @Date: 2023-08-01 13:46:06
|
||||
* @LastEditors: 刘引 liu.yin.work@foxmail.com
|
||||
* @LastEditTime: 2023-08-01 15:56:19
|
||||
* @FilePath: \kthec-emss-web\src\pinia\index.js
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
*/
|
||||
import { defineStore } from 'pinia' // 定义容器
|
||||
|
||||
export let useMain = defineStore('useStore', {
|
||||
/**
|
||||
* 存储全局状态
|
||||
* 1.必须是箭头函数: 为了在服务器端渲染的时候避免交叉请求导致数据状态污染
|
||||
* 和 TS 类型推导
|
||||
*/
|
||||
state: () => {
|
||||
return {
|
||||
count: 0,
|
||||
list: [1, 2, 3, 4]
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 用来封装计算属性 有缓存功能 类似于computed
|
||||
*/
|
||||
getters: {},
|
||||
/**
|
||||
* 编辑业务逻辑 类似于methods
|
||||
*/
|
||||
actions: {
|
||||
changeData(val: number) {
|
||||
this.count = val + 10
|
||||
}
|
||||
}
|
||||
})
|
||||
import { createPinia } from 'pinia'
|
||||
const store = createPinia()
|
||||
export { store }
|
||||
export * from './modules/user'
|
||||
// export let useStore = defineStore('useStore', {
|
||||
// /**
|
||||
// * 存储全局状态
|
||||
// * 1.必须是箭头函数: 为了在服务器端渲染的时候避免交叉请求导致数据状态污染
|
||||
// * 和 TS 类型推导
|
||||
// */
|
||||
// state: () => {
|
||||
// return {
|
||||
// count: 0,
|
||||
// list: [1, 2, 3, 4]
|
||||
// }
|
||||
// },
|
||||
// /**
|
||||
// * 用来封装计算属性 有缓存功能 类似于computed
|
||||
// */
|
||||
// getters: {},
|
||||
// /**
|
||||
// * 编辑业务逻辑 类似于methods
|
||||
// */
|
||||
// actions: {
|
||||
// changeData(val: number) {
|
||||
// this.count = val + 10
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
16
src/store/modules/user.ts
Normal file
16
src/store/modules/user.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'useUserStore',
|
||||
() => {
|
||||
const userInfo = reactive({
|
||||
name: 'yin.liu',
|
||||
age: 18
|
||||
})
|
||||
return { userInfo }
|
||||
},
|
||||
{
|
||||
persist: true
|
||||
}
|
||||
)
|
||||
118
src/utils/request/base-service.ts
Normal file
118
src/utils/request/base-service.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { DialogPlugin } from 'tdesign-vue-next'
|
||||
// 获取浏览器中的url地址和端口号并拼接
|
||||
/* import { config } from 'public/config.js'
|
||||
const getBaseUrl = () => {
|
||||
let baseUrlData = ''
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
baseUrlData = config.baseUrl
|
||||
} else if (process.env.NODE_ENV === 'production') {
|
||||
baseUrlData = 'http://10.0.0.88:3000/api'
|
||||
}
|
||||
return baseUrlData
|
||||
} */
|
||||
// 创建一个新的axios实例
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 3000000,
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config: any) => {
|
||||
// 在发送请求之前做些什么
|
||||
// 例如,添加token到请求头
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
return config
|
||||
},
|
||||
(error: any) => {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 对响应数据做点什么
|
||||
console.log(process.env.NODE_ENV, '变量')
|
||||
|
||||
const data = response.data
|
||||
console.log('接口返回', data)
|
||||
|
||||
// 对于健康检查和数据源状态检查接口,直接返回数据
|
||||
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,则将请求视为失败
|
||||
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,
|
||||
data: data,
|
||||
message: JSON.stringify(data) || '请求失败'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 如果是 Success,或者没有Message字段,则返回数据
|
||||
return response
|
||||
},
|
||||
(error: any) => {
|
||||
// 对响应错误做点什么
|
||||
if (error.response) {
|
||||
// 请求已发出,但服务器响应的状态码不在 2xx 范围内
|
||||
|
||||
console.error('请求错误:', error.response.status, error.response.data)
|
||||
} else if (error.request) {
|
||||
// 请求已经发出,但没有收到响应
|
||||
|
||||
console.error('网络错误:', error.request)
|
||||
} else {
|
||||
// 在设置请求时发生了一些事情,触发了一个错误
|
||||
console.error('错误:', error.message)
|
||||
}
|
||||
|
||||
console.error('报错', error)
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
export default instance
|
||||
57
src/utils/update-version.ts
Normal file
57
src/utils/update-version.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 更新版本信息脚本
|
||||
* 在构建过程中自动更新 version.json 文件
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path, { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// 获取当前文件的目录路径
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// 版本信息
|
||||
const now = new Date()
|
||||
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(
|
||||
2,
|
||||
'0'
|
||||
)}-${String(now.getDate()).padStart(2, '0')}`
|
||||
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(
|
||||
2,
|
||||
'0'
|
||||
)}:${String(now.getSeconds()).padStart(2, '0')}`
|
||||
|
||||
interface VersionInfo {
|
||||
version: string
|
||||
buildTime: string
|
||||
environment: string
|
||||
}
|
||||
|
||||
// 不再需要检查构建序号,直接使用时间戳作为版本号
|
||||
try {
|
||||
if (fs.existsSync(path.resolve(__dirname, '../../public/version.json'))) {
|
||||
const currentVersionInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../public/version.json'), 'utf8'))
|
||||
console.log(`当前版本: ${currentVersionInfo.version}, 更新为时间格式版本`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取当前版本信息失败', error)
|
||||
}
|
||||
|
||||
// 设置版本格式为 YYYY-MM-DD HH:MM:SS
|
||||
const versionFormat = `${dateStr} ${timeStr}`
|
||||
|
||||
const versionInfo: VersionInfo = {
|
||||
version: versionFormat,
|
||||
buildTime: now.toISOString(),
|
||||
environment: process.env.NODE_ENV || 'production'
|
||||
}
|
||||
|
||||
// 版本文件路径
|
||||
// 确保路径正确解析到 pc-front/public/version.json
|
||||
const versionFilePath = path.resolve(__dirname, '../../public/version.json')
|
||||
|
||||
// 写入版本信息
|
||||
fs.writeFileSync(versionFilePath, JSON.stringify(versionInfo, null, 2), 'utf8')
|
||||
|
||||
console.log(`版本信息已更新: ${JSON.stringify(versionInfo)}`)
|
||||
console.log(`当前版本时间戳: ${versionFormat}`)
|
||||
154
src/utils/versionCheck.ts
Normal file
154
src/utils/versionCheck.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 版本检测服务
|
||||
* 定期检查应用版本并提示用户更新
|
||||
*/
|
||||
|
||||
import { NotifyPlugin } from 'tdesign-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
interface VersionInfo {
|
||||
version: string
|
||||
buildTime: string
|
||||
environment: string
|
||||
}
|
||||
|
||||
let currentVersion: string | null = null
|
||||
let checkInterval: number | null = null
|
||||
let isUpdateNotificationShown = false
|
||||
|
||||
/**
|
||||
* 获取当前版本信息
|
||||
*/
|
||||
export const fetchVersionInfo = async (): Promise<VersionInfo> => {
|
||||
try {
|
||||
// 确保使用正确的路径访问version.json
|
||||
const response = await fetch(`/version.json?t=${new Date().getTime()}`)
|
||||
if (!response.ok) {
|
||||
throw new Error('无法获取版本信息')
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('获取版本信息失败:', error)
|
||||
return {
|
||||
version: '0.0.0',
|
||||
buildTime: new Date().toISOString(),
|
||||
environment: 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查版本更新
|
||||
*/
|
||||
export const checkVersion = async (): Promise<boolean> => {
|
||||
try {
|
||||
const versionInfo = await fetchVersionInfo()
|
||||
|
||||
// 首次加载,保存当前版本
|
||||
if (!currentVersion) {
|
||||
currentVersion = versionInfo.version
|
||||
return false
|
||||
}
|
||||
|
||||
// 版本不一致,需要更新
|
||||
if (currentVersion !== versionInfo.version) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('检查版本更新失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示更新提示
|
||||
*/
|
||||
export const showUpdateNotification = () => {
|
||||
// 如果已经显示了更新提示,则不再重复显示
|
||||
if (isUpdateNotificationShown) {
|
||||
return
|
||||
}
|
||||
|
||||
// 标记已显示更新提示
|
||||
isUpdateNotificationShown = true
|
||||
|
||||
const notify = NotifyPlugin.warning({
|
||||
title: '有新的版本需要更新',
|
||||
content: '点击后将刷新页面获取最新版本!',
|
||||
footer: h('div', { class: 't-notification__detail' }, [
|
||||
h(
|
||||
't-button',
|
||||
{
|
||||
class: 't-notification__detail-item',
|
||||
theme: 'default',
|
||||
variant: 'text',
|
||||
onClick: () => window.location.reload()
|
||||
},
|
||||
'立即刷新'
|
||||
),
|
||||
h(
|
||||
't-button',
|
||||
{
|
||||
class: 't-notification__detail-item',
|
||||
theme: 'primary',
|
||||
variant: 'text',
|
||||
onClick: () => {
|
||||
isUpdateNotificationShown = false
|
||||
NotifyPlugin.close(notify)
|
||||
}
|
||||
},
|
||||
'稍后提醒我'
|
||||
)
|
||||
]),
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动版本检测服务
|
||||
* @param intervalTime 检测间隔时间(毫秒),默认5分钟
|
||||
*/
|
||||
export const startVersionCheck = (intervalTime = 1000) => {
|
||||
// 重置通知标志
|
||||
isUpdateNotificationShown = false
|
||||
|
||||
// 初始化获取当前版本
|
||||
fetchVersionInfo().then(info => {
|
||||
currentVersion = info.version
|
||||
console.log('当前应用版本:', currentVersion)
|
||||
})
|
||||
|
||||
// 清除可能存在的旧定时器
|
||||
if (checkInterval) {
|
||||
window.clearInterval(checkInterval)
|
||||
}
|
||||
|
||||
// 设置定期检查
|
||||
checkInterval = window.setInterval(async () => {
|
||||
const needUpdate = await checkVersion()
|
||||
if (needUpdate) {
|
||||
showUpdateNotification()
|
||||
}
|
||||
}, intervalTime)
|
||||
|
||||
return () => {
|
||||
if (checkInterval) {
|
||||
window.clearInterval(checkInterval)
|
||||
checkInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止版本检测服务
|
||||
*/
|
||||
export const stopVersionCheck = () => {
|
||||
if (checkInterval) {
|
||||
window.clearInterval(checkInterval)
|
||||
checkInterval = null
|
||||
}
|
||||
// 重置通知标志
|
||||
isUpdateNotificationShown = false
|
||||
}
|
||||
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>
|
||||
@@ -1,21 +1,11 @@
|
||||
<!--
|
||||
* @Description: {{ByRuin}}
|
||||
* @Version: 2.0
|
||||
* @Author: Ruin 🍭
|
||||
* @Date: 2022-03-10 10:11:16
|
||||
* @LastEditors: 刘引
|
||||
* @LastEditTime: 2022-07-26 14:42:11
|
||||
-->
|
||||
<template>
|
||||
<div class="root">我是home组件的子组件news</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
<!--
|
||||
* @Description: {{ByRuin}}
|
||||
* @Version: 2.0
|
||||
* @Author: Ruin 🍭
|
||||
* @Date: 2022-03-10 10:11:06
|
||||
* @LastEditors: 刘引
|
||||
* @LastEditTime: 2022-07-26 14:42:06
|
||||
-->
|
||||
<template>
|
||||
<div class="root-home">
|
||||
<p>我是home组件</p>
|
||||
<el-button>按钮</el-button>
|
||||
|
||||
<news></news>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onUpdated, ref, reactive } from 'vue';
|
||||
import { computed, watch, onUpdated, ref, reactive } from 'vue'
|
||||
import news from './components/news.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<Head></Head>
|
||||
<div>根组件{{ store.count }}</div>
|
||||
<p>{{ userStore.userInfo.age }}</p>
|
||||
<t-button @click="userStore.userInfo.age++">测试</t-button>
|
||||
<Foot></Foot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, watch } from 'vue'
|
||||
import { useMain } from '@/store'
|
||||
const store = useMain()
|
||||
import { useUserStore } from '@/store'
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<template>
|
||||
<div class="root-home">
|
||||
<news></news>
|
||||
<el-button @click="changeData()">更改值了</el-button>
|
||||
<h1>写一点demo玩一玩</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["element-plus/global"]
|
||||
"types": ["vue", "element-plus/global", "node"]
|
||||
},
|
||||
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
|
||||
@@ -1,39 +1,81 @@
|
||||
/*
|
||||
* @Description: {{ByRuin}}
|
||||
* @Version: 2.0
|
||||
* @Author: Ruin 🍭
|
||||
* @Date: 2022-01-25 16:22:24
|
||||
* @LastEditors: 刘引
|
||||
* @LastEditTime: 2022-03-10 16:34:39
|
||||
*/
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
// ui组件自动导入
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||
import { resolve } from "path";
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, resolve } from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
// ESM 中获取 __dirname 的替代方案
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
injectRegister: 'auto',
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: true // 是否本地localhost调试pwa
|
||||
},
|
||||
|
||||
workbox: {
|
||||
// 设置需要缓存的文件类型
|
||||
globPatterns: ['**/*.{js,css,html,svg,jpg,ico}']
|
||||
},
|
||||
manifest: {
|
||||
name: 'Excel处理工具',
|
||||
short_name: '处理工具',
|
||||
theme_color: '#fff', // 浏览器状态栏主题
|
||||
start_url: './',
|
||||
display: 'standalone',
|
||||
background_color: '#fff',
|
||||
icons: [
|
||||
{
|
||||
src: 'logo.png',
|
||||
sizes: '144x144',
|
||||
type: 'image/png',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: 'logo.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
resolvers: [ElementPlusResolver()]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"), // 路径别名
|
||||
},
|
||||
extensions: [".js", ".json", ".ts"], // 使用路径别名时想要省略的后缀名,可以自己 增减
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8888,
|
||||
// 是否开启 https
|
||||
https: false,
|
||||
},
|
||||
});
|
||||
host: '0.0.0.0',
|
||||
port: 6677,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://10.2.0.32:5000',
|
||||
changeOrigin: true,
|
||||
rewrite: p => p.replace(/^\/api/, ''),
|
||||
bypass: (req, res, options) => {
|
||||
const proxyURL = options.target + options.rewrite(req.url)
|
||||
// console.log('proxyURL', proxyURL)
|
||||
req.headers['x-req-proxyURL'] = proxyURL // 设置未生效
|
||||
res.setHeader('x-req-proxyURL', proxyURL) // 设置响应头可以看到
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user