chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,717 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import { Card, Col, Row, Tag, Button, Space, message, Spin, Empty, InputNumber, Modal, Alert, Result, Popconfirm } from 'antd';
|
||||
import {
|
||||
ApiOutlined,
|
||||
BulbOutlined,
|
||||
SendOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
InfoCircleOutlined,
|
||||
PoweroffOutlined,
|
||||
ThunderboltOutlined,
|
||||
CloudOutlined,
|
||||
FireOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { get, post } from '../api/http';
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
deviceKey?: string;
|
||||
name: string;
|
||||
onlineStatus?: string;
|
||||
}
|
||||
|
||||
interface IRResult {
|
||||
hasResult: boolean;
|
||||
result?: {
|
||||
action: string;
|
||||
success: boolean;
|
||||
no: number;
|
||||
timestamp: string;
|
||||
};
|
||||
learnedCodes?: number[];
|
||||
}
|
||||
|
||||
// 空调遥控按钮配置(顺序学习)
|
||||
const AIRCON_BUTTONS = [
|
||||
{ key: 'power-off', label: '关机', no: 1, icon: 'PoweroffOutlined' },
|
||||
{ key: 'power-on', label: '开机', no: 2, icon: 'PoweroffOutlined' },
|
||||
{ key: 'cooling', label: '制冷', no: 6, icon: 'CloudOutlined' },
|
||||
{ key: 'heating', label: '制热', no: 7, icon: 'FireOutlined' },
|
||||
{ key: 'dehumid', label: '除湿', no: 5, icon: 'ThunderboltOutlined' },
|
||||
];
|
||||
|
||||
// 温度按钮配置(单独学习)
|
||||
const TEMP_BUTTONS = [
|
||||
{ key: 'temp-23', label: '23度', no: 3 },
|
||||
{ key: 'temp-25', label: '25度', no: 4 },
|
||||
{ key: 'temp-27', label: '27度', no: 8 },
|
||||
{ key: 'temp-29', label: '29度', no: 9 },
|
||||
];
|
||||
|
||||
// 图标名 -> 图标组件映射
|
||||
const ICON_MAP: Record<string, ComponentType> = {
|
||||
PoweroffOutlined,
|
||||
ThunderboltOutlined,
|
||||
CloudOutlined,
|
||||
FireOutlined,
|
||||
};
|
||||
|
||||
// 顺序学习的编号顺序(与 AIRCON_BUTTONS 的 no 字段一致)
|
||||
const LEARN_SEQUENCE = [1, 2, 6, 7, 5];
|
||||
|
||||
// 所有可学习的按钮(用于查找按钮标签)
|
||||
const ALL_BUTTONS = [...AIRCON_BUTTONS, ...TEMP_BUTTONS];
|
||||
|
||||
// 闪烁动画样式(正在学习的按钮高亮闪烁)
|
||||
const blinkStyle = `
|
||||
@keyframes ir-blink {
|
||||
0%, 100% { background-color: #faad14; }
|
||||
50% { background-color: #fffbe6; }
|
||||
}
|
||||
.ir-blinking {
|
||||
animation: ir-blink 0.8s infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
const IRControl = () => {
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sending, setSending] = useState('');
|
||||
const [learnModal, setLearnModal] = useState<{ open: boolean; deviceId: string; no: number }>({ open: false, deviceId: '', no: 100 });
|
||||
const [emitNo, setEmitNo] = useState<Record<string, number>>({});
|
||||
const [irResults, setIrResults] = useState<Record<string, { action: string; success: boolean; no: number } | undefined>>({});
|
||||
const [deviceInfo, setDeviceInfo] = useState<Record<string, { signal?: number; lastSeen?: string; learnedCodes?: number[] } | undefined>>({});
|
||||
// 空调顺序学习状态:记录当前正在学习的设备与编号
|
||||
const [learningDevice, setLearningDevice] = useState<{ deviceId: string; currentNo: number } | null>(null);
|
||||
// 使用 ref 跟踪 learningDevice,避免轮询中的闭包陷阱
|
||||
const learningDeviceRef = useRef<{ deviceId: string; currentNo: number } | null>(null);
|
||||
// 空调按钮发射 loading(key = deviceId + button.key)
|
||||
const [airconSending, setAirconSending] = useState<string>('');
|
||||
|
||||
// 同步更新 state 与 ref
|
||||
const updateLearningDevice = (val: { deviceId: string; currentNo: number } | null) => {
|
||||
learningDeviceRef.current = val;
|
||||
setLearningDevice(val);
|
||||
};
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await get<Device[] | { items: Device[] }>('/devices');
|
||||
const all = Array.isArray(data) ? data : data.items ?? [];
|
||||
// 只显示红外控制器(名称包含"红外")
|
||||
const items = all.filter((d) => d.name.includes('红外'));
|
||||
setDevices(items);
|
||||
} catch {
|
||||
message.error('加载设备列表失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDevices();
|
||||
}, [fetchDevices]);
|
||||
|
||||
const actionLabel = (action: string) => {
|
||||
const labels: Record<string, string> = { learn: '学习', emit: '发射', learnCancel: '取消学习', erase: '擦除' };
|
||||
return labels[action] || action;
|
||||
};
|
||||
|
||||
// 擦除结果轮询:erase 命令可能不返回响应,短时间轮询确认
|
||||
const pollEraseResult = async (deviceId: string) => {
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
try {
|
||||
const data = await get<IRResult>(`/devices/${deviceId}/ir/status`, { silent: true });
|
||||
if (data.hasResult && data.result) {
|
||||
const resultTime = new Date(data.result.timestamp).getTime();
|
||||
if (resultTime >= startTime && data.result.action === 'erase') {
|
||||
if (data.result.success) {
|
||||
message.success('擦除成功');
|
||||
} else {
|
||||
message.warning('设备已进入擦除模式,本地列表已清空');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
// 超时:设备可能不返回 erase 响应,本地列表已清空
|
||||
message.info('设备未响应擦除结果,本地红外码列表已清空');
|
||||
};
|
||||
|
||||
// 轮询红外操作结果(原有功能:用于手动学习/发射/擦除)
|
||||
const pollIRResult = async (deviceId: string, action: string, no?: number) => {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
try {
|
||||
const data = await get<IRResult>(`/devices/${deviceId}/ir/status`, { silent: true });
|
||||
if (data.hasResult && data.result) {
|
||||
// 检查是否是本次操作的结果(action 匹配,且时间戳在最近 30 秒内)
|
||||
const resultTime = new Date(data.result.timestamp).getTime();
|
||||
if (Date.now() - resultTime < 30000) {
|
||||
if (data.result.action === action && (no === undefined || data.result.no === no || data.result.no === 0)) {
|
||||
// success=false 表示设备已进入操作模式(如进入学习/擦除模式),不是真正失败,继续轮询
|
||||
if (!data.result.success) {
|
||||
continue;
|
||||
}
|
||||
setIrResults((prev) => ({ ...prev, [deviceId]: data.result }));
|
||||
// 擦除成功后清空本地已学习红外码列表
|
||||
if (action === 'erase') {
|
||||
setDeviceInfo((prev) => {
|
||||
const curInfo = prev?.[deviceId];
|
||||
return { ...prev, [deviceId]: { ...curInfo, learnedCodes: [] } };
|
||||
});
|
||||
}
|
||||
message.success(`${actionLabel(data.result.action)}成功${data.result.no ? `(编号 ${data.result.no})` : ''}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
message.warning(`${actionLabel(action)}等待结果超时,请重试`);
|
||||
};
|
||||
|
||||
// 轮询设备信息(info 响应后 signal 会被入库)
|
||||
const pollDeviceInfo = async (device: Device) => {
|
||||
const key = device.deviceKey || device.id;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
try {
|
||||
// 查询遥测数据(signal)
|
||||
const telemetry = await get<{ metric: string; value: number; timestamp: string }[]>(
|
||||
`/telemetry/${key}/latest`, { silent: true }
|
||||
);
|
||||
// 查询已学习的红外码
|
||||
const irStatus = await get<{ hasResult: boolean; learnedCodes?: number[] }>(
|
||||
`/devices/${device.id}/ir/status`, { silent: true }
|
||||
);
|
||||
const signalRec = Array.isArray(telemetry) ? telemetry.find((r) => r.metric === 'signal') : undefined;
|
||||
if (signalRec || irStatus.learnedCodes) {
|
||||
setDeviceInfo((prev) => ({
|
||||
...prev,
|
||||
[device.id]: {
|
||||
signal: signalRec?.value,
|
||||
lastSeen: signalRec?.timestamp,
|
||||
learnedCodes: irStatus.learnedCodes || [],
|
||||
},
|
||||
}));
|
||||
if (signalRec) {
|
||||
const codes = irStatus.learnedCodes || [];
|
||||
message.success(`设备信息已更新:信号 ${signalRec.value},已学习红外码 ${codes.length} 个`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
message.warning('设备未响应,可能已离线');
|
||||
};
|
||||
|
||||
// 原有通用指令发送(左侧按钮使用)
|
||||
const sendIR = async (device: Device, action: 'learn' | 'emit' | 'cancel' | 'erase' | 'info', no?: number) => {
|
||||
const key = device.id + action + (no ?? '');
|
||||
setSending(key);
|
||||
// 清除之前的结果
|
||||
setIrResults((prev) => ({ ...prev, [device.id]: undefined }));
|
||||
try {
|
||||
if (action === 'learn' && no !== undefined) {
|
||||
await post(`/devices/${device.id}/ir/learn`, { no });
|
||||
message.success(`学习指令已发送(编号 ${no}),请将遥控器对准设备按键`);
|
||||
setLearnModal({ open: true, deviceId: device.id, no });
|
||||
pollIRResult(device.id, 'learn', no);
|
||||
} else if (action === 'emit' && no !== undefined) {
|
||||
await post(`/devices/${device.id}/ir/emit`, { no });
|
||||
message.success(`发射指令已发送(编号 ${no})`);
|
||||
pollIRResult(device.id, 'emit', no);
|
||||
} else if (action === 'cancel') {
|
||||
await post(`/devices/${device.id}/ir/cancel`);
|
||||
message.success('取消学习指令已发送');
|
||||
setLearnModal({ ...learnModal, open: false });
|
||||
// learnCancel 仅在学习模式下有响应,不轮询结果
|
||||
} else if (action === 'erase') {
|
||||
await post(`/devices/${device.id}/ir/erase`);
|
||||
// 立即清空本地已学习红外码列表
|
||||
setDeviceInfo((prev) => {
|
||||
const curInfo = prev?.[device.id];
|
||||
return { ...prev, [device.id]: { ...curInfo, learnedCodes: [] } };
|
||||
});
|
||||
message.success('擦除指令已发送,已清空本地红外码列表');
|
||||
// erase 可能像 learnCancel 一样不返回响应,短时间轮询确认
|
||||
pollEraseResult(device.id);
|
||||
} else if (action === 'info') {
|
||||
await post(`/devices/${device.id}/plug/info`);
|
||||
message.success('查询信息指令已发送,等待设备响应...');
|
||||
// 轮询遥测数据(info 响应中的 signal 会被入库)
|
||||
pollDeviceInfo(device);
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.error || '指令发送失败';
|
||||
message.error(errMsg);
|
||||
}
|
||||
setSending('');
|
||||
};
|
||||
|
||||
// 空调按钮发射(右侧空调面板使用)
|
||||
const emitAircon = async (deviceId: string, button: typeof AIRCON_BUTTONS[number]) => {
|
||||
// 正在顺序学习中,不触发发射
|
||||
if (learningDeviceRef.current && learningDeviceRef.current.deviceId === deviceId) {
|
||||
return;
|
||||
}
|
||||
const key = deviceId + button.key;
|
||||
setAirconSending(key);
|
||||
try {
|
||||
await post(`/devices/${deviceId}/ir/emit`, { no: button.no });
|
||||
message.success(`【${button.label}】指令已发送`);
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.error || `${button.label}指令发送失败`;
|
||||
message.error(errMsg);
|
||||
}
|
||||
setAirconSending('');
|
||||
};
|
||||
|
||||
// 顺序学习轮询:循环查询学习结果,成功后自动进入下一个编号
|
||||
// startTime 用于过滤旧结果,只接受该时间之后产生的学习结果
|
||||
const pollSequentialLearn = async (deviceId: string, no: number, startTime: number) => {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
// 通过 ref 检查是否已被取消或已切换到下一个编号
|
||||
const cur = learningDeviceRef.current;
|
||||
if (!cur || cur.deviceId !== deviceId || cur.currentNo !== no) {
|
||||
return; // 已被取消或已切换
|
||||
}
|
||||
try {
|
||||
const data = await get<IRResult>(`/devices/${deviceId}/ir/status`, { silent: true });
|
||||
if (data.hasResult && data.result) {
|
||||
const resultTime = new Date(data.result.timestamp).getTime();
|
||||
// 只接受 startTime 之后的结果,避免读到上一个编号的旧结果
|
||||
// 设备学习成功时返回 no=0,失败时返回发送的编号,因此匹配条件需兼容 no=0
|
||||
if (resultTime >= startTime && data.result.action === 'learn' && (data.result.no === no || data.result.no === 0)) {
|
||||
const curButton = ALL_BUTTONS.find((b) => b.no === no);
|
||||
if (data.result.success) {
|
||||
// 学习成功,更新本地 learnedCodes
|
||||
setDeviceInfo((prev) => {
|
||||
const curInfo = prev?.[deviceId];
|
||||
const codes = curInfo?.learnedCodes ?? [];
|
||||
if (!codes.includes(no)) codes.push(no);
|
||||
return { ...prev, [deviceId]: { ...curInfo, learnedCodes: [...codes] } };
|
||||
});
|
||||
// 查找当前编号在学习序列中的位置,决定下一个学习的编号
|
||||
const seqIndex = LEARN_SEQUENCE.indexOf(no);
|
||||
const nextNo = seqIndex >= 0 && seqIndex + 1 < LEARN_SEQUENCE.length ? LEARN_SEQUENCE[seqIndex + 1] : null;
|
||||
if (nextNo !== null) {
|
||||
const nextStartTime = Date.now();
|
||||
await post(`/devices/${deviceId}/ir/learn`, { no: nextNo });
|
||||
updateLearningDevice({ deviceId, currentNo: nextNo });
|
||||
const nextLabel = ALL_BUTTONS.find((b) => b.no === nextNo)?.label;
|
||||
message.success(`【${curButton?.label}】学习成功,请按下遥控器的【${nextLabel}】键`);
|
||||
pollSequentialLearn(deviceId, nextNo, nextStartTime);
|
||||
} else {
|
||||
// 全部完成
|
||||
updateLearningDevice(null);
|
||||
message.success('全部学习完成');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// success=false 表示设备已进入学习模式,等待用户按遥控器,继续轮询
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
// 超时:仅当仍处于当前学习状态时才提示
|
||||
const cur = learningDeviceRef.current;
|
||||
if (cur && cur.deviceId === deviceId && cur.currentNo === no) {
|
||||
updateLearningDevice(null);
|
||||
message.warning('学习超时,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 开始顺序学习:从学习序列的第一个编号开始
|
||||
const startSequentialLearn = async (deviceId: string) => {
|
||||
try {
|
||||
const firstNo = LEARN_SEQUENCE[0];
|
||||
const startTime = Date.now();
|
||||
await post(`/devices/${deviceId}/ir/learn`, { no: firstNo });
|
||||
updateLearningDevice({ deviceId, currentNo: firstNo });
|
||||
const firstLabel = ALL_BUTTONS.find((b) => b.no === firstNo)?.label;
|
||||
message.success(`开始学习:请按下遥控器的【${firstLabel}】键`);
|
||||
pollSequentialLearn(deviceId, firstNo, startTime);
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.error || '学习指令发送失败';
|
||||
message.error(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消顺序学习
|
||||
const cancelSequentialLearn = async (deviceId: string) => {
|
||||
try {
|
||||
await post(`/devices/${deviceId}/ir/cancel`, {});
|
||||
} catch {}
|
||||
updateLearningDevice(null);
|
||||
message.success('已取消学习');
|
||||
};
|
||||
|
||||
// 温度按钮单独学习
|
||||
const learnSingleTemp = async (deviceId: string, no: number, label: string) => {
|
||||
// 如果正在顺序学习,不允许单独学习
|
||||
if (learningDeviceRef.current !== null) {
|
||||
message.warning('正在顺序学习中,请先取消');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
updateLearningDevice({ deviceId, currentNo: no });
|
||||
await post(`/devices/${deviceId}/ir/learn`, { no });
|
||||
message.success(`请按下遥控器的【${label}】键`);
|
||||
// 轮询单次学习结果
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const cur = learningDeviceRef.current as { deviceId: string; currentNo: number } | null;
|
||||
if (!cur || cur.deviceId !== deviceId || cur.currentNo !== no) {
|
||||
return; // 已被取消
|
||||
}
|
||||
try {
|
||||
const data = await get<IRResult>(`/devices/${deviceId}/ir/status`, { silent: true });
|
||||
if (data.hasResult && data.result) {
|
||||
const resultTime = new Date(data.result.timestamp).getTime();
|
||||
if (resultTime >= startTime && data.result.action === 'learn' && (data.result.no === no || data.result.no === 0)) {
|
||||
if (!data.result.success) {
|
||||
continue; // 进入学习模式,继续等
|
||||
}
|
||||
// 学习成功
|
||||
setDeviceInfo((prev) => {
|
||||
const curInfo = prev?.[deviceId];
|
||||
const codes = curInfo?.learnedCodes ?? [];
|
||||
if (!codes.includes(no)) codes.push(no);
|
||||
return { ...prev, [deviceId]: { ...curInfo, learnedCodes: [...codes] } };
|
||||
});
|
||||
updateLearningDevice(null);
|
||||
message.success(`【${label}】学习成功`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
// 超时
|
||||
const cur = learningDeviceRef.current as { deviceId: string; currentNo: number } | null;
|
||||
if (cur && cur.deviceId === deviceId && cur.currentNo === no) {
|
||||
updateLearningDevice(null);
|
||||
message.warning(`【${label}】学习超时,请重试`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
updateLearningDevice(null);
|
||||
const errMsg = err?.response?.data?.error || '学习指令发送失败';
|
||||
message.error(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const isOnline = (d: Device) => d.onlineStatus === 'online';
|
||||
|
||||
if (!loading && devices.length === 0) {
|
||||
return <Empty description="暂无设备" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<style>{blinkStyle}</style>
|
||||
<Row gutter={[16, 16]}>
|
||||
{devices.map((d) => {
|
||||
const online = isOnline(d);
|
||||
const currentNo = emitNo[d.id] ?? 100;
|
||||
const irResult = irResults[d.id];
|
||||
const info = deviceInfo[d.id];
|
||||
const learnedCodes = info?.learnedCodes ?? [];
|
||||
// 当前设备是否处于顺序学习
|
||||
const isLearning = learningDevice?.deviceId === d.id;
|
||||
const learningNo = isLearning ? learningDevice!.currentNo : null;
|
||||
|
||||
return (
|
||||
<Col key={d.id} span={24}>
|
||||
<Card
|
||||
title={<Space><ApiOutlined />{d.name}</Space>}
|
||||
extra={<Tag color={online ? 'green' : 'default'}>{online ? '在线' : '离线'}</Tag>}
|
||||
>
|
||||
<Row gutter={[24, 0]}>
|
||||
{/* 左侧:原有通用功能 */}
|
||||
<Col span={12}>
|
||||
{/* 操作结果反馈 */}
|
||||
{irResult && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Result
|
||||
status={irResult.success ? 'success' : 'error'}
|
||||
title={`${actionLabel(irResult.action)}${irResult.success ? '成功' : '失败'}`}
|
||||
subTitle={irResult.no ? `红外码编号: ${irResult.no}` : undefined}
|
||||
style={{ padding: '12px 0' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 设备信息 */}
|
||||
{info && (
|
||||
<div style={{ marginBottom: 16, padding: 12, background: '#f5f5f5', borderRadius: 8 }}>
|
||||
<div style={{ fontSize: 13, color: '#666' }}>设备信息</div>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
4G 信号:<Tag color={(info.signal ?? 0) >= 20 ? 'green' : (info.signal ?? 0) >= 10 ? 'orange' : 'red'}>{info.signal ?? '--'}/31</Tag>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||
最后上报:{info.lastSeen ? new Date(info.lastSeen).toLocaleTimeString() : '--'}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 13, color: '#666' }}>
|
||||
已学习红外码:
|
||||
{info.learnedCodes && info.learnedCodes.length > 0 ? (
|
||||
<Space size={4} wrap style={{ marginTop: 4 }}>
|
||||
{info.learnedCodes.slice().sort((a, b) => a - b).map((no) => (
|
||||
<Tag key={no} color="blue" style={{ cursor: 'pointer' }} onClick={() => setEmitNo({ ...emitNo, [d.id]: no })}>
|
||||
{no}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<span style={{ color: '#999' }}>暂无</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 发射红外码 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>发射红外码</div>
|
||||
<Space>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={248}
|
||||
value={currentNo}
|
||||
onChange={(v) => setEmitNo({ ...emitNo, [d.id]: v ?? 100 })}
|
||||
style={{ width: 80 }}
|
||||
disabled={isLearning}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={sending === d.id + 'emit' + currentNo}
|
||||
onClick={() => sendIR(d, 'emit', currentNo)}
|
||||
disabled={isLearning}
|
||||
>
|
||||
发射
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 学习红外码 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>学习红外码</div>
|
||||
<Space>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={248}
|
||||
value={learnModal.no}
|
||||
onChange={(v) => setLearnModal({ ...learnModal, no: v ?? 100 })}
|
||||
style={{ width: 80 }}
|
||||
disabled={isLearning}
|
||||
/>
|
||||
<Button
|
||||
icon={<BulbOutlined />}
|
||||
loading={sending === d.id + 'learn' + learnModal.no}
|
||||
onClick={() => sendIR(d, 'learn', learnModal.no)}
|
||||
disabled={isLearning}
|
||||
>
|
||||
开始学习
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 其他操作 */}
|
||||
<Space style={{ width: '100%', justifyContent: 'center' }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<StopOutlined />}
|
||||
loading={sending === d.id + 'cancel'}
|
||||
onClick={() => sendIR(d, 'cancel')}
|
||||
>
|
||||
取消学习
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<InfoCircleOutlined />}
|
||||
loading={sending === d.id + 'info'}
|
||||
onClick={() => sendIR(d, 'info')}
|
||||
>
|
||||
查询信息
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认擦除全部红外码?"
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => sendIR(d, 'erase')}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
loading={sending === d.id + 'erase'}
|
||||
>
|
||||
擦除全部
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:空调遥控面板 */}
|
||||
<Col span={12}>
|
||||
<div style={{ marginBottom: 12, fontWeight: 600, fontSize: 15 }}>
|
||||
空调遥控面板
|
||||
{isLearning && (
|
||||
<Tag color="processing" style={{ marginLeft: 8 }}>学习中...</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 开始学习 / 取消学习 按钮 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{isLearning ? (
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
style={{ height: 60, fontSize: 16, fontWeight: 500 }}
|
||||
onClick={() => cancelSequentialLearn(d.id)}
|
||||
>
|
||||
取消学习
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
type="primary"
|
||||
icon={<BulbOutlined />}
|
||||
style={{ height: 60, fontSize: 16, fontWeight: 500 }}
|
||||
onClick={() => startSequentialLearn(d.id)}
|
||||
>
|
||||
开始学习
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 空调控制按钮(顺序学习) */}
|
||||
<Row gutter={[12, 12]}>
|
||||
{AIRCON_BUTTONS.map((btn) => {
|
||||
const Icon = ICON_MAP[btn.icon];
|
||||
const learned = learnedCodes.includes(btn.no);
|
||||
const isBlinking = isLearning && learningNo === btn.no;
|
||||
const isLoading = airconSending === d.id + btn.key;
|
||||
return (
|
||||
<Col key={btn.key} span={12}>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
icon={Icon ? <Icon /> : undefined}
|
||||
loading={isLoading}
|
||||
className={isBlinking ? 'ir-blinking' : undefined}
|
||||
style={{
|
||||
height: 80,
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
...(learned && !isBlinking
|
||||
? { borderColor: '#52c41a', color: '#52c41a' }
|
||||
: {}),
|
||||
}}
|
||||
onClick={() => emitAircon(d.id, btn)}
|
||||
>
|
||||
{btn.label}
|
||||
</Button>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{/* 温度按钮(单独学习) */}
|
||||
<div style={{ marginTop: 16, marginBottom: 8, fontSize: 13, color: '#999' }}>
|
||||
温度按钮(点击未学习的按钮开始单独学习)
|
||||
</div>
|
||||
<Row gutter={[12, 12]}>
|
||||
{TEMP_BUTTONS.map((btn) => {
|
||||
const learned = learnedCodes.includes(btn.no);
|
||||
const isBlinking = isLearning && learningNo === btn.no;
|
||||
return (
|
||||
<Col key={btn.key} span={6}>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
className={isBlinking ? 'ir-blinking' : undefined}
|
||||
style={{
|
||||
height: 60,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
...(learned && !isBlinking
|
||||
? { borderColor: '#52c41a', color: '#52c41a' }
|
||||
: {}),
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isBlinking) {
|
||||
// 正在学习,取消
|
||||
cancelSequentialLearn(d.id);
|
||||
} else if (learned) {
|
||||
// 已学习,发射
|
||||
emitAircon(d.id, { ...btn, icon: '' });
|
||||
} else {
|
||||
// 未学习,开始单独学习
|
||||
learnSingleTemp(d.id, btn.no, btn.label);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{btn.label}
|
||||
</Button>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{isLearning && (
|
||||
<div style={{ marginTop: 12, fontSize: 13, color: '#666' }}>
|
||||
当前学习:【{ALL_BUTTONS.find((b) => b.no === learningNo)?.label}】,请将遥控器对准设备按键
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{/* 学习中提示弹窗(保留原有功能) */}
|
||||
<Modal
|
||||
open={learnModal.open}
|
||||
title="红外码学习中..."
|
||||
onCancel={() => {
|
||||
post(`/devices/${learnModal.deviceId}/ir/cancel`, {}).catch(() => {});
|
||||
setLearnModal({ ...learnModal, open: false });
|
||||
}}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => {
|
||||
post(`/devices/${learnModal.deviceId}/ir/cancel`, {}).catch(() => {});
|
||||
setLearnModal({ ...learnModal, open: false });
|
||||
}}>
|
||||
取消学习
|
||||
</Button>,
|
||||
<Button key="done" type="primary" onClick={() => setLearnModal({ ...learnModal, open: false })}>
|
||||
学习完成
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={`正在学习编号 ${learnModal.no} 的红外码`}
|
||||
description={'请将遥控器对准红外控制器,按一下对应按键。听到"嘀"一声表示学习成功。'}
|
||||
/>
|
||||
</Modal>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default IRControl;
|
||||
Reference in New Issue
Block a user