feat:重构UI

This commit is contained in:
2025-12-26 16:03:12 +08:00
parent 1429e0e66a
commit abcbe3cddc
67 changed files with 12170 additions and 515 deletions

67
apps/web/lib/admin-api.ts Normal file
View File

@@ -0,0 +1,67 @@
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
export const ADMIN_UNAUTHORIZED_EVENT = 'admin:unauthorized'
export class AdminUnauthorizedError extends Error {
constructor(message = '未登录或登录已过期') {
super(message)
this.name = 'AdminUnauthorizedError'
}
}
export function getAdminToken(): string {
if (typeof window === 'undefined') return ''
return localStorage.getItem('admin_token') || ''
}
export function clearAdminToken() {
if (typeof window === 'undefined') return
localStorage.removeItem('admin_token')
}
function notifyUnauthorized(message?: string) {
if (typeof window === 'undefined') return
window.dispatchEvent(new CustomEvent(ADMIN_UNAUTHORIZED_EVENT, { detail: { message } }))
}
async function safeReadErrorMessage(res: Response): Promise<string | null> {
try {
const data = await res.json()
const detail = typeof data?.detail === 'string' ? data.detail : null
if (detail) return detail
} catch {
// ignore
}
return null
}
export async function adminFetch(path: string, init: RequestInit = {}): Promise<Response> {
const token = getAdminToken()
const headers = new Headers(init.headers)
if (!token) {
notifyUnauthorized()
throw new AdminUnauthorizedError()
}
if (token) headers.set('Authorization', `Bearer ${token}`)
if (!headers.has('Content-Type') && init.body) headers.set('Content-Type', 'application/json')
const res = await fetch(`${API_BASE}${path}`, { ...init, headers })
if (res.status === 401) {
const message = (await safeReadErrorMessage(res)) || undefined
clearAdminToken()
notifyUnauthorized(message)
throw new AdminUnauthorizedError(message)
}
return res
}
export async function adminFetchJson<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await adminFetch(path, init)
if (!res.ok) {
const message = (await safeReadErrorMessage(res)) || `请求失败 (${res.status})`
throw new Error(message)
}
return res.json() as Promise<T>
}

76
apps/web/lib/languages.ts Normal file
View File

@@ -0,0 +1,76 @@
import languageMap from './languages.zh.json'
export type LanguageOption = {
code: string
name: string
search: string
}
type LanguageMap = Record<string, string>
const raw = languageMap as LanguageMap
const hasCjk = (text: string) => /[\u4e00-\u9fff]/.test(text)
const normalized = Object.entries(raw)
.map(([code, name]) => ({ code: code.replace(/_/g, '-'), name }))
.filter(({ code }) => code.length <= 10)
.sort((a, b) => {
const aGroup = hasCjk(a.name) ? 0 : 1
const bGroup = hasCjk(b.name) ? 0 : 1
if (aGroup !== bGroup) return aGroup - bGroup
return a.name.localeCompare(b.name, 'zh-CN')
})
const nameByCode: Record<string, string> = Object.fromEntries(
normalized.map(({ code, name }) => [code, name])
)
export const AUTO_LANGUAGE: LanguageOption = {
code: 'auto',
name: '自动检测',
search: 'auto 自动检测',
}
export const LANGUAGE_OPTIONS: LanguageOption[] = normalized.map(({ code, name }) => ({
code,
name,
search: `${name} ${code}`.toLowerCase(),
}))
export const COMMON_LANGUAGE_CODES = [
'zh',
'zh-Hant',
'en',
'ja',
'ko',
'fr',
'de',
'es',
'ru',
'pt',
'it',
'ar',
'hi',
'th',
'vi',
'id',
'tr',
'uk',
'pl',
'nl',
'sv',
]
export const COMMON_LANGUAGE_OPTIONS: LanguageOption[] = COMMON_LANGUAGE_CODES
.map((code) => {
const name = nameByCode[code]
if (!name) return null
return { code, name, search: `${name} ${code}`.toLowerCase() }
})
.filter((v): v is LanguageOption => Boolean(v))
export function getLanguageName(code: string): string {
if (code === AUTO_LANGUAGE.code) return AUTO_LANGUAGE.name
return nameByCode[code] ?? code
}

File diff suppressed because one or more lines are too long

6
apps/web/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}