refactor: 界面重构美化
This commit is contained in:
@@ -1,67 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 1400 800">
|
||||
<defs>
|
||||
<!-- 原背景色保持不变 -->
|
||||
<linearGradient id="bgDark" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0b1324" />
|
||||
<stop offset="58%" stop-color="#162135" />
|
||||
<stop offset="100%" stop-color="#1e2c44" />
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0a0f1a" />
|
||||
<stop offset="50%" stop-color="#0d1424" />
|
||||
<stop offset="100%" stop-color="#111a2e" />
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="glowPrimary" cx="18%" cy="14%" r="62%">
|
||||
<stop offset="0%" stop-color="rgba(98,142,255,0.42)" />
|
||||
<stop offset="46%" stop-color="rgba(98,142,255,0.18)" />
|
||||
<stop offset="100%" stop-color="rgba(98,142,255,0)" />
|
||||
<radialGradient id="glow1" cx="15%" cy="18%" r="55%">
|
||||
<stop offset="0%" stop-color="rgba(22,119,255,0.15)" />
|
||||
<stop offset="45%" stop-color="rgba(22,119,255,0.05)" />
|
||||
<stop offset="100%" stop-color="rgba(22,119,255,0)" />
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="glowSecondary" cx="82%" cy="70%" r="56%">
|
||||
<stop offset="0%" stop-color="rgba(59,108,224,0.28)" />
|
||||
<stop offset="48%" stop-color="rgba(59,108,224,0.14)" />
|
||||
<stop offset="100%" stop-color="rgba(59,108,224,0)" />
|
||||
<radialGradient id="glow2" cx="85%" cy="72%" r="50%">
|
||||
<stop offset="0%" stop-color="rgba(99,102,241,0.1)" />
|
||||
<stop offset="45%" stop-color="rgba(99,102,241,0.03)" />
|
||||
<stop offset="100%" stop-color="rgba(99,102,241,0)" />
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="meshDark" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.1)" />
|
||||
<stop offset="35%" stop-color="rgba(255,255,255,0.04)" />
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)" />
|
||||
</linearGradient>
|
||||
|
||||
</defs>
|
||||
|
||||
<!-- 基础背景 -->
|
||||
<rect width="100%" height="100%" fill="url(#bgDark)" />
|
||||
<rect width="100%" height="100%" fill="url(#glowPrimary)" />
|
||||
<rect width="100%" height="100%" fill="url(#glowSecondary)" />
|
||||
<rect width="100%" height="100%" fill="url(#meshDark)" />
|
||||
|
||||
<g opacity="0.8">
|
||||
<circle cx="280" cy="190" r="120" fill="rgba(98,142,255,0.03)" />
|
||||
<circle cx="280" cy="190" r="120" fill="none" stroke="rgba(98,142,255,0.11)" stroke-width="1" />
|
||||
<circle cx="280" cy="190" r="170" fill="none" stroke="rgba(98,142,255,0.06)" stroke-width="0.8" />
|
||||
|
||||
<g transform="rotate(-12, 1120, 600)">
|
||||
<path d="M900 620 C 980 520, 1120 520, 1240 580 C 1320 620, 1340 680, 1290 720 C 1200 790, 980 760, 900 690 Z" fill="rgba(98,142,255,0.018)" />
|
||||
<path d="M900 620 C 980 520, 1120 520, 1240 580 C 1320 620, 1340 680, 1290 720 C 1200 790, 980 760, 900 690 Z" fill="none" stroke="rgba(98,142,255,0.08)" stroke-width="1" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- 节点圆点 -->
|
||||
<g fill="rgba(98,142,255,0.5)">
|
||||
<circle cx="300" cy="150" r="3" />
|
||||
<circle cx="760" cy="220" r="3" />
|
||||
<circle cx="1180" cy="560" r="3" />
|
||||
</g>
|
||||
|
||||
<!-- 节点光晕 -->
|
||||
<g fill="rgba(98,142,255,0.08)">
|
||||
<circle cx="300" cy="150" r="10" />
|
||||
<circle cx="760" cy="220" r="12" />
|
||||
<circle cx="1180" cy="560" r="10" />
|
||||
</g>
|
||||
|
||||
<!-- 右上角装饰圆弧 -->
|
||||
<g fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="0.5">
|
||||
<circle cx="1400" cy="0" r="400" />
|
||||
<circle cx="1400" cy="0" r="650" />
|
||||
</g>
|
||||
</svg>
|
||||
<rect width="100%" height="100%" fill="url(#bg)" />
|
||||
<rect width="100%" height="100%" fill="url(#glow1)" />
|
||||
<rect width="100%" height="100%" fill="url(#glow2)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,42 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 1400 800">
|
||||
<defs>
|
||||
<linearGradient id="bgLight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f3f7ff" />
|
||||
<stop offset="60%" stop-color="#e3edff" />
|
||||
<stop offset="100%" stop-color="#d6e7ff" />
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#f0f4ff" />
|
||||
<stop offset="50%" stop-color="#f5f8ff" />
|
||||
<stop offset="100%" stop-color="#edf2fe" />
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="glowLight" cx="20%" cy="15%" r="60%">
|
||||
<stop offset="0%" stop-color="rgba(64,128,255,0.35)" />
|
||||
<stop offset="40%" stop-color="rgba(64,128,255,0.18)" />
|
||||
<stop offset="100%" stop-color="rgba(64,128,255,0)" />
|
||||
<radialGradient id="glow1" cx="15%" cy="20%" r="55%">
|
||||
<stop offset="0%" stop-color="rgba(22,119,255,0.12)" />
|
||||
<stop offset="50%" stop-color="rgba(22,119,255,0.04)" />
|
||||
<stop offset="100%" stop-color="rgba(22,119,255,0)" />
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="glowSecondary" cx="80%" cy="70%" r="55%">
|
||||
<stop offset="0%" stop-color="rgba(22,93,255,0.3)" />
|
||||
<stop offset="50%" stop-color="rgba(22,93,255,0.12)" />
|
||||
<stop offset="100%" stop-color="rgba(22,93,255,0)" />
|
||||
<radialGradient id="glow2" cx="85%" cy="75%" r="50%">
|
||||
<stop offset="0%" stop-color="rgba(99,102,241,0.08)" />
|
||||
<stop offset="50%" stop-color="rgba(99,102,241,0.02)" />
|
||||
<stop offset="100%" stop-color="rgba(99,102,241,0)" />
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="meshLight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.6)" />
|
||||
<stop offset="35%" stop-color="rgba(255,255,255,0.2)" />
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect id="bg-layer" width="100%" height="100%" fill="url(#bgLight)" />
|
||||
<rect id="soft-glow" width="100%" height="100%" fill="url(#glowLight)" />
|
||||
<rect width="100%" height="100%" fill="url(#glowSecondary)" />
|
||||
<rect width="100%" height="100%" fill="url(#meshLight)" />
|
||||
|
||||
<!-- 柔和块面光影,替代明显线条 -->
|
||||
<g opacity="0.45">
|
||||
<rect x="-40" y="520" width="520" height="220" rx="180" fill="rgba(255,255,255,0.25)" />
|
||||
<rect x="760" y="90" width="520" height="210" rx="180" fill="rgba(255,255,255,0.22)" />
|
||||
<rect x="420" y="620" width="560" height="190" rx="180" fill="rgba(255,255,255,0.18)" />
|
||||
</g>
|
||||
|
||||
<!-- 去掉点状噪声,仅保留大区域柔光 -->
|
||||
</svg>
|
||||
<rect width="100%" height="100%" fill="url(#bg)" />
|
||||
<rect width="100%" height="100%" fill="url(#glow1)" />
|
||||
<rect width="100%" height="100%" fill="url(#glow2)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -201,7 +201,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.icon-grid-item:hover {
|
||||
border-color: #4080ff;
|
||||
border-color: var(--el-color-primary);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="layout__sidebar" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
|
||||
<div
|
||||
v-show="!appStore.contentFullscreen"
|
||||
class="layout__sidebar"
|
||||
:class="{ 'layout__sidebar--collapsed': !isSidebarOpen }"
|
||||
>
|
||||
<div :class="{ 'has-logo': showLogo }" class="layout-sidebar">
|
||||
<LayoutLogo v-if="showLogo" :collapse="!isSidebarOpen" />
|
||||
<el-scrollbar>
|
||||
@@ -16,9 +20,10 @@
|
||||
:class="{
|
||||
hasTagsView: showTagsView,
|
||||
'layout__main--collapsed': !isSidebarOpen,
|
||||
'layout__main--fullscreen': appStore.contentFullscreen,
|
||||
}"
|
||||
>
|
||||
<LayoutNavbar />
|
||||
<LayoutNavbar v-show="!appStore.contentFullscreen" />
|
||||
<LayoutTagsView v-if="showTagsView" />
|
||||
<LayoutMain />
|
||||
</div>
|
||||
@@ -27,6 +32,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLayout } from "./useLayout";
|
||||
import { useAppStore } from "@/stores";
|
||||
import BaseLayout from "./BaseLayout.vue";
|
||||
import LayoutLogo from "./components/LayoutLogo.vue";
|
||||
import LayoutNavbar from "./components/LayoutNavbar.vue";
|
||||
@@ -35,6 +41,7 @@ import LayoutMain from "./components/LayoutMain.vue";
|
||||
import LayoutSidebar from "./components/LayoutSidebar.vue";
|
||||
|
||||
const { showTagsView, showLogo, isSidebarOpen, routes } = useLayout();
|
||||
const appStore = useAppStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -57,6 +64,7 @@ const { showTagsView, showLogo, isSidebarOpen, routes } = useLayout();
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--menu-background);
|
||||
border-right: 1px solid var(--card-border);
|
||||
transition: width 0.28s;
|
||||
|
||||
&.has-logo {
|
||||
@@ -82,6 +90,10 @@ const { showTagsView, showLogo, isSidebarOpen, routes } = useLayout();
|
||||
margin-left: $sidebar-width-collapsed;
|
||||
}
|
||||
|
||||
&--fullscreen {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<!-- 顶部菜单栏 -->
|
||||
<div class="layout__header">
|
||||
<div v-show="!appStore.contentFullscreen" class="layout__header">
|
||||
<div class="layout__header-content">
|
||||
<div v-if="showLogo" class="layout__header-logo">
|
||||
<LayoutLogo :collapse="isLogoCollapsed" />
|
||||
@@ -37,7 +37,11 @@
|
||||
<!-- 主内容区容器 -->
|
||||
<div class="layout__container">
|
||||
<!-- 左侧菜单栏 -->
|
||||
<div class="layout__sidebar--left" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
|
||||
<div
|
||||
v-show="!appStore.contentFullscreen"
|
||||
class="layout__sidebar--left"
|
||||
:class="{ 'layout__sidebar--collapsed': !isSidebarOpen }"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="activeSideMenuPath"
|
||||
@@ -233,7 +237,8 @@ watch(
|
||||
width: 100%;
|
||||
height: $navbar-height;
|
||||
background-color: var(--menu-background);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 6%);
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
@@ -301,6 +306,7 @@ watch(
|
||||
width: $sidebar-width;
|
||||
height: 100%;
|
||||
background-color: var(--menu-background);
|
||||
border-right: 1px solid var(--card-border);
|
||||
transition: width 0.28s;
|
||||
|
||||
&.layout__sidebar--collapsed {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseLayout>
|
||||
<!-- 顶部菜单-->
|
||||
<div class="layout__header">
|
||||
<div v-show="!appStore.contentFullscreen" class="layout__header">
|
||||
<div class="layout__header-left">
|
||||
<div v-if="showLogo" class="layout__header-logo">
|
||||
<LayoutLogo :collapse="isLogoCollapsed" />
|
||||
@@ -26,6 +26,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
import { useLayout } from "./useLayout";
|
||||
import { useAppStore } from "@/stores";
|
||||
import { usePermissionStore } from "@/stores";
|
||||
import BaseLayout from "./BaseLayout.vue";
|
||||
import LayoutLogo from "./components/LayoutLogo.vue";
|
||||
@@ -35,6 +36,7 @@ import LayoutTagsView from "./components/LayoutTagsView.vue";
|
||||
import LayoutMain from "./components/LayoutMain.vue";
|
||||
|
||||
const { showTagsView, showLogo } = useLayout();
|
||||
const appStore = useAppStore();
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const permissionStore = usePermissionStore();
|
||||
@@ -58,6 +60,7 @@ const isLogoCollapsed = computed(() => width.value < 768);
|
||||
width: 100%;
|
||||
height: $navbar-height;
|
||||
background-color: $menu-background;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
|
||||
@@ -79,7 +79,7 @@ const transitionName = computed(() => {
|
||||
.app-main {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
background-color: var(--el-bg-color-page);
|
||||
background-color: var(--page-bg);
|
||||
|
||||
/* fade */
|
||||
.fade-enter-active,
|
||||
|
||||
@@ -33,6 +33,10 @@ function toggleSideBar() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: $navbar-height;
|
||||
padding: 0 16px 0 4px;
|
||||
background-color: var(--content-bg);
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 3%);
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
|
||||
@@ -1,66 +1,139 @@
|
||||
<template>
|
||||
<div class="tags-container">
|
||||
<!-- 水平滚动容器 -->
|
||||
<!-- 页签滚动区 -->
|
||||
<el-scrollbar
|
||||
ref="scrollbarRef"
|
||||
class="scroll-container"
|
||||
:view-style="{ height: '100%' }"
|
||||
@wheel="handleScroll"
|
||||
>
|
||||
<div h-full flex-y-center gap-8px>
|
||||
<el-tag
|
||||
<div class="tags-wrapper">
|
||||
<div
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.fullPath"
|
||||
h-26px
|
||||
cursor-pointer
|
||||
:closable="!tag.affix"
|
||||
:effect="tagsViewStore.isActive(tag) ? 'dark' : 'light'"
|
||||
:type="tagsViewStore.isActive(tag) ? 'primary' : 'info'"
|
||||
@click.middle="handleMiddleClick(tag)"
|
||||
@contextmenu.prevent="(event: MouseEvent) => openContextMenu(tag, event)"
|
||||
@close="closeSelectedTag(tag)"
|
||||
class="tags-item"
|
||||
:class="{
|
||||
'is-active': tagsViewStore.isActive(tag),
|
||||
'is-affix': tag.affix,
|
||||
}"
|
||||
@click="
|
||||
router.push({
|
||||
path: tag.fullPath,
|
||||
query: tag.query,
|
||||
})
|
||||
"
|
||||
@click.middle="handleMiddleClick(tag)"
|
||||
@contextmenu.prevent="openContextMenu(tag, $event)"
|
||||
>
|
||||
{{ translateRouteTitle(tag.title) }}
|
||||
</el-tag>
|
||||
<template v-if="tag.icon">
|
||||
<el-icon v-if="isEpIcon(tag.icon)" :size="14">
|
||||
<component :is="toEpIconName(tag.icon)" />
|
||||
</el-icon>
|
||||
<span v-else class="tags-item__icon" :class="`i-svg:${tag.icon}`" />
|
||||
</template>
|
||||
<span class="tags-item__text">
|
||||
{{ translateRouteTitle(tag.title) }}
|
||||
</span>
|
||||
<span v-if="!tag.affix" class="tags-item__close" @click.stop="closeSelectedTag(tag)">
|
||||
<div class="i-svg:close" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 标签右键菜单 -->
|
||||
<!-- 右侧操作 -->
|
||||
<div class="tags-actions">
|
||||
<!-- 刷新当前 -->
|
||||
<span class="tags-actions__btn" @click="refreshSelectedTag(currentTag)">
|
||||
<el-icon :size="16"><Refresh /></el-icon>
|
||||
</span>
|
||||
<!-- 内容全屏 -->
|
||||
<span class="tags-actions__btn" @click="appStore.toggleContentFullscreen()">
|
||||
<el-icon :size="16">
|
||||
<FullScreen v-if="!appStore.contentFullscreen" />
|
||||
<Close v-else />
|
||||
</el-icon>
|
||||
</span>
|
||||
<!-- 下拉菜单 -->
|
||||
<el-dropdown trigger="click" @command="handleActionCommand">
|
||||
<span class="tags-actions__btn">
|
||||
<el-icon :size="16"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="refresh">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
<span>刷新当前</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="currentTag && !currentTag.affix" command="closeCurrent">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<Close />
|
||||
</el-icon>
|
||||
<span>关闭当前</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="closeOthers">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<Remove />
|
||||
</el-icon>
|
||||
<span>关闭其它</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeLeft">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<span>关闭左侧</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="closeRight">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
<span>关闭右侧</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="closeAll">
|
||||
<el-icon :size="14" class="tags-actions__dropdown-icon">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
<span>关闭所有</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<Teleport to="body">
|
||||
<ul
|
||||
v-show="contextMenu.visible"
|
||||
class="contextmenu"
|
||||
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
|
||||
>
|
||||
<ul v-show="contextMenu.visible" class="contextmenu" :style="contextMenuStyle">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<div class="i-svg:refresh" />
|
||||
刷新
|
||||
<el-icon :size="16" class="contextmenu__icon"><Refresh /></el-icon>
|
||||
<span>刷新</span>
|
||||
</li>
|
||||
<li v-if="!selectedTag?.affix" @click="closeSelectedTag(selectedTag)">
|
||||
<div class="i-svg:close" />
|
||||
关闭
|
||||
<li
|
||||
v-if="!selectedTag?.affix"
|
||||
class="contextmenu__danger"
|
||||
@click="closeSelectedTag(selectedTag)"
|
||||
>
|
||||
<el-icon :size="16" class="contextmenu__icon"><Close /></el-icon>
|
||||
<span>关闭</span>
|
||||
</li>
|
||||
<li class="contextmenu__divider" />
|
||||
<li @click="closeOtherTags">
|
||||
<div class="i-svg:close_other" />
|
||||
关闭其它
|
||||
<el-icon :size="16" class="contextmenu__icon"><Remove /></el-icon>
|
||||
<span>关闭其它</span>
|
||||
</li>
|
||||
<li v-if="!isFirstView" @click="closeLeftTags">
|
||||
<div class="i-svg:close_left" />
|
||||
关闭左侧
|
||||
<el-icon :size="16" class="contextmenu__icon"><DArrowLeft /></el-icon>
|
||||
<span>关闭左侧</span>
|
||||
</li>
|
||||
<li v-if="!isLastView" @click="closeRightTags">
|
||||
<div class="i-svg:close_right" />
|
||||
关闭右侧
|
||||
<el-icon :size="16" class="contextmenu__icon"><DArrowRight /></el-icon>
|
||||
<span>关闭右侧</span>
|
||||
</li>
|
||||
<li class="contextmenu__divider" />
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<div class="i-svg:close_all" />
|
||||
关闭所有
|
||||
<el-icon :size="16" class="contextmenu__icon"><CircleClose /></el-icon>
|
||||
<span>关闭所有</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Teleport>
|
||||
@@ -71,7 +144,7 @@
|
||||
import { useRoute, useRouter, type RouteRecordRaw } from "vue-router";
|
||||
import { resolve } from "path-browserify";
|
||||
import { translateRouteTitle } from "@/lang/utils";
|
||||
import { usePermissionStore, useTagsViewStore } from "@/stores";
|
||||
import { useAppStore, usePermissionStore, useTagsViewStore } from "@/stores";
|
||||
|
||||
interface ContextMenu {
|
||||
visible: boolean;
|
||||
@@ -82,26 +155,79 @@ interface ContextMenu {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 状态管理
|
||||
const appStore = useAppStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||
|
||||
// 当前选中的标签
|
||||
const selectedTag = ref<TagView | null>(null);
|
||||
|
||||
// 右键菜单状态"
|
||||
const contextMenu = reactive<ContextMenu>({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
// 滚动条引用
|
||||
const scrollbarRef = ref();
|
||||
|
||||
// 路由映射缓存,提升查找性能
|
||||
const currentTag = computed(() => {
|
||||
return visitedViews.value.find((tag) => tagsViewStore.isActive(tag)) || null;
|
||||
});
|
||||
|
||||
// 图标兼容:判断是否为 Element Plus 图标 (el-icon-xxx)
|
||||
const isEpIcon = (icon: string) => icon.startsWith("el-icon");
|
||||
|
||||
// 图标兼容:el-icon-xxx → PascalCase 组件名
|
||||
const toEpIconName = (icon: string) =>
|
||||
icon.replace("el-icon-", "").replace(/(^|-)\w/g, (s) => s.slice(-1).toUpperCase());
|
||||
|
||||
const contextMenuStyle = computed(() => {
|
||||
const menuWidth = 160;
|
||||
const menuHeight = 220;
|
||||
const padding = 8;
|
||||
|
||||
let x = contextMenu.x;
|
||||
let y = contextMenu.y;
|
||||
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x = window.innerWidth - menuWidth - padding;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight - padding;
|
||||
}
|
||||
|
||||
return {
|
||||
left: `${Math.max(padding, x)}px`,
|
||||
top: `${Math.max(padding, y)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
// 标签栏右侧操作按钮分发
|
||||
const handleActionCommand = (command: string) => {
|
||||
switch (command) {
|
||||
case "refresh":
|
||||
refreshSelectedTag(currentTag.value);
|
||||
break;
|
||||
case "closeCurrent":
|
||||
closeSelectedTag(currentTag.value);
|
||||
break;
|
||||
case "closeOthers":
|
||||
closeOtherTagsForActive();
|
||||
break;
|
||||
case "closeLeft":
|
||||
closeLeftTagsForActive();
|
||||
break;
|
||||
case "closeRight":
|
||||
closeRightTagsForActive();
|
||||
break;
|
||||
case "closeAll":
|
||||
closeAllTags(currentTag.value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// path → TagView 映射,用于快速查找标签
|
||||
const routePathMap = computed(() => {
|
||||
const map = new Map<string, TagView>();
|
||||
visitedViews.value.forEach((tag) => {
|
||||
@@ -110,7 +236,7 @@ const routePathMap = computed(() => {
|
||||
return map;
|
||||
});
|
||||
|
||||
// 判断是否为第一个标签
|
||||
// 判断右键选中标签是否为最左侧(不可关闭左侧)
|
||||
const isFirstView = computed(() => {
|
||||
if (!selectedTag.value) return false;
|
||||
return (
|
||||
@@ -119,15 +245,13 @@ const isFirstView = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// 判断是否为最后一个标签
|
||||
// 判断右键选中标签是否为最右侧(不可关闭右侧)
|
||||
const isLastView = computed(() => {
|
||||
if (!selectedTag.value) return false;
|
||||
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath;
|
||||
});
|
||||
|
||||
/**
|
||||
* 递归提取固定标签
|
||||
*/
|
||||
// 从路由表中提取所有 affix 固定标签
|
||||
const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] => {
|
||||
const affixTags: TagView[] = [];
|
||||
|
||||
@@ -135,19 +259,18 @@ const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] =
|
||||
routeList.forEach((route) => {
|
||||
const fullPath = resolve(currentBasePath, route.path);
|
||||
|
||||
// 如果是固定标签,添加到列表
|
||||
if (route.meta?.affix) {
|
||||
affixTags.push({
|
||||
path: fullPath,
|
||||
fullPath,
|
||||
name: String(route.name || ""),
|
||||
title: route.meta.title || "no-name",
|
||||
icon: route.meta.icon,
|
||||
affix: true,
|
||||
keepAlive: route.meta.keepAlive || false,
|
||||
});
|
||||
}
|
||||
|
||||
// 递归处理子路由
|
||||
if (route.children?.length) {
|
||||
traverse(route.children, fullPath);
|
||||
}
|
||||
@@ -158,9 +281,6 @@ const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] =
|
||||
return affixTags;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化固定标签
|
||||
*/
|
||||
const initAffixTags = () => {
|
||||
const affixTags = extractAffixTags(permissionStore.routes);
|
||||
|
||||
@@ -171,9 +291,6 @@ const initAffixTags = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加当前路由标签
|
||||
*/
|
||||
const addCurrentTag = () => {
|
||||
if (!route.meta?.title) return;
|
||||
|
||||
@@ -182,15 +299,13 @@ const addCurrentTag = () => {
|
||||
title: route.meta.title,
|
||||
path: route.path,
|
||||
fullPath: route.fullPath,
|
||||
icon: route.meta.icon,
|
||||
affix: route.meta.affix || false,
|
||||
keepAlive: route.meta.keepAlive || false,
|
||||
query: route.query,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新当前标签
|
||||
*/
|
||||
const updateCurrentTag = () => {
|
||||
nextTick(() => {
|
||||
const currentTag = routePathMap.value.get(route.path);
|
||||
@@ -201,6 +316,7 @@ const updateCurrentTag = () => {
|
||||
title: route.meta?.title || "",
|
||||
path: route.path,
|
||||
fullPath: route.fullPath,
|
||||
icon: route.meta?.icon,
|
||||
affix: route.meta?.affix || false,
|
||||
keepAlive: route.meta?.keepAlive || false,
|
||||
query: route.query,
|
||||
@@ -209,36 +325,24 @@ const updateCurrentTag = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理中键点击
|
||||
*/
|
||||
const handleMiddleClick = (tag: TagView) => {
|
||||
if (!tag.affix) {
|
||||
closeSelectedTag(tag);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开右键菜单
|
||||
*/
|
||||
const openContextMenu = (tag: TagView, event: MouseEvent) => {
|
||||
contextMenu.x = event.clientX;
|
||||
contextMenu.y = event.clientY;
|
||||
contextMenu.visible = true;
|
||||
|
||||
selectedTag.value = tag;
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭右键菜单
|
||||
*/
|
||||
const closeContextMenu = () => {
|
||||
contextMenu.visible = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理滚轮事件
|
||||
*/
|
||||
// 鼠标滚轮横向滚动(标签栏超出可视区时生效)
|
||||
const handleScroll = (event: WheelEvent) => {
|
||||
closeContextMenu();
|
||||
|
||||
@@ -254,9 +358,7 @@ const handleScroll = (event: WheelEvent) => {
|
||||
scrollbarRef.value.setScrollLeft(newScrollLeft);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新标签
|
||||
*/
|
||||
// 刷新指定标签页:移除缓存后跳转 redirect 中转页重新加载
|
||||
const refreshSelectedTag = (tag: TagView | null) => {
|
||||
if (!tag) return;
|
||||
|
||||
@@ -266,9 +368,7 @@ const refreshSelectedTag = (tag: TagView | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭标签
|
||||
*/
|
||||
// 关闭指定标签页,若关闭的是当前激活标签则跳转到最后一个
|
||||
const closeSelectedTag = (tag: TagView | null) => {
|
||||
if (!tag) return;
|
||||
|
||||
@@ -279,58 +379,75 @@ const closeSelectedTag = (tag: TagView | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭左侧标签
|
||||
*/
|
||||
// 关闭当前激活标签以外的其他标签
|
||||
const closeOtherTagsForActive = () => {
|
||||
if (!currentTag.value) return;
|
||||
tagsViewStore.delOtherViews(currentTag.value).then(() => {
|
||||
updateCurrentTag();
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭当前激活标签左侧的所有标签
|
||||
const closeLeftTagsForActive = () => {
|
||||
if (!currentTag.value) return;
|
||||
tagsViewStore.delLeftViews(currentTag.value).then((result: any) => {
|
||||
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path);
|
||||
if (!hasCurrentRoute) {
|
||||
tagsViewStore.toLastView(result.visitedViews);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭当前激活标签右侧的所有标签
|
||||
const closeRightTagsForActive = () => {
|
||||
if (!currentTag.value) return;
|
||||
tagsViewStore.delRightViews(currentTag.value).then((result: any) => {
|
||||
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path);
|
||||
if (!hasCurrentRoute) {
|
||||
tagsViewStore.toLastView(result.visitedViews);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭右键选中标签左侧的所有标签
|
||||
const closeLeftTags = () => {
|
||||
if (!selectedTag.value) return;
|
||||
|
||||
tagsViewStore.delLeftViews(selectedTag.value).then((result: any) => {
|
||||
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path);
|
||||
|
||||
if (!hasCurrentRoute) {
|
||||
tagsViewStore.toLastView(result.visitedViews);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭右侧标签
|
||||
*/
|
||||
// 关闭右键选中标签右侧的所有标签
|
||||
const closeRightTags = () => {
|
||||
if (!selectedTag.value) return;
|
||||
|
||||
tagsViewStore.delRightViews(selectedTag.value).then((result: any) => {
|
||||
const hasCurrentRoute = result.visitedViews.some((item: TagView) => item.path === route.path);
|
||||
|
||||
if (!hasCurrentRoute) {
|
||||
tagsViewStore.toLastView(result.visitedViews);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭其他标签
|
||||
*/
|
||||
// 关闭右键选中标签以外的其他标签
|
||||
const closeOtherTags = () => {
|
||||
if (!selectedTag.value) return;
|
||||
|
||||
router.push(selectedTag.value);
|
||||
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
|
||||
updateCurrentTag();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭所有标签
|
||||
*/
|
||||
// 关闭所有标签页
|
||||
const closeAllTags = (tag: TagView | null) => {
|
||||
tagsViewStore.delAllViews().then((result: any) => {
|
||||
tagsViewStore.toLastView(result.visitedViews, tag || undefined);
|
||||
});
|
||||
};
|
||||
|
||||
// 右键菜单管理
|
||||
// 右键菜单生命周期:显示时监听外部点击关闭,卸载时移除监听
|
||||
const useContextMenuManager = () => {
|
||||
const handleOutsideClick = () => {
|
||||
closeContextMenu();
|
||||
@@ -344,13 +461,12 @@ const useContextMenuManager = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("click", handleOutsideClick);
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
// 路由变化时同步添加标签并更新激活状态
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
@@ -360,51 +476,259 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 初始化
|
||||
// 初始化固定标签(affix)
|
||||
onMounted(() => {
|
||||
initAffixTags();
|
||||
});
|
||||
|
||||
// 启用右键菜单管理
|
||||
useContextMenuManager();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// ============================================
|
||||
// 容器
|
||||
// ============================================
|
||||
|
||||
.tags-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: $tags-view-height;
|
||||
padding: 0 15px;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
padding: 0 4px 0 8px;
|
||||
background-color: var(--content-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 标签列表
|
||||
// ============================================
|
||||
|
||||
.tags-wrapper {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 标签项
|
||||
// 未激活: 透明底,融入白色容器背景
|
||||
// 激活: 浅主色底 + 边框,从背景中"跳"出来
|
||||
// ============================================
|
||||
|
||||
.tags-item {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 1px;
|
||||
font-size: 0;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.12s ease,
|
||||
background-color 0.12s ease;
|
||||
|
||||
:deep(div) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(0 0 0 / 10%);
|
||||
}
|
||||
}
|
||||
|
||||
// 悬浮态 — 浅灰底浮现
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
|
||||
.tags-item__close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tags-item__icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 激活态 — 浅主色块突出显示
|
||||
&.is-active {
|
||||
font-weight: 500;
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
|
||||
.tags-item__icon {
|
||||
color: var(--el-color-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tags-item__close {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-7);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 固定标签 — 不可关闭,视觉上与未激活一致
|
||||
&.is-affix {
|
||||
.tags-item__text {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 右侧操作按钮
|
||||
// ============================================
|
||||
|
||||
.tags-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 4px;
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
&:hover :deep(.el-icon) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 右键菜单
|
||||
// ============================================
|
||||
|
||||
.contextmenu {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
padding: 5px 0;
|
||||
min-width: 150px;
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
list-style-type: none;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
background: var(--el-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 7px 16px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
margin: 1px 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&__danger {
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
|
||||
.contextmenu__icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
padding: 0 !important;
|
||||
margin: 5px 8px !important;
|
||||
pointer-events: none;
|
||||
cursor: default !important;
|
||||
background-color: var(--el-border-color-lighter);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -210,19 +210,19 @@ function handleSettingsClick() {
|
||||
|
||||
:deep(.i-svg\:language) {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
background-size: 18px 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
// 图标样式
|
||||
:deep([class^="i-svg:"]) {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
color: var(--el-text-color-regular);
|
||||
transition: color 0.3s;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -297,7 +297,7 @@ function handleSettingsClick() {
|
||||
.navbar-actions--dark-text {
|
||||
.navbar-actions__item {
|
||||
:deep([class^="i-svg:"]) {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
color: var(--el-text-color-secondary) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -25,8 +25,8 @@ export const appConfig = {
|
||||
// ============================================
|
||||
export const defaults = {
|
||||
theme: prefersDark ? ThemeMode.DARK : ThemeMode.LIGHT,
|
||||
themeColor: "#4080FF",
|
||||
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
|
||||
themeColor: "#1677FF",
|
||||
sidebarColorScheme: SidebarColor.MINIMAL_WHITE,
|
||||
layout: LayoutMode.LEFT,
|
||||
size: ComponentSize.DEFAULT,
|
||||
language: LanguageEnum.ZH_CN,
|
||||
@@ -42,14 +42,14 @@ export const defaults = {
|
||||
// 主题色预设
|
||||
// ============================================
|
||||
export const themeColorPresets = [
|
||||
"#4080FF",
|
||||
"#1890FF",
|
||||
"#409EFF",
|
||||
"#FA8C16",
|
||||
"#1677FF",
|
||||
"#165DFF",
|
||||
"#3370FF",
|
||||
"#22C55E",
|
||||
"#FAAD14",
|
||||
"#FF4D4F",
|
||||
"#722ED1",
|
||||
"#13C2C2",
|
||||
"#52C41A",
|
||||
"#F5222D",
|
||||
"#2F54EB",
|
||||
"#EB2F96",
|
||||
"#13C2C2",
|
||||
"#F97316",
|
||||
] as const;
|
||||
|
||||
@@ -15,6 +15,7 @@ export const useAppStore = defineStore("app", () => {
|
||||
withoutAnimation: false,
|
||||
});
|
||||
const activeTopMenuPath = useStorage(STORAGE_KEYS.ACTIVE_TOP_MENU_PATH, "");
|
||||
const contentFullscreen = ref(false);
|
||||
|
||||
const locale = computed(() => (language?.value === "en" ? en : zhCn));
|
||||
|
||||
@@ -49,6 +50,10 @@ export const useAppStore = defineStore("app", () => {
|
||||
activeTopMenuPath.value = val;
|
||||
}
|
||||
|
||||
function toggleContentFullscreen() {
|
||||
contentFullscreen.value = !contentFullscreen.value;
|
||||
}
|
||||
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
@@ -56,12 +61,14 @@ export const useAppStore = defineStore("app", () => {
|
||||
locale,
|
||||
size,
|
||||
activeTopMenu,
|
||||
contentFullscreen,
|
||||
toggleDevice,
|
||||
changeSize,
|
||||
changeLanguage,
|
||||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
toggleContentFullscreen,
|
||||
activeTopMenuPath,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,6 +32,12 @@ export const useSettingsStore = defineStore("setting", () => {
|
||||
// 主题
|
||||
const theme = useStorage<ThemeMode>(STORAGE_KEYS.THEME, defaults.theme);
|
||||
const themeColor = useStorage(STORAGE_KEYS.THEME_COLOR, defaults.themeColor);
|
||||
|
||||
// 旧默认值 → 新默认值 自动迁移(用户自定义的颜色不会被覆盖)
|
||||
const LEGACY_DEFAULTS = ["#4080FF", "#4080ff", "#2563EB", "#2563eb"];
|
||||
if (LEGACY_DEFAULTS.includes(themeColor.value)) {
|
||||
themeColor.value = defaults.themeColor;
|
||||
}
|
||||
const resolvedTheme = ref<ThemeMode>(resolveThemeMode(theme.value));
|
||||
|
||||
// 特殊模式
|
||||
|
||||
@@ -218,6 +218,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
title: route.meta.title as string,
|
||||
path: route.path,
|
||||
fullPath: route.fullPath,
|
||||
icon: route.meta?.icon as string | undefined,
|
||||
affix: route.meta?.affix,
|
||||
keepAlive: route.meta?.keepAlive,
|
||||
query: route.query,
|
||||
|
||||
@@ -36,11 +36,32 @@ body {
|
||||
"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑",
|
||||
Arial, sans-serif;
|
||||
line-height: inherit;
|
||||
background-color: var(--page-bg);
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
|
||||
/* 全局平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 滚动条现代化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(0 0 0 / 12%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
display: inline-block;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* 主题变量定义
|
||||
* 只放 :root、html.dark、html.sidebar-color-blue 等定义真实颜色的 CSS 变量
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
@@ -15,8 +14,42 @@
|
||||
--menu-hover: #e6f4ff;
|
||||
|
||||
// 侧边栏 Logo
|
||||
--sidebar-logo-background: #f5f5f5;
|
||||
--sidebar-logo-text-color: #303133;
|
||||
--sidebar-logo-background: #fafafa;
|
||||
--sidebar-logo-text-color: #383853;
|
||||
|
||||
// 页面背景与内容区
|
||||
--page-bg: #fafbfc;
|
||||
--content-bg: #ffffff;
|
||||
|
||||
// 边框
|
||||
--border-color: #e2e8ee;
|
||||
--card-border: rgb(0 0 0 / 8%);
|
||||
|
||||
// 卡片
|
||||
--card-radius: calc(var(--custom-radius) + 4px);
|
||||
--card-shadow: 0 1px 3px 0 rgb(0 0 0 / 6%), 0 1px 2px -1px rgb(0 0 0 / 4%);
|
||||
|
||||
// 全局圆角基准(可被 settings 动态覆盖)
|
||||
--custom-radius: 8px;
|
||||
|
||||
// 页面布局间距
|
||||
--page-padding: 20px;
|
||||
--page-gap: 16px;
|
||||
|
||||
// 灰色阶
|
||||
--gray-100: #f9fafb;
|
||||
--gray-200: #f2f4f5;
|
||||
--gray-300: #e6eaeb;
|
||||
--gray-400: #dbdfe1;
|
||||
--gray-500: #949eb7;
|
||||
--gray-600: #7987a1;
|
||||
--gray-700: #4d5875;
|
||||
--gray-800: #383853;
|
||||
--gray-900: #323251;
|
||||
|
||||
// 悬浮与激活
|
||||
--hover-color: #edeff0;
|
||||
--active-color: #f2f4f5;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -44,7 +77,25 @@ html.dark {
|
||||
--sidebar-logo-background: rgb(0 0 0 / 20%);
|
||||
--sidebar-logo-text-color: #fff;
|
||||
|
||||
// Element Plus 表格选中行背景色
|
||||
--page-bg: #070707;
|
||||
--content-bg: #161618;
|
||||
--border-color: rgb(255 255 255 / 10%);
|
||||
--card-border: rgb(255 255 255 / 8%);
|
||||
--card-shadow: 0 1px 3px 0 rgb(0 0 0 / 40%), 0 1px 2px -1px rgb(0 0 0 / 30%);
|
||||
|
||||
--gray-100: #110f0f;
|
||||
--gray-200: #17171c;
|
||||
--gray-300: #393946;
|
||||
--gray-400: #505062;
|
||||
--gray-500: #73738c;
|
||||
--gray-600: #8f8fa3;
|
||||
--gray-700: #ababba;
|
||||
--gray-800: #c7c7d1;
|
||||
--gray-900: #e3e3e8;
|
||||
|
||||
--hover-color: #252530;
|
||||
--active-color: #202226;
|
||||
|
||||
.el-table {
|
||||
--el-table-current-row-bg-color: var(--el-fill-color-light);
|
||||
}
|
||||
@@ -54,7 +105,6 @@ html.dark {
|
||||
// 无障碍模式
|
||||
// ============================================
|
||||
|
||||
// 色弱模式
|
||||
html.color-weak {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
$sidebar-width: 210px;
|
||||
$sidebar-width-collapsed: 54px;
|
||||
$navbar-height: 50px;
|
||||
$tags-view-height: 34px;
|
||||
$tags-view-height: 40px;
|
||||
|
||||
// ============================================
|
||||
// CSS 变量映射(供组件和 SCSS 使用)
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
.page-container {
|
||||
padding: 16px;
|
||||
padding: var(--page-padding);
|
||||
}
|
||||
|
||||
.page-aside {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--page-gap);
|
||||
|
||||
@media (min-width: 992px) {
|
||||
margin-bottom: 0;
|
||||
@@ -17,15 +17,18 @@
|
||||
.page-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: var(--page-gap);
|
||||
}
|
||||
|
||||
.page-search {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--page-gap);
|
||||
|
||||
/* 覆盖 el-card 内部 padding */
|
||||
.el-card__body {
|
||||
padding: 12px 12px 0;
|
||||
padding: 14px 16px 4px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +42,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--page-gap);
|
||||
|
||||
&__left,
|
||||
&__right {
|
||||
|
||||
129
src/styles/vendors/_element-plus-override.scss
vendored
129
src/styles/vendors/_element-plus-override.scss
vendored
@@ -2,14 +2,46 @@
|
||||
* Element Plus 组件样式定制
|
||||
*/
|
||||
|
||||
$border: 1px solid var(--el-border-color-light);
|
||||
$border: 1px solid var(--border-color);
|
||||
|
||||
/* el-dialog */
|
||||
/* 全局组件圆角基准 */
|
||||
:root {
|
||||
--el-border-radius-base: 6px;
|
||||
--el-border-radius-small: 4px;
|
||||
--el-font-weight-primary: 400;
|
||||
}
|
||||
|
||||
/* 卡片 — 边框模式 */
|
||||
.el-card {
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--card-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 4px 12px 0 rgb(0 0 0 / 8%),
|
||||
0 1px 3px 0 rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 15px 20px;
|
||||
padding: 20px 20px 0;
|
||||
margin: 0;
|
||||
border-bottom: $border;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
@@ -17,15 +49,15 @@ $border: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 15px;
|
||||
border-top: $border;
|
||||
padding: 0 20px 20px;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* el-drawer */
|
||||
/* 抽屉 */
|
||||
.el-drawer {
|
||||
.el-drawer__header {
|
||||
padding: 15px 20px;
|
||||
padding: 16px 20px;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
border-bottom: $border;
|
||||
@@ -36,11 +68,89 @@ $border: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.el-drawer__footer {
|
||||
padding: 15px;
|
||||
padding: 16px;
|
||||
border-top: $border;
|
||||
}
|
||||
}
|
||||
|
||||
/* el-table */
|
||||
.el-table {
|
||||
--el-table-border-color: var(--border-color);
|
||||
--el-table-header-bg-color: var(--gray-100);
|
||||
--el-table-row-hover-bg-color: var(--el-fill-color-lighter);
|
||||
--el-table-current-row-bg-color: var(--el-fill-color-light);
|
||||
|
||||
border-radius: var(--custom-radius);
|
||||
|
||||
/* 表头 */
|
||||
.el-table__header th {
|
||||
padding: 12px 0;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 单元格 */
|
||||
.el-table__body td {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* 无数据时不显示底部边框 */
|
||||
.el-table__empty-block {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 斑马纹 — 更柔和 */
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: rgb(0 0 0 / 1.5%);
|
||||
}
|
||||
|
||||
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: rgb(255 255 255 / 2%);
|
||||
}
|
||||
|
||||
/* 下拉菜单项 */
|
||||
.el-dropdown-menu__item,
|
||||
.el-select-dropdown__item {
|
||||
margin: 2px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.el-button {
|
||||
--el-button-border-radius: var(--el-border-radius-base);
|
||||
letter-spacing: 0.01em;
|
||||
|
||||
&:not(.is-link):not(.el-button--large):not(.el-button--small) {
|
||||
padding: 9px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--default {
|
||||
--el-button-border-color: var(--border-color);
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary,
|
||||
.el-button--success,
|
||||
.el-button--warning,
|
||||
.el-button--danger {
|
||||
&:hover:not(.is-disabled) {
|
||||
box-shadow: 0 2px 6px 0 var(--el-button-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.el-tag {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 抽屉和对话框底部按钮区域 */
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
@@ -56,6 +166,7 @@ $border: 1px solid var(--el-border-color-light);
|
||||
--el-menu-hover-bg-color: var(--menu-hover);
|
||||
|
||||
background-color: var(--menu-background);
|
||||
border-radius: 8px;
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
|
||||
10
src/styles/vendors/_element-plus.scss
vendored
10
src/styles/vendors/_element-plus.scss
vendored
@@ -6,19 +6,19 @@
|
||||
$colors: (
|
||||
"primary": (
|
||||
// 默认主题色 - 修改此值时需同步修改 src/settings.ts 中的 themeColor
|
||||
"base": #4080ff,
|
||||
"base": #1677ff,
|
||||
),
|
||||
"success": (
|
||||
"base": #23c343,
|
||||
"base": #22c55e,
|
||||
),
|
||||
"warning": (
|
||||
"base": #ff9a2e,
|
||||
"base": #faad14,
|
||||
),
|
||||
"danger": (
|
||||
"base": #f76560,
|
||||
"base": #ff4d4f,
|
||||
),
|
||||
"info": (
|
||||
"base": #a9aeb8,
|
||||
"base": #788896,
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export function generateThemeColors(primary: string, theme: ThemeMode) {
|
||||
primary,
|
||||
};
|
||||
|
||||
// 生成浅色变体
|
||||
// 生成 primary 浅色/深色变体
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
colors[`primary-light-${i}`] =
|
||||
resolvedTheme === ThemeMode.LIGHT
|
||||
@@ -67,12 +67,33 @@ export function generateThemeColors(primary: string, theme: ThemeMode) {
|
||||
: `${getDarkColor(primary, i / 10)}`;
|
||||
}
|
||||
|
||||
// 生成深色变体
|
||||
colors["primary-dark-2"] =
|
||||
resolvedTheme === ThemeMode.LIGHT
|
||||
? `${getLightColor(primary, 0.2)}`
|
||||
: `${getDarkColor(primary, 0.3)}`;
|
||||
|
||||
// 语义色(success / warning / danger / info)
|
||||
const semanticColors: Record<string, string> = {
|
||||
success: "#22c55e",
|
||||
warning: "#faad14",
|
||||
danger: "#ff4d4f",
|
||||
info: "#788896",
|
||||
};
|
||||
|
||||
Object.entries(semanticColors).forEach(([name, base]) => {
|
||||
colors[name] = base;
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
colors[`${name}-light-${i}`] =
|
||||
resolvedTheme === ThemeMode.LIGHT
|
||||
? `${getLightColor(base, i / 10)}`
|
||||
: `${getDarkColor(base, i / 10)}`;
|
||||
}
|
||||
colors[`${name}-dark-2`] =
|
||||
resolvedTheme === ThemeMode.LIGHT
|
||||
? `${getLightColor(base, 0.2)}`
|
||||
: `${getDarkColor(base, 0.3)}`;
|
||||
});
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
21
src/views/demo/icon-select.vue
Normal file
21
src/views/demo/icon-select.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<!-- 图标选择器示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/icon-select.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
class="mb-10"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<icon-select v-model="iconName" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// element-plus 图标格式以el-icon-开头
|
||||
const iconName = ref("el-icon-edit");
|
||||
// 本地SVG图标格式为src/assets/icons 下的文件名,不需要svg后缀
|
||||
// const iconName = ref("api");
|
||||
</script>
|
||||
@@ -1,293 +0,0 @@
|
||||
<template>
|
||||
<div class="auth-panel-form">
|
||||
<h3 class="auth-panel-form__title" text-center>{{ t("login.login") }}</h3>
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginFormData"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<!-- 用户名 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="loginFormData.username" :placeholder="t('login.username')">
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model.trim="loginFormData.password"
|
||||
:placeholder="t('login.password')"
|
||||
type="password"
|
||||
show-password
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="handleLoginSubmit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<el-form-item prop="captchaCode">
|
||||
<div flex items-center gap-10px>
|
||||
<el-input
|
||||
v-model.trim="loginFormData.captchaCode"
|
||||
:placeholder="t('login.captchaCode')"
|
||||
clearable
|
||||
class="flex-1"
|
||||
@keyup.enter="handleLoginSubmit"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="i-svg:captcha" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div cursor-pointer h-44px w-140px flex-center @click="getCaptcha">
|
||||
<el-icon v-if="codeLoading" class="is-loading" size="20"><Loading /></el-icon>
|
||||
<img
|
||||
v-else-if="captchaBase64"
|
||||
border-rd-4px
|
||||
w-full
|
||||
h-full
|
||||
block
|
||||
object-cover
|
||||
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
|
||||
:src="captchaBase64"
|
||||
alt="captchaCode"
|
||||
title="点击刷新验证码"
|
||||
@error="getCaptcha"
|
||||
/>
|
||||
<el-text v-else type="info" size="small">点击获取验证码</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="flex-x-between w-full">
|
||||
<el-checkbox v-model="loginFormData.rememberMe">{{ t("login.rememberMe") }}</el-checkbox>
|
||||
<el-link type="primary" underline="never" @click="toOtherForm('resetPwd')">
|
||||
{{ t("login.forgetPassword") }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" type="primary" class="w-full" @click="handleLoginSubmit">
|
||||
{{ t("login.login") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div flex-center gap-10px>
|
||||
<el-text size="default">{{ t("login.noAccount") }}</el-text>
|
||||
<el-link type="primary" underline="never" @click="toOtherForm('register')">
|
||||
{{ t("login.reg") }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="third-party-login">
|
||||
<div class="divider-container">
|
||||
<div class="divider-line"></div>
|
||||
<span class="divider-text">{{ t("login.otherLoginMethods") }}</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<div class="social-login">
|
||||
<div class="social-login__item">
|
||||
<div class="i-svg:wechat" />
|
||||
</div>
|
||||
<div class="social-login__item">
|
||||
<div class="i-svg:qq" />
|
||||
</div>
|
||||
<div class="social-login__item">
|
||||
<div class="i-svg:github" />
|
||||
</div>
|
||||
<div class="social-login__item">
|
||||
<div class="i-svg:gitee" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from "element-plus";
|
||||
import AuthAPI from "@/api/auth";
|
||||
import type { LoginRequest } from "@/api/auth";
|
||||
import router from "@/router";
|
||||
import { useUserStore } from "@/stores";
|
||||
import { AuthStorage } from "@/utils/auth";
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => getCaptcha());
|
||||
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
// 是否大写锁定
|
||||
const isCapsLock = ref(false);
|
||||
// 验证码图片 Base64
|
||||
const captchaBase64 = ref();
|
||||
// 记住我
|
||||
const rememberMe = AuthStorage.getRememberMe();
|
||||
const loginFormData = ref<LoginRequest>({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
captchaId: "",
|
||||
captchaCode: "",
|
||||
rememberMe,
|
||||
});
|
||||
|
||||
const loginRules = computed(() => {
|
||||
return {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.username.required"),
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
captchaCode: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.captchaCode.required"),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// 获取验证码
|
||||
const codeLoading = ref(false);
|
||||
function getCaptcha() {
|
||||
codeLoading.value = true;
|
||||
AuthAPI.getCaptcha()
|
||||
.then((data) => {
|
||||
loginFormData.value.captchaId = data.captchaId;
|
||||
captchaBase64.value = data.captchaBase64;
|
||||
})
|
||||
.finally(() => (codeLoading.value = false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录提交
|
||||
*/
|
||||
async function handleLoginSubmit() {
|
||||
// 1. 表单验证
|
||||
const valid = await loginFormRef.value?.validate().then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
if (!valid) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 2. 执行登录
|
||||
await userStore.login(loginFormData.value).then(
|
||||
async () => {
|
||||
// 登录成功,跳转到目标页面
|
||||
const redirectPath = (route.query.redirect as string) || "/";
|
||||
await router.push(decodeURIComponent(redirectPath));
|
||||
},
|
||||
(error) => {
|
||||
// 登录失败,刷新验证码
|
||||
getCaptcha();
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查输入大小写
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
// 防止浏览器密码自动填充时报错
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
function toOtherForm(type: "register" | "resetPwd") {
|
||||
emit("update:modelValue", type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-panel-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.auth-panel-form__title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.third-party-login {
|
||||
.divider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--el-border-color-light), transparent);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.social-login {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-input v-model="deptName" placeholder="部门名称" clearable>
|
||||
<el-input v-model="deptName" placeholder="搜索部门" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
class="dept-card__tree"
|
||||
:data="deptList"
|
||||
:props="{ children: 'children', label: 'label', disabled: '' }"
|
||||
:expand-on-click-node="false"
|
||||
|
||||
Reference in New Issue
Block a user