Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa2a058ee9 | |||
| 2d6e908109 | |||
| e846ecdfa1 |
@@ -14,10 +14,10 @@
|
|||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@antdv-next/icons": "^1.0.1",
|
||||||
"@ckeditor/ckeditor5-vue": "^7.3.0",
|
"@ckeditor/ckeditor5-vue": "^7.3.0",
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"ant-design-vue": "^4.2.6",
|
"antdv-next": "^1.0.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"ckeditor5": "^47.4.0",
|
"ckeditor5": "^47.4.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { onMounted, computed, watch, nextTick } from 'vue'
|
|||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18nStore } from './stores/modules/i18n'
|
import { useI18nStore } from './stores/modules/i18n'
|
||||||
import { useLayoutStore } from './stores/modules/layout'
|
import { useLayoutStore } from './stores/modules/layout'
|
||||||
import { theme } from 'ant-design-vue'
|
import { theme } from 'antdv-next'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
import zhCN from 'antdv-next/locale/zh_CN'
|
||||||
import enUS from 'ant-design-vue/es/locale/en_US'
|
import enUS from 'antdv-next/locale/en_US'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
import 'dayjs/locale/en'
|
import 'dayjs/locale/en'
|
||||||
|
|||||||
BIN
src/assets/images/default_avatar.jpg
Normal file
BIN
src/assets/images/default_avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
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,4 +1,4 @@
|
|||||||
import * as AIcons from '@ant-design/icons-vue'
|
import * as AIcons from '@antdv-next/icons'
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'antdv-next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表格通用hooks
|
* 表格通用hooks
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</span>
|
</span>
|
||||||
<a v-else @click.prevent="handleLink(item)">
|
<a v-else @click.prevent="handleLink(item)">
|
||||||
<component :is="item.meta?.icon || 'HomeOutlined'" />
|
<component :is="item.meta?.icon || 'FileTextOutlined'" />
|
||||||
{{ item.meta.title }}
|
{{ item.meta.title }}
|
||||||
</a>
|
</a>
|
||||||
</a-breadcrumb-item>
|
</a-breadcrumb-item>
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import config from '@/config'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -23,7 +24,6 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const breadcrumbList = ref([])
|
const breadcrumbList = ref([])
|
||||||
|
|
||||||
// 获取面包屑列表
|
// 获取面包屑列表
|
||||||
@@ -32,16 +32,16 @@ const getBreadcrumb = () => {
|
|||||||
|
|
||||||
// 如果第一个不是首页,添加首页
|
// 如果第一个不是首页,添加首页
|
||||||
const first = matched[0]
|
const first = matched[0]
|
||||||
if (first && first.path !== '/') {
|
if (first && first.path !== config.DASHBOARD_URL) {
|
||||||
matched = [{ path: '/', meta: { title: '首页' } }].concat(matched)
|
matched = [{ path: config.DASHBOARD_URL, meta: { title: '', icon: 'HomeOutlined' } }].concat(matched)
|
||||||
}
|
}
|
||||||
|
|
||||||
breadcrumbList.value = matched
|
breadcrumbList.value = matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理点击面包屑
|
// 处理点击面包屑
|
||||||
const handleLink = (item) => {
|
const handleLink = () => {
|
||||||
router.push(item.path)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import * as icons from '@ant-design/icons-vue'
|
import * as icons from '@antdv-next/icons'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
menuItems: {
|
menuItems: {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { SearchOutlined, MenuOutlined } from '@ant-design/icons-vue'
|
import { SearchOutlined } from '@antdv-next/icons'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'antdv-next'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '@/stores/modules/layout'
|
||||||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
import { CheckOutlined, ReloadOutlined } from '@antdv-next/icons'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -137,8 +137,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'antdv-next'
|
||||||
import { SearchOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
import { SearchOutlined, DeleteOutlined, PlusOutlined } from '@antdv-next/icons'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
// 定义组件名称
|
// 定义组件名称
|
||||||
|
|||||||
@@ -99,22 +99,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 菜单搜索弹窗 -->
|
<!-- 菜单搜索弹窗 -->
|
||||||
<search v-model:visible="searchVisible" />
|
<MenuSearch v-model:visible="searchVisible" />
|
||||||
|
|
||||||
<!-- 任务抽屉 -->
|
<!-- 任务抽屉 -->
|
||||||
<task v-model:visible="taskVisible" v-model:tasks="tasks" />
|
<TaskDrawer v-model:visible="taskVisible" v-model:tasks="tasks" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'antdv-next'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { useI18nStore } from '@/stores/modules/i18n'
|
import { useI18nStore } from '@/stores/modules/i18n'
|
||||||
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined, DeleteOutlined } from '@antdv-next/icons'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import search from './search.vue'
|
import MenuSearch from './search.vue'
|
||||||
import task from './task.vue'
|
import TaskDrawer from './task.vue'
|
||||||
|
|
||||||
// 定义组件名称(多词命名)
|
// 定义组件名称(多词命名)
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- 第一个侧边栏:显示一级菜单 -->
|
<!-- 第一个侧边栏:显示一级菜单 -->
|
||||||
<a-layout-sider theme="dark" width="70" class="left-sidebar">
|
<a-layout-sider theme="dark" width="70" class="left-sidebar">
|
||||||
<div class="logo-box">
|
<div class="logo-box">
|
||||||
<span class="logo-text">VUE</span>
|
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||||
</div>
|
</div>
|
||||||
<ul class="left-nav">
|
<ul class="left-nav">
|
||||||
<li v-for="(item, index) in menuList" :key="index"
|
<li v-for="(item, index) in menuList" :key="index"
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
:collapsible="true" @collapse="handleCollapse" class="full-menu-sidebar" width="200"
|
:collapsible="true" @collapse="handleCollapse" class="full-menu-sidebar" width="200"
|
||||||
:collapsed-width="64">
|
:collapsed-width="64">
|
||||||
<div class="logo-box-full">
|
<div class="logo-box-full">
|
||||||
<span v-if="!sidebarCollapsed" class="logo-text">VUE ADMIN</span>
|
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||||
<span v-else class="logo-text-mini">V</span>
|
<span v-if="!sidebarCollapsed" class="app-name">{{ config.APP_NAME }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||||||
:selected-keys="[route.path]">
|
:selected-keys="[route.path]">
|
||||||
@@ -87,7 +87,8 @@
|
|||||||
<a-layout-header class="app-header top-header">
|
<a-layout-header class="app-header top-header">
|
||||||
<div class="top-header-left">
|
<div class="top-header-left">
|
||||||
<div class="logo-box-top">
|
<div class="logo-box-top">
|
||||||
<span class="logo-text">VUE ADMIN</span>
|
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||||
|
<span class="app-name">{{ config.APP_NAME }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
||||||
style="line-height: 60px">
|
style="line-height: 60px">
|
||||||
@@ -123,8 +124,9 @@ import { computed, ref, watch, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useLayoutStore } from '@/stores/modules/layout'
|
import { useLayoutStore } from '@/stores/modules/layout'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { SettingOutlined } from '@ant-design/icons-vue'
|
import { SettingOutlined } from '@antdv-next/icons'
|
||||||
import * as icons from '@ant-design/icons-vue'
|
import * as icons from '@antdv-next/icons'
|
||||||
|
import config from '@/config/index.js'
|
||||||
|
|
||||||
import userbar from './components/userbar.vue'
|
import userbar from './components/userbar.vue'
|
||||||
import navMenu from './components/navMenu.vue'
|
import navMenu from './components/navMenu.vue'
|
||||||
@@ -366,11 +368,10 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
.logo-text {
|
.logo-image {
|
||||||
color: #ffffff;
|
width: 40px;
|
||||||
font-size: 20px;
|
height: 40px;
|
||||||
font-weight: bold;
|
object-fit: contain;
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,18 +513,24 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
.logo-text {
|
.logo-image {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
.logo-text-mini {
|
text-overflow: ellipsis;
|
||||||
color: #1890ff;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,10 +632,21 @@ onMounted(() => {
|
|||||||
gap: 30px;
|
gap: 30px;
|
||||||
|
|
||||||
.logo-box-top {
|
.logo-box-top {
|
||||||
.logo-text {
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
color: #1890ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,401 +1,228 @@
|
|||||||
<script setup>
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const goHome = () => {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const goBack = () => {
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="not-found">
|
<div class="not-found-container">
|
||||||
<div class="content-wrapper">
|
<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-code">404</div>
|
||||||
<div class="error-text">
|
<div class="error-title">页面未找到</div>
|
||||||
<h1>页面未找到</h1>
|
<div class="error-description">抱歉,您访问的页面不存在或已被移除</div>
|
||||||
<p>抱歉,您访问的页面不存在或已被移除</p>
|
|
||||||
</div>
|
|
||||||
<div class="error-illustration">
|
|
||||||
<div class="planet"></div>
|
|
||||||
<div class="star star-1"></div>
|
|
||||||
<div class="star star-2"></div>
|
|
||||||
<div class="star star-3"></div>
|
|
||||||
<div class="rocket">
|
|
||||||
<div class="rocket-body"></div>
|
|
||||||
<div class="rocket-window"></div>
|
|
||||||
<div class="rocket-flame"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn btn-primary" @click="goHome">
|
<a-button type="primary" size="large" @click="goBack">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<template #icon>
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
<ArrowLeftOutlined />
|
||||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
</template>
|
||||||
</svg>
|
|
||||||
返回首页
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" @click="goBack">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7"></path>
|
|
||||||
</svg>
|
|
||||||
返回上一页
|
返回上一页
|
||||||
</button>
|
</a-button>
|
||||||
|
<a-button size="large" @click="goHome">
|
||||||
|
<template #icon>
|
||||||
|
<HomeOutlined />
|
||||||
|
</template>
|
||||||
|
返回首页
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup>
|
||||||
.not-found {
|
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;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, #0c1929 0%, #1a237e 50%, #0d47a1 100%);
|
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
/* 星空背景 */
|
.tech-decoration {
|
||||||
.not-found::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
pointer-events: none;
|
||||||
bottom: 0;
|
overflow: hidden;
|
||||||
background-image:
|
|
||||||
radial-gradient(2px 2px at 20px 30px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(2px 2px at 40px 70px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(1px 1px at 90px 40px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(2px 2px at 160px 120px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(1px 1px at 230px 80px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(2px 2px at 300px 150px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(1px 1px at 370px 200px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(2px 2px at 450px 50px, #ffffff, rgba(0, 0, 0, 0)),
|
|
||||||
radial-gradient(1px 1px at 520px 180px, #ffffff, rgba(0, 0, 0, 0));
|
|
||||||
background-size: 550px 250px;
|
|
||||||
animation: stars 50s linear infinite;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stars {
|
.tech-circle {
|
||||||
0% {
|
position: absolute;
|
||||||
background-position: 0 0;
|
border: 2px solid rgba(255, 107, 53, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
.tech-circle:nth-child(1) {
|
||||||
background-position: 550px 250px;
|
width: 300px;
|
||||||
}
|
height: 300px;
|
||||||
}
|
top: -150px;
|
||||||
|
left: -150px;
|
||||||
.content-wrapper {
|
animation-delay: 0s;
|
||||||
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 {
|
.tech-circle:nth-child(2) {
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-code {
|
|
||||||
font-size: 180px;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
position: relative;
|
|
||||||
animation: float 3s ease-in-out infinite;
|
|
||||||
text-shadow: 0 0 60px rgba(0, 212, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text h1 {
|
|
||||||
font-size: 36px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
margin: 0 0 12px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text p {
|
|
||||||
font-size: 16px;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-illustration {
|
|
||||||
position: relative;
|
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
margin: 0 auto 40px;
|
bottom: -100px;
|
||||||
}
|
right: -100px;
|
||||||
|
|
||||||
.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;
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-circle:nth-child(3) {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
bottom: 20%;
|
||||||
|
left: -75px;
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes twinkle {
|
@keyframes pulse {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 0.3;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.6;
|
||||||
transform: scale(0.8);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rocket {
|
.not-found-content {
|
||||||
position: absolute;
|
text-align: center;
|
||||||
top: 30%;
|
padding: 40px;
|
||||||
right: 0;
|
|
||||||
animation: flyRocket 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rocket-body {
|
|
||||||
width: 20px;
|
|
||||||
height: 40px;
|
|
||||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
|
||||||
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
z-index: 1;
|
||||||
|
|
||||||
.rocket-window {
|
.error-code {
|
||||||
width: 10px;
|
font-size: 120px;
|
||||||
height: 10px;
|
font-weight: 700;
|
||||||
background: white;
|
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||||
border-radius: 50%;
|
-webkit-background-clip: text;
|
||||||
position: absolute;
|
-webkit-text-fill-color: transparent;
|
||||||
top: 12px;
|
background-clip: text;
|
||||||
left: 50%;
|
margin-bottom: 20px;
|
||||||
transform: translateX(-50%);
|
line-height: 1;
|
||||||
box-shadow: inset -2px -2px 4px rgba(0, 0, 0, 0.2);
|
animation: float 3s ease-in-out infinite;
|
||||||
}
|
|
||||||
|
|
||||||
.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% {
|
@keyframes float {
|
||||||
height: 30px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flyRocket {
|
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0) rotate(-15deg);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-15px) rotate(-10deg);
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.error-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.ant-btn {
|
||||||
display: flex;
|
height: 48px;
|
||||||
align-items: center;
|
padding: 0 32px;
|
||||||
gap: 8px;
|
font-size: 16px;
|
||||||
padding: 14px 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
transition: all 0.3s ease;
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||||
.btn svg {
|
transform: translateY(-2px);
|
||||||
flex-shrink: 0;
|
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%);
|
&:not(.ant-btn-primary) {
|
||||||
color: white;
|
background: rgba(255, 255, 255, 0.9);
|
||||||
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
|
border: 2px solid var(--border-color);
|
||||||
}
|
color: var(--text-secondary);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
.btn-primary:hover {
|
transition: all 0.3s ease;
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 6px 25px rgba(0, 212, 255, 0.6);
|
&:hover {
|
||||||
}
|
border-color: var(--auth-primary);
|
||||||
|
color: var(--auth-primary);
|
||||||
.btn-secondary {
|
transform: translateY(-2px);
|
||||||
background: rgba(255, 255, 255, 0.1);
|
box-shadow: 0 8px 20px rgba(255, 107, 53, 0.15);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.not-found-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
.error-code {
|
.error-code {
|
||||||
font-size: 120px;
|
font-size: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text h1 {
|
.error-title {
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text p {
|
.error-description {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-illustration {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column;
|
.ant-btn {
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,170 +1,146 @@
|
|||||||
<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>
|
<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-content">
|
||||||
<div class="empty-illustration">
|
<div class="empty-icon">
|
||||||
<div class="illustration-wrapper">
|
<InboxOutlined :style="{ fontSize: '120px', color: '#ff6b35' }" />
|
||||||
<svg class="empty-icon" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="boxGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color: #667eea; stop-opacity: 0.2" />
|
|
||||||
<stop offset="100%" style="stop-color: #764ba2; stop-opacity: 0.2" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="searchGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color: #00d4ff; stop-opacity: 1" />
|
|
||||||
<stop offset="100%" style="stop-color: #7c4dff; stop-opacity: 1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- 背景圆 -->
|
|
||||||
<circle cx="100" cy="100" r="80" fill="url(#boxGradient)" />
|
|
||||||
|
|
||||||
<!-- 搜索放大镜 -->
|
|
||||||
<g transform="translate(70, 70)">
|
|
||||||
<circle cx="30" cy="30" r="25" stroke="url(#searchGradient)" stroke-width="4" fill="none" />
|
|
||||||
<line x1="50" y1="50" x2="70" y2="70" stroke="url(#searchGradient)" stroke-width="4"
|
|
||||||
stroke-linecap="round" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- 小装饰元素 -->
|
|
||||||
<circle cx="50" cy="60" r="4" fill="#667eea" opacity="0.6">
|
|
||||||
<animate attributeName="cy" values="60;55;60" dur="2s" repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
<circle cx="150" cy="80" r="3" fill="#764ba2" opacity="0.6">
|
|
||||||
<animate attributeName="cy" values="80;75;80" dur="1.5s" repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
<circle cx="60" cy="140" r="5" fill="#00d4ff" opacity="0.5">
|
|
||||||
<animate attributeName="cy" values="140;145;140" dur="2.5s" repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="empty-text">
|
<div class="empty-title">暂无数据</div>
|
||||||
<h3 class="empty-title">{{ title }}</h3>
|
<div class="empty-description">
|
||||||
<p class="empty-description">{{ description }}</p>
|
{{ description || '当前页面暂无数据,请稍后再试' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showAction" class="empty-action">
|
<a-button v-if="showButton" type="primary" size="large" @click="handleAction">
|
||||||
<button class="action-btn" @click="handleAction">
|
<template #icon v-if="buttonIcon">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<component :is="buttonIcon" />
|
||||||
<path d="M23 4v6h-6"></path>
|
</template>
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
{{ buttonText || '刷新页面' }}
|
||||||
</svg>
|
</a-button>
|
||||||
{{ actionText }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup>
|
||||||
.empty-state {
|
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||||
min-height: 400px;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 60px 20px;
|
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.03) 0%, rgba(118, 75, 162, 0.03) 100%);
|
|
||||||
border-radius: 16px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
/* 装饰性背景 */
|
.tech-decoration {
|
||||||
.empty-state::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
pointer-events: none;
|
||||||
bottom: 0;
|
overflow: hidden;
|
||||||
background-image:
|
|
||||||
radial-gradient(2px 2px at 20px 30px, rgba(102, 126, 234, 0.3), transparent),
|
|
||||||
radial-gradient(2px 2px at 40px 70px, rgba(118, 75, 162, 0.3), transparent),
|
|
||||||
radial-gradient(1px 1px at 90px 40px, rgba(0, 212, 255, 0.3), transparent),
|
|
||||||
radial-gradient(2px 2px at 160px 120px, rgba(102, 126, 234, 0.3), transparent);
|
|
||||||
background-size: 200px 150px;
|
|
||||||
animation: floatBg 20s linear infinite;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatBg {
|
.tech-circle {
|
||||||
0% {
|
position: absolute;
|
||||||
background-position: 0 0;
|
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% {
|
100% {
|
||||||
background-position: 200px 150px;
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-width: 480px;
|
|
||||||
animation: fadeIn 0.6s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.empty-icon {
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-illustration {
|
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
position: relative;
|
animation: float 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.illustration-wrapper {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
width: 180px;
|
|
||||||
height: 180px;
|
|
||||||
animation: floatIcon 3s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes floatIcon {
|
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -173,116 +149,63 @@ const handleAction = () => {
|
|||||||
50% {
|
50% {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-title {
|
||||||
margin-bottom: 32px;
|
font-size: 28px;
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
margin: 0 0 12px;
|
margin-bottom: 16px;
|
||||||
letter-spacing: 0.5px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.empty-description {
|
.empty-description {
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
color: #666;
|
color: var(--text-secondary);
|
||||||
margin: 0;
|
margin-bottom: 40px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-action {
|
.ant-btn {
|
||||||
display: flex;
|
height: 48px;
|
||||||
justify-content: center;
|
padding: 0 40px;
|
||||||
}
|
font-size: 16px;
|
||||||
|
|
||||||
.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;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
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;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.4);
|
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn svg {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 暗色主题适配 */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.empty-title {
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-description {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式适配 */
|
// Responsive design
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.empty-state {
|
.empty-content {
|
||||||
min-height: 300px;
|
padding: 20px;
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
:deep(.anticon) {
|
||||||
width: 140px;
|
font-size: 80px;
|
||||||
height: 140px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-title {
|
.empty-title {
|
||||||
font-size: 20px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-description {
|
.empty-description {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.ant-btn {
|
||||||
padding: 10px 24px;
|
width: 100%;
|
||||||
font-size: 14px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.empty-state {
|
|
||||||
padding: 30px 16px;
|
|
||||||
min-height: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-description {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
import Antd from 'ant-design-vue'
|
import Antd from 'antdv-next'
|
||||||
import 'ant-design-vue/dist/reset.css'
|
import 'antdv-next/dist/reset.css'
|
||||||
import '@/assets/style/app.scss'
|
import '@/assets/style/app.scss'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-card :bordered="true" title="系统信息" class="info-card">
|
<div class="info-card">
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">系统信息</span>
|
||||||
|
</div>
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-descriptions bordered :column="1">
|
<a-descriptions bordered :column="1">
|
||||||
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
||||||
@@ -7,7 +10,7 @@
|
|||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -42,8 +45,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.info-card {
|
.info-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 16px 0;
|
||||||
|
.header{
|
||||||
|
padding-bottom: 10px;
|
||||||
|
.title{
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:deep(.ant-descriptions-item-label) {
|
:deep(.ant-descriptions-item-label) {
|
||||||
width: 120px;
|
width: 140px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-card :bordered="true" title="版本信息" class="ver-card">
|
<div class="ver-card">
|
||||||
|
<div class="header">
|
||||||
|
<span class="title">版本信息</span>
|
||||||
|
</div>
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-descriptions bordered :column="1">
|
<a-descriptions bordered :column="1">
|
||||||
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
<a-descriptions-item v-for="(item, index) in sysInfo" :key="index" :label="item.label">
|
||||||
@@ -7,7 +10,7 @@
|
|||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</a-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -48,8 +51,18 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.ver-card {
|
.ver-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 16px 0;
|
||||||
|
.header{
|
||||||
|
padding-bottom: 10px;
|
||||||
|
.title{
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:deep(.ant-descriptions-item-label) {
|
:deep(.ant-descriptions-item-label) {
|
||||||
width: 120px;
|
width: 160px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,201 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container auth-container">
|
<div class="auth-container">
|
||||||
<!-- 科技感背景 -->
|
<div class="tech-decoration">
|
||||||
<div class="tech-bg">
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></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>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<div class="auth-card">
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 右侧表单区 -->
|
|
||||||
<div class="form-area">
|
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<h1>Vue Admin</h1>
|
<h1 class="auth-title">欢迎回来</h1>
|
||||||
<p class="subtitle">登录您的账户</p>
|
<p class="auth-subtitle">登录您的账户继续探索科技世界</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form :model="formState" @finish="handleLogin" layout="vertical" class="auth-form login-form">
|
<a-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="auth-form" @finish="handleLogin" layout="vertical">
|
||||||
<a-form-item name="username" :rules="[{ required: true, message: '请输入用户名' }]">
|
<a-form-item name="username">
|
||||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" size="large"
|
<a-input v-model:value="loginForm.username" placeholder="请输入用户名/邮箱" size="large">
|
||||||
:prefix="h(UserOutlined)" />
|
<template #prefix>
|
||||||
|
<UserOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="password" :rules="[{ required: true, message: '请输入密码' }]">
|
<a-form-item name="password">
|
||||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" size="large"
|
<a-input-password v-model:value="loginForm.password" placeholder="请输入密码" size="large" @pressEnter="handleLogin">
|
||||||
:prefix="h(LockOutlined)" />
|
<template #prefix>
|
||||||
|
<LockOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input-password>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="form-options">
|
<div class="auth-links">
|
||||||
<a-checkbox v-model:checked="formState.remember">记住密码</a-checkbox>
|
<a-checkbox v-model:checked="loginForm.rememberMe" class="remember-me"> 记住我 </a-checkbox>
|
||||||
<router-link to="/reset-password" class="forgot-password">
|
<router-link to="/reset-password" class="forgot-password"> 忘记密码? </router-link>
|
||||||
忘记密码?
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||||
登录
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="form-footer">
|
|
||||||
<span>还没有账号?</span>
|
|
||||||
<router-link to="/register" class="auth-link">
|
|
||||||
立即注册
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</a-form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, h } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import authApi from '@/api/auth'
|
import auth from '@/api/auth'
|
||||||
import '@/assets/style/auth-pages.scss'
|
import config from '@/config'
|
||||||
|
import system from '@/api/system'
|
||||||
|
import '@/assets/style/auth.scss'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LoginPage'
|
name: 'LoginPage',
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const loginFormRef = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const formState = reactive({
|
// Login form data
|
||||||
|
const loginForm = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
remember: false,
|
rememberMe: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Form validation rules
|
||||||
|
const loginRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名或邮箱' },
|
||||||
|
{ min: 3, max: 50, message: '长度在 3 到 50 个字符' },
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ min: 6, message: '密码长度不能少于 6 位' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle login
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
loading.value = true
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res = await authApi.login.post({
|
// Validate form
|
||||||
username: formState.username,
|
await loginFormRef.value.validate()
|
||||||
password: formState.password,
|
loading.value = true
|
||||||
|
|
||||||
|
// 1. Call login API
|
||||||
|
const loginResponse = await auth.login.post({
|
||||||
|
username: loginForm.username,
|
||||||
|
password: loginForm.password,
|
||||||
})
|
})
|
||||||
if (res.code == 1) {
|
|
||||||
userStore.setToken(res.data.access_token)
|
// Check if login was successful
|
||||||
let userInfo = await authApi.user.get()
|
if (loginResponse.code !== 1) {
|
||||||
if (userInfo.code == 1) {
|
throw new Error(loginResponse.msg || '登录失败')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('登录成功')
|
const loginData = loginResponse.data
|
||||||
const redirect = router.currentRoute.value.query.redirect || '/'
|
|
||||||
router.push(redirect)
|
// 2. Store access_token persistently
|
||||||
|
if (loginData.access_token) {
|
||||||
|
userStore.setToken(loginData.access_token)
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
message.error('登录失败,请检查用户名和密码')
|
// 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 {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,156 +1,164 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reset-container auth-container">
|
<div class="auth-container">
|
||||||
<!-- 科技感背景 -->
|
<div class="tech-decoration">
|
||||||
<div class="tech-bg">
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></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>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<div class="auth-card">
|
||||||
<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>
|
|
||||||
<div class="decoration-text">
|
|
||||||
<h2>重置密码</h2>
|
|
||||||
<p>找回您的账户访问权限</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧表单区 -->
|
|
||||||
<div class="form-area">
|
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<h1>Vue Admin</h1>
|
<h1 class="auth-title">找回密码</h1>
|
||||||
<p class="subtitle">设置新密码</p>
|
<p class="auth-subtitle">输入您的邮箱,我们将发送重置密码链接</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form :model="formState" @finish="handleReset" layout="vertical" class="auth-form reset-form">
|
<a-form ref="forgotFormRef" :model="forgotForm" :rules="forgotRules" class="auth-form" @finish="handleSubmit" layout="vertical">
|
||||||
<a-form-item name="email" :rules="[
|
<a-form-item name="email">
|
||||||
{ required: true, message: '请输入邮箱' },
|
<a-input v-model:value="forgotForm.email" placeholder="请输入注册邮箱" size="large" @pressEnter="handleSubmit">
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
<template #prefix>
|
||||||
]">
|
<MailOutlined />
|
||||||
<a-input v-model:value="formState.email" placeholder="请输入邮箱地址" size="large"
|
</template>
|
||||||
:prefix="h(MailOutlined)" />
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="verificationCode" :rules="[{ required: true, message: '请输入验证码' }]">
|
<a-form-item name="captcha" v-if="showCaptcha">
|
||||||
<div class="code-input-wrapper">
|
<div style="display: flex; gap: 12px">
|
||||||
<a-input v-model:value="formState.verificationCode" placeholder="请输入验证码" size="large"
|
<a-input v-model:value="forgotForm.captcha" placeholder="请输入验证码" size="large" style="flex: 1">
|
||||||
:prefix="h(SafetyOutlined)" class="code-input" />
|
<template #prefix>
|
||||||
<a-button type="primary" :disabled="countdown > 0" @click="sendCode" class="code-btn"
|
<SafetyOutlined />
|
||||||
size="large">
|
</template>
|
||||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
</a-input>
|
||||||
|
<a-button type="default" size="large" :disabled="captchaDisabled" @click="sendCaptcha">
|
||||||
|
{{ captchaButtonText }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</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-form-item>
|
||||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
|
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||||
重置密码
|
{{ loading ? '提交中...' : '发送重置链接' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="form-footer">
|
|
||||||
<span>记得密码了?</span>
|
|
||||||
<router-link to="/login" class="auth-link">
|
|
||||||
立即登录
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</a-form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, h } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { MailOutlined, SafetyOutlined, LockOutlined } from '@ant-design/icons-vue'
|
import { MailOutlined, SafetyOutlined } from '@ant-design/icons-vue'
|
||||||
import '@/assets/style/auth-pages.scss'
|
import '@/assets/style/auth.scss'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ResetPasswordPage'
|
name: 'ResetPasswordPage',
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const forgotFormRef = ref(null)
|
||||||
const loading = ref(false)
|
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: '',
|
email: '',
|
||||||
verificationCode: '',
|
captcha: '',
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const validateConfirmPassword = async (rule, value) => {
|
// Captcha button text
|
||||||
if (value !== formState.newPassword) {
|
const captchaButtonText = ref('获取验证码')
|
||||||
return Promise.reject('两次输入的密码不一致')
|
|
||||||
}
|
// Form validation rules
|
||||||
return Promise.resolve()
|
const forgotRules = {
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱地址' },
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱地址' },
|
||||||
|
],
|
||||||
|
captcha: [
|
||||||
|
{ required: true, message: '请输入验证码' },
|
||||||
|
{ len: 6, message: '验证码为6位数字' },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendCode = () => {
|
// Send captcha code
|
||||||
if (!formState.email) {
|
const sendCaptcha = async () => {
|
||||||
|
if (!forgotForm.email) {
|
||||||
message.warning('请先输入邮箱地址')
|
message.warning('请先输入邮箱地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 实现发送验证码逻辑
|
// Validate email format
|
||||||
message.success('验证码已发送')
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
countdown.value = 60
|
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(() => {
|
const timer = setInterval(() => {
|
||||||
countdown.value--
|
countdown.value--
|
||||||
|
captchaButtonText.value = `${countdown.value}秒后重试`
|
||||||
|
|
||||||
if (countdown.value <= 0) {
|
if (countdown.value <= 0) {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
|
captchaDisabled.value = false
|
||||||
|
captchaButtonText.value = '获取验证码'
|
||||||
|
countdown.value = 60
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
showCaptcha.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send captcha failed:', error)
|
||||||
|
message.error('发送验证码失败,请稍后重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = async () => {
|
// Handle submit
|
||||||
loading.value = true
|
const handleSubmit = async () => {
|
||||||
|
if (!forgotFormRef.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 实现重置密码逻辑
|
await forgotFormRef.value.validate()
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
loading.value = true
|
||||||
message.success('密码重置成功,请登录')
|
|
||||||
|
// 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')
|
router.push('/login')
|
||||||
} catch {
|
}, 2000)
|
||||||
message.error('密码重置失败,请稍后重试')
|
} catch (error) {
|
||||||
|
console.error('Forgot password failed:', error)
|
||||||
|
message.error('提交失败,请检查邮箱地址和验证码')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,149 +1,161 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="register-container auth-container">
|
<div class="auth-container">
|
||||||
<!-- 科技感背景 -->
|
<div class="tech-decoration">
|
||||||
<div class="tech-bg">
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></div>
|
||||||
<div class="grid-line"></div>
|
<div class="tech-circle"></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>
|
</div>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<div class="auth-card">
|
||||||
<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="form-area">
|
|
||||||
<div class="auth-header">
|
<div class="auth-header">
|
||||||
<h1>Vue Admin</h1>
|
<h1 class="auth-title">创建账户</h1>
|
||||||
<p class="subtitle">注册新账户</p>
|
<p class="auth-subtitle">加入我们,开启科技之旅</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form :model="formState" @finish="handleRegister" layout="vertical"
|
<a-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="auth-form" @finish="handleRegister" layout="vertical">
|
||||||
class="auth-form register-form">
|
<a-form-item name="username">
|
||||||
<a-form-item name="username" :rules="[
|
<a-input v-model:value="registerForm.username" placeholder="请输入用户名" size="large">
|
||||||
{ required: true, message: '请输入用户名' },
|
<template #prefix>
|
||||||
{ min: 3, message: '用户名至少3个字符' },
|
<UserOutlined />
|
||||||
]">
|
</template>
|
||||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" size="large"
|
</a-input>
|
||||||
:prefix="h(UserOutlined)" />
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="email" :rules="[
|
<a-form-item name="email">
|
||||||
{ required: true, message: '请输入邮箱' },
|
<a-input v-model:value="registerForm.email" placeholder="请输入邮箱地址" size="large">
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
<template #prefix>
|
||||||
]">
|
<MailOutlined />
|
||||||
<a-input v-model:value="formState.email" placeholder="请输入邮箱地址" size="large"
|
</template>
|
||||||
:prefix="h(MailOutlined)" />
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="password" :rules="[
|
<a-form-item name="password">
|
||||||
{ required: true, message: '请输入密码' },
|
<a-input-password v-model:value="registerForm.password" placeholder="请输入密码(至少6位)" size="large">
|
||||||
{ min: 6, message: '密码至少6个字符' },
|
<template #prefix>
|
||||||
]">
|
<LockOutlined />
|
||||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" size="large"
|
</template>
|
||||||
:prefix="h(LockOutlined)" />
|
</a-input-password>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="confirmPassword" :rules="[
|
<a-form-item name="confirmPassword">
|
||||||
{ required: true, message: '请确认密码' },
|
<a-input-password v-model:value="registerForm.confirmPassword" placeholder="请再次输入密码" size="large" @pressEnter="handleRegister">
|
||||||
{ validator: validateConfirmPassword },
|
<template #prefix>
|
||||||
]">
|
<LockOutlined />
|
||||||
<a-input-password v-model:value="formState.confirmPassword" placeholder="请再次输入密码"
|
</template>
|
||||||
size="large" :prefix="h(LockOutlined)" />
|
</a-input-password>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item name="agreeTerms">
|
||||||
<a-checkbox v-model:checked="formState.agree" class="agreement-checkbox">
|
<a-checkbox v-model:checked="registerForm.agreeTerms" class="remember-me">
|
||||||
<span class="agreement-text">
|
|
||||||
我已阅读并同意
|
我已阅读并同意
|
||||||
<a href="#" class="link">用户协议</a>
|
<a href="#" class="auth-link">服务条款</a>
|
||||||
和
|
和
|
||||||
<a href="#" class="link">隐私政策</a>
|
<a href="#" class="auth-link">隐私政策</a>
|
||||||
</span>
|
|
||||||
</a-checkbox>
|
</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" html-type="submit" size="large" block :loading="loading"
|
<a-button type="primary" :loading="loading" size="large" html-type="submit" block>
|
||||||
:disabled="!formState.agree">
|
{{ loading ? '注册中...' : '注册' }}
|
||||||
注册
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="form-footer">
|
|
||||||
<span>已有账号?</span>
|
|
||||||
<router-link to="/login" class="auth-link">
|
|
||||||
立即登录
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</a-form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, h } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { UserOutlined, MailOutlined, LockOutlined } from '@ant-design/icons-vue'
|
import { UserOutlined, MailOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||||
import '@/assets/style/auth-pages.scss'
|
import '@/assets/style/auth.scss'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'RegisterPage'
|
name: 'RegisterPage',
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const registerFormRef = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const formState = reactive({
|
// Register form data
|
||||||
|
const registerForm = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
agree: false,
|
agreeTerms: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const validateConfirmPassword = async (rule, value) => {
|
// Form validation rules
|
||||||
if (value !== formState.password) {
|
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.reject('两次输入的密码不一致')
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
agreeTerms: [
|
||||||
|
{
|
||||||
|
type: 'enum',
|
||||||
|
enum: [true],
|
||||||
|
message: '请阅读并同意服务条款和隐私政策',
|
||||||
|
transform: (value) => value || false,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle register
|
||||||
const handleRegister = async () => {
|
const handleRegister = async () => {
|
||||||
loading.value = true
|
if (!registerFormRef.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 实现注册逻辑
|
await registerFormRef.value.validate()
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
loading.value = true
|
||||||
message.success('注册成功,请登录')
|
|
||||||
|
// 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')
|
router.push('/login')
|
||||||
} catch {
|
}, 1500)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register failed:', error)
|
||||||
message.error('注册失败,请稍后重试')
|
message.error('注册失败,请稍后重试')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 所有样式已移至统一样式文件 src/assets/style/auth-pages.scss */
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<!-- 主要内容区域 -->
|
<!-- 主要内容区域 -->
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
<a-card>
|
||||||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="setting-tabs">
|
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="setting-tabs">
|
||||||
<template #rightExtra>
|
<template #rightExtra>
|
||||||
<a-button type="primary" @click="handleAddConfig">
|
<a-button type="primary" @click="handleAddConfig">
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
</a-empty>
|
</a-empty>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 配置弹窗 -->
|
<!-- 配置弹窗 -->
|
||||||
@@ -410,7 +412,6 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
:deep(.ant-tabs-tab-btn) {
|
:deep(.ant-tabs-tab-btn) {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.ucenter {
|
.ucenter {
|
||||||
|
padding: 16px;
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
import { useUserStore } from '@/stores/modules/user'
|
import { useUserStore } from '@/stores/modules/user'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'antdv-next'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user