更新
This commit is contained in:
@@ -5,4 +5,4 @@ indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
max_line_length = 260
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.13.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.26",
|
||||
|
||||
154
src/assets/css/auth.css
Normal file
154
src/assets/css/auth.css
Normal file
@@ -0,0 +1,154 @@
|
||||
/* 认证页面统一样式 */
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 50%);
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.auth-header .logo {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
font-size: 14px;
|
||||
color: #7f8c8d;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-actions .forgot-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-actions .forgot-link:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.auth-footer .link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-footer .link:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.auth-footer .text {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.code-button {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #667eea;
|
||||
background: transparent;
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.code-button:hover:not(:disabled) {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
color: #764ba2;
|
||||
border-color: #764ba2;
|
||||
}
|
||||
|
||||
.code-button:disabled {
|
||||
border-color: #dcdfe6;
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.auth-card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
76
src/assets/css/nprogress.css
Normal file
76
src/assets/css/nprogress.css
Normal file
@@ -0,0 +1,76 @@
|
||||
/* 自定义 NProgress 样式 */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: linear-gradient(90deg, #00d4ff 0%, #7c4dff 100%);
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 螺旋加载器样式 */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow:
|
||||
0 0 10px #00d4ff,
|
||||
0 0 5px #7c4dff;
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
/* 进度条动画 */
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 如果启用加载器时的样式 */
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
border-top-color: #00d4ff;
|
||||
border-left-color: #7c4dff;
|
||||
border-radius: 50%;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
/* 进度条闪烁效果 */
|
||||
@keyframes nprogress-flicker {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
animation: nprogress-flicker 0.5s ease-in-out;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
app_title: 'vueadmin',
|
||||
DASHBOARD_URL: '/home',
|
||||
baseURL: ''
|
||||
}
|
||||
|
||||
18
src/config/routes.js
Normal file
18
src/config/routes.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 用户静态路由配置
|
||||
* 这些路由会根据用户角色进行过滤后添加到路由中
|
||||
*/
|
||||
const userRoutes = [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: 'home',
|
||||
meta: {
|
||||
title: 'dashboard',
|
||||
icon: 'el-icon-odometer',
|
||||
role: ['admin']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default userRoutes
|
||||
@@ -70,7 +70,47 @@ export default {
|
||||
usernamePlaceholder: 'Please enter username',
|
||||
passwordPlaceholder: 'Please enter password',
|
||||
noAccount: "Don't have an account?",
|
||||
registerNow: 'Register Now'
|
||||
registerNow: 'Register Now',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
rememberMe: 'Remember Me'
|
||||
},
|
||||
register: {
|
||||
title: 'User Registration',
|
||||
subtitle: 'Create your account and get started',
|
||||
registerButton: 'Register',
|
||||
registerSuccess: 'Registration Successful',
|
||||
registerFailed: 'Registration Failed',
|
||||
usernamePlaceholder: 'Please enter username',
|
||||
emailPlaceholder: 'Please enter email address',
|
||||
passwordPlaceholder: 'Please enter password',
|
||||
confirmPasswordPlaceholder: 'Please enter password again',
|
||||
usernameRule: 'Username length between 3 to 20 characters',
|
||||
emailRule: 'Please enter a valid email address',
|
||||
passwordRule: 'Password length between 6 to 20 characters',
|
||||
agreeRule: 'Please agree to the user agreement',
|
||||
agreeTerms: 'I have read and agree to the',
|
||||
terms: 'User Agreement',
|
||||
hasAccount: 'Already have an account?',
|
||||
loginNow: 'Login Now'
|
||||
},
|
||||
resetPassword: {
|
||||
title: 'Reset Password',
|
||||
subtitle: 'Reset your password via email verification code',
|
||||
resetButton: 'Reset Password',
|
||||
resetSuccess: 'Password reset successful',
|
||||
resetFailed: 'Reset failed',
|
||||
emailPlaceholder: 'Please enter email address',
|
||||
codePlaceholder: 'Please enter verification code',
|
||||
newPasswordPlaceholder: 'Please enter new password',
|
||||
confirmPasswordPlaceholder: 'Please enter new password again',
|
||||
emailRule: 'Please enter a valid email address',
|
||||
codeRule: 'Verification code must be 6 characters',
|
||||
passwordRule: 'Password length between 6 to 20 characters',
|
||||
sendCode: 'Send Code',
|
||||
codeSent: 'Verification code has been sent to your email',
|
||||
resendCode: 'Resend in {seconds} seconds',
|
||||
sendCodeFirst: 'Please enter email address first',
|
||||
backToLogin: 'Back to Login'
|
||||
},
|
||||
layout: {
|
||||
toggleSidebar: 'Toggle Sidebar',
|
||||
|
||||
@@ -70,7 +70,47 @@ export default {
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
noAccount: '还没有账户?',
|
||||
registerNow: '立即注册'
|
||||
registerNow: '立即注册',
|
||||
forgotPassword: '忘记密码?',
|
||||
rememberMe: '记住我'
|
||||
},
|
||||
register: {
|
||||
title: '用户注册',
|
||||
subtitle: '创建您的账户,开始使用',
|
||||
registerButton: '注册',
|
||||
registerSuccess: '注册成功',
|
||||
registerFailed: '注册失败',
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
emailPlaceholder: '请输入邮箱地址',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
confirmPasswordPlaceholder: '请再次输入密码',
|
||||
usernameRule: '用户名长度在 3 到 20 个字符',
|
||||
emailRule: '请输入正确的邮箱地址',
|
||||
passwordRule: '密码长度在 6 到 20 个字符',
|
||||
agreeRule: '请同意用户协议',
|
||||
agreeTerms: '我已阅读并同意',
|
||||
terms: '用户协议',
|
||||
hasAccount: '已有账户?',
|
||||
loginNow: '立即登录'
|
||||
},
|
||||
resetPassword: {
|
||||
title: '重置密码',
|
||||
subtitle: '通过邮箱验证码重置您的密码',
|
||||
resetButton: '重置密码',
|
||||
resetSuccess: '密码重置成功',
|
||||
resetFailed: '重置失败',
|
||||
emailPlaceholder: '请输入邮箱地址',
|
||||
codePlaceholder: '请输入验证码',
|
||||
newPasswordPlaceholder: '请输入新密码',
|
||||
confirmPasswordPlaceholder: '请再次输入新密码',
|
||||
emailRule: '请输入正确的邮箱地址',
|
||||
codeRule: '验证码长度为6位',
|
||||
passwordRule: '密码长度在 6 到 20 个字符',
|
||||
sendCode: '发送验证码',
|
||||
codeSent: '验证码已发送到您的邮箱',
|
||||
resendCode: '{seconds}秒后重新发送',
|
||||
sendCodeFirst: '请先输入邮箱地址',
|
||||
backToLogin: '返回登录'
|
||||
},
|
||||
layout: {
|
||||
toggleSidebar: '切换侧边栏',
|
||||
|
||||
401
src/layouts/other/404.vue
Normal file
401
src/layouts/other/404.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<div class="content-wrapper">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-text">
|
||||
<h1>页面未找到</h1>
|
||||
<p>抱歉,您访问的页面不存在或已被移除</p>
|
||||
</div>
|
||||
<div class="error-illustration">
|
||||
<div class="planet"></div>
|
||||
<div class="star star-1"></div>
|
||||
<div class="star star-2"></div>
|
||||
<div class="star star-3"></div>
|
||||
<div class="rocket">
|
||||
<div class="rocket-body"></div>
|
||||
<div class="rocket-window"></div>
|
||||
<div class="rocket-flame"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" @click="goHome">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||
</svg>
|
||||
返回首页
|
||||
</button>
|
||||
<button class="btn btn-secondary" @click="goBack">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
返回上一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0c1929 0%, #1a237e 50%, #0d47a1 100%);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 星空背景 */
|
||||
.not-found::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(2px 2px at 40px 70px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(1px 1px at 90px 40px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(2px 2px at 160px 120px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(1px 1px at 230px 80px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(2px 2px at 300px 150px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(1px 1px at 370px 200px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(2px 2px at 450px 50px, #ffffff, rgba(0, 0, 0, 0)),
|
||||
radial-gradient(1px 1px at 520px 180px, #ffffff, rgba(0, 0, 0, 0));
|
||||
background-size: 550px 250px;
|
||||
animation: stars 50s linear infinite;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@keyframes stars {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 550px 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 600px;
|
||||
animation: fadeIn 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
text-shadow: 0 0 60px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.error-text h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.error-text p {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-illustration {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 40px;
|
||||
}
|
||||
|
||||
.planet {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow:
|
||||
0 0 60px rgba(102, 126, 234, 0.6),
|
||||
inset -10px -10px 20px rgba(0, 0, 0, 0.2);
|
||||
animation: rotatePlanet 20s linear infinite;
|
||||
}
|
||||
|
||||
.planet::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotateX(75deg);
|
||||
}
|
||||
|
||||
.planet::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
top: 20%;
|
||||
left: 30%;
|
||||
}
|
||||
|
||||
@keyframes rotatePlanet {
|
||||
from {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: twinkle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.star-1 {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
top: 20%;
|
||||
right: 10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.star-2 {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
top: 60%;
|
||||
left: 5%;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.star-3 {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
bottom: 10%;
|
||||
right: 20%;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.rocket {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
right: 0;
|
||||
animation: flyRocket 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.rocket-body {
|
||||
width: 20px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rocket-window {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: inset -2px -2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.rocket-flame {
|
||||
width: 12px;
|
||||
height: 20px;
|
||||
background: linear-gradient(to bottom, #feca57, #ff6b6b, transparent);
|
||||
position: absolute;
|
||||
bottom: -18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 50% 50% 20% 20%;
|
||||
animation: flame 0.3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes flame {
|
||||
0% {
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 30px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flyRocket {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotate(-15deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-15px) rotate(-10deg);
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 25px rgba(0, 212, 255, 0.6);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-code {
|
||||
font-size: 120px;
|
||||
}
|
||||
|
||||
.error-text h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.error-text p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-illustration {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
288
src/layouts/other/empty.vue
Normal file
288
src/layouts/other/empty.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '当前页面暂无相关数据,您可以稍后再来查看'
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '刷新页面'
|
||||
}
|
||||
})
|
||||
|
||||
const handleAction = () => {
|
||||
router.go(0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-content">
|
||||
<div class="empty-illustration">
|
||||
<div class="illustration-wrapper">
|
||||
<svg class="empty-icon" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="boxGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color: #667eea; stop-opacity: 0.2" />
|
||||
<stop offset="100%" style="stop-color: #764ba2; stop-opacity: 0.2" />
|
||||
</linearGradient>
|
||||
<linearGradient id="searchGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color: #00d4ff; stop-opacity: 1" />
|
||||
<stop offset="100%" style="stop-color: #7c4dff; stop-opacity: 1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景圆 -->
|
||||
<circle cx="100" cy="100" r="80" fill="url(#boxGradient)" />
|
||||
|
||||
<!-- 搜索放大镜 -->
|
||||
<g transform="translate(70, 70)">
|
||||
<circle cx="30" cy="30" r="25" stroke="url(#searchGradient)" stroke-width="4" fill="none" />
|
||||
<line x1="50" y1="50" x2="70" y2="70" stroke="url(#searchGradient)" stroke-width="4"
|
||||
stroke-linecap="round" />
|
||||
</g>
|
||||
|
||||
<!-- 小装饰元素 -->
|
||||
<circle cx="50" cy="60" r="4" fill="#667eea" opacity="0.6">
|
||||
<animate attributeName="cy" values="60;55;60" dur="2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="150" cy="80" r="3" fill="#764ba2" opacity="0.6">
|
||||
<animate attributeName="cy" values="80;75;80" dur="1.5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
<circle cx="60" cy="140" r="5" fill="#00d4ff" opacity="0.5">
|
||||
<animate attributeName="cy" values="140;145;140" dur="2.5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-text">
|
||||
<h3 class="empty-title">{{ title }}</h3>
|
||||
<p class="empty-description">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="showAction" class="empty-action">
|
||||
<button class="action-btn" @click="handleAction">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M23 4v6h-6"></path>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.03) 0%, rgba(118, 75, 162, 0.03) 100%);
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 装饰性背景 */
|
||||
.empty-state::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(2px 2px at 20px 30px, rgba(102, 126, 234, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 40px 70px, rgba(118, 75, 162, 0.3), transparent),
|
||||
radial-gradient(1px 1px at 90px 40px, rgba(0, 212, 255, 0.3), transparent),
|
||||
radial-gradient(2px 2px at 160px 120px, rgba(102, 126, 234, 0.3), transparent);
|
||||
background-size: 200px 150px;
|
||||
animation: floatBg 20s linear infinite;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes floatBg {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 200px 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 480px;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-illustration {
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.illustration-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
animation: floatIcon 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes floatIcon {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.empty-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.empty-state {
|
||||
min-height: 300px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.empty-state {
|
||||
padding: 30px 16px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import pinia from './stores'
|
||||
@@ -8,6 +10,7 @@ import { useI18nStore } from './stores/modules/i18n'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
|
||||
@@ -1,399 +1,147 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/useI18n'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
|
||||
import '@/assets/css/auth.css'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const handleLogin = () => {
|
||||
loading.value = true
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false
|
||||
})
|
||||
|
||||
// 模拟登录请求
|
||||
setTimeout(() => {
|
||||
console.log('登录信息:', loginForm.value)
|
||||
loading.value = false
|
||||
// 这里可以添加实际的登录逻辑
|
||||
alert(t('login.loginSuccess') + '!(模拟)')
|
||||
}, 1000)
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: t('login.usernamePlaceholder'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: t('form.minLength', { min: 3, max: 20 }), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('login.passwordPlaceholder'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('form.minLength', { min: 6 }), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
await loginFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟登录请求
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟登录成功
|
||||
const mockUserInfo = {
|
||||
id: 1,
|
||||
username: loginForm.username,
|
||||
nickname: '管理员',
|
||||
email: 'admin@example.com',
|
||||
role: ['admin'],
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
const mockToken = 'mock-token-' + Date.now()
|
||||
const mockRefreshToken = 'mock-refresh-token-' + Date.now()
|
||||
|
||||
userStore.setToken(mockToken)
|
||||
userStore.setRefreshToken(mockRefreshToken)
|
||||
userStore.setUserInfo(mockUserInfo)
|
||||
|
||||
ElMessage.success(t('login.loginSuccess'))
|
||||
|
||||
// 跳转到首页或重定向页面
|
||||
const redirect = router.currentRoute.value.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || t('login.loginFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToRegister = () => {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
const goToResetPassword = () => {
|
||||
router.push('/reset-password')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (loginFormRef.value) {
|
||||
loginFormRef.value.clearValidate()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="language-switcher-wrapper">
|
||||
<div class="auth-container">
|
||||
<div class="language-switcher">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h2>{{ t('login.title') }}</h2>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="logo">
|
||||
<el-icon :size="48">
|
||||
<User />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h1>{{ t('login.title') }}</h1>
|
||||
<p>{{ t('login.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">{{ t('common.username') }}</label>
|
||||
<input id="username" v-model="loginForm.username" type="text"
|
||||
:placeholder="t('login.usernamePlaceholder')" required />
|
||||
</div>
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="auth-form" size="large">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" :placeholder="t('login.usernamePlaceholder')"
|
||||
:prefix-icon="User" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">{{ t('common.password') }}</label>
|
||||
<input id="password" v-model="loginForm.password" type="password"
|
||||
:placeholder="t('login.passwordPlaceholder')" required />
|
||||
</div>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" :placeholder="t('login.passwordPlaceholder')"
|
||||
:prefix-icon="Lock" show-password @keyup.enter="handleLogin" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<label class="remember-me">
|
||||
<input type="checkbox" />
|
||||
<span>{{ t('common.rememberMe') }}</span>
|
||||
</label>
|
||||
<a href="#" class="forgot-password">{{ t('common.forgotPassword') }}</a>
|
||||
</div>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="loginForm.remember">
|
||||
{{ t('login.rememberMe') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<button type="submit" class="login-btn" :disabled="loading">
|
||||
<span v-if="!loading">{{ t('login.loginButton') }}</span>
|
||||
<span v-else>{{ t('common.loading') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="handleLogin">
|
||||
{{ t('login.loginButton') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>{{ t('login.noAccount') }} <a href="#">{{ t('login.registerNow') }}</a></p>
|
||||
<div class="auth-actions">
|
||||
<el-link type="primary" @click="goToResetPassword">
|
||||
{{ t('login.forgotPassword') }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
<span class="text">{{ t('login.noAccount') }}</span>
|
||||
<router-link to="/register" class="link">
|
||||
{{ t('login.registerNow') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0c1929 0%, #1a237e 50%, #0d47a1 100%);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.language-switcher-wrapper {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 网格背景 */
|
||||
.login-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
animation: gridMove 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 科技光效 */
|
||||
.login-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(0, 212, 255, 0.1) 0%, transparent 50%);
|
||||
animation: rotate 30s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.3),
|
||||
0 0 40px rgba(0, 212, 255, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
padding: 40px 40px 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, transparent, #00d4ff, #7c4dff, transparent);
|
||||
border-radius: 2px;
|
||||
animation: lineGlow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes lineGlow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(0, 212, 255, 0.8), 0 0 30px rgba(124, 77, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 20px 40px 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
border: 2px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: white;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3), inset 0 0 10px rgba(0, 212, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.form-group input:focus::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.remember-me:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.remember-me input[type='checkbox'] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #00d4ff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.remember-me input[type='checkbox']:checked {
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 14px;
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.forgot-password:hover {
|
||||
color: #7c4dff;
|
||||
text-shadow: 0 0 10px rgba(124, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled)::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 212, 255, 0.4),
|
||||
0 0 20px rgba(124, 77, 255, 0.3);
|
||||
}
|
||||
|
||||
.login-btn:active:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 15px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
padding: 20px 40px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: #00d4ff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-footer a:hover {
|
||||
color: #7c4dff;
|
||||
text-shadow: 0 0 10px rgba(124, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.login-header,
|
||||
.login-form,
|
||||
.login-footer {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
196
src/pages/login/resetPassword.vue
Normal file
196
src/pages/login/resetPassword.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/useI18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Lock, Message } from '@element-plus/icons-vue'
|
||||
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
|
||||
import '@/assets/css/auth.css'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const resetFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const countdown = ref(0)
|
||||
const timer = ref(null)
|
||||
|
||||
const resetForm = reactive({
|
||||
email: '',
|
||||
code: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const validatePass2 = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('form.passwordMismatch')))
|
||||
} else if (value !== resetForm.newPassword) {
|
||||
callback(new Error(t('form.passwordMismatch')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const resetRules = {
|
||||
email: [
|
||||
{ required: true, message: t('resetPassword.emailPlaceholder'), trigger: 'blur' },
|
||||
{ type: 'email', message: t('resetPassword.emailRule'), trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: t('resetPassword.codePlaceholder'), trigger: 'blur' },
|
||||
{ len: 6, message: t('resetPassword.codeRule'), trigger: 'blur' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: t('resetPassword.newPasswordPlaceholder'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('resetPassword.passwordRule'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validatePass2, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const sendCode = () => {
|
||||
if (!resetForm.email) {
|
||||
ElMessage.warning(t('resetPassword.sendCodeFirst'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(resetForm.email)) {
|
||||
ElMessage.warning(t('resetPassword.emailRule'))
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟发送验证码
|
||||
countdown.value = 60
|
||||
timer.value = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
ElMessage.success(t('resetPassword.codeSent'))
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
if (!resetFormRef.value) return
|
||||
|
||||
await resetFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟重置密码请求
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
ElMessage.success(t('resetPassword.resetSuccess'))
|
||||
|
||||
// 清除定时器
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
countdown.value = 0
|
||||
}
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || t('resetPassword.resetFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
// 清除定时器
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
countdown.value = 0
|
||||
}
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resetFormRef.value) {
|
||||
resetFormRef.value.clearValidate()
|
||||
}
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="language-switcher">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="logo">
|
||||
<el-icon :size="48">
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h1>{{ t('resetPassword.title') }}</h1>
|
||||
<p>{{ t('resetPassword.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<el-form ref="resetFormRef" :model="resetForm" :rules="resetRules" class="auth-form" size="large">
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="resetForm.email" :placeholder="t('resetPassword.emailPlaceholder')"
|
||||
:prefix-icon="Message" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="code">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="14">
|
||||
<el-input v-model="resetForm.code" :placeholder="t('resetPassword.codePlaceholder')"
|
||||
:prefix-icon="Lock" maxlength="6" />
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-button class="code-button" :disabled="countdown > 0" @click="sendCode">
|
||||
{{
|
||||
countdown > 0
|
||||
? t('resetPassword.resendCode', { seconds: countdown })
|
||||
: t('resetPassword.sendCode')
|
||||
}}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="newPassword">
|
||||
<el-input v-model="resetForm.newPassword" type="password"
|
||||
:placeholder="t('resetPassword.newPasswordPlaceholder')" :prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="resetForm.confirmPassword" type="password"
|
||||
:placeholder="t('resetPassword.confirmPasswordPlaceholder')" :prefix-icon="Lock" show-password
|
||||
@keyup.enter="handleReset" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="handleReset">
|
||||
{{ t('resetPassword.resetButton') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<span class="text">{{ t('common.back') }}</span>
|
||||
<el-link type="primary" @click="goToLogin">
|
||||
{{ t('resetPassword.backToLogin') }}
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
162
src/pages/login/userRegister.vue
Normal file
162
src/pages/login/userRegister.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/useI18n'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Message } from '@element-plus/icons-vue'
|
||||
import LanguageSwitcher from '@/layouts/components/LanguageSwitcher.vue'
|
||||
import '@/assets/css/auth.css'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const registerFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agree: false
|
||||
})
|
||||
|
||||
const validatePass2 = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('form.passwordMismatch')))
|
||||
} else if (value !== registerForm.password) {
|
||||
callback(new Error(t('form.passwordMismatch')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, message: t('register.usernamePlaceholder'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: t('register.usernameRule'), trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: t('register.emailPlaceholder'), trigger: 'blur' },
|
||||
{ type: 'email', message: t('register.emailRule'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('register.passwordPlaceholder'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('register.passwordRule'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validatePass2, trigger: 'blur' }
|
||||
],
|
||||
agree: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.agreeRule')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
await registerFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟注册请求
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
ElMessage.success(t('register.registerSuccess'))
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || t('register.registerFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (registerFormRef.value) {
|
||||
registerFormRef.value.clearValidate()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<div class="language-switcher">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<div class="logo">
|
||||
<el-icon :size="48">
|
||||
<User />
|
||||
</el-icon>
|
||||
</div>
|
||||
<h1>{{ t('register.title') }}</h1>
|
||||
<p>{{ t('register.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" size="large">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" :placeholder="t('register.usernamePlaceholder')"
|
||||
:prefix-icon="User" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="registerForm.email" :placeholder="t('register.emailPlaceholder')"
|
||||
:prefix-icon="Message" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="registerForm.password" type="password"
|
||||
:placeholder="t('register.passwordPlaceholder')" :prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password"
|
||||
:placeholder="t('register.confirmPasswordPlaceholder')" :prefix-icon="Lock" show-password
|
||||
@keyup.enter="handleRegister" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="agree">
|
||||
<el-checkbox v-model="registerForm.agree">
|
||||
<span v-html="t('register.agreeTerms')"></span>
|
||||
<el-link type="primary">{{ t('register.terms') }}</el-link>
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="handleRegister">
|
||||
{{ t('register.registerButton') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<span class="text">{{ t('register.hasAccount') }}</span>
|
||||
<router-link to="/login" class="link">
|
||||
{{ t('register.loginNow') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,57 +1,316 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import Layout from '@/layouts/index.vue'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import systemRouter from './systemRouter'
|
||||
import userRoutes from '@/config/routes'
|
||||
import NProgress from 'nprogress'
|
||||
import tool from '@/utils/tool'
|
||||
import i18n from '@/i18n'
|
||||
import { beforeEach, afterEach } from './scrollBehavior'
|
||||
import '@/assets/css/nprogress.css'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/pages/login/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Layout,
|
||||
redirect: '/home',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
component: () => import('@/pages/home/Dashboard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
// 配置 NProgress
|
||||
NProgress.configure({
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.3
|
||||
})
|
||||
|
||||
// 匹配pages里面所有的.vue文件
|
||||
const modules = import.meta.glob('@/pages/**/*.vue')
|
||||
|
||||
// 特殊路由模块
|
||||
const otherModules = {
|
||||
'404': () => import('@/layouts/other/404.vue'),
|
||||
empty: () => import('@/layouts/other/empty.vue')
|
||||
}
|
||||
|
||||
// 系统路由
|
||||
const routes = systemRouter
|
||||
|
||||
// 是否已加载过动态/静态路由
|
||||
let isGetRouter = false
|
||||
|
||||
// 404路由移除函数
|
||||
let routes_404_r = null
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: routes,
|
||||
routes: routes
|
||||
})
|
||||
|
||||
/**
|
||||
* 设置页面标题
|
||||
* @param {Object} meta - 路由元信息
|
||||
*/
|
||||
const setPageTitle = (meta) => {
|
||||
const title = 'VueAdmin'
|
||||
if (meta?.title) {
|
||||
try {
|
||||
const translatedTitle = i18n.global.te(meta.title) ? i18n.global.t(meta.title) : meta.title
|
||||
document.title = `${translatedTitle} - ${title}`
|
||||
} catch (error) {
|
||||
document.title = `${meta.title} - ${title}`
|
||||
}
|
||||
} else {
|
||||
document.title = title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路由是否需要认证
|
||||
* @param {Object} to - 目标路由
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const checkAuthRequired = (to) => {
|
||||
return to.matched.some((record) => record.meta.requiresAuth !== false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除404路由
|
||||
*/
|
||||
const remove404Route = () => {
|
||||
if (routes_404_r) {
|
||||
router.removeRoute('404')
|
||||
routes_404_r = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加404路由
|
||||
*/
|
||||
const add404Route = () => {
|
||||
if (!routes_404_r) {
|
||||
routes_404_r = router.addRoute({
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: '404',
|
||||
hidden: true,
|
||||
component: otherModules['404']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动态路由
|
||||
* @param {Object} to - 目标路由对象
|
||||
* @returns {boolean} 是否加载成功
|
||||
*/
|
||||
const loadDynamicRoutes = async (to) => {
|
||||
try {
|
||||
// 从 store 获取菜单和用户信息
|
||||
const userStore = useUserStore()
|
||||
|
||||
const apiMenu = userStore.getMenu() || []
|
||||
const userInfo = userStore.userInfo
|
||||
|
||||
// 如果没有用户信息,不做处理
|
||||
if (!userInfo) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据用户角色过滤静态路由
|
||||
const userMenu = treeFilter(userRoutes, (node) => {
|
||||
return node.meta.role
|
||||
? node.meta.role.some((item) => userInfo.role?.includes(item))
|
||||
: true
|
||||
})
|
||||
|
||||
// 合并静态路由和API菜单
|
||||
const menu = [...userMenu, ...apiMenu]
|
||||
|
||||
// 转换异步路由
|
||||
const menuRouter = filterAsyncRouter(menu)
|
||||
|
||||
// 将树形路由转平铺
|
||||
const flatMenuRouter = tool.tree_to_list(menuRouter)
|
||||
|
||||
// 添加所有路由
|
||||
flatMenuRouter.forEach((item) => {
|
||||
router.addRoute('layout', item)
|
||||
})
|
||||
|
||||
// 添加404路由(必须在所有路由之后)
|
||||
add404Route()
|
||||
|
||||
isGetRouter = true
|
||||
|
||||
// 检查目标路由是否存在
|
||||
const hasRoute = router.hasRoute(to.name) || to.matched.length > 0
|
||||
|
||||
if (!hasRoute && to.name !== '404') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('加载动态路由失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 路由拦截器 - 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// 开始进度条
|
||||
NProgress.start()
|
||||
|
||||
// 设置页面标题
|
||||
setPageTitle(to.meta)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isLoggedIn = userStore.isLoggedIn()
|
||||
const requiresAuth = checkAuthRequired(to)
|
||||
|
||||
// 检查路由是否需要认证
|
||||
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth !== false)
|
||||
// 处理404页面
|
||||
if (to.name === '404') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理登录页
|
||||
if (to.path === '/login') {
|
||||
isGetRouter = false
|
||||
remove404Route()
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是系统路由,直接放行
|
||||
if (routes.some((r) => r.path === to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 需要认证但未登录,重定向到登录页
|
||||
if (requiresAuth && !isLoggedIn) {
|
||||
// 需要认证但未登录,重定向到登录页
|
||||
NProgress.done()
|
||||
isGetRouter = false
|
||||
userStore.clearMenu()
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }, // 保存目标路径,登录后可以跳转回去
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
} else if (to.path === '/login' && isLoggedIn) {
|
||||
// 已登录但访问登录页,重定向到首页
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
// 其他情况正常跳转
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 整页路由处理
|
||||
if (to.meta.fullpage) {
|
||||
to.matched = [to.matched[to.matched.length - 1]]
|
||||
}
|
||||
|
||||
// 调用前置守卫
|
||||
beforeEach(to, from)
|
||||
|
||||
// 加载动态/静态路由
|
||||
if (!isGetRouter) {
|
||||
const loadSuccess = await loadDynamicRoutes(to)
|
||||
|
||||
// 如果路由加载失败,跳转到404
|
||||
if (!loadSuccess) {
|
||||
next({ name: '404', replace: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查路由是否存在(动态路由已加载后)
|
||||
if (isGetRouter) {
|
||||
const hasRoute = router.hasRoute(to.name)
|
||||
// 如果路由不存在且不是404页面,跳转到404
|
||||
if (!hasRoute && to.name !== '404' && to.matched.length === 0) {
|
||||
next({ name: '404', replace: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// 全局后置钩子
|
||||
router.afterEach((to, from) => {
|
||||
// 调用后置钩子
|
||||
afterEach(to, from)
|
||||
|
||||
// 结束进度条
|
||||
NProgress.done()
|
||||
})
|
||||
|
||||
// 路由错误处理
|
||||
router.onError((error) => {
|
||||
NProgress.done()
|
||||
ElNotification.error({
|
||||
title: '路由错误',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 动态加载组件
|
||||
* @param {string} component - 组件路径
|
||||
* @returns {Function}
|
||||
*/
|
||||
function loadComponent(component) {
|
||||
if (!component) {
|
||||
return otherModules.empty
|
||||
}
|
||||
|
||||
for (const path in modules) {
|
||||
const dir = path.split('pages/')[1]?.split('.vue')[0]
|
||||
if (dir === component || dir === `${component}/index`) {
|
||||
return () => modules[path]()
|
||||
}
|
||||
}
|
||||
|
||||
return otherModules.empty
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换异步路由
|
||||
* @param {Array} routerMap - 路由映射
|
||||
* @returns {Array} 转换后的路由数组
|
||||
*/
|
||||
function filterAsyncRouter(routerMap) {
|
||||
if (!routerMap || !Array.isArray(routerMap)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return routerMap.map((item) => {
|
||||
const meta = item.meta || {}
|
||||
|
||||
// 处理外部链接特殊路由
|
||||
if (meta.type === 'iframe') {
|
||||
meta.url = item.path
|
||||
item.path = `/i/${item.name}`
|
||||
}
|
||||
|
||||
return {
|
||||
path: item.path,
|
||||
name: item.name,
|
||||
meta: meta,
|
||||
redirect: item.redirect,
|
||||
children: item.children ? filterAsyncRouter(item.children) : undefined,
|
||||
component: loadComponent(item.component)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤树结构
|
||||
* @param {Array} tree - 树结构数据
|
||||
* @param {Function} func - 过滤函数
|
||||
* @returns {Array} 过滤后的树结构
|
||||
*/
|
||||
function treeFilter(tree, func) {
|
||||
if (!tree || !Array.isArray(tree)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tree
|
||||
.map((node) => ({ ...node }))
|
||||
.filter((node) => {
|
||||
node.children = node.children ? treeFilter(node.children, func) : undefined
|
||||
return func(node) || (node.children && node.children.length > 0)
|
||||
})
|
||||
}
|
||||
|
||||
export default router
|
||||
|
||||
30
src/router/scrollBehavior.js
Normal file
30
src/router/scrollBehavior.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
export function beforeEach(to, from) {
|
||||
const adminMain = document.querySelector('#adminui-main')
|
||||
if (!adminMain) {
|
||||
return false
|
||||
}
|
||||
|
||||
const layoutStore = useLayoutStore()
|
||||
layoutStore.updateViewTags({
|
||||
fullPath: from.fullPath,
|
||||
scrollTop: adminMain.scrollTop
|
||||
})
|
||||
}
|
||||
|
||||
export function afterEach(to) {
|
||||
const adminMain = document.querySelector('#adminui-main')
|
||||
if (!adminMain) {
|
||||
return false
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const layoutStore = useLayoutStore()
|
||||
const beforeRoute = layoutStore.viewTags.find((v) => v.fullPath === to.fullPath)
|
||||
if (beforeRoute) {
|
||||
adminMain.scrollTop = beforeRoute.scrollTop || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
36
src/router/systemRouter.js
Normal file
36
src/router/systemRouter.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import config from "@/config";
|
||||
|
||||
//系统路由
|
||||
const routes = [
|
||||
{
|
||||
name: "layout",
|
||||
path: "/",
|
||||
component: () => import("@/layouts/index.vue"),
|
||||
redirect: config.DASHBOARD_URL || "/dashboard",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
component: () =>
|
||||
import("@/pages/login/index.vue"),
|
||||
meta: {
|
||||
title: "登录",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
component: () => import("@/pages/login/userRegister.vue"),
|
||||
meta: {
|
||||
title: "注册",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/reset-password",
|
||||
component: () => import("@/pages/login/resetPassword.vue"),
|
||||
meta: {
|
||||
title: "重置密码",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -8,6 +8,9 @@ export const useLayoutStore = defineStore('layout', () => {
|
||||
// 侧边栏折叠状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// 视图标签页(用于记录页面滚动位置)
|
||||
const viewTags = ref([])
|
||||
|
||||
// 切换侧边栏折叠
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
@@ -18,10 +21,37 @@ export const useLayoutStore = defineStore('layout', () => {
|
||||
layoutMode.value = mode
|
||||
}
|
||||
|
||||
// 更新视图标签
|
||||
const updateViewTags = (tag) => {
|
||||
const index = viewTags.value.findIndex((item) => item.fullPath === tag.fullPath)
|
||||
if (index !== -1) {
|
||||
viewTags.value[index] = tag
|
||||
} else {
|
||||
viewTags.value.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除视图标签
|
||||
const removeViewTags = (fullPath) => {
|
||||
const index = viewTags.value.findIndex((item) => item.fullPath === fullPath)
|
||||
if (index !== -1) {
|
||||
viewTags.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空视图标签
|
||||
const clearViewTags = () => {
|
||||
viewTags.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
layoutMode,
|
||||
sidebarCollapsed,
|
||||
viewTags,
|
||||
toggleSidebar,
|
||||
setLayoutMode,
|
||||
updateViewTags,
|
||||
removeViewTags,
|
||||
clearViewTags,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
|
||||
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || 'null'))
|
||||
const menu = ref(JSON.parse(localStorage.getItem('MENU') || '[]'))
|
||||
|
||||
// 设置 token
|
||||
function setToken(newToken) {
|
||||
@@ -24,14 +25,33 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.setItem('userInfo', JSON.stringify(info))
|
||||
}
|
||||
|
||||
// 设置菜单
|
||||
function setMenu(newMenu) {
|
||||
menu.value = newMenu
|
||||
localStorage.setItem('MENU', JSON.stringify(newMenu))
|
||||
}
|
||||
|
||||
// 获取菜单
|
||||
function getMenu() {
|
||||
return menu.value
|
||||
}
|
||||
|
||||
// 清除菜单
|
||||
function clearMenu() {
|
||||
menu.value = []
|
||||
localStorage.removeItem('MENU')
|
||||
}
|
||||
|
||||
// 登出
|
||||
function logout() {
|
||||
token.value = ''
|
||||
refreshToken.value = ''
|
||||
userInfo.value = null
|
||||
menu.value = []
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('userInfo')
|
||||
localStorage.removeItem('MENU')
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
@@ -43,9 +63,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
token,
|
||||
refreshToken,
|
||||
userInfo,
|
||||
menu,
|
||||
setToken,
|
||||
setRefreshToken,
|
||||
setUserInfo,
|
||||
setMenu,
|
||||
getMenu,
|
||||
clearMenu,
|
||||
logout,
|
||||
isLoggedIn,
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@ async function refreshToken() {
|
||||
// 这里需要根据实际的刷新 token 接口进行修改
|
||||
// 假设刷新接口是 /auth/refresh
|
||||
const refreshUrl = config.baseURL + '/auth/refresh'
|
||||
const refreshTokenValue = localStorage.getItem('refreshToken')
|
||||
const userStore = useUserStore()
|
||||
const refreshTokenValue = userStore.refreshToken
|
||||
|
||||
const response = await axios.post(refreshUrl, {
|
||||
refreshToken: refreshTokenValue
|
||||
|
||||
180
src/utils/tool.js
Normal file
180
src/utils/tool.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 工具类
|
||||
*/
|
||||
const tool = {
|
||||
/**
|
||||
* 本地存储操作
|
||||
*/
|
||||
data: {
|
||||
/**
|
||||
* 设置本地存储
|
||||
* @param {string} key - 键名
|
||||
* @param {*} value - 值
|
||||
*/
|
||||
set(key, value) {
|
||||
if (typeof value === 'object') {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
} else {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取本地存储
|
||||
* @param {string} key - 键名
|
||||
* @param {*} defaultValue - 默认值
|
||||
* @returns {*}
|
||||
*/
|
||||
get(key, defaultValue = null) {
|
||||
const value = localStorage.getItem(key)
|
||||
if (!value) {
|
||||
return defaultValue
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除本地存储
|
||||
* @param {string} key - 键名
|
||||
*/
|
||||
remove(key) {
|
||||
localStorage.removeItem(key)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空本地存储
|
||||
*/
|
||||
clear() {
|
||||
localStorage.clear()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 树形结构转列表
|
||||
* @param {Array} tree - 树形结构数据
|
||||
* @param {string} childrenKey - 子节点键名
|
||||
* @returns {Array} 扁平化后的数组
|
||||
*/
|
||||
tree_to_list(tree, childrenKey = 'children') {
|
||||
if (!tree || !Array.isArray(tree)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result = []
|
||||
|
||||
const traverse = (nodes) => {
|
||||
if (!nodes || !Array.isArray(nodes)) {
|
||||
return
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
result.push(node)
|
||||
if (node[childrenKey] && node[childrenKey].length > 0) {
|
||||
traverse(node[childrenKey])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
traverse(tree)
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* 列表转树形结构
|
||||
* @param {Array} list - 列表数据
|
||||
* @param {string} idKey - ID键名
|
||||
* @param {string} parentIdKey - 父ID键名
|
||||
* @param {string} childrenKey - 子节点键名
|
||||
* @returns {Array} 树形结构数据
|
||||
*/
|
||||
list_to_tree(list, idKey = 'id', parentIdKey = 'parentId', childrenKey = 'children') {
|
||||
if (!list || !Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const map = {}
|
||||
const roots = []
|
||||
|
||||
// 创建映射表
|
||||
list.forEach((item) => {
|
||||
map[item[idKey]] = { ...item, [childrenKey]: [] }
|
||||
})
|
||||
|
||||
// 构建树形结构
|
||||
list.forEach((item) => {
|
||||
const node = map[item[idKey]]
|
||||
const parentId = item[parentIdKey]
|
||||
|
||||
if (parentId && map[parentId]) {
|
||||
map[parentId][childrenKey].push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
},
|
||||
|
||||
/**
|
||||
* 深拷贝
|
||||
* @param {*} obj - 要拷贝的对象
|
||||
* @returns {*} 拷贝后的对象
|
||||
*/
|
||||
deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.deepClone(item))
|
||||
}
|
||||
|
||||
const cloned = {}
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = this.deepClone(obj[key])
|
||||
}
|
||||
}
|
||||
|
||||
return cloned
|
||||
},
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要防抖的函数
|
||||
* @param {number} wait - 等待时间
|
||||
* @returns {Function}
|
||||
*/
|
||||
debounce(func, wait = 300) {
|
||||
let timeout
|
||||
return function (...args) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
func.apply(this, args)
|
||||
}, wait)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要节流的函数
|
||||
* @param {number} wait - 等待时间
|
||||
* @returns {Function}
|
||||
*/
|
||||
throttle(func, wait = 300) {
|
||||
let timeout
|
||||
return function (...args) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
func.apply(this, args)
|
||||
timeout = null
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default tool
|
||||
134
yarn.lock
134
yarn.lock
@@ -253,6 +253,16 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@ctrl/tinycolor@^3.4.1":
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
|
||||
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
|
||||
|
||||
"@element-plus/icons-vue@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz#7e9cb231fb738b2056f33e22c3a29e214b538dcf"
|
||||
integrity sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==
|
||||
|
||||
"@esbuild/aix-ppc64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c"
|
||||
@@ -451,6 +461,26 @@
|
||||
"@eslint/core" "^0.17.0"
|
||||
levn "^0.4.1"
|
||||
|
||||
"@floating-ui/core@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
|
||||
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.0.1":
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
|
||||
integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.7.3"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/utils@^0.2.10":
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@humanfs/core@^0.19.1":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
||||
@@ -539,6 +569,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
|
||||
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
|
||||
|
||||
"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
|
||||
version "2.11.7"
|
||||
resolved "https://registry.yarnpkg.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
|
||||
integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
|
||||
|
||||
"@rolldown/pluginutils@1.0.0-beta.53":
|
||||
version "1.0.0-beta.53"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz#c57a5234ae122671aff6fe72e673a7ed90f03f87"
|
||||
@@ -679,6 +714,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
|
||||
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*", "@types/lodash@^4.17.20":
|
||||
version "4.17.23"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.23.tgz#c1bb06db218acc8fc232da0447473fc2fb9d9841"
|
||||
integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==
|
||||
|
||||
"@types/web-bluetooth@^0.0.20":
|
||||
version "0.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
|
||||
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
|
||||
|
||||
"@vitejs/plugin-vue@^6.0.3":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz#b857c5dcbc5cfb30bf5d7f9d6e274afcca2d46d1"
|
||||
@@ -869,6 +921,28 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
|
||||
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
|
||||
|
||||
"@vueuse/core@^10.11.0":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
|
||||
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
|
||||
dependencies:
|
||||
"@types/web-bluetooth" "^0.0.20"
|
||||
"@vueuse/metadata" "10.11.1"
|
||||
"@vueuse/shared" "10.11.1"
|
||||
vue-demi ">=0.14.8"
|
||||
|
||||
"@vueuse/metadata@10.11.1":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
|
||||
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
|
||||
|
||||
"@vueuse/shared@10.11.1":
|
||||
version "10.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.11.1.tgz#62b84e3118ae6e1f3ff38f4fbe71b0c5d0f10938"
|
||||
integrity sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==
|
||||
dependencies:
|
||||
vue-demi ">=0.14.8"
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -906,6 +980,11 @@ argparse@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
async-validator@^4.2.5:
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
|
||||
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@@ -1047,6 +1126,11 @@ csstype@^3.2.3:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
|
||||
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
|
||||
|
||||
dayjs@^1.11.19:
|
||||
version "1.11.19"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938"
|
||||
integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==
|
||||
|
||||
debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.1:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||
@@ -1096,6 +1180,26 @@ electron-to-chromium@^1.5.263:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
|
||||
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
|
||||
|
||||
element-plus@^2.13.1:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-2.13.1.tgz#2cc6059da0f0f217f27d657f5140a45ecb0fd221"
|
||||
integrity sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.4.1"
|
||||
"@element-plus/icons-vue" "^2.3.2"
|
||||
"@floating-ui/dom" "^1.0.1"
|
||||
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
"@types/lodash" "^4.17.20"
|
||||
"@types/lodash-es" "^4.17.12"
|
||||
"@vueuse/core" "^10.11.0"
|
||||
async-validator "^4.2.5"
|
||||
dayjs "^1.11.19"
|
||||
lodash "^4.17.21"
|
||||
lodash-es "^4.17.21"
|
||||
lodash-unified "^1.0.3"
|
||||
memoize-one "^6.0.0"
|
||||
normalize-wheel-es "^1.2.0"
|
||||
|
||||
entities@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
|
||||
@@ -1578,11 +1682,26 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.22"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.22.tgz#eb7d123ec2470d69b911abe34f85cb694849b346"
|
||||
integrity sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==
|
||||
|
||||
lodash-unified@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894"
|
||||
integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
@@ -1602,6 +1721,11 @@ math-intrinsics@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
@@ -1656,6 +1780,11 @@ node-releases@^2.0.27:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||
|
||||
normalize-wheel-es@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
|
||||
integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
|
||||
|
||||
nprogress@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
|
||||
@@ -2035,6 +2164,11 @@ vite@^7.3.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vue-demi@>=0.14.8:
|
||||
version "0.14.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
|
||||
|
||||
vue-i18n@^11.2.8:
|
||||
version "11.2.8"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-11.2.8.tgz#f431b583134776dcf59e59250c5231e4eaed8404"
|
||||
|
||||
Reference in New Issue
Block a user