初始化

This commit is contained in:
2025-11-20 16:21:56 +08:00
commit e38e9cf6be
42 changed files with 13041 additions and 0 deletions

1049
src/App.vue Normal file

File diff suppressed because it is too large Load Diff

86
src/assets/base.css Normal file
View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

37
src/assets/main.css Normal file
View File

@@ -0,0 +1,37 @@
@import './base.css';
#app {
/*max-width: 1440px;*/
margin: 0 auto;
width: 100%;
height: 100%;
padding: 0px;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
/*grid-template-columns: 1fr 1fr;*/
padding: 0 ;
}
}

42
src/axios.js Normal file
View File

@@ -0,0 +1,42 @@
import axios from 'axios';
import config from '../public/config.js';
import { ElMessage } from 'element-plus'
const instance = axios.create({
baseURL: config.Api, // 设置基础URL
timeout: 150000,
});
// 添加请求拦截器
instance.interceptors.request.use(config => {
// 设置默认的 Content-Type
if (config.headers['Content-Type'] == undefined) {
config.headers['Content-Type'] = 'application/json';
}
// 如果发送的数据是对象则使用JSON库进行序列化
if (config.data && typeof config.data === 'object') {
config.data = JSON.stringify(config.data);
}
return config;
}, error => {
// 请求错误处理
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(response => {
// 对响应数据处理
return response;
}, error => {
// 对响应错误处理
// 可以在这里添加例如token过期后的处理逻辑
console.log(error)
return Promise.reject(error);
});
export default instance;

View File

@@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

26
src/components/i18n.js Normal file
View File

@@ -0,0 +1,26 @@
import { createI18n } from 'vue-i18n';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
const messages = {
en: {
message: {
hello: 'hello world'
},
el: en // Element Plus的英文翻译
},
zh: {
message: {
hello: '你好,世界'
},
el: zhCn // Element Plus的中文翻译
}
};
const i18n = createI18n({
locale: 'zh', // 设置默认语言
fallbackLocale: 'en', // 设置备用语言
messages, // 设置翻译信息
});
export default i18n;

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

29
src/main.js Normal file
View File

@@ -0,0 +1,29 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import config from '../public/config.js';
import axios from './axios'
import i18n from './components/i18n.js' // 导入i18n
import './assets/main.css'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// axios挂载到全局属性
app.config.globalProperties.$http = axios
// 提供依赖注入
app.provide('config', config)
app.provide('$http', axios)
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.use(i18n)
app.mount('#app')

110
src/pages/404/index.vue Normal file
View File

@@ -0,0 +1,110 @@
<template>
<div class="interactive-container">
<div class="error-card">
<div class="liquid"></div>
<div class="content">
<h1>404</h1>
<p>错误(404)当前页面未找到</p>
<p>SYSTEM ERROR: PAGE NOT FOUND</p>
<el-button type="success" plain round
class="magnetic-btn" :icon="HomeFilled"
@mouseenter="playHoverSound"
@click="goHome">
返回主页
<span class="hover-effect"></span>
</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { HomeFilled } from '@element-plus/icons-vue'
const router = useRouter();
const goHome = () => {
router.push('home');
};
</script>
<style scoped>
.interactive-container {
height: 80%;
width: 90%;
display: flex;
align-items: center;
justify-content: center;
}
.error-card {
position: relative;
width: 400px;
height: 500px;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 45px rgba(0,0,0,0.2);
}
.liquid {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
animation: rotate 10s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.content {
position: absolute;
inset: 2px;
border-radius: 18px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
h1 {
font-size: 8rem;
margin: 0;
}
p {
margin: 1rem 0 3rem;
}
.magnetic-btn {
position: relative;
padding: 1rem 2rem;
border: 2px solid rgba(255,255,255,0.5);
overflow: hidden;
transition: all 0.3s ease;
}
.magnetic-btn:hover {
}
.hover-effect {
position: absolute;
background: radial-gradient(circle at center, rgba(255,255,255,0.4) 0%, transparent 70%);
transform: scale(0);
transition: transform 0.3s ease;
width: 100px;
height: 100px;
pointer-events: none;
}
.magnetic-btn:hover .hover-effect {
transform: scale(2);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,498 @@
<template>
<div class="container">
<div class="header">
<div>字典名称</div>
<el-select v-model="currentVarName"
placeholder="请选择字典项"
@change="handleDictChange">
<el-option v-for="item in dictList"
:key="item.varName"
:label="item.varName"
:value="item.varName" />
</el-select>
</div>
<div>字典修改</div>
<!-- 区域字典特殊表格 -->
<el-table v-if="isRegionDict"
:data="regionData"
stripe
style="width: 777px">
<el-table-column prop="key" label="大区名称">
<template #default="{ row }">
<el-input v-model="row.key"
placeholder="请输入大区名称" />
</template>
</el-table-column>
<el-table-column prop="value" label="包含省份(点击修改省份/地区)" width="521">
<template #default="{ row }">
<el-button type="primary" @click="showProvinceDialog(row)"
link style="width: 100%; padding: 0; height: auto; min-height: 32px; white-space: normal; word-break: break-all; line-height: 1.5; display: inline-flex; align-items: center;">
<span style="text-align: left;">
{{ row.value.join('') || '点击选择省份' }}
</span>
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="63">
<template #default="{ $index }">
<el-button type="danger"
:icon="Delete"
circle
@click="deleteRegion($index)" />
</template>
</el-table-column>
</el-table>
<!-- 普通字典表格 -->
<el-table v-else
:data="currentValues"
stripe
style="width: 777px">
<el-table-column prop="value" label="字典值">
<template #default="{ $index }">
<el-input v-model="currentValues[$index]" placeholder="请输入" />
</template>
</el-table-column>
<el-table-column label="操作" width="63">
<template #default="{ $index }">
<el-button type="danger"
:icon="Delete"
circle
@click="handleDelete($index)" />
</template>
</el-table-column>
</el-table>
<!-- 操作按钮区域 -->
<div class="footer">
<template v-if="isRegionDict">
<el-button type="primary"
:icon="Plus"
plain
round
@click="addNewRegion">
添加大区
</el-button>
</template>
<template v-else>
<el-button type="primary"
:icon="Plus"
plain
round
@click="handleAdd">
添加值
</el-button>
</template>
<el-button type="success"
:icon="FolderChecked"
@click="handleSave">
保存
</el-button>
</div>
<!-- 省份选择对话框 -->
<el-dialog v-model="provinceDialogVisible"
title="选择省份/地区"
width="50%">
<el-checkbox-group v-model="selectedProvinces">
<el-row :gutter="20">
<el-col v-for="province in allProvinces"
:key="province"
:span="8"
style="margin-bottom: 15px;">
<el-checkbox :label="province"
:value="province" />
</el-col>
</el-row>
</el-checkbox-group>
<template #footer>
<el-button @click="provinceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmProvinceSelection">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, inject, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Delete, Edit, UploadFilled, RefreshLeft, Upload, EditPen, FolderChecked } from '@element-plus/icons-vue';
const $http = inject('$http')
const config = inject('config');
// 初始字典数据
const dictList = reactive([]);
// 新增响应式数据
const provinceDialogVisible = ref(false);
const selectedProvinces = ref([]);
const currentEditingRegion = ref(null);
const allProvinces = computed(() => {
const provinceDict = dictList.find(d => d.varName == '省份地区');
return provinceDict?.varValue || [];
});
// 获取字典
const getDic = async () => {
try {
const response = await $http.post('ConfigPY/GetConfigString');
const serverData = response.data.response || [];
//console.log(response);
// 清空原有数据
dictList.length = 0;
// 转换数据格式
serverData.forEach(item => {
dictList.push({
varName: item.varName,
varValue: tryParseJson(item.varValue) || []
});
});
//console.log(dictList);
// 默认选中第一个(如果存在)
if (dictList.length > 0) {
currentVarName.value = dictList[0].varName;
}
} catch (error) {
console.error('获取字典失败:', error);
ElMessage.error('字典数据加载失败');
}
};
// JSON安全解析方法
const tryParseJson = (str) => {
try {
return JSON.parse(str);
} catch {
return null;
}
};
const currentVarName = ref('');
const currentDict = computed(() =>
dictList.find(item => item.varName === currentVarName.value)
);
const currentValues = computed({
get: () => currentDict.value?.varValue || [],
set: (val) => {
if (currentDict.value) {
currentDict.value.varValue = val;
}
}
});
// 处理字典项变化
const handleDictChange = (val) => {
if (!val) return;
};
// 判断是否是区域字典
const isRegionDict = computed(() => currentVarName.value === '区域');
// 修改区域数据计算属性移除setter
const regionData = computed(() => {
if (!isRegionDict.value) return [];
const regionDict = dictList.find(d => d.varName === '区域');
return Object.entries(regionDict?.varValue || {}).map(([key, value]) => ({
key,
value: Array.isArray(value) ? value : [value]
}));
});
// 显示省份选择对话框
const showProvinceDialog = (row) => {
currentEditingRegion.value = row;
selectedProvinces.value = [...row.value];
provinceDialogVisible.value = true;
};
// 修改省份选择确认方法
const confirmProvinceSelection = async () => {
try {
if (!currentEditingRegion.value) return;
// 获取当前编辑的大区信息
const currentKey = currentEditingRegion.value.key;
const selectedProvincesList = selectedProvinces.value;
// 获取区域字典数据
const regionDict = dictList.find(d => d.varName === '区域');
if (!regionDict) return;
// 收集所有其他大区的省份映射
const provinceMap = new Map();
Object.entries(regionDict.varValue).forEach(([region, provinces]) => {
if (region !== currentKey) {
provinces.forEach(province => {
provinceMap.set(province, region);
});
}
});
// 检查重复省份
const duplicates = [];
selectedProvincesList.forEach(province => {
if (provinceMap.has(province)) {
duplicates.push({
province,
region: provinceMap.get(province)
});
}
});
// 如果有重复则提示
if (duplicates.length > 0) {
const confirmMessage = `
以下省份已在其他大区存在:<br>
${duplicates.map(d => `${d.province}${d.region}`).join('<br>')}<br>
是否确认继续保存?
`;
await ElMessageBox.confirm(confirmMessage, '重复省份警告', {
confirmButtonText: '强制保存',
cancelButtonText: '返回修改',
type: 'warning',
dangerouslyUseHTMLString: true
});
}
// 执行保存操作
regionDict.varValue = {
...regionDict.varValue,
[currentKey]: selectedProvincesList
};
provinceDialogVisible.value = false;
} catch (error) {
if (error !== 'cancel') {
console.error(error);
}
return; // 用户取消则中断流程
}
};
/* const confirmProvinceSelection = () => {
if (currentEditingRegion.value) {
const regionDict = dictList.find(d => d.varName === '区域');
if (regionDict) {
regionDict.varValue = {
...regionDict.varValue,
[currentEditingRegion.value.key]: selectedProvinces.value
};
}
}
provinceDialogVisible.value = false;
};*/
// 修改添加大区方法
const addNewRegion = () => {
const regionDict = dictList.find(d => d.varName === '区域');
if (!regionDict) return;
// 生成唯一键名
let newKey = "新增大区";
let counter = 1;
while (regionDict.varValue[newKey]) {
newKey = `新增大区${counter++}`;
}
// 直接修改原始数据
regionDict.varValue = {
...regionDict.varValue,
[newKey]: []
};
};
// 修改删除大区方法
const deleteRegion = (index) => {
ElMessageBox.confirm('确定要删除该大区吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const regionDict = dictList.find(d => d.varName === '区域');
if (!regionDict) return;
const keyToDelete = regionData.value[index].key;
const newValue = { ...regionDict.varValue };
delete newValue[keyToDelete];
regionDict.varValue = newValue;
});
};
// 添加行
const handleAdd = () => {
if (!currentDict.value) return;
currentDict.value.varValue.push('');
};
// 删除行
const handleDelete = (index) => {
ElMessageBox.confirm('确定要删除该行吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
currentDict.value.varValue.splice(index, 1);
});
};
// 保存字典
const saveDic = async (name, value) => {
try {
let filteredValue;
// 根据数据类型处理
if (Array.isArray(value)) {
// 处理普通数组格式
filteredValue = value
.map(item => String(item).trim())
.filter(item => item !== "");
if (filteredValue.length === 0) {
throw new Error("不能保存空字典项");
}
} else if (typeof value === 'object' && value !== null) {
// 处理区域字典的对象格式
filteredValue = Object.entries(value).reduce((acc, [key, values]) => {
const filtered = values
.map(item => String(item).trim())
.filter(item => item !== "");
// 保留key即使值为空数组如"其他"
if (key === '其他' || filtered.length > 0) {
acc[key] = filtered;
}
return acc;
}, {});
// 检查是否为空对象
if (Object.keys(filteredValue).length === 0) {
throw new Error("至少需要保留一个有效区域");
}
} else {
throw new Error("无效的字典格式");
}
const valueJs = JSON.stringify(filteredValue);
const rs = await $http.post('ConfigPY/SaveOrAddConfigString', {
VarName: name,
VarValue: valueJs
});
return filteredValue;
} catch (error) {
console.error(error);
throw error;
}
};
// 保存处理
const handleSave = async () => {
if (!currentDict.value) {
ElMessage.warning('请先选择要修改的字典项');
return;
}
try {
await ElMessageBox.confirm(
'此操作将永久修改字典数据,是否继续?',
'警告',
{
confirmButtonText: '确认保存',
cancelButtonText: '取消',
type: 'warning',
center: true,
dangerouslyUseHTMLString: true,
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
saveDic(currentDict.value.varName, currentDict.value.varValue)
.then(() => {
done();
ElMessage.success('保存成功');
})
.catch(() => {
instance.confirmButtonLoading = false;
ElMessage.error('保存失败');
});
} else {
done();
}
}
}
);
// 处理区域字典的特殊结构
let saveData = currentDict.value.varValue;
if (isRegionDict.value) {
// 转换回原始对象格式
saveData = regionData.value.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {});
}
// 保存并获取处理后的数据
const filteredData = await saveDic(
currentDict.value.varName,
saveData
);
// 更新前端数据
currentDict.value.varValue = isRegionDict.value
? { ...filteredData } // 对象格式
: [...filteredData]; // 数组格式
} catch (error) {
if (error === 'cancel') {
//console.log('用户取消保存');
} else if (error.message === '不能保存空字典项') {
ElMessage.warning('字典项至少需要包含一个有效值');
} else {
ElMessage.error('保存失败: ' + error.message);
}
}
};
onMounted(() => {
getDic()
// 初始化选择第一个
if (dictList.length > 0) {
currentVarName.value = dictList[0].varName;
}
});
</script>
<style scoped>
.container {
display: block;
padding: 20px;
width: 100%;
}
.header {
width: 333px;
margin-bottom: 20px;
}
.footer {
width: 777px;
display: flex;
justify-content: space-between; /* 推荐方案 */
gap: 10px;
margin-top: 20px;
}
.el-message-box {
max-height: 70vh;
overflow-y: auto;
}
</style>

287
src/pages/home/index.vue Normal file
View File

@@ -0,0 +1,287 @@
<!--<template>
Home
</template>
<script setup>
import { ref, inject, onMounted, watch, computed } from 'vue';
import { ElMessage, ElIcon, ElButton } from 'element-plus'
import { House, OfficeBuilding, Close, View, Checked, Connection } from '@element-plus/icons-vue'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
onMounted(() => {
//router.push('/radarlog')
localStorage.setItem('url', '/home')
checkLoginStatus()
})
</script>
<style scoped>
</style>临时屏蔽下方为展示用
-->
<template>
<div class="home-container">
<div class="header">
<h1>宝来威云服务平台仪表盘</h1>
</div>
<div class="stats-grid">
<div class="card">
<div class="card-header">
<div class="card-title">
<el-icon><house /></el-icon>
<span>房间</span>
</div>
</div>
<el-statistic :value="39066" value-style="font-size: 36px; font-weight: bold; color: #409eff;">
<template #title>
<div style="font-size: 14px;">云服务房间数量</div>
</template>
</el-statistic>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">
<el-icon><office-building /></el-icon>
<span>酒店</span>
</div>
</div>
<el-statistic :value="1096" value-style="font-size: 36px; font-weight: bold; color: #67c23a;">
<template #title>
<div style="font-size: 14px;">联网酒店总数</div>
</template>
</el-statistic>
</div>
</div>
<h2 class="section-title">系统健康状态</h2>
<div class="dashboard">
<div class="card">
<div class="card-header">
<div class="card-title">
<el-icon><connection /></el-icon>
<span>系统健康指数</span>
</div>
</div>
<div class="health-container">
<el-progress type="dashboard"
:percentage="97"
:width="160"
color="#67c23a"
:stroke-width="12" />
<div class="status-text status-optimal">
状态
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">
<el-icon><View /></el-icon>
<span>设备在线率</span>
</div>
</div>
<div class="health-container">
<el-progress type="dashboard"
:percentage="98.91"
:width="160"
color="#409eff"
:stroke-width="12" />
<div class="status-text status-optimal">
设备在线率98.91%
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title">
<el-icon><Checked /></el-icon>
<span>当前设备故障数</span>
</div>
</div>
<div class="health-container">
<el-progress type="dashboard"
:percentage="0.012"
:width="160"
color="#e6a23c"
:stroke-width="12" />
<div class="status-text status-optimal">
设备故障19
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, watch, computed } from 'vue';
import { ElMessage, ElIcon, ElButton } from 'element-plus'
import { House, OfficeBuilding, Close, View, Checked, Connection } from '@element-plus/icons-vue'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
onMounted(() => {
//router.push('/radarlog')
localStorage.setItem('url', '/home')
checkLoginStatus()
})
</script>
<style scoped>
:root {
--primary: #409eff;
--success: #67c23a;
--warning: #e6a23c;
--danger: #f56c6c;
--info: #909399;
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #303133;
--border: #dcdfe6;
}
body {
background-color: var(--bg);
color: var(--text);
}
.home-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
margin-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.header h1 {
color: var(--primary);
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 24px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.stats-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.health-container {
display: flex;
flex-direction: column;
align-items: center;
}
.status-text {
margin-top: 15px;
font-size: 16px;
font-weight: 500;
}
.status-optimal {
color: var(--success);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.section-title {
font-size: 20px;
margin: 25px 0 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
color: var(--text);
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
.stats-container {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
}
</style>

401
src/pages/log/index.vue Normal file
View File

@@ -0,0 +1,401 @@
<template>
<div class="container">
<!-- 顶部标题行 -->
<div class="header-container">
<!-- 标题 -->
<h1 class="header">房间列表</h1>
<!-- 功能按钮组 -->
<div class="header-actions">
<!-- 第一组全部加入/取消监控按钮 -->
<div class="button-group">
<el-button v-if="showAddAllButton"
type="primary"
plain
size="small"
@click="addAllMonitor"
class="action-btn">
<el-icon><Checked /></el-icon>
全部加入监控
</el-button>
<el-button v-if="showCancelAllButton"
type="warning"
plain
size="small"
@click="cancelAllMonitor"
class="action-btn">
<el-icon><Close /></el-icon>
全部取消监控
</el-button>
</div>
<!-- 第二组统计标签 -->
<div class="stats-group">
<el-tag type="info">总房间: {{ totalRoomCount }}</el-tag>
<el-tag type="success" v-if="monitoredRoomCount > 0">
监控中: {{ monitoredRoomCount }}
</el-tag>
</div>
</div>
</div>
<!-- 主机盒子容器 -->
<div class="room-container">
<div v-for="(host, index) in mergedRooms" :key="index" class="room-box" @click="viewDetail(host)">
<!-- 第一行房间号 -->
<div class="room-number">
<el-icon><House /></el-icon>
{{ host.roomno || host.roomNumber }}
</div>
<!-- 第二行酒店代码 -->
<div class="hotel-code">
<el-icon><OfficeBuilding /></el-icon>
{{ host.hotelCode }}
</div>
<!-- 第三行操作按钮 -->
<div class="actions">
<!-- 已监控的显示取消按钮 -->
<el-button v-if="host.isMonitored"
size="small"
type="warning"
plain
@click.stop="cancelMonitor(host)"
class="action-btn">
<el-icon><Close /></el-icon>
取消监控
</el-button>
<!-- 未监控的显示加入按钮 -->
<el-button v-else
size="small"
type="success"
plain
@click.stop="addMonitor(host)"
class="action-btn">
<el-icon><Checked /></el-icon>
加入监控
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, watch, computed } from 'vue';
import { ElMessage, ElIcon, ElButton } from 'element-plus'
import { House, OfficeBuilding, Close, View, Checked, Connection } from '@element-plus/icons-vue'
import qs from 'qs'
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
const allRooms = ref([])
const allHostRooms = ref([])
const totalRoomCount = computed(() => allRooms.value.length);
const monitoredRoomCount = computed(() => allHostRooms.value.length);
// 控制按钮显示的条件计算属性
const showAddAllButton = computed(() => {
return totalRoomCount.value > 0 && monitoredRoomCount.value < totalRoomCount.value;
});
const showCancelAllButton = computed(() => {
return monitoredRoomCount.value > 0;
});
// 创建合并后的房间列表
const mergedRooms = computed(() => {
// 创建映射表key为roomNumber用于查找
const monitoredMap = new Map();
allHostRooms.value.forEach(hostRoom => {
// 使用roomNumber作为键
monitoredMap.set(hostRoom.roomNumber, hostRoom);
});
console.log(allRooms.value)
return allRooms.value.map(room => {
// 查找对应的监控房间(通过房间号匹配)
const matchedHostRoom = monitoredMap.get(room.roomno);
// 创建合并对象
const merged = {
// 首先添加房间的基础字段allRooms
...room,
// 添加监控状态
isMonitored: !!matchedHostRoom,
// 然后添加监控房间中的唯一字段
...(matchedHostRoom ? {
tx: matchedHostRoom.tx,
rx: matchedHostRoom.rx,
lastData: matchedHostRoom.lastData,
hostID: matchedHostRoom.hostID,
hotelCode: matchedHostRoom.hotelCode,
key_HostNumber: matchedHostRoom.key_HostNumber,
createDateTime: matchedHostRoom.createDateTime,
} : {
hotelCode: localStorage.getItem('currentHotelCode'),
})
};
return merged;
});
});
// 获取所有房间
const getRooms = async () => {
try {
const getdate = {
hotel_code: localStorage.getItem('currentHotelCode'),
createDate: localStorage.getItem('currentHotelCreateDate'),
}
const header = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const rs = await $http.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
if (rs.data.isok) {
allRooms.value = rs.data.response
} else {
ElMessage.error(rs.data.message)
}
} catch (error) {
ElMessage.error('获取主机列表失败')
console.error(error)
}
}
// 获取已经监控的房间
const getHostRooms = async () => {
try {
const getdate = {
hotelcode: localStorage.getItem('currentHotelCode'),
}
const rs = await $http.post('LowerMachineLog/GetSpe_MonitorHost', getdate);
if (rs.data.isok) {
allHostRooms.value = rs.data.response.data || []
} else {
ElMessage.error(rs.data.message)
}
} catch (error) {
ElMessage.error('获取监控列表失败')
console.error(error)
}
}
// 取消监控
const cancelMonitor = async (host) => {
try {
// 构造数据时使用正确的房间标识
const data = {
roomNumber: host.roomno || host.roomNumber, // 使用allRooms中的roomno作为标识
hotelCode: host.hotelCode
}
const rs = await $http.post('LowerMachineLog/CancelMonitorHost', data);
if (rs.data.isok) {
ElMessage.success('取消监控成功')
// 刷新监控列表
await getHostRooms()
} else {
ElMessage.error(rs.data.message)
}
} catch (error) {
ElMessage.error('取消监控操作失败')
console.error(error)
}
}
// 加入监控
const addMonitor = async (host) => {
try {
return await true
} catch (error) {
ElMessage.error('加入监控操作失败')
console.error(error)
}
}
// 需要实现的方法(只需提供方法注释)
const addAllMonitor = async () => {
/**
* 全部加入监控实现步骤:
* 1. 从mergedRooms中筛选出所有isMonitored为false的房间
* 2. 遍历这些未监控的房间调用addMonitor方法需修改现有的addMonitor方法支持批量操作
* 3. 所有操作完成后刷新监控列表
* 4. 操作成功/失败提示
*/
};
const cancelAllMonitor = async () => {
/**
* 全部取消监控实现步骤:
* 1. 从allHostRooms中获取所有已监控的房间
* 2. 遍历这些已监控的房间调用cancelMonitor方法需修改现有的cancelMonitor方法支持批量操作
* 3. 所有操作完成后刷新监控列表
* 4. 操作成功/失败提示
*/
};
// 查看详情
const viewDetail = (host) => {
console.log('查看详情:', host)
// 这里实现跳转到详情页或其他操作
}
onMounted(() => {
localStorage.setItem('url', '/home')
checkLoginStatus()
// 同时获取所有房间和监控列表
Promise.all([getRooms(), getHostRooms()])
})
</script>
<style scoped>
.container {
padding: 20px;
}
.header {
text-align: left;
margin-bottom: 0;
}
.room-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 6px 10px; /* 上下6px左右10px */
padding: 10px;
}
.room-box {
width: 122px;
height: 135px;
border-radius: 8px;
padding: 8px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--el-border-color);
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.room-box:hover {
transform: translateY(-3px);
cursor: pointer;
}
.room-box::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #409eff, #67c23a, #e6a23c);
opacity: 0.6;
}
.room-number {
font-weight: bold;
font-size: 14px;
display: flex;
align-items: center;
margin-top: 8px;
}
.hotel-code {
font-size: 12px;
margin: 4px 0;
display: flex;
align-items: center;
}
.actions {
display: flex;
flex-direction: column;
width: 100%;
gap: 3px;
margin-top: auto;
}
.action-btn {
width: 100%;
padding: 0 5px;
margin: 0;
font-size: 13px !important;
height: 24px;
--el-button-size: 24px;
}
/* 新增头部容器样式 */
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
gap: 15px;
}
/* 头部操作区 */
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 8px;
}
/* 统计标签组 */
.stats-group {
display: flex;
gap: 8px;
}
/* 响应式调整列数 */
@media (max-width: 600px) {
.room-container {
justify-content: flex-start;
}
.room-box {
height: auto;
min-height: 100px;
}
}
@media (min-width: 1200px) {
.room-container {
justify-content: flex-start;
}
}
/* 深色模式样式调整 */
.dark .room-box {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
border-color: var(--el-border-color-lighter);
}
.dark .room-box:hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
}
/* 减少深色模式下的亮度变化 */
.dark .room-box::before {
opacity: 0.4;
}
</style>

566
src/pages/login/index.vue Normal file
View File

@@ -0,0 +1,566 @@
<template>
<div style="height:100vh">
<div class="container">
<div class="logo-container">
<img src="../../../public/logobig.svg" class="log-img" />
<!--<div class="brand-logo">
<div class="text-container">
<div class="main-text">
inHaos
<span class="tm">TM</span>
</div>
<div class="sub-text">Everything done in house</div>
</div>
</div>-->
</div>
<div class="login-container">
<h1>宝来威设备监控平台</h1>
<el-form :model="form"
status-icon
:disabled="isLocked"
class="login-form"
@submit.prevent="handleSubmit">
<!-- 账号输入 -->
<el-form-item prop="username">
<el-input v-model="form.username"
placeholder="请输入账号"
clearable
@focus="scrollToFormBottom"
:prefix-icon="User" />
</el-form-item>
<!-- 密码输入 -->
<el-form-item prop="password">
<el-input v-model="form.password"
type="password"
placeholder="请输入密码"
show-password
@focus="scrollToFormBottom"
:prefix-icon="Lock" />
</el-form-item>
<!-- 验证码区域 -->
<el-form-item prop="code" class="captcha-container">
<div class="captcha-input">
<el-input v-model="form.code"
@focus="scrollToFormBottom"
placeholder="请输入验证码"
:prefix-icon="Key"
maxlength="4" />
<img :src="captchaSrc"
class="captcha-image"
alt="验证码"
@click="generateCaptcha" />
</div>
</el-form-item>
<!-- 记住我 & 操作按钮 -->
<el-form-item>
<div class="form-actions">
<el-checkbox v-model="form.remember">记住我</el-checkbox>
<el-button type="primary"
native-type="submit"
:loading="loading"
:disabled="submitDisabled">
{{ isLocked ? `请等待${lockRemainTime}后重试` : '立即登录' }}
</el-button>
</div>
</el-form-item>
<!-- 错误提示 -->
<div v-if="errorAttempts > 0" class="error-tip">
已错误尝试 {{ errorAttempts }} 5次后将锁定账号
</div>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key } from '@element-plus/icons-vue'
import { useNow } from '@vueuse/core';
const checkLoginStatus = inject('checkLoginStatus');
const calculateTimeDiff = inject('calculateTimeDiff');
const router = useRouter()
const $http = inject('$http')
// 响应式状态
const form = ref({
username: localStorage.getItem('rememberedUsername') || '',
password: localStorage.getItem('rememberedPassword') || '',
remember: localStorage.getItem('rememberedUsername') ?
Boolean(localStorage.getItem('rememberedUsername')) :
true,
code: ''
})
const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
const loading = ref(false);
const submitDisabled = ref(false);
const isLocked = ref(false);
const errorAttempts = ref(parseInt(localStorage.getItem('loginErrorAttempts')) || 0);
const lockUntil = ref(parseInt(localStorage.getItem('loginLockUntil')) || 0);
const captchaSrc = ref('');
const captchaValue = ref('');
// 计算属性
const lockRemainTime = computed(() => {
if (!isLocked) return '00:00'
const remain = (lockUntil.value - Date.now()) / 1000
return `${Math.floor(remain / 60)}${Math.floor(remain % 60)}`
})
// 方法实现
const initAuthState = () => {
if (lockUntil.value && Date.now() < lockUntil.value) {
setupUnlockTimer(lockUntil.value - Date.now())
}
}
// 新增深色模式状态
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDarkMode.value = true;
} else {
isDarkMode.value = false;
}
const setupUnlockTimer = (duration) => {
isLocked.value = true
setTimeout(() => {
isLocked.value = false
errorAttempts.value = 0
localStorage.removeItem('loginLockUntil')
localStorage.removeItem('loginErrorAttempts')
}, duration)
}
// 生成验证码
const generateCaptcha = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const chars = '0123456789';
let captcha = '';
canvas.width = 86;
canvas.height = 30;
// 生成验证码字符串
for (let i = 0; i < 4; i++) {
captcha += chars.charAt(Math.floor(Math.random() * chars.length));
}
captchaValue.value = captcha; // 存储验证码值用于验证
// 绘制背景
ctx.fillStyle = isDarkMode.value ? '#242424' : '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制噪点
ctx.fillStyle = isDarkMode.value ? '#FFFFFF' : '#000000';
for (let i = 0; i < 33; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
ctx.fill();
}
// 绘制验证码
ctx.font = '24px Arial';
let xnd = 10;
for (let i = 0; i < captcha.length; i++) {
const angle = (Math.random() * 30) - 15; // -10度到+10度
// 保存当前状态
ctx.save();
// 旋转并绘制文字
ctx.translate(xnd, 20);
ctx.rotate(angle * Math.PI / 180);
ctx.fillText(captcha[i], 0, 0);
// 恢复状态
ctx.restore();
// 更新下一个字符的x位置
xnd += 15 + Math.abs(angle) / 2; // 根据角度调整字符间距
}
// 添加5-10条干扰条纹
ctx.strokeStyle = '#6F4A2F';
const bug = Math.floor(Math.random() * 6) + 3
for (let i = 0; i < bug; i++) {
ctx.beginPath();
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke();
}
// 将canvas转换为图片URL
captchaSrc.value = canvas.toDataURL('image/png');
}
// 仅供展示
import config from '../../../public/config.js';
// 登录方法
const handleSubmit = async () => {
if (isLocked.value || submitDisabled.value) return
if (!validateForm()) return
try {
loading.value = true
/* const response = await $http.post('LeiDa/Login', {
username: form.value.username,
password: form.value.password
})
console.log(response)*/
// 仅供展示使用
form.value = {
username: 'admin',
password: 'blw@123',
remember: true,
}
const response = config.adminLogin
//
setTimeout(() => {
handleLoginResponse(response.data)
}, 555)
//handleLoginResponse(response.data)
} catch (error) {
handleLoginError(error)
} finally {
loading.value = false
}
}
// 新增表单验证方法
const validateForm = () => {
if (!form.value.username.trim()) {
ElMessage.error('请输入账号!')
return false
}
if (!form.value.password.trim()) {
ElMessage.error('请输入密码!')
return false
}
if (!form.value.code.trim()) {
ElMessage.error('请输入验证码!')
return false
}
if (form.value.code !== captchaValue.value) {
ElMessage.error('验证码错误!')
generateCaptcha()
return false
}
return true
}
// 新增错误处理方法
const handleLoginError = (error) => {
console.error('登录请求失败:', error)
ElMessage.error('网络请求失败,请检查网络连接')
errorAttempts.value++
}
const handleLoginResponse = (data) => {
if (data.isok) {
handleLoginSuccess(JSON.parse(data.response))
} else {
handleLoginFailure(data.message)
}
}
// 滚动到表单底部(用于移动端输入框聚焦)
const scrollToFormBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}
const handleLoginSuccess = (userData) => {
if (!userData.isok) {
handleLoginFailure(userData.message)
return
}
// 登录成功处理...
errorAttempts.value = 0
if (localStorage.getItem('ATTEMPTS_STORAGE_KEY')) {
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
}
if (localStorage.getItem('LOCK_STORAGE_KEY')) {
localStorage.removeItem(LOCK_STORAGE_KEY)
}
localStorage.removeItem('loginErrorAttempts')
localStorage.setItem('username', form.value.username)
localStorage.setItem('login', true)
//localStorage.setItem('permission', userData.response)
localStorage.setItem('AccessibleHotels', JSON.stringify(userData.response))
localStorage.setItem("TokenT", new Date())
// 记住账号和密码
if (form.value.remember) {
localStorage.setItem('rememberedUsername', form.value.username)
localStorage.setItem('rememberedPassword', form.value.password)
} else {
if (localStorage.getItem('rememberedUsername')) {
localStorage.removeItem('rememberedPassword')
}
}
ElMessage.success(`登录成功,欢迎:${form.value.username}`)
router.push('/home')
}
const handleLoginFailure = (message) => {
ElMessage.error(message)
errorAttempts.value++
localStorage.setItem('loginErrorAttempts', errorAttempts.value)
if (errorAttempts.value >= 5) {
const lockUntil = Date.now() + 3600000
lockUntil.value = lockUntil
localStorage.setItem('loginLockUntil', lockUntil)
setupUnlockTimer(3600000)
}
}
// 验证时间
const TokenTCheck = () => {
generateCaptcha()
//console.log(localStorage.getItem("TokenT"))
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) < 660000) {
form.value.code = captchaValue.value
if (validateForm()) {
handleSubmit()
}
} else {
localStorage.removeItem("TokenT")
}
}
}
// 初始化
onMounted(() => {
localStorage.removeItem('AccessibleHotels')
localStorage.setItem('login', false)
TokenTCheck()
initAuthState()
checkLoginStatus()
})
</script>
<style scoped>
.container {
display: flex;
height: 100%;
width: 100%;
}
.logo-container, .login-container {
flex: 1; /* 各占50% */
display: flex;
flex-direction: column;
align-items: center; /* 水平居中 */
justify-content: center; /* 垂直居中 */
/*min-height: 100vh;*/
}
.logo-image {
width: 80%;
max-width: 400px;
height: auto;
}
.login-title {
margin: 2rem 0;
text-align: center;
}
/* 外层容器样式 */
div[style*="display: flex"] {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
}
/* 通用样式保持 */
.captcha-container {
margin-bottom: 18px;
}
.captcha-input {
display: flex;
gap: 10px;
}
.captcha-image {
height: 40px;
cursor: pointer;
border-radius: 4px;
border: 1px solid var(--el-border-color);
}
.form-actions {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-tip {
color: var(--el-color-danger);
font-size: 12px;
text-align: center;
margin-top: -10px;
margin-bottom: 10px;
}
.brand-logo {
display: flex;
align-items: center;
padding: 20px;
font-family: Arial, sans-serif;
cursor: default; /* 禁用文本选择光标 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.symbol-container {
position: relative;
margin-right: 25px;
pointer-events: none; /* 禁用图形部分交互 */
}
.text-container {
display: flex;
flex-direction: column;
pointer-events: none; /* 禁用文字部分交互 */
}
.main-text {
font-size: 88px;
font-weight: 900; /* 加粗程度提升 */
font-style: italic; /* 新增斜体效果 */
margin-bottom: 5px;
font-family: Arial, sans-serif; /* 确保斜体生效 */
transform: skewX(-10deg);
}
/* 其他保持原有样式 */
h1 {
margin: 100px 0px 100px 0px;
text-align: center
}
.sub-text {
font-size: 12px;
letter-spacing: 0.5px;
text-align: left;
}
.log-img {
width: 400px;
height: auto;
}
/* 移动端适配 */
@media (max-width: 768px) {
.log-img {
width: 220px;
}
div [style*="display: flex"] {
flex-direction: column; /* 改为垂直布局 */
padding: 100px;
}
/* 图片容器调整 */
div[style*="display: flex"] > div:first-child {
width: 100%;
margin: 0 auto;
text-align: center;
}
/* 图片样式调整 */
img[src*="logobig.svg"] {
/* width: 80% !important;*/
max-width: 300px;
margin: 0 auto !important;
display: block;
}
.login-container {
flex: 9;
}
.logo-container,
.login-container {
padding: 0rem;
justify-content: start;
}
.main-text {
font-size: 66px;
}
/* 验证码输入容器 */
/* .captcha-input {
flex-direction: column;
gap: 10px;
}*/
/* 验证码图片 */
.captcha-image {
width: 120px;
height: auto;
}
.container {
flex-direction: column;
}
.logo-container,
.login-container {
width: 100%;
min-height: auto;
padding: 2rem;
}
.logo-image {
max-width: 280px;
margin-top: 2rem;
}
.login-container {
box-shadow: none;
}
h1 {
margin: 10px 0px 50px 0px;
}
.login-form {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,378 @@
<template>
<div class="user-center-container">
<!-- 头部操作按钮 -->
<div class="header-actions">
<el-button type="primary"
:icon="Edit"
@click="enableEdit"
v-show="!isEdit">
编辑信息
</el-button>
<el-button type="success"
:icon="UploadFilled"
@click="submitForm"
v-show="isEdit && formModified">
保存修改
</el-button>
</div>
<!-- 用户信息表单 -->
<el-form :model="userValue"
:rules="rules"
ref="formRef"
label-width="100px"
label-position="right"
class="user-form">
<el-form-item label="登录名" prop="username">
<el-input v-model="userValue.username"
disabled="false"
placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="真实姓名" prop="realname">
<el-input v-model="userValue.realname"
:disabled="!isEdit"
placeholder="请输入真实姓名" />
</el-form-item>
<!-- <el-form-item label="所属公司" prop="comId">
<el-select v-model="userValue.comId"
:disabled="!isEdit"
placeholder="请选择公司">
<el-option v-for="company in companies"
:key="company.id"
:label="company.nameCn"
:value="company.id" />
</el-select>
</el-form-item>-->
<!-- <el-form-item label="职位" prop="position">
<el-select v-model="userValue.position"
placeholder="请选择用户类型"
:disabled="!isEdit">
<el-option v-for="item in positionList"
:key="item"
:label="item"
:value="item" />
</el-select>
</el-form-item>-->
<el-form-item label="手机号" prop="mobile">
<el-input v-model="userValue.mobile"
:disabled="!isEdit"
placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userValue.email"
:disabled="!isEdit"
placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="微信号" prop="weixin">
<el-input v-model="userValue.weixin"
:disabled="!isEdit"
placeholder="请输入微信号" />
</el-form-item>
<!-- 头像上传组件 -->
<el-form-item label="头像" prop="avatar">
<el-upload class="avatar-uploader"
:disabled="!isEdit"
drag
accept="image/*"
:auto-upload="false"
:show-file-list="false"
:on-change="handleAvatarChange"
ref="avatarUploadRef">
<div v-if="userValue.avatar" class="avatar-preview">
<img :src="selectedFile ? avatarPreview : config.Ads + userValue.avatar"
class="avatar-image" />
<div v-show="isEdit" class="avatar-mask">
<el-icon :size="23"><Edit /></el-icon>
<div>点击更换头像</div>
</div>
</div>
<template v-else>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
拖拽图片到此 <em>点击上传</em>
</div>
<div class="el-upload__tip">
支持JPG/PNG大小不超过5MB
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, inject, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus, Delete, Edit, UploadFilled } from '@element-plus/icons-vue';
const $http = inject('$http');
const ajaxfile = inject('ajaxfile');
const config = inject('config');
// 初始状态定义
const isEdit = ref(true);
const formModified = ref(false);
const formRef = ref(null);
const avatarUploadRef = ref(null);
const selectedFile = ref(null);
const avatarPreview = ref('');
const loading = ref(false);
const companies = ref([]);
const userValue = reactive({
id: null,
username: "",
realname: "",
comId: null,
company: "",
password: "",
roleId: null,
position: "",
weixin: "",
email: "",
mobile: "",
avatar: ""
});
// 监听表单变化
watch(userValue, () => {
formModified.value = true;
}, { deep: true });
// 启用编辑
const enableEdit = () => {
isEdit.value = true;
};
// 头像处理
const handleAvatarChange = (file) => {
selectedFile.value = file.raw;
avatarPreview.value = URL.createObjectURL(file.raw);
};
// 验证规则
const rules = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
],
mobile: [
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
email: [
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
]
});
// 获取用户列表
const getUser = async () => {
try {
loading.value = true;
const rs = await $http.post('Company/GetComInfo', {
IsAll: true,
ID: 0,
});
if (rs.data.isok) {
companies.value = rs.data.response;
const rs1 = await $http.post('Users/GetUserInfo', {
IsAll: false,
ID: localStorage.getItem("uid"),
});
if (rs1.data.isok) {
// 1. 创建公司映射表(提高查找效率)
const companyMap = companies.value.reduce((map, company) => {
map[company.id] = company.nameCn;
return map;
}, {});
// 2. 处理用户数据
Object.assign(userValue, rs1.data.response);
userValue.company = companyMap[userValue.comId];
console.log(userValue)
} else {
ElMessage.error(rs1.data.message);
}
}
} catch (error) {
ElMessage.error('获取用户失败');
console.error(error);
} finally {
loading.value = false;
}
};
// 表单提交
const submitForm = async () => {
try {
// 表单验证
await formRef.value.validate();
// 头像上传逻辑
/* if (selectedFile.value) {
let filedata = {
File: selectedFile.value,
Folder: "face"
}
const uploadRes = await ajaxfile(filedata);
if (uploadRes?.fileName) {
userValue.avatar = uploadRes.fileName;
}
}*/
if (selectedFile.value) {
const uploadRes = await ajaxfile(filedata);
if (uploadRes?.fileName) {
userValue.avatar = uploadRes.fileName; // 根据实际接口返回结构调整
} else {
ElMessage.error('头像上传失败: ' + uploadRes);
return;
}
} else {
userValue.avatar = userValue.avatar.substring(userValue.avatar.lastIndexOf('/') + 1);
}
// 更新用户信息
const rs = await $http.post('Users/EditUser', userValue);
if (rs.data.isok) {
ElMessage.success('信息更新成功');
isEdit.value = false;
formModified.value = false;
selectedFile.value = null;
avatarUploadRef.value.clearFiles();
await getUser(); // 刷新数据
}
} catch (error) {
ElMessage.error('保存失败: ' + error.message);
}
};
// 获取身份
const positionList = ref([]);
const getShengfen = () => {
try {
const rs = $http.post('ConfigPY/GetSingleValue', {
VarName: "用户类型",
}).then(rs => {
//console.log(JSON.parse(rs.data.response));
positionList.value = JSON.parse(rs.data.response)
})
} catch (error) {
console.log(error);
}
};
onMounted(() => {
getUser()
getShengfen()
})
</script>
<style scoped>
.user-center-container {
padding: 20px;
max-width: 444px;
margin: 0;
}
.header-actions {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}
.avatar-preview {
position: relative;
width: 178px;
height: 178px;
}
.avatar {
width: 120px;
height: 120px;
display: block;
}
/* 拖拽区域样式 */
.avatar-uploader :deep(.el-upload-dragger) {
width: 220px;
height: 220px;
padding: 20px;
border-radius: 5%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.avatar-uploader:hover :deep(.el-upload-dragger) {
border-color: var(--el-color-primary);
}
/* 头像预览样式 */
.avatar-preview {
position: relative;
width: 100%;
height: 100%;
}
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
border-radius: 5%;
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.avatar-mask:hover {
opacity: 1;
}
/* 图标和文字样式 */
.el-icon--upload {
font-size: 36px;
color: var(--el-text-color-secondary);
margin-bottom: 12px;
}
.el-upload__text {
font-size: 14px;
color: var(--el-text-color-regular);
text-align: center;
}
.el-upload__text em {
color: var(--el-color-primary);
font-style: normal;
}
.el-upload__tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 8px;
}
</style>

1820
src/pages/radarlog/index.vue Normal file

File diff suppressed because it is too large Load Diff

107
src/router/index.js Normal file
View File

@@ -0,0 +1,107 @@
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../pages/home/index.vue';
import Login from '../pages/login/index.vue';
import RadarLog from '../pages/radarlog/index.vue';
import LogSetup from '../pages/logsetup/index.vue';
import DicManage from '../pages/dicmanage/index.vue';
import DBInfoLog from '../pages/dbinfolog/index.vue';
const routes = [
{
path: '/',
// 动态重定向到登录页或主页
redirect: () => {
const isAuthenticated = localStorage.getItem('TokenT');
return isAuthenticated ? '/home' : '/login';
}
},
{
path: '/login',
name: '登录',
component: Login
},
{
path: '/home',
name: '主页',
component: Home,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/dicmanage',
name: '字典管理',
component: DicManage,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/radarlog',
name: '雷达日志',
component: RadarLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/dbinfolog',
name: '分析日志',
component: DBInfoLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/logsetup',
name: '日志设置',
component: LogSetup,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/:pathMatch(.*)*',
name: '404错误',
component: () => import('../pages/404/index.vue') // 404页面
},
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
//next() //跳过守护
const isAuthenticated = localStorage.getItem('AccessibleHotels') // 检查是否登陆过
//const username = localStorage.getItem('username') // 获取用户名
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) > 660000) { // 3天内免密登录
next('/login')
}
} else if (to.fullPath != "/login") {
next('/login')
}
if (to.meta.requiresAuth && !isAuthenticated) {
// 如果需要认证且没有token则跳转到登录页面
next('/login')
} /*else if ((to.path == '/usermanage' || to.path == '/scopemanage') && (username != 'Admin' && username != 'MoMoWen')) {
// 如果访问的是用户管理页面但用户名不是Admin则跳转到主页
next('/home')
} */else {
// 其他情况正常放行
sessionStorage.setItem('currentRoute', to.fullPath)
next()
}
})
export default router;
// 计算时间差
const calculateTimeDiff = (targetTimeStr) => {
const targetDate = new Date(targetTimeStr);
// 检查日期是否有效
if (isNaN(targetDate.getTime())) {
throw new Error("无效的时间格式");
}
const now = new Date();
return Math.floor((now - targetDate) / 1000);
}