fix(web): 语言下拉换自定义菜单(原生 select 弹层被系统定位/漂移)
Deploy Site / deploy-site (push) Successful in 4m31s

原生 <select> 的选项弹层由 OS 定位(macOS 锚在选中项上、整体偏移=漂移),CSS 控
不了。改成自控下拉:按钮(复用药丸样式)+ 绝对定位菜单(top:100%,right:0)+ 点外/
Esc 关闭 + 选中高亮。用户中心 LangSeg 与官网 Header 同款;官网选项用 <a href> 导航
(SEO 友好)、用户中心用 setLang。两端 build 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-07 22:42:00 +08:00
parent b584188ce2
commit 386e0bce90
2 changed files with 147 additions and 39 deletions
+42 -11
View File
@@ -3,7 +3,7 @@
* 迁自 design/ui_kits/website/index.html 的 <header> + site.js 的菜单/滚动毛玻璃逻辑。
* 语言切换由原型的 JS 文本替换改为「路由跳转」(zh=/, en=/en/),单显不并排(铁律 6)。
*/
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Download, Menu } from 'lucide-react';
import { SITE } from '../config/site';
@@ -29,6 +29,17 @@ function Mark() {
export default function Header({ lang = 'zh', t = {} }) {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [langOpen, setLangOpen] = useState(false);
const langRef = useRef(null);
useEffect(() => {
if (!langOpen) return;
const onDoc = (e) => { if (langRef.current && !langRef.current.contains(e.target)) setLangOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setLangOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
}, [langOpen]);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
@@ -64,16 +75,36 @@ export default function Header({ lang = 'zh', t = {} }) {
))}
</nav>
<div class="right">
<select
class="langsel"
value={lang}
onChange={(e) => { window.location.href = langHref(e.target.value); }}
aria-label="Language"
>
{langs.map(([code, label]) => (
<option key={code} value={code}>{label}</option>
))}
</select>
<div ref={langRef} style={{ position: 'relative', display: 'inline-block' }}>
<button
type="button"
class="langsel"
onClick={() => setLangOpen((o) => !o)}
aria-haspopup="listbox"
aria-expanded={langOpen}
aria-label="Language"
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<span>{(langs.find(([c]) => c === lang) || ['', 'English'])[1]}</span>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true"
style={{ transform: langOpen ? 'rotate(180deg)' : 'none', transition: 'transform 140ms' }}>
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{langOpen && (
<div role="listbox" style={{ position: 'absolute', top: 'calc(100% + 6px)', right: 0, minWidth: 132, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, boxShadow: '0 10px 30px rgba(20, 12, 6, 0.22)', padding: 5, zIndex: 60, display: 'flex', flexDirection: 'column', gap: 1 }}>
{langs.map(([code, label]) => {
const on = code === lang;
return (
<a key={code} role="option" aria-selected={on} href={langHref(code)}
style={{ textAlign: 'left', textDecoration: 'none', background: on ? 'var(--accent-subtle, var(--bg-subtle))' : 'transparent', color: on ? 'var(--accent)' : 'var(--fg1)', fontWeight: on ? 700 : 500, fontFamily: 'var(--font-sans)', fontSize: 13, padding: '8px 11px', borderRadius: 8, whiteSpace: 'nowrap' }}>
{label}
</a>
);
})}
</div>
)}
</div>
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
<a class="btn btn-primary" href="#download">
<Download />