perf: 优化节点下拉框UI

This commit is contained in:
2026-03-01 16:13:50 +08:00
parent f872db6c59
commit beeb62c4fc
3 changed files with 59 additions and 44 deletions

View File

@@ -3,13 +3,14 @@
import {createFloating} from '../utils/createFloating';
import type {Placement} from '@floating-ui/dom';
const { children, floating, placement = 'bottom', onShow, onHide }:
const { children, floating, placement = 'bottom', onShow, onHide, syncWidth = false }:
{
children: Snippet,
floating: Snippet,
placement?: Placement,
onShow?: () => void,
onHide?: () => void
onHide?: () => void,
syncWidth?: boolean
} = $props();
let triggerEl!: HTMLDivElement, contentEl!: HTMLDivElement;
@@ -22,7 +23,8 @@
interactive: true,
placement,
onShow,
onHide
onHide,
syncWidth
});
return () => {

View File

@@ -179,7 +179,7 @@
{/snippet}
<div {...rest} class="tf-select {rest['class']}">
<FloatingTrigger bind:this={triggerObject} onShow={() => isOpen = true} onHide={() => { isOpen = false; hoveredItem = null; }}>
<FloatingTrigger bind:this={triggerObject} onShow={() => isOpen = true} onHide={() => { isOpen = false; hoveredItem = null; }} syncWidth={true}>
<button class="tf-select-input nopan nodrag {isOpen ? 'active' : ''}" {...rest}>
<div class="tf-select-input-value">
{#each activeItemsState as item, index (`${index}_${item.value}`)}
@@ -303,11 +303,10 @@
flex-direction: column;
background: #fff;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
width: max-content;
min-width: 100%;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 4px;
width: 100%;
z-index: 99999;
box-sizing: border-box;
max-height: 220px;
@@ -318,20 +317,20 @@
.tf-select-default-item {
display: flex;
align-items: center;
padding: 5px 10px;
padding: 6px 12px;
border: none;
background: #fff;
border-radius: 5px;
border-radius: 4px;
cursor: pointer;
line-height: 100%;
line-height: 1.5;
gap: 2px;
width: 100%;
text-align: left;
font-size: 13px;
color: #333;
color: #111827;
&:hover {
background: #f0f0f0;
background: #f5f5f7;
}
}
@@ -347,11 +346,11 @@
background: #fff;
margin-top: 5px;
border: 1px solid #e5e7eb;
border-radius: 10px; /* slightly smaller radius */
width: 280px; /* Reduced width to be more compact */
border-radius: 10px;
/* Removed fixed width to allow syncWidth to control it */
z-index: 99999;
box-sizing: border-box;
max-height: 400px; /* slightly less tall */
max-height: 400px;
overflow: hidden;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
@@ -360,13 +359,13 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px; /* tighter padding */
padding: 10px 12px;
border-bottom: 1px solid #f3f4f6;
background: #fff;
}
.tf-select-model-header-title {
font-size: 13px; /* smaller title */
font-size: 13px;
font-weight: 600;
color: #111827;
}
@@ -381,13 +380,13 @@
.tf-select-model-list {
display: flex;
flex-direction: column;
padding: 8px; /* tighter padding */
padding: 8px;
overflow-y: auto;
gap: 4px; /* tighter gap between items */
gap: 4px;
}
.tf-select-model-group-title {
font-size: 12px; /* smaller group title */
font-size: 12px;
font-weight: 500;
color: #6b7280;
margin-top: 6px;
@@ -399,7 +398,7 @@
}
.tf-select-model-group-icon {
width: 14px; /* smaller icon */
width: 14px;
height: 14px;
display: flex;
align-items: center;
@@ -420,7 +419,7 @@
.tf-select-model-item {
display: flex;
align-items: flex-start;
padding: 6px 8px; /* tighter item padding */
padding: 6px 8px;
border: none;
background: transparent;
border-radius: 6px;
@@ -440,7 +439,7 @@
.tf-select-model-icon {
flex-shrink: 0;
width: 32px; /* smaller item icon */
width: 32px;
height: 32px;
border-radius: 6px;
overflow: hidden;
@@ -452,7 +451,7 @@
}
.tf-select-model-icon :global(svg), .tf-select-model-icon img {
width: 70%; /* icon inside box */
width: 70%;
height: 70%;
object-fit: contain;
}
@@ -466,13 +465,13 @@
.tf-select-model-info {
display: flex;
flex-direction: column;
gap: 2px; /* tighter info gap */
gap: 2px;
flex-grow: 1;
overflow: hidden;
}
.tf-select-model-title {
font-size: 13px; /* smaller font */
font-size: 13px;
font-weight: 500;
color: #111827;
white-space: nowrap;
@@ -489,19 +488,19 @@
.tf-select-model-tag {
background: #f3f4f6;
color: #6b7280;
font-size: 10px; /* smaller tag */
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
}
.tf-select-model-desc {
font-size: 11px; /* smaller desc */
font-size: 11px;
color: #6b7280;
line-height: 1.3;
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 2; /* limit lines */
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
@@ -513,11 +512,10 @@
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border: 1px solid #e5e7eb;
overflow: hidden;
min-width: 220px;
/* width expands naturally since createFloating sets minWidth, not width */
width: max-content;
max-width: 600px;
box-sizing: border-box;
max-height: 480px;
transition: width 0.2s ease;
z-index: 99999;
}
@@ -528,22 +526,27 @@
overflow-y: auto;
&.tf-select-primary-list {
width: 220px;
width: 100%; /* Default fills the wrapper, which is minWidth-constrained by the input */
flex-shrink: 0;
background: #f9fafb;
}
/* When a secondary list is open, we fix the primary list to 100% of the input's width (done via absolute positioning or just keeping it wide enough) */
.tf-select-wrapper:has(.tf-select-secondary-list) &.tf-select-primary-list {
/* Let it take the width of the input minus borders/paddings if needed, but minWidth handles it mostly */
width: auto;
min-width: 180px;
}
&.tf-select-secondary-list {
flex-grow: 1;
min-width: 200px;
min-width: 220px;
background: #fff;
padding: 12px;
border-left: 1px solid #f3f4f6;
animation: slideIn 0.2s ease-out;
box-sizing: border-box;
}
}
@keyframes slideIn {
} @keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}

View File

@@ -6,7 +6,8 @@ import {
type OffsetOptions,
type Placement,
shift,
type ShiftOptions
type ShiftOptions,
size
} from '@floating-ui/dom';
export type FloatingOptions = {
@@ -21,6 +22,7 @@ export type FloatingOptions = {
showArrow?: boolean;
onShow?: () => void;
onHide?: () => void;
syncWidth?: boolean;
};
export type FloatingInstance = {
@@ -40,7 +42,8 @@ export const createFloating = ({
interactive,
showArrow,
onShow,
onHide
onHide,
syncWidth = false
}: FloatingOptions): FloatingInstance => {
if (typeof trigger === 'string') {
const triggerEl = document.querySelector(trigger);
@@ -83,7 +86,14 @@ export const createFloating = ({
offset(offsetOptions), // 手动偏移配置
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : [])
...(showArrow ? [arrow({ element: arrowElement })] : []),
...(syncWidth ? [size({
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
minWidth: `${rects.reference.width}px`,
});
}
})] : [])
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {