This commit is contained in:
2026-01-27 23:01:04 +08:00
parent 5569e73ef1
commit e846ecdfa1
6 changed files with 1112 additions and 916 deletions

356
src/assets/style/auth.scss Normal file
View 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;
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>