chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { get, post } from '../api/http';
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
level?: 'info' | 'warn' | 'danger';
|
||||
severity?: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
message?: string;
|
||||
houseId?: string;
|
||||
deviceId?: string;
|
||||
deviceKey?: string;
|
||||
read?: boolean;
|
||||
acknowledged?: boolean;
|
||||
open?: boolean;
|
||||
createdAt?: string;
|
||||
triggeredAt?: string;
|
||||
}
|
||||
|
||||
export interface AlarmClip {
|
||||
id: string;
|
||||
playbackUrl?: string;
|
||||
cameraId?: string;
|
||||
startAt?: string;
|
||||
endAt?: string;
|
||||
mock?: boolean;
|
||||
}
|
||||
|
||||
const toList = (data: Alert[] | { items: Alert[]; total?: number }) => {
|
||||
const items = Array.isArray(data) ? data : data.items;
|
||||
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
|
||||
};
|
||||
|
||||
export const listAlerts = async (params?: any) =>
|
||||
toList(await get<Alert[] | { items: Alert[]; total?: number }>('/alarms', { params }));
|
||||
export const markAlertRead = (id: string) => post(`/alarms/${id}/ack`);
|
||||
export const markAllRead = async () => {
|
||||
const res = await listAlerts({ openOnly: true });
|
||||
await Promise.all(res.items.filter((item) => !item.acknowledged).map((item) => markAlertRead(item.id)));
|
||||
return { ok: true };
|
||||
};
|
||||
export const getAlertClip = (id: string) => get<AlarmClip>(`/alarms/${id}/clip`);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { get, post } from '../api/http';
|
||||
|
||||
export interface LoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResp {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
token?: string;
|
||||
user: { id: string; username?: string; name?: string; fullName?: string; role?: string; permissions?: string[] };
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export const loginApi = (payload: LoginPayload) =>
|
||||
post<LoginResp>('/auth/login', payload, { silent: true });
|
||||
|
||||
export const logoutApi = () => post('/auth/logout', {});
|
||||
|
||||
export const changePasswordApi = (payload: ChangePasswordPayload) =>
|
||||
post('/auth/change-password', payload);
|
||||
|
||||
export interface MeResp {
|
||||
sub: string;
|
||||
username: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export const meApi = () => get<MeResp>('/auth/me');
|
||||
@@ -0,0 +1,148 @@
|
||||
import { get } from '../api/http';
|
||||
|
||||
export interface Room {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
location?: string;
|
||||
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
|
||||
}
|
||||
|
||||
export interface RealtimeMetric {
|
||||
key: string;
|
||||
name: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
status: 'normal' | 'warn' | 'danger';
|
||||
}
|
||||
|
||||
export interface TrendPoint {
|
||||
time: string;
|
||||
temp?: number;
|
||||
humidity?: number;
|
||||
co2?: number;
|
||||
}
|
||||
|
||||
export interface DeviceSummary {
|
||||
total: number;
|
||||
online: number;
|
||||
offline: number;
|
||||
alarm: number;
|
||||
/** 蚕房运行中数量(status=running/active) */
|
||||
running: number;
|
||||
/** 蚕房停用数量(status=inactive) */
|
||||
inactive: number;
|
||||
}
|
||||
|
||||
interface TelemetryRecord {
|
||||
id?: string;
|
||||
deviceKey: string;
|
||||
metric: string;
|
||||
value: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const METRIC_NAME: Record<string, string> = {
|
||||
temperature: '温度',
|
||||
temp: '温度',
|
||||
humidity: '湿度',
|
||||
co2: 'CO₂',
|
||||
light: '光照',
|
||||
ph: 'PH',
|
||||
};
|
||||
|
||||
const METRIC_UNIT: Record<string, string> = {
|
||||
temperature: '℃',
|
||||
temp: '℃',
|
||||
humidity: '%',
|
||||
co2: 'ppm',
|
||||
light: 'lux',
|
||||
ph: '',
|
||||
};
|
||||
|
||||
const toArray = <T>(data: T[] | { items?: T[] }) => (Array.isArray(data) ? data : data.items ?? []);
|
||||
|
||||
const normalizeMetricKey = (metric: string) => (metric === 'temperature' ? 'temp' : metric);
|
||||
|
||||
const metricStatus = (key: string, value: number): RealtimeMetric['status'] => {
|
||||
if (key === 'temp') return value < 20 || value > 30 ? 'danger' : value < 22 || value > 28 ? 'warn' : 'normal';
|
||||
if (key === 'humidity') return value < 55 || value > 85 ? 'danger' : value < 60 || value > 80 ? 'warn' : 'normal';
|
||||
if (key === 'co2') return value > 1500 ? 'danger' : value > 1000 ? 'warn' : 'normal';
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
const normalizeRealtime = (records: TelemetryRecord[]): RealtimeMetric[] => {
|
||||
const latestByMetric = new Map<string, TelemetryRecord>();
|
||||
for (const record of records) {
|
||||
const key = normalizeMetricKey(record.metric);
|
||||
const prev = latestByMetric.get(key);
|
||||
if (!prev || new Date(record.timestamp).getTime() > new Date(prev.timestamp).getTime()) {
|
||||
latestByMetric.set(key, record);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(latestByMetric.entries()).map(([key, record]) => ({
|
||||
key,
|
||||
name: METRIC_NAME[key] || record.metric,
|
||||
value: Number(record.value),
|
||||
unit: METRIC_UNIT[key] || '',
|
||||
status: metricStatus(key, Number(record.value)),
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchRooms = async () => toArray(await get<Room[] | { items: Room[] }>('/rooms', { silent: true }));
|
||||
|
||||
export const fetchRealtime = async () => {
|
||||
const records = await get<TelemetryRecord[]>('/telemetry', {
|
||||
params: { limit: 100 },
|
||||
silent: true,
|
||||
});
|
||||
return normalizeRealtime(records);
|
||||
};
|
||||
|
||||
export const fetchRoomRealtime = async (roomId: string) => {
|
||||
try {
|
||||
const data = await get<TelemetryRecord[] | RealtimeMetric[]>(`/rooms/${roomId}/telemetry/latest`, { silent: true });
|
||||
const arr = toArray(data as TelemetryRecord[] | { items: TelemetryRecord[] });
|
||||
if (arr.length > 0 && 'metric' in arr[0]) return normalizeRealtime(arr as TelemetryRecord[]);
|
||||
return arr as unknown as RealtimeMetric[];
|
||||
} catch {
|
||||
return fetchRealtime();
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTrend = async (hours = 24) => {
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - hours * 3600 * 1000);
|
||||
const records = await get<TelemetryRecord[]>('/telemetry', {
|
||||
params: { from: from.toISOString(), to: to.toISOString(), limit: 1000 },
|
||||
silent: true,
|
||||
});
|
||||
return records
|
||||
.slice()
|
||||
.reverse()
|
||||
.reduce<TrendPoint[]>((acc, record) => {
|
||||
const key = normalizeMetricKey(record.metric);
|
||||
if (!['temp', 'humidity', 'co2'].includes(key)) return acc;
|
||||
const time = new Date(record.timestamp).toLocaleTimeString().slice(0, 5);
|
||||
const point = acc.find((item) => item.time === time) ?? { time };
|
||||
(point as unknown as Record<string, string | number | undefined>)[key] = Number(record.value);
|
||||
if (!acc.includes(point)) acc.push(point);
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const fetchDeviceSummary = async (): Promise<DeviceSummary> => {
|
||||
const [rooms, alarms] = await Promise.all([
|
||||
fetchRooms().catch(() => []),
|
||||
get<any[]>('/alarms', { params: { openOnly: true }, silent: true }).catch(() => []),
|
||||
]);
|
||||
return {
|
||||
total: rooms.length,
|
||||
online: rooms.filter((room) => room.status !== 'inactive').length,
|
||||
offline: rooms.filter((room) => room.status === 'inactive').length,
|
||||
running: rooms.filter((room) => room.status === 'running' || room.status === 'active').length,
|
||||
inactive: rooms.filter((room) => room.status === 'inactive').length,
|
||||
alarm: alarms.length,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { get, post, patch, del } from '../api/http';
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
deviceKey?: string;
|
||||
name: string;
|
||||
type?: 'sensor' | 'actuator' | 'gateway';
|
||||
kind?: 'sensor' | 'actuator' | 'gateway';
|
||||
houseId?: string;
|
||||
roomId?: string;
|
||||
status?: 'online' | 'offline' | 'unknown';
|
||||
onlineStatus?: 'online' | 'offline' | 'unknown';
|
||||
lastSeen?: string;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
const toList = (data: Device[] | { items: Device[]; total?: number }) => {
|
||||
const items = Array.isArray(data) ? data : data.items;
|
||||
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
|
||||
};
|
||||
|
||||
const toBackendDevice = (data: Partial<Device>) => ({
|
||||
...data,
|
||||
kind: data.kind || data.type,
|
||||
roomId: data.roomId || data.houseId,
|
||||
});
|
||||
|
||||
export const listDevices = async (params?: any) =>
|
||||
toList(await get<Device[] | { items: Device[]; total?: number }>('/devices', { params }));
|
||||
export const getDevice = (id: string) => get<Device>(`/devices/${id}`);
|
||||
export const createDevice = (data: Partial<Device>) =>
|
||||
post<Device>('/devices', toBackendDevice(data));
|
||||
export const updateDevice = (id: string, data: Partial<Device>) =>
|
||||
patch<Device>(`/devices/${id}`, toBackendDevice(data));
|
||||
export const deleteDevice = (id: string) => del(`/devices/${id}`);
|
||||
export const sendDeviceCommand = (deviceKey: string, action: string, payload?: any) =>
|
||||
post<{ ok?: boolean; success?: boolean }>('/control/send', { deviceKey, action, payload });
|
||||
@@ -0,0 +1,14 @@
|
||||
import { get } from '../api/http';
|
||||
|
||||
export interface ControlLog {
|
||||
id: string;
|
||||
deviceId?: string;
|
||||
deviceName?: string;
|
||||
command: string;
|
||||
operator?: string;
|
||||
success?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const listLogs = (params?: any) =>
|
||||
get<{ items: ControlLog[]; total: number }>('/logs/control', { params });
|
||||
@@ -0,0 +1,27 @@
|
||||
import { get, post, patch, del } from '../api/http';
|
||||
|
||||
export interface SilkwormHouse {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
location?: string;
|
||||
description?: string;
|
||||
status?: 'active' | 'inactive' | 'running' | 'idle' | 'alarm';
|
||||
capacity?: number;
|
||||
stage?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
const toList = (data: SilkwormHouse[] | { items: SilkwormHouse[]; total?: number }) => {
|
||||
const items = Array.isArray(data) ? data : data.items;
|
||||
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
|
||||
};
|
||||
|
||||
export const listHouses = async (params?: any) =>
|
||||
toList(await get<SilkwormHouse[] | { items: SilkwormHouse[]; total?: number }>('/rooms', { params }));
|
||||
export const getHouse = (id: string) => get<SilkwormHouse>(`/rooms/${id}`);
|
||||
export const createHouse = (data: Partial<SilkwormHouse>) =>
|
||||
post<SilkwormHouse>('/rooms', data);
|
||||
export const updateHouse = (id: string, data: Partial<SilkwormHouse>) =>
|
||||
patch<SilkwormHouse>(`/rooms/${id}`, data);
|
||||
export const deleteHouse = (id: string) => del(`/rooms/${id}`);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { get, post, patch, del } from '../api/http';
|
||||
|
||||
export interface Threshold {
|
||||
id: string;
|
||||
name?: string;
|
||||
metric?: string;
|
||||
houseId?: string;
|
||||
roomId?: string;
|
||||
sensorId?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
debounceSeconds?: number;
|
||||
severity?: number;
|
||||
unit?: string;
|
||||
enabled?: boolean;
|
||||
sensor?: { id: string; name?: string; metric?: string; unit?: string };
|
||||
}
|
||||
|
||||
const toList = (data: Threshold[] | { items: Threshold[]; total?: number }) => {
|
||||
const items = Array.isArray(data) ? data : data.items;
|
||||
return { items, total: Array.isArray(data) ? data.length : data.total ?? items.length };
|
||||
};
|
||||
|
||||
const toBackendThreshold = (data: Partial<Threshold>) => ({
|
||||
...data,
|
||||
minValue: data.minValue ?? data.min,
|
||||
maxValue: data.maxValue ?? data.max,
|
||||
});
|
||||
|
||||
export const listThresholds = async (params?: any) =>
|
||||
toList(await get<Threshold[] | { items: Threshold[]; total?: number }>('/thresholds', { params }));
|
||||
export const createThreshold = (data: Partial<Threshold>) =>
|
||||
post<Threshold>('/thresholds', toBackendThreshold(data));
|
||||
export const updateThreshold = (id: string, data: Partial<Threshold>) =>
|
||||
patch<Threshold>(`/thresholds/${id}`, toBackendThreshold(data));
|
||||
export const deleteThreshold = (id: string) => del(`/thresholds/${id}`);
|
||||
@@ -0,0 +1,217 @@
|
||||
import { del, get, patch, post } from '../api/http';
|
||||
|
||||
export interface Camera {
|
||||
id: string;
|
||||
name: string;
|
||||
online?: boolean;
|
||||
isOnline?: boolean;
|
||||
enabled?: boolean;
|
||||
roomId?: string;
|
||||
streamUrl?: string;
|
||||
rtspUrl?: string;
|
||||
httpUrl?: string;
|
||||
hlsUrl?: string;
|
||||
flvUrl?: string;
|
||||
webrtcUrl?: string;
|
||||
snapshotUrl?: string;
|
||||
username?: string;
|
||||
passwordEnc?: string;
|
||||
position?: string;
|
||||
resolution?: string;
|
||||
fps?: number;
|
||||
gbDeviceId?: string;
|
||||
gbChannelId?: string;
|
||||
gbAuthId?: string;
|
||||
gbAuthPassword?: string;
|
||||
gbStreamType?: string;
|
||||
gbTransport?: string;
|
||||
gbAlarmChannelId?: string;
|
||||
gbVoiceChannelId?: string;
|
||||
gbManufacturer?: string;
|
||||
manufacturerId?: string;
|
||||
}
|
||||
|
||||
export interface WvpSipConfig {
|
||||
sipId: string;
|
||||
sipDomain: string;
|
||||
sipPassword: string;
|
||||
sipPort: number;
|
||||
sipShowIp: string;
|
||||
}
|
||||
|
||||
export interface PlayInfo {
|
||||
cameraId: string;
|
||||
code?: string;
|
||||
format: 'hls' | 'flv' | 'webrtc';
|
||||
url: string;
|
||||
expiresAt?: string;
|
||||
mock?: boolean;
|
||||
gbDeviceId?: string | null;
|
||||
gbChannelId?: string | null;
|
||||
}
|
||||
|
||||
export interface LiveInfo {
|
||||
cameraId: string;
|
||||
gbDeviceId: string | null;
|
||||
gbChannelId: string | null;
|
||||
format: 'hls' | 'flv' | 'webrtc';
|
||||
url: string;
|
||||
expiresAt?: string;
|
||||
mock?: boolean;
|
||||
}
|
||||
|
||||
export interface VideoClip {
|
||||
id: string;
|
||||
cameraId: string;
|
||||
trigger: 'alarm' | 'manual' | 'schedule' | 'motion';
|
||||
format: 'hls' | 'flv' | 'webrtc' | 'mp4';
|
||||
startAt: string;
|
||||
endAt?: string;
|
||||
durationSec: number;
|
||||
resolution?: string;
|
||||
sizeBytes?: string | number;
|
||||
playbackUrl?: string;
|
||||
mock?: boolean;
|
||||
}
|
||||
|
||||
export interface ClipListResponse {
|
||||
items: VideoClip[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ClipPlayInfo {
|
||||
clipId: string;
|
||||
url: string;
|
||||
format: string;
|
||||
expiresAt?: string;
|
||||
}
|
||||
|
||||
export interface ClipQuery {
|
||||
cameraId?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const toArray = <T>(data: T[] | { items?: T[] }) => (Array.isArray(data) ? data : data.items ?? []);
|
||||
|
||||
export const listCameras = async () => toArray(await get<Camera[] | { items: Camera[] }>('/video/cameras'));
|
||||
|
||||
export const playCamera = (id: string, format: PlayInfo['format'] = 'hls') =>
|
||||
post<PlayInfo>(`/video/cameras/${id}/play`, { format });
|
||||
|
||||
export const getLiveUrl = (id: string, format: 'hls' | 'flv' | 'webrtc' = 'hls') =>
|
||||
post<LiveInfo>(`/video/cameras/${id}/live`, { format });
|
||||
|
||||
export const listClips = (query: ClipQuery = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (query.cameraId) params.set('cameraId', query.cameraId);
|
||||
if (query.from) params.set('from', query.from);
|
||||
if (query.to) params.set('to', query.to);
|
||||
if (query.limit) params.set('limit', String(query.limit));
|
||||
const qs = params.toString();
|
||||
return get<ClipListResponse>(`/video/clips${qs ? `?${qs}` : ''}`);
|
||||
};
|
||||
|
||||
export const getClipPlayUrl = (clipId: string) =>
|
||||
get<ClipPlayInfo>(`/video/clips/${clipId}/play`);
|
||||
|
||||
export const createCamera = (data: Partial<Camera>) =>
|
||||
post<Camera>('/video/cameras', data);
|
||||
|
||||
export const updateCamera = (id: string, data: Partial<Camera>) =>
|
||||
patch<Camera>(`/video/cameras/${id}`, data);
|
||||
|
||||
export const deleteCamera = (id: string) =>
|
||||
del<{ id: string }>(`/video/cameras/${id}`);
|
||||
|
||||
export const getWvpConfig = () =>
|
||||
get<WvpSipConfig>('/video/wvp-config');
|
||||
|
||||
export interface WvpDevice {
|
||||
id: number;
|
||||
deviceId: string;
|
||||
name: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
onLine: boolean;
|
||||
transport: string;
|
||||
streamMode: string;
|
||||
hostAddress: string;
|
||||
}
|
||||
|
||||
export interface WvpChannel {
|
||||
id: number;
|
||||
deviceId: string;
|
||||
channelId: string;
|
||||
name: string;
|
||||
onLine: boolean;
|
||||
}
|
||||
|
||||
export const listWvpDevices = (query?: string) =>
|
||||
get<WvpDevice[]>(`/video/wvp/devices${query ? `?query=${encodeURIComponent(query)}` : ''}`);
|
||||
|
||||
export const listWvpChannels = (deviceId: string) =>
|
||||
get<WvpChannel[]>(`/video/wvp/devices/${encodeURIComponent(deviceId)}/channels`);
|
||||
|
||||
export const syncWvpDevice = (deviceId: string) =>
|
||||
post<{ ok: boolean }>(`/video/wvp/devices/${encodeURIComponent(deviceId)}/sync`);
|
||||
|
||||
export interface ActiveRecording {
|
||||
cameraId: string;
|
||||
deviceId: string;
|
||||
channelId: string;
|
||||
stream: string;
|
||||
app: string;
|
||||
startedAt: string;
|
||||
}
|
||||
|
||||
/** 开始录制 */
|
||||
export const startRecording = (cameraId: string) =>
|
||||
post<{ ok: boolean }>(`/video/cameras/${cameraId}/record/start`);
|
||||
|
||||
/** 停止录制并归档 */
|
||||
export const stopRecording = (cameraId: string) =>
|
||||
post<VideoClip>(`/video/cameras/${cameraId}/record/stop`);
|
||||
|
||||
/** 获取正在录制的摄像头 */
|
||||
export const getActiveRecordings = () =>
|
||||
get<ActiveRecording[]>(`/video/recordings/active`);
|
||||
|
||||
// ===== 存储状态 =====
|
||||
export interface CephStorageData {
|
||||
df: {
|
||||
stats: {
|
||||
total_bytes: number;
|
||||
total_used_bytes: number;
|
||||
total_avail_bytes: number;
|
||||
};
|
||||
pools: Array<{
|
||||
name: string;
|
||||
id: number;
|
||||
stats: {
|
||||
stored: number;
|
||||
objects: number;
|
||||
max_avail: number;
|
||||
percent_used: number;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
osdTree: {
|
||||
nodes: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
weight: number;
|
||||
class?: string;
|
||||
}>;
|
||||
};
|
||||
health: {
|
||||
status: string;
|
||||
checks?: Record<string, { severity: string; summary: { message: string; count: number } }>;
|
||||
};
|
||||
}
|
||||
|
||||
export const getCephStorage = () =>
|
||||
get<CephStorageData>('/storage/ceph');
|
||||
Reference in New Issue
Block a user