格式化代码,websocket功能完善
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path">
|
||||
<span v-if="index === breadcrumbList.length - 1" class="no-redirect">
|
||||
<a-breadcrumb-item
|
||||
v-for="(item, index) in breadcrumbList"
|
||||
:key="item.path"
|
||||
>
|
||||
<span
|
||||
v-if="index === breadcrumbList.length - 1"
|
||||
class="no-redirect"
|
||||
>
|
||||
<component :is="item.meta?.icon || 'FileTextOutlined'" />
|
||||
{{ item.meta.title }}
|
||||
</span>
|
||||
@@ -14,42 +20,47 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import config from '@/config'
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import config from "@/config";
|
||||
|
||||
// 定义组件名称(多词命名)
|
||||
defineOptions({
|
||||
name: 'LayoutBreadcrumb'
|
||||
})
|
||||
name: "LayoutBreadcrumb",
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbList = ref([])
|
||||
const route = useRoute();
|
||||
const breadcrumbList = ref([]);
|
||||
|
||||
// 获取面包屑列表
|
||||
const getBreadcrumb = () => {
|
||||
let matched = route.matched.filter(item => item.meta && item.meta.title)
|
||||
let matched = route.matched.filter((item) => item.meta && item.meta.title);
|
||||
|
||||
// 如果第一个不是首页,添加首页
|
||||
const first = matched[0]
|
||||
const first = matched[0];
|
||||
if (first && first.path !== config.DASHBOARD_URL) {
|
||||
matched = [{ path: config.DASHBOARD_URL, meta: { title: '', icon: 'HomeOutlined' } }].concat(matched)
|
||||
matched = [
|
||||
{
|
||||
path: config.DASHBOARD_URL,
|
||||
meta: { title: "", icon: "HomeOutlined" },
|
||||
},
|
||||
].concat(matched);
|
||||
}
|
||||
|
||||
breadcrumbList.value = matched
|
||||
}
|
||||
breadcrumbList.value = matched;
|
||||
};
|
||||
|
||||
// 处理点击面包屑
|
||||
const handleLink = () => {
|
||||
return
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
getBreadcrumb()
|
||||
getBreadcrumb();
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
<template>
|
||||
<template v-for="item in menuItems" :key="item.path || item.name">
|
||||
<!-- 有子菜单 - 使用递归 -->
|
||||
<a-sub-menu v-if="item.children && item.children.length > 0" :key="`${item.path}`">
|
||||
<a-sub-menu
|
||||
v-if="item.children && item.children.length > 0"
|
||||
:key="`${item.path}`"
|
||||
>
|
||||
<template #icon v-if="item.meta?.icon">
|
||||
<component :is="getIconComponent(item.meta.icon)" />
|
||||
</template>
|
||||
<template #title>{{ item.title || item.name }}</template>
|
||||
<navMenu :menu-items="item.children" :active-path="activePath" :parent-path="item.path" />
|
||||
<navMenu
|
||||
:menu-items="item.children"
|
||||
:active-path="activePath"
|
||||
:parent-path="item.path"
|
||||
/>
|
||||
</a-sub-menu>
|
||||
<!-- 无子菜单的菜单项 -->
|
||||
<a-menu-item v-else :key="item.path" :class="{ 'ant-menu-item-selected': item.path === activePath }"
|
||||
@click="handleMenuClick(item)">
|
||||
<a-menu-item
|
||||
v-else
|
||||
:key="item.path"
|
||||
:class="{ 'ant-menu-item-selected': item.path === activePath }"
|
||||
@click="handleMenuClick(item)"
|
||||
>
|
||||
<template #icon v-if="item.meta?.icon">
|
||||
<component :is="getIconComponent(item.meta.icon)" />
|
||||
</template>
|
||||
@@ -20,35 +31,35 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as icons from '@ant-design/icons-vue'
|
||||
import { useRouter } from "vue-router";
|
||||
import * as icons from "@ant-design/icons-vue";
|
||||
|
||||
defineProps({
|
||||
menuItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
activePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: "",
|
||||
},
|
||||
parentPath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
// 获取图标组件
|
||||
const getIconComponent = (iconName) => {
|
||||
return icons[iconName] || icons.FileTextOutlined
|
||||
}
|
||||
return icons[iconName] || icons.FileTextOutlined;
|
||||
};
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = (item) => {
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
router.push(item.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div class="result-title">{{ item.title }}</div>
|
||||
<div v-if="item.breadcrumbs" class="result-path">{{ item.breadcrumbs }}</div>
|
||||
<div v-if="item.breadcrumbs" class="result-path">
|
||||
{{ item.breadcrumbs }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,20 +48,20 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="search-tips">
|
||||
<div class="tip-title">{{ $t('common.searchTips') }}</div>
|
||||
<div class="tip-title">{{ $t("common.searchTips") }}</div>
|
||||
<div class="tip-list">
|
||||
<div class="tip-item">
|
||||
<kbd>↑</kbd>
|
||||
<kbd>↓</kbd>
|
||||
<span>{{ $t('common.navigateResults') }}</span>
|
||||
<span>{{ $t("common.navigateResults") }}</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<kbd>Enter</kbd>
|
||||
<span>{{ $t('common.selectResult') }}</span>
|
||||
<span>{{ $t("common.selectResult") }}</span>
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<kbd>Esc</kbd>
|
||||
<span>{{ $t('common.closeSearch') }}</span>
|
||||
<span>{{ $t("common.closeSearch") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,133 +70,137 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SearchOutlined, MenuOutlined } from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, computed, watch, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { SearchOutlined, MenuOutlined } from "@ant-design/icons-vue";
|
||||
import { useUserStore } from "@/stores/modules/user";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'MenuSearch',
|
||||
})
|
||||
name: "MenuSearch",
|
||||
});
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const visible = defineModel('visible', { type: Boolean, default: false })
|
||||
const searchKeyword = ref('')
|
||||
const searchResults = ref([])
|
||||
const selectedIndex = ref(0)
|
||||
const searchInputRef = ref(null)
|
||||
const visible = defineModel("visible", { type: Boolean, default: false });
|
||||
const searchKeyword = ref("");
|
||||
const searchResults = ref([]);
|
||||
const selectedIndex = ref(0);
|
||||
const searchInputRef = ref(null);
|
||||
|
||||
// 将扁平化的菜单数据转换为可搜索格式
|
||||
function flattenMenus(menus, breadcrumbs = []) {
|
||||
const result = []
|
||||
const result = [];
|
||||
|
||||
menus.forEach((menu) => {
|
||||
if (menu.hidden) return
|
||||
if (menu.hidden) return;
|
||||
|
||||
const currentBreadcrumbs = [...breadcrumbs, menu.title]
|
||||
const currentBreadcrumbs = [...breadcrumbs, menu.title];
|
||||
|
||||
// 如果有路径且不是外部链接,添加到搜索结果
|
||||
if (menu.path && !menu.path.startsWith('http')) {
|
||||
if (menu.path && !menu.path.startsWith("http")) {
|
||||
result.push({
|
||||
title: menu.title,
|
||||
path: menu.path,
|
||||
icon: menu.icon,
|
||||
breadcrumbs: currentBreadcrumbs.join(' / '),
|
||||
})
|
||||
breadcrumbs: currentBreadcrumbs.join(" / "),
|
||||
});
|
||||
}
|
||||
|
||||
// 递归处理子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const children = flattenMenus(menu.children, currentBreadcrumbs)
|
||||
result.push(...children)
|
||||
const children = flattenMenus(menu.children, currentBreadcrumbs);
|
||||
result.push(...children);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取所有菜单项
|
||||
const allMenus = computed(() => {
|
||||
const menus = userStore.menu || []
|
||||
return flattenMenus(menus)
|
||||
})
|
||||
const menus = userStore.menu || [];
|
||||
return flattenMenus(menus);
|
||||
});
|
||||
|
||||
// 执行搜索
|
||||
function handleSearch() {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
searchResults.value = []
|
||||
selectedIndex.value = 0
|
||||
return
|
||||
searchResults.value = [];
|
||||
selectedIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const keyword = searchKeyword.value.toLowerCase().trim()
|
||||
const keyword = searchKeyword.value.toLowerCase().trim();
|
||||
searchResults.value = allMenus.value.filter((menu) => {
|
||||
return menu.title.toLowerCase().includes(keyword) ||
|
||||
return (
|
||||
menu.title.toLowerCase().includes(keyword) ||
|
||||
menu.breadcrumbs.toLowerCase().includes(keyword)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
selectedIndex.value = 0
|
||||
selectedIndex.value = 0;
|
||||
}
|
||||
|
||||
// 键盘导航
|
||||
function handleKeydown(e) {
|
||||
if (!searchResults.value.length) return
|
||||
if (!searchResults.value.length) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
selectedIndex.value = selectedIndex.value > 0
|
||||
? selectedIndex.value - 1
|
||||
: searchResults.value.length - 1
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
selectedIndex.value = selectedIndex.value < searchResults.value.length - 1
|
||||
? selectedIndex.value + 1
|
||||
: 0
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
selectedIndex.value =
|
||||
selectedIndex.value > 0
|
||||
? selectedIndex.value - 1
|
||||
: searchResults.value.length - 1;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
selectedIndex.value =
|
||||
selectedIndex.value < searchResults.value.length - 1
|
||||
? selectedIndex.value + 1
|
||||
: 0;
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (searchResults.value[selectedIndex.value]) {
|
||||
handleSelect(searchResults.value[selectedIndex.value])
|
||||
handleSelect(searchResults.value[selectedIndex.value]);
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
handleClose()
|
||||
break
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
handleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择菜单项
|
||||
function handleSelect(item) {
|
||||
visible.value = false
|
||||
router.push(item.path)
|
||||
visible.value = false;
|
||||
router.push(item.path);
|
||||
}
|
||||
|
||||
// 关闭搜索弹窗
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
searchKeyword.value = ''
|
||||
searchResults.value = []
|
||||
selectedIndex.value = 0
|
||||
visible.value = false;
|
||||
searchKeyword.value = "";
|
||||
searchResults.value = [];
|
||||
selectedIndex.value = 0;
|
||||
}
|
||||
|
||||
// 监听弹窗显示,自动聚焦输入框
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
nextTick(() => {
|
||||
searchInputRef.value?.focus()
|
||||
})
|
||||
searchInputRef.value?.focus();
|
||||
});
|
||||
} else {
|
||||
handleClose()
|
||||
handleClose();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
<template>
|
||||
<a-drawer v-model:open="open" title="布局配置" placement="right" :width="420">
|
||||
<a-drawer
|
||||
v-model:open="open"
|
||||
title="布局配置"
|
||||
placement="right"
|
||||
:width="420"
|
||||
>
|
||||
<div class="setting-content">
|
||||
<div class="setting-item">
|
||||
<div class="setting-title">布局模式</div>
|
||||
<div class="layout-mode-list">
|
||||
<div v-for="mode in layoutModes" :key="mode.value" class="layout-mode-item"
|
||||
:class="{ active: layoutStore.layoutMode === mode.value }"
|
||||
@click="handleLayoutChange(mode.value)">
|
||||
<div class="layout-preview" :class="`preview-${mode.value}`">
|
||||
<div
|
||||
v-for="mode in layoutModes"
|
||||
:key="mode.value"
|
||||
class="layout-mode-item"
|
||||
:class="{
|
||||
active: layoutStore.layoutMode === mode.value,
|
||||
}"
|
||||
@click="handleLayoutChange(mode.value)"
|
||||
>
|
||||
<div
|
||||
class="layout-preview"
|
||||
:class="`preview-${mode.value}`"
|
||||
>
|
||||
<div class="preview-sidebar"></div>
|
||||
<div v-if="mode.value === 'default'" class="preview-sidebar-2"></div>
|
||||
<div
|
||||
v-if="mode.value === 'default'"
|
||||
class="preview-sidebar-2"
|
||||
></div>
|
||||
<div class="preview-content">
|
||||
<div class="preview-header"></div>
|
||||
<div class="preview-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-name">{{ mode.label }}</div>
|
||||
<CheckOutlined v-if="layoutStore.layoutMode === mode.value" class="check-icon" />
|
||||
<CheckOutlined
|
||||
v-if="layoutStore.layoutMode === mode.value"
|
||||
class="check-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,9 +44,14 @@
|
||||
<div class="setting-item">
|
||||
<div class="setting-title">主题颜色</div>
|
||||
<div class="color-list">
|
||||
<div v-for="color in themeColors" :key="color" class="color-item"
|
||||
:class="{ active: themeColor === color }" :style="{ backgroundColor: color }"
|
||||
@click="changeThemeColor(color)">
|
||||
<div
|
||||
v-for="color in themeColors"
|
||||
:key="color"
|
||||
class="color-item"
|
||||
:class="{ active: themeColor === color }"
|
||||
:style="{ backgroundColor: color }"
|
||||
@click="changeThemeColor(color)"
|
||||
>
|
||||
<CheckOutlined v-if="themeColor === color" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,11 +62,17 @@
|
||||
<div class="toggle-list">
|
||||
<div class="toggle-item">
|
||||
<span>显示标签栏</span>
|
||||
<a-switch v-model:checked="showTags" @change="handleShowTagsChange" />
|
||||
<a-switch
|
||||
v-model:checked="showTags"
|
||||
@change="handleShowTagsChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="toggle-item">
|
||||
<span>显示面包屑</span>
|
||||
<a-switch v-model:checked="showBreadcrumb" @change="handleShowBreadcrumbChange" />
|
||||
<a-switch
|
||||
v-model:checked="showBreadcrumb"
|
||||
@change="handleShowBreadcrumbChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,108 +91,126 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { useLayoutStore } from "@/stores/modules/layout";
|
||||
import { CheckOutlined, ReloadOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
// 定义组件名称(多词命名)
|
||||
defineOptions({
|
||||
name: 'LayoutSetting',
|
||||
})
|
||||
name: "LayoutSetting",
|
||||
});
|
||||
|
||||
const layoutStore = useLayoutStore()
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const open = ref(false)
|
||||
const themeColor = ref('#1890ff')
|
||||
const showTags = ref(true)
|
||||
const showBreadcrumb = ref(true)
|
||||
const open = ref(false);
|
||||
const themeColor = ref("#1890ff");
|
||||
const showTags = ref(true);
|
||||
const showBreadcrumb = ref(true);
|
||||
|
||||
const layoutModes = [
|
||||
{ value: 'default', label: '默认布局' },
|
||||
{ value: 'menu', label: '菜单布局' },
|
||||
{ value: 'top', label: '顶部布局' },
|
||||
]
|
||||
{ value: "default", label: "默认布局" },
|
||||
{ value: "menu", label: "菜单布局" },
|
||||
{ value: "top", label: "顶部布局" },
|
||||
];
|
||||
|
||||
const themeColors = ['#1890ff', '#f5222d', '#fa541c', '#faad14', '#13c2c2', '#52c41a', '#2f54eb', '#722ed1']
|
||||
const themeColors = [
|
||||
"#1890ff",
|
||||
"#f5222d",
|
||||
"#fa541c",
|
||||
"#faad14",
|
||||
"#13c2c2",
|
||||
"#52c41a",
|
||||
"#2f54eb",
|
||||
"#722ed1",
|
||||
];
|
||||
|
||||
const openDrawer = () => {
|
||||
open.value = true
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const closeDrawer = () => {
|
||||
open.value = false
|
||||
}
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
})
|
||||
});
|
||||
|
||||
// 切换布局
|
||||
const handleLayoutChange = (mode) => {
|
||||
layoutStore.setLayoutMode(mode)
|
||||
const modeLabel = layoutModes.find((m) => m.value === mode)?.label || mode
|
||||
message.success(`已切换到${modeLabel}`)
|
||||
}
|
||||
layoutStore.setLayoutMode(mode);
|
||||
const modeLabel = layoutModes.find((m) => m.value === mode)?.label || mode;
|
||||
message.success(`已切换到${modeLabel}`);
|
||||
};
|
||||
|
||||
// 切换主题颜色
|
||||
const changeThemeColor = (color) => {
|
||||
themeColor.value = color
|
||||
themeColor.value = color;
|
||||
// 更新 CSS 变量
|
||||
document.documentElement.style.setProperty('--primary-color', color)
|
||||
message.success('主题颜色已更新')
|
||||
}
|
||||
document.documentElement.style.setProperty("--primary-color", color);
|
||||
message.success("主题颜色已更新");
|
||||
};
|
||||
|
||||
// 切换标签栏显示
|
||||
const handleShowTagsChange = (checked) => {
|
||||
showTags.value = checked
|
||||
showTags.value = checked;
|
||||
// 触发自定义事件或更新状态
|
||||
document.documentElement.style.setProperty('--show-tags', checked ? 'block' : 'none')
|
||||
message.success(checked ? '标签栏已显示' : '标签栏已隐藏')
|
||||
}
|
||||
document.documentElement.style.setProperty(
|
||||
"--show-tags",
|
||||
checked ? "block" : "none",
|
||||
);
|
||||
message.success(checked ? "标签栏已显示" : "标签栏已隐藏");
|
||||
};
|
||||
|
||||
// 切换面包屑显示
|
||||
const handleShowBreadcrumbChange = (checked) => {
|
||||
showBreadcrumb.value = checked
|
||||
message.success(checked ? '面包屑已显示' : '面包屑已隐藏')
|
||||
}
|
||||
showBreadcrumb.value = checked;
|
||||
message.success(checked ? "面包屑已显示" : "面包屑已隐藏");
|
||||
};
|
||||
|
||||
// 重置设置
|
||||
const handleResetSettings = () => {
|
||||
themeColor.value = '#1890ff'
|
||||
showTags.value = true
|
||||
showBreadcrumb.value = true
|
||||
layoutStore.setLayoutMode('default')
|
||||
document.documentElement.style.setProperty('--primary-color', '#1890ff')
|
||||
document.documentElement.style.setProperty('--show-tags', 'block')
|
||||
message.success('设置已重置')
|
||||
}
|
||||
themeColor.value = "#1890ff";
|
||||
showTags.value = true;
|
||||
showBreadcrumb.value = true;
|
||||
layoutStore.setLayoutMode("default");
|
||||
document.documentElement.style.setProperty("--primary-color", "#1890ff");
|
||||
document.documentElement.style.setProperty("--show-tags", "block");
|
||||
message.success("设置已重置");
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 从本地存储或其他地方恢复设置
|
||||
const savedThemeColor = localStorage.getItem('themeColor')
|
||||
const savedThemeColor = localStorage.getItem("themeColor");
|
||||
if (savedThemeColor) {
|
||||
themeColor.value = savedThemeColor
|
||||
document.documentElement.style.setProperty('--primary-color', savedThemeColor)
|
||||
themeColor.value = savedThemeColor;
|
||||
document.documentElement.style.setProperty(
|
||||
"--primary-color",
|
||||
savedThemeColor,
|
||||
);
|
||||
}
|
||||
|
||||
const savedShowTags = localStorage.getItem('showTags')
|
||||
const savedShowTags = localStorage.getItem("showTags");
|
||||
if (savedShowTags !== null) {
|
||||
showTags.value = savedShowTags === 'true'
|
||||
document.documentElement.style.setProperty('--show-tags', savedShowTags === 'true' ? 'block' : 'none')
|
||||
showTags.value = savedShowTags === "true";
|
||||
document.documentElement.style.setProperty(
|
||||
"--show-tags",
|
||||
savedShowTags === "true" ? "block" : "none",
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 监听设置变化并保存到本地存储
|
||||
watch(themeColor, (newVal) => {
|
||||
localStorage.setItem('themeColor', newVal)
|
||||
})
|
||||
localStorage.setItem("themeColor", newVal);
|
||||
});
|
||||
|
||||
watch(showTags, (newVal) => {
|
||||
localStorage.setItem('showTags', String(newVal))
|
||||
})
|
||||
localStorage.setItem("showTags", String(newVal));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,29 +1,57 @@
|
||||
<template>
|
||||
<a-menu mode="inline" :theme="theme" :collapsed="collapsed" :selected-keys="selectedKeys" :open-keys="openKeys"
|
||||
@select="handleSelect" @open-change="handleOpenChange" class="side-menu">
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:theme="theme"
|
||||
:collapsed="collapsed"
|
||||
:selected-keys="selectedKeys"
|
||||
:open-keys="openKeys"
|
||||
@select="handleSelect"
|
||||
@open-change="handleOpenChange"
|
||||
class="side-menu"
|
||||
>
|
||||
<template v-for="item in menuList">
|
||||
<!-- 有子菜单 -->
|
||||
<a-sub-menu v-if="item.children && item.children.length > 0" :key="item.path + '-submenu'">
|
||||
<a-sub-menu
|
||||
v-if="item.children && item.children.length > 0"
|
||||
:key="item.path + '-submenu'"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="item.meta?.icon || 'MenuOutlined'" />
|
||||
</template>
|
||||
<template #title>{{ item.meta?.title || item.name }}</template>
|
||||
<a-menu-item v-for="child in item.children.filter(sub => !sub.children || sub.children.length === 0)"
|
||||
:key="child.path">
|
||||
<a-menu-item
|
||||
v-for="child in item.children.filter(
|
||||
(sub) => !sub.children || sub.children.length === 0,
|
||||
)"
|
||||
:key="child.path"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="child.meta?.icon || 'FileOutlined'" />
|
||||
</template>
|
||||
{{ child.meta?.title || child.name }}
|
||||
</a-menu-item>
|
||||
<a-sub-menu v-for="child in item.children.filter(sub => sub.children && sub.children.length > 0)"
|
||||
:key="child.path">
|
||||
<a-sub-menu
|
||||
v-for="child in item.children.filter(
|
||||
(sub) => sub.children && sub.children.length > 0,
|
||||
)"
|
||||
:key="child.path"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="child.meta?.icon || 'AppstoreOutlined'" />
|
||||
<component
|
||||
:is="child.meta?.icon || 'AppstoreOutlined'"
|
||||
/>
|
||||
</template>
|
||||
<template #title>{{ child.meta?.title || child.name }}</template>
|
||||
<a-menu-item v-for="grandChild in child.children" :key="grandChild.path">
|
||||
<template #title>{{
|
||||
child.meta?.title || child.name
|
||||
}}</template>
|
||||
<a-menu-item
|
||||
v-for="grandChild in child.children"
|
||||
:key="grandChild.path"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="grandChild.meta?.icon || 'FileOutlined'" />
|
||||
<component
|
||||
:is="grandChild.meta?.icon || 'FileOutlined'"
|
||||
/>
|
||||
</template>
|
||||
{{ grandChild.meta?.title || grandChild.name }}
|
||||
</a-menu-item>
|
||||
@@ -41,112 +69,119 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getUserMenu } from '@/api/menu'
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getUserMenu } from "@/api/menu";
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light'
|
||||
}
|
||||
})
|
||||
default: "light",
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const menuList = ref([])
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
const menuList = ref([]);
|
||||
const selectedKeys = ref([]);
|
||||
const openKeys = ref([]);
|
||||
|
||||
// 获取菜单数据
|
||||
const getMenuList = async () => {
|
||||
try {
|
||||
const res = await getUserMenu()
|
||||
const res = await getUserMenu();
|
||||
if (res.code === 200) {
|
||||
menuList.value = res.data || []
|
||||
menuList.value = res.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error)
|
||||
console.error("获取菜单失败:", error);
|
||||
// 模拟数据
|
||||
menuList.value = [
|
||||
{
|
||||
path: '/home',
|
||||
name: 'Home',
|
||||
meta: { title: '首页', icon: 'HomeOutlined' }
|
||||
path: "/home",
|
||||
name: "Home",
|
||||
meta: { title: "首页", icon: "HomeOutlined" },
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
meta: { title: '系统管理', icon: 'SettingOutlined' },
|
||||
path: "/system",
|
||||
name: "System",
|
||||
meta: { title: "系统管理", icon: "SettingOutlined" },
|
||||
children: [
|
||||
{
|
||||
path: '/system/user',
|
||||
name: 'User',
|
||||
meta: { title: '用户管理', icon: 'UserOutlined' }
|
||||
path: "/system/user",
|
||||
name: "User",
|
||||
meta: { title: "用户管理", icon: "UserOutlined" },
|
||||
},
|
||||
{
|
||||
path: '/system/role',
|
||||
name: 'Role',
|
||||
meta: { title: '角色管理', icon: 'TeamOutlined' }
|
||||
path: "/system/role",
|
||||
name: "Role",
|
||||
meta: { title: "角色管理", icon: "TeamOutlined" },
|
||||
},
|
||||
{
|
||||
path: '/system/menu',
|
||||
name: 'Menu',
|
||||
meta: { title: '菜单管理', icon: 'MenuOutlined' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
path: "/system/menu",
|
||||
name: "Menu",
|
||||
meta: { title: "菜单管理", icon: "MenuOutlined" },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新选中的菜单
|
||||
const updateSelectedKeys = () => {
|
||||
selectedKeys.value = [route.path]
|
||||
selectedKeys.value = [route.path];
|
||||
|
||||
// 获取父级菜单路径
|
||||
const matched = route.matched
|
||||
.filter(item => item.path !== '/' && item.path !== route.path)
|
||||
.map(item => item.path)
|
||||
.filter((item) => item.path !== "/" && item.path !== route.path)
|
||||
.map((item) => item.path);
|
||||
|
||||
// 折叠时不自动展开
|
||||
if (!props.collapsed) {
|
||||
openKeys.value = matched
|
||||
openKeys.value = matched;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理菜单选择
|
||||
const handleSelect = ({ key }) => {
|
||||
router.push(key)
|
||||
}
|
||||
router.push(key);
|
||||
};
|
||||
|
||||
// 处理菜单展开/收起
|
||||
const handleOpenChange = (keys) => {
|
||||
openKeys.value = keys
|
||||
}
|
||||
openKeys.value = keys;
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
updateSelectedKeys()
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
updateSelectedKeys();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 监听折叠状态
|
||||
watch(() => props.collapsed, (val) => {
|
||||
if (val) {
|
||||
openKeys.value = []
|
||||
} else {
|
||||
updateSelectedKeys()
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(val) => {
|
||||
if (val) {
|
||||
openKeys.value = [];
|
||||
} else {
|
||||
updateSelectedKeys();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getMenuList()
|
||||
})
|
||||
getMenuList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
:key="tag.fullPath"
|
||||
:closable="!tag.meta?.affix"
|
||||
class="tag-item"
|
||||
:class="{ active: isActive(tag), 'tag-affix': tag.meta?.affix }"
|
||||
:class="{
|
||||
active: isActive(tag),
|
||||
'tag-affix': tag.meta?.affix,
|
||||
}"
|
||||
@click="clickTag(tag)"
|
||||
@close="closeSelectedTag(tag)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)">
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||
>
|
||||
<template #icon v-if="tag.meta?.affix">
|
||||
<PushpinFilled />
|
||||
</template>
|
||||
@@ -20,7 +24,11 @@
|
||||
</div>
|
||||
|
||||
<div class="tags-actions">
|
||||
<a-dropdown v-model:open="actionMenuVisible" trigger="click" placement="bottomRight">
|
||||
<a-dropdown
|
||||
v-model:open="actionMenuVisible"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<a-button size="small" type="text">
|
||||
<MoreOutlined />
|
||||
</a-button>
|
||||
@@ -59,16 +67,20 @@
|
||||
position: 'fixed',
|
||||
left: contextMenu.x + 'px',
|
||||
top: contextMenu.y + 'px',
|
||||
zIndex: 9999
|
||||
zIndex: 9999,
|
||||
}"
|
||||
class="context-menu"
|
||||
@click="closeContextMenu">
|
||||
@click="closeContextMenu"
|
||||
>
|
||||
<a-menu @click="handleMenuClick">
|
||||
<a-menu-item key="refresh">
|
||||
<ReloadOutlined />
|
||||
<span>刷新</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="selectedTag && !selectedTag.meta?.affix" key="close">
|
||||
<a-menu-item
|
||||
v-if="selectedTag && !selectedTag.meta?.affix"
|
||||
key="close"
|
||||
>
|
||||
<CloseOutlined />
|
||||
<span>关闭</span>
|
||||
</a-menu-item>
|
||||
@@ -87,39 +99,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import config from '@/config'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useLayoutStore } from "@/stores/modules/layout";
|
||||
import config from "@/config";
|
||||
|
||||
defineOptions({
|
||||
name: 'TagsView',
|
||||
})
|
||||
name: "TagsView",
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const showTags = ref(true)
|
||||
const selectedTag = ref(null)
|
||||
const visitedViews = computed(() => layoutStore.viewTags)
|
||||
const showTags = ref(true);
|
||||
const selectedTag = ref(null);
|
||||
const visitedViews = computed(() => layoutStore.viewTags);
|
||||
// 右键菜单状态
|
||||
const contextMenu = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
y: 0,
|
||||
});
|
||||
// 顶部操作菜单状态
|
||||
const actionMenuVisible = ref(false)
|
||||
const actionMenuVisible = ref(false);
|
||||
|
||||
// 判断是否是当前激活的标签
|
||||
const isActive = (tag) => {
|
||||
return tag.fullPath === route.fullPath
|
||||
}
|
||||
return tag.fullPath === route.fullPath;
|
||||
};
|
||||
|
||||
// 添加标签
|
||||
const addTags = () => {
|
||||
const { name } = route
|
||||
const { name } = route;
|
||||
if (name && !route.meta?.noCache) {
|
||||
layoutStore.updateViewTags({
|
||||
fullPath: route.fullPath,
|
||||
@@ -127,223 +139,232 @@ const addTags = () => {
|
||||
name: name,
|
||||
query: route.query,
|
||||
params: route.params,
|
||||
meta: route.meta
|
||||
})
|
||||
meta: route.meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 移除标签
|
||||
const closeSelectedTag = (view) => {
|
||||
// 如果是固定标签,不允许关闭
|
||||
if (view.meta?.affix) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
layoutStore.removeViewTags(view.fullPath)
|
||||
layoutStore.removeViewTags(view.fullPath);
|
||||
|
||||
// 如果关闭的是当前激活的标签,需要跳转
|
||||
if (isActive(view)) {
|
||||
const nextTag = visitedViews.value.find((tag) => tag.fullPath !== view.fullPath)
|
||||
const nextTag = visitedViews.value.find(
|
||||
(tag) => tag.fullPath !== view.fullPath,
|
||||
);
|
||||
if (nextTag) {
|
||||
router.push(nextTag.fullPath)
|
||||
router.push(nextTag.fullPath);
|
||||
} else {
|
||||
// 如果没有其他标签,跳转到首页
|
||||
router.push(config.DASHBOARD_URL)
|
||||
router.push(config.DASHBOARD_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭其他标签
|
||||
const closeOthersTags = () => {
|
||||
if (!selectedTag.value || !selectedTag.value.fullPath) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// 保留固定标签和当前选中的标签
|
||||
const tagsToKeep = visitedViews.value.filter(
|
||||
(tag) => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath
|
||||
)
|
||||
(tag) => tag.meta?.affix || tag.fullPath === selectedTag.value.fullPath,
|
||||
);
|
||||
|
||||
// 更新标签列表
|
||||
layoutStore.viewTags = tagsToKeep
|
||||
layoutStore.viewTags = tagsToKeep;
|
||||
|
||||
// 如果当前不在选中的标签页,跳转到选中的标签
|
||||
if (!isActive(selectedTag.value)) {
|
||||
router.push(selectedTag.value.fullPath)
|
||||
router.push(selectedTag.value.fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭所有标签
|
||||
const closeAllTags = () => {
|
||||
// 只保留固定标签
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix)
|
||||
layoutStore.viewTags = affixTags
|
||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix);
|
||||
layoutStore.viewTags = affixTags;
|
||||
|
||||
// 如果还有固定标签,跳转到第一个固定标签
|
||||
if (affixTags.length > 0) {
|
||||
router.push(affixTags[0].fullPath)
|
||||
router.push(affixTags[0].fullPath);
|
||||
} else {
|
||||
// 如果没有固定标签,跳转到首页
|
||||
router.push(config.DASHBOARD_URL)
|
||||
router.push(config.DASHBOARD_URL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭左侧标签
|
||||
const closeLeftTags = () => {
|
||||
const currentTag = selectedTag.value || visitedViews.value.find((tag) => isActive(tag))
|
||||
if (!currentTag) return
|
||||
const currentTag =
|
||||
selectedTag.value || visitedViews.value.find((tag) => isActive(tag));
|
||||
if (!currentTag) return;
|
||||
|
||||
const currentIndex = visitedViews.value.findIndex((tag) => tag.fullPath === currentTag.fullPath)
|
||||
if (currentIndex === -1) return
|
||||
const currentIndex = visitedViews.value.findIndex(
|
||||
(tag) => tag.fullPath === currentTag.fullPath,
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
// 保留当前标签及其右侧的标签,以及所有固定标签
|
||||
const tagsToKeep = visitedViews.value.filter((tag, index) => {
|
||||
return tag.meta?.affix || index >= currentIndex
|
||||
})
|
||||
return tag.meta?.affix || index >= currentIndex;
|
||||
});
|
||||
|
||||
layoutStore.viewTags = tagsToKeep
|
||||
}
|
||||
layoutStore.viewTags = tagsToKeep;
|
||||
};
|
||||
|
||||
// 关闭右侧标签
|
||||
const closeRightTags = () => {
|
||||
const currentTag = selectedTag.value || visitedViews.value.find((tag) => isActive(tag))
|
||||
if (!currentTag) return
|
||||
const currentTag =
|
||||
selectedTag.value || visitedViews.value.find((tag) => isActive(tag));
|
||||
if (!currentTag) return;
|
||||
|
||||
const currentIndex = visitedViews.value.findIndex((tag) => tag.fullPath === currentTag.fullPath)
|
||||
if (currentIndex === -1) return
|
||||
const currentIndex = visitedViews.value.findIndex(
|
||||
(tag) => tag.fullPath === currentTag.fullPath,
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
// 保留当前标签及其左侧的标签,以及所有固定标签
|
||||
const tagsToKeep = visitedViews.value.filter((tag, index) => {
|
||||
return tag.meta?.affix || index <= currentIndex
|
||||
})
|
||||
return tag.meta?.affix || index <= currentIndex;
|
||||
});
|
||||
|
||||
layoutStore.viewTags = tagsToKeep
|
||||
}
|
||||
layoutStore.viewTags = tagsToKeep;
|
||||
};
|
||||
|
||||
// 点击标签
|
||||
const clickTag = (tag) => {
|
||||
if (!isActive(tag)) {
|
||||
router.push(tag.fullPath)
|
||||
router.push(tag.fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新指定标签
|
||||
const refreshTag = (tag) => {
|
||||
// 如果刷新的是当前激活的标签
|
||||
if (isActive(tag)) {
|
||||
// 调用 store 的刷新方法,触发组件重新渲染
|
||||
layoutStore.refreshTag()
|
||||
layoutStore.refreshTag();
|
||||
} else {
|
||||
// 如果刷新的是其他标签,先跳转到该标签
|
||||
router.push(tag.fullPath)
|
||||
router.push(tag.fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新当前选中的标签(用于顶部操作按钮)
|
||||
const refreshSelectedTag = () => {
|
||||
// 找到当前激活的标签
|
||||
const currentTag = visitedViews.value.find((tag) => isActive(tag))
|
||||
const currentTag = visitedViews.value.find((tag) => isActive(tag));
|
||||
if (currentTag) {
|
||||
refreshTag(currentTag)
|
||||
refreshTag(currentTag);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 右键菜单处理
|
||||
const handleContextMenu = (event, tag) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
selectedTag.value = tag
|
||||
selectedTag.value = tag;
|
||||
contextMenu.value = {
|
||||
visible: true,
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
}
|
||||
y: event.clientY,
|
||||
};
|
||||
};
|
||||
|
||||
// 关闭右键菜单
|
||||
const closeContextMenu = () => {
|
||||
contextMenu.value.visible = false
|
||||
}
|
||||
contextMenu.value.visible = false;
|
||||
};
|
||||
|
||||
// 菜单点击处理
|
||||
const handleMenuClick = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'refresh':
|
||||
case "refresh":
|
||||
if (selectedTag.value) {
|
||||
refreshTag(selectedTag.value)
|
||||
refreshTag(selectedTag.value);
|
||||
}
|
||||
break
|
||||
case 'close':
|
||||
break;
|
||||
case "close":
|
||||
if (selectedTag.value && !selectedTag.value.meta?.affix) {
|
||||
closeSelectedTag(selectedTag.value)
|
||||
closeSelectedTag(selectedTag.value);
|
||||
}
|
||||
break
|
||||
case 'closeOthers':
|
||||
closeOthersTags()
|
||||
break
|
||||
case 'closeAll':
|
||||
closeAllTags()
|
||||
break
|
||||
break;
|
||||
case "closeOthers":
|
||||
closeOthersTags();
|
||||
break;
|
||||
case "closeAll":
|
||||
closeAllTags();
|
||||
break;
|
||||
}
|
||||
closeContextMenu()
|
||||
}
|
||||
closeContextMenu();
|
||||
};
|
||||
|
||||
// 顶部操作菜单点击处理
|
||||
const handleActionMenuClick = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'refresh':
|
||||
refreshSelectedTag()
|
||||
break
|
||||
case 'closeOthers':
|
||||
closeOthersTags()
|
||||
break
|
||||
case 'closeLeft':
|
||||
closeLeftTags()
|
||||
break
|
||||
case 'closeRight':
|
||||
closeRightTags()
|
||||
break
|
||||
case 'closeAll':
|
||||
closeAllTags()
|
||||
break
|
||||
case "refresh":
|
||||
refreshSelectedTag();
|
||||
break;
|
||||
case "closeOthers":
|
||||
closeOthersTags();
|
||||
break;
|
||||
case "closeLeft":
|
||||
closeLeftTags();
|
||||
break;
|
||||
case "closeRight":
|
||||
closeRightTags();
|
||||
break;
|
||||
case "closeAll":
|
||||
closeAllTags();
|
||||
break;
|
||||
}
|
||||
actionMenuVisible.value = false
|
||||
}
|
||||
actionMenuVisible.value = false;
|
||||
};
|
||||
|
||||
// 点击其他地方关闭右键菜单
|
||||
const handleClickOutside = (event) => {
|
||||
if (contextMenu.value.visible) {
|
||||
const menuElement = document.querySelector('.context-menu')
|
||||
const menuElement = document.querySelector(".context-menu");
|
||||
if (menuElement && !menuElement.contains(event.target)) {
|
||||
closeContextMenu()
|
||||
closeContextMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化,自动添加标签
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
addTags()
|
||||
addTags();
|
||||
// 更新当前选中的标签
|
||||
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || null
|
||||
selectedTag.value =
|
||||
visitedViews.value.find((tag) => isActive(tag)) || null;
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
addTags()
|
||||
addTags();
|
||||
// 初始化选中的标签
|
||||
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || null
|
||||
selectedTag.value = visitedViews.value.find((tag) => isActive(tag)) || null;
|
||||
// 添加点击事件监听器
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除点击事件监听器
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -460,7 +481,9 @@ onBeforeUnmount(() => {
|
||||
.context-menu {
|
||||
background: #ffffff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
box-shadow:
|
||||
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 4px 0;
|
||||
|
||||
@@ -11,15 +11,21 @@
|
||||
<div class="task-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ totalTasks }}</div>
|
||||
<div class="stat-label">{{ $t('common.totalTasks') }}</div>
|
||||
<div class="stat-label">{{ $t("common.totalTasks") }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number pending">{{ pendingTasks }}</div>
|
||||
<div class="stat-label">{{ $t('common.pendingTasks') }}</div>
|
||||
<div class="stat-label">
|
||||
{{ $t("common.pendingTasks") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number completed">{{ completedTasks }}</div>
|
||||
<div class="stat-label">{{ $t('common.completedTasks') }}</div>
|
||||
<div class="stat-number completed">
|
||||
{{ completedTasks }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
{{ $t("common.completedTasks") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,21 +47,23 @@
|
||||
size="small"
|
||||
@click="setFilter('all')"
|
||||
>
|
||||
{{ $t('common.all') }}
|
||||
{{ $t("common.all") }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:type="filterType === 'pending' ? 'primary' : 'default'"
|
||||
size="small"
|
||||
@click="setFilter('pending')"
|
||||
>
|
||||
{{ $t('common.pending') }}
|
||||
{{ $t("common.pending") }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:type="filterType === 'completed' ? 'primary' : 'default'"
|
||||
:type="
|
||||
filterType === 'completed' ? 'primary' : 'default'
|
||||
"
|
||||
size="small"
|
||||
@click="setFilter('completed')"
|
||||
>
|
||||
{{ $t('common.completed') }}
|
||||
{{ $t("common.completed") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,8 +86,15 @@
|
||||
<div class="task-content">
|
||||
<div class="task-title">{{ task.title }}</div>
|
||||
<div class="task-meta">
|
||||
<span class="task-priority" :class="task.priority">
|
||||
{{ $t(`common.priority${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}`) }}
|
||||
<span
|
||||
class="task-priority"
|
||||
:class="task.priority"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`common.priority${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}`,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="task-time">{{ task.time }}</span>
|
||||
</div>
|
||||
@@ -103,10 +118,10 @@
|
||||
<div class="drawer-footer">
|
||||
<a-button @click="showAddTask">
|
||||
<PlusOutlined />
|
||||
{{ $t('common.addTask') }}
|
||||
{{ $t("common.addTask") }}
|
||||
</a-button>
|
||||
<a-button danger @click="clearAllTasks">
|
||||
{{ $t('common.clearAll') }}
|
||||
{{ $t("common.clearAll") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,13 +136,22 @@
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="$t('common.taskTitle')">
|
||||
<a-input v-model:value="newTask.title" :placeholder="$t('common.enterTaskTitle')" />
|
||||
<a-input
|
||||
v-model:value="newTask.title"
|
||||
:placeholder="$t('common.enterTaskTitle')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('common.taskPriority')">
|
||||
<a-select v-model:value="newTask.priority">
|
||||
<a-select-option value="low">{{ $t('common.priorityLow') }}</a-select-option>
|
||||
<a-select-option value="medium">{{ $t('common.priorityMedium') }}</a-select-option>
|
||||
<a-select-option value="high">{{ $t('common.priorityHigh') }}</a-select-option>
|
||||
<a-select-option value="low">{{
|
||||
$t("common.priorityLow")
|
||||
}}</a-select-option>
|
||||
<a-select-option value="medium">{{
|
||||
$t("common.priorityMedium")
|
||||
}}</a-select-option>
|
||||
<a-select-option value="high">{{
|
||||
$t("common.priorityHigh")
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -136,95 +160,101 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { SearchOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import {
|
||||
SearchOutlined,
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'TaskDrawer',
|
||||
})
|
||||
name: "TaskDrawer",
|
||||
});
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = defineModel('visible', { type: Boolean, default: false })
|
||||
const visible = defineModel("visible", { type: Boolean, default: false });
|
||||
|
||||
const tasks = defineModel('tasks', { type: Array, default: () => [] })
|
||||
const tasks = defineModel("tasks", { type: Array, default: () => [] });
|
||||
|
||||
// 搜索关键词
|
||||
const searchKeyword = ref('')
|
||||
const searchKeyword = ref("");
|
||||
// 筛选类型:all, pending, completed
|
||||
const filterType = ref('all')
|
||||
const filterType = ref("all");
|
||||
|
||||
// 添加任务弹窗
|
||||
const addTaskVisible = ref(false)
|
||||
const addTaskVisible = ref(false);
|
||||
const newTask = ref({
|
||||
title: '',
|
||||
priority: 'medium',
|
||||
})
|
||||
title: "",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const totalTasks = computed(() => tasks.value.length)
|
||||
const pendingTasks = computed(() => tasks.value.filter(t => !t.completed).length)
|
||||
const completedTasks = computed(() => tasks.value.filter(t => t.completed).length)
|
||||
const totalTasks = computed(() => tasks.value.length);
|
||||
const pendingTasks = computed(
|
||||
() => tasks.value.filter((t) => !t.completed).length,
|
||||
);
|
||||
const completedTasks = computed(
|
||||
() => tasks.value.filter((t) => t.completed).length,
|
||||
);
|
||||
|
||||
// 筛选后的任务列表
|
||||
const filteredTasks = computed(() => {
|
||||
let result = [...tasks.value]
|
||||
let result = [...tasks.value];
|
||||
|
||||
// 按状态筛选
|
||||
if (filterType.value === 'pending') {
|
||||
result = result.filter(t => !t.completed)
|
||||
} else if (filterType.value === 'completed') {
|
||||
result = result.filter(t => t.completed)
|
||||
if (filterType.value === "pending") {
|
||||
result = result.filter((t) => !t.completed);
|
||||
} else if (filterType.value === "completed") {
|
||||
result = result.filter((t) => t.completed);
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value.trim()) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(t =>
|
||||
t.title.toLowerCase().includes(keyword)
|
||||
)
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
result = result.filter((t) => t.title.toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
|
||||
// 切换任务状态
|
||||
const toggleTask = (task) => {
|
||||
task.completed = !task.completed
|
||||
}
|
||||
task.completed = !task.completed;
|
||||
};
|
||||
|
||||
// 删除任务
|
||||
const deleteTask = (id) => {
|
||||
const index = tasks.value.findIndex(t => t.id === id)
|
||||
const index = tasks.value.findIndex((t) => t.id === id);
|
||||
if (index > -1) {
|
||||
tasks.value.splice(index, 1)
|
||||
message.success(t('common.deleted'))
|
||||
tasks.value.splice(index, 1);
|
||||
message.success(t("common.deleted"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 清空所有任务
|
||||
const clearAllTasks = () => {
|
||||
tasks.value = []
|
||||
message.success(t('common.cleared'))
|
||||
}
|
||||
tasks.value = [];
|
||||
message.success(t("common.cleared"));
|
||||
};
|
||||
|
||||
// 显示添加任务弹窗
|
||||
const showAddTask = () => {
|
||||
newTask.value = {
|
||||
title: '',
|
||||
priority: 'medium',
|
||||
}
|
||||
addTaskVisible.value = true
|
||||
}
|
||||
title: "",
|
||||
priority: "medium",
|
||||
};
|
||||
addTaskVisible.value = true;
|
||||
};
|
||||
|
||||
// 确认添加任务
|
||||
const confirmAddTask = () => {
|
||||
if (!newTask.value.title.trim()) {
|
||||
message.warning(t('common.pleaseEnterTaskTitle'))
|
||||
return
|
||||
message.warning(t("common.pleaseEnterTaskTitle"));
|
||||
return;
|
||||
}
|
||||
|
||||
tasks.value.unshift({
|
||||
@@ -232,30 +262,30 @@ const confirmAddTask = () => {
|
||||
title: newTask.value.title,
|
||||
priority: newTask.value.priority,
|
||||
completed: false,
|
||||
time: t('common.justNow'),
|
||||
})
|
||||
time: t("common.justNow"),
|
||||
});
|
||||
|
||||
addTaskVisible.value = false
|
||||
message.success(t('common.added'))
|
||||
}
|
||||
addTaskVisible.value = false;
|
||||
message.success(t("common.added"));
|
||||
};
|
||||
|
||||
// 设置筛选类型
|
||||
const setFilter = (type) => {
|
||||
filterType.value = type
|
||||
}
|
||||
filterType.value = type;
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在 computed 中自动处理
|
||||
}
|
||||
};
|
||||
|
||||
// 监听抽窗关闭,重置搜索和筛选
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
searchKeyword.value = ''
|
||||
filterType.value = 'all'
|
||||
searchKeyword.value = "";
|
||||
filterType.value = "all";
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,21 @@
|
||||
<!-- 第一个侧边栏:显示一级菜单 -->
|
||||
<a-layout-sider theme="dark" width="70" class="left-sidebar">
|
||||
<div class="logo-box">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
<img
|
||||
src="@/assets/images/logo.png"
|
||||
alt="logo"
|
||||
class="logo-image"
|
||||
/>
|
||||
</div>
|
||||
<ul class="left-nav">
|
||||
<li v-for="(item, index) in menuList" :key="index"
|
||||
:class="{ active: selectedParentMenu?.path === item.path }"
|
||||
@click="handleParentMenuClick(item)">
|
||||
<li
|
||||
v-for="(item, index) in menuList"
|
||||
:key="index"
|
||||
:class="{
|
||||
active: selectedParentMenu?.path === item.path,
|
||||
}"
|
||||
@click="handleParentMenuClick(item)"
|
||||
>
|
||||
<component :is="getIconComponent(item.meta?.icon)" />
|
||||
<span>{{ item.title }}</span>
|
||||
</li>
|
||||
@@ -19,16 +28,37 @@
|
||||
|
||||
<!-- 第二个侧边栏:显示选中的父菜单的子菜单 -->
|
||||
<a-layout-sider
|
||||
v-if="selectedParentMenu && selectedParentMenu.children && selectedParentMenu.children.length > 0"
|
||||
theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" width="200"
|
||||
:collapsed-width="64" class="right-sidebar">
|
||||
v-if="
|
||||
selectedParentMenu &&
|
||||
selectedParentMenu.children &&
|
||||
selectedParentMenu.children.length > 0
|
||||
"
|
||||
theme="light"
|
||||
:collapsed="sidebarCollapsed"
|
||||
:collapsible="true"
|
||||
@collapse="handleCollapse"
|
||||
width="200"
|
||||
:collapsed-width="64"
|
||||
class="right-sidebar"
|
||||
>
|
||||
<div class="parent-title">
|
||||
<component :is="getIconComponent(selectedParentMenu.meta?.icon)" />
|
||||
<span v-if="!sidebarCollapsed">{{ selectedParentMenu.title }}</span>
|
||||
<component
|
||||
:is="getIconComponent(selectedParentMenu.meta?.icon)"
|
||||
/>
|
||||
<span v-if="!sidebarCollapsed">{{
|
||||
selectedParentMenu.title
|
||||
}}</span>
|
||||
</div>
|
||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||||
:selected-keys="[route.path]">
|
||||
<navMenu :menu-items="selectedParentMenu.children" :active-path="route.path" />
|
||||
<a-menu
|
||||
v-model:openKeys="openKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
:selected-keys="[route.path]"
|
||||
>
|
||||
<navMenu
|
||||
:menu-items="selectedParentMenu.children"
|
||||
:active-path="route.path"
|
||||
/>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
@@ -52,15 +82,32 @@
|
||||
|
||||
<!-- Menu布局:左侧菜单栏布局 -->
|
||||
<template v-else-if="layoutMode === 'menu'">
|
||||
<a-layout-sider theme="light" style="border-right: 1px solid #f0f0f0" :collapsed="sidebarCollapsed"
|
||||
:collapsible="true" @collapse="handleCollapse" class="full-menu-sidebar" width="200"
|
||||
:collapsed-width="64">
|
||||
<a-layout-sider
|
||||
theme="light"
|
||||
style="border-right: 1px solid #f0f0f0"
|
||||
:collapsed="sidebarCollapsed"
|
||||
:collapsible="true"
|
||||
@collapse="handleCollapse"
|
||||
class="full-menu-sidebar"
|
||||
width="200"
|
||||
:collapsed-width="64"
|
||||
>
|
||||
<div class="logo-box-full">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
<span v-if="!sidebarCollapsed" class="app-name">{{ config.APP_NAME }}</span>
|
||||
<img
|
||||
src="@/assets/images/logo.png"
|
||||
alt="logo"
|
||||
class="logo-image"
|
||||
/>
|
||||
<span v-if="!sidebarCollapsed" class="app-name">{{
|
||||
config.APP_NAME
|
||||
}}</span>
|
||||
</div>
|
||||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||||
:selected-keys="[route.path]">
|
||||
<a-menu
|
||||
v-model:openKeys="openKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
:selected-keys="[route.path]"
|
||||
>
|
||||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
@@ -87,12 +134,23 @@
|
||||
<a-layout-header class="app-header top-header">
|
||||
<div class="top-header-left">
|
||||
<div class="logo-box-top">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo-image" />
|
||||
<img
|
||||
src="@/assets/images/logo.png"
|
||||
alt="logo"
|
||||
class="logo-image"
|
||||
/>
|
||||
<span class="app-name">{{ config.APP_NAME }}</span>
|
||||
</div>
|
||||
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
||||
style="line-height: 60px">
|
||||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="horizontal"
|
||||
:selected-keys="[route.path]"
|
||||
style="line-height: 60px"
|
||||
>
|
||||
<navMenu
|
||||
:menu-items="menuList"
|
||||
:active-path="route.path"
|
||||
/>
|
||||
</a-menu>
|
||||
</div>
|
||||
<userbar />
|
||||
@@ -120,139 +178,143 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useLayoutStore } from '@/stores/modules/layout'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { SettingOutlined } from '@ant-design/icons-vue'
|
||||
import * as icons from '@ant-design/icons-vue'
|
||||
import config from '@/config/index.js'
|
||||
import { computed, ref, watch, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useLayoutStore } from "@/stores/modules/layout";
|
||||
import { useUserStore } from "@/stores/modules/user";
|
||||
import { SettingOutlined } from "@ant-design/icons-vue";
|
||||
import * as icons from "@ant-design/icons-vue";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
import userbar from './components/userbar.vue'
|
||||
import navMenu from './components/navMenu.vue'
|
||||
import breadcrumb from './components/breadcrumb.vue'
|
||||
import tags from './components/tags.vue'
|
||||
import setting from './components/setting.vue'
|
||||
import userbar from "./components/userbar.vue";
|
||||
import navMenu from "./components/navMenu.vue";
|
||||
import breadcrumb from "./components/breadcrumb.vue";
|
||||
import tags from "./components/tags.vue";
|
||||
import setting from "./components/setting.vue";
|
||||
|
||||
// 定义组件名称(多词命名)
|
||||
defineOptions({
|
||||
name: 'AppLayouts',
|
||||
})
|
||||
name: "AppLayouts",
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const layoutStore = useLayoutStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const settingRef = ref(null)
|
||||
const settingRef = ref(null);
|
||||
|
||||
const layoutMode = computed(() => layoutStore.layoutMode)
|
||||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
||||
const selectedParentMenu = computed(() => layoutStore.selectedParentMenu)
|
||||
const layoutMode = computed(() => layoutStore.layoutMode);
|
||||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed);
|
||||
const selectedParentMenu = computed(() => layoutStore.selectedParentMenu);
|
||||
|
||||
// 缓存的视图列表
|
||||
const cachedViews = computed(() => {
|
||||
return layoutStore.viewTags.filter((tag) => !tag.meta?.noCache).map((tag) => tag.name)
|
||||
})
|
||||
return layoutStore.viewTags
|
||||
.filter((tag) => !tag.meta?.noCache)
|
||||
.map((tag) => tag.name);
|
||||
});
|
||||
|
||||
// 布局类名
|
||||
const layoutClass = computed(() => {
|
||||
return {
|
||||
'layout-default': layoutMode.value === 'default',
|
||||
'layout-menu': layoutMode.value === 'menu',
|
||||
'layout-top': layoutMode.value === 'top',
|
||||
'is-collapse': sidebarCollapsed.value,
|
||||
}
|
||||
})
|
||||
"layout-default": layoutMode.value === "default",
|
||||
"layout-menu": layoutMode.value === "menu",
|
||||
"layout-top": layoutMode.value === "top",
|
||||
"is-collapse": sidebarCollapsed.value,
|
||||
};
|
||||
});
|
||||
|
||||
// 获取刷新 key
|
||||
const refreshKey = computed(() => layoutStore.refreshKey)
|
||||
const refreshKey = computed(() => layoutStore.refreshKey);
|
||||
|
||||
const openKeys = ref([])
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([]);
|
||||
const selectedKeys = ref([]);
|
||||
const menuList = computed(() => {
|
||||
return userStore.menu
|
||||
})
|
||||
return userStore.menu;
|
||||
});
|
||||
|
||||
// 获取图标组件
|
||||
const getIconComponent = (iconName) => {
|
||||
return icons[iconName] || icons.FileTextOutlined
|
||||
}
|
||||
return icons[iconName] || icons.FileTextOutlined;
|
||||
};
|
||||
|
||||
// 处理父菜单点击(默认布局的第一级菜单)
|
||||
const handleParentMenuClick = (item) => {
|
||||
// 设置选中的父菜单
|
||||
layoutStore.setSelectedParentMenu(item)
|
||||
layoutStore.setSelectedParentMenu(item);
|
||||
// 如果没有子菜单,直接跳转
|
||||
if (!item.children || item.children.length === 0) {
|
||||
if (item.path) {
|
||||
router.push(item.path)
|
||||
router.push(item.path);
|
||||
}
|
||||
} else {
|
||||
// 默认展开第一个子菜单
|
||||
if (item.children.length > 0 && item.children[0].path) {
|
||||
router.push(item.children[0].path)
|
||||
router.push(item.children[0].path);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理折叠
|
||||
const handleCollapse = (collapsed) => {
|
||||
layoutStore.sidebarCollapsed = collapsed
|
||||
}
|
||||
layoutStore.sidebarCollapsed = collapsed;
|
||||
};
|
||||
|
||||
// 打开设置抽屉
|
||||
const openSetting = () => {
|
||||
settingRef.value?.openDrawer()
|
||||
}
|
||||
settingRef.value?.openDrawer();
|
||||
};
|
||||
|
||||
// 更新选中的菜单和展开的菜单
|
||||
const updateMenuState = () => {
|
||||
selectedKeys.value = [route.path]
|
||||
selectedKeys.value = [route.path];
|
||||
|
||||
// 获取所有父级路径
|
||||
const matched = route.matched.filter((item) => item.path !== '/' && item.path !== route.path)
|
||||
const parentPaths = matched.map((item) => item.path)
|
||||
const matched = route.matched.filter(
|
||||
(item) => item.path !== "/" && item.path !== route.path,
|
||||
);
|
||||
const parentPaths = matched.map((item) => item.path);
|
||||
|
||||
// 对于不同的布局模式,处理方式不同
|
||||
if (layoutMode.value === 'default') {
|
||||
if (layoutMode.value === "default") {
|
||||
// 默认布局:找到当前路由对应的父菜单
|
||||
const currentMenu = findMenuByPath(menuList.value, route.path)
|
||||
const currentMenu = findMenuByPath(menuList.value, route.path);
|
||||
if (currentMenu) {
|
||||
// 如果当前菜单有子菜单,设置为选中的父菜单
|
||||
if (currentMenu.children && currentMenu.children.length > 0) {
|
||||
layoutStore.setSelectedParentMenu(currentMenu)
|
||||
layoutStore.setSelectedParentMenu(currentMenu);
|
||||
} else {
|
||||
// 如果当前菜单是子菜单,找到它的父菜单
|
||||
const parentMenu = findParentMenu(menuList.value, route.path)
|
||||
const parentMenu = findParentMenu(menuList.value, route.path);
|
||||
if (parentMenu) {
|
||||
layoutStore.setSelectedParentMenu(parentMenu)
|
||||
layoutStore.setSelectedParentMenu(parentMenu);
|
||||
} else {
|
||||
layoutStore.setSelectedParentMenu(currentMenu)
|
||||
layoutStore.setSelectedParentMenu(currentMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!sidebarCollapsed.value) {
|
||||
// 其他布局模式:展开所有父级菜单
|
||||
openKeys.value = parentPaths
|
||||
openKeys.value = parentPaths;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 根据路径查找菜单
|
||||
const findMenuByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuByPath(menu.children, path)
|
||||
const found = findMenuByPath(menu.children, path);
|
||||
if (found) {
|
||||
return found
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 查找父菜单
|
||||
const findParentMenu = (menus, path) => {
|
||||
@@ -260,58 +322,62 @@ const findParentMenu = (menus, path) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
for (const child of menu.children) {
|
||||
if (child.path === path) {
|
||||
return menu
|
||||
return menu;
|
||||
}
|
||||
if (child.children && child.children.length > 0) {
|
||||
const found = findParentMenu([child], path)
|
||||
const found = findParentMenu([child], path);
|
||||
if (found) {
|
||||
return menu
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 监听路由变化,更新菜单状态
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
console.log('路由变化:', newPath)
|
||||
updateMenuState()
|
||||
console.log("路由变化:", newPath);
|
||||
updateMenuState();
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
);
|
||||
|
||||
// 监听布局模式变化,确保菜单状态正确
|
||||
watch(
|
||||
() => layoutMode.value,
|
||||
() => {
|
||||
updateMenuState()
|
||||
updateMenuState();
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// 监听折叠状态
|
||||
watch(
|
||||
() => sidebarCollapsed.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
openKeys.value = []
|
||||
openKeys.value = [];
|
||||
} else {
|
||||
updateMenuState()
|
||||
updateMenuState();
|
||||
}
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果还没有选中的父菜单,默认选中第一个
|
||||
if (layoutMode.value === 'default' && !selectedParentMenu.value && menuList.value.length > 0) {
|
||||
layoutStore.setSelectedParentMenu(menuList.value[0])
|
||||
if (
|
||||
layoutMode.value === "default" &&
|
||||
!selectedParentMenu.value &&
|
||||
menuList.value.length > 0
|
||||
) {
|
||||
layoutStore.setSelectedParentMenu(menuList.value[0]);
|
||||
}
|
||||
updateMenuState()
|
||||
})
|
||||
updateMenuState();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -467,14 +533,14 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.ant-menu-submenu {
|
||||
>.ant-menu-submenu-title {
|
||||
> .ant-menu-submenu-title {
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-menu-submenu-open {
|
||||
>.ant-menu-submenu-title {
|
||||
> .ant-menu-submenu-title {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
@@ -569,7 +635,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.ant-menu-submenu {
|
||||
>.ant-menu-submenu-title {
|
||||
> .ant-menu-submenu-title {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
margin: 0;
|
||||
@@ -581,7 +647,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&.ant-menu-submenu-open {
|
||||
>.ant-menu-submenu-title {
|
||||
> .ant-menu-submenu-title {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
<div class="not-found-content">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-title">页面未找到</div>
|
||||
<div class="error-description">抱歉,您访问的页面不存在或已被移除</div>
|
||||
<div class="error-description">
|
||||
抱歉,您访问的页面不存在或已被移除
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" size="large" @click="goBack">
|
||||
@@ -30,21 +32,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeftOutlined, HomeOutlined } from '@ant-design/icons-vue'
|
||||
import '@/assets/style/auth.scss'
|
||||
import { useRouter } from "vue-router";
|
||||
import { ArrowLeftOutlined, HomeOutlined } from "@ant-design/icons-vue";
|
||||
import "@/assets/style/auth.scss";
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
// Go back to previous page
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Go to home page
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
router.push("/");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -54,7 +56,11 @@ const goHome = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--bg-gradient-start) 0%,
|
||||
var(--bg-gradient-end) 100%
|
||||
);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -122,7 +128,11 @@ const goHome = () => {
|
||||
.error-code {
|
||||
font-size: 120px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--auth-primary-dark), var(--auth-primary));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--auth-primary-dark),
|
||||
var(--auth-primary)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -170,13 +180,21 @@ const goHome = () => {
|
||||
border-radius: 12px;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--auth-primary),
|
||||
var(--auth-primary-dark)
|
||||
);
|
||||
border: none;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--auth-primary-light),
|
||||
var(--auth-primary)
|
||||
);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
|
||||
@@ -8,38 +8,45 @@
|
||||
|
||||
<div class="empty-content">
|
||||
<div class="empty-icon">
|
||||
<InboxOutlined :style="{ fontSize: '120px', color: '#ff6b35' }" />
|
||||
<InboxOutlined
|
||||
:style="{ fontSize: '120px', color: '#ff6b35' }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="empty-title">暂无数据</div>
|
||||
<div class="empty-description">
|
||||
{{ description || '当前页面暂无数据,请稍后再试' }}
|
||||
{{ description || "当前页面暂无数据,请稍后再试" }}
|
||||
</div>
|
||||
|
||||
<a-button v-if="showButton" type="primary" size="large" @click="handleAction">
|
||||
<a-button
|
||||
v-if="showButton"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleAction"
|
||||
>
|
||||
<template #icon v-if="buttonIcon">
|
||||
<component :is="buttonIcon" />
|
||||
</template>
|
||||
{{ buttonText || '刷新页面' }}
|
||||
{{ buttonText || "刷新页面" }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { InboxOutlined } from '@ant-design/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { InboxOutlined } from "@ant-design/icons-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
defineOptions({
|
||||
name: 'EmptyPage',
|
||||
})
|
||||
name: "EmptyPage",
|
||||
});
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
defineProps({
|
||||
description: {
|
||||
type: String,
|
||||
default: '当前页面暂无数据,请稍后再试',
|
||||
default: "当前页面暂无数据,请稍后再试",
|
||||
},
|
||||
showButton: {
|
||||
type: Boolean,
|
||||
@@ -47,21 +54,21 @@ defineProps({
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '刷新页面',
|
||||
default: "刷新页面",
|
||||
},
|
||||
buttonIcon: {
|
||||
type: [String, Object],
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action'])
|
||||
const emit = defineEmits(["action"]);
|
||||
|
||||
const handleAction = () => {
|
||||
emit('action')
|
||||
emit("action");
|
||||
// Default behavior: refresh page
|
||||
router.go(0)
|
||||
}
|
||||
router.go(0);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -71,7 +78,11 @@ const handleAction = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--bg-gradient-start) 0%,
|
||||
var(--bg-gradient-end) 100%
|
||||
);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -171,14 +182,22 @@ const handleAction = () => {
|
||||
padding: 0 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, var(--auth-primary), var(--auth-primary-dark));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--auth-primary),
|
||||
var(--auth-primary-dark)
|
||||
);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.35);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, var(--auth-primary-light), var(--auth-primary));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--auth-primary-light),
|
||||
var(--auth-primary)
|
||||
);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(255, 107, 53, 0.45);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user