chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
export function formatDateTime(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
export function formatDate(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
export function formatTime(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('HH:mm:ss');
|
||||
}
|
||||
|
||||
export function formatNumber(value: number | undefined, decimals: number = 1): string {
|
||||
if (value === undefined || value === null) return '-';
|
||||
return Number(value).toFixed(decimals);
|
||||
}
|
||||
|
||||
export function formatRelativeTime(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
const now = dayjs();
|
||||
const target = dayjs(date);
|
||||
const diff = now.diff(target, 'second');
|
||||
|
||||
if (diff < 60) return '刚刚';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
|
||||
return `${Math.floor(diff / 86400)}天前`;
|
||||
}
|
||||
|
||||
export function metricLabel(metric: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
temperature: '温度',
|
||||
humidity: '湿度',
|
||||
co2: 'CO₂',
|
||||
light: '光照',
|
||||
pressure: '气压',
|
||||
pm25: 'PM2.5',
|
||||
soil_moisture: '土壤湿度',
|
||||
ph: 'pH值',
|
||||
};
|
||||
return labels[metric] || metric;
|
||||
}
|
||||
|
||||
export function metricUnit(metric: string): string {
|
||||
const units: Record<string, string> = {
|
||||
temperature: '°C',
|
||||
humidity: '%',
|
||||
co2: 'ppm',
|
||||
light: 'lux',
|
||||
pressure: 'hPa',
|
||||
pm25: 'μg/m³',
|
||||
soil_moisture: '%',
|
||||
ph: '',
|
||||
};
|
||||
return units[metric] || '';
|
||||
}
|
||||
|
||||
export function severityLabel(severity: string | number | undefined): string {
|
||||
if (severity === undefined || severity === null) return '-';
|
||||
const key = String(severity).toLowerCase();
|
||||
const labels: Record<string, string> = {
|
||||
critical: '严重',
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低',
|
||||
info: '信息',
|
||||
'1': '严重',
|
||||
'2': '高',
|
||||
'3': '中',
|
||||
'4': '低',
|
||||
'5': '信息',
|
||||
};
|
||||
return labels[key] || String(severity);
|
||||
}
|
||||
|
||||
export function severityColor(severity: string | number | undefined): string {
|
||||
if (severity === undefined || severity === null) return '#86909c';
|
||||
const key = String(severity).toLowerCase();
|
||||
const colors: Record<string, string> = {
|
||||
critical: '#f53f3f',
|
||||
high: '#ff7d00',
|
||||
medium: '#ffc408',
|
||||
low: '#00b42a',
|
||||
info: '#165dff',
|
||||
'1': '#f53f3f',
|
||||
'2': '#ff7d00',
|
||||
'3': '#ffc408',
|
||||
'4': '#00b42a',
|
||||
'5': '#165dff',
|
||||
};
|
||||
return colors[key] || '#86909c';
|
||||
}
|
||||
|
||||
export function deviceKindLabel(kind: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
sensor: '传感器',
|
||||
actuator: '执行器',
|
||||
camera: '摄像头',
|
||||
controller: '控制器',
|
||||
gateway: '网关',
|
||||
};
|
||||
return labels[kind] || kind;
|
||||
}
|
||||
|
||||
export function roomStatusLabel(status: string | undefined): string {
|
||||
const labels: Record<string, string> = {
|
||||
active: '运行中',
|
||||
inactive: '未启用',
|
||||
maintenance: '维护中',
|
||||
alarm: '告警中',
|
||||
};
|
||||
return labels[status || ''] || status || '未知';
|
||||
}
|
||||
|
||||
export function onlineStatusLabel(status: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
unknown: '未知',
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
// ===== 对齐 app 端的工具函数 =====
|
||||
|
||||
export function normalizeMetricKey(metric: string): string {
|
||||
return metric === 'temperature' ? 'temp' : metric;
|
||||
}
|
||||
|
||||
export function getMetricName(metric: string): string {
|
||||
return metricLabel(metric);
|
||||
}
|
||||
|
||||
export function getMetricUnit(metric: string): string {
|
||||
return metricUnit(metric);
|
||||
}
|
||||
|
||||
export function metricStatus(key: string, value: number): 'normal' | 'warn' | 'danger' {
|
||||
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';
|
||||
}
|
||||
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (!seconds || isNaN(seconds)) return '-';
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
return `${min}分${sec}秒`;
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number | string | undefined): string {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
const size = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes;
|
||||
if (isNaN(size)) return '-';
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { getWsUrl } from '@/api/config';
|
||||
|
||||
type MessageHandler = (data: unknown) => void;
|
||||
|
||||
class WebSocketManager {
|
||||
private connected: boolean = false;
|
||||
private handlers: Map<string, Set<MessageHandler>> = new Map();
|
||||
private token: string = '';
|
||||
private reconnectAttempts: number = 0;
|
||||
private maxReconnectAttempts: number = 5;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private isManualClose: boolean = false;
|
||||
|
||||
connect(token: string): void {
|
||||
this.token = token;
|
||||
this.isManualClose = false;
|
||||
this.doConnect();
|
||||
}
|
||||
|
||||
private doConnect(): void {
|
||||
if (!this.token) {
|
||||
console.warn('[WS] 无 token,无法连接 WebSocket');
|
||||
return;
|
||||
}
|
||||
|
||||
// 先关闭旧连接
|
||||
if (this.connected) {
|
||||
try {
|
||||
Taro.closeSocket();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
const url = getWsUrl(this.token);
|
||||
console.log('[WS] 正在连接:', url);
|
||||
|
||||
// 注册全局回调(每次调用会替换上一次的回调)
|
||||
Taro.onSocketOpen(() => {
|
||||
console.log('[WS] 连接已建立');
|
||||
this.connected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
});
|
||||
|
||||
Taro.onSocketMessage((res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data as string);
|
||||
const type = (data.type || data.event || 'default') as string;
|
||||
const handlers = this.handlers.get(type);
|
||||
if (handlers) {
|
||||
handlers.forEach((handler) => handler(data));
|
||||
}
|
||||
// 广播给通配符监听器
|
||||
const wildcardHandlers = this.handlers.get('*');
|
||||
if (wildcardHandlers) {
|
||||
wildcardHandlers.forEach((handler) => handler(data));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WS] 消息解析失败:', e, res.data);
|
||||
}
|
||||
});
|
||||
|
||||
Taro.onSocketClose(() => {
|
||||
console.log('[WS] 连接已关闭');
|
||||
this.connected = false;
|
||||
if (!this.isManualClose) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
Taro.onSocketError((err) => {
|
||||
console.error('[WS] 连接错误:', err);
|
||||
});
|
||||
|
||||
Taro.connectSocket({
|
||||
url,
|
||||
fail: (err) => {
|
||||
console.error('[WS] 连接失败:', err);
|
||||
this.scheduleReconnect();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.isManualClose) return;
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.warn('[WS] 达到最大重连次数,停止重连');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||
console.log(`[WS] ${delay}ms 后第 ${this.reconnectAttempts} 次重连...`);
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
}
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.doConnect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
on(type: string, handler: MessageHandler): () => void {
|
||||
if (!this.handlers.has(type)) {
|
||||
this.handlers.set(type, new Set());
|
||||
}
|
||||
this.handlers.get(type)!.add(handler);
|
||||
return () => {
|
||||
this.handlers.get(type)?.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
off(type: string, handler?: MessageHandler): void {
|
||||
if (handler) {
|
||||
this.handlers.get(type)?.delete(handler);
|
||||
} else {
|
||||
this.handlers.delete(type);
|
||||
}
|
||||
}
|
||||
|
||||
send(data: unknown): void {
|
||||
if (this.connected) {
|
||||
Taro.sendSocketMessage({
|
||||
data: typeof data === 'string' ? data : JSON.stringify(data),
|
||||
fail: (err) => {
|
||||
console.error('[WS] 发送失败:', err);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn('[WS] WebSocket 未连接,无法发送消息');
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.isManualClose = true;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.connected) {
|
||||
Taro.closeSocket();
|
||||
this.connected = false;
|
||||
}
|
||||
this.handlers.clear();
|
||||
this.reconnectAttempts = 0;
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
}
|
||||
|
||||
export const wsManager = new WebSocketManager();
|
||||
Reference in New Issue
Block a user