import type { GuideOverrides, KnowledgeEntry, WelcomeMessages } from "../kioskStorage"; import { emitKioskUpdate, saveGuideOverrides, saveHomeBackgrounds, saveKnowledgeEntries, saveWelcome, } from "../kioskStorage"; /** 开发时代理到 ThinkPHP;生产可设为完整域名,如 https://api.example.com */ export function apiBase(): string { return (import.meta.env.VITE_API_BASE_URL ?? "").replace(/\/$/, ""); } export function apiUrl(path: string): string { const p = path.startsWith("/") ? path : `/${path}`; return `${apiBase()}${p}`; } export type ApiResult = { code: number; data?: T; msg?: string }; async function parseBody(res: Response): Promise { const t = await res.text(); if (!t) return {}; try { return JSON.parse(t); } catch { return {}; } } export async function kioskGet(path: string): Promise> { const res = await fetch(apiUrl(path), { method: "GET", headers: { Accept: "application/json" }, }); const body = (await parseBody(res)) as ApiResult; if (!res.ok && body.code === undefined) { return { code: res.status, msg: res.statusText }; } return body; } export type KioskBundle = { homeBackgrounds: string[]; welcome: WelcomeMessages; guide: GuideOverrides; knowledge: Array>; }; export type DataDisplayPayload = { realtimeImages: Array<{ id: number; name: string; time: string; telescope: string; exposure: string; image: string; }>; status: { weather: string; seeing: string; transparency: string; moonPhase: string; }; historicalData: Array<{ date: string; observations: number; quality: string; weather: string; }>; }; export type ObservatoryHistoryPayload = { items: Array<{ id: number; kind: string; title: string; summary: string; date: string; thumb: string; }>; }; function normalizeKnowledgeRow(row: Record): KnowledgeEntry | null { if (typeof row.id !== "string" || typeof row.title !== "string") return null; const t = row.type; const type: KnowledgeEntry["type"] = t === "图片" || t === "视频" || t === "文字" ? t : "文字"; const tagsRaw = row.tags; const tags = Array.isArray(tagsRaw) ? tagsRaw.filter((x): x is string => typeof x === "string") : []; const videoRaw = row.videoUrl ?? row.video_url; const imageRaw = row.image; return { id: row.id, type, title: row.title, content: typeof row.content === "string" ? row.content : "", date: typeof row.date === "string" ? row.date : "", tags, image: typeof imageRaw === "string" && imageRaw ? imageRaw : undefined, videoUrl: typeof videoRaw === "string" && videoRaw ? videoRaw : undefined, }; } /** 将 bundle 写入 localStorage 并通知各页刷新(失败由调用方处理) */ export function applyKioskBundleToStorage(data: KioskBundle): void { const bg = data.homeBackgrounds; if (Array.isArray(bg)) { saveHomeBackgrounds(bg.filter((u) => typeof u === "string" && u !== "")); } const w = data.welcome; if (w && typeof w["zh-CN"] === "string" && typeof w.en === "string" && typeof w.bo === "string") { saveWelcome(w); } const g = data.guide; if (Array.isArray(g) && g.length === 0) { saveGuideOverrides({}); } else if (g && typeof g === "object" && !Array.isArray(g)) { saveGuideOverrides(g as GuideOverrides); } const kn = data.knowledge; if (Array.isArray(kn)) { const list: KnowledgeEntry[] = []; for (const row of kn) { if (!row || typeof row !== "object") continue; const e = normalizeKnowledgeRow(row as Record); if (e) list.push(e); } saveKnowledgeEntries(list); } emitKioskUpdate(); } export async function fetchAndApplyKioskBundle(): Promise { const res = await kioskGet("/api/kiosk/bundle"); if (res.code !== 0 || !res.data) { return false; } applyKioskBundleToStorage(res.data); return true; } export async function fetchDataDisplay(): Promise { const res = await kioskGet("/api/kiosk/data-display"); if (res.code !== 0 || !res.data) return null; return res.data; } export async function fetchObservatoryHistory(): Promise { const res = await kioskGet("/api/kiosk/observatory-history"); if (res.code !== 0 || !res.data?.items) return null; return res.data.items; } function adminHeaders(): HeadersInit { const token = import.meta.env.VITE_ADMIN_API_TOKEN ?? ""; const h: Record = { "Content-Type": "application/json", Accept: "application/json" }; if (token) h["X-Admin-Token"] = token; return h; } export function shouldSyncAdminToServer(): boolean { return import.meta.env.VITE_SYNC_ADMIN_TO_SERVER === "true"; } export async function pushHomeBackgroundsToServer(urls: string[]): Promise { const res = await fetch(apiUrl("/api/admin/kiosk/home-backgrounds"), { method: "POST", headers: adminHeaders(), body: JSON.stringify({ urls }), }); const body = (await parseBody(res)) as ApiResult; return res.ok && body.code === 0; } export async function pushWelcomeToServer(w: WelcomeMessages): Promise { const res = await fetch(apiUrl("/api/admin/kiosk/welcome"), { method: "POST", headers: adminHeaders(), body: JSON.stringify(w), }); const body = (await parseBody(res)) as ApiResult; return res.ok && body.code === 0; } export async function pushGuideToServer(guide: GuideOverrides): Promise { const res = await fetch(apiUrl("/api/admin/kiosk/guide"), { method: "POST", headers: adminHeaders(), body: JSON.stringify({ guide }), }); const body = (await parseBody(res)) as ApiResult; return res.ok && body.code === 0; } export async function pushKnowledgeToServer(entries: KnowledgeEntry[]): Promise { const res = await fetch(apiUrl("/api/admin/knowledge/sync"), { method: "POST", headers: adminHeaders(), body: JSON.stringify({ entries }), }); const body = (await parseBody(res)) as ApiResult; return res.ok && body.code === 0; }