更新
This commit is contained in:
356
src/assets/style/auth.scss
Normal file
356
src/assets/style/auth.scss
Normal file
@@ -0,0 +1,356 @@
|
||||
// Auth Pages - Warm Tech Theme
|
||||
// Warm color palette with tech-inspired design
|
||||
|
||||
:root {
|
||||
--auth-primary: #ff6b35;
|
||||
--auth-primary-light: #ff8c5a;
|
||||
--auth-primary-dark: #e55a2b;
|
||||
--auth-secondary: #ffb347;
|
||||
--accent-orange: #ffa500;
|
||||
--accent-coral: #ff7f50;
|
||||
--accent-amber: #ffc107;
|
||||
|
||||
--bg-gradient-start: #fff5f0;
|
||||
--bg-gradient-end: #ffe8dc;
|
||||
--card-bg: rgba(255, 255, 255, 0.95);
|
||||
|
||||
--text-primary: #2d1810;
|
||||
--text-secondary: #6b4423;
|
||||
--text-muted: #a67c52;
|
||||
|
||||
--border-color: #ffd4b8;
|
||||
--shadow-color: rgba(255, 107, 53, 0.15);
|
||||
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
|
||||
--tech-blue: #007bff;
|
||||
--tech-purple: #6f42c1;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Tech pattern background
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 107, 53, 0.03) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 179, 71, 0.05) 0%, transparent 40%),
|
||||
radial-gradient(circle at 40% 80%, rgba(255, 127, 80, 0.04) 0%, transparent 40%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Animated tech elements
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(255, 107, 53, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: -200px;
|
||||
right: -200px;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px;
|
||||
box-shadow:
|
||||
0 20px 60px var(--shadow-color),
|
||||
0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 20px;
|
||||
|
||||
// Tech accent line
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--auth-primary), var(--auth-secondary));
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--auth-primary-light);
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.ant-input-focused {
|
||||
border-color: var(--auth-primary);
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.08);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--auth-primary-light);
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.ant-input-affix-wrapper-focused {
|
||||
border-color: var(--auth-primary);
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-prefix {
|
||||
color: var(--auth-primary);
|
||||
font-size: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ant-input-suffix {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.remember-me {
|
||||
.ant-checkbox-inner {
|
||||
border-radius: 4px;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.ant-checkbox-wrapper-checked {
|
||||
.ant-checkbox-inner {
|
||||
background-color: var(--auth-primary);
|
||||
border-color: var(--auth-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: var(--auth-primary);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
|
||||
.auth-link {
|
||||
color: var(--auth-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--auth-primary-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tech-decoration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
.tech-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(1) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
left: -150px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(2) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.auth-card {
|
||||
padding: 40px 24px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,401 +1,228 @@
|
||||
<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="not-found-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<div class="not-found-content">
|
||||
<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="error-title">页面未找到</div>
|
||||
<div class="error-description">抱歉,您访问的页面不存在或已被移除</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>
|
||||
<a-button type="primary" size="large" @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
返回上一页
|
||||
</button>
|
||||
</a-button>
|
||||
<a-button size="large" @click="goHome">
|
||||
<template #icon>
|
||||
<HomeOutlined />
|
||||
</template>
|
||||
返回首页
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeftOutlined, HomeOutlined } from '@ant-design/icons-vue'
|
||||
import '@/assets/style/auth.scss'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Go back to previous page
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// Go to home page
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.not-found-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0c1929 0%, #1a237e 50%, #0d47a1 100%);
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
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;
|
||||
}
|
||||
.tech-decoration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
@keyframes stars {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
.tech-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 550px 250px;
|
||||
.tech-circle:nth-child(1) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
left: -150px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(2) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@keyframes pulse {
|
||||
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;
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
.not-found-content {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
}
|
||||
z-index: 1;
|
||||
|
||||
.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;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.error-text h1 {
|
||||
font-size: 28px;
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.error-text p {
|
||||
font-size: 14px;
|
||||
.error-title {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-illustration {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
.error-description {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.ant-btn {
|
||||
height: 48px;
|
||||
padding: 0 32px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-btn-primary) {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--auth-primary);
|
||||
color: var(--auth-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.not-found-content {
|
||||
padding: 20px;
|
||||
|
||||
.error-code {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,288 +1,211 @@
|
||||
<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-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<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 class="empty-icon">
|
||||
<InboxOutlined :style="{ fontSize: '120px', color: '#ff6b35' }" />
|
||||
</div>
|
||||
|
||||
<div class="empty-text">
|
||||
<h3 class="empty-title">{{ title }}</h3>
|
||||
<p class="empty-description">{{ description }}</p>
|
||||
<div class="empty-title">暂无数据</div>
|
||||
<div class="empty-description">
|
||||
{{ description || '当前页面暂无数据,请稍后再试' }}
|
||||
</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>
|
||||
<a-button v-if="showButton" type="primary" size="large" @click="handleAction">
|
||||
<template #icon v-if="buttonIcon">
|
||||
<component :is="buttonIcon" />
|
||||
</template>
|
||||
{{ buttonText || '刷新页面' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
min-height: 400px;
|
||||
<script setup>
|
||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'EmptyPage',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
defineProps({
|
||||
description: {
|
||||
type: String,
|
||||
default: '当前页面暂无数据,请稍后再试',
|
||||
},
|
||||
showButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '刷新页面',
|
||||
},
|
||||
buttonIcon: {
|
||||
type: [String, Object],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
const handleAction = () => {
|
||||
emit('action')
|
||||
// Default behavior: refresh page
|
||||
router.go(0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.empty-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
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;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
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;
|
||||
}
|
||||
.tech-decoration {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
@keyframes floatBg {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
.tech-circle {
|
||||
position: absolute;
|
||||
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(1) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
left: -150px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(2) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.tech-circle:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 20%;
|
||||
left: -75px;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 200px 150px;
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 480px;
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
.empty-icon {
|
||||
margin-bottom: 32px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.6;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
height: 48px;
|
||||
padding: 0 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.empty-state {
|
||||
min-height: 300px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.empty-content {
|
||||
padding: 20px;
|
||||
|
||||
.empty-icon {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
:deep(.anticon) {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
.empty-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
.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;
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,131 +1,201 @@
|
||||
<template>
|
||||
<div class="login-container auth-container">
|
||||
<!-- 科技感背景 -->
|
||||
<div class="tech-bg">
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="login-wrapper auth-wrapper">
|
||||
<div class="login-card auth-card">
|
||||
<!-- 左侧装饰区 -->
|
||||
<div class="decoration-area">
|
||||
<div class="tech-circle">
|
||||
<div class="circle-inner"></div>
|
||||
<div class="circle-ring"></div>
|
||||
<div class="circle-ring circle-ring-2"></div>
|
||||
</div>
|
||||
<div class="decoration-text">
|
||||
<h2>欢迎回来</h2>
|
||||
<p>进入智能管理系统</p>
|
||||
</div>
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">欢迎回来</h1>
|
||||
<p class="auth-subtitle">登录您的账户继续探索科技世界</p>
|
||||
</div>
|
||||
|
||||
<a-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="auth-form" @finish="handleLogin" layout="vertical">
|
||||
<a-form-item name="username">
|
||||
<a-input v-model:value="loginForm.username" placeholder="请输入用户名/邮箱" size="large">
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password">
|
||||
<a-input-password v-model:value="loginForm.password" placeholder="请输入密码" size="large" @pressEnter="handleLogin">
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<div class="auth-links">
|
||||
<a-checkbox v-model:checked="loginForm.rememberMe" class="remember-me"> 记住我 </a-checkbox>
|
||||
<router-link to="/reset-password" class="forgot-password"> 忘记密码? </router-link>
|
||||
</div>
|
||||
|
||||
<!-- 右侧表单区 -->
|
||||
<div class="form-area">
|
||||
<div class="auth-header">
|
||||
<h1>Vue Admin</h1>
|
||||
<p class="subtitle">登录您的账户</p>
|
||||
</div>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form :model="formState" @finish="handleLogin" layout="vertical" class="auth-form login-form">
|
||||
<a-form-item name="username" :rules="[{ required: true, message: '请输入用户名' }]">
|
||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" size="large"
|
||||
:prefix="h(UserOutlined)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" size="large"
|
||||
:prefix="h(LockOutlined)" />
|
||||
</a-form-item>
|
||||
|
||||
<div class="form-options">
|
||||
<a-checkbox v-model:checked="formState.remember">记住密码</a-checkbox>
|
||||
<router-link to="/reset-password" class="forgot-password">
|
||||
忘记密码?
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<div class="form-footer">
|
||||
<span>还没有账号?</span>
|
||||
<router-link to="/register" class="auth-link">
|
||||
立即注册
|
||||
</router-link>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
还没有账户?
|
||||
<router-link to="/userRegister" class="auth-link"> 立即注册 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import authApi from '@/api/auth'
|
||||
import '@/assets/style/auth-pages.scss'
|
||||
import auth from '@/api/auth'
|
||||
import config from '@/config'
|
||||
import system from '@/api/system'
|
||||
import '@/assets/style/auth.scss'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginPage'
|
||||
name: 'LoginPage',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const loginFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const formState = reactive({
|
||||
// Login form data
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
rememberMe: false,
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
let res = await authApi.login.post({
|
||||
username: formState.username,
|
||||
password: formState.password,
|
||||
})
|
||||
if (res.code == 1) {
|
||||
userStore.setToken(res.data.access_token)
|
||||
let userInfo = await authApi.user.get()
|
||||
if (userInfo.code == 1) {
|
||||
userStore.setUserInfo(userInfo.data)
|
||||
}
|
||||
let authData = await authApi.menu.my.get()
|
||||
if (authData.code == 1) {
|
||||
userStore.setMenu(authData.data.menu)
|
||||
userStore.setPermissions(authData.data.permissions)
|
||||
}
|
||||
// Form validation rules
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名或邮箱' },
|
||||
{ min: 3, max: 50, message: '长度在 3 到 50 个字符' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码长度不能少于 6 位' },
|
||||
],
|
||||
}
|
||||
|
||||
message.success('登录成功')
|
||||
const redirect = router.currentRoute.value.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
// Handle login
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
// Validate form
|
||||
await loginFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// 1. Call login API
|
||||
const loginResponse = await auth.login.post({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
})
|
||||
|
||||
// Check if login was successful
|
||||
if (loginResponse.code !== 1) {
|
||||
throw new Error(loginResponse.msg || '登录失败')
|
||||
}
|
||||
} catch {
|
||||
message.error('登录失败,请检查用户名和密码')
|
||||
|
||||
const loginData = loginResponse.data
|
||||
|
||||
// 2. Store access_token persistently
|
||||
if (loginData.access_token) {
|
||||
userStore.setToken(loginData.access_token)
|
||||
}
|
||||
|
||||
// Store refresh token if available
|
||||
if (loginData.refresh_token) {
|
||||
userStore.setRefreshToken(loginData.refresh_token)
|
||||
}
|
||||
|
||||
// 3. Get user information after login
|
||||
const userResponse = await auth.user.get()
|
||||
|
||||
if (userResponse.code === 1 && userResponse.data) {
|
||||
userStore.setUserInfo(userResponse.data)
|
||||
}
|
||||
|
||||
// 4. Get authorized menu information
|
||||
const menuResponse = await auth.menu.my.get()
|
||||
|
||||
if (menuResponse.code === 1 && menuResponse.data) {
|
||||
userStore.setMenu(menuResponse.data.menu)
|
||||
userStore.setPermissions(menuResponse.data.permissions)
|
||||
}
|
||||
|
||||
// // 5. Cache system configuration data
|
||||
// try {
|
||||
// const settingResponse = await system.setting.list.get()
|
||||
// if (settingResponse && settingResponse.data) {
|
||||
// tool.data.set('system_setting', settingResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache system settings:', error)
|
||||
// }
|
||||
|
||||
// // 6. Cache dictionary data
|
||||
// try {
|
||||
// const dictResponse = await system.dictionary.list.get()
|
||||
// if (dictResponse && dictResponse.data) {
|
||||
// tool.data.set('system_dictionary', dictResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache dictionary data:', error)
|
||||
// }
|
||||
|
||||
// // 7. Cache area data
|
||||
// try {
|
||||
// const areaResponse = await system.area.list.get()
|
||||
// if (areaResponse && areaResponse.data) {
|
||||
// tool.data.set('system_area', areaResponse.data)
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Failed to cache area data:', error)
|
||||
// }
|
||||
|
||||
// Success message
|
||||
message.success('登录成功!')
|
||||
|
||||
// 5. Redirect to dashboard or redirect parameter
|
||||
setTimeout(() => {
|
||||
// Get redirect from query parameter
|
||||
const redirect = route.query.redirect
|
||||
|
||||
if (redirect) {
|
||||
// If there's a redirect parameter, go there
|
||||
router.push(redirect)
|
||||
} else {
|
||||
// Otherwise, go to configured dashboard URL
|
||||
router.push(config.DASHBOARD_URL)
|
||||
}
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error)
|
||||
|
||||
// Clear user data on login failure
|
||||
userStore.logout()
|
||||
|
||||
// Show error message
|
||||
const errorMsg = error.response?.data?.msg || error.msg || error.message || '登录失败,请检查用户名和密码'
|
||||
message.error(errorMsg)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
||||
</style>
|
||||
|
||||
@@ -1,156 +1,164 @@
|
||||
<template>
|
||||
<div class="reset-container auth-container">
|
||||
<!-- 科技感背景 -->
|
||||
<div class="tech-bg">
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="reset-wrapper auth-wrapper">
|
||||
<div class="reset-card auth-card">
|
||||
<!-- 左侧装饰区 -->
|
||||
<div class="decoration-area">
|
||||
<div class="tech-circle">
|
||||
<div class="circle-inner"></div>
|
||||
<div class="circle-ring"></div>
|
||||
<div class="circle-ring circle-ring-2"></div>
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">找回密码</h1>
|
||||
<p class="auth-subtitle">输入您的邮箱,我们将发送重置密码链接</p>
|
||||
</div>
|
||||
|
||||
<a-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form" @finish="handleSubmit" layout="vertical">
|
||||
<a-form-item name="email">
|
||||
<a-input v-model:value="forgotForm.email" placeholder="请输入注册邮箱" size="large" @pressEnter="handleSubmit">
|
||||
<template #prefix>
|
||||
<MailOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="captcha" v-if="showCaptcha">
|
||||
<div style="display: flex; gap: 12px">
|
||||
<a-input v-model:value="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1">
|
||||
<template #prefix>
|
||||
<SafetyOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button type="default" size="large" :disabled="captchaDisabled" @click="sendCaptcha">
|
||||
{{ captchaButtonText }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="decoration-text">
|
||||
<h2>重置密码</h2>
|
||||
<p>找回您的账户访问权限</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 右侧表单区 -->
|
||||
<div class="form-area">
|
||||
<div class="auth-header">
|
||||
<h1>Vue Admin</h1>
|
||||
<p class="subtitle">设置新密码</p>
|
||||
</div>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||
{{ loading ? '提交中...' : '发送重置链接' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form :model="formState" @finish="handleReset" layout="vertical" class="auth-form reset-form">
|
||||
<a-form-item name="email" :rules="[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]">
|
||||
<a-input v-model:value="formState.email" placeholder="请输入邮箱地址" size="large"
|
||||
:prefix="h(MailOutlined)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="verificationCode" :rules="[{ required: true, message: '请输入验证码' }]">
|
||||
<div class="code-input-wrapper">
|
||||
<a-input v-model:value="formState.verificationCode" placeholder="请输入验证码" size="large"
|
||||
:prefix="h(SafetyOutlined)" class="code-input" />
|
||||
<a-button type="primary" :disabled="countdown > 0" @click="sendCode" class="code-btn"
|
||||
size="large">
|
||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="newPassword" :rules="[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]">
|
||||
<a-input-password v-model:value="formState.newPassword" placeholder="请输入新密码" size="large"
|
||||
:prefix="h(LockOutlined)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="confirmPassword" :rules="[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
{ validator: validateConfirmPassword },
|
||||
]">
|
||||
<a-input-password v-model:value="formState.confirmPassword" placeholder="请再次输入新密码"
|
||||
size="large" :prefix="h(LockOutlined)" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
||||
重置密码
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<div class="form-footer">
|
||||
<span>记得密码了?</span>
|
||||
<router-link to="/login" class="auth-link">
|
||||
立即登录
|
||||
</router-link>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
想起密码了?
|
||||
<router-link to="/login" class="auth-link"> 返回登录 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, h } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { MailOutlined, SafetyOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import '@/assets/style/auth-pages.scss'
|
||||
import { MailOutlined, SafetyOutlined } from '@ant-design/icons-vue'
|
||||
import '@/assets/style/auth.scss'
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPasswordPage'
|
||||
name: 'ResetPasswordPage',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const forgotFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const countdown = ref(0)
|
||||
const showCaptcha = ref(false)
|
||||
const captchaDisabled = ref(false)
|
||||
const countdown = ref(60)
|
||||
|
||||
const formState = reactive({
|
||||
// Forgot password form data
|
||||
const forgotForm = reactive({
|
||||
email: '',
|
||||
verificationCode: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
captcha: '',
|
||||
})
|
||||
|
||||
const validateConfirmPassword = async (rule, value) => {
|
||||
if (value !== formState.newPassword) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
// Captcha button text
|
||||
const captchaButtonText = ref('获取验证码')
|
||||
|
||||
// Form validation rules
|
||||
const forgotRules = {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址' },
|
||||
],
|
||||
captcha: [
|
||||
{ required: true, message: '请输入验证码' },
|
||||
{ len: 6, message: '验证码为6位数字' },
|
||||
],
|
||||
}
|
||||
|
||||
const sendCode = () => {
|
||||
if (!formState.email) {
|
||||
// Send captcha code
|
||||
const sendCaptcha = async () => {
|
||||
if (!forgotForm.email) {
|
||||
message.warning('请先输入邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 实现发送验证码逻辑
|
||||
message.success('验证码已发送')
|
||||
countdown.value = 60
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 1000)
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(forgotForm.email)) {
|
||||
message.warning('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate API call - Replace with actual API call
|
||||
// Example: const response = await sendCaptchaApi(forgotForm.email)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
message.success('验证码已发送至您的邮箱')
|
||||
|
||||
// Start countdown
|
||||
captchaDisabled.value = true
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
captchaButtonText.value = `${countdown.value}秒后重试`
|
||||
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
captchaDisabled.value = false
|
||||
captchaButtonText.value = '获取验证码'
|
||||
countdown.value = 60
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
showCaptcha.value = true
|
||||
} catch (error) {
|
||||
console.error('Send captcha failed:', error)
|
||||
message.error('发送验证码失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
loading.value = true
|
||||
// Handle submit
|
||||
const handleSubmit = async () => {
|
||||
if (!forgotFormRef.value) return
|
||||
|
||||
try {
|
||||
// TODO: 实现重置密码逻辑
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
message.success('密码重置成功,请登录')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
message.error('密码重置失败,请稍后重试')
|
||||
await forgotFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// Simulate API call - Replace with actual API call
|
||||
// Example: const response = await forgotPasswordApi(forgotForm)
|
||||
|
||||
// Simulated delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Success message
|
||||
message.success('密码重置链接已发送至您的邮箱,请注意查收')
|
||||
|
||||
// Redirect to login page
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('Forgot password failed:', error)
|
||||
message.error('提交失败,请检查邮箱地址和验证码')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
||||
</style>
|
||||
|
||||
@@ -1,149 +1,161 @@
|
||||
<template>
|
||||
<div class="register-container auth-container">
|
||||
<!-- 科技感背景 -->
|
||||
<div class="tech-bg">
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="grid-line"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="light-spot"></div>
|
||||
<div class="auth-container">
|
||||
<div class="tech-decoration">
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
<div class="tech-circle"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="register-wrapper auth-wrapper">
|
||||
<div class="register-card auth-card">
|
||||
<!-- 左侧装饰区 -->
|
||||
<div class="decoration-area">
|
||||
<div class="tech-circle">
|
||||
<div class="circle-inner"></div>
|
||||
<div class="circle-ring"></div>
|
||||
<div class="circle-ring circle-ring-2"></div>
|
||||
</div>
|
||||
<div class="decoration-text">
|
||||
<h2>创建账号</h2>
|
||||
<p>加入智能管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1 class="auth-title">创建账户</h1>
|
||||
<p class="auth-subtitle">加入我们,开启科技之旅</p>
|
||||
</div>
|
||||
|
||||
<!-- 右侧表单区 -->
|
||||
<div class="form-area">
|
||||
<div class="auth-header">
|
||||
<h1>Vue Admin</h1>
|
||||
<p class="subtitle">注册新账户</p>
|
||||
</div>
|
||||
<a-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" @finish="handleRegister" layout="vertical">
|
||||
<a-form-item name="username">
|
||||
<a-input v-model:value="registerForm.username" placeholder="请输入用户名" size="large">
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form :model="formState" @finish="handleRegister" layout="vertical"
|
||||
class="auth-form register-form">
|
||||
<a-form-item name="username" :rules="[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
]">
|
||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" size="large"
|
||||
:prefix="h(UserOutlined)" />
|
||||
</a-form-item>
|
||||
<a-form-item name="email">
|
||||
<a-input v-model:value="registerForm.email" placeholder="请输入邮箱地址" size="large">
|
||||
<template #prefix>
|
||||
<MailOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="email" :rules="[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]">
|
||||
<a-input v-model:value="formState.email" placeholder="请输入邮箱地址" size="large"
|
||||
:prefix="h(MailOutlined)" />
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password v-model:value="registerForm.password" placeholder="请输入密码(至少6位)" size="large">
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" :rules="[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]">
|
||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" size="large"
|
||||
:prefix="h(LockOutlined)" />
|
||||
</a-form-item>
|
||||
<a-form-item name="confirmPassword">
|
||||
<a-input-password v-model:value="registerForm.confirmPassword" placeholder="请再次输入密码" size="large" @pressEnter="handleRegister">
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="confirmPassword" :rules="[
|
||||
{ required: true, message: '请确认密码' },
|
||||
{ validator: validateConfirmPassword },
|
||||
]">
|
||||
<a-input-password v-model:value="formState.confirmPassword" placeholder="请再次输入密码"
|
||||
size="large" :prefix="h(LockOutlined)" />
|
||||
</a-form-item>
|
||||
<a-form-item name="agreeTerms">
|
||||
<a-checkbox v-model:checked="registerForm.agreeTerms" class="remember-me">
|
||||
我已阅读并同意
|
||||
<a href="#" class="auth-link">服务条款</a>
|
||||
和
|
||||
<a href="#" class="auth-link">隐私政策</a>
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="formState.agree" class="agreement-checkbox">
|
||||
<span class="agreement-text">
|
||||
我已阅读并同意
|
||||
<a href="#" class="link">用户协议</a>
|
||||
和
|
||||
<a href="#" class="link">隐私政策</a>
|
||||
</span>
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||
{{ loading ? '注册中...' : '注册' }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading"
|
||||
:disabled="!formState.agree">
|
||||
注册
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<div class="form-footer">
|
||||
<span>已有账号?</span>
|
||||
<router-link to="/login" class="auth-link">
|
||||
立即登录
|
||||
</router-link>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="auth-footer">
|
||||
<p class="auth-footer-text">
|
||||
已有账户?
|
||||
<router-link to="/login" class="auth-link"> 立即登录 </router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, h } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UserOutlined, MailOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import '@/assets/style/auth-pages.scss'
|
||||
import '@/assets/style/auth.scss'
|
||||
|
||||
defineOptions({
|
||||
name: 'RegisterPage'
|
||||
name: 'RegisterPage',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const registerFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const formState = reactive({
|
||||
// Register form data
|
||||
const registerForm = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agree: false,
|
||||
agreeTerms: false,
|
||||
})
|
||||
|
||||
const validateConfirmPassword = async (rule, value) => {
|
||||
if (value !== formState.password) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
// Form validation rules
|
||||
const registerRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符' },
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码长度不能少于 6 位' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请再次输入密码' },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (value !== registerForm.password) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
},
|
||||
],
|
||||
agreeTerms: [
|
||||
{
|
||||
type: 'enum',
|
||||
enum: [true],
|
||||
message: '请阅读并同意服务条款和隐私政策',
|
||||
transform: (value) => value || false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Handle register
|
||||
const handleRegister = async () => {
|
||||
loading.value = true
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
try {
|
||||
// TODO: 实现注册逻辑
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
message.success('注册成功,请登录')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
await registerFormRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// Simulate API call - Replace with actual API call
|
||||
// Example: const response = await registerApi(registerForm)
|
||||
|
||||
// Simulated delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
|
||||
// Success message
|
||||
message.success('注册成功!正在跳转到登录页面...')
|
||||
|
||||
// Redirect to login page
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('Register failed:', error)
|
||||
message.error('注册失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user