feat(layout): 实现路由锁定功能并优化侧边栏样式
- 新增 `useRouteLock` 组合式函数,用于在表单编辑等场景下锁定路由切换- 在 `AppLayout.vue` 中集成路由锁定逻辑,防止用户误操作离开当前页面 - 优化侧边栏菜单样式,增强可读性与维护性 - 调整代码格式,提升模板和样式部分的可读性
This commit is contained in:
@@ -3,7 +3,10 @@
|
|||||||
<a-layout-sider
|
<a-layout-sider
|
||||||
:width="SIDER_WIDTH"
|
:width="SIDER_WIDTH"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
: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">
|
<div class="sider-content">
|
||||||
<a-menu
|
<a-menu
|
||||||
@@ -34,17 +37,18 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
HomeOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
UnorderedListOutlined,
|
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
|
HomeOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { computed, h } from '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 { useTheme } from '../composables/useTheme.ts'
|
||||||
|
import { useRouteLock } from '../composables/useRouteLock.ts'
|
||||||
import type { MenuProps } from 'ant-design-vue'
|
import type { MenuProps } from 'ant-design-vue'
|
||||||
|
|
||||||
const SIDER_WIDTH = 160
|
const SIDER_WIDTH = 160
|
||||||
@@ -52,6 +56,7 @@ const SIDER_WIDTH = 160
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
const { isRouteLocked, triggerBlockCallback } = useRouteLock()
|
||||||
|
|
||||||
// 工具:生成菜单项
|
// 工具:生成菜单项
|
||||||
const icon = (Comp: any) => () => h(Comp)
|
const icon = (Comp: any) => () => h(Comp)
|
||||||
@@ -79,54 +84,99 @@ const selectedKeys = computed(() => {
|
|||||||
|
|
||||||
const onMenuClick: MenuProps['onClick'] = info => {
|
const onMenuClick: MenuProps['onClick'] = info => {
|
||||||
const target = String(info.key)
|
const target = String(info.key)
|
||||||
|
|
||||||
|
// 检查路由是否被锁定
|
||||||
|
if (isRouteLocked.value) {
|
||||||
|
// 如果路由被锁定,触发回调而不进行路由跳转
|
||||||
|
triggerBlockCallback(target)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (route.path !== target) router.push(target)
|
if (route.path !== target) router.push(target)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sider-content { height:100%; display:flex; flex-direction:column; padding:10px 3px; }
|
.sider-content {
|
||||||
.sider-content :deep(.ant-menu) { border-inline-end: none !important; background: transparent !important; }
|
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) {
|
.sider-content :deep(.ant-menu .ant-menu-item) {
|
||||||
color: var(--ant-color-text);
|
color: var(--ant-color-text);
|
||||||
margin: 2px auto; /* 水平居中 */
|
margin: 2px auto; /* 水平居中 */
|
||||||
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
width: calc(100% - 16px); /* 两侧各留 8px 空隙 */
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 5px 16px !important; /* 左右内边距 */
|
padding: 5px 16px !important; /* 左右内边距 */
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start; /* 左对齐图标与文字 */
|
justify-content: flex-start; /* 左对齐图标与文字 */
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
transition: background .16s ease, color .16s ease;
|
transition:
|
||||||
|
background 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
|
.sider-content :deep(.ant-menu .ant-menu-item .anticon) {
|
||||||
color: var(--ant-color-text-secondary);
|
color: var(--ant-color-text-secondary);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: color .16s ease;
|
transition: color 0.16s ease;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover */
|
/* Hover */
|
||||||
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
|
.sider-content :deep(.ant-menu .ant-menu-item:hover) {
|
||||||
background: var(--ant-color-primary-bg);
|
background: var(--ant-color-primary-bg);
|
||||||
color: var(--ant-color-text);
|
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 */
|
/* Selected */
|
||||||
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
|
.sider-content :deep(.ant-menu .ant-menu-item-selected) {
|
||||||
background: var(--ant-color-primary-bg);
|
background: var(--ant-color-primary-bg);
|
||||||
color: var(--ant-color-text) !important;
|
color: var(--ant-color-text) !important;
|
||||||
font-weight: 500;
|
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-light .ant-menu-item::after),
|
||||||
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) { display: none; }
|
.sider-content :deep(.ant-menu-dark .ant-menu-item::after) {
|
||||||
.bottom-menu { margin-top:auto; }
|
display: none;
|
||||||
.content-area { min-height:0; overflow:auto; scrollbar-width:none; -ms-overflow-style:none; padding:32px;}
|
}
|
||||||
.content-area::-webkit-scrollbar { 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>
|
</style>
|
||||||
|
|
||||||
<!-- 使用标准 Sider 布局,去除 fixed 与 marginLeft,保持菜单样式与滚动行为 -->
|
<!-- 使用标准 Sider 布局,去除 fixed 与 marginLeft,保持菜单样式与滚动行为 -->
|
||||||
|
|||||||
49
frontend/src/composables/useRouteLock.example.js
Normal file
49
frontend/src/composables/useRouteLock.example.js
Normal 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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
50
frontend/src/composables/useRouteLock.ts
Normal file
50
frontend/src/composables/useRouteLock.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user