kioskApi.ts 6.15 KB
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<T> = { code: number; data?: T; msg?: string };

async function parseBody(res: Response): Promise<unknown> {
  const t = await res.text();
  if (!t) return {};
  try {
    return JSON.parse(t);
  } catch {
    return {};
  }
}

export async function kioskGet<T>(path: string): Promise<ApiResult<T>> {
  const res = await fetch(apiUrl(path), {
    method: "GET",
    headers: { Accept: "application/json" },
  });
  const body = (await parseBody(res)) as ApiResult<T>;
  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<Record<string, unknown>>;
};

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<string, unknown>): 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<string, unknown>);
      if (e) list.push(e);
    }
    saveKnowledgeEntries(list);
  }
  emitKioskUpdate();
}

export async function fetchAndApplyKioskBundle(): Promise<boolean> {
  const res = await kioskGet<KioskBundle>("/api/kiosk/bundle");
  if (res.code !== 0 || !res.data) {
    return false;
  }
  applyKioskBundleToStorage(res.data);
  return true;
}

export async function fetchDataDisplay(): Promise<DataDisplayPayload | null> {
  const res = await kioskGet<DataDisplayPayload>("/api/kiosk/data-display");
  if (res.code !== 0 || !res.data) return null;
  return res.data;
}

export async function fetchObservatoryHistory(): Promise<ObservatoryHistoryPayload["items"] | null> {
  const res = await kioskGet<ObservatoryHistoryPayload>("/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<string, string> = { "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<boolean> {
  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<unknown>;
  return res.ok && body.code === 0;
}

export async function pushWelcomeToServer(w: WelcomeMessages): Promise<boolean> {
  const res = await fetch(apiUrl("/api/admin/kiosk/welcome"), {
    method: "POST",
    headers: adminHeaders(),
    body: JSON.stringify(w),
  });
  const body = (await parseBody(res)) as ApiResult<unknown>;
  return res.ok && body.code === 0;
}

export async function pushGuideToServer(guide: GuideOverrides): Promise<boolean> {
  const res = await fetch(apiUrl("/api/admin/kiosk/guide"), {
    method: "POST",
    headers: adminHeaders(),
    body: JSON.stringify({ guide }),
  });
  const body = (await parseBody(res)) as ApiResult<unknown>;
  return res.ok && body.code === 0;
}

export async function pushKnowledgeToServer(entries: KnowledgeEntry[]): Promise<boolean> {
  const res = await fetch(apiUrl("/api/admin/knowledge/sync"), {
    method: "POST",
    headers: adminHeaders(),
    body: JSON.stringify({ entries }),
  });
  const body = (await parseBody(res)) as ApiResult<unknown>;
  return res.ok && body.code === 0;
}