init: AI培训与智能巡检系统
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import type {
|
||||
AuthResult,
|
||||
Board,
|
||||
Category,
|
||||
EditableField,
|
||||
Family,
|
||||
Identification,
|
||||
LeaderboardEntry,
|
||||
Maintainer,
|
||||
ModelDetail,
|
||||
ModelListItem,
|
||||
ModelQuery,
|
||||
Paged,
|
||||
Photo,
|
||||
PublicUser,
|
||||
Revision,
|
||||
SearchResult,
|
||||
Sighting,
|
||||
Spot,
|
||||
Stats,
|
||||
Thread,
|
||||
UserStats,
|
||||
} from '../types';
|
||||
|
||||
const BASE = import.meta.env?.VITE_API_BASE ?? '';
|
||||
const TOKEN_KEY = 'train.token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function setToken(t: string) {
|
||||
try {
|
||||
localStorage.setItem(TOKEN_KEY, t);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
export function clearToken() {
|
||||
try {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** 把查询对象转为 URLSearchParams 字符串(剔除 null/undefined/空串)。*/
|
||||
export function buildQuery(params: Record<string, unknown>): string {
|
||||
const sp = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v === undefined || v === null || v === '') continue;
|
||||
sp.set(k, String(v));
|
||||
}
|
||||
const s = sp.toString();
|
||||
return s ? `?${s}` : '';
|
||||
}
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
const t = getToken();
|
||||
return t ? { Authorization: `Bearer ${t}` } : {};
|
||||
}
|
||||
|
||||
async function getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, { headers: { ...authHeaders() } });
|
||||
if (!res.ok) throw new Error(`请求失败 ${res.status}: ${path}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function postJson<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg = Array.isArray((data as any)?.message)
|
||||
? (data as any).message.join(';')
|
||||
: (data as any)?.message || `请求失败 ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function deleteJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: { ...authHeaders() },
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((data as any)?.message || `请求失败 ${res.status}`);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function patchJson<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((data as any)?.message || `请求失败 ${res.status}`);
|
||||
return data as T;
|
||||
}
|
||||
|
||||
async function postForm<T>(path: string, form: FormData): Promise<T> {
|
||||
// 不手动设置 Content-Type,让浏览器带上 multipart boundary
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { ...authHeaders() },
|
||||
body: form,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg = Array.isArray((data as any)?.message)
|
||||
? (data as any).message.join(';')
|
||||
: (data as any)?.message || `上传失败 ${res.status}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
categories: () => getJson<Category[]>('/api/categories'),
|
||||
|
||||
models: (q: ModelQuery = {}) =>
|
||||
getJson<Paged<ModelListItem>>(
|
||||
`/api/models${buildQuery(q as Record<string, unknown>)}`,
|
||||
),
|
||||
|
||||
model: (id: number) => getJson<ModelDetail>(`/api/models/${id}`),
|
||||
|
||||
search: (q: string, limit = 10) =>
|
||||
getJson<{ query: string; results: SearchResult[] }>(
|
||||
`/api/search${buildQuery({ q, limit })}`,
|
||||
),
|
||||
|
||||
auth: {
|
||||
register: (email: string, password: string, displayName: string) =>
|
||||
postJson<AuthResult>('/api/auth/register', { email, password, displayName }),
|
||||
login: (email: string, password: string) =>
|
||||
postJson<AuthResult>('/api/auth/login', { email, password }),
|
||||
me: () => getJson<UserStats>('/api/auth/me'),
|
||||
},
|
||||
|
||||
editableFields: () => getJson<EditableField[]>('/api/editable-fields'),
|
||||
modelRevisions: (id: number) => getJson<Revision[]>(`/api/models/${id}/revisions`),
|
||||
submitRevision: (id: number, changes: Record<string, string>, note: string) =>
|
||||
postJson<Revision>(`/api/models/${id}/revisions`, { changes, note }),
|
||||
pendingRevisions: () => getJson<Revision[]>('/api/revisions/pending'),
|
||||
approveRevision: (rid: number) =>
|
||||
postJson<Revision>(`/api/revisions/${rid}/approve`, {}),
|
||||
rejectRevision: (rid: number) =>
|
||||
postJson<Revision>(`/api/revisions/${rid}/reject`, {}),
|
||||
|
||||
leaderboard: (limit = 20) =>
|
||||
getJson<LeaderboardEntry[]>(`/api/leaderboard${buildQuery({ limit })}`),
|
||||
maintainers: (id: number) => getJson<Maintainer[]>(`/api/models/${id}/maintainers`),
|
||||
claimMaintainer: (id: number) =>
|
||||
postJson<Maintainer[]>(`/api/models/${id}/maintainers`, {}),
|
||||
unclaimMaintainer: (id: number) =>
|
||||
deleteJson<Maintainer[]>(`/api/models/${id}/maintainers`),
|
||||
|
||||
boards: () => getJson<Board[]>('/api/boards'),
|
||||
threads: (params: { board?: string; modelId?: number } = {}) =>
|
||||
getJson<Thread[]>(`/api/threads${buildQuery(params)}`),
|
||||
thread: (id: number) => getJson<Thread>(`/api/threads/${id}`),
|
||||
createThread: (data: {
|
||||
board: string;
|
||||
modelId?: number;
|
||||
title: string;
|
||||
body: string;
|
||||
}) => postJson<Thread>('/api/threads', data),
|
||||
addReply: (id: number, body: string) =>
|
||||
postJson<Thread>(`/api/threads/${id}/replies`, { body }),
|
||||
|
||||
modelSightings: (id: number) =>
|
||||
getJson<Sighting[]>(`/api/models/${id}/sightings`),
|
||||
createSighting: (
|
||||
id: number,
|
||||
data: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
station?: string;
|
||||
carNumber?: string;
|
||||
spottedAt?: string;
|
||||
description?: string;
|
||||
},
|
||||
) => postJson<Sighting>(`/api/models/${id}/sightings`, data),
|
||||
recentSightings: (limit = 30) =>
|
||||
getJson<Sighting[]>(`/api/sightings/recent${buildQuery({ limit })}`),
|
||||
sightingsMap: () => getJson<Sighting[]>('/api/sightings/map'),
|
||||
spots: () => getJson<Spot[]>('/api/sightings/spots'),
|
||||
families: (category?: string) =>
|
||||
getJson<Family[]>(`/api/models/families${buildQuery({ category: category ?? '' })}`),
|
||||
stats: () => getJson<Stats>('/api/stats'),
|
||||
|
||||
modelPhotos: (id: number) => getJson<Photo[]>(`/api/models/${id}/photos`),
|
||||
uploadPhoto: (id: number, file: File, caption = '') => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (caption) fd.append('caption', caption);
|
||||
return postForm<Photo>(`/api/models/${id}/photos`, fd);
|
||||
},
|
||||
deletePhoto: (pid: number) => deleteJson<{ ok: boolean }>(`/api/photos/${pid}`),
|
||||
confirmPhoto: (pid: number) => postJson<Photo>(`/api/photos/${pid}/confirm`, {}),
|
||||
featurePhoto: (pid: number) => postJson<Photo>(`/api/photos/${pid}/feature`, {}),
|
||||
candidatePhotos: () =>
|
||||
getJson<(Photo & { modelCode: string; category: string })[]>(
|
||||
'/api/photos/candidates',
|
||||
),
|
||||
|
||||
identify: (file: File) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return postForm<Identification>('/api/identify', fd);
|
||||
},
|
||||
identifications: () => getJson<Identification[]>('/api/identifications'),
|
||||
updateIdentification: (id: number, note: string) =>
|
||||
patchJson<Identification>(`/api/identifications/${id}`, { note }),
|
||||
deleteIdentification: (id: number) =>
|
||||
deleteJson<{ ok: boolean }>(`/api/identifications/${id}`),
|
||||
};
|
||||
Reference in New Issue
Block a user