From f37f1f3b59fb4b7a364d3429e094004705723a37 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 8 Jul 2026 09:16:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=BF=AE=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E6=BC=82=E7=A7=BB(class=20=E5=8C=96)+=20?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E7=99=BB=E5=BD=95=E6=80=81=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E6=8C=89=E9=92=AE=20+=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E8=BF=94=E5=9B=9E=E4=B8=BB=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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 顶栏加 (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 --- design/prototype/icons.js | 1 + tools/check-l1-sync.mjs | 3 ++- web/usercenter/components/UserCenter.tsx | 4 ++++ web/usercenter/components/icons.tsx | 1 + web/usercenter/lib/i18n.ts | 1 + web/website/src/components/Header.jsx | 27 +++++++++++++++++------- web/website/src/i18n/strings.ts | 1 + web/website/src/layouts/Site.astro | 1 + web/website/src/styles/website.css | 11 ++++++++++ 9 files changed, 41 insertions(+), 9 deletions(-) diff --git a/design/prototype/icons.js b/design/prototype/icons.js index 5904581..470444a 100644 --- a/design/prototype/icons.js +++ b/design/prototype/icons.js @@ -57,6 +57,7 @@ 'compass': '', 'chart-no-axes-column': '', 'chart': '', + 'home': '', 'map-pin': '', 'laptop': '', 'monitor-smartphone': '', diff --git a/tools/check-l1-sync.mjs b/tools/check-l1-sync.mjs index 3f045d1..5a56301 100644 --- a/tools/check-l1-sync.mjs +++ b/tools/check-l1-sync.mjs @@ -73,7 +73,8 @@ function checkWebTokenSync(label, webPath) { // usercenter: export const LUCIDE: Record = { 'name': '', ... } const ucJs = read('web/usercenter/components/icons.tsx'); const ucBody = ucJs.match(/LUCIDE\s*:[^=]*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ucJs.match(/LUCIDE\s*=\s*\{([\s\S]*?)\n\};/)?.[1] ?? ''; - for (const [, id, body] of ucBody.matchAll(/'([^']+)'\s*:\s*'([^']*)'/g)) { + // 键可能带引号('refresh-cw')或裸标识符(home)—— 两种都匹配(裸键此前被漏检)。 + for (const [, id, body] of ucBody.matchAll(/['"]?([\w-]+)['"]?\s*:\s*'([^']*)'/g)) { if (!(id in protoIcons)) { problems.push(`[icons] usercenter 图标「${id}」不在原型 sprite —— 先登记 design/prototype/icons.js 再用`); } else if (protoIcons[id] !== body) { diff --git a/web/usercenter/components/UserCenter.tsx b/web/usercenter/components/UserCenter.tsx index 5e8df3a..7bda309 100644 --- a/web/usercenter/components/UserCenter.tsx +++ b/web/usercenter/components/UserCenter.tsx @@ -152,6 +152,10 @@ export default function UserCenter() { {!mobile && } {mobile &&
} + + + {!mobile && t('backHome')} + diff --git a/web/usercenter/components/icons.tsx b/web/usercenter/components/icons.tsx index 99db11d..cb6c3f2 100644 --- a/web/usercenter/components/icons.tsx +++ b/web/usercenter/components/icons.tsx @@ -25,6 +25,7 @@ export const LUCIDE: Record = { 'shopping-bag': '', 'log-out': '', 'external-link': '', + home: '', gift: '', smartphone: '', lock: '', diff --git a/web/usercenter/lib/i18n.ts b/web/usercenter/lib/i18n.ts index 6fda3a8..b7f17de 100644 --- a/web/usercenter/lib/i18n.ts +++ b/web/usercenter/lib/i18n.ts @@ -12,6 +12,7 @@ export const STRINGS: Record = { navInvite: { zh: '邀请返利', en: 'Referral', ja: '紹介', ko: '추천', ru: 'Рефералы', es: 'Referidos' }, navSettings: { zh: '设置', en: 'Settings', ja: '設定', ko: '설정', ru: 'Настройки', es: 'Ajustes' }, signOut: { zh: '退出', en: 'Sign out', ja: 'ログアウト', ko: '로그아웃', ru: 'Выйти', es: 'Cerrar sesión' }, + backHome: { zh: '返回主页', en: 'Home', ja: 'ホーム', ko: '홈', ru: 'На главную', es: 'Inicio' }, /* login */ loginTitle: { zh: '登录用户中心', en: 'Log in to your account', ja: 'アカウントにログイン', ko: '계정에 로그인', ru: 'Вход в аккаунт', es: 'Inicia sesión en tu cuenta' }, diff --git a/web/website/src/components/Header.jsx b/web/website/src/components/Header.jsx index 3848d52..cdf9dc2 100644 --- a/web/website/src/components/Header.jsx +++ b/web/website/src/components/Header.jsx @@ -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 = {} }) { ))}
-
+
{langOpen && ( -
+
{langs.map(([code, label]) => { const on = code === lang; return ( - + {label} ); @@ -105,7 +112,11 @@ export default function Header({ lang = 'zh', t = {} }) {
)}
- {t.login} + {loggedIn ? ( + {t.center} + ) : ( + {t.login} + )} {t.get} diff --git a/web/website/src/i18n/strings.ts b/web/website/src/i18n/strings.ts index 7630648..42d0909 100644 --- a/web/website/src/i18n/strings.ts +++ b/web/website/src/i18n/strings.ts @@ -38,6 +38,7 @@ export const STRINGS: Record> = { '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' }, diff --git a/web/website/src/layouts/Site.astro b/web/website/src/layouts/Site.astro index 8441d93..50b0fc4 100644 --- a/web/website/src/layouts/Site.astro +++ b/web/website/src/layouts/Site.astro @@ -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'), }; diff --git a/web/website/src/styles/website.css b/web/website/src/styles/website.css index 2e7ce30..2d821b7 100644 --- a/web/website/src/styles/website.css +++ b/web/website/src/styles/website.css @@ -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}