3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
1
2
3
4
5
6
|
import type {
GuideOverrides,
KnowledgeEntry,
KioskServerBundle,
VideoSourceConfig,
WelcomeMessages,
|
bc518174
王天杨
提交两个项目文件
|
7
|
} from "../kioskStorage";
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
8
|
import { applyServerBundle } from "../kioskStorage";
|
bc518174
王天杨
提交两个项目文件
|
9
|
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
10
11
12
13
14
15
|
/**
* API 根地址。
* - 若设置 `VITE_API_BASE_URL`(非空):开发与生产均使用该地址(浏览器直连该主机,需后端 CORS 正确)。
* - 生产构建且未设置:空字符串 → 同域 `/api/...`,由 Nginx 反代到 PHP。
* - 开发且未设置:空字符串 → 与 Vite 同源(默认 9999),由 `vite.config.ts` 把 `/api`、`/uploads` 代理到后端,避免 9999→9901 跨域导致 Failed to fetch。
*/
|
bc518174
王天杨
提交两个项目文件
|
16
|
export function apiBase(): string {
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
17
18
19
20
21
|
const raw = import.meta.env.VITE_API_BASE_URL;
if (typeof raw === "string" && raw.trim() !== "") {
return raw.replace(/\/$/, "");
}
return "";
|
bc518174
王天杨
提交两个项目文件
|
22
23
24
25
26
27
28
29
30
|
}
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 };
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
31
32
33
34
35
36
|
export type KioskBundle = KioskServerBundle;
/** 避免 ThinkPHP APP_DEBUG 下 trace 在 JSON 后追加脚本导致整段无法 parse */
function parseJsonTextLoose(text: string): unknown {
const trimmed = text.trim();
if (!trimmed) return {};
|
bc518174
王天杨
提交两个项目文件
|
37
|
try {
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
38
|
return JSON.parse(trimmed);
|
bc518174
王天杨
提交两个项目文件
|
39
|
} catch {
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
40
41
42
43
44
45
46
47
48
|
const s = trimmed.indexOf("{");
const e = trimmed.lastIndexOf("}");
if (s >= 0 && e > s) {
try {
return JSON.parse(trimmed.slice(s, e + 1));
} catch {
return {};
}
}
|
bc518174
王天杨
提交两个项目文件
|
49
50
51
52
|
return {};
}
}
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
53
54
55
56
57
58
59
60
61
62
63
64
65
|
async function parseBody(res: Response): Promise<unknown> {
const t = await res.text();
return parseJsonTextLoose(t);
}
export function apiCodeOk(code: unknown): boolean {
return code === 0 || code === "0";
}
function formatSyncError(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
|
bc518174
王天杨
提交两个项目文件
|
66
67
68
|
export async function kioskGet<T>(path: string): Promise<ApiResult<T>> {
const res = await fetch(apiUrl(path), {
method: "GET",
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
69
70
71
72
|
headers: {
Accept: "application/json",
"X-Requested-With": "XMLHttpRequest",
},
|
bc518174
王天杨
提交两个项目文件
|
73
74
75
76
77
78
79
80
|
});
const body = (await parseBody(res)) as ApiResult<T>;
if (!res.ok && body.code === undefined) {
return { code: res.status, msg: res.statusText };
}
return body;
}
|
bc518174
王天杨
提交两个项目文件
|
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
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;
}>;
};
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
115
|
/** 从后端拉取 bundle 并写入内存(不写 localStorage) */
|
bc518174
王天杨
提交两个项目文件
|
116
117
|
export async function fetchAndApplyKioskBundle(): Promise<boolean> {
const res = await kioskGet<KioskBundle>("/api/kiosk/bundle");
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
118
|
if (!apiCodeOk(res.code) || !res.data) {
|
bc518174
王天杨
提交两个项目文件
|
119
120
|
return false;
}
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
121
|
applyServerBundle(res.data);
|
bc518174
王天杨
提交两个项目文件
|
122
123
124
125
126
|
return true;
}
export async function fetchDataDisplay(): Promise<DataDisplayPayload | null> {
const res = await kioskGet<DataDisplayPayload>("/api/kiosk/data-display");
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
127
|
if (!apiCodeOk(res.code) || !res.data) return null;
|
bc518174
王天杨
提交两个项目文件
|
128
129
130
131
132
|
return res.data;
}
export async function fetchObservatoryHistory(): Promise<ObservatoryHistoryPayload["items"] | null> {
const res = await kioskGet<ObservatoryHistoryPayload>("/api/kiosk/observatory-history");
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
133
|
if (!apiCodeOk(res.code) || !res.data?.items) return null;
|
bc518174
王天杨
提交两个项目文件
|
134
135
136
137
138
|
return res.data.items;
}
function adminHeaders(): HeadersInit {
const token = import.meta.env.VITE_ADMIN_API_TOKEN ?? "";
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
const h: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
"X-Requested-With": "XMLHttpRequest",
};
if (token) h["X-Admin-Token"] = token;
return h;
}
/** multipart 上传勿带 Content-Type,由浏览器自动带 boundary */
function adminHeadersMultipart(): HeadersInit {
const token = import.meta.env.VITE_ADMIN_API_TOKEN ?? "";
const h: Record<string, string> = {
Accept: "application/json",
"X-Requested-With": "XMLHttpRequest",
};
|
bc518174
王天杨
提交两个项目文件
|
155
156
157
158
|
if (token) h["X-Admin-Token"] = token;
return h;
}
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
|
/** 知识库 image / videoUrl:data URL、绝对 URL 原样返回;否则拼 API 根(开发直连后端、生产同域相对路径) */
export function resolveKioskMediaUrl(stored: string | undefined | null): string {
if (stored == null) return "";
const s = stored.trim();
if (s === "") return "";
if (s.startsWith("data:") || s.startsWith("http://") || s.startsWith("https://")) return s;
const base = apiBase();
if (s.startsWith("/")) return base ? `${base}${s}` : s;
return base ? `${base}/${s}` : `/${s}`;
}
/** 上传封面或视频,成功返回 `/uploads/knowledge/...`;失败 throw(含服务器 msg) */
export async function uploadKnowledgeMediaFile(file: File): Promise<string> {
const fd = new FormData();
fd.append("file", file);
let res: Response;
try {
res = await fetch(apiUrl("/api/admin/knowledge/upload-media"), {
method: "POST",
headers: adminHeadersMultipart(),
body: fd,
});
} catch (e) {
throw new Error(
`无法连接上传接口(${formatSyncError(e)})。请确认后端已启动且可访问 ${apiUrl("/api/admin/knowledge/upload-media")}。`
);
}
const body = (await parseBody(res)) as ApiResult<{ url?: string }> & { url?: unknown };
const fromData = body.data?.url;
const fromTop = body.url;
const url =
typeof fromData === "string" && fromData
? fromData
: typeof fromTop === "string" && fromTop
? fromTop
: "";
if (!res.ok || !apiCodeOk(body.code) || !url) {
const detail =
typeof body.msg === "string" && body.msg.trim() !== ""
? body.msg
: `HTTP ${res.status} · ${JSON.stringify(body).slice(0, 400)}`;
throw new Error(`上传失败:${detail}`);
}
return url;
|
bc518174
王天杨
提交两个项目文件
|
203
204
205
206
207
208
209
210
211
|
}
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>;
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
212
|
return res.ok && apiCodeOk(body.code);
|
bc518174
王天杨
提交两个项目文件
|
213
214
215
216
217
218
219
220
221
|
}
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>;
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
222
|
return res.ok && apiCodeOk(body.code);
|
bc518174
王天杨
提交两个项目文件
|
223
224
225
226
227
228
229
230
231
|
}
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>;
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
232
|
return res.ok && apiCodeOk(body.code);
|
bc518174
王天杨
提交两个项目文件
|
233
234
|
}
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
/**
* 全量同步知识库。成功时 resolve;失败时 throw(网络、序列化、或接口返回错误),便于展示具体原因。
*/
export async function pushKnowledgeToServer(entries: KnowledgeEntry[]): Promise<void> {
let body: string;
try {
body = JSON.stringify({ entries });
} catch (e) {
throw new Error(
`知识库 JSON 序列化失败(常见原因:库里仍有旧版写入的超大 base64 图片/视频,整表一起提交时超出浏览器限制):${formatSyncError(e)}`
);
}
const approxMb = Math.round(body.length / (1024 * 1024));
if (body.length > 60 * 1024 * 1024) {
throw new Error(
`本次同步请求体约 ${approxMb}MB,过大。请删除或编辑仍含「data:image/…」「data:video/…」大内联媒体的条目后重试,或调大 PHP post_max_size / Nginx client_max_body_size。`
);
}
let res: Response;
try {
res = await fetch(apiUrl("/api/admin/knowledge/sync"), {
method: "POST",
headers: adminHeaders(),
body,
});
} catch (e) {
throw new Error(
`无法连接保存接口(${formatSyncError(e)})。说明:页面在 ${typeof window !== "undefined" ? window.location.origin : ""},接口在 ${apiUrl("/api/admin/knowledge/sync")};本机开发请运行 daocheng-api(composer run dev 或 php think run -p 9901),并与 .env.development 中 VITE_DEV_PROXY_TARGET 端口一致。`
);
}
const parsed = (await parseBody(res)) as ApiResult<unknown>;
if (!res.ok || !apiCodeOk(parsed.code)) {
const detail =
typeof parsed.msg === "string" && parsed.msg.trim() !== ""
? parsed.msg
: `${res.status} ${res.statusText}`.trim();
throw new Error(`服务器拒绝保存:${detail}`);
}
}
export async function pushVideoSourceToServer(cfg: VideoSourceConfig): Promise<boolean> {
const res = await fetch(apiUrl("/api/admin/kiosk/video-source"), {
|
bc518174
王天杨
提交两个项目文件
|
279
280
|
method: "POST",
headers: adminHeaders(),
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
body: JSON.stringify({
protocol: cfg.protocol,
url: cfg.url,
note: cfg.note,
}),
});
const body = (await parseBody(res)) as ApiResult<unknown>;
return res.ok && apiCodeOk(body.code);
}
export async function pushCarouselBackgroundsToServer(urls: string[]): Promise<boolean> {
const res = await fetch(apiUrl("/api/admin/kiosk/carousel-backgrounds"), {
method: "POST",
headers: adminHeaders(),
body: JSON.stringify({ urls }),
|
bc518174
王天杨
提交两个项目文件
|
296
297
|
});
const body = (await parseBody(res)) as ApiResult<unknown>;
|
3a3dc915
王天杨
feat: 稻城亚丁项目批量更新
|
298
|
return res.ok && apiCodeOk(body.code);
|
bc518174
王天杨
提交两个项目文件
|
299
|
}
|