'城市详情'

This commit is contained in:
eric
2026-03-08 23:20:16 -05:00
parent e36d533a31
commit 41487dc22e
53 changed files with 3417 additions and 1206 deletions

View File

@@ -0,0 +1,100 @@
"use client";
import {
createContext,
useContext,
useCallback,
type ReactNode,
} from "react";
export type Locale = "zh" | "en";
const LOCALES: Locale[] = ["zh", "en"];
const DEFAULT_LOCALE: Locale = "zh";
type Messages = Record<string, unknown>;
type LocaleContextValue = {
locale: Locale;
messages: Messages;
t: (key: string) => string;
setLocale: (locale: Locale) => void;
};
const LocaleContext = createContext<LocaleContextValue | null>(null);
function getNested(obj: unknown, path: string): string | undefined {
const keys = path.split(".");
let current: unknown = obj;
for (const key of keys) {
if (current == null || typeof current !== "object") return undefined;
current = (current as Record<string, unknown>)[key];
}
return typeof current === "string" ? current : undefined;
}
export function LocaleProvider({
locale,
messages,
children,
}: {
locale: Locale;
messages: Messages;
children: ReactNode;
}) {
const t = useCallback(
(key: string) => {
const value = getNested(messages, key);
return value ?? key;
},
[messages]
);
const setLocale = useCallback((newLocale: Locale) => {
const path = window.location.pathname;
const newPath = path.replace(/^\/(zh|en)/, `/${newLocale}`);
window.location.href = newPath || `/${newLocale}`;
}, []);
const value: LocaleContextValue = {
locale,
messages,
t,
setLocale,
};
return (
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
);
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useLocale must be used within LocaleProvider");
return ctx.locale;
}
export function useTranslation(namespace?: string) {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useTranslation must be used within LocaleProvider");
return {
t: (key: string) => {
const fullKey = namespace ? `${namespace}.${key}` : key;
return ctx.t(fullKey);
},
};
}
export function useLocaleRouter() {
const locale = useLocale();
return {
replace: (pathname: string, opts?: { locale?: Locale }) => {
const newLocale = opts?.locale ?? locale;
const cleanPath = pathname.replace(/^\/(zh|en)/, "") || "/";
const newPath = `/${newLocale}${cleanPath === "/" ? "" : cleanPath}`;
window.location.href = newPath;
},
};
}
export { LOCALES, DEFAULT_LOCALE };

View File

@@ -0,0 +1,68 @@
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
const STORAGE_KEY = "theme";
export type Theme = "light" | "dark";
type ThemeContextValue = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>("light");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
const hasDark = document.documentElement.classList.contains("dark");
setThemeState(hasDark ? "dark" : "light");
}, []);
useEffect(() => {
if (!mounted) return;
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem(STORAGE_KEY, theme);
}, [theme, mounted]);
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
}, []);
const toggleTheme = useCallback(() => {
setThemeState((prev) => (prev === "light" ? "dark" : "light"));
}, []);
const value: ThemeContextValue = {
theme,
setTheme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}