feat: 统一管理端弹窗与内容区交互样式

- 收敛管理端公共 Modal 链路,新增表单弹窗与普通内容弹窗包装\n- 迁移 Bot、知识库、插件、工作流、资源、MCP、数据中枢与系统管理页面级弹窗\n- 统一内容区工具栏、列表容器、导航与顶部按钮的视觉密度和交互节奏
This commit is contained in:
2026-03-06 19:58:26 +08:00
parent 76c2954a70
commit b191d1aaed
99 changed files with 3148 additions and 1623 deletions

View File

@@ -18,7 +18,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
contentClass: 'easyflow-tabs-content',
contextMenus: () => [],
gap: 7,
gap: 8,
tabs: () => [],
});
@@ -76,7 +76,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
ref="contentRef"
:class="contentClass"
:style="style"
class="tabs-chrome !flex h-full w-max overflow-y-hidden pr-6"
class="tabs-chrome !flex h-full w-max items-center gap-1 overflow-y-hidden px-2"
>
<TransitionGroup name="slide-left">
<div
@@ -92,7 +92,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
]"
:data-active-tab="active"
:data-index="i"
class="tabs-chrome__item draggable translate-all group relative -mr-3 flex h-full select-none items-center"
class="tabs-chrome__item draggable translate-all group relative flex h-full min-w-0 select-none items-center"
data-tab-item="true"
@click="active = tab.key"
@mousedown="onMouseDown($event, tab)"
@@ -103,63 +103,45 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
:modal="false"
item-class="pr-6"
>
<div class="relative size-full px-1">
<div class="relative size-full">
<!-- divider -->
<div
v-if="i !== 0 && tab.key !== active"
class="tabs-chrome__divider bg-border absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
></div>
<!-- background -->
<div
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
class="tabs-chrome__background absolute inset-0 z-[-1] px-0 py-0 transition-opacity duration-150"
>
<div
class="tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
class="tabs-chrome__background-content h-[calc(100%-8px)] rounded-2xl bg-transparent transition-all duration-150 group-[.is-active]:bg-[hsl(var(--glass-tint))/0.74] group-[.is-active]:shadow-[0_16px_28px_-24px_hsl(var(--foreground)/0.34)]"
></div>
<svg
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
</svg>
<svg
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
height="7"
width="7"
>
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
</svg>
</div>
<!-- extra -->
<div
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
class="tabs-chrome__extra absolute right-2 top-1/2 z-[3] size-4 -translate-y-1/2"
>
<!-- close-icon -->
<X
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
class="mt-[2px] size-3 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
@click.stop="() => emit('close', tab.key)"
/>
<Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
class="mt-[1px] size-3.5 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- tab-item-main -->
<div
class="tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150"
class="tabs-chrome__item-main z-[2] mx-3 my-0 flex h-full min-w-0 items-center overflow-hidden pl-2 pr-5 text-[hsl(var(--nav-item-muted-foreground))] duration-150 group-[.is-active]:font-medium group-[.is-active]:text-[hsl(var(--nav-item-active-foreground))]"
>
<EasyFlowIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-1 flex size-4 items-center overflow-hidden"
class="mr-2 flex size-[15px] items-center overflow-hidden"
/>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
<span class="flex-1 overflow-hidden whitespace-nowrap text-[13px]">
{{ tab.title }}
</span>
</div>
@@ -187,10 +169,8 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
}
.tabs-chrome__background {
@apply pb-[2px];
&-content {
@apply bg-accent mx-[2px] rounded-md;
@apply bg-[hsl(var(--nav-item-hover))];
}
}
}

View File

@@ -38,7 +38,7 @@ const typeWithClass = computed(() => {
},
plain: {
content:
'h-full [&:not(:first-child)]:border-l last:border-r border-border',
"h-[calc(100%-8px)] rounded-2xl border border-transparent bg-transparent transition-all duration-150 [&.is-active]:bg-[hsl(var(--glass-tint))/0.72] [&.is-active]:shadow-[0_16px_28px_-24px_hsl(var(--foreground)/0.34)] [&:not(.is-active)]:hover:bg-[hsl(var(--nav-item-hover))/0.9]",
},
};
@@ -81,7 +81,7 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<template>
<div
:class="contentClass"
class="relative !flex h-full w-max items-center overflow-hidden pr-6"
class="relative !flex h-full w-max items-center gap-1 overflow-hidden pr-3"
>
<TransitionGroup name="slide-left">
<div
@@ -89,14 +89,14 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
:key="tab.key"
:class="[
{
'is-active dark:bg-accent bg-primary/15': tab.key === active,
'is-active': tab.key === active,
draggable: !tab.affixTab,
'affix-tab': tab.affixTab,
},
typeWithClass.content,
]"
:data-index="i"
class="tab-item [&:not(.is-active)]:hover:bg-accent translate-all group relative flex cursor-pointer select-none"
class="tab-item translate-all group relative flex min-w-0 cursor-pointer select-none"
data-tab-item="true"
@click="active = tab.key"
@mousedown="onMouseDown($event, tab)"
@@ -115,28 +115,28 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
<!-- close-icon -->
<X
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
class="size-3 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
@click.stop="() => emit('close', tab.key)"
/>
<Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
class="mt-[1px] size-3.5 cursor-pointer rounded-full stroke-[hsl(var(--nav-item-muted-foreground))] transition-all hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:stroke-[hsl(var(--foreground))] group-[.is-active]:stroke-[hsl(var(--nav-item-active-foreground))]"
@click.stop="() => emit('unpin', tab)"
/>
</div>
<!-- tab-item-main -->
<div
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
class="mx-3 mr-4 flex h-full items-center overflow-hidden pr-3 text-[hsl(var(--nav-item-muted-foreground))] transition-all duration-150 group-[.is-active]:font-medium group-[.is-active]:text-[hsl(var(--nav-item-active-foreground))]"
>
<EasyFlowIcon
v-if="showIcon"
:icon="tab.icon"
class="mr-2 flex size-4 items-center overflow-hidden"
class="mr-2 flex size-[15px] items-center overflow-hidden"
fallback
/>
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
<span class="flex-1 overflow-hidden whitespace-nowrap text-[13px]">
{{ tab.title }}
</span>
</div>

View File

@@ -10,7 +10,7 @@ defineProps<DropdownMenuProps>();
<template>
<EasyFlowDropdownMenu :menus="menus" :modal="false">
<div
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
class="flex-center mr-1 h-8 w-8 cursor-pointer rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.52] text-[hsl(var(--nav-item-muted-foreground))] shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform] hover:-translate-y-0.5 hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:text-foreground"
>
<ChevronDown class="size-4" />
</div>

View File

@@ -10,7 +10,7 @@ function toggleScreen() {
<template>
<div
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
class="flex-center h-8 w-8 cursor-pointer rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.52] text-[hsl(var(--nav-item-muted-foreground))] shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform] hover:-translate-y-0.5 hover:bg-[hsl(var(--surface-contrast-soft))/0.92] hover:text-foreground"
@click="toggleScreen"
>
<Minimize2 v-if="screen" class="size-4" />

View File

@@ -53,10 +53,10 @@ useTabsDrag(props, emit);
<span
v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtLeft,
'pointer-events-none opacity-30': scrollIsAtLeft,
}"
class="border-r px-2"
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.44] px-2 shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform]"
@click="scrollDirection('left')"
>
<ChevronLeft class="size-4 h-full" />
@@ -64,7 +64,7 @@ useTabsDrag(props, emit);
<div
:class="{
'pt-[3px]': styleType === 'chrome',
'pt-[2px]': styleType === 'chrome',
}"
class="size-full flex-1 overflow-hidden"
>
@@ -94,10 +94,10 @@ useTabsDrag(props, emit);
<span
v-show="showScrollButton"
:class="{
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtRight,
'pointer-events-none opacity-30': scrollIsAtRight,
}"
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.44] px-2 shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.3)] backdrop-blur-xl transition-[background-color,color,transform]"
@click="scrollDirection('right')"
>
<ChevronRight class="size-4 h-full" />

View File

@@ -1,5 +1,7 @@
import type { TabsProps } from './types';
import type { ComponentPublicInstance } from 'vue';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
@@ -7,12 +9,13 @@ import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
import { useDebounceFn } from '@vueuse/core';
type DomElement = Element | null | undefined;
type ScrollbarInstance = ComponentPublicInstance & { $el: HTMLElement };
export function useTabsViewScroll(props: TabsProps) {
let resizeObserver: null | ResizeObserver = null;
let mutationObserver: MutationObserver | null = null;
let tabItemCount = 0;
const scrollbarRef = ref<InstanceType<typeof EasyFlowScrollbar> | null>(null);
const scrollbarRef = ref<ScrollbarInstance | null>(null);
const scrollViewportEl = ref<DomElement>(null);
const showScrollButton = ref(false);
const scrollIsAtLeft = ref(true);