5b89de656e
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 25s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 7s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 26s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 11s
ci-pangolin / Go — build + test (pull_request) Successful in 15s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 26s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m36s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 10m3s
Deploy Site / deploy-site (push) Failing after 13m20s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 14m42s
ci-pangolin / OpenAPI Sync Check (pull_request) Failing after 14m52s
用户中心: - 统一浅色:theme.tsx 无偏好时不再跟随系统 prefers-color-scheme:dark(登录页变 黑真因),恒浅色;保留手动切换。 - 登录回跳:UserCenter 加 safeRedirect 白名单,登录成功按 ?redirect= 回来源页 (官网带 /,登录后回主页),无则进 overview。 - logo locale:新增 i18n brandName(zh 穿山甲/其余 Pangolin),Login/UserCenter /Subscription 三处引用。 - 存 pg_uc_email(getMe 时)/ clearSession 删,供官网读用户名。 官网(全走 CSS class 合 CSP,无内联 style): - logo locale:i18n nav.brand(zh 穿山甲/其余 Pangolin),Header+Footer。 - 登录态头部显示用户名(读同源 pg_uc_email)+ 下拉菜单(进入用户中心/切换用户/ 退出登录,复用 .langmenu 风格);未登录 Log in 带 ?redirect=/ 回跳。 - Docs 四卡片补真内容页(en+zh:quickstart/faq/protocol/privacy + Doc.astro 布局),卡片改回 <a href>;Protocol 改正 sing-box+REALITY(去 WireGuard)。 验证:同源闸绿 · 两端 build 过 · redline 0 · Header 零内联 style · 无 WireGuard。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
'use client';
|
|
// theme.tsx — 语言 + 明暗主题上下文(localStorage 持久化,SSR 安全)
|
|
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
import type { Lang } from './i18n';
|
|
|
|
type Theme = 'light' | 'dark';
|
|
|
|
interface Ctx {
|
|
lang: Lang;
|
|
setLang: (l: Lang) => void;
|
|
theme: Theme;
|
|
setTheme: (t: Theme) => void;
|
|
toggleTheme: () => void;
|
|
}
|
|
|
|
const ThemeCtx = createContext<Ctx | null>(null);
|
|
const LANG_KEY = 'pg_uc_lang';
|
|
const THEME_KEY = 'pg_uc_theme';
|
|
|
|
export function UIProvider({ children }: { children: React.ReactNode }) {
|
|
const [lang, setLangState] = useState<Lang>('en'); // 默认英文(国际化默认语种)
|
|
const [theme, setThemeState] = useState<Theme>('light');
|
|
|
|
// 挂载后读取持久化(避免 hydration 不一致)
|
|
useEffect(() => {
|
|
try {
|
|
const l = window.localStorage.getItem(LANG_KEY) as Lang | null;
|
|
const t = window.localStorage.getItem(THEME_KEY) as Theme | null;
|
|
if (l && (['zh', 'en', 'ja', 'ko', 'ru', 'es'] as Lang[]).includes(l)) setLangState(l);
|
|
// 默认恒浅色:只有用户手动切换过(localStorage 有显式偏好)才用保存值,
|
|
// 不跟随系统 prefers-color-scheme(避免系统暗色把登录页/用户中心染黑)。
|
|
const initial: Theme = t === 'dark' || t === 'light' ? t : 'light';
|
|
setThemeState(initial);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
document.documentElement.setAttribute('lang', lang);
|
|
}, [theme, lang]);
|
|
|
|
const setLang = useCallback((l: Lang) => {
|
|
setLangState(l);
|
|
try {
|
|
window.localStorage.setItem(LANG_KEY, l);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, []);
|
|
|
|
const setTheme = useCallback((t: Theme) => {
|
|
setThemeState(t);
|
|
try {
|
|
window.localStorage.setItem(THEME_KEY, t);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, []);
|
|
|
|
const toggleTheme = useCallback(() => setTheme(theme === 'dark' ? 'light' : 'dark'), [theme, setTheme]);
|
|
|
|
return <ThemeCtx.Provider value={{ lang, setLang, theme, setTheme, toggleTheme }}>{children}</ThemeCtx.Provider>;
|
|
}
|
|
|
|
export function useUI(): Ctx {
|
|
const c = useContext(ThemeCtx);
|
|
if (!c) throw new Error('useUI must be used within UIProvider');
|
|
return c;
|
|
}
|