格式化代码,websocket功能完善

This commit is contained in:
2026-02-18 21:50:05 +08:00
parent 6543e2ccdd
commit b6c133952b
101 changed files with 15829 additions and 10739 deletions
@@ -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">
+144 -121
View File
@@ -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;
+102 -72
View File
@@ -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
+171 -105
View File
@@ -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;
}
}
+31 -13
View File
@@ -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);
}
+38 -19
View File
@@ -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);
}