feat:重构UI
This commit is contained in:
67
apps/web/lib/admin-api.ts
Normal file
67
apps/web/lib/admin-api.ts
Normal 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
76
apps/web/lib/languages.ts
Normal 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
|
||||
}
|
||||
1
apps/web/lib/languages.zh.json
Normal file
1
apps/web/lib/languages.zh.json
Normal file
File diff suppressed because one or more lines are too long
6
apps/web/lib/utils.ts
Normal file
6
apps/web/lib/utils.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user