Compare commits
1 Commits
d18a334626
...
8d5aee9c78
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d5aee9c78 |
@@ -1,8 +1,6 @@
|
|||||||
VITE_APP_API_BASE_URL='http://10.97.245.96'
|
VITE_APP_API_BASE_URL='http://10.97.245.96'
|
||||||
VITE_APP_API_INT_BASE_URL='http://127.0.0.1'
|
|
||||||
VITE_APP_API_EXT_BASE_URL= 'http://8.134.12.216:3005'
|
|
||||||
# 32字节密钥(AES-256)
|
# 32字节密钥(AES-256)
|
||||||
VITE_APP_KEY='cms.SBU4.PE$20@2aXz3kL9pR7sQ4fT1'
|
VITE_APP_KEY='cms.SBU4.PE$20@2aXz3kL9pR7sQ4fT1vB'
|
||||||
|
|
||||||
# 16字节向量
|
# 16字节向量
|
||||||
VITE_APP_IV='m8Zp2x7cK9bF3dS5'
|
VITE_APP_IV='m8Zp2x7cK9bF3dS5'
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
VITE_APP_API_BASE_URL='http://10.97.245.96'
|
VITE_APP_AUTH_API='http://192.168.8.8:9000'
|
||||||
VITE_APP_API_INT_BASE_URL='http://10.97.245.96'
|
VITE_APP_API_BASE_URL='http://192.168.8.8:9002'
|
||||||
VITE_APP_API_EXT_BASE_URL= 'http://8.134.12.216:3005'
|
|
||||||
# 32字节密钥(AES-256)
|
# 32字节密钥(AES-256)
|
||||||
VITE_APP_KEY='cms.SBU4.PE$20@2aXz3kL9pR7sQ4fT1'
|
VITE_APP_KEY='cms.SBU4.PE$20@2aXz3kL9pR7sQ4fT1vB'
|
||||||
|
|
||||||
# 16字节向量
|
# 16字节向量
|
||||||
VITE_APP_IV='m8Zp2x7cK9bF3dS5'
|
VITE_APP_IV='m8Zp2x7cK9bF3dS5'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# pev1
|
# pe
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
|||||||
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -6,5 +6,5 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
components.d.ts
vendored
20
components.d.ts
vendored
@@ -9,27 +9,35 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AuthPage: typeof import('./src/components/authentication/AuthPage.vue')['default']
|
AuthPage: typeof import('./src/components/authentication/AuthPage.vue')['default']
|
||||||
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||||
|
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||||
|
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElFooter: typeof import('element-plus/es')['ElFooter']
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
LoginForm: typeof import('./src/components/authentication/LoginForm.vue')['default']
|
LoginForm: typeof import('./src/components/authentication/LoginForm.vue')['default']
|
||||||
RegisterForm: typeof import('./src/components/authentication/RegisterForm.vue')['default']
|
RegisterForm: typeof import('./src/components/authentication/RegisterForm.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Sidebar: typeof import('./src/components/menu/Sidebar.vue')['default']
|
|
||||||
SidebarItem: typeof import('./src/components/menu/SidebarItem.vue')['default']
|
|
||||||
}
|
|
||||||
export interface GlobalDirectives {
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SBU4 I4.0</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "pe",
|
"name": "pev1",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 894 B After Width: | Height: | Size: 4.2 KiB |
@@ -80,6 +80,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject, onMounted } from 'vue';
|
import { ref, inject, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import LoginForm from './LoginForm.vue';
|
import LoginForm from './LoginForm.vue';
|
||||||
import RegisterForm from './RegisterForm.vue';
|
import RegisterForm from './RegisterForm.vue';
|
||||||
@@ -87,12 +88,11 @@ import { getLocale, setLocale } from '@/utils/storage';
|
|||||||
import request from '@/utils/api';
|
import request from '@/utils/api';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import backgroundImage from '@/assets/background/vtech.png';
|
import backgroundImage from '@/assets/background/vtech.png';
|
||||||
import crypto from '@/utils/crypto';
|
|
||||||
import router from '@/router';
|
|
||||||
import { checkLoginStatus, clearLoginState } from '@/router';
|
|
||||||
|
|
||||||
// 初始化i18n
|
// 初始化i18n
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
// 路由实例
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 主题相关
|
// 主题相关
|
||||||
const theme = inject('theme', null);
|
const theme = inject('theme', null);
|
||||||
@@ -105,6 +105,18 @@ const backgroundImageStyle = ref({});
|
|||||||
const loginLoading = ref(false);
|
const loginLoading = ref(false);
|
||||||
const currentLanguage = ref(getLocale() || 'zh-CN');
|
const currentLanguage = ref(getLocale() || 'zh-CN');
|
||||||
|
|
||||||
|
// 加密工具兜底方案
|
||||||
|
let crypto;
|
||||||
|
try {
|
||||||
|
crypto = require('@/utils/crypto').default;
|
||||||
|
} catch (e) {
|
||||||
|
// 生产环境不输出警告
|
||||||
|
crypto = {
|
||||||
|
encrypt: (data) => data,
|
||||||
|
decrypt: (data) => data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
locale.value = getLocale();
|
locale.value = getLocale();
|
||||||
@@ -117,11 +129,10 @@ const checkRememberedLogin = () => {
|
|||||||
const savedUser = localStorage.getItem('rememberedUser');
|
const savedUser = localStorage.getItem('rememberedUser');
|
||||||
if (savedUser) {
|
if (savedUser) {
|
||||||
try {
|
try {
|
||||||
const decryptedData = crypto.decrypt(savedUser);
|
const userData = JSON.parse(crypto.decrypt(savedUser));
|
||||||
const userData = JSON.parse(decryptedData);
|
// 移除调试输出
|
||||||
// 可以在这里自动填充用户名到登录表单
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析记住的用户数据失败:', e);
|
// 生产环境不输出错误
|
||||||
localStorage.removeItem('rememberedUser');
|
localStorage.removeItem('rememberedUser');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +146,7 @@ const handleThemeChange = (newTheme) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载背景图片
|
// 加载背景图片的方法
|
||||||
const loadBackgroundImage = () => {
|
const loadBackgroundImage = () => {
|
||||||
try {
|
try {
|
||||||
const imgUrl = backgroundImage;
|
const imgUrl = backgroundImage;
|
||||||
@@ -174,12 +185,14 @@ const toggleMode = () => {
|
|||||||
isLoginMode.value = !isLoginMode.value;
|
isLoginMode.value = !isLoginMode.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 错误信息提取
|
// 错误信息提取方法
|
||||||
const getErrorMessage = (error) => {
|
const getErrorMessage = (error) => {
|
||||||
|
// 处理字符串错误
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理Axios错误
|
||||||
if (error && (error.name === 'AxiosError' || error.isAxiosError)) {
|
if (error && (error.name === 'AxiosError' || error.isAxiosError)) {
|
||||||
if (error.code === 'ECONNABORTED') {
|
if (error.code === 'ECONNABORTED') {
|
||||||
return t('auth.requestTimeout');
|
return t('auth.requestTimeout');
|
||||||
@@ -200,17 +213,19 @@ const getErrorMessage = (error) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理普通对象错误
|
||||||
if (error && typeof error === 'object') {
|
if (error && typeof error === 'object') {
|
||||||
return error.message || error.msg || t('auth.unknownError');
|
return error.message || error.msg || t('auth.unknownError');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兜底
|
||||||
return t('auth.unknownError');
|
return t('auth.unknownError');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 登录处理
|
|
||||||
const handleLogin = async (formData) => {
|
const handleLogin = async (formData) => {
|
||||||
try {
|
try {
|
||||||
// 验证表单数据
|
// 1. 验证表单数据是否存在
|
||||||
if (!formData || !formData.username || !formData.password) {
|
if (!formData || !formData.username || !formData.password) {
|
||||||
ElMessage.error(t('auth.usernameOrPasswordEmpty'));
|
ElMessage.error(t('auth.usernameOrPasswordEmpty'));
|
||||||
return;
|
return;
|
||||||
@@ -218,75 +233,55 @@ const handleLogin = async (formData) => {
|
|||||||
|
|
||||||
loginLoading.value = true;
|
loginLoading.value = true;
|
||||||
|
|
||||||
// 发送登录请求
|
// 2. 发送请求
|
||||||
const response = await request.postWithoutEncryption(
|
const response = await request.postWithoutEncryption(
|
||||||
'/api/i4/v1/auth/login.php',
|
'/api/i4/v1/auth/login.php',
|
||||||
{
|
{
|
||||||
login_name: formData.username,
|
login_name: formData.username,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
rememberMe: formData.rememberMe
|
rememberMe: formData.rememberMe
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 关键修复:强制解析响应为JSON对象
|
if (response && response.code === 200) {
|
||||||
let parsedResponse = response;
|
|
||||||
if (typeof response === 'string') {
|
const userInfo = response.data;
|
||||||
// 移除可能的多余符号(如< >)
|
// 关键修复:设置路由守卫依赖的登录状态
|
||||||
const cleanedResponse = response.replace(/[<>]/g, '');
|
|
||||||
try {
|
|
||||||
parsedResponse = JSON.parse(cleanedResponse);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('响应解析失败:', e);
|
|
||||||
ElMessage.error('登录失败,响应格式错误');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用解析后的响应判断
|
|
||||||
console.log('解析后响应:', parsedResponse, 'BOOL:', parsedResponse?.code === 200);
|
|
||||||
if (parsedResponse && parsedResponse.code === 200) {
|
|
||||||
const userInfo = parsedResponse.data;
|
|
||||||
console.log('用户信息:', userInfo);
|
|
||||||
// 存储登录状态
|
|
||||||
localStorage.setItem('isLoggedIn', 'true');
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
// 存储token(如果后端返回了token)
|
||||||
// 加密存储用户信息
|
Cookies.set('access_token', userInfo.token || '');
|
||||||
const encryptedUserInfo = crypto.encrypt(JSON.stringify(userInfo));
|
localStorage.setItem('userInfo', crypto.encrypt(JSON.stringify(userInfo)));
|
||||||
localStorage.setItem('userInfo', encryptedUserInfo);
|
|
||||||
|
|
||||||
// 存储token
|
|
||||||
Cookies.set('access_token', userInfo.token || '', {
|
|
||||||
expires: formData.rememberMe ? 7 : 2/24, // 记住我时保存7天,否则2小时
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理记住我
|
|
||||||
if (formData.rememberMe) {
|
if (formData.rememberMe) {
|
||||||
const rememberedData = crypto.encrypt(JSON.stringify({
|
localStorage.setItem('rememberedUser', crypto.encrypt(JSON.stringify({
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
rememberMe: true
|
rememberMe: true
|
||||||
}));
|
})));
|
||||||
localStorage.setItem('rememberedUser', rememberedData);
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('rememberedUser');
|
localStorage.removeItem('rememberedUser');
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success(t('auth.loginSuccess'));
|
ElMessage.success(t('auth.loginSuccess'));
|
||||||
|
|
||||||
// 验证登录状态并跳转
|
// 改进跳转逻辑:使用路由实例的push并处理可能的异常
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (checkLoginStatus()) {
|
await router.push('/main');
|
||||||
await router.push('/');
|
// 跳转成功后可以刷新页面确保状态生效(可选)
|
||||||
} else {
|
// window.location.reload();
|
||||||
throw new Error('登录状态验证失败');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('跳转失败:', err);
|
console.error('路由跳转失败:', err);
|
||||||
ElMessage.error('登录状态异常,请重试');
|
// 失败时的备选方案
|
||||||
clearLoginState();
|
window.location.href = '/main';
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = response?.message || t('auth.loginFailed');
|
const errorMsg = response?.message || t('auth.loginFailed');
|
||||||
ElMessage.error(errorMsg);
|
ElMessage.error(errorMsg);
|
||||||
@@ -299,11 +294,6 @@ const handleLogin = async (formData) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 注册处理(如果需要实现)
|
|
||||||
const handleRegister = async (formData) => {
|
|
||||||
// 注册逻辑实现
|
|
||||||
ElMessage.info('注册功能待实现');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 语言切换
|
// 语言切换
|
||||||
const toggleLanguage = (newLocale) => {
|
const toggleLanguage = (newLocale) => {
|
||||||
@@ -314,6 +304,7 @@ const toggleLanguage = (newLocale) => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-container {
|
.page-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -340,7 +331,7 @@ const toggleLanguage = (newLocale) => {
|
|||||||
height: 390px;
|
height: 390px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 10px 25px -5px var(--color-shadow);
|
box-shadow: 0 10px 25px -5px var(--color-shadow);
|
||||||
background-color: rgba(255, 255, 255, 0.001);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
:model="loginForm"
|
:model="loginForm"
|
||||||
:rules="loginRules"
|
:rules="loginRules"
|
||||||
class="login-form"
|
class="login-form"
|
||||||
@keyup.enter.native="handleSubmit"
|
|
||||||
>
|
>
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginForm.username"
|
v-model="loginForm.username"
|
||||||
:placeholder="$t('auth.usernamePlaceholder')"
|
:placeholder="$t('auth.usernamePlaceholder')"
|
||||||
@keyup.enter.native="handleSubmit"
|
|
||||||
clearable
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon class="el-input__icon"><User /></el-icon>
|
<el-icon class="el-input__icon"><User /></el-icon>
|
||||||
@@ -25,7 +22,6 @@
|
|||||||
:placeholder="$t('auth.passwordPlaceholder')"
|
:placeholder="$t('auth.passwordPlaceholder')"
|
||||||
type="password"
|
type="password"
|
||||||
show-password
|
show-password
|
||||||
@keyup.enter.native="handleSubmit"
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon class="el-input__icon"><Lock /></el-icon>
|
<el-icon class="el-input__icon"><Lock /></el-icon>
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-menu
|
|
||||||
:default-active="defaultActive"
|
|
||||||
class="el-menu-vertical"
|
|
||||||
:collapse="isCollapse"
|
|
||||||
:unique-opened="true"
|
|
||||||
router
|
|
||||||
@select="handleMenuSelect"
|
|
||||||
>
|
|
||||||
<!-- 折叠按钮 -->
|
|
||||||
<el-button
|
|
||||||
v-if="!isCollapse"
|
|
||||||
icon="Expand"
|
|
||||||
size="mini"
|
|
||||||
class="collapse-btn"
|
|
||||||
@click="$emit('toggle-collapse')"
|
|
||||||
:title="t('common.collapseMenu')"
|
|
||||||
/>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
icon="Fold"
|
|
||||||
size="mini"
|
|
||||||
class="collapse-btn"
|
|
||||||
@click="$emit('toggle-collapse')"
|
|
||||||
:title="t('common.expandMenu')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-for="item in topLevelMenus" :key="item.id">
|
|
||||||
<sidebar-item
|
|
||||||
:item="item"
|
|
||||||
:menu-data="rowData"
|
|
||||||
:depth="1"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, defineProps, defineEmits, watch, onMounted, inject } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import request from '@/utils/api';
|
|
||||||
import SidebarItem from './SidebarItem.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
isCollapse: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注入主题配置
|
|
||||||
const theme = inject('theme', { currentTheme: 'light' });
|
|
||||||
|
|
||||||
// 国际化
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
// 获取路由实例
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const currentRouteFullPath = ref(route.fullPath)
|
|
||||||
|
|
||||||
// 监听路由变化
|
|
||||||
watch(
|
|
||||||
() => route.fullPath,
|
|
||||||
(newFullPath) => {
|
|
||||||
currentRouteFullPath.value = newFullPath
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 处理菜单选中事件
|
|
||||||
const handleMenuSelect = (index) => {
|
|
||||||
const targetRoute = router.resolve(index)
|
|
||||||
if (targetRoute.fullPath === route.fullPath) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emit = defineEmits(['toggle-collapse'])
|
|
||||||
|
|
||||||
const rowData = ref([]) // 响应式菜单数据
|
|
||||||
|
|
||||||
// 获取菜单数据
|
|
||||||
const getMenuData = async () => {
|
|
||||||
try {
|
|
||||||
const response = await request.postWithoutEncryption(
|
|
||||||
'/api/i4/v1/get/menu.php',
|
|
||||||
{
|
|
||||||
login_name: 'NR1798' // 可替换为动态值
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response && response.code === 200 && Array.isArray(response.data)) {
|
|
||||||
return response.data;
|
|
||||||
} else {
|
|
||||||
const errorMsg = response?.message || t('common.unknownError');
|
|
||||||
ElMessage.error(`${t('menu.fetchFailed')}: ${errorMsg}`);
|
|
||||||
console.error('后端错误:', response);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const msg = error.response?.data?.message
|
|
||||||
|| error.message
|
|
||||||
|| t('common.networkError');
|
|
||||||
ElMessage.error(`${t('menu.fetchFailed')}: ${msg}`);
|
|
||||||
console.error('获取菜单数据失败:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const data = await getMenuData();
|
|
||||||
if (data) {
|
|
||||||
rowData.value = data;
|
|
||||||
} else {
|
|
||||||
ElMessage.warning(t('menu.noMenuData'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(t('menu.loadFailed'));
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 顶级菜单过滤
|
|
||||||
const topLevelMenus = computed(() => {
|
|
||||||
if (!Array.isArray(rowData.value)) return [];
|
|
||||||
return rowData.value.filter(item => item.parent_id === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 默认激活项计算
|
|
||||||
const defaultActive = computed(() => {
|
|
||||||
if (!Array.isArray(rowData.value) || !currentRouteFullPath.value) return '';
|
|
||||||
|
|
||||||
// 查找与当前路由匹配的菜单项
|
|
||||||
const matchedItem = rowData.value.find(item => {
|
|
||||||
if (!item.menu_url) return false;
|
|
||||||
// 处理完整匹配和部分匹配
|
|
||||||
return currentRouteFullPath.value === item.menu_url ||
|
|
||||||
currentRouteFullPath.value.startsWith(item.menu_url + '/');
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchedItem ? (matchedItem.menu_url || String(matchedItem.id)) : '';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 设置菜单项的高度 */
|
|
||||||
.el-menu-vertical :deep(.el-menu-item),
|
|
||||||
.el-menu-vertical :deep(.el-sub-menu__title) {
|
|
||||||
height: 50px;
|
|
||||||
line-height: 50px;
|
|
||||||
color: var(--color-text); /* 使用全局主题文本色 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 子菜单项的高度 */
|
|
||||||
.el-menu-vertical :deep(.el-menu-item) {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu {
|
|
||||||
height: 100vh;
|
|
||||||
border-right: 1px solid var(--color-border); /* 使用全局主题边框色 */
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-surface); /* 使用全局主题表面色 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: -10px;
|
|
||||||
z-index: 10;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--color-surface); /* 使用全局主题表面色 */
|
|
||||||
border: 1px solid var(--color-border); /* 使用全局主题边框色 */
|
|
||||||
color: var(--color-text); /* 使用全局主题文本色 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 适配全局主题的菜单样式 */
|
|
||||||
:deep(.el-menu) {
|
|
||||||
background-color: var(--color-surface) !important;
|
|
||||||
border-right-color: var(--color-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 关键修改:通过样式设置激活文本颜色,替代属性绑定 */
|
|
||||||
:deep(.el-menu-item.is-active),
|
|
||||||
:deep(.el-sub-menu__title.is-active) {
|
|
||||||
color: var(--color-primary) !important;
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item:hover:not(.is-active)) {
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-sub-menu__title:hover:not(.is-active)) {
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 菜单折叠/展开动画 */
|
|
||||||
.el-menu-vertical {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<template>
|
|
||||||
<template v-if="depth > 10">
|
|
||||||
<el-menu-item disabled>
|
|
||||||
<span>{{ t('menu.menuTooDeep') }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-sub-menu v-else-if="hasChildren" :index="indexValue">
|
|
||||||
<template #title>
|
|
||||||
<span>{{ t(item.menu_name) }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-for="child in children" :key="child.id">
|
|
||||||
<sidebar-item
|
|
||||||
:item="child"
|
|
||||||
:menu-data="menuData"
|
|
||||||
:depth="depth + 1"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-sub-menu>
|
|
||||||
|
|
||||||
<el-menu-item
|
|
||||||
v-else
|
|
||||||
:index="indexValue"
|
|
||||||
:route="item.menu_url || undefined"
|
|
||||||
>
|
|
||||||
<span>{{ t(item.menu_name) }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { defineProps, computed } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
menuData: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
depth: {
|
|
||||||
type: Number,
|
|
||||||
default: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算索引值,优先使用menu_url,否则使用id
|
|
||||||
const indexValue = computed(() => {
|
|
||||||
return props.item.menu_url || String(props.item.id);
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取子菜单
|
|
||||||
const children = computed(() => {
|
|
||||||
return props.menuData
|
|
||||||
.filter(child =>
|
|
||||||
child.parent_id === props.item.id &&
|
|
||||||
child.id !== props.item.id // 防止自引用
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.id - b.id); // 按ID排序
|
|
||||||
})
|
|
||||||
|
|
||||||
// 判断是否有子菜单
|
|
||||||
const hasChildren = computed(() => {
|
|
||||||
return children.value.length > 0;
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -10,8 +10,8 @@ export default {
|
|||||||
passwordMinLength: 'Password must be at least 6 characters',
|
passwordMinLength: 'Password must be at least 6 characters',
|
||||||
confirmPassword: 'Confirm Password',
|
confirmPassword: 'Confirm Password',
|
||||||
passwordMismatch: 'Passwords do not match',
|
passwordMismatch: 'Passwords do not match',
|
||||||
usernamePlaceholder: 'Enter username',
|
usernamePlaceholder: 'User name',
|
||||||
passwordPlaceholder: 'Enter password',
|
passwordPlaceholder: 'Password',
|
||||||
loginBtn: 'Login',
|
loginBtn: 'Login',
|
||||||
confirmPasswordPlaceholder: 'Please confirm your password',
|
confirmPasswordPlaceholder: 'Please confirm your password',
|
||||||
registerBtn: 'Register',
|
registerBtn: 'Register',
|
||||||
@@ -21,95 +21,16 @@ export default {
|
|||||||
invalidCredentials: 'Invalid username or password',
|
invalidCredentials: 'Invalid username or password',
|
||||||
serverError: 'Server error, please try again later',
|
serverError: 'Server error, please try again later',
|
||||||
networkError: 'Network error, please check your connection',
|
networkError: 'Network error, please check your connection',
|
||||||
unknownError: 'An unknown error occurred, please try again',
|
unknownError: 'An unknown error occurred, please try again'
|
||||||
requestTimeout: 'Request timed out, please try again later',
|
|
||||||
unauthorized: 'Unauthorized access, please login again',
|
|
||||||
forbidden: 'Access forbidden, insufficient permissions',
|
|
||||||
notFound: 'Requested resource not found',
|
|
||||||
requestFailed: 'Request processing failed',
|
|
||||||
usernameExists: 'Username already exists',
|
|
||||||
emailExists: 'Email already registered',
|
|
||||||
invalidUsername: 'Invalid username format',
|
|
||||||
invalidPassword: 'Password format does not meet requirements',
|
|
||||||
registerSuccess: 'Registration successful',
|
|
||||||
registerFailed: 'Registration failed, please try again'
|
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
switchLang: 'Switch Language',
|
switchLang: 'Switch Language'
|
||||||
languageChanged: 'Language switched successfully',
|
|
||||||
close: 'Close',
|
|
||||||
confirm: 'Confirm',
|
|
||||||
cancel: 'Cancel',
|
|
||||||
refresh: 'Refresh',
|
|
||||||
collapseMenu: 'Collapse Menu',
|
|
||||||
expandMenu: 'Expand Menu',
|
|
||||||
operationSuccess: 'Operation successful',
|
|
||||||
operationFailed: 'Operation failed',
|
|
||||||
pleaseWait: 'Please wait...',
|
|
||||||
back: 'Back',
|
|
||||||
save: 'Save',
|
|
||||||
delete: 'Delete',
|
|
||||||
edit: 'Edit',
|
|
||||||
add: 'Add',
|
|
||||||
search: 'Search',
|
|
||||||
reset: 'Reset'
|
|
||||||
},
|
},
|
||||||
el: {
|
el: {
|
||||||
form: {
|
form: {
|
||||||
required: 'Please fill in the required field'
|
required: 'Please fill in the required field'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
settings: 'Settings',
|
|
||||||
manage: 'Management',
|
|
||||||
equipment: 'Equipment',
|
|
||||||
purchase: 'Purchase',
|
|
||||||
spare: 'Spare Parts',
|
|
||||||
maintenance: 'Maintenance',
|
|
||||||
report: 'Reports',
|
|
||||||
capacity: 'Capacity',
|
|
||||||
applet: 'Mini Program',
|
|
||||||
useringroup: 'User Groups',
|
|
||||||
userindepartment: 'User Departments',
|
|
||||||
passwordreset: 'Password Reset',
|
|
||||||
users: 'Users',
|
|
||||||
groups: 'Groups',
|
|
||||||
departments: 'Departments',
|
|
||||||
instruments: 'Instruments',
|
|
||||||
fixtures: 'Fixtures',
|
|
||||||
racks: 'Aging Racks',
|
|
||||||
testers: 'Test Stations',
|
|
||||||
purlist: 'Purchase List',
|
|
||||||
splist: 'Spare Parts List',
|
|
||||||
dashboard: 'Dashboard',
|
|
||||||
home: 'Home',
|
|
||||||
help: 'Help Center',
|
|
||||||
dailyReport: 'Daily Report',
|
|
||||||
monthlyReport: 'Monthly Report',
|
|
||||||
annualReport: 'Annual Report',
|
|
||||||
faultReport: 'Fault Report',
|
|
||||||
systemBasic: 'Basic Settings',
|
|
||||||
security: 'Security Settings',
|
|
||||||
notifications: 'Notification Settings',
|
|
||||||
logs: 'System Logs',
|
|
||||||
fetchFailed: 'Failed to fetch menu',
|
|
||||||
noMenuData: 'No menu data available',
|
|
||||||
loadFailed: 'Failed to load menu information',
|
|
||||||
noPermission: 'No access permission',
|
|
||||||
menuTooDeep: "Menu level too deep"
|
|
||||||
},
|
|
||||||
main: {
|
|
||||||
welcome: 'Welcome to the system',
|
|
||||||
contentDescription: 'Please select a menu item on the left to proceed'
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
profile: "Profile",
|
|
||||||
logout: "Logout",
|
|
||||||
notifications: "Notifications",
|
|
||||||
defaultName: "User",
|
|
||||||
defaultRole: "Regular User"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
theme: {
|
|
||||||
light: '亮色主题',
|
|
||||||
dark: '暗色主题',
|
|
||||||
custom: '自定义主题',
|
|
||||||
themeSwitched: '已切换到{theme}主题'
|
|
||||||
},
|
|
||||||
auth: {
|
auth: {
|
||||||
loginTitle: '用户登录',
|
loginTitle: '用户登录',
|
||||||
registerTitle: '用户注册',
|
registerTitle: '用户注册',
|
||||||
@@ -16,7 +10,7 @@ export default {
|
|||||||
passwordMinLength: '密码长度不能少于6个字符',
|
passwordMinLength: '密码长度不能少于6个字符',
|
||||||
confirmPassword: '确认密码',
|
confirmPassword: '确认密码',
|
||||||
passwordMismatch: '两次输入的密码不一致',
|
passwordMismatch: '两次输入的密码不一致',
|
||||||
usernamePlaceholder: '请输入用户名',
|
usernamePlaceholder: '请输入用户名',
|
||||||
passwordPlaceholder: '请输入密码',
|
passwordPlaceholder: '请输入密码',
|
||||||
loginBtn: '登录',
|
loginBtn: '登录',
|
||||||
confirmPasswordPlaceholder: '请再次输入密码',
|
confirmPasswordPlaceholder: '请再次输入密码',
|
||||||
@@ -30,20 +24,17 @@ export default {
|
|||||||
serverError: '服务器错误,请稍后再试',
|
serverError: '服务器错误,请稍后再试',
|
||||||
networkError: '网络连接错误,请检查网络',
|
networkError: '网络连接错误,请检查网络',
|
||||||
unknownError: '发生未知错误,请重试',
|
unknownError: '发生未知错误,请重试',
|
||||||
requestTimeout: '请求超时超时,请稍后重试',
|
// 新增错误类型翻译
|
||||||
unauthorized: '未授权访问,请,请重新登录',
|
requestTimeout: '请求超时,请稍后重试',
|
||||||
forbidden: '禁止访问访问,没有权限',
|
unauthorized: '未授权访问,请重新登录',
|
||||||
|
forbidden: '禁止访问,没有权限',
|
||||||
notFound: '请求的资源不存在',
|
notFound: '请求的资源不存在',
|
||||||
requestFailed: '请求处理失败',
|
requestFailed: '请求处理失败',
|
||||||
|
// 可能需要的其他注册相关提示
|
||||||
usernameExists: '用户名已存在',
|
usernameExists: '用户名已存在',
|
||||||
emailExists: '邮箱已被注册',
|
emailExists: '邮箱已被注册',
|
||||||
invalidUsername: '用户名格式名格式不正确',
|
invalidUsername: '用户名格式不正确',
|
||||||
invalidPassword: '密码格式不符合要求',
|
invalidPassword: '密码格式不符合要求'
|
||||||
sessionExpired: '登录状态已失效,请重新登录',
|
|
||||||
loginSuccess: '登录成功',
|
|
||||||
welcomeBack: '欢迎回来',
|
|
||||||
logoutSuccess: '退出登录成功',
|
|
||||||
logoutFailed: '退出登录失败'
|
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@@ -52,63 +43,12 @@ export default {
|
|||||||
languageChanged: '语言已切换',
|
languageChanged: '语言已切换',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
cancel: '取消',
|
cancel: '取消'
|
||||||
refresh: '刷新',
|
|
||||||
collapseMenu: '折叠菜单',
|
|
||||||
expandMenu: '展开菜单',
|
|
||||||
unknownError: '未知错误',
|
|
||||||
networkError: '网络异常',
|
|
||||||
languageChanged: '语言已切换'
|
|
||||||
},
|
},
|
||||||
el: {
|
el: {
|
||||||
form: {
|
form: {
|
||||||
required: '请输入必填项'
|
required: '请输入必填项'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
settings: '设置',
|
|
||||||
manage: '管理',
|
|
||||||
equipment: '设备',
|
|
||||||
purchase: '采购',
|
|
||||||
spare: '备件',
|
|
||||||
maintenance: '维护',
|
|
||||||
report: '报告',
|
|
||||||
capacity: '容量',
|
|
||||||
applet: '小程序',
|
|
||||||
useringroup: '用户组别',
|
|
||||||
userindepartment: '用户部门',
|
|
||||||
passwordreset: '密码重置',
|
|
||||||
users: '用户',
|
|
||||||
groups: '组别',
|
|
||||||
departments: '部门',
|
|
||||||
instruments: '仪器',
|
|
||||||
fixtures: '夹具',
|
|
||||||
racks: '老化架',
|
|
||||||
testers: '测试位',
|
|
||||||
purlist: '采购清单',
|
|
||||||
splist: '备件清单',
|
|
||||||
fetchFailed: '获取菜单失败',
|
|
||||||
menuTooDeep: "菜单层级过深"
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
profile: "个人信息",
|
|
||||||
logout: "退出登录",
|
|
||||||
notifications: "通知",
|
|
||||||
defaultName: "用户",
|
|
||||||
defaultRole: "普通用户",
|
|
||||||
unknown: '未知用户',
|
|
||||||
unauthorized: '未授权',
|
|
||||||
defaultRole: '普通用户'
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
noUserInfo: '未找到用户信息',
|
|
||||||
invalidBase64: '加密数据格式无效',
|
|
||||||
decryptFailed: '用户信息解密失败',
|
|
||||||
jsonParseFailed: 'JSON解析失败',
|
|
||||||
invalidUserInfo: '用户信息格式无效',
|
|
||||||
loadUserFailed: '加载用户信息失败'
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
openCenter: '打开通知中心'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,84 +1,58 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import AuthPage from '../components/authentication/AuthPage.vue';
|
import AuthPage from '../components/authentication/AuthPage.vue';
|
||||||
import MainPage from '../views/mainpage/index.vue';
|
import MainPage from '../views/mainpage/index.vue';
|
||||||
import UutReport from '../views/uut/Report.vue';
|
|
||||||
import UutHistory from '../views/uut/History.vue';
|
|
||||||
import DashboardTest from '../views/dashboard/test.vue';
|
|
||||||
import Profile from '../views/user/Profile.vue';
|
|
||||||
import Testers from '../views/equipment/Testers.vue';
|
|
||||||
|
|
||||||
// 登录状态检查工具函数
|
// 路由守卫:检查是否登录 - 优化版
|
||||||
const checkLoginStatus = () => {
|
const requireAuth = (to, from, next) => {
|
||||||
|
// 更可靠的登录状态检查
|
||||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
const hasUserInfo = !!localStorage.getItem('userInfo');
|
const hasUserInfo = !!localStorage.getItem('userInfo');
|
||||||
const hasToken = !!Cookies.get('access_token');
|
|
||||||
return isLoggedIn && hasUserInfo && hasToken;
|
if (isLoggedIn && hasUserInfo) {
|
||||||
};
|
|
||||||
|
|
||||||
// 清除登录状态方法
|
|
||||||
const clearLoginState = () => {
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
localStorage.removeItem('userInfo');
|
|
||||||
localStorage.removeItem('rememberedUser');
|
|
||||||
Cookies.remove('access_token');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 路由守卫:需要登录的路由
|
|
||||||
const requireAuth = (to, from, next) => {
|
|
||||||
if (checkLoginStatus()) {
|
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
clearLoginState();
|
// 清除无效的登录状态
|
||||||
next('/auth');
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
next('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 路由配置
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/auth',
|
path: '/',
|
||||||
name: 'Auth',
|
name: 'Auth',
|
||||||
component: AuthPage,
|
component: AuthPage,
|
||||||
beforeEnter: (to, from, next) => {
|
beforeEnter: (to, from, next) => {
|
||||||
if (checkLoginStatus()) {
|
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
next('/');
|
const hasUserInfo = !!localStorage.getItem('userInfo');
|
||||||
|
|
||||||
|
if (isLoggedIn && hasUserInfo) {
|
||||||
|
next('/main');
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/main',
|
||||||
name: 'Main',
|
name: 'Main',
|
||||||
component: MainPage,
|
component: MainPage,
|
||||||
beforeEnter: requireAuth,
|
beforeEnter: requireAuth
|
||||||
children: [
|
|
||||||
{ path: '', redirect: '/dashboard' },
|
|
||||||
{ path: 'dashboard', name: 'Dashboard', component: DashboardTest },
|
|
||||||
{ path: 'uut/report', name: 'UutReport', component: UutReport },
|
|
||||||
{ path: 'uut/history', name: 'UutHistory', component: UutHistory },
|
|
||||||
{ path: 'profile', component: Profile },
|
|
||||||
{ path: 'testers', component: Testers }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'NotFound',
|
redirect: '/'
|
||||||
redirect: (to) => checkLoginStatus() ? '/' : '/auth'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory('/sbu4i4/'),
|
history: createWebHistory(),
|
||||||
routes,
|
routes
|
||||||
scrollBehavior(to, from, savedPosition) {
|
|
||||||
return savedPosition || { top: 0 };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 全局路由守卫
|
// 全局路由守卫,处理可能的登录状态异常
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 对所有需要授权的路由进行检查
|
||||||
if (to.matched.some(record => record.beforeEnter === requireAuth)) {
|
if (to.matched.some(record => record.beforeEnter === requireAuth)) {
|
||||||
requireAuth(to, from, next);
|
requireAuth(to, from, next);
|
||||||
} else {
|
} else {
|
||||||
@@ -86,5 +60,4 @@ router.beforeEach((to, from, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出路由和工具函数
|
export default router;
|
||||||
export { router as default, clearLoginState, checkLoginStatus };
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
// 所有API接口路径集中管理
|
|
||||||
export const ApiPaths = {
|
|
||||||
// 仪器相关接口
|
|
||||||
machine: {
|
|
||||||
list: '/api/i4/v1/get/machines.php', // 获取仪器列表
|
|
||||||
update: '/api/i4/v1/update/machine.php', // 更新仪器信息
|
|
||||||
create: '/api/i4/v1/create/machine.php', // 创建仪器
|
|
||||||
export: '/download.php?x=Machine List' // 导出仪器列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 用户相关接口
|
|
||||||
user: {
|
|
||||||
list: '/api/i4/v1/get/users.php', // 获取用户列表
|
|
||||||
listWithDisabled: '/api/i4/v1/get/users.php?include_disabled=1' // 包含禁用用户
|
|
||||||
},
|
|
||||||
|
|
||||||
// 品牌相关接口
|
|
||||||
brand: {
|
|
||||||
list: '/api/i4/v1/get/brands.php' // 获取品牌列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 客户相关接口
|
|
||||||
customer: {
|
|
||||||
list: '/api/i4/v1/get/customers.php' // 获取客户列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 来源类型相关接口
|
|
||||||
sourceType: {
|
|
||||||
list: '/api/i4/v1/get/eq_sources.php' // 获取来源类型列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 其他来源类型相关接口
|
|
||||||
otherSource: {
|
|
||||||
list: '/api/i4/v1/get/eq_source_other.php' // 获取其他来源类型列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 状态相关接口
|
|
||||||
status: {
|
|
||||||
list: '/api/i4/v1/get/eq_status.php' // 获取状态列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 位置相关接口
|
|
||||||
location: {
|
|
||||||
list: '/api/i4/v1/get/locations.php' // 获取位置列表
|
|
||||||
},
|
|
||||||
|
|
||||||
// 部门相关接口
|
|
||||||
department: {
|
|
||||||
list: '/api/i4/v1/get/departments.php' // 获取部门列表
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiPaths;
|
|
||||||
@@ -2,37 +2,17 @@ import axios from "axios";
|
|||||||
import crypto from "@/utils/crypto";
|
import crypto from "@/utils/crypto";
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { ElMessage, ElLoading } from 'element-plus';
|
import { ElMessage, ElLoading } from 'element-plus';
|
||||||
import router, { clearLoginState } from '@/router'; // 修正路由导入路径
|
|
||||||
|
|
||||||
// 获取基础 API 地址
|
// 获取基础 API 地址
|
||||||
const API_BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
const API_BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||||
|
|
||||||
// 获取内网 API 地址
|
if (!API_BASE_URL) {
|
||||||
const VITE_APP_API_INT_BASE_URL = import.meta.env.VITE_APP_API_INT_BASE_URL;
|
|
||||||
|
|
||||||
// 获取外网 API 地址
|
|
||||||
const VITE_APP_API_EXT_BASE_URL = import.meta.env.VITE_APP_API_EXT_BASE_URL;
|
|
||||||
|
|
||||||
|
|
||||||
if (!API_BASE_URL || !VITE_APP_API_INT_BASE_URL || !VITE_APP_API_EXT_BASE_URL) {
|
|
||||||
console.warn('API基础地址未配置,请在环境变量中设置VITE_APP_API_BASE_URL');
|
console.warn('API基础地址未配置,请在环境变量中设置VITE_APP_API_BASE_URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置基础 API 地址
|
|
||||||
const hostname = `${window.location.hostname}`;
|
|
||||||
var auth_api_url = ''
|
|
||||||
if(hostname == '192.168.6.104') {
|
|
||||||
auth_api_url = API_BASE_URL
|
|
||||||
}else if (hostname == '8.134.12.216') {
|
|
||||||
auth_api_url = VITE_APP_API_EXT_BASE_URL
|
|
||||||
}else if (hostname == '10.97.245.96') {
|
|
||||||
auth_api_url = VITE_APP_API_INT_BASE_URL
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: auth_api_url,
|
baseURL: API_BASE_URL,
|
||||||
timeout: 15000, // 请求超时时间
|
timeout: 15000, // 请求超时时间
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -79,16 +59,16 @@ api.interceptors.request.use(
|
|||||||
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理POST请求加密(不加密请求会标记isEncrypted: true)
|
// 处理POST请求加密
|
||||||
if (config.method?.toLowerCase() === 'post' && !config.isEncrypted) {
|
if (config.method?.toLowerCase() === 'post' && !config.isEncrypted) {
|
||||||
try {
|
try {
|
||||||
// 添加用户信息
|
// 添加用户信息
|
||||||
let requestData = { ...config.data };
|
let requestData = { ...config.data };
|
||||||
const storedUserInfo = localStorage.getItem('userInfo'); // 修正存储键名
|
const storedUserInfo = localStorage.getItem('info');
|
||||||
|
|
||||||
if (storedUserInfo) {
|
if (storedUserInfo) {
|
||||||
const userInfo = JSON.parse(crypto.decrypt(storedUserInfo));
|
const info = JSON.parse(crypto.decrypt(storedUserInfo));
|
||||||
requestData['user_id'] = userInfo.userId; // 匹配后端返回的userId字段
|
requestData['user_id'] = info.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加密数据
|
// 加密数据
|
||||||
@@ -119,12 +99,13 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
// 处理业务逻辑错误
|
// 处理业务逻辑错误
|
||||||
const { data } = response;
|
const { data } = response;
|
||||||
// 根据实际后端规范调整(确保与后端code一致)
|
|
||||||
if (data.code == null || ![0, 200].includes(data.code)) {
|
// 根据实际后端规范调整
|
||||||
ElMessage.error(data.message || '操作失败');
|
if (data.code && data.code !== 200) {
|
||||||
return Promise.reject(new Error(data.message || '请求失败'));
|
ElMessage.error(data.message || '操作失败');
|
||||||
|
return Promise.reject(new Error(data.message || '请求失败'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
@@ -142,10 +123,13 @@ api.interceptors.response.use(
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
// 未授权,处理token过期逻辑
|
// 未授权,处理token过期逻辑
|
||||||
if (!router.currentRoute.value.path.includes('/auth')) {
|
ElMessage.error('登录已过期,请重新登录');
|
||||||
ElMessage.error('登录已过期,请重新登录');
|
// 清除登录状态
|
||||||
clearLoginState(); // 使用路由文件中的清除方法
|
Cookies.remove('access_token');
|
||||||
router.push('/auth');
|
localStorage.removeItem('info');
|
||||||
|
// 跳转到登录页(假设使用vue-router)
|
||||||
|
if (window.router) {
|
||||||
|
window.router.push('/');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
@@ -232,7 +216,7 @@ const request = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不加密的POST请求(用于登录等不需要加密的场景)
|
* 不加密的POST请求
|
||||||
* @param {string} url 请求地址
|
* @param {string} url 请求地址
|
||||||
* @param {object} data 请求数据
|
* @param {object} data 请求数据
|
||||||
* @param {object} config 额外配置
|
* @param {object} config 额外配置
|
||||||
@@ -244,7 +228,6 @@ const request = {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
isEncrypted: true, // 标记为已加密,跳过加密处理
|
isEncrypted: true, // 标记为已加密,跳过加密处理
|
||||||
showLoading: config.showLoading !== false, // 默认为显示加载状态
|
|
||||||
...config
|
...config
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,30 @@
|
|||||||
import CryptoJS from 'crypto-js'; // 假设使用crypto-js库
|
import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
// 密钥必须与加密时完全一致(建议从环境变量读取,避免硬编码)
|
const key = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_APP_KEY);
|
||||||
const SECRET_KEY = import.meta.env.VITE_APP_KEY || 'your-secret-key-32bytes'; // 32字节密钥(AES-256)
|
const iv = CryptoJS.enc.Utf8.parse(import.meta.env.VITE_APP_IV);
|
||||||
const IV = import.meta.env.VITE_APP_IV || '16bytesIvValue!'; // 16字节IV
|
|
||||||
|
|
||||||
// 验证密钥和IV长度(关键修复)
|
const crypto = {
|
||||||
if (SECRET_KEY.length !== 32) {
|
encrypt: (str) => {
|
||||||
console.error(`加密密钥长度错误:需要32字节,实际${SECRET_KEY.length}字节`);
|
const encrypted = CryptoJS.AES.encrypt(str, key, {
|
||||||
}
|
|
||||||
if (IV.length !== 16) {
|
|
||||||
console.error(`IV长度错误:需要16字节,实际${IV.length}字节`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加密函数
|
|
||||||
* @param {string} data - 要加密的字符串(需确保是有效的UTF-8)
|
|
||||||
* @returns {string} 加密后的Base64字符串
|
|
||||||
*/
|
|
||||||
export const encrypt = (data) => {
|
|
||||||
try {
|
|
||||||
// 确保输入是字符串,且是有效的UTF-8
|
|
||||||
if (typeof data !== 'string') {
|
|
||||||
throw new Error('加密数据必须是字符串');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证UTF-8有效性
|
|
||||||
new TextDecoder().decode(new TextEncoder().encode(data));
|
|
||||||
|
|
||||||
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY);
|
|
||||||
const iv = CryptoJS.enc.Utf8.parse(IV);
|
|
||||||
|
|
||||||
const encrypted = CryptoJS.AES.encrypt(
|
|
||||||
data,
|
|
||||||
key,
|
|
||||||
{
|
|
||||||
iv: iv,
|
iv: iv,
|
||||||
mode: CryptoJS.mode.CBC,
|
mode: CryptoJS.mode.CBC,
|
||||||
padding: CryptoJS.pad.Pkcs7
|
padding: CryptoJS.pad.Pkcs7
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 返回Base64格式的密文(避免特殊字符问题)
|
|
||||||
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加密失败:', error);
|
|
||||||
throw new Error(`加密错误: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解密函数
|
|
||||||
* @param {string} encryptedData - 加密后的Base64字符串
|
|
||||||
* @returns {string} 解密后的UTF-8字符串
|
|
||||||
*/
|
|
||||||
export const decrypt = (encryptedData) => {
|
|
||||||
try {
|
|
||||||
// 验证输入是否为字符串
|
|
||||||
if (typeof encryptedData !== 'string' || encryptedData.trim() === '') {
|
|
||||||
throw new Error('解密数据必须是非空字符串');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证Base64有效性(关键修复)
|
|
||||||
if (!/^[A-Za-z0-9+/=]+$/.test(encryptedData)) {
|
|
||||||
throw new Error('解密数据不是有效的Base64格式');
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = CryptoJS.enc.Utf8.parse(SECRET_KEY);
|
|
||||||
const iv = CryptoJS.enc.Utf8.parse(IV);
|
|
||||||
|
|
||||||
// 先将Base64转换为CipherParams对象
|
|
||||||
const cipherParams = CryptoJS.lib.CipherParams.create({
|
|
||||||
ciphertext: CryptoJS.enc.Base64.parse(encryptedData)
|
|
||||||
});
|
});
|
||||||
|
// 将加密结果转换为字节数组,然后转换为十六进制字符串
|
||||||
const decrypted = CryptoJS.AES.decrypt(
|
const hexString = CryptoJS.enc.Hex.stringify(encrypted.ciphertext);
|
||||||
cipherParams,
|
return hexString;
|
||||||
key,
|
},
|
||||||
{
|
decrypt: (hexString) => {
|
||||||
|
// 将十六进制字符串转换回字节数组
|
||||||
|
const ciphertext = CryptoJS.enc.Hex.parse(hexString);
|
||||||
|
const encryptedParams = CryptoJS.lib.CipherParams.create({ ciphertext });
|
||||||
|
const decrypted = CryptoJS.AES.decrypt(encryptedParams, key, {
|
||||||
iv: iv,
|
iv: iv,
|
||||||
mode: CryptoJS.mode.CBC,
|
mode: CryptoJS.mode.CBC,
|
||||||
padding: CryptoJS.pad.Pkcs7
|
padding: CryptoJS.pad.Pkcs7
|
||||||
}
|
});
|
||||||
);
|
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||||
|
|
||||||
// 转换为UTF-8字符串(处理可能的编码错误)
|
|
||||||
const result = decrypted.toString(CryptoJS.enc.Utf8);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new Error('解密结果为空,可能是密钥不匹配或数据损坏');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 再次验证UTF-8有效性
|
|
||||||
new TextDecoder().decode(new TextEncoder().encode(result));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解密失败:', error);
|
|
||||||
throw new Error(`解密错误: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { encrypt, decrypt };
|
export default crypto;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
test
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-container" :class="themeClass">
|
<div class="main-container">
|
||||||
<!-- 左侧菜单栏组件 - 添加主题类传递 -->
|
<!-- 左侧菜单栏组件 -->
|
||||||
<div class="menu-view" :class="{ collapsed: isSidebarCollapsed }">
|
<div class="menu-view">
|
||||||
<MainMenu
|
<MainMenu
|
||||||
|
:menu-items="menuItems"
|
||||||
:collapsed="isSidebarCollapsed"
|
:collapsed="isSidebarCollapsed"
|
||||||
@toggle="isSidebarCollapsed = !isSidebarCollapsed"
|
@toggle="isSidebarCollapsed = !isSidebarCollapsed"
|
||||||
@menu-click="handleMenuChange"
|
@menu-click="handleMenuChange"
|
||||||
:theme-class="themeClass"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧主内容区 -->
|
<!-- 右侧主内容区 -->
|
||||||
<div class="right-view">
|
<div class="right-view">
|
||||||
<div class="top-view">
|
<div class="top-view">
|
||||||
<!-- 主题和语言切换控件 -->
|
|
||||||
<div class="header-controls">
|
|
||||||
<!-- 语言切换 -->
|
|
||||||
<el-select
|
|
||||||
v-model="currentLanguage"
|
|
||||||
size="small"
|
|
||||||
class="language-select"
|
|
||||||
@change="handleLanguageChange"
|
|
||||||
>
|
|
||||||
<el-option label="中文" value="zh-CN"></el-option>
|
|
||||||
<el-option label="English" value="en-US"></el-option>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<!-- 主题切换 -->
|
|
||||||
<el-select
|
|
||||||
v-model="currentTheme"
|
|
||||||
size="small"
|
|
||||||
class="theme-select"
|
|
||||||
@change="handleThemeChange"
|
|
||||||
>
|
|
||||||
<el-option :label="t('theme.light')" value="light"></el-option>
|
|
||||||
<el-option :label="t('theme.dark')" value="dark"></el-option>
|
|
||||||
<el-option :label="t('theme.custom')" value="custom"></el-option>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部用户信息栏组件 -->
|
<!-- 顶部用户信息栏组件 -->
|
||||||
<UserInfoBar
|
<UserInfoBar
|
||||||
:user-info="currentUser"
|
:user-info="currentUser"
|
||||||
@@ -48,197 +22,138 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="connect-view">
|
<div class="connect-view">
|
||||||
<!-- 路由视图放在右侧内容区 -->
|
<!-- 内容显示区 -->
|
||||||
<RouterView />
|
<ContentArea>
|
||||||
|
<template #header>
|
||||||
|
<ContentHeader
|
||||||
|
:title="currentPageTitle"
|
||||||
|
:actions="currentPageActions"
|
||||||
|
@action-click="handlePageAction"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</ContentArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject, watch, nextTick } from 'vue'; // 增加nextTick用于强制刷新
|
// 脚本内容保持不变
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { ElMessage } from 'element-plus';
|
||||||
import { ElMessage, ElNotification } from 'element-plus';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import crypto from '@/utils/crypto';
|
|
||||||
import MainMenu from './layout/MainMenu.vue';
|
import MainMenu from './layout/MainMenu.vue';
|
||||||
import UserInfoBar from './layout/UserInfoBar.vue';
|
import UserInfoBar from './layout/UserInfoBar.vue';
|
||||||
import { getLocale, setLocale } from '@/utils/storage';
|
import ContentArea from './layout/ContentArea.vue';
|
||||||
|
import ContentHeader from './layout/ContentHeader.vue';
|
||||||
|
|
||||||
// 注入主题配置
|
// 模拟API函数(实际项目中应从api文件导入)
|
||||||
const theme = inject('theme', {
|
const getUserInfo = () => Promise.resolve({
|
||||||
currentTheme: 'light',
|
name: '管理员',
|
||||||
switchTheme: () => {}
|
role: '系统管理员',
|
||||||
|
avatar: 'https://picsum.photos/id/64/200/200'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 国际化
|
const logout = () => Promise.resolve();
|
||||||
const { locale, t } = useI18n();
|
|
||||||
|
|
||||||
// 退出登录函数
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem('userInfo');
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
localStorage.removeItem('user_gid');
|
|
||||||
Cookies.remove('access_token');
|
|
||||||
|
|
||||||
await router.push('/auth');
|
|
||||||
window.location.reload();
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('退出登录失败:', error);
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const isSidebarCollapsed = ref(false);
|
const isSidebarCollapsed = ref(false);
|
||||||
const unreadNotifications = ref(3);
|
const unreadNotifications = ref(3);
|
||||||
const currentUser = ref({
|
const currentPageTitle = ref('仪表盘');
|
||||||
name: t('user.unknown'),
|
const currentUser = ref({});
|
||||||
role: t('user.unauthorized'),
|
|
||||||
avatar: 'https://picsum.photos/id/64/200/200',
|
|
||||||
userId: '',
|
|
||||||
departmentId: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// 主题和语言状态
|
|
||||||
const currentTheme = ref(theme.currentTheme);
|
|
||||||
const currentLanguage = ref(getLocale() || 'zh-CN');
|
|
||||||
const themeClass = ref(`theme-${currentTheme.value}`);
|
|
||||||
|
|
||||||
// 路由实例
|
// 路由实例
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 从localStorage获取并解密用户信息
|
// 菜单配置
|
||||||
const loadUserInfo = () => {
|
const menuItems = reactive([
|
||||||
try {
|
{
|
||||||
const encryptedUserInfo = localStorage.getItem('userInfo');
|
label: '仪表盘',
|
||||||
|
icon: 'fa-tachometer',
|
||||||
if (!encryptedUserInfo) {
|
path: '/main/dashboard',
|
||||||
throw new Error(t('error.noUserInfo'));
|
active: true
|
||||||
}
|
},
|
||||||
|
{
|
||||||
if (!/^[A-Za-z0-9+/=]+$/.test(encryptedUserInfo)) {
|
label: '用户管理',
|
||||||
throw new Error(t('error.invalidBase64'));
|
icon: 'fa-users',
|
||||||
}
|
path: '/main/users',
|
||||||
|
active: false,
|
||||||
const decryptedInfo = crypto.decrypt(encryptedUserInfo);
|
children: [
|
||||||
|
{ label: '用户列表', icon: 'fa-list', path: '/main/users/list', active: false },
|
||||||
if (!decryptedInfo || decryptedInfo === '') {
|
{ label: '新增用户', icon: 'fa-plus', path: '/main/users/add', active: false }
|
||||||
throw new Error(t('error.decryptFailed'));
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
let userData;
|
label: '系统设置',
|
||||||
try {
|
icon: 'fa-cog',
|
||||||
userData = JSON.parse(decryptedInfo);
|
path: '/main/settings',
|
||||||
} catch (jsonError) {
|
active: false
|
||||||
throw new Error(`${t('error.jsonParseFailed')}: ${jsonError.message}`);
|
},
|
||||||
}
|
{
|
||||||
|
label: '数据报表',
|
||||||
if (typeof userData !== 'object' || userData === null) {
|
icon: 'fa-bar-chart',
|
||||||
throw new Error(t('error.invalidUserInfo'));
|
path: '/main/reports',
|
||||||
}
|
active: false
|
||||||
|
|
||||||
const userName = userData.userName || userData.loginName || t('user.unknown');
|
|
||||||
const roles = Array.isArray(userData.roles) ? userData.roles : [];
|
|
||||||
const role = roles.length > 0 ? roles[0] : t('user.defaultRole');
|
|
||||||
|
|
||||||
if (userData.userGid) {
|
|
||||||
localStorage.setItem('user_gid', userData.userGid);
|
|
||||||
} else if (userData.groupId) {
|
|
||||||
localStorage.setItem('user_gid', userData.groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: userName,
|
|
||||||
role: role,
|
|
||||||
avatar: 'https://picsum.photos/id/64/200/200',
|
|
||||||
userId: userData.userId || '',
|
|
||||||
departmentId: userData.departmentId || '',
|
|
||||||
loginName: userData.loginName || '',
|
|
||||||
loginTime: userData.loginTime || ''
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载用户信息失败详情:', error);
|
|
||||||
localStorage.removeItem('userInfo');
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
localStorage.removeItem('user_gid');
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
]);
|
||||||
|
|
||||||
|
// 当前页面操作按钮
|
||||||
|
const currentPageActions = ref([
|
||||||
|
{ label: '新增', icon: 'fa-plus', action: 'add' },
|
||||||
|
{ label: '导出', icon: 'fa-download', action: 'export' },
|
||||||
|
{ label: '刷新', icon: 'fa-refresh', action: 'refresh' }
|
||||||
|
]);
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
locale.value = currentLanguage.value;
|
const userData = await getUserInfo();
|
||||||
|
|
||||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
|
||||||
const hasToken = !!Cookies.get('access_token');
|
|
||||||
|
|
||||||
if (!isLoggedIn || !hasToken) {
|
|
||||||
ElMessage.warning(t('auth.sessionExpired'));
|
|
||||||
await router.push('/auth');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = loadUserInfo();
|
|
||||||
currentUser.value = userData;
|
currentUser.value = userData;
|
||||||
|
setActiveMenuByRoute(router.currentRoute.value.path);
|
||||||
ElNotification.success({
|
|
||||||
title: t('auth.loginSuccess'),
|
|
||||||
message: `${t('auth.welcomeBack')},${userData.name}`,
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(`${t('error.loadUserFailed')}: ${error.message}`);
|
ElMessage.error('加载用户信息失败');
|
||||||
console.error('初始化用户信息时出错:', error);
|
console.error(error);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
router.push('/auth');
|
|
||||||
}, 2000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理主题切换 - 增加强制刷新逻辑
|
// 根据路由设置活跃菜单
|
||||||
const handleThemeChange = async (newTheme) => {
|
const setActiveMenuByRoute = (path) => {
|
||||||
if (theme.switchTheme) {
|
menuItems.forEach(item => {
|
||||||
theme.switchTheme(newTheme);
|
item.active = item.path === path;
|
||||||
currentTheme.value = newTheme;
|
|
||||||
themeClass.value = `theme-${newTheme}`;
|
|
||||||
|
|
||||||
// 强制DOM更新,确保所有子组件感知主题变化
|
if (item.children && item.children.length) {
|
||||||
await nextTick();
|
item.children.forEach(subItem => {
|
||||||
// 触发重绘技巧:短暂移除再添加主题类
|
subItem.active = subItem.path === path;
|
||||||
const tempClass = themeClass.value;
|
});
|
||||||
themeClass.value = '';
|
item.expanded = item.children.some(sub => sub.active);
|
||||||
await nextTick();
|
}
|
||||||
themeClass.value = tempClass;
|
});
|
||||||
|
|
||||||
ElMessage.success(t('theme.themeSwitched', { theme: newTheme }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理语言切换
|
|
||||||
const handleLanguageChange = (newLocale) => {
|
|
||||||
locale.value = newLocale;
|
|
||||||
setLocale(newLocale);
|
|
||||||
currentLanguage.value = newLocale;
|
|
||||||
ElMessage.success(t('common.languageChanged'));
|
|
||||||
|
|
||||||
if (!currentUser.value.name || currentUser.value.name === '未知用户') {
|
|
||||||
currentUser.value.name = t('user.unknown');
|
|
||||||
}
|
|
||||||
if (!currentUser.value.role || currentUser.value.role === '未授权') {
|
|
||||||
currentUser.value.role = t('user.unauthorized');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理菜单切换
|
// 处理菜单切换
|
||||||
const handleMenuChange = (path) => {
|
const handleMenuChange = (path) => {
|
||||||
if (path && path !== router.currentRoute.value.path) {
|
currentPageTitle.value = menuItems.find(item => item.path === path)?.label || '页面';
|
||||||
router.push(path);
|
router.push(path);
|
||||||
|
setActiveMenuByRoute(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理页面操作
|
||||||
|
const handlePageAction = (action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'add':
|
||||||
|
ElMessage.info('新增操作');
|
||||||
|
break;
|
||||||
|
case 'export':
|
||||||
|
ElMessage.info('导出操作');
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
ElMessage.success('已刷新');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,130 +161,68 @@ const handleMenuChange = (path) => {
|
|||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await logout();
|
await logout();
|
||||||
ElMessage.success(t('auth.logoutSuccess'));
|
router.push('/login');
|
||||||
|
ElMessage.success('退出登录成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(t('auth.logoutFailed'));
|
ElMessage.error('退出登录失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理通知点击
|
// 处理通知点击
|
||||||
const handleNotificationClick = () => {
|
const handleNotificationClick = () => {
|
||||||
ElMessage.info(t('notification.openCenter'));
|
router.push('/main/notifications');
|
||||||
|
unreadNotifications.value = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理设置点击
|
// 处理设置点击
|
||||||
const handleSettingsClick = () => {
|
const handleSettingsClick = () => {
|
||||||
router.push('/profile');
|
router.push('/main/settings');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听主题变化 - 确保菜单同步更新
|
|
||||||
watch(
|
|
||||||
() => theme.currentTheme,
|
|
||||||
async (newTheme) => {
|
|
||||||
currentTheme.value = newTheme;
|
|
||||||
themeClass.value = `theme-${newTheme}`;
|
|
||||||
// 主题变化时强制菜单重绘
|
|
||||||
await nextTick();
|
|
||||||
const menuEl = document.querySelector('.menu-view');
|
|
||||||
if (menuEl) {
|
|
||||||
menuEl.style.display = 'none';
|
|
||||||
await nextTick();
|
|
||||||
menuEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 关键修复:设置html和body的高度基准 */
|
||||||
|
:root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh; /* 使用视口高度确保占满屏幕 */
|
||||||
display: flex;
|
display: flex; /* 使用flex布局避免定位问题 */
|
||||||
overflow: hidden;
|
overflow: hidden; /* 防止页面溢出 */
|
||||||
background-color: var(--color-background);
|
|
||||||
color: var(--color-text);
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关键修复:确保主题类能影响菜单内部样式 */
|
|
||||||
.menu-view {
|
.menu-view {
|
||||||
|
/* 移除absolute定位,使用flex布局 */
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: 1px solid var(--color-border);
|
border: 1px solid #ff0000;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0; /* 防止菜单被压缩 */
|
||||||
transition: width 0.3s ease;
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 穿透到子组件的菜单样式 - 解决scoped隔离问题 */
|
|
||||||
:deep(.menu-view) {
|
|
||||||
--menu-bg-color: var(--color-surface);
|
|
||||||
--menu-text-color: var(--color-text);
|
|
||||||
--menu-hover-color: var(--color-surface-alt);
|
|
||||||
--menu-active-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-view.collapsed {
|
|
||||||
width: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-view {
|
.right-view {
|
||||||
flex: 1;
|
flex: 1; /* 自动填充剩余空间 */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column; /* 垂直排列顶部和内容区 */
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-view {
|
.top-view {
|
||||||
height: 60px;
|
height: 60px; /* 固定顶部高度 */
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid #ccc;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-select, .theme-select {
|
|
||||||
width: 110px;
|
|
||||||
/* 确保选择器本身也响应主题 */
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
border-color: var(--color-border) !important;
|
|
||||||
color: var(--color-text) !important;
|
|
||||||
}
|
|
||||||
:deep(.el-select-dropdown) {
|
|
||||||
background-color: var(--color-surface) !important;
|
|
||||||
border-color: var(--color-border) !important;
|
|
||||||
:deep(.el-select-dropdown__item) {
|
|
||||||
color: var(--color-text) !important;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
}
|
|
||||||
&.selected {
|
|
||||||
color: var(--color-primary) !important;
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-view {
|
.connect-view {
|
||||||
flex: 1;
|
flex: 1; /* 填充剩余高度 */
|
||||||
overflow: auto;
|
overflow: auto; /* 内容溢出时可滚动 */
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
background-color: var(--color-background);
|
|
||||||
border: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
42
src/views/mainpage/layout/ContentHeader.vue
Normal file
42
src/views/mainpage/layout/ContentHeader.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-header bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between">
|
||||||
|
<h1 class="page-title text-xl font-semibold">{{ title }}</h1>
|
||||||
|
|
||||||
|
<div class="page-actions flex items-center space-x-2 mt-2 sm:mt-0">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
import { ElButton } from 'element-plus';
|
||||||
|
|
||||||
|
// 定义属性
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['action-click']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-header {
|
||||||
|
@apply shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-actions {
|
||||||
|
@apply flex-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,85 +1,188 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-menu-container" :class="themeClass">
|
<el-menu
|
||||||
<!-- 侧边栏菜单 -->
|
:default-active="activeMenu"
|
||||||
<Sidebar
|
class="sidebar-menu"
|
||||||
:is-collapse="sidebarCollapse"
|
mode="vertical"
|
||||||
@toggle-collapse="handleSidebarToggle"
|
@select="handleMenuSelect"
|
||||||
class="sidebar-menu"
|
background-color=""
|
||||||
/>
|
text-color="#94a3b8"
|
||||||
</div>
|
active-text-color="#ffffff"
|
||||||
|
:collapse="isCollapse"
|
||||||
|
:collapse-transition="false"
|
||||||
|
>
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<template v-if="loading">
|
||||||
|
<el-menu-item v-for="i in 3" :key="i">
|
||||||
|
<el-skeleton active :width="isCollapse ? '24px' : '100%'" />
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<template v-else-if="error">
|
||||||
|
<el-menu-item disabled>
|
||||||
|
<el-alert
|
||||||
|
title="菜单加载失败"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
class="error-alert"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
@click="fetchMenuData"
|
||||||
|
class="retry-btn"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</el-button>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 菜单内容 -->
|
||||||
|
<template v-else>
|
||||||
|
<menu-item
|
||||||
|
v-for="item in menuData"
|
||||||
|
:key="item.id"
|
||||||
|
:menu-item="item"
|
||||||
|
:is-collapse="isCollapse"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject, computed } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
// 导入Sidebar组件
|
import MenuItem from './MenuItem.vue';
|
||||||
import Sidebar from '/src/components/menu/Sidebar.vue'
|
import { request } from '@/utils/api'; // 假设request工具已存在
|
||||||
|
import { Menu } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
// 注入主题配置
|
// 组件属性
|
||||||
const theme = inject('theme', {
|
const props = defineProps({
|
||||||
currentTheme: 'light',
|
isCollapse: {
|
||||||
switchTheme: () => {}
|
type: Boolean,
|
||||||
})
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 国际化
|
// 状态管理
|
||||||
const { t } = useI18n()
|
const loading = ref(true);
|
||||||
|
const error = ref(false);
|
||||||
|
const rawMenuData = ref([]);
|
||||||
|
const menuData = ref([]);
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// 控制侧边栏折叠状态
|
// 当前激活的菜单
|
||||||
const sidebarCollapse = ref(false)
|
const activeMenu = computed(() => {
|
||||||
|
return route.path;
|
||||||
|
});
|
||||||
|
|
||||||
// 计算主题类名
|
// 监听路由变化,更新激活菜单
|
||||||
const themeClass = computed(() => `theme-${theme.currentTheme}`)
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
|
// 可以在这里添加额外的逻辑
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 处理侧边栏折叠/展开切换
|
// 处理菜单选择
|
||||||
const handleSidebarToggle = () => {
|
const handleMenuSelect = (key) => {
|
||||||
sidebarCollapse.value = !sidebarCollapse.value
|
// 如果是外部链接
|
||||||
}
|
if (key.startsWith('http://') || key.startsWith('https://')) {
|
||||||
|
window.open(key, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内部路由跳转
|
||||||
|
if (key && key !== route.path) {
|
||||||
|
router.push(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将扁平数据转换为树形结构
|
||||||
|
const buildMenuTree = (items) => {
|
||||||
|
const map = new Map();
|
||||||
|
const tree = [];
|
||||||
|
|
||||||
|
// 首先将所有项存入map
|
||||||
|
items.forEach(item => {
|
||||||
|
map.set(item.id, { ...item, children: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建树形结构
|
||||||
|
items.forEach(item => {
|
||||||
|
const current = map.get(item.id);
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
if (item.parent_id === null || !map.has(item.parent_id)) {
|
||||||
|
// 顶级菜单
|
||||||
|
tree.push(current);
|
||||||
|
} else {
|
||||||
|
// 子菜单
|
||||||
|
const parent = map.get(item.parent_id);
|
||||||
|
if (parent) {
|
||||||
|
parent.children.push(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按menu_code排序,确保菜单顺序正确
|
||||||
|
return tree.sort((a, b) => a.menu_code.localeCompare(b.menu_code));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从API获取菜单数据
|
||||||
|
const fetchMenuData = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = false;
|
||||||
|
|
||||||
|
const response = await request.postWithoutEncryption(
|
||||||
|
'/api/i4/v1/get/menu.php',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 假设后端返回的数据结构是 { code: 200, data: MenuItem[] }
|
||||||
|
if (response.code === 200 && Array.isArray(response.data)) {
|
||||||
|
rawMenuData.value = response.data;
|
||||||
|
menuData.value = buildMenuTree(rawMenuData.value);
|
||||||
|
} else {
|
||||||
|
throw new Error('菜单数据格式不正确');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取菜单数据失败:', err);
|
||||||
|
error.value = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时获取菜单数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchMenuData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-menu-container {
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--color-background);
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--color-background);
|
|
||||||
color: var(--color-text);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu {
|
.sidebar-menu {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
background-color: var(--color-surface);
|
border-right: none;
|
||||||
border-right: 1px solid var(--color-border);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 适配不同主题的滚动条样式 */
|
.error-alert {
|
||||||
:deep(.sidebar-menu ::-webkit-scrollbar) {
|
margin: 10px;
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.sidebar-menu ::-webkit-scrollbar-track) {
|
.retry-btn {
|
||||||
background: var(--color-surface-alt);
|
margin-left: 10px;
|
||||||
border-radius: 3px;
|
color: #409eff;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.sidebar-menu ::-webkit-scrollbar-thumb) {
|
|
||||||
background: var(--color-border);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.sidebar-menu ::-webkit-scrollbar-thumb:hover) {
|
|
||||||
background: var(--color-primary);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
68
src/views/mainpage/layout/MenuItem.vue
Normal file
68
src/views/mainpage/layout/MenuItem.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!hasChildren">
|
||||||
|
<el-menu-item
|
||||||
|
:index="menuItem.menu_url"
|
||||||
|
:disabled="menuItem.disabled"
|
||||||
|
:class="{ 'menu-item': true }"
|
||||||
|
>
|
||||||
|
<el-icon v-if="menuItem.menu_grade === 1 && !isCollapse">
|
||||||
|
<Menu />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ menuItem.menu_name }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<el-sub-menu
|
||||||
|
:index="menuItem.menu_url"
|
||||||
|
:disabled="menuItem.disabled"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<el-icon v-if="menuItem.menu_grade === 1 && !isCollapse">
|
||||||
|
<Menu />
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ menuItem.menu_name }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<menu-item
|
||||||
|
v-for="child in menuItem.children"
|
||||||
|
:key="child.id"
|
||||||
|
:menu-item="child"
|
||||||
|
:is-collapse="isCollapse"
|
||||||
|
/>
|
||||||
|
</el-sub-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, computed } from 'vue';
|
||||||
|
import { Menu } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
// 接收父组件传递的属性
|
||||||
|
const props = defineProps({
|
||||||
|
menuItem: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isCollapse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否有子菜单
|
||||||
|
const hasChildren = computed(() => {
|
||||||
|
return props.menuItem.children && props.menuItem.children.length > 0;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,82 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="user-info-bar" :class="themeClass">
|
<header class="user-info-bar bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 h-16 flex items-center justify-between px-6 shadow-sm">
|
||||||
<!-- 通知按钮 -->
|
<!-- 用户信息 -->
|
||||||
<div class="action-buttons">
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="relative">
|
||||||
|
<img
|
||||||
|
:src="userInfo.avatar || defaultAvatar"
|
||||||
|
alt="用户头像"
|
||||||
|
class="w-10 h-10 rounded-full object-cover border-2 border-white dark:border-gray-700 shadow-sm"
|
||||||
|
>
|
||||||
|
<span class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="font-medium">{{ userInfo.name || '用户' }}</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400">{{ userInfo.role || '普通用户' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
<button
|
<button
|
||||||
class="action-btn notification-btn"
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors relative"
|
||||||
@click="$emit('notification-click')"
|
@click="$emit('notification-click')"
|
||||||
aria-label="通知"
|
aria-label="通知"
|
||||||
:title="t('user.notifications')"
|
|
||||||
>
|
>
|
||||||
<BellIcon class="icon" />
|
<i class="fa fa-bell-o text-lg"></i>
|
||||||
<span v-if="unreadCount > 0" class="notification-badge">
|
<span v-if="unreadCount > 0" class="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
||||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户信息 - 右侧布局 -->
|
|
||||||
<div class="user-info">
|
|
||||||
<!-- 头像与状态 -->
|
|
||||||
<div class="avatar-container" @click.stop="toggleMenu">
|
|
||||||
<img
|
|
||||||
:src="userInfo.avatar || defaultAvatar"
|
|
||||||
:alt="t('user.profile')"
|
|
||||||
class="avatar-img"
|
|
||||||
>
|
|
||||||
<span class="status-indicator" :title="t('user.online')"></span>
|
|
||||||
|
|
||||||
<!-- 弹出菜单 -->
|
|
||||||
<el-menu
|
|
||||||
v-if="menuVisible"
|
|
||||||
class="user-dropdown-menu"
|
|
||||||
mode="vertical"
|
|
||||||
@select="handleMenuSelect"
|
|
||||||
>
|
|
||||||
<el-menu-item index="profile">
|
|
||||||
<UserIcon class="icon mr-2" />{{ t('user.profile') }}
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="settings">
|
|
||||||
<SettingIcon class="icon mr-2" />{{ t('user.settings') }}
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="logout" class="logout-item">
|
|
||||||
<ArrowRightIcon class="icon mr-2" />{{ t('user.logout') }}
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<button
|
||||||
<div class="user-text">
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
<div class="user-name-text">{{ userInfo.name || t('user.defaultName') }}</div>
|
@click="$emit('settings-click')"
|
||||||
<div class="user-role-text">{{ userInfo.role || t('user.defaultRole') }}</div>
|
aria-label="设置"
|
||||||
</div>
|
>
|
||||||
|
<i class="fa fa-cog text-lg"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
@click="$emit('logout')"
|
||||||
|
aria-label="退出登录"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sign-out text-lg"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, onMounted, onBeforeUnmount, inject, computed } from 'vue';
|
import { defineProps, defineEmits, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
// Element Plus图标
|
|
||||||
import { Bell, User, ArrowRight, Setting } from '@element-plus/icons-vue';
|
|
||||||
|
|
||||||
// 图标组件定义
|
// 定义属性
|
||||||
const BellIcon = Bell;
|
|
||||||
const UserIcon = User;
|
|
||||||
const ArrowRightIcon = ArrowRight;
|
|
||||||
const SettingIcon = Setting;
|
|
||||||
|
|
||||||
// 注入主题配置
|
|
||||||
const theme = inject('theme', { currentTheme: 'light' });
|
|
||||||
|
|
||||||
// 计算主题类名
|
|
||||||
const themeClass = computed(() => `theme-${theme.currentTheme}`);
|
|
||||||
|
|
||||||
// 路由实例
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// 组件属性
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
userInfo: {
|
userInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -88,235 +64,18 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件事件
|
// 定义事件
|
||||||
const emit = defineEmits(['logout', 'notification-click', 'settings-click']);
|
const emit = defineEmits(['logout', 'notification-click', 'settings-click']);
|
||||||
|
|
||||||
// 状态管理
|
// 默认头像
|
||||||
const menuVisible = ref(false);
|
|
||||||
const defaultAvatar = ref('https://picsum.photos/id/64/200/200');
|
const defaultAvatar = ref('https://picsum.photos/id/64/200/200');
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
// 切换菜单显示状态
|
|
||||||
const toggleMenu = () => {
|
|
||||||
menuVisible.value = !menuVisible.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理菜单选项
|
|
||||||
const handleMenuSelect = (index) => {
|
|
||||||
if (index === 'profile') {
|
|
||||||
router.push('/profile');
|
|
||||||
emit('settings-click');
|
|
||||||
} else if (index === 'settings') {
|
|
||||||
router.push('/settings');
|
|
||||||
emit('settings-click');
|
|
||||||
} else if (index === 'logout') {
|
|
||||||
emit('logout');
|
|
||||||
}
|
|
||||||
menuVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击外部关闭菜单
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
const avatarContainer = document.querySelector('.avatar-container');
|
|
||||||
if (avatarContainer && !avatarContainer.contains(event.target)) {
|
|
||||||
menuVisible.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生命周期钩子
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener('click', handleClickOutside);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 组件样式完全依赖全局主题变量 */
|
|
||||||
.icon {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info-bar {
|
.user-info-bar {
|
||||||
height: 60px;
|
@apply z-10;
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
box-shadow: 0 1px 3px var(--color-shadow);
|
|
||||||
z-index: 10;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
img {width: 40px; height: 40px;}
|
||||||
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container {
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-img {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-cover: cover;
|
|
||||||
border: 2px solid var(--color-surface);
|
|
||||||
box-shadow: 0 1px 2px var(--color-shadow);
|
|
||||||
transition: transform 0.2s ease, border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container:hover .avatar-img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid var(--color-surface);
|
|
||||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.user-text {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name-text {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-text);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-role-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text);
|
|
||||||
opacity: 0.8;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
transition: color 0.3s ease, opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--color-text);
|
|
||||||
opacity: 0.8;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
background-color: var(--color-surface-alt);
|
|
||||||
color: var(--color-primary);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: var(--color-error, #ef4444);
|
|
||||||
color: white;
|
|
||||||
font-size: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
right: -90px;
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
min-width: 160px;
|
|
||||||
z-index: 1000;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 2px 10px var(--color-shadow);
|
|
||||||
animation: fadeIn 0.2s ease;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 下拉菜单主题适配 */
|
|
||||||
:deep(.el-menu) {
|
|
||||||
background-color: var(--color-surface) !important;
|
|
||||||
border-color: var(--color-border) !important;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item) {
|
|
||||||
color: var(--color-text) !important;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item:hover) {
|
|
||||||
background-color: var(--color-surface-alt) !important;
|
|
||||||
color: var(--color-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.logout-item) {
|
|
||||||
color: var(--color-error, #ef4444) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.logout-item:hover) {
|
|
||||||
background-color: rgba(239, 68, 68, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画效果 */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(-5px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
User Profile Page
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
History
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
UUT Report
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -6,7 +6,6 @@ import Components from 'unplugin-vue-components/vite'
|
|||||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/sbu4i4/',
|
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
|
|||||||
Reference in New Issue
Block a user