2026-07-02 05:38:01 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 柜子详情页
|
|
|
|
|
|
* - 显示柜子基本信息(ID、状态)
|
|
|
|
|
|
* - 按仓控板分组展示 6 个通道的状态卡片
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Typography,
|
2026-07-04 04:03:02 +08:00
|
|
|
|
Space,
|
2026-07-02 05:38:01 +08:00
|
|
|
|
Spin,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Descriptions,
|
2026-07-02 17:27:32 +08:00
|
|
|
|
Message,
|
|
|
|
|
|
Modal,
|
2026-07-02 05:38:01 +08:00
|
|
|
|
} from '@arco-design/web-react';
|
2026-07-04 04:03:02 +08:00
|
|
|
|
import { IconLeft, IconDesktop, IconCopy, IconUnlock, IconPlayArrow, IconPoweroff } from '@arco-design/web-react/icon';
|
2026-07-02 05:38:01 +08:00
|
|
|
|
import { cabinetApi, type CabinetDetail } from '@/api/devices';
|
|
|
|
|
|
|
|
|
|
|
|
/** 状态映射 */
|
|
|
|
|
|
const STATUS_MAP: Record<number, { label: string; color: string }> = {
|
|
|
|
|
|
0: { label: '空闲', color: 'green' },
|
|
|
|
|
|
1: { label: '充电中', color: 'blue' },
|
|
|
|
|
|
2: { label: '故障', color: 'red' },
|
|
|
|
|
|
3: { label: '禁用', color: 'orange' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** 柜子状态映射 */
|
|
|
|
|
|
const CABINET_STATUS_MAP: Record<number, { label: string; color: string }> = {
|
2026-07-04 04:03:02 +08:00
|
|
|
|
0: { label: '离线', color: 'red' },
|
2026-07-02 05:38:01 +08:00
|
|
|
|
1: { label: '在线', color: 'green' },
|
|
|
|
|
|
2: { label: '充电中', color: 'blue' },
|
|
|
|
|
|
3: { label: '故障', color: 'red' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default function CabinetDetail() {
|
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [detail, setDetail] = useState<CabinetDetail | null>(null);
|
2026-07-02 17:27:32 +08:00
|
|
|
|
const [powLoading, setPowLoading] = useState(false);
|
2026-07-04 04:03:02 +08:00
|
|
|
|
const [doorLoading, setDoorLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
/** 复制到剪贴板 */
|
|
|
|
|
|
const copyText = (text: string, label: string) => {
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(
|
|
|
|
|
|
() => Message.success(`${label} 已复制`),
|
|
|
|
|
|
() => Message.error('复制失败'),
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2026-07-02 05:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
cabinetApi
|
|
|
|
|
|
.getDetail(Number(id))
|
|
|
|
|
|
.then((res) => setDetail(res.data))
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
// 如果详情接口不可用,显示提示
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => setLoading(false));
|
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
2026-07-04 04:03:02 +08:00
|
|
|
|
/** 开门 */
|
|
|
|
|
|
const handleOpenDoor = async () => {
|
|
|
|
|
|
if (!detail) return;
|
|
|
|
|
|
setDoorLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const firstComp = detail.cabin_boards[0]?.compartments[0];
|
|
|
|
|
|
if (!firstComp) { Message.warning('没有可用的仓体'); return; }
|
|
|
|
|
|
await cabinetApi.openDoor(firstComp.id);
|
|
|
|
|
|
Message.success('开门指令已发送');
|
|
|
|
|
|
} catch { Message.error('开门失败'); }
|
|
|
|
|
|
finally { setDoorLoading(false); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-02 17:27:32 +08:00
|
|
|
|
/** 接触器合闸 */
|
|
|
|
|
|
const handlePowOn = () => {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认操作',
|
|
|
|
|
|
content: '确定要合闸恢复380V供电吗?',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
setPowLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await cabinetApi.powOn(Number(id));
|
|
|
|
|
|
Message.success('合闸指令已发送');
|
|
|
|
|
|
// 刷新详情
|
|
|
|
|
|
const res = await cabinetApi.getDetail(Number(id));
|
|
|
|
|
|
setDetail(res.data);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Message.error('操作失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setPowLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** 接触器分闸 */
|
|
|
|
|
|
const handlePowOff = () => {
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认操作',
|
|
|
|
|
|
content: '确定要分闸切断380V供电吗?',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
setPowLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await cabinetApi.powOff(Number(id));
|
|
|
|
|
|
Message.success('分闸指令已发送');
|
|
|
|
|
|
// 刷新详情
|
|
|
|
|
|
const res = await cabinetApi.getDetail(Number(id));
|
|
|
|
|
|
setDetail(res.data);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
Message.error('操作失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setPowLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-02 05:38:01 +08:00
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
|
|
|
|
|
<Spin dot />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!detail) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<Typography.Text type="secondary">柜子不存在或加载失败</Typography.Text>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cabStatus = CABINET_STATUS_MAP[detail.status] || { label: '未知', color: 'gray' };
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
|
|
|
|
{/* 顶部导航 */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<IconLeft />}
|
2026-07-02 18:40:56 +08:00
|
|
|
|
onClick={() => navigate('/devices')}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
>
|
|
|
|
|
|
返回
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<IconDesktop style={{ fontSize: 24, color: 'var(--color-text-2)' }} />
|
|
|
|
|
|
<Typography.Title heading={5} style={{ margin: 0 }}>
|
|
|
|
|
|
柜子 {detail.abstract_id}
|
|
|
|
|
|
</Typography.Title>
|
|
|
|
|
|
<Tag color={cabStatus.color}>{cabStatus.label}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 柜子信息 */}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
title="基本信息"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
extra={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button loading={doorLoading} icon={<IconUnlock />} onClick={handleOpenDoor}>开门</Button>
|
|
|
|
|
|
<Button type="primary" status="success" loading={powLoading} icon={<IconPlayArrow />} onClick={handlePowOn}>
|
2026-07-02 17:27:32 +08:00
|
|
|
|
合闸供电
|
|
|
|
|
|
</Button>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<Button type="primary" status="danger" loading={powLoading} icon={<IconPoweroff />} onClick={handlePowOff}>
|
2026-07-02 17:27:32 +08:00
|
|
|
|
分闸断电
|
|
|
|
|
|
</Button>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Descriptions
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: '抽象 ID', value: <>{detail.abstract_id} <IconCopy style={{ cursor: 'pointer', color: 'var(--color-text-3)' }} onClick={() => copyText(detail.abstract_id!, '抽象ID')} /></> },
|
|
|
|
|
|
{ label: 'IMEI', value: <>{detail.imei} <IconCopy style={{ cursor: 'pointer', color: 'var(--color-text-3)' }} onClick={() => copyText(detail.imei, 'IMEI')} /></> },
|
|
|
|
|
|
{ label: 'ICCID', value: <>{detail.iccid || '-'} {detail.iccid && <IconCopy style={{ cursor: 'pointer', color: 'var(--color-text-3)' }} onClick={() => copyText(detail.iccid!, 'ICCID')} />}</> },
|
|
|
|
|
|
{ label: '名称', value: detail.name || '-' },
|
|
|
|
|
|
{ label: '状态', value: <Tag color={cabStatus.color}>{cabStatus.label}</Tag> },
|
|
|
|
|
|
{ label: 'RSSI', value: detail.rssi != null ? String(detail.rssi) : '-' },
|
|
|
|
|
|
{ label: '直流供电', value: detail.pow_fail_dc === true ? <span style={{ color: '#dc2626' }}>断电</span> : detail.pow_fail_dc === false ? <span style={{ color: '#16a34a' }}>正常</span> : '-' },
|
|
|
|
|
|
{ label: '交流供电', value: detail.pow_fail_ac === true ? <span style={{ color: '#dc2626' }}>断电</span> : detail.pow_fail_ac === false ? <span style={{ color: '#16a34a' }}>正常</span> : '-' },
|
|
|
|
|
|
{ label: '地址', value: detail.address || '-' },
|
|
|
|
|
|
]}
|
|
|
|
|
|
column={3}
|
|
|
|
|
|
layout="horizontal"
|
|
|
|
|
|
labelStyle={{ paddingRight: 36 }}
|
|
|
|
|
|
/>
|
2026-07-02 05:38:01 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 仓控板列表 */}
|
|
|
|
|
|
{detail.cabin_boards.map((board) => (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={board.id}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
title={`${detail.imei}-${board.board_id}`}
|
|
|
|
|
|
extra={<IconCopy style={{ cursor: 'pointer', color: 'var(--color-text-3)', fontSize: 14 }} onClick={() => copyText(`${detail.imei}-${board.board_id}`, '仓板ID')} />}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
size="small"
|
|
|
|
|
|
>
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
|
2026-07-02 05:38:01 +08:00
|
|
|
|
{board.compartments.map((comp) => {
|
|
|
|
|
|
const st = STATUS_MAP[comp.status] || { label: '未知', color: 'gray' };
|
2026-07-04 04:03:02 +08:00
|
|
|
|
const c = comp as any;
|
2026-07-02 05:38:01 +08:00
|
|
|
|
return (
|
2026-07-04 04:03:02 +08:00
|
|
|
|
<div key={comp.id} style={{ border: '1px solid var(--color-border)', borderRadius: 6, padding: '6px 8px', fontSize: 12 }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }}>
|
|
|
|
|
|
<span style={{ fontWeight: 600, fontSize: 13, display: 'inline-flex', alignItems: 'center', gap: 3 }}>
|
|
|
|
|
|
{detail.imei}-{board.board_id}-{comp.channel_id}
|
|
|
|
|
|
<IconCopy style={{ cursor: 'pointer', color: 'var(--color-text-4)', fontSize: 12 }} onClick={(e) => { e.stopPropagation(); copyText(`${detail.imei}-${board.board_id}-${comp.channel_id}`, '通道号'); }} />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Tag color={st.color} size="small">{st.label}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ color: 'var(--color-text-2)', fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'nowrap', overflow: 'hidden' }}>
|
|
|
|
|
|
<span>{c.voltage ?? '-'}mV</span>
|
|
|
|
|
|
<span>{c.current ?? '-'}mA</span>
|
|
|
|
|
|
<span>{c.power ?? '-'}W</span>
|
|
|
|
|
|
{c.soc != null && <span style={{ color: '#2563eb', fontWeight: 600 }}>{c.soc}%</span>}
|
|
|
|
|
|
{c.battery_id && <span style={{ color: 'var(--color-text-3)', flexShrink: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.battery_id}</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-02 05:38:01 +08:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-07-04 04:03:02 +08:00
|
|
|
|
</div>
|
2026-07-02 05:38:01 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|