feat(layout): 实现路由锁定功能并优化侧边栏样式
- 新增 `useRouteLock` 组合式函数,用于在表单编辑等场景下锁定路由切换- 在 `AppLayout.vue` 中集成路由锁定逻辑,防止用户误操作离开当前页面 - 优化侧边栏菜单样式,增强可读性与维护性 - 调整代码格式,提升模板和样式部分的可读性
This commit is contained in:
@@ -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,保持菜单样式与滚动行为 -->
|
||||
|
||||
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