前端代码格式化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user