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

@@ -35,7 +35,7 @@ function handleClick(path?: string) {
</script>
<template>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbList class="easyflow-breadcrumb flex-nowrap">
<TransitionGroup name="breadcrumb-transition">
<template
v-for="(item, index) in breadcrumbs"
@@ -44,10 +44,10 @@ function handleClick(path?: string) {
<BreadcrumbItem>
<div v-if="item.items?.length ?? 0 > 0">
<DropdownMenu>
<DropdownMenuTrigger class="flex items-center gap-1">
<EasyFlowIcon v-if="showIcon" :icon="item.icon" class="size-5" />
{{ item.title }}
<ChevronDown class="size-4" />
<DropdownMenuTrigger class="easyflow-breadcrumb__link">
<EasyFlowIcon v-if="showIcon" :icon="item.icon" class="size-4" />
<span class="max-w-[180px] truncate">{{ item.title }}</span>
<ChevronDown class="size-3.5" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<template
@@ -63,32 +63,37 @@ function handleClick(path?: string) {
</div>
<BreadcrumbLink
v-else-if="index !== breadcrumbs.length - 1"
class="easyflow-breadcrumb__link"
href="javascript:void 0"
@click.stop="handleClick(item.path)"
>
<div class="flex-center">
<div class="flex items-center">
<EasyFlowIcon
v-if="showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
<span class="max-w-[180px] truncate">{{ item.title }}</span>
</div>
</BreadcrumbLink>
<BreadcrumbPage v-else>
<div class="flex-center">
<BreadcrumbPage
v-else
class="easyflow-breadcrumb__current"
>
<div class="flex items-center">
<EasyFlowIcon
v-if="showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
<span class="max-w-[220px] truncate">{{ item.title }}</span>
</div>
</BreadcrumbPage>
<BreadcrumbSeparator
v-if="index < breadcrumbs.length - 1 && !item.isHome"
class="easyflow-breadcrumb__separator"
/>
</BreadcrumbItem>
</template>
@@ -96,3 +101,60 @@ function handleClick(path?: string) {
</BreadcrumbList>
</Breadcrumb>
</template>
<style scoped>
.easyflow-breadcrumb {
gap: 4px;
color: hsl(var(--breadcrumb-muted));
font-size: 13px;
}
.easyflow-breadcrumb__link {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
padding: 4px 8px;
border-radius: 999px;
color: hsl(var(--breadcrumb-muted));
transition:
color 0.16s ease,
background-color 0.16s ease,
transform 0.16s ease;
}
.easyflow-breadcrumb__link:hover {
background: hsl(var(--nav-item-hover) / 0.7);
color: hsl(var(--nav-item-active-foreground));
transform: translateY(-0.5px);
}
.easyflow-breadcrumb__current {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
padding: 4px 10px;
color: hsl(var(--breadcrumb-current));
font-weight: 600;
letter-spacing: 0.01em;
background: linear-gradient(
135deg,
hsl(var(--nav-item-active) / 0.88),
hsl(var(--glass-tint) / 0.92)
);
border-radius: 999px;
box-shadow:
inset 0 1px 0 hsl(var(--nav-sheen) / 0.42),
0 10px 22px -18px hsl(var(--primary) / 0.22);
}
.easyflow-breadcrumb__separator {
color: hsl(var(--breadcrumb-muted) / 0.72);
}
.easyflow-breadcrumb__separator :deep(svg) {
width: 12px;
height: 12px;
}
</style>

View File

@@ -72,12 +72,16 @@ defineExpose({
</Transition>
<AlertDialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position: 'fixed' }"
:style="{
...(zIndex ? { zIndex } : {}),
position: 'fixed',
backdropFilter: 'blur(var(--glass-blur)) saturate(170%)',
}"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup bg-background p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] p-6 shadow-[var(--shadow-float)] outline-none supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] sm:rounded-[22px]',
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui';
import type { CSSProperties } from 'vue';
import type { ClassType } from '@easyflow-core/typings';
import { computed, ref } from 'vue';
@@ -20,16 +22,19 @@ const props = withDefaults(
class?: ClassType;
closeClass?: ClassType;
closeDisabled?: boolean;
contentStyle?: CSSProperties;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
overlayClass?: ClassType;
overlayStyle?: CSSProperties;
showClose?: boolean;
zIndex?: number;
}
>(),
{
appendTo: 'body',
animationType: 'slide',
animationType: 'scale',
closeDisabled: false,
showClose: true,
},
@@ -41,8 +46,11 @@ const emits = defineEmits<
const delegatedProps = computed(() => {
const {
class: _,
contentStyle: _contentStyle,
modal: _modal,
open: _open,
overlayClass: _overlayClass,
overlayStyle: _overlayStyle,
showClose: __,
animationType: ___,
...delegated
@@ -86,9 +94,11 @@ defineExpose({
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
:class="props.overlayClass"
:style="{
...(zIndex ? { zIndex } : {}),
position,
...props.overlayStyle,
backdropFilter:
overlayBlur && overlayBlur > 0 ? `blur(${overlayBlur}px)` : 'none',
}"
@@ -97,14 +107,18 @@ defineExpose({
</Transition>
<DialogContent
ref="contentRef"
:style="{ ...(zIndex ? { zIndex } : {}), position }"
:style="{
...(zIndex ? { zIndex } : {}),
position,
...props.contentStyle,
}"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:duration-220 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] p-6 shadow-[var(--shadow-float)] outline-none data-[state=closed]:duration-150 data-[state=closed]:ease-in data-[state=open]:ease-out supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] sm:rounded-[22px]',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
'data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-4':
animationType === 'slide',
},
props.class,
@@ -118,7 +132,7 @@ defineExpose({
:disabled="closeDisabled"
:class="
cn(
'data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none',
'data-[state=open]:text-muted-foreground hover:text-accent-foreground text-foreground/80 flex-center opacity-78 absolute right-3 top-3 h-7 w-7 rounded-full px-1 text-lg shadow-[0_10px_24px_-24px_hsl(var(--foreground)/0.34)] transition-opacity hover:bg-[hsl(var(--surface-contrast-soft))/0.98] hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92]',
props.closeClass,
)
"

View File

@@ -1,11 +1,29 @@
<script setup lang="ts">
import { inject } from 'vue';
import { computed, inject, useAttrs } from 'vue';
import { useScrollLock } from '@easyflow-core/composables';
import { cn } from '@easyflow-core/shared/utils';
defineOptions({
inheritAttrs: false,
});
useScrollLock();
const attrs = useAttrs();
const id = inject('DISMISSABLE_MODAL_ID');
const overlayClass = computed(() => {
const customClass = attrs.class as string | undefined;
return cn(
customClass ? 'z-popup inset-0' : 'bg-overlay z-popup inset-0',
customClass,
);
});
</script>
<template>
<div :data-dismissable-modal="id" class="bg-overlay z-popup inset-0"></div>
<div
:data-dismissable-modal="id"
:class="overlayClass"
:style="$attrs.style"
></div>
</template>

View File

@@ -91,6 +91,7 @@ function onAnimationEnd(event: AnimationEvent) {
:style="{
...(zIndex ? { zIndex } : {}),
position,
backdropFilter: 'blur(var(--glass-blur)) saturate(170%)',
}"
@animationend="onAnimationEnd"
v-bind="{ ...forwarded, ...$attrs }"

View File

@@ -3,7 +3,7 @@ import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const sheetVariants = cva(
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
'border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] shadow-[var(--shadow-float)] transition ease-in-out supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
defaultVariants: {
side: 'right',
@@ -11,11 +11,11 @@ export const sheetVariants = cva(
variants: {
side: {
bottom:
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
'inset-x-0 bottom-0 border-t border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
right:
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
'inset-y-0 right-0 w-3/4 border-l border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
top: 'inset-x-0 top-0 border-b border-[hsl(var(--divider-faint))/0.3] data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
},
},
},