From ded2c4cf8b84a7d360509c95acc3c1537631bd74 Mon Sep 17 00:00:00 2001 From: zhangtao <9480807882@qq.com> Date: Sat, 15 Nov 2025 03:01:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor(layout):=20=E4=BC=98=E5=8C=96=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E5=B8=83=E5=B1=80=E7=BB=93=E6=9E=84=E5=92=8C=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(editor): 调整默认高度并移除冗余属性 refactor(tagsview): 重构标签视图逻辑和交互优化 style(myapp): 改进应用卡片布局和交互体验 --- frontend/src/components/WangEditor/index.vue | 2 +- .../layouts/components/Menu/MixTopMenu.vue | 66 +-- .../src/layouts/components/TagsView/index.vue | 105 ++-- frontend/src/layouts/views/MixLayout.vue | 18 +- .../views/module_application/myapp/index.vue | 457 +++++++++++------- .../src/views/module_system/notice/index.vue | 3 +- 6 files changed, 384 insertions(+), 267 deletions(-) diff --git a/frontend/src/components/WangEditor/index.vue b/frontend/src/components/WangEditor/index.vue index b28b1260..4a588783 100644 --- a/frontend/src/components/WangEditor/index.vue +++ b/frontend/src/components/WangEditor/index.vue @@ -59,7 +59,7 @@ const FileAPI = { defineProps({ height: { type: String, - default: "200px", + default: "300px", }, }); // 双向绑定 diff --git a/frontend/src/layouts/components/Menu/MixTopMenu.vue b/frontend/src/layouts/components/Menu/MixTopMenu.vue index 2b0d11b5..c32cec94 100644 --- a/frontend/src/layouts/components/Menu/MixTopMenu.vue +++ b/frontend/src/layouts/components/Menu/MixTopMenu.vue @@ -58,53 +58,31 @@ const topMenus = ref([]); // 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题 const processedTopMenus = computed(() => { - return ( - topMenus.value - .map((route) => { - // 如果路由本身设置了hidden=true,直接返回null - if (route.meta?.hidden === true) { - return null; - } + return topMenus.value.map((route) => { + // 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由 + if (route.meta?.alwaysShow || !route.children || route.children.length === 0) { + return route; + } - // 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由 - if (route.meta?.alwaysShow || !route.children || route.children.length === 0) { - return route; - } + // 过滤出非隐藏的子菜单 + const visibleChildren = route.children.filter((child) => !child.meta?.hidden); - // 过滤出非隐藏的子菜单 - const visibleChildren = route.children.filter((child) => !child.meta?.hidden); + // 如果只有一个非隐藏的子菜单,显示子菜单的信息 + if (visibleChildren.length === 1) { + const onlyChild = visibleChildren[0]; + return { + ...route, + meta: { + ...route.meta, + title: onlyChild.meta?.title || route.meta?.title, + icon: onlyChild.meta?.icon || route.meta?.icon, + }, + }; + } - // 如果没有可见子菜单,返回null - if (visibleChildren.length === 0) { - return null; - } - - // 如果只有一个非隐藏的子菜单,显示子菜单的信息 - if (visibleChildren.length === 1) { - const onlyChild = visibleChildren[0]; - // 检查子菜单是否应该显示在顶部菜单 - if (onlyChild.meta?.hidden !== true) { - return { - ...route, - meta: { - ...route.meta, - title: onlyChild.meta?.title || route.meta?.title, - icon: onlyChild.meta?.icon || route.meta?.icon, - }, - // 使用子菜单的path确保路由匹配正确 - path: onlyChild.path, - }; - } - // 如果子菜单应该隐藏,则不显示该顶级菜单 - return null; - } - - // 其他情况返回原路由 - return route; - }) - // 过滤掉null值 - .filter((route) => route !== null) - ); + // 其他情况返回原路由 + return route; + }); }); /** diff --git a/frontend/src/layouts/components/TagsView/index.vue b/frontend/src/layouts/components/TagsView/index.vue index e11ab8e4..b380ab33 100644 --- a/frontend/src/layouts/components/TagsView/index.vue +++ b/frontend/src/layouts/components/TagsView/index.vue @@ -19,7 +19,6 @@ > - + {{ t("navbar.closeLeft") }} - + @@ -65,8 +58,8 @@ @@ -92,7 +85,6 @@ - {{ translateRouteTitle(tag.title) }} - + {{ t("navbar.closeLeft") }} - + @@ -205,11 +203,6 @@ const displayedViews = computed(() => { // 当前选中的标签 const selectedTag = ref(null); -// 基于当前激活路由的回退标签(当未选中右键标签时) -const activeTag = computed(() => { - return selectedTag.value || routePathMap.value.get(route.path) || null; -}); - // 滚动条引用 const scrollbarRef = ref(); @@ -217,28 +210,41 @@ const scrollbarRef = ref(); const tagSwitchSource = ref<"menu" | "tab" | null>(null); // 路由映射缓存,提升查找性能 -const routePathMap = computed(() => { - const map = new Map(); +const routePathMap = new Map(); + +// 更新路由映射缓存 +const updateRoutePathMap = () => { + routePathMap.clear(); visitedViews.value.forEach((tag) => { - map.set(tag.path, tag); + routePathMap.set(tag.path, tag); }); - return map; -}); +}; // 判断是否为第一个标签 -const isFirstView = computed(() => { - if (!activeTag.value) return false; - return ( - activeTag.value.path === "/dashboard" || - activeTag.value.fullPath === visitedViews.value[1]?.fullPath - ); -}); +const isFirstView = (tag?: TagView) => { + if (tag) { + // 传入特定标签时使用传入的标签 + return tag.path === "/" || tag.fullPath === visitedViews.value[1]?.fullPath; + } else { + // 未传入标签时使用当前激活标签(向后兼容) + const currentTag = routePathMap.get(route.path); + if (!currentTag) return false; + return currentTag.path === "/" || currentTag.fullPath === visitedViews.value[1]?.fullPath; + } +}; // 判断是否为最后一个标签 -const isLastView = computed(() => { - if (!activeTag.value) return false; - return activeTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; -}); +const isLastView = (tag?: TagView) => { + if (tag) { + // 传入特定标签时使用传入的标签 + return tag.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; + } else { + // 未传入标签时使用当前激活标签(向后兼容) + const currentTag = routePathMap.get(route.path); + if (!currentTag) return false; + return currentTag.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; + } +}; /** * 递归提取固定标签 @@ -340,7 +346,7 @@ const addCurrentTag = () => { */ const updateCurrentTag = () => { nextTick(() => { - const currentTag = routePathMap.value.get(route.path); + const currentTag = routePathMap.get(route.path); if (currentTag && currentTag.fullPath !== route.fullPath) { tagsViewStore.updateVisitedView({ @@ -422,7 +428,7 @@ const refreshSelectedTag = (tag: TagView | null) => { */ const closeSelectedTag = (tag: TagView | null) => { // 如果传入了具体的标签,使用传入的标签;否则使用当前路由对应的标签 - const targetTag = tag || routePathMap.value.get(route.path); + const targetTag = tag || routePathMap.get(route.path); if (!targetTag) return; tagsViewStore.delView(targetTag).then((result: any) => { @@ -439,8 +445,8 @@ const closeSelectedTag = (tag: TagView | null) => { /** * 关闭左侧标签 */ -const closeLeftTags = () => { - const targetTag = selectedTag.value || routePathMap.value.get(route.path); +const closeLeftTags = (tag?: TagView) => { + const targetTag = tag || selectedTag.value || routePathMap.get(route.path); if (!targetTag) return; tagsViewStore.delLeftViews(targetTag).then((result: any) => { @@ -459,8 +465,8 @@ const closeLeftTags = () => { /** * 关闭右侧标签 */ -const closeRightTags = () => { - const targetTag = selectedTag.value || routePathMap.value.get(route.path); +const closeRightTags = (tag?: TagView) => { + const targetTag = tag || selectedTag.value || routePathMap.get(route.path); if (!targetTag) return; tagsViewStore.delRightViews(targetTag).then((result: any) => { @@ -479,8 +485,8 @@ const closeRightTags = () => { /** * 关闭其他标签 */ -const closeOtherTags = () => { - const targetTag = selectedTag.value || routePathMap.value.get(route.path); +const closeOtherTags = (tag?: TagView) => { + const targetTag = tag || selectedTag.value || routePathMap.get(route.path); if (!targetTag) return; router.push(targetTag); @@ -496,7 +502,7 @@ const closeOtherTags = () => { /** * 关闭所有标签 */ -const closeAllTags = (tag: TagView | null) => { +const closeAllTags = (tag?: TagView) => { tagsViewStore.delAllViews().then((result: any) => { tagsViewStore.toLastView(result.visitedViews, tag || undefined); // 关闭所有标签后重置滚动状态 @@ -511,7 +517,7 @@ const closeAllTags = (tag: TagView | null) => { */ const handleAction = async (action: string) => { // 总是使用当前路由对应的标签 - const currentTag = routePathMap.value.get(route.path); + const currentTag = routePathMap.get(route.path); if (!currentTag) return; switch (action) { @@ -557,7 +563,7 @@ const handleAction = async (action: string) => { const onContextMenuVisibleChange = (visible: boolean, tag?: TagView) => { if (visible) { // 设置当前右键点击的标签 - selectedTag.value = tag || routePathMap.value.get(route.path) || null; + selectedTag.value = tag || routePathMap.get(route.path) || null; } else { // 关闭菜单时清空选择 selectedTag.value = null; @@ -713,6 +719,7 @@ watch( } addCurrentTag(); updateCurrentTag(); + updateRoutePathMap(); }, { immediate: true } ); @@ -724,6 +731,9 @@ let resizeObserver: ResizeObserver | null = null; watch( () => visitedViews.value.length, () => { + // 更新路由映射缓存 + updateRoutePathMap(); + // 只有在通过菜单添加新标签时才滚动 if (tagSwitchSource.value === "menu") { nextTick(() => { @@ -747,6 +757,9 @@ watch( onMounted(() => { initAffixTags(); + // 初始化路由映射缓存 + updateRoutePathMap(); + // 监听容器大小变化 const tagsContainer = document.querySelector(".tags-container"); if (tagsContainer && window.ResizeObserver) { diff --git a/frontend/src/layouts/views/MixLayout.vue b/frontend/src/layouts/views/MixLayout.vue index a52ea139..a4eda3ba 100644 --- a/frontend/src/layouts/views/MixLayout.vue +++ b/frontend/src/layouts/views/MixLayout.vue @@ -3,12 +3,13 @@
+ + +
- -
@@ -107,11 +108,10 @@ function resolvePath(routePath: string) { return routePath; } - // if (routePath.startsWith("/")) { - // return activeTopMenuPath.value + routePath; - // } - // return `${activeTopMenuPath.value}/${routePath}`; - return routePath; + if (routePath.startsWith("/")) { + return routePath; + } + return `${activeTopMenuPath.value}/${routePath}`; } watch( diff --git a/frontend/src/views/module_application/myapp/index.vue b/frontend/src/views/module_application/myapp/index.vue index 08d78b32..8850aaf7 100644 --- a/frontend/src/views/module_application/myapp/index.vue +++ b/frontend/src/views/module_application/myapp/index.vue @@ -69,7 +69,12 @@ - -
- - + +
+
+
+ + + -
-
-
- - {{ app.creator?.name || "未知" }} + + + + + +
- -
- - 外部打开 - - - 内部打开 - -
- +
-
@@ -273,6 +268,7 @@ const dialogType = ref<"create" | "edit">("create"); const currentApp = ref(null); const isExpand = ref(false); const isExpandable = ref(true); +const hoveredCard = ref(null); // 分页查询参数 const queryFormData = reactive({ @@ -400,17 +396,14 @@ async function handleAppAction(command: string, app: ApplicationInfo) { } } -// 外部打开应用 -function openAppExternal(url: string | undefined) { - if (url) { - window.open(url, "_blank"); - } -} - // 内部打开应用 function openAppInternal(app: ApplicationInfo) { - if (!app.status) { - ElMessage.warning("应用已停用,无法打开"); + if (!app.status || !app.id) { + if (!app.status) { + ElMessage.warning("应用已停用,无法打开"); + } else { + ElMessage.warning("应用ID不存在,无法打开"); + } return; } @@ -428,7 +421,7 @@ function openAppInternal(app: ApplicationInfo) { router .push({ path: appPath, - query: { url: app.access_url, appId: app.id?.toString() || "", appName: appTitle }, + query: { url: app.access_url, appId: app.id.toString(), appName: appTitle }, }) .then(() => { // 导航完成后,手动添加或更新标签视图 @@ -450,11 +443,11 @@ function openAppInternal(app: ApplicationInfo) { path: appPath, fullPath: appPath + - `?url=${encodeURIComponent(app.access_url || "")}&appId=${app.id || ""}&appName=${encodeURIComponent(appTitle)}`, + `?url=${encodeURIComponent(app.access_url || "")}&appId=${app.id}&appName=${encodeURIComponent(appTitle)}`, icon: "Monitor", affix: false, keepAlive: false, - query: { url: app.access_url, appId: app.id?.toString() || "", appName: appTitle }, + query: { url: app.access_url, appId: app?.id?.toString(), appName: appTitle }, }); } }); @@ -524,124 +517,258 @@ onMounted(() => { justify-content: space-between; } -.app-grid { +// 网格容器 +.app-grid-container { + flex: 1; + padding: 2px 0; +} + +.grid-wrapper { display: grid; - flex: 1; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; + padding: 0 2px; + + @media (max-width: 768px) { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 14px; + } + + @media (max-width: 480px) { + grid-template-columns: 1fr; + gap: 12px; + } } -.app-card-el { +// 卡片项 +.app-grid-item { + height: fit-content; +} + +// 卡片样式 +.app-card { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + cursor: pointer; + background: linear-gradient(145deg, var(--el-bg-color) 0%, var(--el-bg-color-page) 100%); border: 1px solid var(--el-border-color-lighter); - border-radius: 6px; - transition: all 0.3s ease; -} - -.app-card-header-el { - display: flex; - align-items: center; - justify-content: space-between; + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + border-color: var(--el-color-primary); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); + } + + &:active { + transform: translateY(0); + } + + &.card-disabled { + cursor: not-allowed; + opacity: 0.6; + + &:hover { + border-color: var(--el-border-color-lighter); + box-shadow: none; + transform: none; + } + } + + :deep(.el-card__header) { + padding: 16px 18px 14px; + background: linear-gradient( + 90deg, + rgba(var(--el-color-primary-rgb), 0.02) 0%, + transparent 100% + ); + border-bottom: 1px solid var(--el-border-color-lighter); + } + + :deep(.el-card__body) { + display: flex; + flex: 1; + flex-direction: column; + padding: 18px; + } + + :deep(.el-card__footer) { + padding: 12px 18px 16px; + background: rgba(var(--el-fill-color-light-rgb), 0.3); + border-top: 1px solid var(--el-border-color-lighter); + } } +// 头部信息 .app-info-header { + position: relative; display: flex; - flex: 1; gap: 12px; - align-items: center; + align-items: flex-start; min-width: 0; } -.app-name-el { - margin: 0 0 4px 0; +.app-avatar { + flex-shrink: 0; + background: var(--el-fill-color-light); + border: 2px solid var(--el-border-color-lighter); + transition: all 0.3s ease; + + .app-card:hover & { + background: rgba(var(--el-color-primary-rgb), 0.1); + border-color: var(--el-color-primary); + } +} + +.app-title-wrap { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.app-name { + margin: 0; overflow: hidden; text-overflow: ellipsis; font-size: 16px; font-weight: 600; - line-height: 1.3; + line-height: 1.4; color: var(--el-text-color-primary); white-space: nowrap; } -.app-card-content { - margin: 12px 0; +.status-tag { + align-self: flex-start; + padding: 2px 10px; + font-size: 12px; + font-weight: 500; + border-radius: 12px; +} + +// 悬停操作按钮 +.card-actions { + position: absolute; + top: 50%; + right: -10px; + z-index: 10; + padding: 8px; + background: var(--el-bg-color); + border: 1px solid var(--el-border-color-lighter); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + transform: translateY(-50%); + + .el-button { + padding: 4px 8px; + margin: 0 2px; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } +} + +// 内容区域 +.app-content { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; } .app-description { display: -webkit-box; + min-height: 2.8em; + margin: 0; overflow: hidden; + text-overflow: ellipsis; -webkit-line-clamp: 2; line-clamp: 2; font-size: 14px; + line-height: 1.6; color: var(--el-text-color-regular); -webkit-box-orient: vertical; } -.app-meta-info { +// 底部信息 +.card-footer { display: flex; - flex-direction: column; - gap: 8px; - font-size: 13px; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 4px; +} + +.footer-item { + display: flex; + flex-shrink: 0; + gap: 6px; + align-items: center; + min-width: 0; +} + +.footer-icon { color: var(--el-text-color-secondary); } -.app-meta-row { - display: flex; - gap: 12px; - align-items: center; - justify-content: space-between; -} - -.meta-item { - display: flex; - gap: 6px; - align-items: center; - overflow: hidden; - white-space: nowrap; -} - -.meta-item.left { - flex: 1; - justify-content: flex-start; - min-width: 0; -} - -.meta-item.right { - flex: 1; - justify-content: flex-end; - min-width: 0; - text-align: right; -} - -.meta-icon { - flex-shrink: 0; - color: var(--el-text-color-placeholder); -} - -.meta-item span { +.footer-text { overflow: hidden; text-overflow: ellipsis; + font-size: 13px; + color: var(--el-text-color-secondary); white-space: nowrap; } -.app-card-actions { - display: flex; - gap: 8px; - margin-top: 12px; +// 响应式调整 +@media (max-width: 768px) { + .app-name { + font-size: 15px; + } - .action-btn { - flex: 1; - padding: 4px 8px; - font-size: 12px; + .app-description { + font-size: 13px; + } + + .card-actions { + position: static; + align-self: flex-end; + margin-top: 8px; + transform: none; + + .el-button { + padding: 3px 6px; + font-size: 12px; + } } } -.pagination-container { - display: flex; - justify-content: flex-end; -} +@media (max-width: 480px) { + .app-card { + :deep(.el-card__header) { + padding: 14px 16px; + } -.dialog-footer { - display: flex; - gap: 12px; - justify-content: flex-end; + :deep(.el-card__body) { + padding: 16px; + } + + :deep(.el-card__footer) { + padding: 10px 16px 12px; + } + } + + .app-name { + font-size: 14px; + } } diff --git a/frontend/src/views/module_system/notice/index.vue b/frontend/src/views/module_system/notice/index.vue index 42ac0688..c55aaaf1 100644 --- a/frontend/src/views/module_system/notice/index.vue +++ b/frontend/src/views/module_system/notice/index.vue @@ -426,8 +426,7 @@ - - +