前端代码格式化

This commit is contained in:
2026-02-19 11:46:27 +08:00
parent d310a29c03
commit f0f0763ceb
101 changed files with 8952 additions and 13203 deletions
@@ -1,13 +1,7 @@
<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>
@@ -20,47 +14,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" },
meta: { title: '', icon: 'HomeOutlined' },
},
].concat(matched);
].concat(matched)
}
breadcrumbList.value = matched;
};
breadcrumbList.value = matched
}
// 处理点击面包屑
const handleLink = () => {
return;
};
return
}
// 监听路由变化
watch(
() => route.path,
() => {
getBreadcrumb();
getBreadcrumb()
},
{ immediate: true },
);
)
</script>
@@ -1,27 +1,15 @@
<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>
@@ -31,8 +19,8 @@
</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: {
@@ -41,25 +29,25 @@ defineProps({
},
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>
+69 -100
View File
@@ -1,36 +1,14 @@
<template>
<a-modal
v-model:open="visible"
:title="$t('common.searchMenu')"
:footer="null"
:width="600"
:destroyOnClose="true"
@cancel="handleClose"
>
<a-modal v-model:open="visible" :title="$t('common.searchMenu')" :footer="null" :width="600" :destroyOnClose="true" @cancel="handleClose">
<div class="menu-search">
<a-input
v-model:value="searchKeyword"
:placeholder="$t('common.searchPlaceholder')"
size="large"
allow-clear
@input="handleSearch"
@keydown="handleKeydown"
ref="searchInputRef"
>
<a-input v-model:value="searchKeyword" :placeholder="$t('common.searchPlaceholder')" size="large" allow-clear @input="handleSearch" @keydown="handleKeydown" ref="searchInputRef">
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<div v-if="searchResults.length > 0" class="search-results">
<div
v-for="(item, index) in searchResults"
:key="item.path"
class="result-item"
:class="{ active: selectedIndex === index }"
@click="handleSelect(item)"
@mouseenter="selectedIndex = index"
>
<div v-for="(item, index) in searchResults" :key="item.path" class="result-item" :class="{ active: selectedIndex === index }" @click="handleSelect(item)" @mouseenter="selectedIndex = index">
<div class="result-icon">
<component :is="item.icon || 'MenuOutlined'" />
</div>
@@ -48,20 +26,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>
@@ -70,137 +48,128 @@
</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) ||
menu.breadcrumbs.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,10 +1,5 @@
<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>
@@ -18,25 +13,16 @@
}"
@click="handleLayoutChange(mode.value)"
>
<div
class="layout-preview"
:class="`preview-${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>
@@ -44,14 +30,7 @@
<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>
@@ -62,17 +41,11 @@
<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>
@@ -91,126 +64,108 @@
</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,57 +1,26 @@
<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>
@@ -69,9 +38,9 @@
</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: {
@@ -80,108 +49,106 @@ const props = defineProps({
},
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);
const matched = route.matched.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();
updateSelectedKeys()
},
{ immediate: true },
);
)
// 监听折叠状态
watch(
() => props.collapsed,
(val) => {
if (val) {
openKeys.value = [];
openKeys.value = []
} else {
updateSelectedKeys();
updateSelectedKeys()
}
},
);
)
onMounted(() => {
getMenuList();
});
getMenuList()
})
</script>
<style scoped lang="scss">
+111 -129
View File
@@ -24,11 +24,7 @@
</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>
@@ -77,10 +73,7 @@
<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>
@@ -99,39 +92,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,
});
})
// 顶部操作菜单状态
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,
@@ -140,231 +133,220 @@ const addTags = () => {
query: route.query,
params: route.params,
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,
);
const tagsToKeep = visitedViews.value.filter((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,
};
};
}
}
// 关闭右键菜单
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 },
);
)
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">
+77 -145
View File
@@ -1,22 +1,16 @@
<template>
<a-drawer
v-model:open="visible"
:title="$t('common.taskCenter')"
placement="right"
:width="400"
:destroyOnClose="true"
>
<a-drawer v-model:open="visible" :title="$t('common.taskCenter')" placement="right" :width="400" :destroyOnClose="true">
<div class="task-drawer">
<!-- 任务统计 -->
<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") }}
{{ $t('common.pendingTasks') }}
</div>
</div>
<div class="stat-item">
@@ -24,46 +18,27 @@
{{ completedTasks }}
</div>
<div class="stat-label">
{{ $t("common.completedTasks") }}
{{ $t('common.completedTasks') }}
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="task-actions">
<a-input
v-model:value="searchKeyword"
:placeholder="$t('common.searchTasks')"
allow-clear
@input="handleSearch"
>
<a-input v-model:value="searchKeyword" :placeholder="$t('common.searchTasks')" allow-clear @input="handleSearch">
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<div class="filter-buttons">
<a-button
:type="filterType === 'all' ? 'primary' : 'default'"
size="small"
@click="setFilter('all')"
>
{{ $t("common.all") }}
<a-button :type="filterType === 'all' ? 'primary' : 'default'" size="small" @click="setFilter('all')">
{{ $t('common.all') }}
</a-button>
<a-button
:type="filterType === 'pending' ? 'primary' : 'default'"
size="small"
@click="setFilter('pending')"
>
{{ $t("common.pending") }}
<a-button :type="filterType === 'pending' ? 'primary' : 'default'" size="small" @click="setFilter('pending')">
{{ $t('common.pending') }}
</a-button>
<a-button
:type="
filterType === 'completed' ? 'primary' : 'default'
"
size="small"
@click="setFilter('completed')"
>
{{ $t("common.completed") }}
<a-button :type="filterType === 'completed' ? 'primary' : 'default'" size="small" @click="setFilter('completed')">
{{ $t('common.completed') }}
</a-button>
</div>
</div>
@@ -71,41 +46,21 @@
<!-- 任务列表 -->
<div class="task-list">
<div v-if="filteredTasks.length > 0">
<div
v-for="task in filteredTasks"
:key="task.id"
class="task-item"
:class="{ completed: task.completed }"
>
<div v-for="task in filteredTasks" :key="task.id" class="task-item" :class="{ completed: task.completed }">
<div class="task-checkbox">
<a-checkbox
:checked="task.completed"
@change="toggleTask(task)"
/>
<a-checkbox :checked="task.completed" @change="toggleTask(task)" />
</div>
<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>
</div>
<div class="task-actions">
<a-popconfirm
:title="$t('common.confirmDelete')"
:ok-text="$t('common.confirm')"
:cancel-text="$t('common.cancel')"
@confirm="deleteTask(task.id)"
>
<a-popconfirm :title="$t('common.confirmDelete')" :ok-text="$t('common.confirm')" :cancel-text="$t('common.cancel')" @confirm="deleteTask(task.id)">
<DeleteOutlined class="action-icon" />
</a-popconfirm>
</div>
@@ -118,40 +73,25 @@
<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>
<!-- 添加任务弹窗 -->
<a-modal
v-model:open="addTaskVisible"
:title="$t('common.addTask')"
:ok-text="$t('common.confirm')"
:cancel-text="$t('common.cancel')"
@ok="confirmAddTask"
>
<a-modal v-model:open="addTaskVisible" :title="$t('common.addTask')" :ok-text="$t('common.confirm')" :cancel-text="$t('common.cancel')" @ok="confirmAddTask">
<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>
@@ -160,101 +100,93 @@
</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({
@@ -262,30 +194,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">
+189 -287
View File
@@ -8,12 +8,7 @@
</a-tooltip>
<!-- 消息通知 -->
<a-dropdown
v-model:open="messageVisible"
:trigger="['click']"
placement="bottomRight"
@openChange="handleMessageDropdownOpen"
>
<a-dropdown v-model:open="messageVisible" :trigger="['click']" placement="bottomRight" @openChange="handleMessageDropdownOpen">
<a-badge :count="messageCount" :offset="[-5, 5]">
<a-button type="text" class="action-btn">
<BellOutlined />
@@ -23,22 +18,13 @@
<a-card class="dropdown-card" :bordered="false">
<template #title>
<div class="message-header">
<span>{{ $t("common.messages") }}</span>
<span>{{ $t('common.messages') }}</span>
<a-space size="small">
<a-button
v-if="messageCount > 0"
type="link"
size="small"
@click="markAllAsRead"
>
{{ $t("common.markAllAsRead") }}
<a-button v-if="messageCount > 0" type="link" size="small" @click="markAllAsRead">
{{ $t('common.markAllAsRead') }}
</a-button>
<a-button
type="link"
size="small"
@click="clearMessages"
>
{{ $t("common.clearAll") }}
<a-button type="link" size="small" @click="clearMessages">
{{ $t('common.clearAll') }}
</a-button>
</a-space>
</div>
@@ -46,74 +32,35 @@
<!-- 消息类型筛选 -->
<div class="message-tabs">
<a-tabs
v-model:activeKey="currentMessageType"
size="small"
@change="changeMessageType"
>
<a-tabs v-model:activeKey="currentMessageType" size="small" @change="changeMessageType">
<a-tab-pane key="all" :tab="$t('common.all')" />
<a-tab-pane
key="notification"
:tab="$t('common.notification')"
/>
<a-tab-pane key="notification" :tab="$t('common.notification')" />
<a-tab-pane key="task" :tab="$t('common.task')" />
<a-tab-pane
key="warning"
:tab="$t('common.warning')"
/>
<a-tab-pane key="warning" :tab="$t('common.warning')" />
</a-tabs>
</div>
<div class="message-list">
<div
v-for="msg in messages"
:key="msg.id"
class="message-item"
:class="{ unread: !msg.is_read }"
@click="handleMessageRead(msg)"
>
<div v-for="msg in messages" :key="msg.id" class="message-item" :class="{ unread: !msg.is_read }" @click="handleMessageRead(msg)">
<div class="message-content">
<div class="message-title">{{ msg.title }}</div>
<div class="message-content-text">
{{ msg.content }}
</div>
<div class="message-time">
{{
notificationStore.formatNotificationTime(
msg.created_at,
)
}}
{{ notificationStore.formatNotificationTime(msg.created_at) }}
</div>
</div>
<a-button
type="text"
size="small"
danger
class="delete-btn"
@click.stop="handleDeleteMessage(msg.id)"
>
<a-button type="text" size="small" danger class="delete-btn" @click.stop="handleDeleteMessage(msg.id)">
<DeleteOutlined />
</a-button>
</div>
<a-empty
v-if="messages.length === 0"
:description="$t('common.noMessages')"
/>
<a-empty v-if="messages.length === 0" :description="$t('common.noMessages')" />
</div>
<!-- 分页 -->
<div
v-if="messagesTotal > messagesPageSize"
class="message-pagination"
>
<a-pagination
v-model:current="messagesPage"
v-model:pageSize="messagesPageSize"
:total="messagesTotal"
size="small"
:show-size-changer="false"
@change="handleMessagePageChange"
/>
<div v-if="messagesTotal > messagesPageSize" class="message-pagination">
<a-pagination v-model:current="messagesPage" v-model:pageSize="messagesPageSize" :total="messagesTotal" size="small" :show-size-changer="false" @change="handleMessagePageChange" />
</div>
</a-card>
</template>
@@ -122,11 +69,7 @@
<!-- 任务列表 -->
<a-tooltip :title="$t('common.taskCenter')">
<a-badge :count="taskCount" :offset="[-5, 5]">
<a-button
type="text"
@click="taskVisible = true"
class="action-btn"
>
<a-button type="text" @click="taskVisible = true" class="action-btn">
<CheckSquareOutlined />
</a-button>
</a-badge>
@@ -139,11 +82,7 @@
</a-button>
<template #overlay>
<a-menu @click="handleLanguageChange">
<a-menu-item
v-for="locale in i18nStore.availableLocales"
:key="locale.value"
:disabled="i18nStore.currentLocale === locale.value"
>
<a-menu-item v-for="locale in i18nStore.availableLocales" :key="locale.value" :disabled="i18nStore.currentLocale === locale.value">
<span>{{ locale.label }}</span>
</a-menu-item>
</a-menu>
@@ -162,34 +101,29 @@
<a-dropdown :trigger="['click']">
<div class="user-info">
<a-avatar :size="32" :src="userStore.user?.avatar || ''">
{{
userStore.user?.username?.charAt(0)?.toUpperCase() ||
"U"
}}
{{ userStore.user?.username?.charAt(0)?.toUpperCase() || 'U' }}
</a-avatar>
<span class="username">{{
userStore.user?.username || "Admin"
}}</span>
<span class="username">{{ userStore.user?.username || 'Admin' }}</span>
<DownOutlined />
</div>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="profile">
<UserOutlined />
<span>{{ $t("common.personalCenter") }}</span>
<span>{{ $t('common.personalCenter') }}</span>
</a-menu-item>
<a-menu-item key="settings">
<SettingOutlined />
<span>{{ $t("common.systemSettings") }}</span>
<span>{{ $t('common.systemSettings') }}</span>
</a-menu-item>
<a-menu-item key="clearCache">
<DeleteOutlined />
<span>{{ $t("common.clearCache") }}</span>
<span>{{ $t('common.clearCache') }}</span>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<LogoutOutlined />
<span>{{ $t("common.logout") }}</span>
<span>{{ $t('common.logout') }}</span>
</a-menu-item>
</a-menu>
</template>
@@ -204,112 +138,98 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { message, Modal } from "ant-design-vue";
import { useUserStore } from "@/stores/modules/user";
import { useI18nStore } from "@/stores/modules/i18n";
import { useNotificationStore } from "@/stores/modules/notification";
import {
DownOutlined,
UserOutlined,
LogoutOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
BellOutlined,
CheckSquareOutlined,
GlobalOutlined,
SearchOutlined,
SettingOutlined,
DeleteOutlined,
} from "@ant-design/icons-vue";
import { useI18n } from "vue-i18n";
import search from "./search.vue";
import task from "./task.vue";
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { message, Modal } from 'ant-design-vue'
import { useUserStore } from '@/stores/modules/user'
import { useI18nStore } from '@/stores/modules/i18n'
import { useNotificationStore } from '@/stores/modules/notification'
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
import search from './search.vue'
import task from './task.vue'
// 定义组件名称(多词命名)
defineOptions({
name: "UserBar",
});
name: 'UserBar',
})
const { t } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const i18nStore = useI18nStore();
const notificationStore = useNotificationStore();
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const i18nStore = useI18nStore()
const notificationStore = useNotificationStore()
const isFullscreen = ref(false);
const searchVisible = ref(false);
const taskVisible = ref(false);
const messageVisible = ref(false);
const currentMessageType = ref("all");
const messagesPage = ref(1);
const messagesPageSize = ref(10);
const notificationsList = ref([]);
const isFullscreen = ref(false)
const searchVisible = ref(false)
const taskVisible = ref(false)
const messageVisible = ref(false)
const currentMessageType = ref('all')
const messagesPage = ref(1)
const messagesPageSize = ref(10)
const notificationsList = ref([])
// 未读消息数量
const messageCount = computed(() => notificationStore.unreadCount);
const messageCount = computed(() => notificationStore.unreadCount)
// 消息总数(用于分页)
const messagesTotal = computed(() => notificationStore.total);
const messagesTotal = computed(() => notificationStore.total)
// 从 store 获取消息数据
const messages = computed(() => notificationsList.value);
const messages = computed(() => notificationsList.value)
// 任务数据
const tasks = ref([
{
id: 1,
title: "完成用户审核",
priority: "high",
title: '完成用户审核',
priority: 'high',
completed: false,
time: "今天",
time: '今天',
},
{
id: 2,
title: "更新系统文档",
priority: "medium",
title: '更新系统文档',
priority: 'medium',
completed: false,
time: "明天",
time: '明天',
},
{
id: 3,
title: "优化数据库查询",
priority: "low",
title: '优化数据库查询',
priority: 'low',
completed: true,
time: "昨天",
time: '昨天',
},
]);
])
const taskCount = computed(
() => tasks.value.filter((t) => !t.completed).length,
);
const taskCount = computed(() => tasks.value.filter((t) => !t.completed).length)
// 切换全屏
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
isFullscreen.value = true;
document.documentElement.requestFullscreen()
isFullscreen.value = true
} else {
document.exitFullscreen();
isFullscreen.value = false;
document.exitFullscreen()
isFullscreen.value = false
}
};
}
// 监听全屏变化
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement;
};
isFullscreen.value = !!document.fullscreenElement
}
// 加载未读通知
const loadNotifications = async () => {
try {
await notificationStore.fetchUnreadCount();
await loadUnreadNotifications();
await notificationStore.fetchUnreadCount()
await loadUnreadNotifications()
} catch (error) {
console.error("加载通知失败:", error);
console.error('加载通知失败:', error)
}
};
}
// 加载未读通知列表
const loadUnreadNotifications = async () => {
@@ -317,16 +237,13 @@ const loadUnreadNotifications = async () => {
const res = await notificationStore.fetchUnreadNotifications({
page: messagesPage.value,
page_size: messagesPageSize.value,
type:
currentMessageType.value === "all"
? null
: currentMessageType.value,
});
notificationsList.value = res.list || [];
type: currentMessageType.value === 'all' ? null : currentMessageType.value,
})
notificationsList.value = res.list || []
} catch (error) {
console.error("加载未读通知列表失败:", error);
console.error('加载未读通知列表失败:', error)
}
};
}
// 加载所有通知
const loadAllNotifications = async () => {
@@ -334,231 +251,224 @@ const loadAllNotifications = async () => {
await notificationStore.fetchNotifications({
page: messagesPage.value,
page_size: messagesPageSize.value,
type:
currentMessageType.value === "all"
? null
: currentMessageType.value,
});
notificationsList.value = notificationStore.notifications;
type: currentMessageType.value === 'all' ? null : currentMessageType.value,
})
notificationsList.value = notificationStore.notifications
} catch (error) {
console.error("加载通知列表失败:", error);
console.error('加载通知列表失败:', error)
}
};
}
onMounted(() => {
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener('fullscreenchange', handleFullscreenChange)
// 加载通知数据
loadNotifications();
loadNotifications()
// 注意:WebSocket 已在 App.vue 中统一初始化,这里不需要重复调用
// 只需要处理消息即可,消息处理已经在 useWebSocket 中注册
});
})
onUnmounted(() => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
});
document.removeEventListener('fullscreenchange', handleFullscreenChange)
})
// 显示搜索功能
const showSearch = () => {
searchVisible.value = true;
};
searchVisible.value = true
}
// 清除消息
const clearMessages = async () => {
Modal.confirm({
title: t("common.confirmClear"),
content: t("common.confirmClearMessages"),
okText: t("common.confirm"),
cancelText: t("common.cancel"),
title: t('common.confirmClear'),
content: t('common.confirmClearMessages'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
await notificationStore.clearReadNotifications();
message.success(t("common.cleared"));
notificationsList.value = [];
await notificationStore.clearReadNotifications()
message.success(t('common.cleared'))
notificationsList.value = []
} catch (error) {
console.error("清空消息失败:", error);
console.error('清空消息失败:', error)
}
},
});
};
})
}
// 标记消息为已读
const handleMessageRead = async (msg) => {
if (!msg.is_read) {
try {
await notificationStore.markAsRead(msg.id);
await notificationStore.markAsRead(msg.id)
// 更新本地状态
const notification = notificationsList.value.find(
(n) => n.id === msg.id,
);
const notification = notificationsList.value.find((n) => n.id === msg.id)
if (notification) {
notification.is_read = true;
notification.read_at = new Date().toISOString();
notification.is_read = true
notification.read_at = new Date().toISOString()
}
} catch (error) {
console.error("标记已读失败:", error);
console.error('标记已读失败:', error)
}
}
};
}
// 标记所有消息为已读
const markAllAsRead = async () => {
Modal.confirm({
title: "确认全部已读",
content: "确定要将所有消息标记为已读吗?",
okText: "确定",
cancelText: "取消",
title: '确认全部已读',
content: '确定要将所有消息标记为已读吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await notificationStore.markAllAsRead();
message.success(t("common.markedAsRead"));
await notificationStore.markAllAsRead()
message.success(t('common.markedAsRead'))
// 更新本地状态
notificationsList.value.forEach((n) => {
n.is_read = true;
n.read_at = n.read_at || new Date().toISOString();
});
n.is_read = true
n.read_at = n.read_at || new Date().toISOString()
})
} catch (error) {
console.error("标记全部已读失败:", error);
console.error('标记全部已读失败:', error)
}
},
});
};
})
}
// 删除消息
const handleDeleteMessage = async (msgId) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这条消息吗?",
okText: "确定",
cancelText: "取消",
title: '确认删除',
content: '确定要删除这条消息吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await notificationStore.deleteNotification(msgId);
message.success("删除成功");
await notificationStore.deleteNotification(msgId)
message.success('删除成功')
// 更新本地状态
const index = notificationsList.value.findIndex(
(n) => n.id === msgId,
);
const index = notificationsList.value.findIndex((n) => n.id === msgId)
if (index !== -1) {
notificationsList.value.splice(index, 1);
notificationsList.value.splice(index, 1)
}
} catch (error) {
console.error("删除消息失败:", error);
console.error('删除消息失败:', error)
}
},
});
};
})
}
// 切换消息类型
const changeMessageType = async (type) => {
currentMessageType.value = type;
messagesPage.value = 1;
if (type === "all") {
await loadAllNotifications();
currentMessageType.value = type
messagesPage.value = 1
if (type === 'all') {
await loadAllNotifications()
} else {
await loadUnreadNotifications();
await loadUnreadNotifications()
}
};
}
// 分页变化
const handleMessagePageChange = async (page) => {
messagesPage.value = page;
if (currentMessageType.value === "all") {
await loadAllNotifications();
messagesPage.value = page
if (currentMessageType.value === 'all') {
await loadAllNotifications()
} else {
await loadUnreadNotifications();
await loadUnreadNotifications()
}
};
}
// 下拉框打开时加载数据
const handleMessageDropdownOpen = async (open) => {
if (open) {
await loadNotifications();
await loadNotifications()
}
};
}
// 显示任务抽屉
const showTasks = () => {
taskVisible.value = true;
};
taskVisible.value = true
}
// 切换语言
const handleLanguageChange = ({ key }) => {
i18nStore.setLocale(key);
message.success(t("common.languageChanged"));
};
i18nStore.setLocale(key)
message.success(t('common.languageChanged'))
}
// 处理菜单点击
const handleMenuClick = ({ key }) => {
switch (key) {
case "profile":
router.push("/ucenter");
break;
case "settings":
router.push("/system/setting");
break;
case "clearCache":
handleClearCache();
break;
case "logout":
handleLogout();
break;
case 'profile':
router.push('/ucenter')
break
case 'settings':
router.push('/system/setting')
break
case 'clearCache':
handleClearCache()
break
case 'logout':
handleLogout()
break
}
};
}
// 清除缓存
const handleClearCache = () => {
Modal.confirm({
title: t("common.confirmClearCache"),
content: t("common.clearCacheConfirm"),
okText: t("common.confirm"),
cancelText: t("common.cancel"),
title: t('common.confirmClearCache'),
content: t('common.clearCacheConfirm'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
try {
// 清除 localStorage
localStorage.clear();
localStorage.clear()
// 清除 sessionStorage
sessionStorage.clear();
sessionStorage.clear()
// 清除所有缓存
if ("caches" in window) {
if ('caches' in window) {
caches.keys().then((names) => {
names.forEach((name) => {
caches.delete(name);
});
});
caches.delete(name)
})
})
}
message.success(t("common.cacheCleared"));
message.success(t('common.cacheCleared'))
// 延迟刷新页面以应用缓存清除
setTimeout(() => {
window.location.reload();
}, 1000);
window.location.reload()
}, 1000)
} catch (error) {
message.error(t("common.clearCacheFailed"));
console.error("清除缓存失败:", error);
message.error(t('common.clearCacheFailed'))
console.error('清除缓存失败:', error)
}
},
});
};
})
}
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: t("common.confirmLogout"),
content: t("common.logoutConfirm"),
okText: t("common.confirm"),
cancelText: t("common.cancel"),
title: t('common.confirmLogout'),
content: t('common.logoutConfirm'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
await userStore.logout();
message.success(t("common.logoutSuccess"));
router.push("/login");
await userStore.logout()
message.success(t('common.logoutSuccess'))
router.push('/login')
} catch {
message.error(t("common.logoutFailed"));
message.error(t('common.logoutFailed'))
}
},
});
};
})
}
</script>
<style scoped lang="scss">
@@ -766,21 +676,13 @@ const handleLogout = () => {
}
&.unread {
background: linear-gradient(
135deg,
rgba(24, 144, 255, 0.05) 0%,
rgba(24, 144, 255, 0.02) 100%
);
background: linear-gradient(135deg, rgba(24, 144, 255, 0.05) 0%, rgba(24, 144, 255, 0.02) 100%);
border: 1px solid rgba(24, 144, 255, 0.15);
border-left: 4px solid #1890ff;
padding-left: 14px;
&:hover {
background: linear-gradient(
135deg,
rgba(24, 144, 255, 0.08) 0%,
rgba(24, 144, 255, 0.04) 100%
);
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(24, 144, 255, 0.04) 100%);
border-color: rgba(24, 144, 255, 0.25);
}
+91 -160
View File
@@ -5,11 +5,7 @@
<!-- 第一个侧边栏显示一级菜单 -->
<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
@@ -27,38 +23,13 @@
</a-layout-sider>
<!-- 第二个侧边栏显示选中的父菜单的子菜单 -->
<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"
>
<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">
<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>
@@ -82,32 +53,12 @@
<!-- 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>
@@ -134,23 +85,11 @@
<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 />
@@ -178,143 +117,139 @@
</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) => {
@@ -322,62 +257,58 @@ 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">
+13 -31
View File
@@ -9,9 +9,7 @@
<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">
@@ -32,21 +30,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">
@@ -56,11 +54,7 @@ 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;
@@ -128,11 +122,7 @@ 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;
@@ -180,21 +170,13 @@ 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);
}
+19 -38
View File
@@ -8,45 +8,38 @@
<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,
@@ -54,21 +47,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">
@@ -78,11 +71,7 @@ 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;
@@ -182,22 +171,14 @@ 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);
}