feat(layout): 实现路由锁定功能并优化侧边栏样式

- 新增 `useRouteLock` 组合式函数,用于在表单编辑等场景下锁定路由切换- 在 `AppLayout.vue` 中集成路由锁定逻辑,防止用户误操作离开当前页面
- 优化侧边栏菜单样式,增强可读性与维护性
- 调整代码格式,提升模板和样式部分的可读性
This commit is contained in:
MoeSnowyFox
2025-09-20 23:25:21 +08:00
parent 4cc1d1186e
commit e0ba88d7b0
3 changed files with 169 additions and 20 deletions

View File

@@ -3,7 +3,10 @@
<a-layout-sider
:width="SIDER_WIDTH"
:theme="isDark ? 'dark' : 'light'"
:style="{ background: 'var(--ant-color-bg-elevated)', borderRight: '1px solid var(--ant-color-border)' }"
:style="{
background: 'var(--ant-color-bg-elevated)',
borderRight: '1px solid var(--ant-color-border)',
}"
>
<div class="sider-content">
<a-menu
@@ -34,17 +37,18 @@
<script lang="ts" setup>
import {
HomeOutlined,
FileTextOutlined,
CalendarOutlined,
UnorderedListOutlined,
ControlOutlined,
FileTextOutlined,
HistoryOutlined,
HomeOutlined,
SettingOutlined,
UnorderedListOutlined,
} from '@ant-design/icons-vue'
import { computed, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { useTheme } from '../composables/useTheme.ts'
import { useRouteLock } from '../composables/useRouteLock.ts'
import type { MenuProps } from 'ant-design-vue'
const SIDER_WIDTH = 160
@@ -52,6 +56,7 @@ const SIDER_WIDTH = 160
const router = useRouter()
const route = useRoute()
const { isDark } = useTheme()
const { isRouteLocked, triggerBlockCallback } = useRouteLock()
// 工具:生成菜单项
const icon = (Comp: any) => () => h(Comp)
@@ -79,54 +84,99 @@ const selectedKeys = computed(() => {
const onMenuClick: MenuProps['onClick'] = info => {
const target = String(info.key)
// 检查路由是否被锁定
if (isRouteLocked.value) {
// 如果路由被锁定,触发回调而不进行路由跳转
triggerBlockCallback(target)
return
}
if (route.path !== target) router.push(target)
}
</script>
<style scoped>
.sider-content { height:100%; display:flex; flex-direction:column; padding:10px 3px; }
.sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
.sider-content {
height: 100%;
display: flex;
flex-direction: column;
padding: 10px 3px;
}
.sider-content :deep(.ant-menu) {
border-inline-end: none !important;
background: transparent !important;
}
/* 菜单项外框居中(左右留空),内容左对齐 */
.sider-content :deep(.ant-menu .ant-menu-item) {
color: var(--ant-color-text);
margin: 2px auto; /* 水平居中 */
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
margin: 2px auto; /* 水平居中 */
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
border-radius: 6px;
padding: 5px 16px !important; /* 左右内边距 */
padding: 5px 16px !important; /* 左右内边距 */
line-height: 36px;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-start; /* 左对齐图标与文字 */
justify-content: flex-start; /* 左对齐图标与文字 */
gap: 6px;
transition: background .16s ease, color .16s ease;
transition:
background 0.16s ease,
color 0.16s ease;
text-align: left;
}
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
color: var(--ant-color-text-secondary);
font-size: 18px;
line-height: 1;
transition: color .16s ease;
transition: color 0.16s ease;
margin-right: 0;
}
/* Hover */
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
background: var(--ant-color-primary-bg);
color: var(--ant-color-text);
}
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) { color: var(--ant-color-text); }
.sider-content :deep(.ant-menu .ant-menu-item:hover .anticon) {
color: var(--ant-color-text);
}
/* Selected */
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
background: var(--ant-color-primary-bg);
color: var(--ant-color-text) !important;
font-weight: 500;
}
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) { color: var(--ant-color-text-secondary); }
.sider-content :deep(.ant-menu .ant-menu-item-selected .anticon) {
color: var(--ant-color-text-secondary);
}
.sider-content :deep(.ant-menu-light .ant-menu-item::after),
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) { display: none; }
.bottom-menu { margin-top:auto; }
.content-area { min-height:0; overflow:auto; scrollbar-width:none; -ms-overflow-style:none; padding:32px;}
.content-area::-webkit-scrollbar { display:none; }
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) {
display: none;
}
.bottom-menu {
margin-top: auto;
}
.content-area {
min-height: 0;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 32px;
}
.content-area::-webkit-scrollbar {
display: none;
}
</style>
<!-- 使用标准 Sider 布局去除 fixed marginLeft保持菜单样式与滚动行为 -->
<!-- 使用标准 Sider 布局去除 fixed marginLeft保持菜单样式与滚动行为 -->

View File

@@ -0,0 +1,49 @@
// 使用示例:在任何组件中使用路由锁定功能
import { useRouteLock } from '@/composables/useRouteLock'
export default {
setup() {
const { lockRoute, unlockRoute, isRouteLocked } = useRouteLock()
// 示例1在表单编辑时锁定路由
const startEditing = () => {
lockRoute(targetRoute => {
// 用户尝试切换路由时的回调
console.log(`用户尝试切换到 ${targetRoute},但表单正在编辑中`)
// 可以显示确认对话框
if (confirm('表单正在编辑中,确定要离开吗?')) {
unlockRoute() // 解锁路由
// 然后可以手动导航到目标路由
router.push(targetRoute)
}
})
}
// 示例2保存完成后解锁路由
const saveForm = async () => {
try {
// 保存逻辑...
await saveData()
// 保存成功后解锁路由
unlockRoute()
} catch (error) {
console.error('保存失败', error)
}
}
// 示例3取消编辑时解锁路由
const cancelEdit = () => {
unlockRoute()
}
return {
startEditing,
saveForm,
cancelEdit,
isRouteLocked,
}
},
}

View File

@@ -0,0 +1,50 @@
import { ref } from 'vue'
type RouteBlockCallback = (targetRoute: string) => void
const isRouteLocked = ref(false)
let blockCallback: RouteBlockCallback | null = null
export function useRouteLock() {
/**
* 锁定路由切换
* @param callback 当用户尝试切换路由时的回调函数
*/
const lockRoute = (callback: RouteBlockCallback) => {
isRouteLocked.value = true
blockCallback = callback
}
/**
* 解锁路由切换
*/
const unlockRoute = () => {
isRouteLocked.value = false
blockCallback = null
}
/**
* 检查路由是否被锁定
*/
const checkRouteLocked = () => {
return isRouteLocked.value
}
/**
* 触发路由阻止回调
* @param targetRoute 用户尝试访问的目标路由
*/
const triggerBlockCallback = (targetRoute: string) => {
if (blockCallback) {
blockCallback(targetRoute)
}
}
return {
isRouteLocked,
lockRoute,
unlockRoute,
checkRouteLocked,
triggerBlockCallback,
}
}