fix: 修复管理端前端 lint 与构建问题

- 收敛 easyflow-ui-admin 的 lint、格式和类型问题

- 修正 demo 页面与管理端前端构建失败点

- 验证 pnpm lint 与 pnpm build 均已通过
This commit is contained in:
2026-04-05 21:39:13 +08:00
parent bb72e19c84
commit 7e7c236c2a
240 changed files with 5151 additions and 4701 deletions

View File

@@ -114,6 +114,12 @@
}
@layer components {
@media (max-width: 768px) {
.easyflow-modal-grid--2 {
grid-template-columns: 1fr;
}
}
.flex-center {
@apply flex items-center justify-center;
}
@@ -161,11 +167,11 @@
}
.easyflow-modal-section {
padding: 14px;
background: hsl(var(--modal-content-surface-strong));
border: 1px solid hsl(var(--modal-divider));
border-radius: 14px;
box-shadow: none;
padding: 14px;
}
.easyflow-modal-grid {
@@ -178,10 +184,10 @@
}
.easyflow-modal-field-tip {
color: hsl(var(--text-muted));
margin-top: 4px;
font-size: 11px;
line-height: 1.5;
margin-top: 4px;
color: hsl(var(--text-muted));
}
.easyflow-modal-form .el-form-item {
@@ -189,11 +195,11 @@
}
.easyflow-modal-form .el-form-item__label {
color: hsl(var(--text-strong));
padding-bottom: 6px;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
padding-bottom: 6px;
color: hsl(var(--text-strong));
}
.easyflow-modal-form .el-input__wrapper,
@@ -202,7 +208,7 @@
.easyflow-modal-form .el-date-editor.el-input__wrapper {
background: hsl(var(--input-background));
border-radius: 12px;
box-shadow: inset 0 0 0 1px hsl(var(--input) / 0.92);
box-shadow: inset 0 0 0 1px hsl(var(--input) / 92%);
transition:
box-shadow 0.2s ease,
border-color 0.2s ease,
@@ -224,7 +230,7 @@
.easyflow-modal-form .el-select__wrapper:hover,
.easyflow-modal-form .el-textarea__inner:hover,
.easyflow-modal-form .el-date-editor.el-input__wrapper:hover {
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.18);
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 18%);
}
.easyflow-modal-form .el-input__wrapper.is-focus,
@@ -232,8 +238,8 @@
.easyflow-modal-form .el-textarea__inner:focus,
.easyflow-modal-form .el-date-editor.el-input__wrapper.is-focus {
box-shadow:
inset 0 0 0 1px hsl(var(--primary) / 0.72),
0 0 0 4px hsl(var(--primary) / 0.12);
inset 0 0 0 1px hsl(var(--primary) / 72%),
0 0 0 4px hsl(var(--primary) / 12%);
}
.easyflow-modal-form .el-form-item.is-error .el-input__wrapper,
@@ -243,14 +249,8 @@
.el-form-item.is-error
.el-date-editor.el-input__wrapper {
box-shadow:
inset 0 0 0 1px hsl(var(--destructive) / 0.8),
0 0 0 4px hsl(var(--destructive) / 0.08);
}
@media (max-width: 768px) {
.easyflow-modal-grid--2 {
grid-template-columns: 1fr;
}
inset 0 0 0 1px hsl(var(--destructive) / 80%),
0 0 0 4px hsl(var(--destructive) / 8%);
}
}
@@ -263,11 +263,12 @@ html.grayscale-mode {
}
.page-container {
padding: 20px;
margin: 20px;
background-color: var(--el-bg-color);
border-radius: var(--el-border-radius-base);
padding: 20px;
}
.handle-div {
margin-bottom: 10px;
}

View File

@@ -141,7 +141,7 @@
--modal-footer-surface: 220 13% 12%;
--modal-border: 218 16% 24%;
--modal-divider: 218 18% 24%;
--modal-shadow: 0 24px 60px -30px hsl(212 60% 2% / 0.58);
--modal-shadow: 0 24px 60px -30px hsl(212deg 60% 2% / 58%);
--modal-shell-highlight: 0 0% 100% / 0;
--modal-shell-border-soft: 218 16% 24% / 0.96;
--modal-shell-shadow-near: 212 50% 2% / 0.38;
@@ -159,9 +159,9 @@
--nav-flow-accent: 214 88% 70%;
--nav-flow-trace: 189 62% 56%;
--nav-sheen: 210 100% 96%;
--shadow-subtle: 0 18px 42px -30px hsl(212 100% 5% / 0.48);
--shadow-toolbar: 0 28px 54px -36px hsl(212 100% 4% / 0.58);
--shadow-float: 0 34px 70px -38px hsl(212 100% 3% / 0.68);
--shadow-subtle: 0 18px 42px -30px hsl(212deg 100% 5% / 48%);
--shadow-toolbar: 0 28px 54px -36px hsl(212deg 100% 4% / 58%);
--shadow-float: 0 34px 70px -38px hsl(212deg 100% 3% / 68%);
--bot-back-item: hsl(0deg 0% 91.76%);
--bot-collapse-itme-back: hsl(var(--background-deep));
--bot-chat-message-container: hsl(var(--background-deep));
@@ -171,6 +171,7 @@
--table-header-bg-color: hsl(var(--table-header-bg));
--table-cell-shadow-color: hsl(var(--table-row-border));
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
color-scheme: dark;
}

View File

@@ -141,7 +141,7 @@
--modal-footer-surface: 210 20% 98.4%;
--modal-border: 214 22% 86%;
--modal-divider: 214 22% 89%;
--modal-shadow: 0 22px 54px -28px hsl(215 28% 18% / 0.18);
--modal-shadow: 0 22px 54px -28px hsl(215deg 28% 18% / 18%);
--modal-shell-highlight: 0 0% 100% / 0;
--modal-shell-border-soft: 214 22% 86% / 0.94;
--modal-shell-shadow-near: 216 30% 18% / 0.08;
@@ -159,13 +159,9 @@
--nav-flow-accent: 214 100% 88%;
--nav-flow-trace: 191 78% 84%;
--nav-sheen: 0 0% 100%;
--shadow-subtle: 0 18px 42px -30px hsl(211 78% 48% / 0.16);
--shadow-toolbar: 0 28px 54px -34px hsl(211 78% 48% / 0.2);
--shadow-float: 0 34px 68px -38px hsl(211 78% 48% / 0.24);
accent-color: var(--primary);
color-scheme: light;
--shadow-subtle: 0 18px 42px -30px hsl(211deg 78% 48% / 16%);
--shadow-toolbar: 0 28px 54px -34px hsl(211deg 78% 48% / 20%);
--shadow-float: 0 34px 68px -38px hsl(211deg 78% 48% / 24%);
--bot-back-item: hsl(0deg 0% 91.76%);
--bot-collapse-itme-back: hsl(0deg 0% 96.08%);
--bot-chat-message-container: hsl(228deg 33.33% 97.06%);
@@ -175,6 +171,9 @@
--table-header-bg-color: hsl(var(--table-header-bg));
--table-cell-shadow-color: hsl(var(--table-row-border));
--bot-select-data-item-back: hsl(0deg 0% 98.04%);
accent-color: var(--primary);
color-scheme: light;
}
[data-theme='violet'] {

View File

@@ -1,2 +1,2 @@
export * from './globals';
export * from './easyflow';
export * from './globals';

View File

@@ -263,4 +263,3 @@ export function sortNodes(nodesJson: any): any[] {
extra: node.extra,
}));
}

View File

@@ -3,7 +3,11 @@ import { computed, toRaw, unref, watch } from 'vue';
import { useSimpleLocale } from '@easyflow-core/composables';
import { EasyFlowExpandableArrow } from '@easyflow-core/shadcn-ui';
import { cn, isFunction, triggerWindowResize } from '@easyflow-core/shared/utils';
import {
cn,
isFunction,
triggerWindowResize,
} from '@easyflow-core/shared/utils';
import { COMPONENT_MAP } from '../config';
import { injectFormProps } from '../use-form-context';

View File

@@ -2,8 +2,8 @@ import type { Component } from 'vue';
import type {
BaseFormComponentType,
FormCommonConfig,
EasyFlowFormAdapterOptions,
FormCommonConfig,
} from './types';
import { h } from 'vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Recordable } from '@easyflow-core/typings';
import type { ExtendedFormApi, EasyFlowFormProps } from './types';
import type { EasyFlowFormProps, ExtendedFormApi } from './types';
// import { toRaw, watch } from 'vue';
import { nextTick, onMounted, watch } from 'vue';

View File

@@ -9,7 +9,7 @@ import type { ComponentPublicInstance } from 'vue';
import type { Recordable } from '@easyflow-core/typings';
import type { FormActions, FormSchema, EasyFlowFormProps } from './types';
import type { EasyFlowFormProps, FormActions, FormSchema } from './types';
import { isRef, toRaw } from 'vue';
@@ -55,7 +55,7 @@ export class FormApi {
public form = {} as FormActions;
isMounted = false;
public state: null | EasyFlowFormProps = null;
public state: EasyFlowFormProps | null = null;
stateHandler: StateHandler;
public store: Store<EasyFlowFormProps>;
@@ -68,7 +68,7 @@ export class FormApi {
// 最后一次点击提交时的表单值
private latestSubmissionValues: null | Recordable<any> = null;
private prevState: null | EasyFlowFormProps = null;
private prevState: EasyFlowFormProps | null = null;
constructor(options: EasyFlowFormProps = {}) {
const { ...storeState } = options;

View File

@@ -15,7 +15,12 @@ import {
FormItem,
FormMessage,
} from '@easyflow-core/shadcn-ui';
import { cn, isFunction, isObject, isString } from '@easyflow-core/shared/utils';
import {
cn,
isFunction,
isObject,
isString,
} from '@easyflow-core/shared/utils';
import { toTypedSchema } from '@vee-validate/zod';
import { useFieldError, useFormValues } from 'vee-validate';

View File

@@ -2,9 +2,9 @@
import type { CustomRenderType } from '../types';
import {
FormLabel,
EasyFlowHelpTooltip,
EasyFlowRenderContent,
FormLabel,
} from '@easyflow-core/shadcn-ui';
import { cn } from '@easyflow-core/shared/utils';

View File

@@ -2,9 +2,9 @@ export { setupEasyFlowForm } from './config';
export type {
BaseFormComponentType,
ExtendedFormApi,
EasyFlowFormProps,
FormSchema as EasyFlowFormSchema,
ExtendedFormApi,
} from './types';
export * from './use-easyflow-form';

View File

@@ -12,12 +12,12 @@ export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type BaseFormComponentType =
| 'DefaultButton'
| 'PrimaryButton'
| 'EasyFlowCheckbox'
| 'EasyFlowInput'
| 'EasyFlowInputPassword'
| 'EasyFlowPinInput'
| 'EasyFlowSelect'
| 'PrimaryButton'
| (Record<never, never> & string);
type Breakpoints = '2xl:' | '3xl:' | '' | 'lg:' | 'md:' | 'sm:' | 'xl:';

View File

@@ -1,15 +1,15 @@
import type {
BaseFormComponentType,
ExtendedFormApi,
EasyFlowFormProps,
ExtendedFormApi,
} from './types';
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
import { useStore } from '@easyflow-core/shared/store';
import { FormApi } from './form-api';
import EasyFlowUseForm from './easyflow-use-form.vue';
import { FormApi } from './form-api';
export function useEasyFlowForm<
T extends BaseFormComponentType = BaseFormComponentType,

View File

@@ -2,12 +2,16 @@ import type { ZodRawShape } from 'zod';
import type { ComputedRef } from 'vue';
import type { ExtendedFormApi, FormActions, EasyFlowFormProps } from './types';
import type { EasyFlowFormProps, ExtendedFormApi, FormActions } from './types';
import { computed, unref, useSlots } from 'vue';
import { createContext } from '@easyflow-core/shadcn-ui';
import { isString, mergeWithArrayOverride, set } from '@easyflow-core/shared/utils';
import {
isString,
mergeWithArrayOverride,
set,
} from '@easyflow-core/shared/utils';
import { useForm } from 'vee-validate';
import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod';

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed} from 'vue';
import type { CSSProperties } from 'vue';
import type {ContentCompactType} from '@easyflow-core/typings';
import type { ContentCompactType } from '@easyflow-core/typings';
import {useLayoutContentStyle} from '@easyflow-core/composables';
import {Slot} from '@easyflow-core/shadcn-ui';
import { computed } from 'vue';
import { useLayoutContentStyle } from '@easyflow-core/composables';
import { Slot } from '@easyflow-core/shadcn-ui';
interface Props {
/**

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, useSlots} from 'vue';
import type { CSSProperties } from 'vue';
import { computed, useSlots } from 'vue';
interface Props {
/**

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, shallowRef, useSlots, watchEffect} from 'vue';
import type { CSSProperties } from 'vue';
import {EasyFlowScrollbar} from '@easyflow-core/shadcn-ui';
import { computed, shallowRef, useSlots, watchEffect } from 'vue';
import {useScrollLock} from '@vueuse/core';
import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
import {SidebarCollapseButton, SidebarFixedButton} from './widgets';
import { useScrollLock } from '@vueuse/core';
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
interface Props {
/**
@@ -295,15 +296,11 @@ function handleFixedToggle(nextValue: boolean) {
},
]"
:style="style"
class="fixed left-0 top-0 h-full overflow-hidden transition-all duration-150"
class="fixed left-0 top-0 h-full overflow-hidden transition-all duration-150"
@mouseenter="handleMouseenter"
@mouseleave="handleMouseleave"
>
<div
v-if="slots.logo"
:style="headerStyle"
class="relative z-10 px-2 pt-2"
>
<div v-if="slots.logo" :style="headerStyle" class="relative z-10 px-2 pt-2">
<slot name="logo"></slot>
</div>
<EasyFlowScrollbar :style="contentStyle" class="relative z-10">
@@ -333,17 +330,14 @@ function handleFixedToggle(nextValue: boolean) {
>
<slot name="extra-title"></slot>
</div>
<EasyFlowScrollbar
:style="extraContentStyle"
class="relative z-10 py-2"
>
<EasyFlowScrollbar :style="extraContentStyle" class="relative z-10 py-2">
<slot name="extra"></slot>
</EasyFlowScrollbar>
<div
v-if="expandOnHover || !extraCollapse"
:style="toolStyle"
class="absolute right-3 z-20 flex flex-col gap-2"
>
v-if="expandOnHover || !extraCollapse"
:style="toolStyle"
class="absolute right-3 z-20 flex flex-col gap-2"
>
<SidebarCollapseButton
v-if="expandOnHover"
v-model:collapsed="extraCollapse"

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed} from 'vue';
import type { CSSProperties } from 'vue';
import { computed } from 'vue';
interface Props {
/**

View File

@@ -21,7 +21,7 @@ function handleCollapsed() {
<button
aria-label="切换侧边栏折叠状态"
:style="buttonStyle"
class="flex-center h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
class="flex-center hover:text-foreground h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
@click.stop="handleCollapsed"
>
<ChevronsRight v-if="collapsed" class="size-4" />

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import type { CSSProperties } from 'vue';
import {Pin, PinOff} from '@easyflow-core/icons';
import { Pin, PinOff } from '@easyflow-core/icons';
const expandOnHover = defineModel<boolean>('expandOnHover');
const emit = defineEmits<{
toggle: [value: boolean];
}>();
const expandOnHover = defineModel<boolean>('expandOnHover');
const buttonStyle: CSSProperties = {
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
backgroundImage:
@@ -26,7 +26,7 @@ function toggleFixed() {
<button
aria-label="切换侧边栏固定状态"
:style="buttonStyle"
class="flex-center h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
class="flex-center hover:text-foreground h-9 w-9 rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]"
@click="toggleFixed"
>
<PinOff v-if="!expandOnHover" class="size-3.5" />

View File

@@ -1,19 +1,20 @@
<script setup lang="ts">
import type {CSSProperties} from 'vue';
import {computed, ref, watch} from 'vue';
import type { CSSProperties } from 'vue';
import type {EasyFlowLayoutProps} from './easyflow-layout';
import type { EasyFlowLayoutProps } from './easyflow-layout';
import { computed, ref, watch } from 'vue';
import {
SCROLL_FIXED_CLASS,
useLayoutFooterStyle,
useLayoutHeaderStyle,
} from '@easyflow-core/composables';
import {IconifyIcon} from '@easyflow-core/icons';
import {EasyFlowIconButton} from '@easyflow-core/shadcn-ui';
import {ELEMENT_ID_MAIN_CONTENT} from '@easyflow-core/shared/constants';
import { IconifyIcon } from '@easyflow-core/icons';
import { EasyFlowIconButton } from '@easyflow-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@easyflow-core/shared/constants';
import {useMouse, useScroll, useThrottleFn} from '@vueuse/core';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import {
LayoutContent,
@@ -22,7 +23,7 @@ import {
LayoutSidebar,
LayoutTabbar,
} from './components';
import {useLayout} from './hooks/use-layout';
import { useLayout } from './hooks/use-layout';
interface Props extends EasyFlowLayoutProps {}
@@ -30,24 +31,6 @@ defineOptions({
name: 'EasyFlowLayout',
});
const headerToggleButtonClass =
'my-0 mr-1 flex h-9 w-9 items-center justify-center rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]';
const headerToggleButtonStyle: CSSProperties = {
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
backgroundImage:
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.8))',
border: '1px solid hsl(var(--glass-border) / 0.2)',
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
};
const layoutFlowFieldStyle: CSSProperties = {
backgroundImage:
'radial-gradient(ellipse 14% 18% at 5% 3%, hsl(var(--nav-flow-core) / 0.34) 0%, hsl(var(--nav-flow-core) / 0.14) 34%, transparent 74%), radial-gradient(ellipse 48% 18% at 16% 8%, hsl(var(--nav-flow-core) / 0.12) 0%, transparent 68%), radial-gradient(ellipse 68% 34% at 28% 18%, hsl(var(--nav-flow-secondary) / 0.1) 0%, transparent 70%), linear-gradient(122deg, hsl(var(--nav-flow-trace) / 0.13) 0%, hsl(var(--nav-flow-trace) / 0.1) 8%, hsl(var(--nav-flow-core) / 0.08) 14%, transparent 24%, transparent 42%, hsl(var(--nav-flow-secondary) / 0.06) 58%, hsl(var(--nav-flow-trace) / 0.04) 70%, transparent 88%), radial-gradient(ellipse 84% 34% at 34% 26%, hsl(var(--nav-flow-core) / 0.08) 0%, transparent 72%), radial-gradient(ellipse 74% 30% at 48% 38%, hsl(var(--nav-flow-secondary) / 0.06) 0%, transparent 74%), radial-gradient(ellipse 62% 26% at 60% 48%, hsl(var(--nav-flow-accent) / 0.04) 0%, transparent 76%)',
};
const layoutFlowGlowStyle: CSSProperties = {
backgroundImage:
'radial-gradient(ellipse 18% 20% at 6% 4%, hsl(var(--nav-flow-core) / 0.18) 0%, transparent 74%), radial-gradient(ellipse 72% 28% at 26% 16%, hsl(var(--nav-flow-trace) / 0.08) 0%, transparent 70%), radial-gradient(ellipse 68% 26% at 44% 28%, hsl(var(--nav-flow-secondary) / 0.05) 0%, transparent 72%), radial-gradient(ellipse 58% 22% at 62% 42%, hsl(var(--nav-flow-trace) / 0.04) 0%, transparent 74%)',
};
const props = withDefaults(defineProps<Props>(), {
contentCompact: 'wide',
contentCompactWidth: 1200,
@@ -79,8 +62,25 @@ const props = withDefaults(defineProps<Props>(), {
tabbarHeight: 40,
zIndex: 200,
});
const emit = defineEmits<{ sideMouseLeave: []; toggleSidebar: [] }>();
const headerToggleButtonClass =
'my-0 mr-1 flex h-9 w-9 items-center justify-center rounded-2xl text-[hsl(var(--nav-item-muted-foreground))] backdrop-blur-xl transition-[background-color,color,transform,box-shadow,border-color] hover:-translate-y-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--primary))/0.2]';
const headerToggleButtonStyle: CSSProperties = {
backgroundColor: 'hsl(var(--nav-tool-bg) / 0.92)',
backgroundImage:
'linear-gradient(180deg, hsl(var(--nav-tool-bg) / 0.96), hsl(var(--glass-tint) / 0.8))',
border: '1px solid hsl(var(--glass-border) / 0.2)',
boxShadow: '0 18px 36px -28px hsl(var(--primary) / 0.24)',
};
const layoutFlowFieldStyle: CSSProperties = {
backgroundImage:
'radial-gradient(ellipse 14% 18% at 5% 3%, hsl(var(--nav-flow-core) / 0.34) 0%, hsl(var(--nav-flow-core) / 0.14) 34%, transparent 74%), radial-gradient(ellipse 48% 18% at 16% 8%, hsl(var(--nav-flow-core) / 0.12) 0%, transparent 68%), radial-gradient(ellipse 68% 34% at 28% 18%, hsl(var(--nav-flow-secondary) / 0.1) 0%, transparent 70%), linear-gradient(122deg, hsl(var(--nav-flow-trace) / 0.13) 0%, hsl(var(--nav-flow-trace) / 0.1) 8%, hsl(var(--nav-flow-core) / 0.08) 14%, transparent 24%, transparent 42%, hsl(var(--nav-flow-secondary) / 0.06) 58%, hsl(var(--nav-flow-trace) / 0.04) 70%, transparent 88%), radial-gradient(ellipse 84% 34% at 34% 26%, hsl(var(--nav-flow-core) / 0.08) 0%, transparent 72%), radial-gradient(ellipse 74% 30% at 48% 38%, hsl(var(--nav-flow-secondary) / 0.06) 0%, transparent 74%), radial-gradient(ellipse 62% 26% at 60% 48%, hsl(var(--nav-flow-accent) / 0.04) 0%, transparent 76%)',
};
const layoutFlowGlowStyle: CSSProperties = {
backgroundImage:
'radial-gradient(ellipse 18% 20% at 6% 4%, hsl(var(--nav-flow-core) / 0.18) 0%, transparent 74%), radial-gradient(ellipse 72% 28% at 26% 16%, hsl(var(--nav-flow-trace) / 0.08) 0%, transparent 70%), radial-gradient(ellipse 68% 26% at 44% 28%, hsl(var(--nav-flow-secondary) / 0.05) 0%, transparent 72%), radial-gradient(ellipse 58% 22% at 62% 42%, hsl(var(--nav-flow-trace) / 0.04) 0%, transparent 74%)',
};
const sidebarCollapse = defineModel<boolean>('sidebarCollapse', {
default: false,
});

View File

@@ -434,8 +434,8 @@ $namespace: easyflow;
&:focus-visible {
box-shadow:
0 0 0 1px hsl(var(--glass-border) / 0.72),
0 0 0 4px hsl(var(--ring) / 0.16);
0 0 0 1px hsl(var(--glass-border) / 72%),
0 0 0 4px hsl(var(--ring) / 16%);
}
* {
@@ -472,8 +472,8 @@ $namespace: easyflow;
--menu-item-collapse-margin-x: 8px;
--menu-item-radius: 14px;
--menu-item-shadow:
inset 0 1px 0 hsl(var(--nav-sheen) / 0.54),
0 22px 40px -30px hsl(var(--primary) / 0.24);
inset 0 1px 0 hsl(var(--nav-sheen) / 54%),
0 22px 40px -30px hsl(var(--primary) / 24%);
--menu-item-indent: 18px;
--menu-font-size: 14px;
--menu-item-indicator-width: 0px;
@@ -485,28 +485,28 @@ $namespace: easyflow;
--menu-item-hover-color: hsl(var(--foreground));
--menu-item-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.78),
hsl(var(--glass-tint) / 0.46)
hsl(var(--nav-item-hover) / 78%),
hsl(var(--glass-tint) / 46%)
);
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
--menu-item-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active) / 0.92) 0%,
hsl(var(--nav-item-hover) / 0.72) 54%,
hsl(var(--glass-tint) / 0.52) 100%
hsl(var(--nav-item-active) / 92%) 0%,
hsl(var(--nav-item-hover) / 72%) 54%,
hsl(var(--glass-tint) / 52%) 100%
);
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.78),
hsl(var(--glass-tint) / 0.46)
hsl(var(--nav-item-hover) / 78%),
hsl(var(--glass-tint) / 46%)
);
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
--menu-submenu-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active) / 0.92) 0%,
hsl(var(--nav-item-hover) / 0.72) 54%,
hsl(var(--glass-tint) / 0.52) 100%
hsl(var(--nav-item-active) / 92%) 0%,
hsl(var(--nav-item-hover) / 72%) 54%,
hsl(var(--glass-tint) / 52%) 100%
);
--menu-submenu-background-color: var(--menu-background-color);
}
@@ -518,28 +518,28 @@ $namespace: easyflow;
--menu-item-hover-color: hsl(var(--foreground));
--menu-item-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.78),
hsl(var(--glass-tint) / 0.46)
hsl(var(--nav-item-hover) / 78%),
hsl(var(--glass-tint) / 46%)
);
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
--menu-item-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active) / 0.92) 0%,
hsl(var(--nav-item-hover) / 0.72) 54%,
hsl(var(--glass-tint) / 0.52) 100%
hsl(var(--nav-item-active) / 92%) 0%,
hsl(var(--nav-item-hover) / 72%) 54%,
hsl(var(--glass-tint) / 52%) 100%
);
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.78),
hsl(var(--glass-tint) / 0.46)
hsl(var(--nav-item-hover) / 78%),
hsl(var(--glass-tint) / 46%)
);
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
--menu-submenu-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active) / 0.92) 0%,
hsl(var(--nav-item-hover) / 0.72) 54%,
hsl(var(--glass-tint) / 0.52) 100%
hsl(var(--nav-item-active) / 92%) 0%,
hsl(var(--nav-item-hover) / 72%) 54%,
hsl(var(--glass-tint) / 52%) 100%
);
--menu-submenu-background-color: var(--menu-background-color);
}
@@ -573,28 +573,28 @@ $namespace: easyflow;
--menu-item-hover-color: hsl(var(--foreground));
--menu-item-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.98),
hsl(var(--nav-surface-subtle) / 0.84)
hsl(var(--nav-item-hover) / 98%),
hsl(var(--nav-surface-subtle) / 84%)
);
--menu-item-active-color: hsl(var(--nav-item-active-foreground));
--menu-item-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active)) 0%,
hsl(var(--nav-item-hover) / 0.98) 55%,
hsl(var(--glass-tint) / 0.98) 100%
hsl(var(--nav-item-hover) / 98%) 55%,
hsl(var(--glass-tint) / 98%) 100%
);
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
--menu-submenu-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active)) 0%,
hsl(var(--nav-item-hover) / 0.98) 55%,
hsl(var(--glass-tint) / 0.98) 100%
hsl(var(--nav-item-hover) / 98%) 55%,
hsl(var(--glass-tint) / 98%) 100%
);
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.98),
hsl(var(--nav-surface-subtle) / 0.84)
hsl(var(--nav-item-hover) / 98%),
hsl(var(--nav-surface-subtle) / 84%)
);
}
@@ -603,27 +603,27 @@ $namespace: easyflow;
--menu-item-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active)) 0%,
hsl(var(--nav-item-hover) / 0.98) 55%,
hsl(var(--glass-tint) / 0.98) 100%
hsl(var(--nav-item-hover) / 98%) 55%,
hsl(var(--glass-tint) / 98%) 100%
);
--menu-item-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.98),
hsl(var(--nav-surface-subtle) / 0.84)
hsl(var(--nav-item-hover) / 98%),
hsl(var(--nav-surface-subtle) / 84%)
);
--menu-item-hover-color: hsl(var(--foreground));
--menu-submenu-active-color: hsl(var(--nav-item-active-foreground));
--menu-submenu-active-background-color: linear-gradient(
135deg,
hsl(var(--nav-item-active)) 0%,
hsl(var(--nav-item-hover) / 0.98) 55%,
hsl(var(--glass-tint) / 0.98) 100%
hsl(var(--nav-item-hover) / 98%) 55%,
hsl(var(--glass-tint) / 98%) 100%
);
--menu-submenu-hover-color: hsl(var(--foreground));
--menu-submenu-hover-background-color: linear-gradient(
180deg,
hsl(var(--nav-item-hover) / 0.98),
hsl(var(--nav-surface-subtle) / 0.84)
hsl(var(--nav-item-hover) / 98%),
hsl(var(--nav-surface-subtle) / 84%)
);
}
}
@@ -829,10 +829,10 @@ $namespace: easyflow;
&__content {
display: inline-flex;
gap: 10px;
align-items: center;
width: 100%;
height: var(--menu-item-height);
gap: 10px;
span {
@include menu-title;
@@ -867,7 +867,7 @@ $namespace: easyflow;
text-decoration: none;
cursor: pointer;
background: var(--menu-item-hover-background-color) !important;
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 0.18);
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 18%);
transform: translateX(2px);
}
@@ -902,8 +902,8 @@ $namespace: easyflow;
text-decoration: none;
cursor: pointer;
background: var(--menu-submenu-active-background-color);
fill: var(--menu-submenu-active-color);
box-shadow: var(--menu-item-shadow);
fill: var(--menu-submenu-active-color);
}
}
@@ -914,8 +914,8 @@ $namespace: easyflow;
&:focus-visible > .#{$namespace}-sub-menu-content,
&:focus-within > .#{$namespace}-sub-menu-content {
box-shadow:
0 0 0 1px hsl(var(--glass-border) / 0.72),
0 0 0 4px hsl(var(--ring) / 0.16);
0 0 0 1px hsl(var(--glass-border) / 72%),
0 0 0 4px hsl(var(--ring) / 16%);
}
}
@@ -938,8 +938,9 @@ $namespace: easyflow;
}
&__title {
@include menu-title;
font-weight: 500;
@include menu-title;
}
&.is-collapse-show-title {
@@ -973,7 +974,7 @@ $namespace: easyflow;
text-decoration: none;
cursor: pointer;
background: var(--menu-submenu-hover-background-color) !important;
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 0.18);
box-shadow: 0 18px 34px -30px hsl(var(--primary) / 18%);
transform: translateX(2px);
}
}

View File

@@ -7,7 +7,7 @@ import type { AlertProps, BeforeCloseScope, PromptProps } from './alert';
import { h, nextTick, ref, render } from 'vue';
import { useSimpleLocale } from '@easyflow-core/composables';
import { Input, EasyFlowRenderContent } from '@easyflow-core/shadcn-ui';
import { EasyFlowRenderContent, Input } from '@easyflow-core/shadcn-ui';
import { isFunction, isString } from '@easyflow-core/shared/utils';
import Alert from './alert.vue';

View File

@@ -177,7 +177,10 @@ async function handleOpenChange(val: boolean) {
<div class="m-4 min-h-[30px]">
<EasyFlowRenderContent :content="content" render-br />
</div>
<EasyFlowLoading v-if="loading && contentMasking" :spinning="loading" />
<EasyFlowLoading
v-if="loading && contentMasking"
:spinning="loading"
/>
</AlertDialogDescription>
<div
class="flex items-center justify-end gap-x-2"

View File

@@ -18,6 +18,10 @@ import {
} from '@easyflow-core/composables';
import { X } from '@easyflow-core/icons';
import {
EasyFlowButton,
EasyFlowHelpTooltip,
EasyFlowIconButton,
EasyFlowLoading,
Separator,
Sheet,
SheetClose,
@@ -26,10 +30,6 @@ import {
SheetFooter,
SheetHeader,
SheetTitle,
EasyFlowButton,
EasyFlowHelpTooltip,
EasyFlowIconButton,
EasyFlowLoading,
VisuallyHidden,
} from '@easyflow-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@easyflow-core/shared/constants';
@@ -184,11 +184,16 @@ const getForceMount = computed(() => {
<SheetContent
:append-to="getAppendTo"
:class="
cn('flex w-[520px] flex-col overflow-hidden border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] shadow-[var(--shadow-float)] supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62]', drawerClass, {
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
'max-h-[100vh]': placement === 'bottom' || placement === 'top',
hidden: isClosed,
})
cn(
'flex w-[520px] flex-col overflow-hidden border border-[hsl(var(--glass-border))/0.18] bg-[hsl(var(--glass-tint))/0.84] shadow-[var(--shadow-float)] supports-[backdrop-filter]:bg-[hsl(var(--glass-tint))/0.62]',
drawerClass,
{
'!w-full':
isMobile || placement === 'bottom' || placement === 'top',
'max-h-[100vh]': placement === 'bottom' || placement === 'top',
hidden: isClosed,
},
)
"
:modal="modal"
:open="state?.isOpen"
@@ -223,7 +228,7 @@ const getForceMount = computed(() => {
v-if="closable && closeIconPlacement === 'left'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92] ml-[2px] cursor-pointer rounded-full opacity-84 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
class="opacity-84 ml-[2px] cursor-pointer rounded-full transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92]"
>
<slot name="close-icon">
<EasyFlowIconButton>
@@ -264,7 +269,7 @@ const getForceMount = computed(() => {
v-if="closable && closeIconPlacement === 'right'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92] ml-[2px] cursor-pointer rounded-full opacity-84 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
class="opacity-84 ml-[2px] cursor-pointer rounded-full transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-[hsl(var(--surface-contrast-soft))/0.92]"
>
<slot name="close-icon">
<EasyFlowIconButton>

View File

@@ -105,7 +105,11 @@ export function useEasyFlowDrawer<
const Drawer = defineComponent(
(props: DrawerProps, { attrs, slots }) => {
return () =>
h(EasyFlowDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
h(
EasyFlowDrawer,
{ ...props, ...attrs, drawerApi: extendedApi },
slots,
);
},
// eslint-disable-next-line vue/one-component-per-file
{

View File

@@ -23,9 +23,9 @@ export function setDefaultModalProps(props: Partial<ModalProps>) {
Object.assign(DEFAULT_MODAL_PROPS, props);
}
export function useEasyFlowModal<TParentModalProps extends ModalProps = ModalProps>(
options: ModalApiOptions = {},
) {
export function useEasyFlowModal<
TParentModalProps extends ModalProps = ModalProps,
>(options: ModalApiOptions = {}) {
// Modal一般会抽离出来所以如果有传入 connectedComponent则表示为外部调用与内部组件进行连接
// 外部的Modal通过provide/inject传递api

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type {BreadcrumbProps} from './types';
import type { BreadcrumbProps } from './types';
import {ChevronDown} from '@easyflow-core/icons';
import { ChevronDown } from '@easyflow-core/icons';
import {
Breadcrumb,
@@ -15,7 +15,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../ui';
import {EasyFlowIcon} from '../icon';
import { EasyFlowIcon } from '../icon';
interface Props extends BreadcrumbProps {}
@@ -106,18 +106,18 @@ function handleClick(path?: string) {
<style scoped>
.easyflow-breadcrumb {
gap: 4px;
color: hsl(var(--breadcrumb-muted));
font-size: 13px;
color: hsl(var(--breadcrumb-muted));
}
.easyflow-breadcrumb__link {
display: inline-flex;
align-items: center;
gap: 4px;
align-items: center;
min-width: 0;
padding: 4px 8px;
border-radius: 999px;
color: hsl(var(--breadcrumb-muted));
border-radius: 999px;
transition:
color 0.16s ease,
background-color 0.16s ease,
@@ -125,20 +125,20 @@ function handleClick(path?: string) {
}
.easyflow-breadcrumb__link:hover {
background: hsl(var(--nav-item-hover) / 0.7);
color: hsl(var(--nav-item-active-foreground));
background: hsl(var(--nav-item-hover) / 70%);
transform: translateY(-0.5px);
}
.easyflow-breadcrumb__current {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
align-items: center;
min-width: 0;
padding: 4px 8px 6px;
color: hsl(var(--breadcrumb-current));
font-weight: 600;
color: hsl(var(--breadcrumb-current));
letter-spacing: 0.01em;
isolation: isolate;
}
@@ -150,17 +150,17 @@ function handleClick(path?: string) {
content: '';
background: linear-gradient(
180deg,
hsl(var(--nav-item-active) / 0.36) 0%,
hsl(var(--nav-item-active) / 0.18) 42%,
hsl(var(--glass-tint) / 0.08) 82%,
hsl(var(--nav-item-active) / 36%) 0%,
hsl(var(--nav-item-active) / 18%) 42%,
hsl(var(--glass-tint) / 8%) 82%,
transparent 100%
);
border-radius: 14px 14px 10px 10px;
box-shadow:
inset 1px 0 0 hsl(var(--nav-border) / 0.54),
inset -1px 0 0 hsl(var(--nav-border) / 0.54),
inset 0 1px 0 hsl(var(--nav-sheen) / 0.68),
0 10px 22px -22px hsl(var(--primary) / 0.2);
inset 1px 0 0 hsl(var(--nav-border) / 54%),
inset -1px 0 0 hsl(var(--nav-border) / 54%),
inset 0 1px 0 hsl(var(--nav-sheen) / 68%),
0 10px 22px -22px hsl(var(--primary) / 20%);
}
.easyflow-breadcrumb__current::after {
@@ -168,22 +168,22 @@ function handleClick(path?: string) {
right: -6px;
bottom: 0;
left: -6px;
height: 1px;
z-index: -1;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
hsl(var(--nav-item-active) / 0.2) 22%,
hsl(var(--nav-item-active) / 0.26) 50%,
hsl(var(--nav-item-active) / 0.2) 78%,
hsl(var(--nav-item-active) / 20%) 22%,
hsl(var(--nav-item-active) / 26%) 50%,
hsl(var(--nav-item-active) / 20%) 78%,
transparent
);
opacity: 0.72;
}
.easyflow-breadcrumb__separator {
color: hsl(var(--breadcrumb-muted) / 0.72);
color: hsl(var(--breadcrumb-muted) / 72%);
}
.easyflow-breadcrumb__separator :deep(svg) {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Arrayable } from '@vueuse/core';
import type { ValueType, EasyFlowButtonGroupProps } from './button';
import type { EasyFlowButtonGroupProps, ValueType } from './button';
import { computed, ref, watch } from 'vue';

View File

@@ -16,7 +16,10 @@ const style = computed(() => {
});
</script>
<template>
<div :style="style" class="easyflow-spine-text !bg-clip-text text-transparent">
<div
:style="style"
class="easyflow-spine-text !bg-clip-text text-transparent"
>
<slot></slot>
</div>
</template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type {TabDefinition} from '@easyflow-core/typings';
import type { TabDefinition } from '@easyflow-core/typings';
import type {TabConfig, TabsProps} from '../../types';
import type { TabConfig, TabsProps } from '../../types';
import {computed, ref} from 'vue';
import { computed, ref } from 'vue';
import {Pin, X} from '@easyflow-core/icons';
import {EasyFlowContextMenu, EasyFlowIcon} from '@easyflow-core/shadcn-ui';
import { Pin, X } from '@easyflow-core/icons';
import { EasyFlowContextMenu, EasyFlowIcon } from '@easyflow-core/shadcn-ui';
interface Props extends TabsProps {}
@@ -167,10 +167,10 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
inset: 1px -5px -1px;
content: '';
background: hsl(var(--background));
border-top: 2px solid hsl(var(--primary) / 38%);
border-right: 1px solid hsl(var(--primary) / 16%);
border-left: 1px solid hsl(var(--primary) / 16%);
border-radius: 15px 15px 0 0;
border-top: 2px solid hsl(var(--primary) / 0.38);
border-right: 1px solid hsl(var(--primary) / 0.16);
border-left: 1px solid hsl(var(--primary) / 0.16);
}
&__background-content.is-active::after {

View File

@@ -38,7 +38,7 @@ const typeWithClass = computed(() => {
},
plain: {
content:
"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]",
'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]',
},
};
@@ -136,7 +136,9 @@ function onMouseDown(e: MouseEvent, tab: TabConfig) {
fallback
/>
<span class="flex-1 overflow-hidden whitespace-nowrap text-[13px]">
<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 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"
class="flex-center hover:text-foreground 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]"
>
<ChevronDown class="size-4" />
</div>

View File

@@ -10,7 +10,7 @@ function toggleScreen() {
<template>
<div
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"
class="flex-center hover:text-foreground 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]"
@click="toggleScreen"
>
<Minimize2 v-if="screen" class="size-4" />

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type {TabsEmits, TabsProps} from './types';
import type { TabsEmits, TabsProps } from './types';
import {useForwardPropsEmits} from '@easyflow-core/composables';
import {ChevronLeft, ChevronRight} from '@easyflow-core/icons';
import {EasyFlowScrollbar} from '@easyflow-core/shadcn-ui';
import { useForwardPropsEmits } from '@easyflow-core/composables';
import { ChevronLeft, ChevronRight } from '@easyflow-core/icons';
import { EasyFlowScrollbar } from '@easyflow-core/shadcn-ui';
import {Tabs, TabsChrome} from './components';
import {useTabsDrag} from './use-tabs-drag';
import {useTabsViewScroll} from './use-tabs-view-scroll';
import { Tabs, TabsChrome } from './components';
import { useTabsDrag } from './use-tabs-drag';
import { useTabsViewScroll } from './use-tabs-view-scroll';
interface Props extends TabsProps {}
@@ -53,7 +53,8 @@ useTabsDrag(props, emit);
<span
v-show="showScrollButton"
:class="{
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtLeft,
'hover:text-foreground cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))]':
!scrollIsAtLeft,
'pointer-events-none opacity-30': scrollIsAtLeft,
}"
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.18] px-2 shadow-none backdrop-blur-xl transition-[background-color,color,transform]"
@@ -94,7 +95,8 @@ useTabsDrag(props, emit);
<span
v-show="showScrollButton"
:class="{
'cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))] hover:text-foreground': !scrollIsAtRight,
'hover:text-foreground cursor-pointer text-[hsl(var(--nav-item-muted-foreground))] hover:bg-[hsl(var(--surface-contrast-soft))]':
!scrollIsAtRight,
'pointer-events-none opacity-30': scrollIsAtRight,
}"
class="mx-1 my-1 flex items-center rounded-2xl border border-transparent bg-[hsl(var(--glass-tint))/0.18] px-2 shadow-none backdrop-blur-xl transition-[background-color,color,transform]"

View File

@@ -1,7 +1,7 @@
import type { TabsProps } from './types';
import type { ComponentPublicInstance } from 'vue';
import type { TabsProps } from './types';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
@@ -13,7 +13,7 @@ export function useTabsViewScroll(props: TabsProps) {
let resizeObserver: null | ResizeObserver = null;
let mutationObserver: MutationObserver | null = null;
let tabItemCount = 0;
const scrollbarRef = ref<ScrollbarInstance | null>(null);
const scrollbarRef = ref<null | ScrollbarInstance>(null);
const scrollViewportEl = ref<DomElement>(null);
const showScrollButton = ref(false);
const scrollIsAtLeft = ref(true);

View File

@@ -7,7 +7,12 @@ import { computed, nextTick, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@easyflow/icons';
import { cloneDeep, get, isEqual, isFunction } from '@easyflow-core/shared/utils';
import {
cloneDeep,
get,
isEqual,
isFunction,
} from '@easyflow-core/shared/utils';
import { objectOmit } from '@vueuse/core';

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { ChatThinkingBlockProps } from './types';
import { computed } from 'vue';
defineOptions({
name: 'ChatThinkingBlock',
});
@@ -24,8 +24,8 @@ const emit = defineEmits<{
const normalizedContent = computed(() =>
String(props.content || '')
.replace(/\r\n/g, '\n')
.replace(/^\s*\n+/, '')
.replaceAll('\r\n', '\n')
.replace(/^\s*\n/, '')
.trimEnd(),
);
@@ -131,27 +131,26 @@ function toggleExpanded() {
<style scoped>
.chat-thinking-block {
border: 1px solid hsl(var(--divider-faint) / 0.18);
background: linear-gradient(
180deg,
hsl(var(--glass-tint) / 48%) 0%,
hsl(var(--surface-panel) / 74%) 100%
);
border: 1px solid hsl(var(--divider-faint) / 18%);
border-radius: 16px;
background:
linear-gradient(
180deg,
hsl(var(--glass-tint) / 0.48) 0%,
hsl(var(--surface-panel) / 0.74) 100%
);
box-shadow:
inset 0 1px 0 hsl(var(--glass-border) / 0.24),
0 10px 24px -24px hsl(var(--foreground) / 0.18);
inset 0 1px 0 hsl(var(--glass-border) / 24%),
0 10px 24px -24px hsl(var(--foreground) / 18%);
backdrop-filter: blur(12px);
}
.chat-thinking-block__trigger {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
width: 100%;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 9px 12px;
color: inherit;
text-align: left;
@@ -166,7 +165,7 @@ function toggleExpanded() {
}
.chat-thinking-block__trigger:not(:disabled):hover {
background: hsl(var(--surface-contrast-soft) / 0.34);
background: hsl(var(--surface-contrast-soft) / 34%);
}
.chat-thinking-block__trigger:disabled {
@@ -175,9 +174,9 @@ function toggleExpanded() {
.chat-thinking-block__leading {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 8px;
align-items: center;
min-width: 0;
}
.chat-thinking-block__indicator {
@@ -185,19 +184,19 @@ function toggleExpanded() {
flex: 0 0 auto;
width: 8px;
height: 8px;
background: hsl(var(--text-muted) / 74%);
border-radius: 999px;
background: hsl(var(--text-muted) / 0.74);
}
.chat-thinking-block.is-thinking .chat-thinking-block__indicator {
background: hsl(var(--primary) / 0.82);
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
background: hsl(var(--primary) / 82%);
box-shadow: 0 0 0 4px hsl(var(--primary) / 12%);
animation: chat-thinking-pulse 1.8s ease-in-out infinite;
}
.chat-thinking-block.is-error .chat-thinking-block__indicator {
background: hsl(var(--destructive) / 0.86);
box-shadow: 0 0 0 4px hsl(var(--destructive) / 0.1);
background: hsl(var(--destructive) / 86%);
box-shadow: 0 0 0 4px hsl(var(--destructive) / 10%);
}
.chat-thinking-block__label {
@@ -211,10 +210,10 @@ function toggleExpanded() {
.chat-thinking-block__summary {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
line-height: 1.3;
color: hsl(var(--text-muted));
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -236,15 +235,15 @@ function toggleExpanded() {
}
.chat-thinking-block__content {
margin: 0;
padding: 10px 12px;
border-radius: 12px;
background: hsl(var(--surface-panel) / 0.72);
margin: 0;
font-size: 12px;
line-height: 1.68;
color: hsl(var(--text-secondary));
overflow-wrap: anywhere;
white-space: pre-wrap;
word-break: break-word;
background: hsl(var(--surface-panel) / 72%);
border-radius: 12px;
}
.chat-thinking-block.is-disabled {
@@ -267,12 +266,12 @@ function toggleExpanded() {
@keyframes chat-thinking-pulse {
0%,
100% {
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
box-shadow: 0 0 0 4px hsl(var(--primary) / 12%);
opacity: 0.92;
}
50% {
box-shadow: 0 0 0 7px hsl(var(--primary) / 0.04);
box-shadow: 0 0 0 7px hsl(var(--primary) / 4%);
opacity: 1;
}
}

View File

@@ -1,5 +1,2 @@
export { default as ChatThinkingBlock } from './ChatThinkingBlock.vue';
export type {
ChatThinkingBlockProps,
ChatThinkingBlockStatus,
} from './types';
export type { ChatThinkingBlockProps, ChatThinkingBlockStatus } from './types';

View File

@@ -8,10 +8,10 @@ import { EmptyIcon, Grip, listIcons } from '@easyflow/icons';
import { $t } from '@easyflow/locales';
import {
Button,
EasyFlowIcon,
EasyFlowIconButton,
EasyFlowPopover,
Button,
Input,
Pagination,
PaginationEllipsis,

View File

@@ -4,7 +4,7 @@ import type { TreeProps } from '@easyflow-core/shadcn-ui';
import { Inbox } from '@easyflow/icons';
import { $t } from '@easyflow/locales';
import { treePropsDefaults, EasyFlowTree } from '@easyflow-core/shadcn-ui';
import { EasyFlowTree, treePropsDefaults } from '@easyflow-core/shadcn-ui';
const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
</script>

View File

@@ -1,7 +1,8 @@
import {mount} from '@vue/test-utils';
import {nextTick} from 'vue';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import AuthenticationLogin from '../login.vue';
const { formApi, routerPush } = vi.hoisted(() => ({
@@ -41,7 +42,8 @@ vi.mock('@easyflow-core/form-ui', async () => {
vi.mock('@easyflow-core/shadcn-ui', async (importOriginal) => {
const vue = await import('vue');
const actual = await importOriginal<typeof import('@easyflow-core/shadcn-ui')>();
const actual =
await importOriginal<typeof import('@easyflow-core/shadcn-ui')>();
return {
...actual,
@@ -102,7 +104,7 @@ vi.mock('@easyflow-core/shadcn-ui', async (importOriginal) => {
};
});
describe('AuthenticationLogin', () => {
describe('authenticationLogin', () => {
const rememberKey = `REMEMBER_ME_ACCOUNT_${location.hostname}`;
beforeEach(() => {

View File

@@ -6,7 +6,9 @@
<slot></slot>
</h2>
<p class="text-muted-foreground max-w-[34rem] text-[0.97rem] leading-7 sm:text-[1rem]">
<p
class="text-muted-foreground max-w-[34rem] text-[0.97rem] leading-7 sm:text-[1rem]"
>
<slot name="desc"></slot>
</p>
</div>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import type {Recordable} from '@easyflow/types';
import type { Recordable } from '@easyflow/types';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';

View File

@@ -6,7 +6,7 @@ import { computed, watch } from 'vue';
import { $t } from '@easyflow/locales';
import { useEasyFlowModal } from '@easyflow-core/popup-ui';
import { Slot, EasyFlowAvatar } from '@easyflow-core/shadcn-ui';
import { EasyFlowAvatar, Slot } from '@easyflow-core/shadcn-ui';
interface Props extends AuthenticationProps {
avatar?: string;

View File

@@ -1,16 +1,17 @@
<script setup lang="ts">
import type {Recordable} from '@easyflow/types';
import type { Recordable } from '@easyflow/types';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import type {AuthenticationProps} from './types';
import type { AuthenticationProps } from './types';
import {computed, onMounted, reactive, ref} from 'vue';
import {useRouter} from 'vue-router';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import {$t} from '@easyflow/locales';
import {EasyFlowButton, EasyFlowCheckbox} from '@easyflow-core/shadcn-ui';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton, EasyFlowCheckbox } from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
import ThirdPartyLogin from './third-party-login.vue';
@@ -174,7 +175,10 @@ defineExpose({
</slot>
<slot name="to-register">
<div v-if="showRegister" class="auth-footer-copy mt-5 text-center text-sm">
<div
v-if="showRegister"
class="auth-footer-copy mt-5 text-center text-sm"
>
{{ $t('authentication.accountTip') }}
<button
class="auth-inline-action"
@@ -198,8 +202,8 @@ defineExpose({
}
.auth-form-group :deep(.easyflow-form-ui .text-destructive) {
font-size: 0.84rem;
margin-top: 0.45rem;
font-size: 0.84rem;
}
.auth-login-options {
@@ -207,14 +211,14 @@ defineExpose({
}
.auth-checkbox {
color: hsl(var(--text-muted));
font-size: 0.92rem;
color: hsl(var(--text-muted));
}
.auth-inline-action {
color: hsl(var(--nav-item-active-foreground));
font-size: 0.92rem;
font-weight: 500;
color: hsl(var(--nav-item-active-foreground));
transition: opacity 0.18s ease;
}
@@ -228,20 +232,19 @@ defineExpose({
}
.auth-brand-submit {
background:
linear-gradient(
120deg,
rgb(11 111 211) 0%,
rgb(22 159 200) 38%,
rgb(38 199 193) 62%,
rgb(11 111 211) 100%
);
color: rgb(255 255 255);
background: linear-gradient(
120deg,
rgb(11 111 211) 0%,
rgb(22 159 200) 38%,
rgb(38 199 193) 62%,
rgb(11 111 211) 100%
);
background-size: 200% 200%;
border: none;
box-shadow:
0 22px 34px -22px rgb(11 111 211 / 0.56),
inset 0 1px 0 rgb(255 255 255 / 0.24);
color: rgb(255 255 255);
0 22px 34px -22px rgb(11 111 211 / 56%),
inset 0 1px 0 rgb(255 255 255 / 24%);
transition:
transform 180ms ease,
box-shadow 180ms ease,
@@ -251,8 +254,8 @@ defineExpose({
.auth-brand-submit:hover {
box-shadow:
0 24px 38px -22px rgb(11 111 211 / 0.62),
inset 0 1px 0 rgb(255 255 255 / 0.28);
0 24px 38px -22px rgb(11 111 211 / 62%),
inset 0 1px 0 rgb(255 255 255 / 28%);
filter: saturate(1.04);
transform: translateY(-1px);
}
@@ -262,7 +265,7 @@ defineExpose({
}
.auth-brand-submit:focus-visible {
outline: 2px solid rgb(78 176 255 / 0.8);
outline: 2px solid rgb(78 176 255 / 80%);
outline-offset: 2px;
}

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import {ref} from 'vue';
import {useRouter} from 'vue-router';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import {$t} from '@easyflow/locales';
import { $t } from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import {useQRCode} from '@vueuse/integrations/useQRCode';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import Title from './auth-title.vue';
@@ -112,25 +112,29 @@ function goToLogin() {
}
.auth-qrcode-frame {
align-items: center;
background: linear-gradient(180deg, rgb(255 255 255 / 0.92), rgb(244 249 255 / 0.96));
border: 1px solid hsl(var(--line-subtle));
border-radius: 1.5rem;
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
max-width: 17rem;
padding: 1rem;
margin: 0 auto;
background: linear-gradient(
180deg,
rgb(255 255 255 / 92%),
rgb(244 249 255 / 96%)
);
border: 1px solid hsl(var(--line-subtle));
border-radius: 1.5rem;
box-shadow: inset 0 1px 0 rgb(255 255 255 / 60%);
}
.auth-qrcode-image {
width: min(100%, 13rem);
aspect-ratio: 1;
border-radius: 1rem;
width: min(100%, 13rem);
}
.dark .auth-qrcode-frame {
background: linear-gradient(180deg, rgb(14 22 36 / 0.92), rgb(11 19 31 / 0.96));
background: linear-gradient(180deg, rgb(14 22 36 / 92%), rgb(11 19 31 / 96%));
}
</style>

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import type {Recordable} from '@easyflow/types';
import type { Recordable } from '@easyflow/types';
import type {EasyFlowFormSchema} from '@easyflow-core/form-ui';
import {useEasyFlowForm} from '@easyflow-core/form-ui';
import type { EasyFlowFormSchema } from '@easyflow-core/form-ui';
import {computed, reactive} from 'vue';
import {useRouter} from 'vue-router';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import {$t} from '@easyflow/locales';
import {EasyFlowButton} from '@easyflow-core/shadcn-ui';
import { $t } from '@easyflow/locales';
import { useEasyFlowForm } from '@easyflow-core/form-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -127,9 +128,9 @@ defineExpose({
}
.auth-inline-action {
color: hsl(var(--nav-item-active-foreground));
font-size: 0.92rem;
font-weight: 500;
color: hsl(var(--nav-item-active-foreground));
transition: opacity 0.18s ease;
}

View File

@@ -1,5 +1,10 @@
<script setup lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '@easyflow-core/shadcn-ui';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@easyflow-core/shadcn-ui';
interface Props {
title: string;

View File

@@ -3,7 +3,12 @@ import type { TabOption } from '@easyflow/types';
import { computed } from 'vue';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@easyflow-core/shadcn-ui';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@easyflow-core/shadcn-ui';
interface Props {
tabs?: TabOption[];

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type {ToolbarType} from './types';
import type { ToolbarType } from './types';
import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {useRoute} from 'vue-router';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import {$t} from '@easyflow/locales';
import {preferences, usePreferences} from '@easyflow/preferences';
import { $t } from '@easyflow/locales';
import { preferences, usePreferences } from '@easyflow/preferences';
import {Copyright} from '../basic/copyright';
import { Copyright } from '../basic/copyright';
import AuthenticationFormView from './form.vue';
import Toolbar from './toolbar.vue';
@@ -107,7 +107,9 @@ onBeforeUnmount(() => {
class="absolute left-0 top-0 z-20 flex flex-1"
@click="clickLogo"
>
<div class="auth-brand-chip text-foreground ml-4 mt-4 flex items-center sm:ml-6 sm:mt-6">
<div
class="auth-brand-chip text-foreground ml-4 mt-4 flex items-center sm:ml-6 sm:mt-6"
>
<img
v-if="logoSrc"
:key="logoSrc"
@@ -121,8 +123,12 @@ onBeforeUnmount(() => {
</div>
</slot>
<main class="auth-stage relative z-10 flex min-h-full w-full items-center justify-center px-6 pb-10 pt-28 sm:px-10 sm:pt-32">
<div class="auth-stage-inner mx-auto flex w-full max-w-[1080px] flex-col items-center">
<main
class="auth-stage relative z-10 flex min-h-full w-full items-center justify-center px-6 pb-10 pt-28 sm:px-10 sm:pt-32"
>
<div
class="auth-stage-inner mx-auto flex w-full max-w-[1080px] flex-col items-center"
>
<div
v-if="isLoginRoute"
class="auth-stage-copy w-full max-w-[780px] text-center"
@@ -130,7 +136,10 @@ onBeforeUnmount(() => {
<h1 class="auth-page-title text-foreground">
{{ stageGreeting }}
</h1>
<div class="auth-stage-switcher text-muted-foreground" aria-label="同一入口能力切换">
<div
class="auth-stage-switcher text-muted-foreground"
aria-label="同一入口能力切换"
>
<span class="auth-stage-switcher-label">在同一入口管理</span>
<span class="auth-stage-pill" aria-live="polite">
<Transition mode="out-in" name="auth-pill">
@@ -146,8 +155,8 @@ onBeforeUnmount(() => {
</div>
<AuthenticationFormView
class="auth-window-host w-full"
:class="[
'auth-window-host w-full',
isLoginRoute ? 'max-w-[25rem] sm:max-w-[26rem]' : 'max-w-[31rem]',
isLoginRoute ? 'mt-8 sm:mt-10' : 'mt-0 sm:mt-4',
]"
@@ -170,69 +179,100 @@ onBeforeUnmount(() => {
<style scoped>
.auth-shell {
background:
radial-gradient(circle at top, rgb(255 255 255 / 78%), rgb(255 255 255 / 0) 36%),
radial-gradient(
circle at top,
rgb(255 255 255 / 78%),
rgb(255 255 255 / 0%) 36%
),
linear-gradient(180deg, #f7faff 0%, #eef4fd 55%, #edf3fb 100%);
}
.auth-shell-grid {
background-image:
linear-gradient(rgb(13 74 160 / 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgb(13 74 160 / 0.06) 1px, transparent 1px);
linear-gradient(rgb(13 74 160 / 6%) 1px, transparent 1px),
linear-gradient(90deg, rgb(13 74 160 / 6%) 1px, transparent 1px);
background-position: center center;
background-size: 120px 120px;
mask-image: linear-gradient(180deg, rgb(0 0 0 / 0.28), transparent 75%);
opacity: 0.42;
mask-image: linear-gradient(180deg, rgb(0 0 0 / 28%), transparent 75%);
}
.auth-shell-noise {
background-image:
radial-gradient(circle at 20% 20%, rgb(255 255 255 / 0.35) 0 0.9px, transparent 1.2px),
radial-gradient(circle at 80% 30%, rgb(255 255 255 / 0.22) 0 1px, transparent 1.3px),
radial-gradient(circle at 40% 70%, rgb(11 111 211 / 0.08) 0 1px, transparent 1.4px);
background-size: 180px 180px, 240px 240px, 200px 200px;
radial-gradient(
circle at 20% 20%,
rgb(255 255 255 / 35%) 0 0.9px,
transparent 1.2px
),
radial-gradient(
circle at 80% 30%,
rgb(255 255 255 / 22%) 0 1px,
transparent 1.3px
),
radial-gradient(
circle at 40% 70%,
rgb(11 111 211 / 8%) 0 1px,
transparent 1.4px
);
background-size:
180px 180px,
240px 240px,
200px 200px;
mix-blend-mode: soft-light;
opacity: 0.65;
}
.auth-glow {
border-radius: 9999px;
pointer-events: none;
position: absolute;
pointer-events: none;
border-radius: 9999px;
}
.auth-glow-primary {
background: radial-gradient(circle, rgb(87 150 255 / 0.24) 0%, rgb(87 150 255 / 0) 68%);
height: 26rem;
left: 50%;
top: 4rem;
transform: translateX(-50%);
left: 50%;
width: 26rem;
height: 26rem;
background: radial-gradient(
circle,
rgb(87 150 255 / 24%) 0%,
rgb(87 150 255 / 0%) 68%
);
transform: translateX(-50%);
}
.auth-glow-secondary {
background: radial-gradient(circle, rgb(36 189 211 / 0.18) 0%, rgb(36 189 211 / 0) 72%);
height: 20rem;
left: 14%;
top: 46%;
left: 14%;
width: 20rem;
height: 20rem;
background: radial-gradient(
circle,
rgb(36 189 211 / 18%) 0%,
rgb(36 189 211 / 0%) 72%
);
}
.auth-glow-tertiary {
background: radial-gradient(circle, rgb(66 116 255 / 0.14) 0%, rgb(66 116 255 / 0) 74%);
bottom: 8%;
height: 22rem;
right: 10%;
bottom: 8%;
width: 22rem;
height: 22rem;
background: radial-gradient(
circle,
rgb(66 116 255 / 14%) 0%,
rgb(66 116 255 / 0%) 74%
);
}
.auth-brand-chip {
backdrop-filter: blur(12px);
background: rgb(255 255 255 / 0.72);
border: 1px solid rgb(255 255 255 / 0.84);
border-radius: 9999px;
box-shadow: 0 20px 44px -32px rgb(11 59 132 / 0.26);
min-height: 3rem;
padding: 0.45rem 0.95rem;
background: rgb(255 255 255 / 72%);
border: 1px solid rgb(255 255 255 / 84%);
border-radius: 9999px;
box-shadow: 0 20px 44px -32px rgb(11 59 132 / 26%);
backdrop-filter: blur(12px);
}
.auth-brand-name {
@@ -242,19 +282,19 @@ onBeforeUnmount(() => {
}
.auth-page-title {
max-width: 16ch;
margin: 0 auto;
font-size: clamp(2.1rem, 4vw, 3.7rem);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1.05;
margin: 0 auto;
max-width: 16ch;
letter-spacing: -0.04em;
}
.auth-stage-switcher {
align-items: center;
display: inline-flex;
flex-wrap: wrap;
gap: 0.7rem;
align-items: center;
justify-content: center;
margin-top: 1rem;
}
@@ -265,19 +305,19 @@ onBeforeUnmount(() => {
}
.auth-stage-pill {
align-items: center;
backdrop-filter: blur(10px);
background: rgb(255 255 255 / 0.74);
border: 1px solid rgb(255 255 255 / 0.9);
border-radius: 9999px;
box-shadow: 0 18px 34px -28px rgb(14 61 132 / 0.3);
color: hsl(var(--nav-item-active-foreground));
display: inline-flex;
font-size: 0.95rem;
font-weight: 600;
align-items: center;
justify-content: center;
min-width: 6.5rem;
padding: 0.55rem 1rem;
font-size: 0.95rem;
font-weight: 600;
color: hsl(var(--nav-item-active-foreground));
background: rgb(255 255 255 / 74%);
border: 1px solid rgb(255 255 255 / 90%);
border-radius: 9999px;
box-shadow: 0 18px 34px -28px rgb(14 61 132 / 30%);
backdrop-filter: blur(10px);
}
.auth-stage-pill-text {
@@ -310,13 +350,17 @@ onBeforeUnmount(() => {
.dark.auth-shell {
background:
radial-gradient(circle at top, rgb(35 66 114 / 0.32), rgb(15 22 35 / 0) 40%),
radial-gradient(
circle at top,
rgb(35 66 114 / 32%),
rgb(15 22 35 / 0%) 40%
),
linear-gradient(180deg, #06101b 0%, #08111d 52%, #09131f 100%);
.auth-shell-grid {
background-image:
linear-gradient(rgb(118 160 241 / 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgb(118 160 241 / 0.08) 1px, transparent 1px);
linear-gradient(rgb(118 160 241 / 8%) 1px, transparent 1px),
linear-gradient(90deg, rgb(118 160 241 / 8%) 1px, transparent 1px);
opacity: 0.36;
}
@@ -325,32 +369,44 @@ onBeforeUnmount(() => {
}
.auth-brand-chip {
background: rgb(11 19 31 / 0.68);
border-color: rgb(138 174 255 / 0.18);
box-shadow: 0 24px 48px -34px rgb(0 0 0 / 0.52);
background: rgb(11 19 31 / 68%);
border-color: rgb(138 174 255 / 18%);
box-shadow: 0 24px 48px -34px rgb(0 0 0 / 52%);
}
.auth-stage-switcher-label {
color: rgb(195 206 230 / 0.8);
color: rgb(195 206 230 / 80%);
}
.auth-stage-pill {
background: rgb(11 19 31 / 0.7);
border-color: rgb(138 174 255 / 0.18);
box-shadow: 0 18px 34px -28px rgb(0 0 0 / 0.46);
color: rgb(144 196 255 / 0.92);
color: rgb(144 196 255 / 92%);
background: rgb(11 19 31 / 70%);
border-color: rgb(138 174 255 / 18%);
box-shadow: 0 18px 34px -28px rgb(0 0 0 / 46%);
}
.auth-glow-primary {
background: radial-gradient(circle, rgb(70 120 255 / 0.26) 0%, rgb(70 120 255 / 0) 70%);
background: radial-gradient(
circle,
rgb(70 120 255 / 26%) 0%,
rgb(70 120 255 / 0%) 70%
);
}
.auth-glow-secondary {
background: radial-gradient(circle, rgb(41 170 201 / 0.18) 0%, rgb(41 170 201 / 0) 72%);
background: radial-gradient(
circle,
rgb(41 170 201 / 18%) 0%,
rgb(41 170 201 / 0%) 72%
);
}
.auth-glow-tertiary {
background: radial-gradient(circle, rgb(95 128 255 / 0.16) 0%, rgb(95 128 255 / 0) 74%);
background: radial-gradient(
circle,
rgb(95 128 255 / 16%) 0%,
rgb(95 128 255 / 0%) 74%
);
}
}
@@ -370,9 +426,9 @@ onBeforeUnmount(() => {
}
.auth-page-title {
margin-left: 0;
margin-right: 0;
max-width: none;
margin-right: 0;
margin-left: 0;
}
.auth-stage-switcher {

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import {computed} from 'vue';
import {useRoute} from 'vue-router';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
defineOptions({
name: 'AuthenticationFormView',
@@ -15,22 +15,23 @@ const isLoginRoute = computed(() => route.path === '/auth/login');
</script>
<template>
<div
class="auth-form-wrap relative min-h-full"
>
<div :class="['auth-window-shell', { 'auth-window-shell-plain': isLoginRoute }]">
<div class="auth-form-wrap relative min-h-full">
<div
class="auth-window-shell"
:class="[{ 'auth-window-shell-plain': isLoginRoute }]"
>
<template v-if="!isLoginRoute">
<div class="auth-window-edge auth-window-edge-top"></div>
<div class="auth-window-edge auth-window-edge-bottom"></div>
</template>
<slot></slot>
<RouterView v-slot="{ Component, route }">
<RouterView v-slot="{ Component, route: currentRoute }">
<Transition appear mode="out-in" name="slide-right">
<KeepAlive :include="['Login']">
<component
:is="Component"
:key="route.fullPath"
:key="currentRoute.fullPath"
class="side-content w-full"
:data-side="dataSide"
/>
@@ -53,49 +54,60 @@ const isLoginRoute = computed(() => route.path === '/auth/login');
}
.auth-window-shell {
backdrop-filter: blur(22px);
background:
linear-gradient(180deg, rgb(255 255 255 / 0.96) 0%, rgb(248 251 255 / 0.98) 100%);
border: 1px solid rgb(255 255 255 / 0.82);
position: relative;
padding: 1.25rem;
overflow: hidden;
background: linear-gradient(
180deg,
rgb(255 255 255 / 96%) 0%,
rgb(248 251 255 / 98%) 100%
);
border: 1px solid rgb(255 255 255 / 82%);
border-radius: 2rem;
box-shadow:
0 40px 80px -48px rgb(13 61 132 / 0.38),
0 18px 36px -26px rgb(13 61 132 / 0.18);
overflow: hidden;
padding: 1.25rem;
position: relative;
0 40px 80px -48px rgb(13 61 132 / 38%),
0 18px 36px -26px rgb(13 61 132 / 18%);
backdrop-filter: blur(22px);
}
.auth-window-shell-plain {
backdrop-filter: none;
padding: 0;
overflow: visible;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
overflow: visible;
padding: 0;
backdrop-filter: none;
}
.auth-window-edge {
border-radius: 9999px;
pointer-events: none;
position: absolute;
pointer-events: none;
border-radius: 9999px;
}
.auth-window-edge-top {
background: linear-gradient(90deg, rgb(11 111 211 / 0.18), rgb(22 159 200 / 0.08));
height: 10rem;
left: -4rem;
top: -6rem;
left: -4rem;
width: 14rem;
height: 10rem;
background: linear-gradient(
90deg,
rgb(11 111 211 / 18%),
rgb(22 159 200 / 8%)
);
}
.auth-window-edge-bottom {
background: radial-gradient(circle, rgb(84 132 255 / 0.14) 0%, rgb(84 132 255 / 0) 72%);
bottom: -4rem;
height: 11rem;
right: -4rem;
bottom: -4rem;
width: 11rem;
height: 11rem;
background: radial-gradient(
circle,
rgb(84 132 255 / 14%) 0%,
rgb(84 132 255 / 0%) 72%
);
}
.auth-copyright {
@@ -104,12 +116,15 @@ const isLoginRoute = computed(() => route.path === '/auth/login');
}
.dark .auth-window-shell {
background:
linear-gradient(180deg, rgb(10 18 30 / 0.92) 0%, rgb(9 17 29 / 0.96) 100%);
border-color: rgb(136 168 235 / 0.16);
background: linear-gradient(
180deg,
rgb(10 18 30 / 92%) 0%,
rgb(9 17 29 / 96%) 100%
);
border-color: rgb(136 168 235 / 16%);
box-shadow:
0 42px 84px -50px rgb(0 0 0 / 0.64),
0 18px 36px -28px rgb(0 0 0 / 0.42);
0 42px 84px -50px rgb(0 0 0 / 64%),
0 18px 36px -28px rgb(0 0 0 / 42%);
}
.dark .auth-window-shell-plain {
@@ -119,11 +134,19 @@ const isLoginRoute = computed(() => route.path === '/auth/login');
}
.dark .auth-window-edge-top {
background: linear-gradient(90deg, rgb(69 120 255 / 0.22), rgb(28 155 197 / 0.08));
background: linear-gradient(
90deg,
rgb(69 120 255 / 22%),
rgb(28 155 197 / 8%)
);
}
.dark .auth-window-edge-bottom {
background: radial-gradient(circle, rgb(92 136 255 / 0.16) 0%, rgb(92 136 255 / 0) 72%);
background: radial-gradient(
circle,
rgb(92 136 255 / 16%) 0%,
rgb(92 136 255 / 0%) 72%
);
}
@media (max-width: 640px) {
@@ -132,8 +155,8 @@ const isLoginRoute = computed(() => route.path === '/auth/login');
}
.auth-window-shell {
border-radius: 1.6rem;
padding: 1rem;
border-radius: 1.6rem;
}
}
</style>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type {ToolbarType} from './types';
import type { ToolbarType } from './types';
import {computed} from 'vue';
import { computed } from 'vue';
import {preferences} from '@easyflow/preferences';
import { preferences } from '@easyflow/preferences';
import {
AuthenticationColorToggle,
@@ -48,17 +48,17 @@ const showTheme = computed(() => props.toolbarList.includes('theme'));
<style scoped>
.auth-toolbar {
backdrop-filter: blur(10px);
background: rgb(255 255 255 / 0.72);
border: 1px solid rgb(255 255 255 / 0.78);
border-radius: 9999px;
box-shadow: 0 20px 42px -30px rgb(14 55 124 / 0.28);
padding: 0.25rem 0.58rem;
background: rgb(255 255 255 / 72%);
border: 1px solid rgb(255 255 255 / 78%);
border-radius: 9999px;
box-shadow: 0 20px 42px -30px rgb(14 55 124 / 28%);
backdrop-filter: blur(10px);
}
:deep(.dark) .auth-toolbar {
background: rgb(11 19 34 / 66%);
border-color: rgb(122 167 255 / 18%);
box-shadow: 0 20px 40px -28px rgb(0 0 0 / 0.46);
box-shadow: 0 20px 40px -28px rgb(0 0 0 / 46%);
}
</style>

View File

@@ -1,16 +1,17 @@
<script lang="ts" setup>
import type {VNode} from 'vue';
import {computed} from 'vue';
import type { VNode } from 'vue';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import {RouterView} from 'vue-router';
import {preferences, usePreferences} from '@easyflow/preferences';
import {getTabKey, storeToRefs, useTabbarStore} from '@easyflow/stores';
import { computed } from 'vue';
import { RouterView } from 'vue-router';
import {IFrameRouterView} from '../../iframe';
import { preferences, usePreferences } from '@easyflow/preferences';
import { getTabKey, storeToRefs, useTabbarStore } from '@easyflow/stores';
import { IFrameRouterView } from '../../iframe';
defineOptions({ name: 'LayoutContent' });

View File

@@ -1,13 +1,17 @@
<script lang="ts" setup>
import type {CSSProperties} from 'vue';
import {computed, useSlots} from 'vue';
import type { CSSProperties } from 'vue';
import {useRefresh} from '@easyflow/hooks';
import {RotateCw} from '@easyflow/icons';
import {preferences, usePreferences} from '@easyflow/preferences';
import {useAccessStore} from '@easyflow/stores';
import { computed, useSlots } from 'vue';
import {EasyFlowFullScreen, EasyFlowIconButton} from '@easyflow-core/shadcn-ui';
import { useRefresh } from '@easyflow/hooks';
import { RotateCw } from '@easyflow/icons';
import { preferences, usePreferences } from '@easyflow/preferences';
import { useAccessStore } from '@easyflow/stores';
import {
EasyFlowFullScreen,
EasyFlowIconButton,
} from '@easyflow-core/shadcn-ui';
import {
GlobalSearch,
@@ -174,16 +178,28 @@ function clearPreferencesAndLogout() {
/>
</template>
<template v-else-if="slot.name === 'theme-toggle'">
<ThemeToggle :class="toolbarButtonClass" :style="toolbarButtonStyle" />
<ThemeToggle
:class="toolbarButtonClass"
:style="toolbarButtonStyle"
/>
</template>
<template v-else-if="slot.name === 'language-toggle'">
<LanguageToggle :class="toolbarButtonClass" :style="toolbarButtonStyle" />
<LanguageToggle
:class="toolbarButtonClass"
:style="toolbarButtonStyle"
/>
</template>
<template v-else-if="slot.name === 'fullscreen'">
<EasyFlowFullScreen :class="toolbarButtonClass" :style="toolbarButtonStyle" />
<EasyFlowFullScreen
:class="toolbarButtonClass"
:style="toolbarButtonStyle"
/>
</template>
<template v-else-if="slot.name === 'timezone'">
<TimezoneButton :class="toolbarButtonClass" :style="toolbarButtonStyle" />
<TimezoneButton
:class="toolbarButtonClass"
:style="toolbarButtonStyle"
/>
</template>
</slot>
</template>

View File

@@ -1,13 +1,13 @@
import type {MenuRecordRaw} from '@easyflow/types';
import type { MenuRecordRaw } from '@easyflow/types';
import {computed, onBeforeMount, ref, watch} from 'vue';
import {useRoute} from 'vue-router';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import {preferences, usePreferences} from '@easyflow/preferences';
import {useAccessStore} from '@easyflow/stores';
import {findRootMenuByPath} from '@easyflow/utils';
import { preferences, usePreferences } from '@easyflow/preferences';
import { useAccessStore } from '@easyflow/stores';
import { findRootMenuByPath } from '@easyflow/utils';
import {useNavigation} from './use-navigation';
import { useNavigation } from './use-navigation';
function useMixedMenu() {
const { navigation, prefetch, willOpenedByWindow } = useNavigation();

View File

@@ -237,10 +237,7 @@ onMounted(() => {
class="block truncate text-sm font-medium leading-6 text-[hsl(var(--text-strong))]"
>
<template
v-for="chunk in getHighlightedChunks(
item.name || '',
keyword,
)"
v-for="chunk in getHighlightedChunks(item.name || '', keyword)"
:key="`${item.path}-${chunk.text}-${chunk.matched}`"
>
<span

View File

@@ -6,7 +6,10 @@ import { Languages } from '@easyflow/icons';
import { loadLocaleMessages } from '@easyflow/locales';
import { preferences, updatePreferences } from '@easyflow/preferences';
import { EasyFlowDropdownRadioMenu, EasyFlowIconButton } from '@easyflow-core/shadcn-ui';
import {
EasyFlowDropdownRadioMenu,
EasyFlowIconButton,
} from '@easyflow-core/shadcn-ui';
defineOptions({
name: 'LanguageToggle',

View File

@@ -13,7 +13,10 @@ import {
usePreferences,
} from '@easyflow/preferences';
import { EasyFlowDropdownRadioMenu, EasyFlowIconButton } from '@easyflow-core/shadcn-ui';
import {
EasyFlowDropdownRadioMenu,
EasyFlowIconButton,
} from '@easyflow-core/shadcn-ui';
defineOptions({
name: 'AuthenticationLayoutToggle',

View File

@@ -5,7 +5,10 @@ import { useSlots } from 'vue';
import { CircleHelp } from '@easyflow/icons';
import { EasyFlowCheckButtonGroup, EasyFlowTooltip } from '@easyflow-core/shadcn-ui';
import {
EasyFlowCheckButtonGroup,
EasyFlowTooltip,
} from '@easyflow-core/shadcn-ui';
defineOptions({
name: 'PreferenceCheckboxItem',

View File

@@ -5,7 +5,7 @@ import { useSlots } from 'vue';
import { CircleHelp, CircleX } from '@easyflow/icons';
import { Input, EasyFlowTooltip } from '@easyflow-core/shadcn-ui';
import { EasyFlowTooltip, Input } from '@easyflow-core/shadcn-ui';
defineOptions({
name: 'PreferenceSelectItem',

View File

@@ -6,12 +6,12 @@ import { useSlots } from 'vue';
import { CircleHelp } from '@easyflow/icons';
import {
EasyFlowTooltip,
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
EasyFlowTooltip,
} from '@easyflow-core/shadcn-ui';
defineOptions({

View File

@@ -6,12 +6,12 @@ import { useSlots } from 'vue';
import { CircleHelp } from '@easyflow/icons';
import {
EasyFlowTooltip,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
EasyFlowTooltip,
} from '@easyflow-core/shadcn-ui';
defineOptions({

View File

@@ -3,7 +3,7 @@ import { useSlots } from 'vue';
import { CircleHelp } from '@easyflow/icons';
import { Switch, EasyFlowTooltip } from '@easyflow-core/shadcn-ui';
import { EasyFlowTooltip, Switch } from '@easyflow-core/shadcn-ui';
defineOptions({
name: 'PreferenceSwitchItem',

View File

@@ -10,9 +10,9 @@ import {
} from '@easyflow/preferences';
import {
EasyFlowTooltip,
ToggleGroup,
ToggleGroupItem,
EasyFlowTooltip,
} from '@easyflow-core/shadcn-ui';
import ThemeButton from './theme-button.vue';

View File

@@ -7,9 +7,9 @@ import { useTimezoneStore } from '@easyflow/stores';
import { useEasyFlowModal } from '@easyflow-core/popup-ui';
import {
EasyFlowIconButton,
RadioGroup,
RadioGroupItem,
EasyFlowIconButton,
} from '@easyflow-core/shadcn-ui';
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');

View File

@@ -9,7 +9,10 @@ import type { Ref } from 'vue';
import type { ClassType, DeepPartial } from '@easyflow/types';
import type { BaseFormComponentType, EasyFlowFormProps } from '@easyflow-core/form-ui';
import type {
BaseFormComponentType,
EasyFlowFormProps,
} from '@easyflow-core/form-ui';
import type { VxeGridApi } from './api';

View File

@@ -1,6 +1,7 @@
import type { IconifyIconStructure } from '@easyflow-core/icons';
import { addIcon } from '@easyflow-core/icons';
import chatHistorySvg from './icons/chat-history.svg?raw';
let loaded = false;

View File

@@ -13,8 +13,8 @@
}
},
"dependencies": {
"@intlify/core-base": "catalog:",
"@easyflow-core/composables": "workspace:*",
"@intlify/core-base": "catalog:",
"vue": "catalog:",
"vue-i18n": "catalog:"
}

View File

@@ -63,13 +63,13 @@ html .el-loading-mask {
}
.el-table td.el-table__cell {
box-shadow: inset 0px -1px 0px 0px var(--table-cell-shadow-color);
box-shadow: inset 0 -1px 0 0 var(--table-cell-shadow-color);
}
.el-table--border .el-table__inner-wrapper:after,
.el-table--border:after,
.el-table--border:before,
.el-table__inner-wrapper:before {
.el-table--border .el-table__inner-wrapper::after,
.el-table--border::after,
.el-table--border::before,
.el-table__inner-wrapper::before {
--el-table-border-color: transparent;
}
@@ -84,13 +84,13 @@ html .el-loading-mask {
}
.el-button {
--el-color-primary: hsl(var(--primary))
--el-color-primary: hsl(var(--primary));
}
.el-tooltip {
max-width: 50vw;
}
.el-table .el-button.is-link>span {
.el-table .el-button.is-link > span {
font-weight: 400;
}

View File

@@ -1,5 +1,5 @@
module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': ['off']
}
rules: {
'@typescript-eslint/no-explicit-any': ['off'],
},
};

View File

@@ -1,157 +1,163 @@
import type {useSvelteFlow} from '@xyflow/svelte';
import {componentName} from './consts';
import type {TinyflowData, TinyflowOptions, TinyflowTheme} from './types';
import type { useSvelteFlow } from '@xyflow/svelte';
import { componentName } from './consts';
import type { TinyflowData, TinyflowOptions, TinyflowTheme } from './types';
type FlowInstance = ReturnType<typeof useSvelteFlow>;
export class Tinyflow {
private options!: TinyflowOptions;
private rootEl!: Element;
private svelteFlowInstance!: FlowInstance;
private tinyflowEl!: HTMLElement & {
options: TinyflowOptions;
onInit: (svelteFlowInstance: FlowInstance) => void;
private options!: TinyflowOptions;
private rootEl!: Element;
private svelteFlowInstance!: FlowInstance;
private tinyflowEl!: HTMLElement & {
options: TinyflowOptions;
onInit: (svelteFlowInstance: FlowInstance) => void;
};
constructor(options: TinyflowOptions) {
if (
typeof options.element !== 'string' &&
!(options.element instanceof Element)
) {
throw new Error('element must be a string or Element');
}
this._setOptions(options);
this._init();
}
private _init() {
if (typeof this.options.element === 'string') {
this.rootEl = document.querySelector(this.options.element)!;
if (!this.rootEl) {
throw new Error(
`element not found by document.querySelector('${this.options.element}')`,
);
}
} else if (this.options.element instanceof Element) {
this.rootEl = this.options.element;
} else {
throw new Error('element must be a string or Element');
}
this.tinyflowEl = this._createTinyflowElement();
this.rootEl.appendChild(this.tinyflowEl);
}
private _setOptions(options: TinyflowOptions) {
this.options = {
theme: options.theme || 'light',
...options,
};
}
constructor(options: TinyflowOptions) {
if (typeof options.element !== 'string' && !(options.element instanceof Element)) {
throw new Error('element must be a string or Element');
}
this._setOptions(options);
this._init();
private _getFlowInstance() {
if (!this.svelteFlowInstance) {
console.warn('Tinyflow instance is not initialized');
return null;
}
return this.svelteFlowInstance;
}
private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) {
targetEl.classList.remove('tf-theme-light', 'tf-theme-dark');
targetEl.classList.add(
theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light',
);
}
private _createTinyflowElement() {
const tinyflowEl = document.createElement(componentName) as HTMLElement & {
options: TinyflowOptions;
onInit: (svelteFlowInstance: FlowInstance) => void;
};
tinyflowEl.style.display = 'block';
tinyflowEl.style.width = '100%';
tinyflowEl.style.height = '100%';
this._applyThemeClass(tinyflowEl, this.options.theme);
tinyflowEl.options = this.options;
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
this.svelteFlowInstance = svelteFlowInstance;
};
return tinyflowEl;
}
getOptions() {
return this.options;
}
getData() {
const flow = this._getFlowInstance();
if (!flow) {
return null;
}
return flow.toObject();
}
async focusNode(
nodeId: string,
options?: { duration?: number; zoom?: number },
) {
const flow = this._getFlowInstance();
if (!flow) {
return false;
}
private _init() {
if (typeof this.options.element === 'string') {
this.rootEl = document.querySelector(this.options.element)!;
if (!this.rootEl) {
throw new Error(
`element not found by document.querySelector('${this.options.element}')`
);
}
} else if (this.options.element instanceof Element) {
this.rootEl = this.options.element;
} else {
throw new Error('element must be a string or Element');
}
this.tinyflowEl = this._createTinyflowElement();
this.rootEl.appendChild(this.tinyflowEl);
const targetNode = flow.getNode(nodeId);
if (!targetNode) {
return false;
}
private _setOptions(options: TinyflowOptions) {
this.options = {
theme: options.theme || 'light',
...options
};
// Keep only the target node selected so the canvas has a clear visual focus.
flow.getNodes().forEach((node) => {
flow.updateNode(node.id, { selected: node.id === nodeId });
});
const internalNode = flow.getInternalNode(nodeId) as any;
const absolutePosition = internalNode?.internals?.positionAbsolute ||
(targetNode as any)?.positionAbsolute ||
targetNode.position || { x: 0, y: 0 };
const width =
internalNode?.measured?.width ||
(targetNode as any)?.measured?.width ||
(targetNode as any)?.width ||
260;
const height =
internalNode?.measured?.height ||
(targetNode as any)?.measured?.height ||
(targetNode as any)?.height ||
120;
const centerX = absolutePosition.x + width / 2;
const centerY = absolutePosition.y + height / 2;
const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1);
await flow.setCenter(centerX, centerY, {
zoom: nextZoom,
duration: options?.duration ?? 280,
});
return true;
}
setTheme(theme: TinyflowTheme) {
this.options.theme = theme;
if (this.tinyflowEl) {
this._applyThemeClass(this.tinyflowEl, theme);
}
}
private _getFlowInstance() {
if (!this.svelteFlowInstance) {
console.warn('Tinyflow instance is not initialized');
return null;
}
return this.svelteFlowInstance;
}
private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) {
targetEl.classList.remove('tf-theme-light', 'tf-theme-dark');
targetEl.classList.add(theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light');
}
private _createTinyflowElement() {
const tinyflowEl = document.createElement(componentName) as HTMLElement & {
options: TinyflowOptions;
onInit: (svelteFlowInstance: FlowInstance) => void;
};
tinyflowEl.style.display = 'block';
tinyflowEl.style.width = '100%';
tinyflowEl.style.height = '100%';
this._applyThemeClass(tinyflowEl, this.options.theme);
tinyflowEl.options = this.options;
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
this.svelteFlowInstance = svelteFlowInstance;
};
return tinyflowEl;
}
getOptions() {
return this.options;
}
getData() {
const flow = this._getFlowInstance();
if (!flow) {
return null;
}
return flow.toObject();
}
async focusNode(nodeId: string, options?: { duration?: number; zoom?: number }) {
const flow = this._getFlowInstance();
if (!flow) {
return false;
}
const targetNode = flow.getNode(nodeId);
if (!targetNode) {
return false;
}
// Keep only the target node selected so the canvas has a clear visual focus.
flow.getNodes().forEach((node) => {
flow.updateNode(node.id, { selected: node.id === nodeId });
});
const internalNode = flow.getInternalNode(nodeId) as any;
const absolutePosition =
internalNode?.internals?.positionAbsolute ||
(targetNode as any)?.positionAbsolute ||
targetNode.position ||
{ x: 0, y: 0 };
const width =
internalNode?.measured?.width ||
(targetNode as any)?.measured?.width ||
(targetNode as any)?.width ||
260;
const height =
internalNode?.measured?.height ||
(targetNode as any)?.measured?.height ||
(targetNode as any)?.height ||
120;
const centerX = absolutePosition.x + width / 2;
const centerY = absolutePosition.y + height / 2;
const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1);
await flow.setCenter(centerX, centerY, {
zoom: nextZoom,
duration: options?.duration ?? 280
});
return true;
}
setTheme(theme: TinyflowTheme) {
this.options.theme = theme;
if (this.tinyflowEl) {
this._applyThemeClass(this.tinyflowEl, theme);
}
}
setData(data: TinyflowData) {
this.options.data = data;
this.tinyflowEl = this._createTinyflowElement();
this.destroy();
this.rootEl.appendChild(this.tinyflowEl);
}
destroy() {
while (this.rootEl.firstChild) {
this.rootEl.removeChild(this.rootEl.firstChild);
}
setData(data: TinyflowData) {
this.options.data = data;
this.tinyflowEl = this._createTinyflowElement();
this.destroy();
this.rootEl.appendChild(this.tinyflowEl);
}
destroy() {
while (this.rootEl.firstChild) {
this.rootEl.removeChild(this.rootEl.firstChild);
}
}
}

View File

@@ -13,17 +13,17 @@ import MenuButton from './menu-button.svelte';
import MixedInput from './mixed-input.svelte';
export {
Button,
Checkbox,
Chosen,
Input,
Textarea,
Tabs,
Collapse,
Render,
Select,
FloatingTrigger,
Heading,
MenuButton,
MixedInput
Button,
Checkbox,
Chosen,
Input,
Textarea,
Tabs,
Collapse,
Render,
Select,
FloatingTrigger,
Heading,
MenuButton,
MixedInput,
};

View File

@@ -39,7 +39,7 @@
{#snippet handle()}
<Handle type="source" position={Position.Bottom} id="loop_handle" style="bottom: -12px;width: 100px"
class="loop_handle_wrapper" />
class="loop-handle-wrapper" />
{/snippet}
<div class="heading">
@@ -75,7 +75,7 @@
align-items: center;
}
:global(.loop_handle_wrapper ) {
:global(.loop-handle-wrapper) {
&::after {
//display: none;
content: '循环体';
@@ -92,4 +92,3 @@
}
</style>

View File

@@ -1,4 +1,4 @@
import type {NodeTypes} from '@xyflow/svelte';
import type { NodeTypes } from '@xyflow/svelte';
import StartNode from './StartNode.svelte';
import EndNode from './EndNode.svelte';
import LLMNode from './LLMNode.svelte';
@@ -15,15 +15,15 @@ import ConditionNode from './ConditionNode.svelte';
* @description 节点类型en: node type
*/
export const nodeTypes = {
startNode: StartNode,
codeNode: CodeNode,
confirmNode: ConfirmNode,
llmNode: LLMNode,
templateNode: TemplateNode,
httpNode: HttpNode,
knowledgeNode: KnowledgeNode,
searchEngineNode: SearchEngineNode,
loopNode: LoopNode,
conditionNode: ConditionNode,
endNode: EndNode
startNode: StartNode,
codeNode: CodeNode,
confirmNode: ConfirmNode,
llmNode: LLMNode,
templateNode: TemplateNode,
httpNode: HttpNode,
knowledgeNode: KnowledgeNode,
searchEngineNode: SearchEngineNode,
loopNode: LoopNode,
conditionNode: ConditionNode,
endNode: EndNode,
} as any as NodeTypes;

View File

@@ -1,12 +1,16 @@
export const genShortId = (length = 16) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
};
export const genUuid = () => {
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16),
);
};

View File

@@ -1,26 +1,26 @@
import {getContext} from 'svelte';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import type {TinyflowOptions} from '#types';
import { getContext } from 'svelte';
import { useNodesData, useSvelteFlow } from '@xyflow/svelte';
import type { TinyflowOptions } from '#types';
export const getCurrentNodeId = () => {
return getContext<string>('svelteflow__node_id');
return getContext<string>('svelteflow__node_id');
};
export const getOptions = () => {
return getContext<TinyflowOptions>('tinyflow_options');
return getContext<TinyflowOptions>('tinyflow_options');
};
export const useCurrentNodeData = () => {
const currentNodeId = getCurrentNodeId();
return useNodesData<any>(currentNodeId);
const currentNodeId = getCurrentNodeId();
return useNodesData<any>(currentNodeId);
};
export const useUpdateNodeData = () => {
const { updateNodeData } = useSvelteFlow();
const currentNodeId = getCurrentNodeId();
return {
updateNodeData: (data: any) => {
updateNodeData(currentNodeId, data);
}
};
const { updateNodeData } = useSvelteFlow();
const currentNodeId = getCurrentNodeId();
return {
updateNodeData: (data: any) => {
updateNodeData(currentNodeId, data);
},
};
};

View File

@@ -1,83 +1,109 @@
import { describe, expect, it } from 'vitest';
import {
createBusinessCompletions,
normalizeCodeEngine,
resolveBusinessCompletionContext,
shouldAutoAppendCallParens,
shouldSkipBusinessCompletion
createBusinessCompletions,
normalizeCodeEngine,
resolveBusinessCompletionContext,
shouldAutoAppendCallParens,
shouldSkipBusinessCompletion,
} from './codeCompletion';
describe('codeCompletion utils', () => {
it('should normalize engine aliases', () => {
expect(normalizeCodeEngine('py')).toBe('python');
expect(normalizeCodeEngine('python')).toBe('python');
expect(normalizeCodeEngine('js')).toBe('javascript');
expect(normalizeCodeEngine('javascript')).toBe('javascript');
expect(normalizeCodeEngine('')).toBe('javascript');
it('should normalize engine aliases', () => {
expect(normalizeCodeEngine('py')).toBe('python');
expect(normalizeCodeEngine('python')).toBe('python');
expect(normalizeCodeEngine('js')).toBe('javascript');
expect(normalizeCodeEngine('javascript')).toBe('javascript');
expect(normalizeCodeEngine('')).toBe('javascript');
});
it('should create business completion list with stable priority and dedupe', () => {
const completions = createBusinessCompletions('python', [
{ name: 'input.text', resolved: true },
{ name: 'input.text', resolved: false },
{ name: 'question', resolved: false },
]);
expect(completions[0]).toMatchObject({
label: '_result',
type: 'variable',
});
it('should create business completion list with stable priority and dedupe', () => {
const completions = createBusinessCompletions('python', [
{ name: 'input.text', resolved: true },
{ name: 'input.text', resolved: false },
{ name: 'question', resolved: false }
]);
expect(completions[0]).toMatchObject({
label: '_result',
type: 'variable'
});
expect(completions.filter((item) => item.label === 'input.text').length).toBe(1);
expect(completions.find((item) => item.label === 'input.text')).toMatchObject({
apply: '{{input.text}}',
detail: '参数模板'
});
expect(completions.find((item) => item.label === 'question')).toMatchObject({
apply: '{{question}}',
detail: '参数模板(未映射)'
});
expect(completions.some((item) => item.type === 'snippet' && item.label === 'if-else')).toBe(true);
expect(
completions.filter((item) => item.label === 'input.text').length,
).toBe(1);
expect(
completions.find((item) => item.label === 'input.text'),
).toMatchObject({
apply: '{{input.text}}',
detail: '参数模板',
});
expect(completions.find((item) => item.label === 'question')).toMatchObject(
{
apply: '{{question}}',
detail: '参数模板(未映射)',
},
);
it('should detect blocked nodes for comment/string/property contexts', () => {
expect(shouldSkipBusinessCompletion('Comment')).toBe(true);
expect(shouldSkipBusinessCompletion('LineComment')).toBe(true);
expect(shouldSkipBusinessCompletion('String')).toBe(true);
expect(shouldSkipBusinessCompletion('TemplateString')).toBe(true);
expect(shouldSkipBusinessCompletion('PropertyName')).toBe(true);
expect(shouldSkipBusinessCompletion('VariableName')).toBe(false);
});
expect(
completions.some(
(item) => item.type === 'snippet' && item.label === 'if-else',
),
).toBe(true);
});
it('should resolve template context for parameter token completion', () => {
const source = "value = {{input.use";
const pos = source.length;
const context = resolveBusinessCompletionContext(source, pos);
expect(context).toBeTruthy();
expect(context?.from).toBe(8);
expect(context?.validFor).toBeInstanceOf(RegExp);
expect(context?.validFor.source).toBe('^[\\w.]*$');
});
it('should detect blocked nodes for comment/string/property contexts', () => {
expect(shouldSkipBusinessCompletion('Comment')).toBe(true);
expect(shouldSkipBusinessCompletion('LineComment')).toBe(true);
expect(shouldSkipBusinessCompletion('String')).toBe(true);
expect(shouldSkipBusinessCompletion('TemplateString')).toBe(true);
expect(shouldSkipBusinessCompletion('PropertyName')).toBe(true);
expect(shouldSkipBusinessCompletion('VariableName')).toBe(false);
});
it('should resolve word context and skip property access context', () => {
const source = '_result.value';
expect(resolveBusinessCompletionContext(source, source.length)).toBeNull();
it('should resolve template context for parameter token completion', () => {
const source = 'value = {{input.use';
const pos = source.length;
const context = resolveBusinessCompletionContext(source, pos);
expect(context).toBeTruthy();
expect(context?.from).toBe(8);
expect(context?.validFor).toBeInstanceOf(RegExp);
expect(context?.validFor.source).toBe('^[\\w.]*$');
});
const wordSource = 'ret';
const context = resolveBusinessCompletionContext(wordSource, wordSource.length);
expect(context).toBeTruthy();
expect(context?.from).toBe(0);
expect(context?.validFor).toBeInstanceOf(RegExp);
expect(context?.validFor.source).toBe('^[\\w$]*$');
});
it('should resolve word context and skip property access context', () => {
const source = '_result.value';
expect(resolveBusinessCompletionContext(source, source.length)).toBeNull();
it('should auto append call parens for function/method completions only', () => {
expect(shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '')).toBe(true);
expect(shouldAutoAppendCallParens({ label: 'run', type: 'method' }, '')).toBe(true);
expect(shouldAutoAppendCallParens({ label: 'count', type: 'variable' }, '')).toBe(false);
expect(shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '(')).toBe(false);
expect(shouldAutoAppendCallParens({ label: 'call', type: 'function', apply: 'call()' }, '')).toBe(false);
});
const wordSource = 'ret';
const context = resolveBusinessCompletionContext(
wordSource,
wordSource.length,
);
expect(context).toBeTruthy();
expect(context?.from).toBe(0);
expect(context?.validFor).toBeInstanceOf(RegExp);
expect(context?.validFor.source).toBe('^[\\w$]*$');
});
it('should auto append call parens for function/method completions only', () => {
expect(
shouldAutoAppendCallParens({ label: 'print', type: 'function' }, ''),
).toBe(true);
expect(
shouldAutoAppendCallParens({ label: 'run', type: 'method' }, ''),
).toBe(true);
expect(
shouldAutoAppendCallParens({ label: 'count', type: 'variable' }, ''),
).toBe(false);
expect(
shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '('),
).toBe(false);
expect(
shouldAutoAppendCallParens(
{ label: 'call', type: 'function', apply: 'call()' },
'',
),
).toBe(false);
});
});

View File

@@ -1,217 +1,245 @@
import type { Completion, CompletionResult, CompletionSource } from '@codemirror/autocomplete';
import type {
Completion,
CompletionResult,
CompletionSource,
} from '@codemirror/autocomplete';
import { syntaxTree } from '@codemirror/language';
import type { ParameterCandidate } from './paramToken';
export type CodeEngine = 'javascript' | 'python';
export interface BusinessCompletionConfig {
engine: CodeEngine;
paramCandidates: ParameterCandidate[];
engine: CodeEngine;
paramCandidates: ParameterCandidate[];
}
interface CompletionContextHint {
from: number;
validFor: RegExp;
from: number;
validFor: RegExp;
}
const BLOCKED_NODE_NAMES = new Set([
'String',
'TemplateString',
'FormatString',
'Comment',
'LineComment',
'BlockComment',
'PropertyName',
'PrivatePropertyName'
'String',
'TemplateString',
'FormatString',
'Comment',
'LineComment',
'BlockComment',
'PropertyName',
'PrivatePropertyName',
]);
const JS_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
{
label: 'if-else',
detail: '条件分支',
insert: "if (condition) {\n _result.value = value;\n} else {\n _result.value = null;\n}"
},
{
label: 'for-of',
detail: '遍历数组',
insert: "for (const item of items) {\n // TODO\n}\n_result.done = true;"
},
{
label: 'result-object',
detail: '返回对象',
insert: "_result.message = 'ok';\n_result.data = data;"
}
{
label: 'if-else',
detail: '条件分支',
insert:
'if (condition) {\n _result.value = value;\n} else {\n _result.value = null;\n}',
},
{
label: 'for-of',
detail: '遍历数组',
insert: 'for (const item of items) {\n // TODO\n}\n_result.done = true;',
},
{
label: 'result-object',
detail: '返回对象',
insert: "_result.message = 'ok';\n_result.data = data;",
},
];
const PYTHON_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
{
label: 'if-else',
detail: '条件分支',
insert: "if condition:\n _result['value'] = value\nelse:\n _result['value'] = None"
},
{
label: 'for-loop',
detail: '遍历数组',
insert: "for item in items:\n # TODO\n pass\n_result['done'] = True"
},
{
label: 'result-object',
detail: '返回对象',
insert: "_result['message'] = 'ok'\n_result['data'] = data"
}
const PYTHON_SNIPPETS: Array<{
label: string;
insert: string;
detail: string;
}> = [
{
label: 'if-else',
detail: '条件分支',
insert:
"if condition:\n _result['value'] = value\nelse:\n _result['value'] = None",
},
{
label: 'for-loop',
detail: '遍历数组',
insert: "for item in items:\n # TODO\n pass\n_result['done'] = True",
},
{
label: 'result-object',
detail: '返回对象',
insert: "_result['message'] = 'ok'\n_result['data'] = data",
},
];
export function normalizeCodeEngine(rawEngine?: string): CodeEngine {
const normalized = (rawEngine || 'js').trim().toLowerCase();
if (normalized === 'python' || normalized === 'py') {
return 'python';
}
return 'javascript';
const normalized = (rawEngine || 'js').trim().toLowerCase();
if (normalized === 'python' || normalized === 'py') {
return 'python';
}
return 'javascript';
}
export function shouldSkipBusinessCompletion(nodeName: string): boolean {
if (!nodeName) {
return false;
}
if (BLOCKED_NODE_NAMES.has(nodeName)) {
return true;
}
if (nodeName.endsWith('Comment')) {
return true;
}
return nodeName.includes('String');
if (!nodeName) {
return false;
}
if (BLOCKED_NODE_NAMES.has(nodeName)) {
return true;
}
if (nodeName.endsWith('Comment')) {
return true;
}
return nodeName.includes('String');
}
export function createBusinessCompletions(
targetEngine: CodeEngine,
candidates: ParameterCandidate[]
targetEngine: CodeEngine,
candidates: ParameterCandidate[],
): Completion[] {
const resultCompletion: Completion = {
label: '_result',
type: 'variable',
detail: '代码节点输出对象',
boost: 900
};
const resultCompletion: Completion = {
label: '_result',
type: 'variable',
detail: '代码节点输出对象',
boost: 900,
};
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
label: candidate.name,
type: 'variable',
detail: candidate.resolved ? '参数模板' : '参数模板(未映射)',
apply: `{{${candidate.name}}}`,
boost: 700
}));
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
label: candidate.name,
type: 'variable',
detail: candidate.resolved ? '参数模板' : '参数模板(未映射)',
apply: `{{${candidate.name}}}`,
boost: 700,
}));
const snippetCompletions: Completion[] = (targetEngine === 'python' ? PYTHON_SNIPPETS : JS_SNIPPETS).map(
(snippet) => ({
label: snippet.label,
type: 'snippet',
detail: snippet.detail,
apply: snippet.insert,
boost: 500
})
);
const snippetCompletions: Completion[] = (
targetEngine === 'python' ? PYTHON_SNIPPETS : JS_SNIPPETS
).map((snippet) => ({
label: snippet.label,
type: 'snippet',
detail: snippet.detail,
apply: snippet.insert,
boost: 500,
}));
return dedupeCompletions([resultCompletion, ...parameterCompletions, ...snippetCompletions]);
return dedupeCompletions([
resultCompletion,
...parameterCompletions,
...snippetCompletions,
]);
}
function dedupeCompletions(items: Completion[]): Completion[] {
const seen = new Set<string>();
const deduped: Completion[] = [];
for (const item of items) {
const key = `${item.label}::${item.type || ''}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
deduped.push(item);
const seen = new Set<string>();
const deduped: Completion[] = [];
for (const item of items) {
const key = `${item.label}::${item.type || ''}`;
if (seen.has(key)) {
continue;
}
return deduped;
seen.add(key);
deduped.push(item);
}
return deduped;
}
function resolveTemplateContext(source: string, pos: number): CompletionContextHint | null {
const start = Math.max(0, pos - 300);
const prefix = source.slice(start, pos);
const match = /\{\{\s*([\w.]*)$/.exec(prefix);
if (!match) {
return null;
function resolveTemplateContext(
source: string,
pos: number,
): CompletionContextHint | null {
const start = Math.max(0, pos - 300);
const prefix = source.slice(start, pos);
const match = /\{\{\s*([\w.]*)$/.exec(prefix);
if (!match) {
return null;
}
return {
from: pos - match[0].length,
validFor: /^[\w.]*$/,
};
}
function resolveWordContext(
source: string,
pos: number,
): CompletionContextHint | null {
let from = pos;
while (from > 0 && /[\w$]/.test(source[from - 1])) {
from -= 1;
}
if (from === pos) {
return null;
}
if (from > 0 && source[from - 1] === '.') {
return null;
}
return {
from,
validFor: /^[\w$]*$/,
};
}
export function resolveBusinessCompletionContext(
source: string,
pos: number,
): CompletionContextHint | null {
return resolveTemplateContext(source, pos) || resolveWordContext(source, pos);
}
export function createBusinessCompletionSource(
config: BusinessCompletionConfig,
): CompletionSource {
return (context) => {
const node = syntaxTree(context.state).resolveInner(context.pos, -1);
if (shouldSkipBusinessCompletion(node.name)) {
return null;
}
return {
from: pos - match[0].length,
validFor: /^[\w.]*$/
const docText = context.state.doc.toString();
const resolved = resolveBusinessCompletionContext(docText, context.pos);
if (!resolved && !context.explicit) {
return null;
}
const result: CompletionResult = {
from: resolved?.from ?? context.pos,
options: createBusinessCompletions(config.engine, config.paramCandidates),
};
if (resolved?.validFor) {
result.validFor = resolved.validFor;
}
return result;
};
}
function resolveWordContext(source: string, pos: number): CompletionContextHint | null {
let from = pos;
while (from > 0 && /[\w$]/.test(source[from - 1])) {
from -= 1;
}
if (from === pos) {
return null;
}
export function shouldAutoAppendCallParens(
completion: Completion | null | undefined,
nextChar: string,
): boolean {
if (!completion) {
return false;
}
if (from > 0 && source[from - 1] === '.') {
return null;
}
const completionType = (completion.type || '').toLowerCase();
if (completionType !== 'function' && completionType !== 'method') {
return false;
}
return {
from,
validFor: /^[\w$]*$/
};
}
export function resolveBusinessCompletionContext(source: string, pos: number): CompletionContextHint | null {
return resolveTemplateContext(source, pos) || resolveWordContext(source, pos);
}
export function createBusinessCompletionSource(config: BusinessCompletionConfig): CompletionSource {
return (context) => {
const node = syntaxTree(context.state).resolveInner(context.pos, -1);
if (shouldSkipBusinessCompletion(node.name)) {
return null;
}
const docText = context.state.doc.toString();
const resolved = resolveBusinessCompletionContext(docText, context.pos);
if (!resolved && !context.explicit) {
return null;
}
const result: CompletionResult = {
from: resolved?.from ?? context.pos,
options: createBusinessCompletions(config.engine, config.paramCandidates)
};
if (resolved?.validFor) {
result.validFor = resolved.validFor;
}
return result;
};
}
export function shouldAutoAppendCallParens(completion: Completion | null | undefined, nextChar: string): boolean {
if (!completion) {
return false;
}
const completionType = (completion.type || '').toLowerCase();
if (completionType !== 'function' && completionType !== 'method') {
return false;
}
if (nextChar === '(') {
return false;
}
if (typeof completion.apply === 'string' && completion.apply.includes('(')) {
return false;
}
if (typeof completion.apply === 'function') {
return false;
}
return true;
if (nextChar === '(') {
return false;
}
if (typeof completion.apply === 'string' && completion.apply.includes('(')) {
return false;
}
if (typeof completion.apply === 'function') {
return false;
}
return true;
}

View File

@@ -7,212 +7,226 @@ import {
type Placement,
shift,
type ShiftOptions,
size
size,
} from '@floating-ui/dom';
export type FloatingOptions = {
trigger: string | HTMLElement;
triggerEvent?: string[];
floatContent: string | HTMLElement;
placement?: Placement;
offsetOptions?: OffsetOptions;
flipOptions?: FlipOptions;
shiftOptions?: ShiftOptions;
interactive?: boolean;
showArrow?: boolean;
onShow?: () => void;
onHide?: () => void;
syncWidth?: boolean;
syncWidthMode?: 'min' | 'equal';
trigger: string | HTMLElement;
triggerEvent?: string[];
floatContent: string | HTMLElement;
placement?: Placement;
offsetOptions?: OffsetOptions;
flipOptions?: FlipOptions;
shiftOptions?: ShiftOptions;
interactive?: boolean;
showArrow?: boolean;
onShow?: () => void;
onHide?: () => void;
syncWidth?: boolean;
syncWidthMode?: 'min' | 'equal';
};
export type FloatingInstance = {
destroy: () => void;
hide: () => void;
isVisible: () => boolean;
destroy: () => void;
hide: () => void;
isVisible: () => boolean;
};
export const createFloating = ({
trigger,
triggerEvent,
floatContent,
placement = 'bottom',
offsetOptions,
flipOptions,
shiftOptions,
interactive,
showArrow,
onShow,
onHide,
syncWidth = false,
syncWidthMode = 'min'
trigger,
triggerEvent,
floatContent,
placement = 'bottom',
offsetOptions,
flipOptions,
shiftOptions,
interactive,
showArrow,
onShow,
onHide,
syncWidth = false,
syncWidthMode = 'min',
}: FloatingOptions): FloatingInstance => {
if (typeof trigger === 'string') {
const triggerEl = document.querySelector(trigger);
if (!triggerEl) {
throw new Error("element not found by document.querySelector('" + trigger + "')");
} else {
trigger = triggerEl as HTMLElement;
}
}
let floating: HTMLElement;
if (typeof floatContent === 'string') {
const floatContentEl = document.querySelector(floatContent);
if (!floatContentEl) {
throw new Error("element not found by document.querySelector('" + floatContent + "')");
} else {
floating = floatContentEl as HTMLElement;
}
if (typeof trigger === 'string') {
const triggerEl = document.querySelector(trigger);
if (!triggerEl) {
throw new Error(
"element not found by document.querySelector('" + trigger + "')",
);
} else {
floating = floatContent as HTMLElement;
trigger = triggerEl as HTMLElement;
}
}
let arrowElement: HTMLElement;
if (showArrow) {
arrowElement = document.createElement('div');
arrowElement.style.position = 'absolute';
arrowElement.style.backgroundColor = '#222';
arrowElement.style.width = '8px';
arrowElement.style.height = '8px';
arrowElement.style.transform = 'rotate(45deg)';
arrowElement.style.display = 'none';
floating.firstElementChild!.before(arrowElement);
let floating: HTMLElement;
if (typeof floatContent === 'string') {
const floatContentEl = document.querySelector(floatContent);
if (!floatContentEl) {
throw new Error(
"element not found by document.querySelector('" + floatContent + "')",
);
} else {
floating = floatContentEl as HTMLElement;
}
} else {
floating = floatContent as HTMLElement;
}
function updatePosition() {
computePosition(trigger as Element, floating, {
placement: placement,
middleware: [
offset(offsetOptions), // 手动偏移配置
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : []),
...(syncWidth ? [size({
apply({ rects, elements }) {
if (syncWidthMode === 'equal') {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
minWidth: `${rects.reference.width}px`
});
} else {
Object.assign(elements.floating.style, {
width: '',
minWidth: `${rects.reference.width}px`
});
}
}
})] : [])
let arrowElement: HTMLElement;
if (showArrow) {
arrowElement = document.createElement('div');
arrowElement.style.position = 'absolute';
arrowElement.style.backgroundColor = '#222';
arrowElement.style.width = '8px';
arrowElement.style.height = '8px';
arrowElement.style.transform = 'rotate(45deg)';
arrowElement.style.display = 'none';
floating.firstElementChild!.before(arrowElement);
}
function updatePosition() {
computePosition(trigger as Element, floating, {
placement: placement,
middleware: [
offset(offsetOptions), // 手动偏移配置
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : []),
...(syncWidth
? [
size({
apply({ rects, elements }) {
if (syncWidthMode === 'equal') {
Object.assign(elements.floating.style, {
width: `${rects.reference.width}px`,
minWidth: `${rects.reference.width}px`,
});
} else {
Object.assign(elements.floating.style, {
width: '',
minWidth: `${rects.reference.width}px`,
});
}
},
}),
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`,
position: 'absolute'
});
: []),
],
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`,
position: 'absolute',
});
if (showArrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow as { x: number; y: number };
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right'
}[placement.split('-')[0]] as string;
if (showArrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow as {
x: number;
y: number;
};
const staticSide = {
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
}[placement.split('-')[0]] as string;
Object.assign(arrowElement.style, {
zIndex: -1,
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '2px'
});
}
Object.assign(arrowElement.style, {
zIndex: -1,
left: arrowX != null ? `${arrowX}px` : '',
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: '2px',
});
}
let visible = false;
function showTooltip() {
floating.style.display = 'block';
floating.style.visibility = 'visible';
floating.style.position = 'absolute';
if (showArrow) {
arrowElement.style.display = 'block';
}
visible = true;
updatePosition();
onShow?.();
}
function hideTooltip() {
floating.style.display = 'none';
if (showArrow) {
arrowElement.style.display = 'none';
}
visible = false;
onHide?.();
}
function onTrigger(event: any) {
if (!visible) {
showTooltip();
} else {
hideTooltip();
}
}
function hideTooltipCompute(event: any) {
if (floating.contains(event.target as Node) || (trigger as Node).contains(event.target as Node)) {
return;
}
hideTooltip();
}
if (!triggerEvent || triggerEvent.length == 0) {
if (interactive) {
triggerEvent = ['click'];
} else {
triggerEvent = ['mouseenter', 'focus'];
}
}
triggerEvent.forEach((event) => {
(trigger as HTMLElement).addEventListener(event, onTrigger);
}
});
}
if (interactive) {
document.addEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.addEventListener(event, hideTooltip);
});
let visible = false;
function showTooltip() {
floating.style.display = 'block';
floating.style.visibility = 'visible';
floating.style.position = 'absolute';
if (showArrow) {
arrowElement.style.display = 'block';
}
return {
destroy() {
triggerEvent.forEach((event) => {
(trigger as HTMLElement).removeEventListener(event, onTrigger);
});
visible = true;
updatePosition();
onShow?.();
}
if (interactive) {
document.removeEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.removeEventListener(event, hideTooltip);
});
}
},
hide() {
hideTooltip();
},
function hideTooltip() {
floating.style.display = 'none';
if (showArrow) {
arrowElement.style.display = 'none';
}
visible = false;
onHide?.();
}
isVisible() {
return visible;
}
};
function onTrigger(event: any) {
if (!visible) {
showTooltip();
} else {
hideTooltip();
}
}
function hideTooltipCompute(event: any) {
if (
floating.contains(event.target as Node) ||
(trigger as Node).contains(event.target as Node)
) {
return;
}
hideTooltip();
}
if (!triggerEvent || triggerEvent.length == 0) {
if (interactive) {
triggerEvent = ['click'];
} else {
triggerEvent = ['mouseenter', 'focus'];
}
}
triggerEvent.forEach((event) => {
(trigger as HTMLElement).addEventListener(event, onTrigger);
});
if (interactive) {
document.addEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.addEventListener(event, hideTooltip);
});
}
return {
destroy() {
triggerEvent.forEach((event) => {
(trigger as HTMLElement).removeEventListener(event, onTrigger);
});
if (interactive) {
document.removeEventListener('click', hideTooltipCompute);
} else {
['mouseleave', 'blur'].forEach((event) => {
trigger.removeEventListener(event, hideTooltip);
});
}
},
hide() {
hideTooltip();
},
isVisible() {
return visible;
},
};
};

View File

@@ -1,3 +1,3 @@
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
return JSON.parse(JSON.stringify(obj));
};

View File

@@ -1,37 +1,42 @@
export const deepEqual = <T>(obj1: T, obj2: T) => {
if (obj1 === obj2) return true;
if (obj1 === obj2) return true;
// 处理 null 和 object 类型
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
// 处理 null 和 object 类型
if (
typeof obj1 !== 'object' ||
obj1 === null ||
typeof obj2 !== 'object' ||
obj2 === null
) {
return false;
}
// 判断是否都是数组
const isArray1 = Array.isArray(obj1);
const isArray2 = Array.isArray(obj2);
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
// 数组的情况
if (isArray1 && isArray2) {
if (obj1.length !== obj2.length) return false;
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false;
}
return true;
}
// 普通对象的情况
else {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!(key in obj2)) return false;
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
}
// 判断是否都是数组
const isArray1 = Array.isArray(obj1);
const isArray2 = Array.isArray(obj2);
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
// 数组的情况
if (isArray1 && isArray2) {
if (obj1.length !== obj2.length) return false;
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false;
}
return true;
}
// 普通对象的情况
else {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!(key in obj2)) return false;
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
}
return true;
}
return true;
}
};

View File

@@ -3,14 +3,14 @@
* 适用于快捷键、全局事件监听等需要避免干扰用户输入的场景。
*/
export const isInEditableElement = () => {
const el = document.activeElement;
if (!el || !(el instanceof HTMLElement)) {
return false;
}
const el = document.activeElement;
if (!el || !(el instanceof HTMLElement)) {
return false;
}
return (
el instanceof HTMLInputElement ||
el instanceof HTMLTextAreaElement ||
el.isContentEditable
);
return (
el instanceof HTMLInputElement ||
el instanceof HTMLTextAreaElement ||
el.isContentEditable
);
};

View File

@@ -1,136 +1,137 @@
import type {Node} from '@xyflow/svelte';
import type { Node } from '@xyflow/svelte';
import type {TinyflowOptions} from '#types';
import type { TinyflowOptions } from '#types';
export type NodePaletteItem = {
icon?: string;
title: string;
type: string;
sortNo?: number;
description?: string;
category: string;
extra?: Partial<Node['data']>;
icon?: string;
title: string;
type: string;
sortNo?: number;
description?: string;
category: string;
extra?: Partial<Node['data']>;
};
const BUILT_IN_NODES: NodePaletteItem[] = [
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
title: '开始节点',
type: 'startNode',
sortNo: 100,
description: '开始定义输入参数',
category: '输入输出'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
title: '循环',
type: 'loopNode',
sortNo: 200,
description: '用于循环执行任务',
category: '逻辑'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
title: '条件判断',
type: 'conditionNode',
sortNo: 250,
description: '根据参数值分流到不同分支',
category: '逻辑'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
title: '大模型',
type: 'llmNode',
sortNo: 300,
description: '使用大模型处理问题',
category: '模型与流程'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
title: '知识库',
type: 'knowledgeNode',
sortNo: 400,
description: '通过知识库获取内容',
category: '数据与集成'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
title: '搜索引擎',
type: 'searchEngineNode',
sortNo: 500,
description: '通过搜索引擎搜索内容',
category: '模型与流程'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
title: 'Http 请求',
type: 'httpNode',
sortNo: 600,
description: '通过 HTTP 请求获取数据',
category: '数据与集成'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
title: '动态代码',
type: 'codeNode',
sortNo: 700,
description: '动态执行代码',
category: '逻辑'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
title: '内容模板',
type: 'templateNode',
sortNo: 800,
description: '通过模板引擎生成内容',
category: '逻辑'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
title: '用户确认',
type: 'confirmNode',
sortNo: 900,
description: '确认继续或选择内容',
category: '输入输出'
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
title: '结束节点',
type: 'endNode',
sortNo: 1000,
description: '结束定义输出参数',
category: '输入输出'
}
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
title: '开始节点',
type: 'startNode',
sortNo: 100,
description: '开始定义输入参数',
category: '输入输出',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
title: '循环',
type: 'loopNode',
sortNo: 200,
description: '用于循环执行任务',
category: '逻辑',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
title: '条件判断',
type: 'conditionNode',
sortNo: 250,
description: '根据参数值分流到不同分支',
category: '逻辑',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
title: '大模型',
type: 'llmNode',
sortNo: 300,
description: '使用大模型处理问题',
category: '模型与流程',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
title: '知识库',
type: 'knowledgeNode',
sortNo: 400,
description: '通过知识库获取内容',
category: '数据与集成',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
title: '搜索引擎',
type: 'searchEngineNode',
sortNo: 500,
description: '通过搜索引擎搜索内容',
category: '模型与流程',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
title: 'Http 请求',
type: 'httpNode',
sortNo: 600,
description: '通过 HTTP 请求获取数据',
category: '数据与集成',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
title: '动态代码',
type: 'codeNode',
sortNo: 700,
description: '动态执行代码',
category: '逻辑',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
title: '内容模板',
type: 'templateNode',
sortNo: 800,
description: '通过模板引擎生成内容',
category: '逻辑',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
title: '用户确认',
type: 'confirmNode',
sortNo: 900,
description: '确认继续或选择内容',
category: '输入输出',
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
title: '结束节点',
type: 'endNode',
sortNo: 1000,
description: '结束定义输出参数',
category: '输入输出',
},
];
export function getAvailableNodes(options?: TinyflowOptions) {
const nodes: NodePaletteItem[] = [...BUILT_IN_NODES];
const customNodes = options?.customNodes;
const nodes: NodePaletteItem[] = [...BUILT_IN_NODES];
const customNodes = options?.customNodes;
if (customNodes) {
const keys = Object.keys(customNodes).sort((a, b) => {
return (customNodes[a].sortNo || 0) - (customNodes[b].sortNo || 0);
});
if (customNodes) {
const keys = Object.keys(customNodes).sort((a, b) => {
return (customNodes[a].sortNo || 0) - (customNodes[b].sortNo || 0);
});
for (let key of keys) {
const item = customNodes[key];
nodes.push({
icon: item.icon,
title: item.title,
type: key,
sortNo: item.sortNo,
description: item.description,
category: item.group === 'tools' ? '扩展工具' : '基础节点'
});
}
for (let key of keys) {
const item = customNodes[key];
nodes.push({
icon: item.icon,
title: item.title,
type: key,
sortNo: item.sortNo,
description: item.description,
category: item.group === 'tools' ? '扩展工具' : '基础节点',
});
}
}
const hiddenNodes = typeof options?.hiddenNodes === 'function'
? options?.hiddenNodes()
: options?.hiddenNodes;
const hiddenNodes =
typeof options?.hiddenNodes === 'function'
? options?.hiddenNodes()
: options?.hiddenNodes;
const hiddenSet = new Set(Array.isArray(hiddenNodes) ? hiddenNodes : []);
const filtered = nodes.filter((node) => !hiddenSet.has(node.type));
filtered.sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0));
return filtered;
const hiddenSet = new Set(Array.isArray(hiddenNodes) ? hiddenNodes : []);
const filtered = nodes.filter((node) => !hiddenSet.has(node.type));
filtered.sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0));
return filtered;
}

View File

@@ -1,175 +1,175 @@
import { describe, expect, it } from 'vitest';
import {
flattenParameterCandidates,
findBackspaceTokenRange,
findTokenRangeAtCursor,
flattenParameterNames,
insertTextAtCursor,
parseTokenParts,
splitTokenDisplay
flattenParameterCandidates,
findBackspaceTokenRange,
findTokenRangeAtCursor,
flattenParameterNames,
insertTextAtCursor,
parseTokenParts,
splitTokenDisplay,
} from './paramToken';
describe('paramToken utils', () => {
it('should flatten parameter names with nested paths', () => {
const result = flattenParameterNames([
{
name: 'input'
},
{
name: 'documents',
children: [
{
name: 'title'
},
{
name: 'meta',
children: [
{
name: 'author'
}
]
}
]
}
]);
it('should flatten parameter names with nested paths', () => {
const result = flattenParameterNames([
{
name: 'input',
},
{
name: 'documents',
children: [
{
name: 'title',
},
{
name: 'meta',
children: [
{
name: 'author',
},
],
},
],
},
]);
expect(result).toEqual([
'input',
'documents',
'documents.title',
'documents.meta',
'documents.meta.author'
]);
expect(result).toEqual([
'input',
'documents',
'documents.title',
'documents.meta',
'documents.meta.author',
]);
});
it('should keep unresolved candidates in parameter list', () => {
const result = flattenParameterCandidates([
{
name: 'input',
refType: 'ref',
ref: '',
},
{
name: 'docs',
refType: 'ref',
ref: 'documents',
},
{
name: 'runtimeInput',
refType: 'input',
},
]);
expect(result).toEqual([
{
name: 'input',
resolved: false,
},
{
name: 'docs',
resolved: true,
},
{
name: 'runtimeInput',
resolved: true,
},
]);
});
it('should insert token text in the middle by cursor range', () => {
const result = insertTextAtCursor('hello world', '{{input}}', 6, 11);
expect(result).toEqual({
value: 'hello {{input}}',
cursor: 15,
});
});
it('should append token text when cursor info is missing', () => {
const result = insertTextAtCursor('hello', '{{name}}');
expect(result).toEqual({
value: 'hello{{name}}',
cursor: 13,
});
});
it('should parse token parts and mark valid tokens', () => {
const parts = parseTokenParts('你好 {{ user.name }} 与 {{unknown}}', [
'user.name',
'docs',
]);
expect(parts).toEqual([
{
type: 'text',
text: '你好 ',
},
{
type: 'token',
text: '{{ user.name }}',
key: 'user.name',
valid: true,
},
{
type: 'text',
text: ' 与 ',
},
{
type: 'token',
text: '{{unknown}}',
key: 'unknown',
valid: false,
},
]);
});
it('should keep plain text when token syntax is invalid', () => {
const parts = parseTokenParts('abc {{}} def', ['a']);
expect(parts).toEqual([
{
type: 'text',
text: 'abc {{}} def',
},
]);
});
it('should split token display and hide braces text', () => {
const result = splitTokenDisplay('{{ user.name }}', 'user.name');
expect(result).toEqual({
hiddenPrefix: '{{ ',
visibleText: 'user.name',
hiddenSuffix: ' }}',
});
});
it('should find full token range for backspace delete', () => {
const content = 'hello {{input}} world';
const tokenEndCursor = 'hello {{input}}'.length;
const range = findBackspaceTokenRange(content, tokenEndCursor);
expect(range).toEqual({
start: 6,
end: 15,
text: '{{input}}',
key: 'input',
});
});
it('should support boundary match for arrow skip behavior', () => {
const content = 'x{{docs}}y';
const tokenStart = 1;
const tokenEnd = 9;
const rightBoundary = findTokenRangeAtCursor(content, tokenStart, {
includeStart: true,
includeEnd: false,
});
const leftBoundary = findTokenRangeAtCursor(content, tokenEnd, {
includeStart: false,
includeEnd: true,
});
it('should keep unresolved candidates in parameter list', () => {
const result = flattenParameterCandidates([
{
name: 'input',
refType: 'ref',
ref: ''
},
{
name: 'docs',
refType: 'ref',
ref: 'documents'
},
{
name: 'runtimeInput',
refType: 'input'
}
]);
expect(result).toEqual([
{
name: 'input',
resolved: false
},
{
name: 'docs',
resolved: true
},
{
name: 'runtimeInput',
resolved: true
}
]);
});
it('should insert token text in the middle by cursor range', () => {
const result = insertTextAtCursor('hello world', '{{input}}', 6, 11);
expect(result).toEqual({
value: 'hello {{input}}',
cursor: 15
});
});
it('should append token text when cursor info is missing', () => {
const result = insertTextAtCursor('hello', '{{name}}');
expect(result).toEqual({
value: 'hello{{name}}',
cursor: 13
});
});
it('should parse token parts and mark valid tokens', () => {
const parts = parseTokenParts(
'你好 {{ user.name }} 与 {{unknown}}',
['user.name', 'docs']
);
expect(parts).toEqual([
{
type: 'text',
text: '你好 '
},
{
type: 'token',
text: '{{ user.name }}',
key: 'user.name',
valid: true
},
{
type: 'text',
text: ' 与 '
},
{
type: 'token',
text: '{{unknown}}',
key: 'unknown',
valid: false
}
]);
});
it('should keep plain text when token syntax is invalid', () => {
const parts = parseTokenParts('abc {{}} def', ['a']);
expect(parts).toEqual([
{
type: 'text',
text: 'abc {{}} def'
}
]);
});
it('should split token display and hide braces text', () => {
const result = splitTokenDisplay('{{ user.name }}', 'user.name');
expect(result).toEqual({
hiddenPrefix: '{{ ',
visibleText: 'user.name',
hiddenSuffix: ' }}'
});
});
it('should find full token range for backspace delete', () => {
const content = 'hello {{input}} world';
const tokenEndCursor = 'hello {{input}}'.length;
const range = findBackspaceTokenRange(content, tokenEndCursor);
expect(range).toEqual({
start: 6,
end: 15,
text: '{{input}}',
key: 'input'
});
});
it('should support boundary match for arrow skip behavior', () => {
const content = 'x{{docs}}y';
const tokenStart = 1;
const tokenEnd = 9;
const rightBoundary = findTokenRangeAtCursor(content, tokenStart, {
includeStart: true,
includeEnd: false
});
const leftBoundary = findTokenRangeAtCursor(content, tokenEnd, {
includeStart: false,
includeEnd: true
});
expect(rightBoundary?.key).toBe('docs');
expect(leftBoundary?.key).toBe('docs');
});
expect(rightBoundary?.key).toBe('docs');
expect(leftBoundary?.key).toBe('docs');
});
});

View File

@@ -1,272 +1,292 @@
export interface ParameterLike {
name?: string;
ref?: string;
refType?: string;
children?: ParameterLike[];
name?: string;
ref?: string;
refType?: string;
children?: ParameterLike[];
}
export interface TokenRange {
start: number;
end: number;
text: string;
key: string;
start: number;
end: number;
text: string;
key: string;
}
export type TokenPart =
| {
type: 'text';
text: string;
}
| {
type: 'token';
text: string;
key: string;
valid: boolean;
};
| {
type: 'text';
text: string;
}
| {
type: 'token';
text: string;
key: string;
valid: boolean;
};
export interface ParameterCandidate {
name: string;
resolved: boolean;
name: string;
resolved: boolean;
}
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
export function normalizeTokenKey(tokenKey: string): string {
return tokenKey.trim();
return tokenKey.trim();
}
export function flattenParameterNames(parameters?: ParameterLike[] | null): string[] {
return flattenParameterCandidates(parameters).map((item) => item.name);
export function flattenParameterNames(
parameters?: ParameterLike[] | null,
): string[] {
return flattenParameterCandidates(parameters).map((item) => item.name);
}
function isParameterResolved(parameter?: ParameterLike): boolean {
if (!parameter) {
return false;
}
if (!parameter) {
return false;
}
const refType = (parameter.refType || '').trim();
if (refType === 'fixed' || refType === 'input') {
return true;
}
const refType = (parameter.refType || '').trim();
if (refType === 'fixed' || refType === 'input') {
return true;
}
const ref = (parameter.ref || '').trim();
return !!ref;
const ref = (parameter.ref || '').trim();
return !!ref;
}
export function flattenParameterCandidates(parameters?: ParameterLike[] | null): ParameterCandidate[] {
if (!parameters || parameters.length === 0) {
return [];
export function flattenParameterCandidates(
parameters?: ParameterLike[] | null,
): ParameterCandidate[] {
if (!parameters || parameters.length === 0) {
return [];
}
const candidates: ParameterCandidate[] = [];
const indexMap = new Map<string, number>();
const addCandidate = (name: string, resolved: boolean) => {
const normalized = name.trim();
if (!normalized) {
return;
}
const exists = indexMap.get(normalized);
if (exists === undefined) {
indexMap.set(normalized, candidates.length);
candidates.push({
name: normalized,
resolved,
});
return;
}
const candidates: ParameterCandidate[] = [];
const indexMap = new Map<string, number>();
// 同名参数只要有一个可解析,就视为可解析
if (resolved) {
candidates[exists].resolved = true;
}
};
const addCandidate = (name: string, resolved: boolean) => {
const normalized = name.trim();
if (!normalized) {
return;
}
const exists = indexMap.get(normalized);
if (exists === undefined) {
indexMap.set(normalized, candidates.length);
candidates.push({
name: normalized,
resolved
});
return;
}
const walk = (
items: ParameterLike[],
parentPath = '',
inheritedResolved = true,
) => {
for (const item of items) {
const rawName = item?.name?.trim();
if (!rawName) {
continue;
}
// 同名参数只要有一个可解析,就视为可解析
if (resolved) {
candidates[exists].resolved = true;
}
};
const currentPath = parentPath ? `${parentPath}.${rawName}` : rawName;
const currentResolved = inheritedResolved && isParameterResolved(item);
addCandidate(currentPath, currentResolved);
const walk = (items: ParameterLike[], parentPath = '', inheritedResolved = true) => {
for (const item of items) {
const rawName = item?.name?.trim();
if (!rawName) {
continue;
}
if (item.children && item.children.length > 0) {
walk(item.children, currentPath, currentResolved);
}
}
};
const currentPath = parentPath ? `${parentPath}.${rawName}` : rawName;
const currentResolved = inheritedResolved && isParameterResolved(item);
addCandidate(currentPath, currentResolved);
if (item.children && item.children.length > 0) {
walk(item.children, currentPath, currentResolved);
}
}
};
walk(parameters);
return candidates;
walk(parameters);
return candidates;
}
export function parseTokenParts(content: string, validParams: string[] = []): TokenPart[] {
const source = content ?? '';
const validSet = new Set(validParams.map(normalizeTokenKey));
const parts: TokenPart[] = [];
export function parseTokenParts(
content: string,
validParams: string[] = [],
): TokenPart[] {
const source = content ?? '';
const validSet = new Set(validParams.map(normalizeTokenKey));
const parts: TokenPart[] = [];
let lastIndex = 0;
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
let lastIndex = 0;
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
while (match) {
if (match.index > lastIndex) {
parts.push({
type: 'text',
text: source.slice(lastIndex, match.index)
});
}
const rawToken = match[0];
const tokenKey = normalizeTokenKey(match[1] || '');
parts.push({
type: 'token',
text: rawToken,
key: tokenKey,
valid: validSet.has(tokenKey)
});
lastIndex = match.index + rawToken.length;
match = TOKEN_PATTERN.exec(source);
while (match) {
if (match.index > lastIndex) {
parts.push({
type: 'text',
text: source.slice(lastIndex, match.index),
});
}
if (lastIndex < source.length) {
parts.push({
type: 'text',
text: source.slice(lastIndex)
});
}
const rawToken = match[0];
const tokenKey = normalizeTokenKey(match[1] || '');
parts.push({
type: 'token',
text: rawToken,
key: tokenKey,
valid: validSet.has(tokenKey),
});
if (parts.length === 0) {
parts.push({
type: 'text',
text: source
});
}
lastIndex = match.index + rawToken.length;
match = TOKEN_PATTERN.exec(source);
}
return parts;
if (lastIndex < source.length) {
parts.push({
type: 'text',
text: source.slice(lastIndex),
});
}
if (parts.length === 0) {
parts.push({
type: 'text',
text: source,
});
}
return parts;
}
export function getTokenRanges(content: string): TokenRange[] {
const source = content ?? '';
const ranges: TokenRange[] = [];
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
const source = content ?? '';
const ranges: TokenRange[] = [];
TOKEN_PATTERN.lastIndex = 0;
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
while (match) {
const rawToken = match[0];
ranges.push({
start: match.index,
end: match.index + rawToken.length,
text: rawToken,
key: normalizeTokenKey(match[1] || '')
});
match = TOKEN_PATTERN.exec(source);
}
while (match) {
const rawToken = match[0];
ranges.push({
start: match.index,
end: match.index + rawToken.length,
text: rawToken,
key: normalizeTokenKey(match[1] || ''),
});
match = TOKEN_PATTERN.exec(source);
}
return ranges;
return ranges;
}
export function findTokenRangeAtCursor(
content: string,
cursor: number,
options?: {
includeStart?: boolean;
includeEnd?: boolean;
}
content: string,
cursor: number,
options?: {
includeStart?: boolean;
includeEnd?: boolean;
},
): TokenRange | null {
if (!Number.isInteger(cursor) || cursor < 0) {
return null;
}
const includeStart = options?.includeStart ?? false;
const includeEnd = options?.includeEnd ?? false;
const ranges = getTokenRanges(content);
for (const range of ranges) {
const leftValid = includeStart ? cursor >= range.start : cursor > range.start;
const rightValid = includeEnd ? cursor <= range.end : cursor < range.end;
if (leftValid && rightValid) {
return range;
}
}
if (!Number.isInteger(cursor) || cursor < 0) {
return null;
}
const includeStart = options?.includeStart ?? false;
const includeEnd = options?.includeEnd ?? false;
const ranges = getTokenRanges(content);
for (const range of ranges) {
const leftValid = includeStart
? cursor >= range.start
: cursor > range.start;
const rightValid = includeEnd ? cursor <= range.end : cursor < range.end;
if (leftValid && rightValid) {
return range;
}
}
return null;
}
export function findBackspaceTokenRange(content: string, cursor: number): TokenRange | null {
return findTokenRangeAtCursor(content, cursor, {
includeStart: false,
includeEnd: true
});
export function findBackspaceTokenRange(
content: string,
cursor: number,
): TokenRange | null {
return findTokenRangeAtCursor(content, cursor, {
includeStart: false,
includeEnd: true,
});
}
export function splitTokenDisplay(rawToken: string, normalizedKey?: string): {
hiddenPrefix: string;
visibleText: string;
hiddenSuffix: string;
export function splitTokenDisplay(
rawToken: string,
normalizedKey?: string,
): {
hiddenPrefix: string;
visibleText: string;
hiddenSuffix: string;
} {
const source = rawToken ?? '';
if (!source.startsWith('{{') || !source.endsWith('}}')) {
return {
hiddenPrefix: '',
visibleText: normalizedKey || source,
hiddenSuffix: ''
};
}
const inner = source.slice(2, -2);
const visibleText = normalizeTokenKey(normalizedKey || inner);
if (!visibleText) {
return {
hiddenPrefix: '',
visibleText: source,
hiddenSuffix: ''
};
}
const innerStart = inner.indexOf(visibleText);
const leading = innerStart >= 0 ? inner.slice(0, innerStart) : '';
const trailing = innerStart >= 0 ? inner.slice(innerStart + visibleText.length) : '';
const source = rawToken ?? '';
if (!source.startsWith('{{') || !source.endsWith('}}')) {
return {
hiddenPrefix: `{{${leading}`,
visibleText,
hiddenSuffix: `${trailing}}}`
hiddenPrefix: '',
visibleText: normalizedKey || source,
hiddenSuffix: '',
};
}
const inner = source.slice(2, -2);
const visibleText = normalizeTokenKey(normalizedKey || inner);
if (!visibleText) {
return {
hiddenPrefix: '',
visibleText: source,
hiddenSuffix: '',
};
}
const innerStart = inner.indexOf(visibleText);
const leading = innerStart >= 0 ? inner.slice(0, innerStart) : '';
const trailing =
innerStart >= 0 ? inner.slice(innerStart + visibleText.length) : '';
return {
hiddenPrefix: `{{${leading}`,
visibleText,
hiddenSuffix: `${trailing}}}`,
};
}
export function insertTextAtCursor(
content: string,
insertedText: string,
selectionStart?: number | null,
selectionEnd?: number | null
content: string,
insertedText: string,
selectionStart?: number | null,
selectionEnd?: number | null,
): {
value: string;
cursor: number;
value: string;
cursor: number;
} {
const source = content ?? '';
const start = Number.isInteger(selectionStart)
? Math.max(0, Math.min(selectionStart as number, source.length))
: source.length;
const end = Number.isInteger(selectionEnd)
? Math.max(start, Math.min(selectionEnd as number, source.length))
: start;
const source = content ?? '';
const start = Number.isInteger(selectionStart)
? Math.max(0, Math.min(selectionStart as number, source.length))
: source.length;
const end = Number.isInteger(selectionEnd)
? Math.max(start, Math.min(selectionEnd as number, source.length))
: start;
const nextValue = source.slice(0, start) + insertedText + source.slice(end);
return {
value: nextValue,
cursor: start + insertedText.length
};
const nextValue = source.slice(0, start) + insertedText + source.slice(end);
return {
value: nextValue,
cursor: start + insertedText.length,
};
}
export function escapeHtml(text: string): string {
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}

View File

@@ -1,63 +1,63 @@
import {genShortId} from './IdGen';
import {useSvelteFlow} from '@xyflow/svelte';
import type {Parameter} from '#types';
import { genShortId } from './IdGen';
import { useSvelteFlow } from '@xyflow/svelte';
import type { Parameter } from '#types';
export const fillParameterId = (parameters?: Parameter[]) => {
if (!parameters || parameters.length == 0) {
return parameters;
}
parameters.forEach((parameter) => {
if (!parameter.id) {
parameter.id = genShortId();
}
fillParameterId(parameter.children);
});
if (!parameters || parameters.length == 0) {
return parameters;
}
parameters.forEach((parameter) => {
if (!parameter.id) {
parameter.id = genShortId();
}
fillParameterId(parameter.children);
});
return parameters;
};
export const useAddParameter = () => {
const { updateNodeData } = useSvelteFlow();
return {
addParameter: (
nodeId: string,
dataKey: string = 'parameters',
parameter?: Parameter | Parameter[]
) => {
if (Array.isArray(parameter)) {
parameter.forEach((p) => fillParameterId(p?.children));
} else {
fillParameterId(parameter?.children);
}
const { updateNodeData } = useSvelteFlow();
return {
addParameter: (
nodeId: string,
dataKey: string = 'parameters',
parameter?: Parameter | Parameter[],
) => {
if (Array.isArray(parameter)) {
parameter.forEach((p) => fillParameterId(p?.children));
} else {
fillParameterId(parameter?.children);
}
function createNewParameter(parameter: Parameter) {
return {
name: '',
dataType: 'String',
refType: 'ref',
...parameter,
id: genShortId()
};
}
function createNewParameter(parameter: Parameter) {
return {
name: '',
dataType: 'String',
refType: 'ref',
...parameter,
id: genShortId(),
};
}
const newParameters: Parameter[] = [];
if (Array.isArray(parameter)) {
newParameters.push(...parameter.map(createNewParameter));
} else {
newParameters.push(createNewParameter(parameter as Parameter));
}
const newParameters: Parameter[] = [];
if (Array.isArray(parameter)) {
newParameters.push(...parameter.map(createNewParameter));
} else {
newParameters.push(createNewParameter(parameter as Parameter));
}
updateNodeData(nodeId, (node) => {
let parameters = node.data[dataKey] as Array<any>;
if (parameters) {
parameters.push(...newParameters);
} else {
parameters = [...newParameters];
}
return {
[dataKey]: [...parameters]
};
});
updateNodeData(nodeId, (node) => {
let parameters = node.data[dataKey] as Array<any>;
if (parameters) {
parameters.push(...newParameters);
} else {
parameters = [...newParameters];
}
};
return {
[dataKey]: [...parameters],
};
});
},
};
};

View File

@@ -1,27 +1,27 @@
import {genShortId} from './IdGen';
import {store} from '#store/stores.svelte';
import { genShortId } from './IdGen';
import { store } from '#store/stores.svelte';
export const useCopyNode = () => {
const copyNode = (id: string) => {
const node = store.getNode(id);
if (node) {
const newNodeId = genShortId();
const newNode = {
...node,
id: newNodeId,
position: {
x: node.position.x + 50,
y: node.position.y + 50
}
};
const copyNode = (id: string) => {
const node = store.getNode(id);
if (node) {
const newNodeId = genShortId();
const newNode = {
...node,
id: newNodeId,
position: {
x: node.position.x + 50,
y: node.position.y + 50,
},
};
store.updateNodes((nodes) => {
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
return [...newNodes, newNode];
});
}
};
return {
copyNode
};
store.updateNodes((nodes) => {
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
return [...newNodes, newNode];
});
}
};
return {
copyNode,
};
};

View File

@@ -1,26 +1,26 @@
import {store} from '#store/stores.svelte';
import {genShortId} from '#components/utils/IdGen';
import {type Edge, type Node, useSvelteFlow} from '@xyflow/svelte';
import { store } from '#store/stores.svelte';
import { genShortId } from '#components/utils/IdGen';
import { type Edge, type Node, useSvelteFlow } from '@xyflow/svelte';
interface ClipboardData {
tinyflowNodes: Node[];
tinyflowEdges?: Edge[];
version: string;
tinyflowNodes: Node[];
tinyflowEdges?: Edge[];
version: string;
}
/**
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
*/
function sanitizeNode(node: Node): Node {
const { id, type, position, data, parentId } = node;
return {
id,
type,
position: { x: position.x, y: position.y },
parentId: parentId ? parentId : undefined,
data: data ? JSON.parse(JSON.stringify(data)) : {},
...(parentId !== undefined && { parentId })
};
const { id, type, position, data, parentId } = node;
return {
id,
type,
position: { x: position.x, y: position.y },
parentId: parentId ? parentId : undefined,
data: data ? JSON.parse(JSON.stringify(data)) : {},
...(parentId !== undefined && { parentId }),
};
}
/**
@@ -28,255 +28,254 @@ function sanitizeNode(node: Node): Node {
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
*/
export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
const nodeMap = new Map<string, Node>();
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
const nodeMap = new Map<string, Node>();
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
// 初始化
for (const node of nodes) {
nodeMap.set(node.id, node);
inDegree.set(node.id, 0); // 默认无依赖
if (node.parentId) {
// 子节点依赖父节点
inDegree.set(node.id, 1);
if (!childrenMap.has(node.parentId)) {
childrenMap.set(node.parentId, []);
}
childrenMap.get(node.parentId)!.push(node.id);
}
// 初始化
for (const node of nodes) {
nodeMap.set(node.id, node);
inDegree.set(node.id, 0); // 默认无依赖
if (node.parentId) {
// 子节点依赖父节点
inDegree.set(node.id, 1);
if (!childrenMap.has(node.parentId)) {
childrenMap.set(node.parentId, []);
}
childrenMap.get(node.parentId)!.push(node.id);
}
}
// 所有根节点(无 parentId 或父不存在)入队
const queue: Node[] = [];
for (const node of nodes) {
if (!node.parentId || !nodeMap.has(node.parentId)) {
queue.push(node);
}
// 所有根节点(无 parentId 或父不存在)入队
const queue: Node[] = [];
for (const node of nodes) {
if (!node.parentId || !nodeMap.has(node.parentId)) {
queue.push(node);
}
}
const result: Node[] = [];
const visited = new Set<string>();
const result: Node[] = [];
const visited = new Set<string>();
// BFS 拓扑排序
while (queue.length > 0) {
const node = queue.shift()!;
if (visited.has(node.id)) continue;
visited.add(node.id);
result.push(node);
// BFS 拓扑排序
while (queue.length > 0) {
const node = queue.shift()!;
if (visited.has(node.id)) continue;
visited.add(node.id);
result.push(node);
// 将该节点的所有直接子节点入队(如果其父已处理)
const children = childrenMap.get(node.id) || [];
for (const childId of children) {
if (!visited.has(childId)) {
queue.push(nodeMap.get(childId)!);
}
}
// 将该节点的所有直接子节点入队(如果其父已处理)
const children = childrenMap.get(node.id) || [];
for (const childId of children) {
if (!visited.has(childId)) {
queue.push(nodeMap.get(childId)!);
}
}
}
// 补充可能遗漏的节点(如循环引用或孤立子节点)
for (const node of nodes) {
if (!visited.has(node.id)) {
result.push(node);
}
// 补充可能遗漏的节点(如循环引用或孤立子节点)
for (const node of nodes) {
if (!visited.has(node.id)) {
result.push(node);
}
}
return result;
return result;
}
/**
* 清理边中不可序列化的字段
*/
function sanitizeEdge(edge: Edge): Edge {
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
return {
id,
source,
target,
...(sourceHandle !== undefined && { sourceHandle }),
...(targetHandle !== undefined && { targetHandle }),
...(type !== undefined && { type }),
data: data ? JSON.parse(JSON.stringify(data)) : {}
};
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
return {
id,
source,
target,
...(sourceHandle !== undefined && { sourceHandle }),
...(targetHandle !== undefined && { targetHandle }),
...(type !== undefined && { type }),
data: data ? JSON.parse(JSON.stringify(data)) : {},
};
}
/**
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
*/
function rewriteRefsInData(obj: any, idMap: Map<string, string>): any {
if (obj === null || obj === undefined) {
return obj;
}
// 如果是数组,递归处理每个元素
if (Array.isArray(obj)) {
return obj.map((item) => rewriteRefsInData(item, idMap));
}
// 如果是对象,检查是否为引用对象
if (typeof obj === 'object') {
// 检查是否是引用定义refType === 'ref' 且有 ref 字段
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
if (match) {
const [_, oldNodeId, paramId] = match;
const newNodeId = idMap.get(oldNodeId);
if (newNodeId !== undefined) {
// 返回新对象,避免修改原数据
return {
...obj,
ref: `${newNodeId}.${paramId}`
};
}
}
}
// 递归处理所有子属性
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
result[key] = rewriteRefsInData(obj[key], idMap);
}
}
return result;
}
// 原始类型string/number/boolean直接返回
if (obj === null || obj === undefined) {
return obj;
}
// 如果是数组,递归处理每个元素
if (Array.isArray(obj)) {
return obj.map((item) => rewriteRefsInData(item, idMap));
}
// 如果是对象,检查是否为引用对象
if (typeof obj === 'object') {
// 检查是否是引用定义refType === 'ref' 且有 ref 字段
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
if (match) {
const [_, oldNodeId, paramId] = match;
const newNodeId = idMap.get(oldNodeId);
if (newNodeId !== undefined) {
// 返回新对象,避免修改原数据
return {
...obj,
ref: `${newNodeId}.${paramId}`,
};
}
}
}
// 递归处理所有子属性
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
result[key] = rewriteRefsInData(obj[key], idMap);
}
}
return result;
}
// 原始类型string/number/boolean直接返回
return obj;
}
/**
* 复制粘贴处理器 Hook
*/
export const useCopyPasteHandler = () => {
const svelteFlow = useSvelteFlow();
const svelteFlow = useSvelteFlow();
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
const selectedNodes = store.getNodes().filter((node) => node.selected);
if (selectedNodes.length === 0) return;
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
const selectedNodes = store.getNodes().filter((node) => node.selected);
if (selectedNodes.length === 0) return;
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
const allEdges = store.getEdges();
const relatedEdges = allEdges.filter(
(edge) =>
selectedNodes.some((n) => n.id === edge.source) &&
selectedNodes.some((n) => n.id === edge.target)
);
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
const allEdges = store.getEdges();
const relatedEdges = allEdges.filter(
(edge) =>
selectedNodes.some((n) => n.id === edge.source) &&
selectedNodes.some((n) => n.id === edge.target),
);
const serializableNodes = selectedNodes.map(sanitizeNode);
const serializableEdges = relatedEdges.map(sanitizeEdge);
const serializableNodes = selectedNodes.map(sanitizeNode);
const serializableEdges = relatedEdges.map(sanitizeEdge);
const clipboardData: ClipboardData = {
tinyflowNodes: serializableNodes,
tinyflowEdges: serializableEdges,
version: '1.0'
};
const jsonStr = JSON.stringify(clipboardData, null, 0);
try {
// 优先使用 event.clipboardData在 copy 事件中可用,无需权限)
if ('clipboardData' in event && event.clipboardData) {
event.clipboardData.setData('text/plain', jsonStr);
if (event instanceof ClipboardEvent) {
event.preventDefault();
}
} else {
// 降级到 navigator.clipboard需用户手势
await navigator.clipboard.writeText(jsonStr);
}
console.log('Copied nodes and edges to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
// 可选:同源降级存储
try {
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
} catch {}
}
const clipboardData: ClipboardData = {
tinyflowNodes: serializableNodes,
tinyflowEdges: serializableEdges,
version: '1.0',
};
const jsonStr = JSON.stringify(clipboardData, null, 0);
const pasteHandler = (event: ClipboardEvent) => {
const text = event.clipboardData?.getData('text/plain');
if (!text) return;
let parsed: ClipboardData | null = null;
try {
parsed = JSON.parse(text);
} catch {
return; // 忽略非 JSON 内容
try {
// 优先使用 event.clipboardData在 copy 事件中可用,无需权限)
if ('clipboardData' in event && event.clipboardData) {
event.clipboardData.setData('text/plain', jsonStr);
if (event instanceof ClipboardEvent) {
event.preventDefault();
}
} else {
// 降级到 navigator.clipboard需用户手势
await navigator.clipboard.writeText(jsonStr);
}
console.log('Copied nodes and edges to clipboard');
} catch (err) {
console.error('Failed to copy:', err);
// 可选:同源降级存储
try {
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
} catch {}
}
};
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
return;
}
const pasteHandler = (event: ClipboardEvent) => {
const text = event.clipboardData?.getData('text/plain');
if (!text) return;
event.preventDefault();
let parsed: ClipboardData | null = null;
try {
parsed = JSON.parse(text);
} catch {
return; // 忽略非 JSON 内容
}
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
const pastedEdges = parsed.tinyflowEdges || [];
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
return;
}
// 创建新节点(带新 ID 和偏移)
const newNodeIdMap = new Map<string, string>();
const newNodes: Node[] = [];
event.preventDefault();
for (const node of pastedNodes) {
const newId = `node_${genShortId()}`;
newNodeIdMap.set(node.id, newId);
}
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
const pastedEdges = parsed.tinyflowEdges || [];
// 建新节点(含重写后的 data
for (const node of pastedNodes) {
const newId = newNodeIdMap.get(node.id)!;
// 建新节点(带新 ID 和偏移
const newNodeIdMap = new Map<string, string>();
const newNodes: Node[] = [];
const newParentId =
node.parentId !== undefined
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
: undefined;
for (const node of pastedNodes) {
const newId = `node_${genShortId()}`;
newNodeIdMap.set(node.id, newId);
}
const newData = rewriteRefsInData(node.data, newNodeIdMap);
// 构建新节点(含重写后的 data
for (const node of pastedNodes) {
const newId = newNodeIdMap.get(node.id)!;
newNodes.push({
...node,
id: newId,
parentId: newParentId,
data: newData,
position: {
x: (node.position?.x ?? 0) + 50,
y: (node.position?.y ?? 0) + 50
},
selected: true
});
}
const newParentId =
node.parentId !== undefined
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
: undefined;
// 创建新边(仅当两端都在粘贴范围内)
const newEdges: Edge[] = [];
for (const edge of pastedEdges) {
const newSource = newNodeIdMap.get(edge.source);
const newTarget = newNodeIdMap.get(edge.target);
if (newSource && newTarget) {
newEdges.push({
...edge,
id: `edge_${genShortId()}`,
source: newSource,
target: newTarget
});
}
}
const newData = rewriteRefsInData(node.data, newNodeIdMap);
// 更新 store取消其他节点选中添加新内容
store.updateNodes((nodes) => {
const unselected = nodes.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newNodes];
newNodes.push({
...node,
id: newId,
parentId: newParentId,
data: newData,
position: {
x: (node.position?.x ?? 0) + 50,
y: (node.position?.y ?? 0) + 50,
},
selected: true,
});
}
// 创建新边(仅当两端都在粘贴范围内)
const newEdges: Edge[] = [];
for (const edge of pastedEdges) {
const newSource = newNodeIdMap.get(edge.source);
const newTarget = newNodeIdMap.get(edge.target);
if (newSource && newTarget) {
newEdges.push({
...edge,
id: `edge_${genShortId()}`,
source: newSource,
target: newTarget,
});
}
}
store.updateEdges((edges) => {
const unselected = edges.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newEdges];
});
};
// 更新 store取消其他节点选中添加新内容
store.updateNodes((nodes) => {
const unselected = nodes.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newNodes];
});
return {
copyHandler,
pasteHandler
};
store.updateEdges((edges) => {
const unselected = edges.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newEdges];
});
};
return {
copyHandler,
pasteHandler,
};
};

View File

@@ -1,10 +1,10 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useDeleteEdge = () => {
const deleteEdge = (id: string) => {
store.removeEdge( id);
};
return {
deleteEdge
};
const deleteEdge = (id: string) => {
store.removeEdge(id);
};
return {
deleteEdge,
};
};

View File

@@ -1,11 +1,13 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useDeleteNode = () => {
const deleteNode = (id: string) => {
store.removeNode(id);
store.updateEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id))
};
return {
deleteNode
};
const deleteNode = (id: string) => {
store.removeNode(id);
store.updateEdges((edges) =>
edges.filter((edge) => edge.source !== id && edge.target !== id),
);
};
return {
deleteNode,
};
};

View File

@@ -1,43 +1,49 @@
import {store} from '../../store/stores.svelte';
import { store } from '../../store/stores.svelte';
export const useEnsureParentInNodesBefore = () => {
const ensureParentInNodesBefore = (parentNodeId: string, childNodeId: string) => {
store.updateNodes((nodeArray) => {
let parentIndex = -1;
for (let i = 0; i < nodeArray.length; i++) {
if (nodeArray[i].id === parentNodeId) {
parentIndex = i;
break;
}
}
const ensureParentInNodesBefore = (
parentNodeId: string,
childNodeId: string,
) => {
store.updateNodes((nodeArray) => {
let parentIndex = -1;
for (let i = 0; i < nodeArray.length; i++) {
if (nodeArray[i].id === parentNodeId) {
parentIndex = i;
break;
}
}
if (parentIndex <= 0) {
return nodeArray;
}
if (parentIndex <= 0) {
return nodeArray;
}
let firstChildIndex = -1;
for (let i = 0; i < parentIndex; i++) {
if (nodeArray[i].parentId === parentNodeId || nodeArray[i].id === childNodeId) {
firstChildIndex = i;
break;
}
}
let firstChildIndex = -1;
for (let i = 0; i < parentIndex; i++) {
if (
nodeArray[i].parentId === parentNodeId ||
nodeArray[i].id === childNodeId
) {
firstChildIndex = i;
break;
}
}
if (firstChildIndex == -1) {
return nodeArray;
}
if (firstChildIndex == -1) {
return nodeArray;
}
const parentNode = nodeArray[parentIndex];
for (let i = parentIndex; i > firstChildIndex; i--) {
nodeArray[i] = nodeArray[i - 1];
}
nodeArray[firstChildIndex] = parentNode;
const parentNode = nodeArray[parentIndex];
for (let i = parentIndex; i > firstChildIndex; i--) {
nodeArray[i] = nodeArray[i - 1];
}
nodeArray[firstChildIndex] = parentNode;
return nodeArray;
});
};
return nodeArray;
});
};
return {
ensureParentInNodesBefore
};
return {
ensureParentInNodesBefore,
};
};

View File

@@ -1,11 +1,11 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useGetEdgesBySource = () => {
const getEdgesBySource = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.source === target);
};
return {
getEdgesBySource
};
const getEdgesBySource = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.source === target);
};
return {
getEdgesBySource,
};
};

View File

@@ -1,11 +1,11 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useGetEdgesByTarget = () => {
const getEdgesByTarget = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.target === target);
};
return {
getEdgesByTarget
};
const getEdgesByTarget = (target: string) => {
const edges = store.getEdges();
return edges.filter((edge) => edge.target === target);
};
return {
getEdgesByTarget,
};
};

View File

@@ -1,10 +1,10 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useGetNode = () => {
const getNode = (id: string) => {
return store.getNode(id);
};
return {
getNode
};
const getNode = (id: string) => {
return store.getNode(id);
};
return {
getNode,
};
};

View File

@@ -1,22 +1,22 @@
import {store} from '#store/stores.svelte';
import { store } from '#store/stores.svelte';
export const useGetNodeRelativePosition = () => {
const getNodeRelativePosition = (parentNodeId: string) => {
let node = store.getNode(parentNodeId);
const position = { x: 0, y: 0 };
while (node) {
position.x += node.position.x;
position.y += node.position.y;
const getNodeRelativePosition = (parentNodeId: string) => {
let node = store.getNode(parentNodeId);
const position = { x: 0, y: 0 };
while (node) {
position.x += node.position.x;
position.y += node.position.y;
if (node.parentId) {
node = store.getNode(node.parentId);
} else {
node = undefined;
}
}
return position;
};
return {
getNodeRelativePosition
};
if (node.parentId) {
node = store.getNode(node.parentId);
} else {
node = undefined;
}
}
return position;
};
return {
getNodeRelativePosition,
};
};

View File

@@ -1,31 +1,31 @@
import {store} from '#store/stores.svelte';
import type {Edge, Node} from '@xyflow/svelte';
import { store } from '#store/stores.svelte';
import type { Edge, Node } from '@xyflow/svelte';
export const useGetNodesFromSource = () => {
const getEdgesBySource = (target: string, edges: Edge[]) => {
return edges.filter(
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle'
);
};
const getEdgesBySource = (target: string, edges: Edge[]) => {
return edges.filter(
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle',
);
};
const getNodesFromSource = (sourceNodeId: string) => {
const edges = store.getEdges();
const result: Node[] = [];
const getNodesFromSource = (sourceNodeId: string) => {
const edges = store.getEdges();
const result: Node[] = [];
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
while (edgesFromSource.length > 0) {
const newEdgesFromSource: Edge[] = [];
edgesFromSource.forEach((edge) => {
result.push(store.getNode(edge.target)!);
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
});
edgesFromSource = newEdgesFromSource;
}
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
while (edgesFromSource.length > 0) {
const newEdgesFromSource: Edge[] = [];
edgesFromSource.forEach((edge) => {
result.push(store.getNode(edge.target)!);
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
});
edgesFromSource = newEdgesFromSource;
}
return result;
};
return {
getNodesFromSource
};
return result;
};
return {
getNodesFromSource,
};
};

View File

@@ -1,154 +1,172 @@
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
import type {Parameter} from '#types';
import {getCurrentNodeId, getOptions} from '#components/utils/NodeUtils';
import {nodeIcons} from '../../consts';
import { type Edge, type Node, useNodesData, useStore } from '@xyflow/svelte';
import type { Parameter } from '#types';
import { getCurrentNodeId, getOptions } from '#components/utils/NodeUtils';
import { nodeIcons } from '../../consts';
const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge[]) => {
for (const edge of edges) {
if (edge.target === currentNodeId && edge.source) {
refNodeIds.push(edge.source);
fillRefNodeIds(refNodeIds, edge.source, edges);
}
const fillRefNodeIds = (
refNodeIds: string[],
currentNodeId: string,
edges: Edge[],
) => {
for (const edge of edges) {
if (edge.target === currentNodeId && edge.source) {
refNodeIds.push(edge.source);
fillRefNodeIds(refNodeIds, edge.source, edges);
}
}
};
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean, nodeType: string) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => {
const getChildren = (
params: any,
parentId: string,
nodeIsChildren: boolean,
nodeType: string,
) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => {
const dataType = nodeIsChildren
? `Array<${param.dataType || 'String'}>`
: param.dataType || 'String';
return {
label: param.name,
dataType: dataType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
children: getChildren(
param.children,
parentId + '.' + param.name,
nodeIsChildren,
nodeType,
),
};
});
};
const nodeToOptions = (
node: Node,
nodeIsChildren: boolean,
currentNode: Node,
) => {
const options = getOptions();
const nodeType = node.type || '';
let icon: string | undefined = nodeIcons[nodeType];
if (!icon && options?.customNodes && options.customNodes[nodeType]) {
icon = options.customNodes[nodeType].icon;
}
// 如果仍然获取不到,尝试使用 data.icon (作为回退)
if (!icon && node.data && node.data.icon) {
icon = node.data.icon as string;
}
const title = node.data.title;
if (nodeType === 'startNode') {
const parameters = node.data.parameters as Array<Parameter>;
const children = [];
if (parameters)
for (const parameter of parameters) {
const dataType = nodeIsChildren
? `Array<${param.dataType || 'String'}>`
: (param.dataType || 'String');
return {
label: param.name,
dataType: dataType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren, nodeType)
};
});
};
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
const options = getOptions();
const nodeType = node.type || '';
let icon: string | undefined = nodeIcons[nodeType];
if (!icon && options?.customNodes && options.customNodes[nodeType]) {
icon = options.customNodes[nodeType].icon;
}
// 如果仍然获取不到,尝试使用 data.icon (作为回退)
if (!icon && node.data && node.data.icon) {
icon = node.data.icon as string;
}
const title = node.data.title;
if (nodeType === 'startNode') {
const parameters = node.data.parameters as Array<Parameter>;
const children = [];
if (parameters)
for (const parameter of parameters) {
const dataType = nodeIsChildren
? `Array<${parameter.dataType || 'String'}>`
: (parameter.dataType || 'String');
children.push({
label: parameter.name,
dataType: dataType,
value: node.id + '.' + parameter.name,
selectable: true,
nodeType: nodeType
});
}
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children
};
} else if (nodeType === 'loopNode' && currentNode.parentId) {
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children: [
{
label: 'loopItem',
dataType: 'Any',
value: node.id + '.loopItem',
selectable: true,
nodeType: nodeType
},
{
label: 'index',
dataType: 'Number',
value: node.id + '.index',
selectable: true,
nodeType: nodeType
}
]
};
} else {
const outputDefs = node.data.outputDefs;
if (outputDefs) {
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType)
};
}
? `Array<${parameter.dataType || 'String'}>`
: parameter.dataType || 'String';
children.push({
label: parameter.name,
dataType: dataType,
value: node.id + '.' + parameter.name,
selectable: true,
nodeType: nodeType,
});
}
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children,
};
} else if (nodeType === 'loopNode' && currentNode.parentId) {
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children: [
{
label: 'loopItem',
dataType: 'Any',
value: node.id + '.loopItem',
selectable: true,
nodeType: nodeType,
},
{
label: 'index',
dataType: 'Number',
value: node.id + '.index',
selectable: true,
nodeType: nodeType,
},
],
};
} else {
const outputDefs = node.data.outputDefs;
if (outputDefs) {
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType),
};
}
}
};
export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId);
const { nodes, edges, nodeLookup } = $derived(useStore());
const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId);
const { nodes, edges, nodeLookup } = $derived(useStore());
let selectItems = $derived.by(() => {
const resultOptions = [];
if (!currentNode.current) {
return [];
let selectItems = $derived.by(() => {
const resultOptions = [];
if (!currentNode.current) {
return [];
}
//通过 nodeLookup.get 才会得到有 parentId 的 node
const cNode = nodeLookup.get(currentNodeId)!;
if (useChildrenOnly) {
for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) {
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
} else {
const refNodeIds: string[] = [];
fillRefNodeIds(refNodeIds, currentNodeId, edges);
//通过 nodeLookup.get 才会得到有 parentId 的 node
const cNode = nodeLookup.get(currentNodeId)!;
if (useChildrenOnly) {
for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) {
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
} else {
const refNodeIds: string[] = [];
fillRefNodeIds(refNodeIds, currentNodeId, edges);
for (const node of nodes) {
if (refNodeIds.includes(node.id)) {
const nodeIsChildren = node.parentId === currentNode.current.id;
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
for (const node of nodes) {
if (refNodeIds.includes(node.id)) {
const nodeIsChildren = node.parentId === currentNode.current.id;
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
nodeOptions && resultOptions.push(nodeOptions);
}
}
}
return resultOptions;
});
return resultOptions;
});
return {
get current() {
return selectItems;
}
};
return {
get current() {
return selectItems;
},
};
};

Some files were not shown because too many files have changed in this diff Show More