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