'use client'; import React, { useMemo, useState } from 'react'; import { Button, Empty, Flex, Typography } from 'antd'; import { CaretRightOutlined, ReloadOutlined, VideoCameraOutlined } from '@ant-design/icons'; import type { CameraInfo } from '@/types'; const { Text } = Typography; interface CameraFrameProps { camera?: CameraInfo; active?: boolean; height?: number | string; aspectRatio?: string; } export const CameraFrame: React.FC = ({ camera, active = true, height, aspectRatio }) => { const [reloadKey, setReloadKey] = useState(Date.now()); const streamUrl = useMemo(() => { if (!camera?.streamUrl) { return ''; } return `${camera.streamUrl}${camera.streamUrl.includes('?') ? '&' : '?'}t=${reloadKey}`; }, [camera?.streamUrl, reloadKey]); const offline = !camera || camera.status !== 'online' || camera.placeholder; const isPollingJpeg = camera?.streamUrl === '/api/camera/refresh'; return (
{!offline && active && streamUrl ? ( <> {/* 后端的 AGV 接口是单帧 JPEG,机械臂接口是 MJPEG;img 可同时承载两者。 */} {/* eslint-disable-next-line @next/next/no-img-element */} {camera.name} { if (isPollingJpeg && active) { window.setTimeout(() => setReloadKey(Date.now()), 1500); } }} />
{camera.name} / 实时画面
) : offline ? ( } imageStyle={{ height: 64, marginBottom: 16 }} description={ {camera?.placeholder ? '摄像头未配置' : '设备离线'} {camera?.location ?? '暂无视频源'} } > {camera?.streamUrl && ( )} ) : ( 画面已暂停或未启动 )}
); };