feat: 重构前端架构并实现管理系统基础功能
- 添加Element Plus图标库依赖 - 重构路由结构,实现布局组件和嵌套路由 - 实现登录页面动态角色动画效果 - 开发主布局框架,包含侧边栏和面包屑导航 - 新增用户管理、系统设置和首页功能模块 - 移除旧版样式和示例组件 - 优化全局样式和响应式设计
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.8.4",
|
||||
"element-plus": "^2.9.7",
|
||||
"pinia": "^3.0.1",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.2(vue@3.5.32(typescript@6.0.3))
|
||||
axios:
|
||||
specifier: ^1.8.4
|
||||
version: 1.15.0
|
||||
|
||||
15
src/App.vue
15
src/App.vue
@@ -1,7 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HelloWorld />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
360
src/layouts/MainLayout.vue
Normal file
360
src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import {
|
||||
HomeFilled,
|
||||
UserFilled,
|
||||
Setting,
|
||||
Menu,
|
||||
ArrowDown,
|
||||
Grid,
|
||||
Document,
|
||||
DataAnalysis
|
||||
} from '@element-plus/icons-vue'
|
||||
import Breadcrumb from './components/Breadcrumb.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isCollapse = ref(false)
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/home', name: '首页', icon: HomeFilled },
|
||||
{ path: '/user', name: '用户管理', icon: UserFilled },
|
||||
{ path: '/system/settings', name: '系统设置', icon: Setting },
|
||||
]
|
||||
|
||||
function handleSelect(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function handleCommand(command: string) {
|
||||
if (command === 'logout') {
|
||||
userStore.clearUser()
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapse() {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="sidebar" :class="{ 'is-collapse': isCollapse }">
|
||||
<!-- Logo区域 -->
|
||||
<div class="logo-section">
|
||||
<div class="logo-icon">
|
||||
<el-icon :size="28"><Grid /></el-icon>
|
||||
</div>
|
||||
<span v-show="!isCollapse" class="logo-text">管理系统</span>
|
||||
</div>
|
||||
|
||||
<!-- 菜单区域 -->
|
||||
<div class="menu-container">
|
||||
<el-menu
|
||||
:default-active="route.path"
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
class="custom-menu"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:index="item.path"
|
||||
class="menu-item"
|
||||
>
|
||||
<el-icon class="menu-icon">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span class="menu-title">{{ item.name }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 底部收缩按钮 -->
|
||||
<div class="collapse-section" @click="toggleCollapse">
|
||||
<el-icon class="collapse-arrow" :class="{ 'is-rotate': isCollapse }">
|
||||
<Menu />
|
||||
</el-icon>
|
||||
<span v-show="!isCollapse" class="collapse-text">收起菜单</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="right-container">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :icon="UserFilled" class="user-avatar" />
|
||||
<span class="user-name">{{ userStore.userInfo?.username || '管理员' }}</span>
|
||||
<el-icon class="user-arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-icon><UserFilled /></el-icon>个人中心
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="settings">
|
||||
<el-icon><Setting /></el-icon>系统设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-icon><Document /></el-icon>退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.sidebar {
|
||||
background: linear-gradient(180deg, #1a1f3c 0%, #2d3a5c 100%);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar.is-collapse {
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-section {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 菜单区域 */
|
||||
.menu-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px 12px;
|
||||
}
|
||||
|
||||
.menu-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.menu-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.custom-menu {
|
||||
background: transparent !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.custom-menu .el-menu-item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.custom-menu .el-menu-item:hover) {
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.custom-menu .el-menu-item.is-active) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 底部收缩按钮 */
|
||||
.collapse-section {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-section:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.collapse-arrow {
|
||||
font-size: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.collapse-arrow.is-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.collapse-text {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.right-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
flex-shrink: 0;
|
||||
height: 70px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-arrow {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 折叠状态下的菜单样式调整 */
|
||||
:deep(.custom-menu.el-menu--collapse) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.custom-menu.el-menu--collapse .el-menu-item) {
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.custom-menu.el-menu--collapse .el-tooltip__trigger) {
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
29
src/layouts/components/Breadcrumb.vue
Normal file
29
src/layouts/components/Breadcrumb.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||
return matched.map(item => ({
|
||||
path: item.path,
|
||||
title: item.meta.title as string,
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
|
||||
{{ item.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import { pinia } from '@/stores'
|
||||
import router from '@/router'
|
||||
|
||||
@@ -3,16 +3,35 @@ import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/HomeView.vue'),
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/LoginView.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
path: '/',
|
||||
component: () => import('@/layouts/MainLayout.vue'),
|
||||
redirect: '/home',
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: { title: '首页', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
component: () => import('@/views/user/index.vue'),
|
||||
meta: { title: '用户管理', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'system/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/system/settings.vue'),
|
||||
meta: { title: '系统设置', requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -22,13 +41,7 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
296
src/style.css
296
src/style.css
@@ -1,296 +0,0 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const message = ref('欢迎使用 Vue 项目模板')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home">
|
||||
<h1>{{ message }}</h1>
|
||||
<el-button type="primary">Element Plus 按钮</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,72 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { login } from '@/api/userApi'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function handleLogin() {
|
||||
if (!loginForm.value.username || !loginForm.value.password) {
|
||||
ElMessage.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(loginForm.value)
|
||||
userStore.setToken(res.token)
|
||||
userStore.setUserInfo(res.userInfo)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-card class="login-card">
|
||||
<h2>登录</h2>
|
||||
<el-form :model="loginForm" label-width="80px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="loginForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="handleLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
361
src/views/home/index.vue
Normal file
361
src/views/home/index.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import {
|
||||
User,
|
||||
Document,
|
||||
Setting,
|
||||
TrendCharts,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
MoreFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const stats = ref([
|
||||
{ title: '用户总数', value: '1,234', icon: User, color: '#667eea', trend: '+12%', up: true },
|
||||
{ title: '订单数量', value: '5,678', icon: Document, color: '#10b981', trend: '+8%', up: true },
|
||||
{ title: '系统配置', value: '12', icon: Setting, color: '#f59e0b', trend: '0%', up: true },
|
||||
{ title: '今日访问', value: '2,456', icon: TrendCharts, color: '#ef4444', trend: '-3%', up: false },
|
||||
])
|
||||
|
||||
const recentActivities = ref([
|
||||
{ time: '2024-01-15 10:30', content: '管理员登录系统', type: 'success' },
|
||||
{ time: '2024-01-15 09:15', content: '新增用户: user123', type: 'info' },
|
||||
{ time: '2024-01-14 16:45', content: '修改系统配置', type: 'warning' },
|
||||
{ time: '2024-01-14 14:20', content: '删除用户: test_user', type: 'danger' },
|
||||
{ time: '2024-01-14 11:00', content: '备份数据库', type: 'success' },
|
||||
])
|
||||
|
||||
const quickLinks = ref([
|
||||
{ name: '用户管理', path: '/user', icon: User, color: '#667eea' },
|
||||
{ name: '系统设置', path: '/system/settings', icon: Setting, color: '#10b981' },
|
||||
{ name: '数据报表', path: '#', icon: TrendCharts, color: '#f59e0b' },
|
||||
{ name: '日志管理', path: '#', icon: Document, color: '#ef4444' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- 欢迎卡片 -->
|
||||
<el-card class="welcome-card" shadow="hover">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h2>欢迎回来,{{ userStore.userInfo?.username || '管理员' }}!</h2>
|
||||
<p>今天是 {{ new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }) }}</p>
|
||||
</div>
|
||||
<el-button type="primary" plain>查看个人中心</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :xs="24" :sm="12" :lg="6" v-for="item in stats" :key="item.title">
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon" :style="{ backgroundColor: item.color + '15', color: item.color }">
|
||||
<el-icon :size="28">
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ item.value }}</div>
|
||||
<div class="stat-title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-footer">
|
||||
<span class="trend" :class="item.up ? 'up' : 'down'">
|
||||
<el-icon><component :is="item.up ? ArrowUp : ArrowDown" /></el-icon>
|
||||
{{ item.trend }}
|
||||
</span>
|
||||
<span class="trend-label">较昨日</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-row :gutter="20" class="content-row">
|
||||
<el-col :xs="24" :lg="16">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">数据概览</span>
|
||||
<el-radio-group v-model="chartType" size="small">
|
||||
<el-radio-button label="week">本周</el-radio-button>
|
||||
<el-radio-button label="month">本月</el-radio-button>
|
||||
<el-radio-button label="year">全年</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-placeholder">
|
||||
<el-empty description="图表区域 - 可接入 ECharts">
|
||||
<el-button type="primary">配置图表</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :lg="8">
|
||||
<el-card shadow="hover" class="activity-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最近活动</span>
|
||||
<el-link type="primary" :underline="false">查看更多</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(activity, index) in recentActivities"
|
||||
:key="index"
|
||||
:type="activity.type"
|
||||
:timestamp="activity.time"
|
||||
:hollow="true"
|
||||
>
|
||||
{{ activity.content }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<el-card shadow="hover" class="quick-links-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">快捷入口</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="12" :sm="8" :md="6" :lg="4" v-for="link in quickLinks" :key="link.name">
|
||||
<router-link :to="link.path" class="quick-item" :style="{ '--hover-color': link.color }">
|
||||
<div class="quick-icon" :style="{ backgroundColor: link.color + '15', color: link.color }">
|
||||
<el-icon :size="24">
|
||||
<component :is="link.icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<span class="quick-name">{{ link.name }}</span>
|
||||
</router-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const chartType = ref('week')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 欢迎卡片 */
|
||||
.welcome-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.welcome-card :deep(.el-card__body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-row {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
margin-bottom: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trend.up {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.trend.down {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.trend-label {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-row {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.quick-links-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 24px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.quick-item:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.quick-item:hover .quick-icon {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
.quick-item:hover .quick-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-name {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.welcome-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
656
src/views/login/index.vue
Normal file
656
src/views/login/index.vue
Normal file
@@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed inset-0 overflow-hidden"
|
||||
style="background: radial-gradient(circle at 0% 0%, #1f2937 0, #020617 55%, #000000 100%)"
|
||||
@mousemove="onMouseMove"
|
||||
>
|
||||
<!-- 背景光晕 -->
|
||||
<div
|
||||
class="fixed pointer-events-none -inset-20 blur-sm opacity-85"
|
||||
style="background: radial-gradient(circle at 10% 20%, rgba(96, 165, 250, 0.16) 0, transparent 55%), radial-gradient(circle at 80% 10%, rgba(244, 114, 182, 0.18) 0, transparent 55%), radial-gradient(circle at 50% 90%, rgba(52, 211, 153, 0.12) 0, transparent 60%); transform: translate3d(0, 12px, 0) scale(1.02)"
|
||||
></div>
|
||||
|
||||
<!-- 主容器 -->
|
||||
<div class="absolute inset-0 flex items-center justify-center p-0 m-0 overflow-hidden">
|
||||
<div
|
||||
class="w-full max-w-1120px flex items-center justify-between gap-10 px-8 py-10 box-border"
|
||||
>
|
||||
<!-- 左侧品牌区域 -->
|
||||
<div class="flex-1 flex items-center justify-center text-white">
|
||||
<div class="relative z-20 flex items-end justify-center h-500px">
|
||||
<div class="relative" style="width: 550px; height: 400px">
|
||||
<!-- 紫色角色 -->
|
||||
<div
|
||||
ref="purpleRef"
|
||||
class="purple-character absolute bottom-0 transition-all duration-300 ease-out"
|
||||
:style="{
|
||||
left: '70px',
|
||||
width: '180px',
|
||||
height: (isTyping || (dataForm.password.length > 0 && !showPassword)) ? '440px' : '400px',
|
||||
backgroundColor: 'rgb(108, 63, 245)',
|
||||
borderRadius: '10px 10px 0px 0px',
|
||||
zIndex: 1,
|
||||
transform: (dataForm.password.length > 0 && showPassword)
|
||||
? 'skewX(0deg)'
|
||||
: (isTyping || (dataForm.password.length > 0 && !showPassword))
|
||||
? `skewX(${(calculatePosition(purpleRef)?.bodySkew || 0) - 12}deg) translateX(40px)`
|
||||
: `skewX(${calculatePosition(purpleRef)?.bodySkew || 0}deg)`,
|
||||
transformOrigin: 'center bottom'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute flex gap-8 transition-all duration-700 ease-in-out"
|
||||
:style="{
|
||||
left: (dataForm.password.length > 0 && showPassword) ? '20px' : isLookingAtEachOther ? '55px' : `${45 + (calculatePosition(purpleRef)?.faceX || 0)}px`,
|
||||
top: (dataForm.password.length > 0 && showPassword) ? '35px' : isLookingAtEachOther ? '65px' : `${40 + (calculatePosition(purpleRef)?.faceY || 0)}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rounded-full flex items-center justify-center transition-all duration-150 overflow-hidden"
|
||||
:style="{
|
||||
width: '22px',
|
||||
height: isPurpleBlinking ? '2px' : '22px',
|
||||
backgroundColor: 'white'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!isPurpleBlinking"
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '11px',
|
||||
height: '11px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? (isPurplePeeking ? '4px' : '-4px') : isLookingAtEachOther ? '3px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? (isPurplePeeking ? '5px' : '-4px') : isLookingAtEachOther ? '4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-full flex items-center justify-center transition-all duration-150 overflow-hidden"
|
||||
:style="{
|
||||
width: '22px',
|
||||
height: isPurpleBlinking ? '2px' : '22px',
|
||||
backgroundColor: 'white'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!isPurpleBlinking"
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '11px',
|
||||
height: '11px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? (isPurplePeeking ? '4px' : '-4px') : isLookingAtEachOther ? '3px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? (isPurplePeeking ? '5px' : '-4px') : isLookingAtEachOther ? '4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 黑色角色 -->
|
||||
<div
|
||||
ref="blackRef"
|
||||
class="black-character absolute bottom-0 transition-all duration-300 ease-out"
|
||||
:style="{
|
||||
left: '240px',
|
||||
width: '120px',
|
||||
height: '310px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
borderRadius: '8px 8px 0px 0px',
|
||||
zIndex: 2,
|
||||
transform: (dataForm.password.length > 0 && showPassword)
|
||||
? 'skewX(0deg)'
|
||||
: isLookingAtEachOther
|
||||
? `skewX(${(calculatePosition(blackRef)?.bodySkew || 0) * 1.5 + 10}deg) translateX(20px)`
|
||||
: (isTyping || (dataForm.password.length > 0 && !showPassword))
|
||||
? `skewX(${(calculatePosition(blackRef)?.bodySkew || 0) * 1.5}deg)`
|
||||
: `skewX(${calculatePosition(blackRef)?.bodySkew || 0}deg)`,
|
||||
transformOrigin: 'center bottom'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute flex gap-6 transition-all duration-700 ease-in-out"
|
||||
:style="{
|
||||
left: (dataForm.password.length > 0 && showPassword) ? '10px' : isLookingAtEachOther ? '32px' : `${26 + (calculatePosition(blackRef)?.faceX || 0)}px`,
|
||||
top: (dataForm.password.length > 0 && showPassword) ? '28px' : isLookingAtEachOther ? '12px' : `${32 + (calculatePosition(blackRef)?.faceY || 0)}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rounded-full flex items-center justify-center transition-all duration-150 overflow-hidden"
|
||||
:style="{
|
||||
width: '24px',
|
||||
height: isBlackBlinking ? '2px' : '24px',
|
||||
backgroundColor: 'white'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!isBlackBlinking"
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-4px' : isLookingAtEachOther ? '0px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : isLookingAtEachOther ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-full flex items-center justify-center transition-all duration-150 overflow-hidden"
|
||||
:style="{
|
||||
width: '24px',
|
||||
height: isBlackBlinking ? '2px' : '24px',
|
||||
backgroundColor: 'white'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="!isBlackBlinking"
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-4px' : isLookingAtEachOther ? '0px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : isLookingAtEachOther ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 橙色角色 -->
|
||||
<div
|
||||
ref="orangeRef"
|
||||
class="orange-character absolute bottom-0 transition-all duration-300 ease-out"
|
||||
:style="{
|
||||
left: '0px',
|
||||
width: '240px',
|
||||
height: '200px',
|
||||
zIndex: 3,
|
||||
backgroundColor: 'rgb(255, 155, 107)',
|
||||
borderRadius: '120px 120px 0px 0px',
|
||||
transform: (dataForm.password.length > 0 && showPassword) ? 'skewX(0deg)' : `skewX(${calculatePosition(orangeRef)?.bodySkew || 0}deg)`,
|
||||
transformOrigin: 'center bottom'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute flex gap-8 transition-all duration-200 ease-out"
|
||||
:style="{
|
||||
left: (dataForm.password.length > 0 && showPassword) ? '50px' : `${82 + (calculatePosition(orangeRef)?.faceX || 0)}px`,
|
||||
top: (dataForm.password.length > 0 && showPassword) ? '85px' : `${90 + (calculatePosition(orangeRef)?.faceY || 0)}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-5px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-5px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 黄色角色 -->
|
||||
<div
|
||||
ref="yellowRef"
|
||||
class="yellow-character absolute bottom-0 transition-all duration-300 ease-out"
|
||||
:style="{
|
||||
left: '310px',
|
||||
width: '140px',
|
||||
height: '230px',
|
||||
backgroundColor: 'rgb(232, 215, 84)',
|
||||
borderRadius: '70px 70px 0px 0px',
|
||||
zIndex: 4,
|
||||
transform: (dataForm.password.length > 0 && showPassword) ? 'skewX(0deg)' : `skewX(${calculatePosition(yellowRef)?.bodySkew || 0}deg)`,
|
||||
transformOrigin: 'center bottom'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute flex gap-6 transition-all duration-200 ease-out"
|
||||
:style="{
|
||||
left: (dataForm.password.length > 0 && showPassword) ? '20px' : `${52 + (calculatePosition(yellowRef)?.faceX || 0)}px`,
|
||||
top: (dataForm.password.length > 0 && showPassword) ? '35px' : `${40 + (calculatePosition(yellowRef)?.faceY || 0)}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-5px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="rounded-full login-eyes-dot"
|
||||
:style="{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
backgroundColor: 'rgb(45, 45, 45)',
|
||||
transform: `translate(${(dataForm.password.length > 0 && showPassword) ? '-5px' : '0px'}, ${(dataForm.password.length > 0 && showPassword) ? '-4px' : '0px'})`,
|
||||
transition: 'transform 0.1s ease-out'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute w-20 h-1 bg-#2D2D2D rounded-full transition-all duration-200 ease-out login-mouth-line"
|
||||
:style="{
|
||||
left: (dataForm.password.length > 0 && showPassword) ? '10px' : `${40 + (calculatePosition(yellowRef)?.faceX || 0)}px`,
|
||||
top: (dataForm.password.length > 0 && showPassword) ? '88px' : `${88 + (calculatePosition(yellowRef)?.faceY || 0)}px`
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧登录表单 -->
|
||||
<div
|
||||
class="flex-shrink-0 w-105 px-8 pt-8 pb-7 bg-white rounded-24px text-#0f172a"
|
||||
style="box-shadow: 0 24px 60px rgba(15, 23, 42, 0.55); animation: login-card-in 0.9s cubic-bezier(0.19, 0.64, 0.34, 1.01) 0.1s forwards"
|
||||
>
|
||||
<h3
|
||||
class="text-24px font-bold mb-2 text-center"
|
||||
style="background: linear-gradient(135deg, #4f46e5, #6366f1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: 0 2px 4px rgba(79, 70, 229, 0.2)"
|
||||
>
|
||||
管理系统
|
||||
</h3>
|
||||
<h3
|
||||
class="text-20px font-bold mb-6 text-center"
|
||||
style="background: linear-gradient(135deg, #f59e0b, #ef4444); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-shadow: 0 2px 4px rgba(245, 158, 11, 0.2)"
|
||||
>
|
||||
用户登录
|
||||
</h3>
|
||||
|
||||
<el-form
|
||||
:model="dataForm"
|
||||
:rules="dataRule"
|
||||
ref="dataFormRef"
|
||||
@keyup.enter="dataFormSubmit()"
|
||||
status-icon
|
||||
>
|
||||
<el-form-item prop="userName">
|
||||
<el-input
|
||||
v-model="dataForm.userName"
|
||||
placeholder="帐号"
|
||||
@focus="handleInputFocus"
|
||||
@blur="handleInputBlur"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="dataForm.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="密码"
|
||||
@focus="handleInputFocus"
|
||||
@blur="handleInputBlur"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="togglePasswordVisibility" class="password-toggle-btn">
|
||||
<el-icon v-if="showPassword"><View /></el-icon>
|
||||
<el-icon v-else><Hide /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="captcha">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="14">
|
||||
<el-input
|
||||
v-model="dataForm.captcha"
|
||||
placeholder="验证码"
|
||||
@focus="handleInputFocus"
|
||||
@blur="handleInputBlur"
|
||||
></el-input>
|
||||
</el-col>
|
||||
<el-col :span="10" class="overflow-hidden">
|
||||
<img
|
||||
:src="captchaPath"
|
||||
@click="getCaptcha()"
|
||||
alt=""
|
||||
class="w-full cursor-pointer rounded-xl"
|
||||
style="box-shadow: 0 0 0 1px #e5e7eb"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<div
|
||||
v-if="error"
|
||||
class="px-3 py-3 my-3 rounded-lg text-sm text-#ef4444"
|
||||
style="background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); animation: error-fade-in 0.3s ease-out"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="w-full mt-3 h-46px text-15px font-semibold rounded-full border-0"
|
||||
style="background: linear-gradient(135deg, #4f46e5, #6366f1)"
|
||||
@click="dataFormSubmit()"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { View, Hide } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const dataFormRef = ref()
|
||||
|
||||
const mouseX = ref(0)
|
||||
const mouseY = ref(0)
|
||||
const dataForm = ref({
|
||||
userName: 'admin',
|
||||
password: '123456',
|
||||
uuid: '',
|
||||
captcha: ''
|
||||
})
|
||||
const dataRule = {
|
||||
userName: [{ required: true, message: '帐号不能为空', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
|
||||
captcha: [{ required: true, message: '验证码不能为空', trigger: 'blur' }]
|
||||
}
|
||||
const captchaPath = ref('')
|
||||
const isPurpleBlinking = ref(false)
|
||||
const isBlackBlinking = ref(false)
|
||||
const isTyping = ref(false)
|
||||
const isLookingAtEachOther = ref(false)
|
||||
const isPurplePeeking = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref('')
|
||||
const animationFrameId = ref<number | null>(null)
|
||||
|
||||
const purpleRef = ref<HTMLElement | null>(null)
|
||||
const blackRef = ref<HTMLElement | null>(null)
|
||||
const yellowRef = ref<HTMLElement | null>(null)
|
||||
const orangeRef = ref<HTMLElement | null>(null)
|
||||
|
||||
let purpleBlinkTimeout: NodeJS.Timeout | null = null
|
||||
let blackBlinkTimeout: NodeJS.Timeout | null = null
|
||||
let typingTimeout: NodeJS.Timeout | null = null
|
||||
let peekTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
mouseX.value = e.clientX
|
||||
mouseY.value = e.clientY
|
||||
if (!animationFrameId.value) {
|
||||
animationFrameId.value = requestAnimationFrame(() => {
|
||||
updateEyeOffsets()
|
||||
updateCharacterPositions()
|
||||
animationFrameId.value = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function initBlinkingAnimations() {
|
||||
schedulePurpleBlink()
|
||||
scheduleBlackBlink()
|
||||
}
|
||||
|
||||
function schedulePurpleBlink() {
|
||||
const getRandomBlinkInterval = () => Math.random() * 4000 + 3000
|
||||
purpleBlinkTimeout = setTimeout(() => {
|
||||
isPurpleBlinking.value = true
|
||||
setTimeout(() => {
|
||||
isPurpleBlinking.value = false
|
||||
schedulePurpleBlink()
|
||||
}, 150)
|
||||
}, getRandomBlinkInterval())
|
||||
}
|
||||
|
||||
function scheduleBlackBlink() {
|
||||
const getRandomBlinkInterval = () => Math.random() * 4000 + 3000
|
||||
blackBlinkTimeout = setTimeout(() => {
|
||||
isBlackBlinking.value = true
|
||||
setTimeout(() => {
|
||||
isBlackBlinking.value = false
|
||||
scheduleBlackBlink()
|
||||
}, 150)
|
||||
}, getRandomBlinkInterval())
|
||||
}
|
||||
|
||||
function calculatePosition(ref: HTMLElement | null) {
|
||||
if (!ref) return { faceX: 0, faceY: 0, bodySkew: 0 }
|
||||
const rect = ref.getBoundingClientRect()
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 3
|
||||
const deltaX = mouseX.value - centerX
|
||||
const deltaY = mouseY.value - centerY
|
||||
const faceX = Math.max(-15, Math.min(15, deltaX / 20))
|
||||
const faceY = Math.max(-10, Math.min(10, deltaY / 30))
|
||||
const bodySkew = Math.max(-6, Math.min(6, -deltaX / 120))
|
||||
return { faceX, faceY, bodySkew }
|
||||
}
|
||||
|
||||
function updateCharacterPositions() {
|
||||
// 角色位置更新逻辑
|
||||
}
|
||||
|
||||
function handleInputFocus() {
|
||||
isTyping.value = true
|
||||
isLookingAtEachOther.value = true
|
||||
typingTimeout = setTimeout(() => {
|
||||
isLookingAtEachOther.value = false
|
||||
}, 800)
|
||||
}
|
||||
|
||||
function handleInputBlur() {
|
||||
isTyping.value = false
|
||||
isLookingAtEachOther.value = false
|
||||
if (typingTimeout) {
|
||||
clearTimeout(typingTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
function togglePasswordVisibility() {
|
||||
showPassword.value = !showPassword.value
|
||||
if (showPassword.value && dataForm.value.password.length > 0) {
|
||||
schedulePurplePeek()
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePurplePeek() {
|
||||
if (!showPassword.value || dataForm.value.password.length === 0) {
|
||||
isPurplePeeking.value = false
|
||||
return
|
||||
}
|
||||
peekTimeout = setTimeout(() => {
|
||||
isPurplePeeking.value = true
|
||||
setTimeout(() => {
|
||||
isPurplePeeking.value = false
|
||||
}, 800)
|
||||
}, Math.random() * 3000 + 2000)
|
||||
}
|
||||
|
||||
function updateEyeOffsets() {
|
||||
const dots = document.querySelectorAll('.login-eyes-dot')
|
||||
const mouths = document.querySelectorAll('.login-mouth-line')
|
||||
if (!dots.length) return
|
||||
const vw = window.innerWidth || 1
|
||||
const vh = window.innerHeight || 1
|
||||
const relX = (mouseX.value - vw / 2) / (vw / 2)
|
||||
const relY = (mouseY.value - vh / 2) / (vh / 2)
|
||||
const eyeMoveX = relX * 14
|
||||
const eyeMoveY = relY * 7
|
||||
dots.forEach((dot) => {
|
||||
if (dot) {
|
||||
;(dot as HTMLElement).style.transform = `translate(${eyeMoveX}px, ${eyeMoveY}px)`
|
||||
}
|
||||
})
|
||||
if (mouths.length) {
|
||||
const mouthX = relX * 10
|
||||
const mouthY = relY * 3
|
||||
mouths.forEach((m) => {
|
||||
if (m) {
|
||||
;(m as HTMLElement).style.transform = `translate(${mouthX}px, ${mouthY}px)`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dataFormSubmit() {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
setTimeout(() => {
|
||||
userStore.setToken('mock_token_' + Date.now())
|
||||
userStore.setUserInfo({
|
||||
username: dataForm.value.userName,
|
||||
nickname: '管理员',
|
||||
avatar: ''
|
||||
})
|
||||
ElMessage.success('登录成功')
|
||||
router.replace({ path: '/home' })
|
||||
isLoading.value = false
|
||||
}, 800)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getCaptcha() {
|
||||
dataForm.value.uuid = Date.now().toString()
|
||||
const code = Math.random().toString(36).substring(2, 6).toUpperCase()
|
||||
captchaPath.value = `data:image/svg+xml;base64,${btoa(`<svg xmlns="http://www.w3.org/2000/svg" width="100" height="40"><rect fill="#f0f0f0" width="100" height="40"/><text x="50" y="28" font-size="20" text-anchor="middle" fill="#666">${code}</text></svg>`)}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCaptcha()
|
||||
initBlinkingAnimations()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (purpleBlinkTimeout) clearTimeout(purpleBlinkTimeout)
|
||||
if (blackBlinkTimeout) clearTimeout(blackBlinkTimeout)
|
||||
if (typingTimeout) clearTimeout(typingTimeout)
|
||||
if (peekTimeout) clearTimeout(peekTimeout)
|
||||
if (animationFrameId.value) cancelAnimationFrame(animationFrameId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 动画关键帧 */
|
||||
@keyframes login-card-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 20px, 0) scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes error-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Element Plus 样式覆盖 */
|
||||
:deep(.el-input) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-input:focus-within) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2);
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-button:hover:not(:disabled)) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
:deep(.el-button:active:not(:disabled)) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 自定义尺寸类 */
|
||||
.max-w-1120px {
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
.h-500px {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.w-105 {
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
.h-46px {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.text-24px {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-20px {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text-15px {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.rounded-24px {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.bg-#2D2D2D {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.text-#0f172a {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.text-#ef4444 {
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
234
src/views/system/settings.vue
Normal file
234
src/views/system/settings.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Message, Lock, Monitor } from '@element-plus/icons-vue'
|
||||
|
||||
const activeName = ref('basic')
|
||||
|
||||
const basicForm = ref({
|
||||
siteName: '管理系统',
|
||||
logo: '',
|
||||
copyright: '© 2024 版权所有',
|
||||
icp: '',
|
||||
})
|
||||
|
||||
const emailForm = ref({
|
||||
smtpServer: '',
|
||||
smtpPort: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
enableSsl: true,
|
||||
})
|
||||
|
||||
const securityForm = ref({
|
||||
loginFailCount: 5,
|
||||
lockTime: 30,
|
||||
passwordExpire: 90,
|
||||
enableCaptcha: true,
|
||||
})
|
||||
|
||||
function saveBasic() {
|
||||
ElMessage.success('基础设置保存成功')
|
||||
}
|
||||
|
||||
function saveEmail() {
|
||||
ElMessage.success('邮件设置保存成功')
|
||||
}
|
||||
|
||||
function saveSecurity() {
|
||||
ElMessage.success('安全设置保存成功')
|
||||
}
|
||||
|
||||
function testEmail() {
|
||||
ElMessage.success('测试邮件已发送')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<el-card shadow="hover">
|
||||
<el-tabs v-model="activeName" type="border-card">
|
||||
<!-- 基础设置 -->
|
||||
<el-tab-pane name="basic">
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>基础设置</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="basicForm" label-width="120px" class="settings-form">
|
||||
<el-form-item label="站点名称">
|
||||
<el-input v-model="basicForm.siteName" placeholder="请输入站点名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="站点Logo">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<img v-if="basicForm.logo" :src="basicForm.logo" class="avatar">
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<span class="upload-tip">建议尺寸:200x200px,支持 jpg、png 格式</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版权信息">
|
||||
<el-input
|
||||
v-model="basicForm.copyright"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入版权信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备案号">
|
||||
<el-input v-model="basicForm.icp" placeholder="请输入ICP备案号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveBasic">保存设置</el-button>
|
||||
<el-button @click="basicForm = { siteName: '', logo: '', copyright: '', icp: '' }">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 邮件设置 -->
|
||||
<el-tab-pane name="email">
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
<el-icon><Message /></el-icon>
|
||||
<span>邮件设置</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="emailForm" label-width="120px" class="settings-form">
|
||||
<el-form-item label="SMTP服务器">
|
||||
<el-input v-model="emailForm.smtpServer" placeholder="如: smtp.example.com" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="SMTP端口">
|
||||
<el-input-number v-model="emailForm.smtpPort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发件人邮箱">
|
||||
<el-input v-model="emailForm.username" placeholder="请输入发件人邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="授权码">
|
||||
<el-input v-model="emailForm.password" type="password" placeholder="请输入邮箱授权码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用SSL">
|
||||
<el-switch v-model="emailForm.enableSsl" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveEmail">保存设置</el-button>
|
||||
<el-button @click="testEmail">测试连接</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 安全设置 -->
|
||||
<el-tab-pane name="security">
|
||||
<template #label>
|
||||
<span class="tab-label">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>安全设置</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-form :model="securityForm" label-width="160px" class="settings-form">
|
||||
<el-form-item label="登录失败次数限制">
|
||||
<el-input-number v-model="securityForm.loginFailCount" :min="3" :max="10" />
|
||||
<span class="form-tip">次后锁定账号</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="账号锁定时间">
|
||||
<el-input-number v-model="securityForm.lockTime" :min="5" :max="60" />
|
||||
<span class="form-tip">分钟</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码有效期">
|
||||
<el-input-number v-model="securityForm.passwordExpire" :min="30" :max="180" />
|
||||
<span class="form-tip">天后需修改密码</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用验证码">
|
||||
<el-switch v-model="securityForm.enableCaptcha" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSecurity">保存设置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
max-width: 600px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.avatar-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-left: 12px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-left: 12px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
206
src/views/user/index.vue
Normal file
206
src/views/user/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const tableData = ref([
|
||||
{ id: 1, username: 'admin', email: 'admin@example.com', phone: '13800138000', status: '启用', createTime: '2024-01-15' },
|
||||
{ id: 2, username: 'user1', email: 'user1@example.com', phone: '13800138001', status: '启用', createTime: '2024-01-14' },
|
||||
{ id: 3, username: 'user2', email: 'user2@example.com', phone: '13800138002', status: '禁用', createTime: '2024-01-13' },
|
||||
])
|
||||
|
||||
const searchForm = ref({
|
||||
username: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增用户')
|
||||
const formRef = ref()
|
||||
|
||||
const userForm = ref({
|
||||
id: null,
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: '启用',
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
ElMessage.success('查询成功')
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.value = { username: '', status: '' }
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
dialogTitle.value = '新增用户'
|
||||
userForm.value = { id: null, username: '', email: '', phone: '', status: '启用' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
dialogTitle.value = '编辑用户'
|
||||
userForm.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleDelete(row: any) {
|
||||
ElMessageBox.confirm(`确定删除用户 "${row.username}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogTitle.value === '新增用户' ? '新增成功' : '修改成功')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleStatusChange(row: any) {
|
||||
ElMessage.success(`用户状态已${row.status === '启用' ? '启用' : '禁用'}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-page">
|
||||
<!-- 搜索区域 -->
|
||||
<el-card class="search-card" shadow="hover">
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="用户名">
|
||||
<el-input
|
||||
v-model="searchForm.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="禁用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<el-card class="table-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">用户列表</span>
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增用户</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="tableData" border v-loading="loading" stripe>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
active-value="启用"
|
||||
inactive-value="禁用"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="150" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="100"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form ref="formRef" :model="userForm" label-width="80px">
|
||||
<el-form-item label="用户名" prop="username" required>
|
||||
<el-input v-model="userForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="userForm.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="userForm.status">
|
||||
<el-radio label="启用">启用</el-radio>
|
||||
<el-radio label="禁用">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header .title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user