style: 移除多余空行并统一代码格式

修复代码中的多余空行问题,统一字符串引号使用双引号,调整对象和数组的格式,优化代码缩进和换行,提升代码可读性和一致性
This commit is contained in:
zhangtao
2025-11-14 01:45:22 +08:00
parent 66c345d590
commit d588abb386
100 changed files with 9192 additions and 5181 deletions
@@ -58,51 +58,53 @@ const topMenus = ref<RouteRecordRaw[]>([]);
// 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题
const processedTopMenus = computed(() => {
return topMenus.value
.map((route) => {
// 如果路由本身设置了hidden=true,直接返回null
if (route.meta?.hidden === true) {
return null;
}
// 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
return route;
}
// 过滤出非隐藏的子菜单
const visibleChildren = route.children.filter((child) => !child.meta?.hidden);
// 如果没有可见子菜单,返回null
if (visibleChildren.length === 0) {
return null;
}
// 如果只有一个非隐藏的子菜单,显示子菜单的信息
if (visibleChildren.length === 1) {
const onlyChild = visibleChildren[0];
// 检查子菜单是否应该显示在顶部菜单
if (onlyChild.meta?.hidden !== true) {
return {
...route,
meta: {
...route.meta,
title: onlyChild.meta?.title || route.meta?.title,
icon: onlyChild.meta?.icon || route.meta?.icon,
},
// 使用子菜单的path确保路由匹配正确
path: onlyChild.path
};
return (
topMenus.value
.map((route) => {
// 如果路由本身设置了hidden=true,直接返回null
if (route.meta?.hidden === true) {
return null;
}
// 如果子菜单应该隐藏,则不显示该顶级菜单
return null;
}
// 其他情况返回原路由
return route;
})
// 过滤掉null值
.filter((route) => route !== null);
// 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由
if (route.meta?.alwaysShow || !route.children || route.children.length === 0) {
return route;
}
// 过滤出非隐藏的子菜单
const visibleChildren = route.children.filter((child) => !child.meta?.hidden);
// 如果没有可见子菜单,返回null
if (visibleChildren.length === 0) {
return null;
}
// 如果只有一个非隐藏的子菜单,显示子菜单的信息
if (visibleChildren.length === 1) {
const onlyChild = visibleChildren[0];
// 检查子菜单是否应该显示在顶部菜单
if (onlyChild.meta?.hidden !== true) {
return {
...route,
meta: {
...route.meta,
title: onlyChild.meta?.title || route.meta?.title,
icon: onlyChild.meta?.icon || route.meta?.icon,
},
// 使用子菜单的path确保路由匹配正确
path: onlyChild.path,
};
}
// 如果子菜单应该隐藏,则不显示该顶级菜单
return null;
}
// 其他情况返回原路由
return route;
})
// 过滤掉null值
.filter((route) => route !== null)
);
});
/**
@@ -13,7 +13,7 @@
<!-- <span class="lock-dialog-name">{{ t('navbar.lock') }}</span> -->
<span class="lock-dialog-name">{{ userStore.basicInfo.name }}</span>
</div>
<el-form ref="lockFormRef" :model="form" :rules="rules" >
<el-form ref="lockFormRef" :model="form" :rules="rules">
<el-form-item :label="t('lock.lockPassword')" prop="password">
<el-input
v-model="form.password"
@@ -25,63 +25,61 @@
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="handleLock">{{ t('navbar.lock') }}</el-button>
<el-button type="primary" @click="handleLock">{{ t("navbar.lock") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { useLockStore } from '@/store'
import { useI18n } from 'vue-i18n'
import type { FormInstance, FormRules } from 'element-plus'
import { ref, reactive, computed } from "vue";
import { useLockStore } from "@/store";
import { useI18n } from "vue-i18n";
import type { FormInstance, FormRules } from "element-plus";
import { useUserStore } from "@/store/modules/user.store";
const userStore = useUserStore();
const { t } = useI18n()
const lockStore = useLockStore()
const { t } = useI18n();
const lockStore = useLockStore();
const props = defineProps({
modelValue: {
type: Boolean
}
})
type: Boolean,
},
});
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(["update:modelValue"]);
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
emit("update:modelValue", val);
},
});
const dialogTitle = ref(t('lock.lockScreen'))
const lockFormRef = ref<FormInstance>()
const dialogTitle = ref(t("lock.lockScreen"));
const lockFormRef = ref<FormInstance>();
const form = reactive({
password: ''
})
password: "",
});
const rules: FormRules = {
password: [
{ required: true, message: t('lock.required'), trigger: 'blur' }
]
}
password: [{ required: true, message: t("lock.required"), trigger: "blur" }],
};
// 优化后的锁定逻辑
const handleLock = async () => {
try {
await lockFormRef.value?.validate()
dialogVisible.value = false
await lockFormRef.value?.validate();
dialogVisible.value = false;
lockStore.setLockInfo({
isLock: true,
password: form.password
})
password: form.password,
});
} catch {
// 验证失败时不执行任何操作
}
}
};
</script>
<style lang="scss" scoped>
@@ -1,24 +1,22 @@
<template>
<div class="lockpage">
<div
v-show="showDate"
class="unlock-container"
@click="handleShowForm(false)"
>
<div v-show="showDate" class="unlock-container" @click="handleShowForm(false)">
<el-icon><Lock /></el-icon>
<span>{{ t('lock.unlock') }}</span>
<span>{{ t("lock.unlock") }}</span>
</div>
<div class="time-container w-screen h-screen">
<div class="hour-container mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span v-show="showDate" class="meridiem absolute left-5 top-5 text-md xl:text-xl">{{ meridiem }}</span>
<span v-show="showDate" class="meridiem absolute left-5 top-5 text-md xl:text-xl">
{{ meridiem }}
</span>
</div>
<div class="minute-container w-2/5 h-2/5 md:h-4/5">
<span>{{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div v-show="!showDate" class="entry-wrapper">
<div class="entry-content">
@@ -36,7 +34,7 @@
@keydown.enter="unLock"
/>
<span v-if="errMsg" class="error-message">
{{ t('lock.message') }}
{{ t("lock.message") }}
</span>
<div class="button-group">
<el-button
@@ -47,7 +45,7 @@
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
{{ t("common.back") }}
</el-button>
<el-button
type="primary"
@@ -57,7 +55,7 @@
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
{{ t("lock.backToLogin") }}
</el-button>
<el-button
type="primary"
@@ -67,7 +65,7 @@
:disabled="loading"
@click="unLock()"
>
{{ t('lock.entrySystem') }}
{{ t("lock.entrySystem") }}
</el-button>
</div>
</div>
@@ -76,78 +74,73 @@
<div class="date-container">
<div v-show="!showDate" class="time-display">
{{ hour }}:{{ minute }} <span class="meridiem-display">{{ meridiem }}</span>
{{ hour }}:{{ minute }}
<span class="meridiem-display">{{ meridiem }}</span>
</div>
<div class="full-date">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useUserStore, useLockStore } from "@/store";
import { ElInput } from 'element-plus';
import { useNow } from '@/utils/dateUtil'
import { useTagsViewStore } from '@/store'
import { ElInput } from "element-plus";
import { useNow } from "@/utils/dateUtil";
import { useTagsViewStore } from "@/store";
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const tagsViewStore = useTagsViewStore()
const tagsViewStore = useTagsViewStore();
const { replace } = useRouter();
const { replace } = useRouter()
const password = ref("");
const loading = ref(false);
const errMsg = ref(false);
const showDate = ref(true);
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const lockStore = useLockStore();
const { hour, month, minute, meridiem, year, day, week } = useNow(true);
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
const { t } = useI18n();
// 解锁
async function unLock() {
if (!password.value) {
return
return;
}
const pwd = password.value
const pwd = password.value;
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
loading.value = true;
const res = await lockStore.unLock(pwd);
errMsg.value = !res;
} finally {
loading.value = false
loading.value = false;
}
}
// 返回登录
async function goLogin() {
await userStore.logout().catch(() => {})
userStore.resetAllState()
tagsViewStore.delAllViews()
await userStore.logout().catch(() => {});
userStore.resetAllState();
tagsViewStore.delAllViews();
router.push(`/login?redirect=${route.fullPath}`);
lockStore.resetLockInfo()
replace('/login')
lockStore.resetLockInfo();
replace("/login");
}
const passwordInputRef = ref<InstanceType<typeof ElInput>>()
const passwordInputRef = ref<InstanceType<typeof ElInput>>();
function handleShowForm(show = false) {
showDate.value = show
showDate.value = show;
if (!show) {
requestAnimationFrame(() => {
passwordInputRef.value?.focus()
})
passwordInputRef.value?.focus();
});
}
}
</script>
@@ -160,7 +153,7 @@ function handleShowForm(show = false) {
left: 0;
inset: 0;
display: flex;
width: 100%;;
width: 100%;
height: 100%;
background-color: black;
backdrop-filter: blur(8px);
@@ -193,7 +186,6 @@ function handleShowForm(show = false) {
}
}
.time-container {
display: flex;
justify-content: center;
@@ -214,7 +206,7 @@ function handleShowForm(show = false) {
align-items: center;
font-size: 220px;
}
.minute-container {
display: flex;
justify-content: center;
@@ -240,7 +232,6 @@ function handleShowForm(show = false) {
font-size: 1.25rem;
}
.entry-wrapper {
position: absolute;
top: 0;
@@ -313,11 +304,11 @@ function handleShowForm(show = false) {
width: 100%;
color: #d1d5db;
text-align: center;
@media (min-width: 1280px) {
font-size: 1.25rem;
}
@media (min-width: 1536px) {
font-size: 1.875rem;
}
@@ -326,7 +317,7 @@ function handleShowForm(show = false) {
.time-display {
font-size: 3rem;
margin-bottom: 1rem;
.meridiem-display {
font-size: 1.875rem;
}
@@ -21,7 +21,6 @@
<div v-if="settingStore.showLangSelect" class="navbar-actions__item">
<LangSelect />
</div>
</template>
<!-- 通知 -->
@@ -67,7 +66,7 @@
<el-icon><Lock /></el-icon>
{{ t("navbar.lock") }}
</el-dropdown-item>
<el-dropdown-item @click="logout">
<el-dropdown-item @click="logout">
<el-icon><SwitchButton /></el-icon>
{{ t("navbar.logout") }}
</el-dropdown-item>
@@ -76,7 +75,7 @@
</el-dropdown>
</div>
</div>
<!-- 引导 -->
<Guide v-if="guideVisible" v-model="guideVisible" @skip="handleGuideExit" />
@@ -89,8 +88,7 @@
</teleport>
<!-- 配置中心抽屉 -->
<ConfigInfoDrawer v-model="drawerVisible"/>
<ConfigInfoDrawer v-model="drawerVisible" />
</template>
<script setup lang="ts">
@@ -108,11 +106,10 @@ import Fullscreen from "@/components/Fullscreen/index.vue";
import SizeSelect from "@/components/SizeSelect/index.vue";
import LangSelect from "@/components/LangSelect/index.vue";
import Notification from "@/components/Notification/index.vue";
import LockDialog from './LockDialog.vue'
import LockPage from './LockPage.vue'
import Guide from '@/components/Guide/index.vue'
import ConfigInfoDrawer from "@/views/module_system/param/components/ConfigInfoDrawer.vue"
import LockDialog from "./LockDialog.vue";
import LockPage from "./LockPage.vue";
import Guide from "@/components/Guide/index.vue";
import ConfigInfoDrawer from "@/views/module_system/param/components/ConfigInfoDrawer.vue";
const { t } = useI18n();
const appStore = useAppStore();
@@ -131,7 +128,6 @@ function handleProfileClick() {
router.push({ name: "Profile" });
}
const drawerVisible = ref(false);
/**
* 打开配置中心页面
@@ -140,29 +136,28 @@ function handleConfigClick() {
drawerVisible.value = true;
}
/**
* 项目文档
*/
function handleDocumentClick() {
window.open('https://service.fastapiadmin.com', '_blank');
window.open("https://service.fastapiadmin.com", "_blank");
}
/**
* Gitee 项目地址
*/
function handleGiteeClick() {
window.open('https://gitee.com/tao__tao/FastapiAdmin')
window.open("https://gitee.com/tao__tao/FastapiAdmin");
}
/**
* 项目引导
* 项目引导
*/
// 使用ref而不是computed,以便可以修改引导可见性
// 使用 computed 实现双向绑定,减少 watch 的使用
const guideVisible = computed({
get: () => appStore.guideVisible,
set: (newValue) => appStore.showGuide(newValue)
set: (newValue) => appStore.showGuide(newValue),
});
function handleTourClick() {
@@ -177,27 +172,29 @@ function handleTourClick() {
// 引导结束(点击跳过或最后一步完成关闭)后,自动关闭下次登录的自动展示
function handleGuideExit() {
// 关闭自动展示开关,确保下次登录不再自动开启
settingStore.updateSetting('showGuide', false);
settingStore.updateSetting("showGuide", false);
}
// 监听引导关闭(从 true -> false),也同步关闭自动展示开关
watch(() => guideVisible.value, (val, oldVal) => {
if (oldVal && !val) {
settingStore.updateSetting('showGuide', false);
watch(
() => guideVisible.value,
(val, oldVal) => {
if (oldVal && !val) {
settingStore.updateSetting("showGuide", false);
}
}
});
);
/**
* 锁屏
*/
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const dialogVisible = ref<boolean>(false)
const lockStore = useLockStore();
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false);
const dialogVisible = ref<boolean>(false);
// 锁定屏幕
const handlelockScreen = () => {
dialogVisible.value = true
}
dialogVisible.value = true;
};
// 根据主题和侧边栏配色方案选择样式类
const navbarActionsClass = computed(() => {
@@ -234,15 +231,16 @@ function logout() {
cancelButtonText: "取消",
type: "warning",
lockScroll: false,
}).then(() => {
userStore.logout().then(() => {
router.push(`/login`);
})
.then(() => {
userStore.logout().then(() => {
router.push(`/login`);
});
})
.catch(() => {
ElMessageBox.close();
});
}).catch(() => {
ElMessageBox.close();
});
}
</script>
<style lang="scss" scoped>
@@ -80,12 +80,9 @@
<div class="color-options">
<!-- 预设颜色选项 -->
<div
v-for="(color) in displayColorPresets"
v-for="color in displayColorPresets"
:key="color"
:class="[
'color-option',
{ 'is-active': selectedThemeColor === color }
]"
:class="['color-option', { 'is-active': selectedThemeColor === color }]"
:style="{ backgroundColor: color }"
@click="handleColorSelect(color)"
>
@@ -108,9 +105,8 @@
</div>
</section>
<!-- 导航主题 -->
<section v-if="!isDark" class="config-section ">
<section v-if="!isDark" class="config-section">
<el-divider>{{ t("settings.navigation") }}</el-divider>
<div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.sidebarColorScheme") }}</span>
@@ -171,7 +167,7 @@
<span class="text-xs">{{ t("settings.showNotification") }}</span>
<el-switch v-model="settingsStore.showNotification" />
</div>
<!-- 修改此部分为引导开关,关闭以后登录以后不再启动引导 -->
<div class="flex-x-between">
<span class="text-xs">{{ t("settings.showGuide") }}</span>
@@ -216,8 +212,6 @@
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
@@ -312,7 +306,6 @@ const handleColorSelect = (color: string) => {
selectedThemeColor.value = color;
};
/**
* 复制当前配置
*/
@@ -742,21 +735,21 @@ const handleCloseDrawer = () => {
}
.color-picker-wrapper {
.custom-color-picker {
:deep(.el-color-picker__trigger) {
width: 22px;
height: 22px;
border: 2px solid var(--el-border-color-light);
border-radius: 6px;
transition: all 0.3s var(--el-transition-duration);
box-shadow: var(--el-box-shadow-light);
.custom-color-picker {
:deep(.el-color-picker__trigger) {
width: 22px;
height: 22px;
border: 2px solid var(--el-border-color-light);
border-radius: 6px;
transition: all 0.3s var(--el-transition-duration);
box-shadow: var(--el-box-shadow-light);
&:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: var(--el-box-shadow);
border-color: var(--el-color-primary-light-3);
}
&:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: var(--el-box-shadow);
border-color: var(--el-color-primary-light-3);
}
}
:deep(.el-color-picker__color) {
border: none;
@@ -774,7 +767,6 @@ const handleCloseDrawer = () => {
}
}
/* 深色模式适配 */
.dark {
.theme-color-selector {
@@ -804,7 +796,6 @@ const handleCloseDrawer = () => {
}
}
/* 复制配置对话框样式 */
:deep(.copy-config-dialog) {
.el-message-box__content {
@@ -813,4 +804,3 @@ const handleCloseDrawer = () => {
}
}
</style>
@@ -10,16 +10,20 @@
<el-scrollbar ref="scrollbarRef" class="scroll-container" @wheel="handleScroll">
<VueDraggable v-model="visitedViews" :animation="150">
<router-link
v-for="tag in displayedViews" :key="tag.fullPath"
:class="['tags-item', { active: tagsViewStore.isActive(tag) }]" :to="{ path: tag.path, query: tag.query }"
v-for="tag in displayedViews"
:key="tag.fullPath"
:class="['tags-item', { active: tagsViewStore.isActive(tag) }]"
:to="{ path: tag.path, query: tag.query }"
@click="tagSwitchSource = 'tab'"
@click.middle="handleMiddleClick(tag)">
@click.middle="handleMiddleClick(tag)"
>
<!-- 为所有标签添加右键菜单 -->
<el-dropdown
<el-dropdown
v-if="tagsViewStore.isActive(tag)"
trigger="contextmenu"
@visible-change="(visible) => onContextMenuVisibleChange(visible, tag)"
@click.stop>
@click.stop
>
<span class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
<template #dropdown>
<el-dropdown-menu>
@@ -32,7 +36,8 @@
<el-dropdown-item
:disabled="tag.affix || visitedViews.length <= 1"
@click="closeSelectedTag(tag)">
@click="closeSelectedTag(tag)"
>
<el-icon>
<Close />
</el-icon>
@@ -41,7 +46,8 @@
<el-dropdown-item
:disabled="isFirstView || !tagsViewStore.isActive(tag)"
@click="closeLeftTags">
@click="closeLeftTags"
>
<el-icon>
<Back />
</el-icon>
@@ -50,7 +56,8 @@
<el-dropdown-item
:disabled="isLastView || !tagsViewStore.isActive(tag)"
@click="closeRightTags">
@click="closeRightTags"
>
<el-icon>
<Right />
</el-icon>
@@ -59,16 +66,15 @@
<el-dropdown-item
:disabled="visitedViews.length <= 1 || !tagsViewStore.isActive(tag)"
@click="closeOtherTags">
@click="closeOtherTags"
>
<el-icon>
<Remove />
</el-icon>
{{ t("navbar.closeOther") }}
</el-dropdown-item>
<el-dropdown-item
:disabled="visitedViews.length <= 1"
@click="closeAllTags(tag)">
<el-dropdown-item :disabled="visitedViews.length <= 1" @click="closeAllTags(tag)">
<el-icon>
<Minus />
</el-icon>
@@ -81,21 +87,24 @@
<Star v-if="!isQuickLinkExists(tag)" />
<StarFilled v-else />
</el-icon>
{{ isQuickLinkExists(tag) ? '取消收藏' : '收藏' }}
{{ isQuickLinkExists(tag) ? "取消收藏" : "收藏" }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-else class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
<span v-if="!tag.affix" class="tag-close-btn" @click.prevent.stop="closeSelectedTag(tag)">
<span
v-if="!tag.affix"
class="tag-close-btn"
@click.prevent.stop="closeSelectedTag(tag)"
>
<el-icon>
<Close />
</el-icon>
</span>
</router-link>
</VueDraggable>
</el-scrollbar>
</nav>
@@ -146,7 +155,10 @@
{{ t("navbar.closeRight") }}
</el-dropdown-item>
<el-dropdown-item :disabled="visitedViews.length <= 1" @click="handleAction('closeOther')">
<el-dropdown-item
:disabled="visitedViews.length <= 1"
@click="handleAction('closeOther')"
>
<el-icon>
<Remove />
</el-icon>
@@ -172,7 +184,7 @@ import { translateRouteTitle } from "@/utils/i18n";
import { refreshAppCaches, usePermissionStore, useTagsViewStore } from "@/store";
import { VueDraggable } from "vue-draggable-plus";
import { quickStartManager } from "@/utils/quickStartManager";
import { ElMessage } from 'element-plus';
import { ElMessage } from "element-plus";
const { t } = useI18n();
@@ -183,12 +195,11 @@ const route = useRoute();
const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore();
const { visitedViews } = storeToRefs(tagsViewStore);
// 简单的标签显示逻辑
const displayedViews = computed(() => {
return visitedViews.value
return visitedViews.value;
});
// 当前选中的标签
@@ -203,7 +214,7 @@ const activeTag = computed(() => {
const scrollbarRef = ref();
// 标签切换来源跟踪
const tagSwitchSource = ref<'menu' | 'tab' | null>(null);
const tagSwitchSource = ref<"menu" | "tab" | null>(null);
// 路由映射缓存,提升查找性能
const routePathMap = computed(() => {
@@ -282,14 +293,14 @@ const addCurrentTag = () => {
if (!route.meta?.title) return;
// 检查标签是否已存在
const existingTag = visitedViews.value.find(tag => tag.path === route.path);
const existingTag = visitedViews.value.find((tag) => tag.path === route.path);
if (existingTag) {
// 如果标签已存在,根据来源决定是否移动位置
if (!existingTag.affix) {
if (tagSwitchSource.value === 'menu') {
if (tagSwitchSource.value === "menu") {
// 通过菜单点击:移动到最新位置
const index = visitedViews.value.findIndex(tag => tag.path === route.path);
const index = visitedViews.value.findIndex((tag) => tag.path === route.path);
if (index !== -1) {
const tag = visitedViews.value.splice(index, 1)[0];
visitedViews.value.push(tag);
@@ -312,7 +323,7 @@ const addCurrentTag = () => {
}
// 根据来源决定是否滚动
if (tagSwitchSource.value === 'menu') {
if (tagSwitchSource.value === "menu") {
// 通过菜单点击:滚动到最新标签
nextTick(() => {
autoScrollToLatestTag();
@@ -359,7 +370,6 @@ const handleMiddleClick = (tag: TagView) => {
* 处理滚轮事件(优化后)
*/
const handleScroll = (event: WheelEvent) => {
const scrollWrapper = scrollbarRef.value?.wrapRef;
if (!scrollWrapper) return;
@@ -373,8 +383,20 @@ const handleScroll = (event: WheelEvent) => {
const deltaX = event.deltaX || 0;
// 计算新的滚动位置
const newScrollLeft = Math.max(0, Math.min(scrollWrapper.scrollWidth - scrollWrapper.clientWidth, scrollWrapper.scrollLeft + deltaX));
const newScrollTop = Math.max(0, Math.min(scrollWrapper.scrollHeight - scrollWrapper.clientHeight, scrollWrapper.scrollTop + deltaY));
const newScrollLeft = Math.max(
0,
Math.min(
scrollWrapper.scrollWidth - scrollWrapper.clientWidth,
scrollWrapper.scrollLeft + deltaX
)
);
const newScrollTop = Math.max(
0,
Math.min(
scrollWrapper.scrollHeight - scrollWrapper.clientHeight,
scrollWrapper.scrollTop + deltaY
)
);
scrollbarRef.value.setScrollLeft(newScrollLeft);
scrollbarRef.value.setScrollTop(newScrollTop); // 新增垂直滚动支持
@@ -493,35 +515,35 @@ const handleAction = async (action: string) => {
if (!currentTag) return;
switch (action) {
case 'refresh':
case "refresh":
refreshSelectedTag(currentTag);
break;
case 'close':
case "close":
closeSelectedTag(currentTag);
break;
case 'closeRight':
case "closeRight":
closeRightTags();
break;
case 'closeLeft':
case "closeLeft":
closeLeftTags();
break;
case 'closeOther':
case "closeOther":
closeOtherTags();
break;
case 'closeAll':
case "closeAll":
closeAllTags(currentTag);
break;
case 'refreshCache':
case "refreshCache":
try {
// 1) 刷新服务端缓存(用户/权限/配置/公告/按需字典)并重建路由
await refreshAppCaches();
// 2) 软刷新当前页面,重新加载组件
refreshSelectedTag(currentTag);
// 3) 提示成功
ElMessage.success(t('navbar.refreshCache') + '完成');
ElMessage.success(t("navbar.refreshCache") + "完成");
} catch (error) {
console.error('刷新缓存失败:', error);
ElMessage.error('刷新失败');
console.error("刷新缓存失败:", error);
ElMessage.error("刷新失败");
}
break;
default:
@@ -553,7 +575,7 @@ const toggleQuickStart = (tag: TagView) => {
if (isExists) {
// 取消收藏:找到对应的链接并删除
const links = quickStartManager.getQuickLinks();
const targetLink = links.find(link => link.href === href);
const targetLink = links.find((link) => link.href === href);
if (targetLink?.id) {
quickStartManager.removeQuickLink(targetLink.id);
ElMessage.success(`已取消收藏:${tag.title}`);
@@ -565,8 +587,8 @@ const toggleQuickStart = (tag: TagView) => {
ElMessage.success(`已收藏:${tag.title}`);
}
} catch (error) {
console.error('Failed to toggle quick start:', error);
ElMessage.error('操作失败');
console.error("Failed to toggle quick start:", error);
ElMessage.error("操作失败");
}
};
@@ -581,102 +603,105 @@ const isQuickLinkExists = (tag: TagView): boolean => {
* 向左滚动标签页
*/
const scrollLeft = () => {
const scrollWrapper = scrollbarRef.value?.wrapRef
if (!scrollWrapper) return
const scrollWrapper = scrollbarRef.value?.wrapRef;
if (!scrollWrapper) return;
const newScrollLeft = Math.max(0, scrollWrapper.scrollLeft - 200)
scrollbarRef.value.setScrollLeft(newScrollLeft)
}
const newScrollLeft = Math.max(0, scrollWrapper.scrollLeft - 200);
scrollbarRef.value.setScrollLeft(newScrollLeft);
};
/**
* 向右滚动标签页
*/
const scrollRight = () => {
const scrollWrapper = scrollbarRef.value?.wrapRef
if (!scrollWrapper) return
const scrollWrapper = scrollbarRef.value?.wrapRef;
if (!scrollWrapper) return;
const maxScrollLeft = scrollWrapper.scrollWidth - scrollWrapper.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, scrollWrapper.scrollLeft + 200)
scrollbarRef.value.setScrollLeft(newScrollLeft)
const maxScrollLeft = scrollWrapper.scrollWidth - scrollWrapper.clientWidth;
const newScrollLeft = Math.min(maxScrollLeft, scrollWrapper.scrollLeft + 200);
scrollbarRef.value.setScrollLeft(newScrollLeft);
// 如果滚动到最右边,重置滚动状态以允许下次自动滚动
if (newScrollLeft >= maxScrollLeft - 1) {
scrollState.value.hasScrolledToLatest = false
scrollState.value.hasScrolledToLatest = false;
}
}
};
/**
* 重置滚动状态
*/
const resetScrollState = () => {
scrollState.value.hasScrolledToLatest = false
scrollState.value.isContainerFull = false
}
scrollState.value.hasScrolledToLatest = false;
scrollState.value.isContainerFull = false;
};
// 滚动状态跟踪
const scrollState = ref({
hasScrolledToLatest: false, // 是否已经滚动到最新标签
isContainerFull: false // 容器是否已满
})
isContainerFull: false, // 容器是否已满
});
/**
* 自动滚动到最新标签或确保当前激活标签可见
*/
const autoScrollToLatestTag = () => {
const scrollWrapper = scrollbarRef.value?.wrapRef
if (!scrollWrapper) return
const scrollWrapper = scrollbarRef.value?.wrapRef;
if (!scrollWrapper) return;
// 获取容器宽度和内容宽度
const containerWidth = scrollWrapper.clientWidth
const contentWidth = scrollWrapper.scrollWidth
const containerWidth = scrollWrapper.clientWidth;
const contentWidth = scrollWrapper.scrollWidth;
// 判断容器是否已满(内容宽度是否超过容器宽度)
const isContainerFull = contentWidth > containerWidth
const isContainerFull = contentWidth > containerWidth;
// 查找当前激活的标签元素
const activeTagElement = document.querySelector('.tags-item.active')
const activeTagElement = document.querySelector(".tags-item.active");
if (activeTagElement) {
// 将Element类型断言为HTMLElement类型以访问offsetWidth属性
const activeHtmlElement = activeTagElement as HTMLElement;
// 计算激活标签的位置信息
const activeTagRect = activeHtmlElement.getBoundingClientRect()
const containerRect = scrollWrapper.getBoundingClientRect()
const activeTagRect = activeHtmlElement.getBoundingClientRect();
const containerRect = scrollWrapper.getBoundingClientRect();
// 计算标签相对于容器的位置
const tagLeft = activeTagRect.left - containerRect.left + scrollWrapper.scrollLeft
const tagRight = tagLeft + activeHtmlElement.offsetWidth
const tagLeft = activeTagRect.left - containerRect.left + scrollWrapper.scrollLeft;
const tagRight = tagLeft + activeHtmlElement.offsetWidth;
// 检查标签是否完全在可见区域内
if (tagLeft < scrollWrapper.scrollLeft || tagRight > scrollWrapper.scrollLeft + containerWidth) {
if (
tagLeft < scrollWrapper.scrollLeft ||
tagRight > scrollWrapper.scrollLeft + containerWidth
) {
// 如果标签不在可见区域内,滚动到使标签居中的位置
const targetScrollLeft = tagLeft - (containerWidth - activeHtmlElement.offsetWidth) / 2
const maxScrollLeft = contentWidth - containerWidth
const minScrollLeft = 0
const clampedScrollLeft = Math.max(minScrollLeft, Math.min(maxScrollLeft, targetScrollLeft))
scrollbarRef.value.setScrollLeft(clampedScrollLeft)
scrollState.value.hasScrolledToLatest = true
scrollState.value.isContainerFull = isContainerFull
return
const targetScrollLeft = tagLeft - (containerWidth - activeHtmlElement.offsetWidth) / 2;
const maxScrollLeft = contentWidth - containerWidth;
const minScrollLeft = 0;
const clampedScrollLeft = Math.max(minScrollLeft, Math.min(maxScrollLeft, targetScrollLeft));
scrollbarRef.value.setScrollLeft(clampedScrollLeft);
scrollState.value.hasScrolledToLatest = true;
scrollState.value.isContainerFull = isContainerFull;
return;
}
}
// 如果没有找到激活标签或激活标签已经在可见区域内,则使用原来的逻辑
if (isContainerFull && !scrollState.value.hasScrolledToLatest) {
// 计算需要滚动到的位置,确保最新标签在右侧可见
const maxScrollLeft = contentWidth - containerWidth
scrollbarRef.value.setScrollLeft(maxScrollLeft)
scrollState.value.hasScrolledToLatest = true
scrollState.value.isContainerFull = true
const maxScrollLeft = contentWidth - containerWidth;
scrollbarRef.value.setScrollLeft(maxScrollLeft);
scrollState.value.hasScrolledToLatest = true;
scrollState.value.isContainerFull = true;
} else if (!isContainerFull) {
// 如果内容宽度不超过容器宽度,滚动到最左边
scrollbarRef.value.setScrollLeft(0)
scrollbarRef.value.setScrollLeft(0);
// 重置滚动状态
scrollState.value.hasScrolledToLatest = false
scrollState.value.isContainerFull = false
scrollState.value.hasScrolledToLatest = false;
scrollState.value.isContainerFull = false;
}
}
};
// 监听路由变化
watch(
@@ -684,7 +709,7 @@ watch(
() => {
// 如果没有设置来源,则默认为菜单点击
if (tagSwitchSource.value === null) {
tagSwitchSource.value = 'menu';
tagSwitchSource.value = "menu";
}
addCurrentTag();
updateCurrentTag();
@@ -700,7 +725,7 @@ watch(
() => visitedViews.value.length,
() => {
// 只有在通过菜单添加新标签时才滚动
if (tagSwitchSource.value === 'menu') {
if (tagSwitchSource.value === "menu") {
nextTick(() => {
autoScrollToLatestTag();
});
@@ -721,9 +746,9 @@ watch(
// 初始化
onMounted(() => {
initAffixTags();
// 监听容器大小变化
const tagsContainer = document.querySelector('.tags-container');
const tagsContainer = document.querySelector(".tags-container");
if (tagsContainer && window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => {
// 强制重新计算 displayedViews
@@ -741,7 +766,6 @@ onUnmounted(() => {
resizeObserver.disconnect();
}
});
</script>
<style lang="scss" scoped>
@@ -764,7 +788,6 @@ onUnmounted(() => {
height: $tags-view-height;
cursor: pointer;
border: 1px solid var(--el-border-color-light);
&:hover {
background-color: var(--el-fill-color-light);
@@ -860,7 +883,6 @@ onUnmounted(() => {
&:hover {
color: var(--el-color-white);
}
}
}
+2 -2
View File
@@ -3,7 +3,6 @@
<!-- 顶部菜单栏 -->
<div class="layout__header">
<div class="layout__header-content">
<!-- 顶部菜单区域 -->
<div class="layout__header-menu">
<!-- Logo区域 -->
@@ -119,7 +118,8 @@ watch(
() => route.path,
(newPath) => {
// 获取顶级路径
const topMenuPath = newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/";
const topMenuPath =
newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/";
// 如果当前路径属于当前激活的顶部菜单
if (newPath.startsWith(activeTopMenuPath.value)) {