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

View File

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

View File

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