feat(web): 修语言下拉漂移(class 化)+ 主页登录态用户中心按钮 + 用户中心返回主页
ci-pangolin / Lint — shellcheck (pull_request) Successful in 10s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 29s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 32s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 21s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 24s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 21s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 10s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 8s
ci-pangolin / Go — build + test (pull_request) Successful in 10s
Deploy Site / deploy-site (push) Successful in 2m33s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m33s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 19s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 13m16s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 10s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 29s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 32s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 21s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 24s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 21s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 10s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 8s
ci-pangolin / Go — build + test (pull_request) Successful in 10s
Deploy Site / deploy-site (push) Successful in 2m33s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m33s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 19s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 13m16s
#1 官网语言下拉漂移:根因=官网 CSP 无 unsafe-inline,Header.jsx 下拉全用内联 style= 被浏览器拦→菜单飘视口右边。修:搬进 website.css 的 .langwrap/.langsel /.langmenu/.langmenu a(全走 var(--token),right:0 锚按钮下方,min-width 160 加宽),Header.jsx 删净内联 style。 #2 主页登录态右上角「用户中心」按钮:Header.jsx 读同源 localStorage 'pg_uc_refresh',有则 .linklogin 位显「用户中心」(→ /user/)、无则「Log in」。 i18n nav.center 6 语言。 #3 用户中心「返回主页」按钮:UserCenter.tsx 顶栏加 <a href="/">(home 图标), i18n backHome 6 语言。 顺带堵同源闸漏洞:check-l1-sync ③ 原只匹配带引号图标键('refresh-cw'),漏了 裸标识符键(home),导致新加的 home 未被校验就通过。修解析器匹配裸键;并按 ds-flow 把 home 登记进 design/prototype/icons.js。同源闸复跑绿(裸键现全受检)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,18 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const langRef = useRef(null);
|
||||
|
||||
// 与用户中心同源:登录后 localStorage 存 pg_uc_refresh → 显示「用户中心」入口。
|
||||
useEffect(() => {
|
||||
try {
|
||||
setLoggedIn(!!localStorage.getItem('pg_uc_refresh'));
|
||||
} catch {
|
||||
setLoggedIn(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!langOpen) return;
|
||||
const onDoc = (e) => { if (langRef.current && !langRef.current.contains(e.target)) setLangOpen(false); };
|
||||
@@ -75,7 +85,7 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
))}
|
||||
</nav>
|
||||
<div class="right">
|
||||
<div ref={langRef} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<div ref={langRef} class="langwrap">
|
||||
<button
|
||||
type="button"
|
||||
class="langsel"
|
||||
@@ -83,21 +93,18 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
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' }}>
|
||||
<svg class="caret" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<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 }}>
|
||||
<div class="langmenu" role="listbox">
|
||||
{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' }}>
|
||||
<a key={code} role="option" aria-selected={on} href={langHref(code)}>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
@@ -105,7 +112,11 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
|
||||
{loggedIn ? (
|
||||
<a class="linklogin" href={SITE.usercenter}>{t.center}</a>
|
||||
) : (
|
||||
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
|
||||
)}
|
||||
<a class="btn btn-primary" href="#download">
|
||||
<Download />
|
||||
<span>{t.get}</span>
|
||||
|
||||
@@ -38,6 +38,7 @@ export const STRINGS: Record<string, Record<Lang, string>> = {
|
||||
'nav.docs': { zh: '文档', en: 'Docs', ja: 'ドキュメント', ko: '문서', ru: 'Документация', es: 'Documentación' },
|
||||
'nav.blog': { zh: 'Blog', en: 'Blog', ja: 'ブログ', ko: '블로그', ru: 'Блог', es: 'Blog' },
|
||||
'nav.login': { zh: '登录', en: 'Log in', ja: 'ログイン', ko: '로그인', ru: 'Войти', es: 'Iniciar sesión' },
|
||||
'nav.center': { zh: '用户中心', en: 'Account', ja: 'アカウント', ko: '계정', ru: 'Личный кабинет', es: 'Mi cuenta' },
|
||||
'nav.get': { zh: '立即下载', en: 'Get the app', ja: 'アプリを入手', ko: '앱 받기', ru: 'Получить приложение', es: 'Obtener la app' },
|
||||
|
||||
'hero.eyebrow': { zh: '极速 · 稳定 · 省心', en: 'Fast · Stable · Effortless', ja: '高速 · 安定 · 快適', ko: '빠름 · 안정 · 간편', ru: 'Быстро · Стабильно · Без забот', es: 'Rápido · Estable · Sin complicaciones' },
|
||||
|
||||
@@ -56,6 +56,7 @@ const headerT = {
|
||||
docs: t('nav.docs'),
|
||||
blog: t('nav.blog'),
|
||||
login: t('nav.login'),
|
||||
center: t('nav.center'),
|
||||
get: t('nav.get'),
|
||||
suBtn: t('su.btn'),
|
||||
};
|
||||
|
||||
@@ -42,6 +42,17 @@ img,svg{display:block}
|
||||
.linklogin{font-size:14.5px;font-weight:600;color:var(--fg1);cursor:pointer}
|
||||
.linklogin:hover{color:var(--accent)}
|
||||
|
||||
/* ---------- language dropdown ---------- */
|
||||
.langwrap{position:relative;display:inline-block}
|
||||
.langsel{display:inline-flex;align-items:center;gap:6px;border:1.5px solid var(--border-strong);border-radius:var(--radius-full);padding:8px 14px;background:var(--surface);color:var(--fg1);font-family:var(--font-sans);font-size:14px;font-weight:600;cursor:pointer;transition:border-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out)}
|
||||
.langsel:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.langsel .caret{width:11px;height:11px;transition:transform 140ms var(--ease-out)}
|
||||
.langsel[aria-expanded="true"] .caret{transform:rotate(180deg)}
|
||||
.langmenu{position:absolute;top:calc(100% + 6px);right:0;min-width:160px;display:flex;flex-direction:column;gap:1px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);padding:5px;z-index:60}
|
||||
.langmenu a{text-align:left;text-decoration:none;font-family:var(--font-sans);font-size:13px;font-weight:500;color:var(--fg1);background:transparent;padding:8px 11px;border-radius:var(--radius-sm);white-space:nowrap;transition:background var(--dur-fast) var(--ease-out)}
|
||||
.langmenu a:hover{background:var(--bg-subtle)}
|
||||
.langmenu a[aria-selected="true"]{color:var(--accent);background:var(--accent-subtle);font-weight:700}
|
||||
|
||||
/* mobile nav */
|
||||
.menu-btn{display:none;border:none;background:transparent;cursor:pointer;padding:7px;border-radius:var(--radius-sm);color:var(--fg1)}
|
||||
.menu-btn svg{width:22px;height:22px}
|
||||
|
||||
Reference in New Issue
Block a user