mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-04 17:14:21 +00:00
feat(navbar): enhance NavbarActions and TagsView with improved hover effects and quick link functionality
- Updated NavbarActions.vue to refine hover styles for icons and buttons, ensuring better visual feedback. - Enhanced TagsView.vue by integrating a bookmark button for quick links, allowing users to easily add or remove favorites. - Introduced a mechanism to synchronize quick link changes with local storage for immediate UI updates. - Improved overall styling for better user experience and accessibility.
This commit is contained in:
@@ -314,12 +314,31 @@ function logout() {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
// Element Plus 图标(如 el-icon)样式
|
||||
:deep(.el-icon) {
|
||||
color: var(--el-text-color-regular);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
background: transparent;
|
||||
|
||||
:deep([class^="i-svg:"]) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
// 搜索触发器:确保 hover 时是图标变色,而不是只出现边框变化
|
||||
:deep(.command-palette-trigger) {
|
||||
border-color: var(--el-border-color-lighter) !important;
|
||||
}
|
||||
|
||||
:deep(.command-palette-trigger__left [class^="i-svg:"]) {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,12 +354,18 @@ function logout() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
// 给实际渲染的 el-avatar 增加边框
|
||||
:deep(.el-avatar) {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--el-color-success);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -350,7 +375,7 @@ function logout() {
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #50b731;
|
||||
background-color: var(--el-color-success);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@@ -371,11 +396,27 @@ function logout() {
|
||||
color: color-mix(in srgb, var(--el-color-white) 85%, transparent);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
color: color-mix(in srgb, var(--el-color-white) 85%, transparent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--el-color-white) 10%, transparent);
|
||||
background: transparent;
|
||||
|
||||
:deep([class^="i-svg:"]) {
|
||||
color: var(--el-color-white);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.command-palette-trigger) {
|
||||
border-color: color-mix(in srgb, var(--el-color-white) 35%, transparent) !important;
|
||||
}
|
||||
|
||||
:deep(.command-palette-trigger__left [class^="i-svg:"]) {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,11 +434,19 @@ function logout() {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
background: transparent;
|
||||
|
||||
:deep([class^="i-svg:"]) {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.command-palette-trigger) {
|
||||
border-color: var(--el-border-color-lighter) !important;
|
||||
}
|
||||
|
||||
:deep(.command-palette-trigger__left [class^="i-svg:"]) {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,22 @@
|
||||
@visible-change="(visible) => onContextMenuVisibleChange(visible, tag)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
|
||||
<span class="tag-main">
|
||||
<button
|
||||
type="button"
|
||||
class="tag-bookmark-btn"
|
||||
:class="{ 'is-bookmarked': isQuickLinkExists(tag) }"
|
||||
:title="isQuickLinkExists(tag) ? '取消收藏' : '加入收藏'"
|
||||
:aria-label="isQuickLinkExists(tag) ? '取消收藏' : '加入收藏'"
|
||||
@click.prevent.stop="toggleQuickStart(tag)"
|
||||
>
|
||||
<el-icon :size="14">
|
||||
<StarFilled v-if="isQuickLinkExists(tag)" />
|
||||
<Star v-else />
|
||||
</el-icon>
|
||||
</button>
|
||||
<span class="tag-text">{{ translateRouteTitle(tag.title) }}</span>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="refreshSelectedTag(tag)">
|
||||
@@ -73,15 +88,6 @@
|
||||
</el-icon>
|
||||
{{ t("navbar.closeAll") }}
|
||||
</el-dropdown-item>
|
||||
|
||||
<!-- 收藏到快速开始 -->
|
||||
<el-dropdown-item divided @click="toggleQuickStart(tag)">
|
||||
<el-icon>
|
||||
<Star v-if="!isQuickLinkExists(tag)" />
|
||||
<StarFilled v-else />
|
||||
</el-icon>
|
||||
{{ isQuickLinkExists(tag) ? "取消收藏" : "收藏" }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -207,6 +213,13 @@ const scrollbarRef = ref();
|
||||
// 标签切换来源跟踪
|
||||
const tagSwitchSource = ref<"menu" | "tab" | null>(null);
|
||||
|
||||
/** 收藏列表变更时递增,使星标与 localStorage 同步(否则取消收藏后 UI 不更新) */
|
||||
const quickLinksRevision = ref(0);
|
||||
|
||||
const onQuickLinksChanged = () => {
|
||||
quickLinksRevision.value++;
|
||||
};
|
||||
|
||||
// 路由映射缓存,提升查找性能
|
||||
const routePathMap = new Map<string, TagView>();
|
||||
|
||||
@@ -577,18 +590,19 @@ const toggleQuickStart = (tag: TagView) => {
|
||||
const isExists = quickStartManager.isLinkExists(href);
|
||||
|
||||
if (isExists) {
|
||||
// 取消收藏:找到对应的链接并删除
|
||||
const links = quickStartManager.getQuickLinks();
|
||||
const targetLink = links.find((link) => link.href === href);
|
||||
if (targetLink?.id) {
|
||||
quickStartManager.removeQuickLink(targetLink.id);
|
||||
ElMessage.success(`已取消收藏:${tag.title}`);
|
||||
} else if (href) {
|
||||
quickStartManager.removeQuickLinkByHref(href);
|
||||
}
|
||||
ElMessage.success(`已取消收藏:${tag.title}`);
|
||||
} else {
|
||||
// 添加收藏
|
||||
const quickLink = quickStartManager.createQuickLinkFromRoute(tag);
|
||||
quickStartManager.addQuickLink(quickLink);
|
||||
ElMessage.success(`已收藏:${tag.title}`);
|
||||
if (quickStartManager.addQuickLink(quickLink)) {
|
||||
ElMessage.success(`已收藏:${tag.title}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle quick start:", error);
|
||||
@@ -597,9 +611,10 @@ const toggleQuickStart = (tag: TagView) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查快速链接是否已存在
|
||||
* 检查快速链接是否已存在(依赖 quickLinksRevision,以便收藏增删后星标立即刷新)
|
||||
*/
|
||||
const isQuickLinkExists = (tag: TagView): boolean => {
|
||||
void quickLinksRevision.value;
|
||||
return quickStartManager.isLinkExists(tag.fullPath || tag.path);
|
||||
};
|
||||
|
||||
@@ -755,6 +770,8 @@ watch(
|
||||
onMounted(() => {
|
||||
initAffixTags();
|
||||
|
||||
quickStartManager.addListener(onQuickLinksChanged);
|
||||
|
||||
// 初始化路由映射缓存
|
||||
updateRoutePathMap();
|
||||
|
||||
@@ -773,6 +790,7 @@ onMounted(() => {
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
quickStartManager.removeListener(onQuickLinksChanged);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
@@ -845,10 +863,47 @@ onUnmounted(() => {
|
||||
:deep(.el-dropdown) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tag-main {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tag-bookmark-btn {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
&.is-bookmarked {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
min-width: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -881,6 +936,14 @@ onUnmounted(() => {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tag-bookmark-btn:not(.is-bookmarked) {
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tag-close-btn {
|
||||
color: var(--el-color-primary);
|
||||
&:hover {
|
||||
|
||||
@@ -205,8 +205,6 @@ html .el-table {
|
||||
}
|
||||
|
||||
html.dark .el-table {
|
||||
--el-table-header-bg-color: var(--el-fill-color);
|
||||
/* --el-table-striped-row-bg: var(--el-fill-color); */
|
||||
--el-table-row-hover-bg-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,46 @@
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
// 经典蓝:激活项背景不使用偏白色,改为蓝系轻色差背景
|
||||
html.sidebar-color-blue {
|
||||
.el-menu--vertical .el-menu-item.is-active,
|
||||
.el-menu--popup .el-menu-item.is-active {
|
||||
background-color: var(--menu-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 混合布局左侧菜单 hover 不单独覆盖:
|
||||
// 统一交给 Element Plus 的 --el-menu-hover-bg-color 与 MenuItem.vue 的通用规则,
|
||||
// 确保 Mix-left 与 Left 侧边布局保持一致。
|
||||
|
||||
// 暗色:菜单悬浮背景使用主题浅色,避免黑色悬浮
|
||||
html.dark .el-menu {
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .el-menu--popup {
|
||||
.el-menu-item:hover {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 亮色 + 经典蓝:悬浮背景使用经典蓝体系(比底色略深),避免发白
|
||||
html.sidebar-color-blue .el-menu {
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
background-color: var(--menu-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
html.sidebar-color-blue .el-menu--popup {
|
||||
.el-menu-item:hover {
|
||||
background-color: var(--menu-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 窄屏时隐藏菜单文字,只显示图标 ====================
|
||||
// 窄屏时隐藏菜单文字,只显示图标
|
||||
.hideSidebar {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
/** 收藏数量上限(工作台固定两列展示,不再使用内部滚动区) */
|
||||
export const QUICK_LINK_MAX = 10;
|
||||
|
||||
// 快速链接数据类型
|
||||
export interface QuickLink {
|
||||
title: string;
|
||||
@@ -39,22 +42,29 @@ class QuickStartManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加快速链接
|
||||
addQuickLink(link: QuickLink): void {
|
||||
/**
|
||||
* 添加或更新快速链接。
|
||||
* @returns 是否已保存;新增时若已达上限则提示并返回 false
|
||||
*/
|
||||
addQuickLink(link: QuickLink): boolean {
|
||||
const links = this.getQuickLinks();
|
||||
|
||||
// 检查是否已存在相同路径的链接
|
||||
const existingIndex = links.findIndex((l) => l.href === link.href);
|
||||
if (existingIndex !== -1) {
|
||||
// 更新现有链接
|
||||
links[existingIndex] = { ...links[existingIndex], ...link };
|
||||
ElMessage.success(`已更新快速链接:${link.title}`);
|
||||
} else {
|
||||
// 添加新链接
|
||||
links.push(link);
|
||||
this.saveQuickLinks(links);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (links.length >= QUICK_LINK_MAX) {
|
||||
ElMessage.warning(`收藏已满(最多 ${QUICK_LINK_MAX} 个),请先移除后再添加`);
|
||||
return false;
|
||||
}
|
||||
|
||||
links.push(link);
|
||||
this.saveQuickLinks(links);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 删除快速链接
|
||||
@@ -67,6 +77,15 @@ class QuickStartManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 无 id 的旧数据可按路由路径移除 */
|
||||
removeQuickLinkByHref(href: string): void {
|
||||
const links = this.getQuickLinks();
|
||||
const filteredLinks = links.filter((link) => link.href !== href);
|
||||
if (filteredLinks.length < links.length) {
|
||||
this.saveQuickLinks(filteredLinks);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有快速链接
|
||||
clearQuickLinks(): void {
|
||||
this.saveQuickLinks([]);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { MenuTable } from "@/api/module_system/menu";
|
||||
|
||||
/**
|
||||
* 工作台「模块总览」与 src/views 下目录对应:
|
||||
* module_system / module_monitor / module_task / module_ai / module_application / module_generator / module_example / module_common
|
||||
*/
|
||||
export interface MenuLeaf {
|
||||
title: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
component_path?: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export interface WorkplaceModuleGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
/** 与菜单 component_path 片段匹配(含 views 相对路径) */
|
||||
matchHints: string[];
|
||||
}
|
||||
|
||||
/** 按「业务域」划分,与前端 views 目录一致 */
|
||||
export const WORKPLACE_MODULE_GROUPS: WorkplaceModuleGroup[] = [
|
||||
{
|
||||
key: "system",
|
||||
title: "系统管理",
|
||||
subtitle: "用户、角色、菜单、部门、字典、岗位、参数、公告、日志等",
|
||||
matchHints: ["module_system/"],
|
||||
},
|
||||
{
|
||||
key: "monitor",
|
||||
title: "监控运维",
|
||||
subtitle: "服务监控、缓存、在线用户、资源占用等",
|
||||
matchHints: ["module_monitor/"],
|
||||
},
|
||||
{
|
||||
key: "task",
|
||||
title: "任务与流程",
|
||||
subtitle: "定时任务、执行节点、工作流等",
|
||||
matchHints: ["module_task/"],
|
||||
},
|
||||
{
|
||||
key: "ai",
|
||||
title: "AI 助手",
|
||||
subtitle: "对话、记忆管理等",
|
||||
matchHints: ["module_ai/"],
|
||||
},
|
||||
{
|
||||
key: "app",
|
||||
title: "应用与生成",
|
||||
subtitle: "应用管理、代码生成、示例演示等",
|
||||
matchHints: ["module_application/", "module_generator/", "module_example/"],
|
||||
},
|
||||
{
|
||||
key: "docs",
|
||||
title: "文档中心",
|
||||
subtitle: "Swagger / Redoc / 本地文档等",
|
||||
matchHints: ["module_common/"],
|
||||
},
|
||||
];
|
||||
|
||||
function normalizePath(p: string) {
|
||||
return p.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已授权的叶子菜单中,按 matchHints 顺序找到第一个可进入的路由
|
||||
*/
|
||||
export function resolveEntryPath(
|
||||
leaves: Pick<MenuLeaf, "path" | "component_path">[],
|
||||
hints: string[]
|
||||
): string | undefined {
|
||||
for (const hint of hints) {
|
||||
const hit = leaves.find(
|
||||
(l) => l.component_path && normalizePath(l.component_path).includes(hint)
|
||||
);
|
||||
if (hit) return hit.path;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveEntryTitle(leaves: MenuLeaf[], hints: string[]): string | undefined {
|
||||
for (const hint of hints) {
|
||||
const hit = leaves.find(
|
||||
(l) => l.component_path && normalizePath(l.component_path).includes(hint)
|
||||
);
|
||||
if (hit) return hit.title;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 叶子菜单扁平化(与侧栏同源),按菜单 order 排序 */
|
||||
export function flattenLeafMenusFromTree(menus: MenuTable[]): MenuLeaf[] {
|
||||
const seen = new Set<string>();
|
||||
const out: MenuLeaf[] = [];
|
||||
const walk = (items: MenuTable[]) => {
|
||||
for (const m of items) {
|
||||
if (m.hidden) continue;
|
||||
if (m.children?.length) {
|
||||
walk(m.children);
|
||||
} else if (m.title && m.route_path) {
|
||||
const raw = m.route_path.trim();
|
||||
const path = raw.startsWith("/") ? raw : `/${raw}`;
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
out.push({
|
||||
title: m.title,
|
||||
path,
|
||||
icon: m.icon,
|
||||
component_path: m.component_path,
|
||||
order: m.order,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(menus);
|
||||
out.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
return out;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,32 @@
|
||||
<!-- 部门树 -->
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="deptName" placeholder="部门名称">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
<div class="dept-tree-toolbar">
|
||||
<el-input v-model="deptName" placeholder="部门名称" class="dept-tree-search">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
class="dept-tree-expand-btn"
|
||||
@click="toggleTreeExpandAll"
|
||||
>
|
||||
<el-icon :size="14">
|
||||
<Switch />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<span>{{ treeExpanded ? "收起" : "展开" }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
node-key="value"
|
||||
:data="deptOptions"
|
||||
:props="{ children: 'children', label: 'label', disabled: 'disabled' }"
|
||||
:expand-on-click-node="false"
|
||||
@@ -27,6 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Switch } from "@element-plus/icons-vue";
|
||||
import DeptAPI, { DeptPageQuery } from "@/api/module_system/dept";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import type { FilterNodeMethodFunction, TreeInstance } from "element-plus";
|
||||
@@ -41,15 +57,66 @@ const props = defineProps({
|
||||
const deptOptions = ref<OptionType[]>([]); // 部门列表
|
||||
const deptTreeRef = ref<TreeInstance>(); // 部门树
|
||||
const deptName = ref(); // 部门名称
|
||||
/** 与「全部展开」状态同步,用于按钮提示(手动展开单节点后可能不完全一致) */
|
||||
const treeExpanded = ref(true);
|
||||
|
||||
const emits = defineEmits(["node-click", "update:modelValue"]);
|
||||
|
||||
const deptId = useVModel(props, "modelValue", emits);
|
||||
|
||||
watch(deptName, (val) => {
|
||||
deptTreeRef.value!.filter(val);
|
||||
deptTreeRef.value?.filter(val);
|
||||
});
|
||||
|
||||
type TreeStoreNode = {
|
||||
childNodes?: TreeStoreNode[];
|
||||
expand: () => void;
|
||||
collapse: () => void;
|
||||
};
|
||||
|
||||
function getTreeRoot() {
|
||||
const tree = deptTreeRef.value as TreeInstance & { store?: { root: TreeStoreNode } };
|
||||
return tree?.store?.root;
|
||||
}
|
||||
|
||||
/** 展开整棵树 */
|
||||
function expandAllTreeNodes() {
|
||||
const root = getTreeRoot();
|
||||
if (!root) return;
|
||||
const walk = (node: TreeStoreNode) => {
|
||||
node.childNodes?.forEach((child) => {
|
||||
if (child.childNodes?.length) {
|
||||
child.expand();
|
||||
walk(child);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(root);
|
||||
}
|
||||
|
||||
/** 收起整棵树 */
|
||||
function collapseAllTreeNodes() {
|
||||
const root = getTreeRoot();
|
||||
if (!root) return;
|
||||
const walk = (node: TreeStoreNode) => {
|
||||
node.childNodes?.forEach((child) => {
|
||||
walk(child);
|
||||
child.collapse();
|
||||
});
|
||||
};
|
||||
walk(root);
|
||||
}
|
||||
|
||||
function toggleTreeExpandAll() {
|
||||
if (treeExpanded.value) {
|
||||
collapseAllTreeNodes();
|
||||
treeExpanded.value = false;
|
||||
} else {
|
||||
expandAllTreeNodes();
|
||||
treeExpanded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -87,3 +154,24 @@ onBeforeMount(() => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dept-tree-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.dept-tree-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dept-tree-expand-btn {
|
||||
flex-shrink: 0;
|
||||
height: auto;
|
||||
min-height: unset;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -308,6 +308,7 @@
|
||||
trigger="click"
|
||||
:persistent="false"
|
||||
placement="auto-end"
|
||||
popper-class="node-cron-popover-fix"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
@@ -832,3 +833,33 @@ onMounted(async () => {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- popover 挂载到 body,需单独写;修复 vue3-cron-plus 全局 .el-tag--info { margin-left: -60px } 误伤多选下拉里 tag -->
|
||||
<style lang="scss">
|
||||
.node-cron-popover-fix {
|
||||
.vue3-cron-plus-container .el-select .el-tag {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 具体秒数等多选行:避免文案与选择器挤在同一行错位 */
|
||||
.vue3-cron-plus-container .tabBody .el-radio.long {
|
||||
align-items: flex-start;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
|
||||
.el-radio__label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 8px;
|
||||
align-items: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
flex: 1 1 200px;
|
||||
min-width: 180px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user