From 717d17d5a7dbed46f7831e81fbc3bf1d441b4483 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 28 Jul 2026 14:19:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20FormSelect=20=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E4=B8=BB=E9=A2=98=E5=8C=96=E4=B8=8B=E6=8B=89=E5=BC=B9?= =?UTF-8?q?=E5=B1=82=EF=BC=8C=E6=9B=BF=E4=BB=A3=E5=8E=9F=E7=94=9F=20select?= =?UTF-8?q?=20=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原生 ` 的默认箭头由各浏览器自行绘制,在深色主题与圆角样式下 -/// 位置/颜色不协调(无法垂直居中、无法跟随主题色),故用 `appearance-none` -/// 隐藏后叠一个 `top-1/2 -translate-y-1/2` 垂直居中的 chevron SVG -/// (`pointer-events-none`,点击穿透到 select)。 +/// 纯函数便于单测;`panel_height` 由调用方按选项数估算(见 `measure_flip`)。 +#[allow(dead_code)] // server 构建下仅被 dead 的组件体引用(wasm 与单测为真实调用方) +fn should_flip(trigger_top: f64, trigger_bottom: f64, viewport_height: f64, panel_height: f64) -> bool { + /// 面板与触发器的间隙(mt-1.5)加视口边缘留白。 + const MARGIN: f64 = 14.0; + let below = viewport_height - trigger_bottom; + let above = trigger_top; + below < panel_height + MARGIN && above > below +} + +/// 键盘导航的循环索引:在 `len` 个选项中从 `cur` 移动 `delta`(±1),越界回绕。 +#[allow(dead_code)] // 同 should_flip +fn wrap_index(cur: usize, delta: i32, len: usize) -> usize { + if len == 0 { + return 0; + } + (cur as i32 + delta).rem_euclid(len as i32) as usize +} + +/// 测量触发器视口位置,决定面板展开方向(仅 wasm;SSR 无 DOM)。 +#[cfg(target_arch = "wasm32")] +fn measure_flip(trigger_id: &str, option_count: usize) -> bool { + /// 选项行高:24px 行盒 + py-2.5(20px 垂直内边距)。 + const ROW_HEIGHT: f64 = 44.0; + /// 面板 1px 边框 ×2 + p-1.5 内边距。 + const PANEL_CHROME: f64 = 14.0; + /// 面板高度上限:max-h-60(240px)+ PANEL_CHROME。 + const PANEL_MAX: f64 = 254.0; + + let Some(window) = web_sys::window() else { + return false; + }; + let Some(document) = window.document() else { + return false; + }; + let Some(el) = document.get_element_by_id(trigger_id) else { + return false; + }; + let rect = el.get_bounding_client_rect(); + let viewport = window + .inner_height() + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(800.0); + let panel_height = ((option_count as f64) * ROW_HEIGHT + PANEL_CHROME).min(PANEL_MAX); + should_flip(rect.top(), rect.bottom(), viewport, panel_height) +} + +/// 把指定选项滚入面板可视区(打开/键盘导航时跟随;仅 wasm)。 +#[cfg(target_arch = "wasm32")] +fn scroll_option_into_view(element_id: &str) { + let Some(document) = web_sys::window().and_then(|w| w.document()) else { + return; + }; + let Some(el) = document.get_element_by_id(element_id) else { + return; + }; + let opts = web_sys::ScrollIntoViewOptions::new(); + opts.set_block(web_sys::ScrollLogicalPosition::Nearest); + el.scroll_into_view_with_scroll_into_view_options(&opts); +} + +/// 下拉选择框组件(自定义弹层,全主题化)。 /// -/// 泛型值绑定:`