feat: 添加 视频窗口切换组件

This commit is contained in:
YuanHongbo 2024-03-10 16:19:57 +08:00
commit d064da49a4
57 changed files with 1255 additions and 576 deletions

View File

@ -1,5 +1,47 @@
# @zhst/biz # @zhst/biz
## 0.9.1
### Patch Changes
- 修改 boxSelectTree 类型提示
- Updated dependencies
- @zhst/hooks@0.8.2
- @zhst/func@0.7.3
- @zhst/meta@0.8.3
## 0.9.0
### Minor Changes
- 优化 boxSelectTree 组件,添加可以自定义配置按钮功能
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.8.1
- @zhst/func@0.7.2
- @zhst/meta@0.8.2
## 0.8.0
### Minor Changes
- @zhst/biz 优化数组件
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.8.0
- @zhst/func@0.7.1
- @zhst/meta@0.8.1
## 0.7.0
### Minor Changes
- 新增预警记录卡片组件
## 0.6.0 ## 0.6.0
### Minor Changes ### Minor Changes

View File

@ -1,5 +1,8 @@
export { default as BigImageModal } from './BigImageModal'; export { default as BigImageModal } from './BigImageModal';
export { default as BoxSelectTree } from './boxSelectTree'; export { default as BoxSelectTree } from './boxSelectTree';
export { default as Tree } from './tree'; export { default as Tree } from './tree';
export type { TreeData } from './tree';
export { default as TreeTransfer } from './treeTransfer'; export { default as TreeTransfer } from './treeTransfer';
export { default as TreeTransferModal } from './treeTransferModal'; export { default as TreeTransferModal } from './treeTransferModal';
export { default as WarningRecordCard } from './WarningRecordCard';
export type { IRecord, WarningRecordCardProps } from './WarningRecordCard';

View File

@ -2,4 +2,5 @@ export { default as BigImageModal } from "./BigImageModal";
export { default as BoxSelectTree } from "./boxSelectTree"; export { default as BoxSelectTree } from "./boxSelectTree";
export { default as Tree } from "./tree"; export { default as Tree } from "./tree";
export { default as TreeTransfer } from "./treeTransfer"; export { default as TreeTransfer } from "./treeTransfer";
export { default as TreeTransferModal } from "./treeTransferModal"; export { default as TreeTransferModal } from "./treeTransferModal";
export { default as WarningRecordCard } from "./WarningRecordCard";

View File

@ -1,26 +0,0 @@
declare class Channel {
/**
* io
*/
ioIns: any;
/**
*
*/
listeners: never[];
/**
* /
*/
subscribeListenerId: never[];
unSubscribeListenerId: never[];
init: () => void;
retry: (listener: {
[x: string]: any;
lastRetryInterval: number | undefined;
intervalId: NodeJS.Timeout;
} | undefined) => void;
doEmit: (topic: any, req: any, listenerId: any) => void;
subscribe(topic: any, req: any, handle: any): () => void;
unSubscribe(topic: any, req: any, handleId: any, listenerId: any): void;
}
declare const channelIns: Channel;
export default channelIns;

View File

@ -21,3 +21,11 @@ export declare const DeviceTab: {
REAL_CAMERA_ONLYFACE: number; REAL_CAMERA_ONLYFACE: number;
REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: number; REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: number;
}; };
export declare const BOX_TYPE_LIST: {
value: string;
label: string;
}[];
export declare const ALL_LIST: {
value: string;
label: string;
}[];

View File

@ -27,4 +27,17 @@ export var DeviceTab = {
REAL_CAMERA_NOFACE: 6, REAL_CAMERA_NOFACE: 6,
REAL_CAMERA_ONLYFACE: 7, REAL_CAMERA_ONLYFACE: 7,
REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8 // 只有普通摄像头,没有人脸、没有盒子、直连 REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8 // 只有普通摄像头,没有人脸、没有盒子、直连
}; };
// 盒子 Tab 切换
export var BOX_TYPE_LIST = [{
value: '1',
label: '盒子'
}, {
value: '2',
label: '盒子组'
}];
export var ALL_LIST = [{
value: '',
label: '全部'
}];

View File

@ -1,5 +1,8 @@
export { default as BigImageModal } from './BigImageModal'; export { default as BigImageModal } from './BigImageModal';
export { default as BoxSelectTree } from './boxSelectTree'; export { default as BoxSelectTree } from './boxSelectTree';
export { default as Tree } from './tree'; export { default as Tree } from './tree';
export type { TreeData } from './tree';
export { default as TreeTransfer } from './treeTransfer'; export { default as TreeTransfer } from './treeTransfer';
export { default as TreeTransferModal } from './treeTransferModal'; export { default as TreeTransferModal } from './treeTransferModal';
export { default as WarningRecordCard } from './WarningRecordCard';
export type { IRecord, WarningRecordCardProps } from './WarningRecordCard';

View File

@ -33,7 +33,8 @@ __export(src_exports, {
BoxSelectTree: () => import_boxSelectTree.default, BoxSelectTree: () => import_boxSelectTree.default,
Tree: () => import_tree.default, Tree: () => import_tree.default,
TreeTransfer: () => import_treeTransfer.default, TreeTransfer: () => import_treeTransfer.default,
TreeTransferModal: () => import_treeTransferModal.default TreeTransferModal: () => import_treeTransferModal.default,
WarningRecordCard: () => import_WarningRecordCard.default
}); });
module.exports = __toCommonJS(src_exports); module.exports = __toCommonJS(src_exports);
var import_BigImageModal = __toESM(require("./BigImageModal")); var import_BigImageModal = __toESM(require("./BigImageModal"));
@ -41,11 +42,13 @@ var import_boxSelectTree = __toESM(require("./boxSelectTree"));
var import_tree = __toESM(require("./tree")); var import_tree = __toESM(require("./tree"));
var import_treeTransfer = __toESM(require("./treeTransfer")); var import_treeTransfer = __toESM(require("./treeTransfer"));
var import_treeTransferModal = __toESM(require("./treeTransferModal")); var import_treeTransferModal = __toESM(require("./treeTransferModal"));
var import_WarningRecordCard = __toESM(require("./WarningRecordCard"));
// Annotate the CommonJS export names for ESM import in node: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
BigImageModal, BigImageModal,
BoxSelectTree, BoxSelectTree,
Tree, Tree,
TreeTransfer, TreeTransfer,
TreeTransferModal TreeTransferModal,
WarningRecordCard
}); });

View File

@ -1,26 +0,0 @@
declare class Channel {
/**
* io
*/
ioIns: any;
/**
*
*/
listeners: never[];
/**
* /
*/
subscribeListenerId: never[];
unSubscribeListenerId: never[];
init: () => void;
retry: (listener: {
[x: string]: any;
lastRetryInterval: number | undefined;
intervalId: NodeJS.Timeout;
} | undefined) => void;
doEmit: (topic: any, req: any, listenerId: any) => void;
subscribe(topic: any, req: any, handle: any): () => void;
unSubscribe(topic: any, req: any, handleId: any, listenerId: any): void;
}
declare const channelIns: Channel;
export default channelIns;

View File

@ -21,3 +21,11 @@ export declare const DeviceTab: {
REAL_CAMERA_ONLYFACE: number; REAL_CAMERA_ONLYFACE: number;
REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: number; REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: number;
}; };
export declare const BOX_TYPE_LIST: {
value: string;
label: string;
}[];
export declare const ALL_LIST: {
value: string;
label: string;
}[];

View File

@ -19,7 +19,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/utils/constants.ts // src/utils/constants.ts
var constants_exports = {}; var constants_exports = {};
__export(constants_exports, { __export(constants_exports, {
ALL_LIST: () => ALL_LIST,
BODY_SEARCH_THRESHOID: () => BODY_SEARCH_THRESHOID, BODY_SEARCH_THRESHOID: () => BODY_SEARCH_THRESHOID,
BOX_TYPE_LIST: () => BOX_TYPE_LIST,
DeviceTab: () => DeviceTab, DeviceTab: () => DeviceTab,
ENTER_CIRCLE: () => ENTER_CIRCLE, ENTER_CIRCLE: () => ENTER_CIRCLE,
GLOBAL_IS_BOX_VMS_SHOW: () => GLOBAL_IS_BOX_VMS_SHOW, GLOBAL_IS_BOX_VMS_SHOW: () => GLOBAL_IS_BOX_VMS_SHOW,
@ -62,9 +64,18 @@ var DeviceTab = {
REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8 REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8
// 只有普通摄像头,没有人脸、没有盒子、直连 // 只有普通摄像头,没有人脸、没有盒子、直连
}; };
var BOX_TYPE_LIST = [
{ value: "1", label: "盒子" },
{ value: "2", label: "盒子组" }
];
var ALL_LIST = [
{ value: "", label: "全部" }
];
// Annotate the CommonJS export names for ESM import in node: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
ALL_LIST,
BODY_SEARCH_THRESHOID, BODY_SEARCH_THRESHOID,
BOX_TYPE_LIST,
DeviceTab, DeviceTab,
ENTER_CIRCLE, ENTER_CIRCLE,
GLOBAL_IS_BOX_VMS_SHOW, GLOBAL_IS_BOX_VMS_SHOW,

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/biz", "name": "@zhst/biz",
"version": "0.6.0", "version": "0.9.1",
"description": "业务库", "description": "业务库",
"keywords": [ "keywords": [
"business", "business",
@ -47,6 +47,5 @@
"classnames": "^2.5.1", "classnames": "^2.5.1",
"rc-util": "^5.38.1", "rc-util": "^5.38.1",
"dayjs": "^1.11.10" "dayjs": "^1.11.10"
} }
} }

View File

@ -0,0 +1,103 @@
import { Card, Space, CardProps, Spin, Button } from 'antd';
import { theme } from 'antd/lib';
import { VideoPlayer, type VideoViewRef } from '@zhst/meta';
import React, { useState, useEffect, ReactNode, useRef } from 'react';
import { CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import './index.less'
export interface VideoPlayerCardProps {
windowKey?: string;
selectedWindowKey?: string;
showType?: 'video' | "image";
imgSrc?: string;
videoSrc?: string;
cardProps?: CardProps;
errorReasonText?: string;
isWindowLoading?: boolean;
size?: 'large' | 'small';
title?: string | ReactNode
handleCloseButtonClick?: (key?: string) => void;
handleWindowClick?: (key?: string) => void;
}
export const VideoPlayerCard: React.FC<VideoPlayerCardProps> = (props) => {
const componentName = `zhst-biz-video-player-card`;
const { showType, imgSrc, videoSrc, cardProps, isWindowLoading, errorReasonText, size, title, handleCloseButtonClick, handleWindowClick, windowKey, selectedWindowKey = '' } = props;
const [cardContent, setCardContent] = useState<JSX.Element | null>(null);
const { useToken } = theme
const { token } = useToken()
const videoRef = useRef<VideoViewRef>(null)
const selectedBorderStyle = {
border: `2px solid ${token.colorPrimary}`, boxShadow: " 0px 2px 9px 0px rgba(0,0,0,0.16)"
}
const cardStyle: React.CSSProperties = {
...(size === 'large' ? { height: 931 } : { height: 456, cursor: 'pointer' }),
...(size === 'small' && selectedWindowKey === windowKey ? selectedBorderStyle : {})
};
const videoPlayerCardStyle = size === 'small' ? { width: "calc(50% - 20px)" } : { flex: 1 }
useEffect(() => {
if (!isWindowLoading && (videoSrc || imgSrc)) {
let contentElement: JSX.Element | null = null;
if (videoSrc) {
contentElement = (
<VideoPlayer ref={videoRef} url={videoSrc} />
);
videoRef.current?.setShowCrop(true)
} else if (imgSrc) {
contentElement = (
<img
alt="首帧图"
src={imgSrc}
style={{ width: "100%", height: "100%", display: 'block' }}
/>
);
}
setCardContent(contentElement);
} else {
setCardContent(null)
}
}, [showType, imgSrc, videoSrc, isWindowLoading]);
return (
<div className={componentName} onClick={() => { handleWindowClick?.(windowKey) }} style={videoPlayerCardStyle}>
<Card
title={
<Space style={{ width: "100%", justifyContent: "space-between" }}>
<div>{title}</div>
<div className="card-close-button">
<Button type="text" onClick={() => { handleCloseButtonClick?.(windowKey) }} >
<CloseOutlined />
</Button>
</div>
</Space>}
style={{ display: "flex", flexDirection: "column", borderRadius: 4, overflow: "hidden", ...cardStyle }}
bodyStyle={{ flex: 1 }}
{...cardProps}
>
{cardContent ? (
<>
{cardContent}
</>
) : (
<div style={{ backgroundColor: '#000', height: '100%', display: 'flex', padding: '20px', boxSizing: 'border-box' }}>
{
isWindowLoading ?
<div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} />} />
</div>
: !!errorReasonText && <span style={{ color: token.colorError }}>{errorReasonText}</span>
}
</div>
)}
{/* 其他内容 */}
</Card>
</div>
);
};
export default VideoPlayerCard;

View File

@ -0,0 +1,19 @@
import React from 'react';
import { VideoPlayerCard, type VideoPlayerCardProps } from '@zhst/biz';
import { Space } from 'antd';
const mockVideoPlayerCardProps: VideoPlayerCardProps = {
showType: 'image',
// videoSrc: 'https://example.com/video.mp4',
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png', // 如果需要在没有视频时显示图片封面
size: 'small',
};
export default () => {
return (
<Space size={[8, 16]} direction="vertical">
<VideoPlayerCard {...mockVideoPlayerCardProps} />
</Space>
)
}

View File

@ -0,0 +1,30 @@
.zhst-biz-video-player-card {
.ant-card-head {
padding: 0 20px;
}
.ant-card-body {
padding: 0;
overflow: hidden;
border-radius: 0;
.zhst-image__video-view {
height: 100%;
}
}
.card-close-button {
.ant-btn {
padding: 0 3px;
height: 22px;
color: #00000073;
}
.ant-btn:hover {
padding: 0 3px;
height: 22px;
color: #000000e0;
}
}
}

View File

@ -0,0 +1,22 @@
---
group: 数据展示
category: Components
subtitle: 视频播放卡片
title: VideoPlayerCard 视频播放卡片
---
# VideoPlayerCard 视频播放卡片
<code src="./demo/base.tsx"></code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |

View File

@ -0,0 +1,3 @@
import VideoPlayerCard from './VideoPlayerCard'
export type { VideoPlayerCardProps } from './VideoPlayerCard'
export default VideoPlayerCard

View File

@ -18,6 +18,8 @@ export interface IRecord {
*/ */
warningInfo?: string[] warningInfo?: string[]
/*
id
/* /*
ID ID
*/ */
@ -30,6 +32,8 @@ export interface IRecord {
id id
*/ */
cabietId?: string; cabietId?: string;
/*
/* /*
id id
*/ */
@ -38,15 +42,16 @@ export interface IRecord {
*/ */
warningTime?: string; warningTime?: string;
/* /*
*/ */
warningTimestamp?: string | number warningTimestamp?: string | number
/* /*
@default YYYY-MM-DD HH:mm:ss @default YYYY-MM-DD HH:mm:ss
*/ */
warningTimeFormat?: string; warningTimeFormat?: string;
}; };
export interface WarningRecordCardProps { export interface WarningRecordCardProps {

View File

@ -0,0 +1,71 @@
import React, { useState } from 'react';
import { Segmented } from 'antd';
import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons';
import { VideoPlayerCard, VideoPlayerCardProps } from '@zhst/biz';
import './index.less'
import { theme } from 'antd/lib';
type Size = 'large' | 'small'
interface WindowToggleProps {
dataSource?: VideoPlayerCardProps[];
handleWindowClick?: (key?: string) => void;
handleCloseButtonClick?: (key?: string) => void;
selectedWindowKey?: string;
}
export const WindowToggle: React.FC<WindowToggleProps> = (props) => {
const { dataSource = [], handleWindowClick, handleCloseButtonClick, selectedWindowKey } = props
const [size, setSize] = useState<Size>("large");
const { useToken } = theme
const { token } = useToken()
const getLabelStyle = (isSelected: boolean) => ({
padding: "0 11px", background: "#fff",
...(isSelected ? { background: token.colorPrimary, color: '#fff' } : {}),
});
return (
<div className='zhst-biz-window-toggle'>
{/* 切换按钮 */}
<div className='header'>
<Segmented
defaultValue='large'
options={[
{ value: 'large', label: <div style={getLabelStyle(size === 'large')}><BarsOutlined /></div> },
{ value: 'small', label: <div style={getLabelStyle(size === 'small')}><AppstoreOutlined /></div> },
]}
onChange={(value) => {
// 当一个窗口时 默认 selectedkey 第一条数据的 windowkey
if (value === 'large' && dataSource.length > 0) {
const { windowKey } = dataSource[0]
handleWindowClick?.(windowKey)
}
setSize(value as Size)
}}
/>
</div>
<div className='body'>
{
dataSource?.map((item, index) => {
if (size === "large" && index > 0) return
return (
<VideoPlayerCard
key={item.windowKey}
selectedWindowKey={selectedWindowKey}
size={size} {...item}
handleWindowClick={handleWindowClick}
handleCloseButtonClick={handleCloseButtonClick}
/>)
})
}
</div>
</div>
);
};
export default WindowToggle;

View File

@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { IRecord, VideoPlayerCardProps, WindowToggle, useViewLargerImageModal } from '@zhst/biz';
import { videoData, warningData } from './mock';
import { Space } from 'antd';
import dayjs from 'dayjs'
import WarningRecordList from './components/WarningRecordList';
import './index.less'
export default () => {
// 必须设置初始数据 用于渲染 4个窗口
const initialVideoDataSource: VideoPlayerCardProps[] = [
{
windowKey: 'first-window',
},
{
windowKey: 'second-window',
},
{
windowKey: 'third-window',
},
{
windowKey: 'forth-window',
}
]
const [videoDataSource, setVideoDataSource] = useState<VideoPlayerCardProps[]>(initialVideoDataSource);
const [warningDataSource, setWarningDataSource] = useState<IRecord[]>();
const [selectedWindowKey, setSelectedWindowKey] = useState<string | undefined>('first-window');
const [selectedRecordId, setSelectedRecordId] = React.useState<string | undefined>()
const [isRecordListLoading, setIsRecordListLoading] = React.useState<boolean>(false)
// 把弹窗的ref 拿出来
const viewLargerImageModalRef = useViewLargerImageModal()
const handleWindowClick = (key?: string) => {
setSelectedWindowKey(key)
}
const clearWindowData = (key?: string) => {
// 当关闭窗口时 也要刷新 右侧 预警记录接口 不要忘记
setVideoDataSource((pre) => {
const newVideoDataSource = pre.map((item) => {
if (item.windowKey === key) {
return { windowKey: key };
}
return item; // 保持不变
});
return newVideoDataSource
})
}
const handleDownloadImg = (imgSrc?: string) => {
console.log(imgSrc)
// 可以调用 下面 方法关闭弹窗
// viewLargerImageModalRef.current?.handleCancel()
}
const onRecordClick = (record?: IRecord) => {
// 点击的时候把数据 拿过来处理一下传给大图弹框
const { imgSrc, warningType, boxId, position, cabietId, warningTime, warningTimestamp, warningTimeFormat = 'YYYY-MM-DD HH:mm:ss' } = record || {}
const formattedDate = warningTimestamp ? dayjs(warningTimestamp).format(warningTimeFormat) : '';
const warningTimeShow = warningTime ? warningTime : formattedDate
//用于渲染右侧的 信息
const warningData = [
{ label: '预警类型', value: warningType },
{ label: '预警时间', value: warningTimeShow },
{ label: '盒子', value: boxId },
{ label: '点位', value: position },
{ label: '柜子ID', value: cabietId },
]
// 调用这个方法打开弹框
viewLargerImageModalRef?.current?.show({ imgSrc: imgSrc, warningData: warningData })
setSelectedRecordId(record?.id)
}
const mockData = () => {
// 请求时需要对应窗口为loading 状态
setVideoDataSource((pre) => {
const newVideoDataSource: VideoPlayerCardProps[] = pre.map((item) => {
// 开启loading
if (item.windowKey === selectedWindowKey) {
return { ...item, isWindowLoading: true, title: '' }
}
return item
});
return newVideoDataSource
})
// 模拟 视频数据请求
setTimeout(() => {
// 对后端返回数据进行处理 组装一套符合属性的 数据
const newVideoData: VideoPlayerCardProps = { imgSrc: videoData.imgSrc, title: videoData.title, }
setVideoDataSource((pre) => {
const newVideoDataSource: VideoPlayerCardProps[] = pre.map((item) => {
// 传给 选中的视频窗口
if (item.windowKey === selectedWindowKey) {
return { ...item, ...newVideoData }
}
return item
});
return newVideoDataSource
})
setVideoDataSource((pre) => {
const newVideoDataSource: VideoPlayerCardProps[] = pre.map((item) => {
// 关闭loading
if (item.windowKey === selectedWindowKey) {
return { ...item, isWindowLoading: false }
}
return item
});
return newVideoDataSource
})
}, 3000);
// 模拟 预警记录数据请求
setIsRecordListLoading(true)
setTimeout(() => {
const newWarningDataSource: IRecord[] = warningData.map(o => {
return {
imgSrc: o.imgSrc,
id: o.id,
warningType: o.warningType,
boxId: o.boxId,
position: o.position,
cabietId: o.cabietId,
//,`柜子ID: ${o.cabietId}`
warningInfo: [`盒子${o.boxId}`, `位置${o.position}`, `柜子ID${o.cabietId}`],
// cabietText: `柜子ID: ${o.cabietId}`,
warningTimestamp: o.warningTimestamp,
}
})
setWarningDataSource(newWarningDataSource)
setIsRecordListLoading(false)
}, 1000)
}
return (
<Space size={[8, 16]} direction="vertical">
<div className='zhst-biz-real-time-monitor' style={{ display: 'flex' }} >
<WindowToggle
selectedWindowKey={selectedWindowKey}
dataSource={videoDataSource}
handleWindowClick={handleWindowClick}
handleCloseButtonClick={clearWindowData}
/>
<WarningRecordList
dataSource={warningDataSource}
handleDownloadImg={handleDownloadImg}
onRecordClick={onRecordClick}
selectedRecordId={selectedRecordId}
viewLargerImageModalRef={viewLargerImageModalRef}
isRecordListLoading={isRecordListLoading}
recordListTitle="监控预警记录"
/>
</div>
<button onClick={() => { mockData() }}></button>
</Space>
)
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import { ViewLargerImageModal, WarningRecordCard, IRecord, ViewLargerImageModalRef } from '@zhst/biz';
import { Empty, Space, Spin } from 'antd';
import "./index.less"
import { LoadingOutlined } from '@ant-design/icons';
interface WarningRecordListProps {
dataSource?: IRecord[];
viewLargerImageModalRef?: React.RefObject<ViewLargerImageModalRef>;
handleDownloadImg?: (imgSrc?: string) => void;
onRecordClick?: (record?: IRecord) => void;
selectedRecordId?: string;
isRecordListLoading?: boolean;
recordListTitle?: string;
style?: React.CSSProperties;
cardStyle?: React.CSSProperties;
imgStyle?: React.CSSProperties;
largeImageTitle?: string;
}
const WarningRecordList: React.FC<WarningRecordListProps> = (props) => {
const {
dataSource = [],
viewLargerImageModalRef,
selectedRecordId,
handleDownloadImg,
onRecordClick,
isRecordListLoading,
recordListTitle,
style,
cardStyle,
imgStyle,
largeImageTitle
} = props
return (
<div className='zhst-biz-warning-record-list' style={style}>
<div className='header'>{recordListTitle}</div>
<div className='body'>
{
isRecordListLoading ?
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} />} />
</div>
: (dataSource?.length) > 0 ?
<Space direction='vertical' size={10} >
{dataSource?.map((record, index) => {
if (index > 2) return
return (<WarningRecordCard
key={record?.id}
record={record}
onRecordClick={(record) => { onRecordClick?.(record) }}
selectedRecordId={selectedRecordId}
cardStyle={{ width: 300, height: 264, ...cardStyle }}
imgStyle={{ width: 280, height: 169, ...imgStyle }}
/>)
}
)}
</Space>
:
<div style={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Empty description="暂无数据" />
</div>
}
</div>
{/* 弹窗 绑定ref 后可以调用 handleCancel方法关闭弹窗 show方法打开弹窗 */}
<ViewLargerImageModal ref={viewLargerImageModalRef} downloadImg={handleDownloadImg} title={largeImageTitle} />
</div>
)
};
export default WarningRecordList;

View File

@ -0,0 +1,20 @@
.zhst-biz-warning-record-list {
display: flex;
flex-direction: column;
border-left: solid 1px #00000026;
width: 320px;
.header {
width: 100%;
height: 48px;
background-color: #EFF2F4;
padding: 10px 20px;
box-sizing: border-box;
}
.body {
padding: 10px;
overflow: hidden;
flex: 1;
}
}

View File

@ -0,0 +1,2 @@
import WarningRecordList from './WarningRecordList'
export default WarningRecordList

View File

@ -0,0 +1,5 @@
#windowtoggle-demo-base {
.dumi-default-previewer-demo>div {
width: 100%;
}
}

View File

@ -0,0 +1,65 @@
export const videoData = {
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
// videoSrc: 'ws://10.0.0.7:9033/flv/File/test/test_h264_1.mp4.flv?ip=127.0.0.1',
title: `盒子1 点位1`
}
export const warningData = [
{
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
id: '156156ewr1',
warningType: '火焰识别',
boxId: '2',
position: '2',
cabietId: '002',
// warningTime: "2023-03-01 ",
warningTimestamp: Date.now(),
// warningTimeFormat:"YYYY-MM-DD"
},
{
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
id: '156156ewr155',
warningType: '火焰识别',
boxId: '1',
position: '1',
cabietId: '001',
// warningTime: "2023-03-01 ",
warningTimestamp: Date.now(),
// warningTimeFormat:"YYYY-MM-DD"
},
{
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
id: '156156rew155',
warningType: '火焰识别',
boxId: '3',
position: '3',
cabietId: '004',
// warningTime: "2023-03-01 ",
warningTimestamp: Date.now(),
// warningTimeFormat:"YYYY-MM-DD"
},
{
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
id: '15615615ew5',
warningType: '火焰识别',
boxId: '4',
position: '4',
cabietId: '004',
// warningTime: "2023-03-01 ",
warningTimestamp: Date.now(),
// warningTimeFormat:"YYYY-MM-DD"
},
{
imgSrc: 'https://i.yourimageshare.com/lRHiD2UnAT.png',
id: '15615615sdf5',
warningType: '火焰识别',
boxId: '5',
position: '5',
cabietId: '005',
// warningTime: "2023-03-01 ",
warningTimestamp: Date.now(),
// warningTimeFormat:"YYYY-MM-DD"
}
]

View File

@ -0,0 +1,45 @@
.zhst-biz-window-toggle {
display: flex;
flex-direction: column;
flex: 1;
.header {
width: 100%;
height: 48px;
background-color: #EFF2F4;
padding: 10px 20px;
box-sizing: border-box;
.ant-segmented {
padding: 0;
.ant-segmented-group {
border-radius: 4px;
overflow: hidden;
.ant-segmented-item {
border-radius: 0;
.ant-segmented-item-label {
padding: 0;
}
}
}
}
}
.body {
flex: 1;
width: 100%;
background-color: #E5EAEC;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
>div {
margin: 10px;
}
}
}

View File

@ -0,0 +1,22 @@
---
group: 数据展示
category: Components
subtitle: 窗口切换组件
title: WindowToggle 窗口切换组件
---
# WindowToggle 窗口切换组件
<code src="./demo/base.tsx"></code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |

View File

@ -0,0 +1,2 @@
import WindowToggle from './WindowToggle'
export default WindowToggle

View File

@ -16,6 +16,9 @@ export interface BoxSelectTreeProps {
tabsProps?: TabsProps tabsProps?: TabsProps
searchInputProps?: InputProps searchInputProps?: InputProps
treeProps?: TreeProps treeProps?: TreeProps
showOptions?: boolean
customImport?: any
extraBtns?: any
} }
const BoxSelectTree: FC<BoxSelectTreeProps> = (props) => { const BoxSelectTree: FC<BoxSelectTreeProps> = (props) => {
@ -31,7 +34,10 @@ const BoxSelectTree: FC<BoxSelectTreeProps> = (props) => {
onCreateSubmit, onCreateSubmit,
tabsProps, tabsProps,
searchInputProps, searchInputProps,
treeProps treeProps,
customImport,
extraBtns,
showOptions = true
} = props } = props
const onChange = (key: string) => { const onChange = (key: string) => {
@ -54,6 +60,9 @@ const BoxSelectTree: FC<BoxSelectTreeProps> = (props) => {
onSearch={onSearch} onSearch={onSearch}
onItemCheck={onItemCheck} onItemCheck={onItemCheck}
onItemSelect={onItemSelect} onItemSelect={onItemSelect}
showOptions={showOptions}
customImport={customImport}
extraBtns={extraBtns}
/> />
) )
}, },
@ -72,6 +81,9 @@ const BoxSelectTree: FC<BoxSelectTreeProps> = (props) => {
onSearch={onSearch} onSearch={onSearch}
onItemCheck={onItemCheck} onItemCheck={onItemCheck}
onItemSelect={onItemSelect} onItemSelect={onItemSelect}
showOptions={showOptions}
customImport={customImport}
extraBtns={extraBtns}
/> />
) )
}, },

View File

@ -1,7 +1,7 @@
import React, { FC, useState, useRef } from 'react'; import React, { FC, useState, useRef } from 'react';
import{ Button, Divider, Input, Space, TreeDataNode } from 'antd' import{ Button, Divider, Input, Space, TreeDataNode } from 'antd'
import { ModalForm, ModalFormProps, ProFormInstance, ProFormText } from '@ant-design/pro-components' import { ModalForm, ModalFormProps, ProFormInstance, ProFormText } from '@ant-design/pro-components'
import { DiffOutlined, SwitcherOutlined } from '@ant-design/icons' import { ClockCircleOutlined, CloseCircleOutlined, DiffOutlined, FolderAddOutlined, ImportOutlined, SwitcherOutlined } from '@ant-design/icons'
import type { TreeProps, InputProps } from 'antd'; import type { TreeProps, InputProps } from 'antd';
import TreeTransferModal from '../../../treeTransferModal' import TreeTransferModal from '../../../treeTransferModal'
import BoxTree from '../../../tree'; import BoxTree from '../../../tree';
@ -9,6 +9,7 @@ import BoxTree from '../../../tree';
export interface BoxGroupPanelProps { export interface BoxGroupPanelProps {
searchInputProps?: InputProps searchInputProps?: InputProps
showOptions?: boolean
treeProps?: TreeProps treeProps?: TreeProps
data: TreeDataNode[] data: TreeDataNode[]
boxDataSource: TreeDataNode[] boxDataSource: TreeDataNode[]
@ -19,11 +20,16 @@ export interface BoxGroupPanelProps {
onBoxBatchDelete?: (data?: any) => void onBoxBatchDelete?: (data?: any) => void
onBoxDelete?: (data?: any) => void onBoxDelete?: (data?: any) => void
onCreateSubmit?: ModalFormProps['onFinish'] onCreateSubmit?: ModalFormProps['onFinish']
onClockClick?: () => void
customImport?: any
extraBtns?: any
} }
const BoxGroupPanel: FC<BoxGroupPanelProps> = (props) => { const BoxGroupPanel: FC<BoxGroupPanelProps> = (props) => {
const { const {
searchInputProps, searchInputProps,
showOptions = true,
extraBtns,
data = [], data = [],
onSearch, onSearch,
treeProps, treeProps,
@ -32,7 +38,9 @@ const BoxGroupPanel: FC<BoxGroupPanelProps> = (props) => {
onCreateSubmit, onCreateSubmit,
onBoxBatchDelete, onBoxBatchDelete,
onBoxDelete, onBoxDelete,
boxDataSource onClockClick,
boxDataSource,
customImport
} = props } = props
const [isTreeCheckable, setIsTreeCheckable] = useState(false) const [isTreeCheckable, setIsTreeCheckable] = useState(false)
const [targetItems, setTargetItems] = useState<TreeDataNode[]>([]); const [targetItems, setTargetItems] = useState<TreeDataNode[]>([]);
@ -41,8 +49,7 @@ const BoxGroupPanel: FC<BoxGroupPanelProps> = (props) => {
const createFormRef = useRef< const createFormRef = useRef<
ProFormInstance<{ ProFormInstance<{
name: string; name: string;
company?: string; boxList?: any[];
useMode?: string;
}> }>
>() >()
@ -76,89 +83,109 @@ const BoxGroupPanel: FC<BoxGroupPanelProps> = (props) => {
setTargetItems(pre => pre.filter(o => o.key !== key)) setTargetItems(pre => pre.filter(o => o.key !== key))
} }
const onOk = (data: any) => { // 盒子点击确定
console.log('data', data) const onBoxChoiceOk = async (data: any) => {
createFormRef.current?.setFieldValue('boxList', data)
createFormRef.current?.setFieldValue('boxName', 123)
console.log(createFormRef.current?.getFieldValue('boxList'))
setBoxChoiceOpen(false)
} }
const onReset = () => { // 盒子选择重置
const onBoxChoiceReset = () => {
setCheckedKeys([]) setCheckedKeys([])
setTargetItems([]) setTargetItems([])
} }
return ( return (
<div style={{ padding: '0 16px' }}> <div style={{ padding: '0 16px' }}>
{/* 盒子选择弹框 */}
<TreeTransferModal <TreeTransferModal
open={boxChoiceOpen} open={boxChoiceOpen}
onCancel={() => setBoxChoiceOpen(false)} onCancel={() => setBoxChoiceOpen(false)}
onRadioChange={(val) => console.log('radio', val)} // 顶部 radio 事件 onRadioChange={(e) => console.log('radio', e.target.value)} // 顶部 radio 事件
dataSource={boxDataSource} // 数据源 dataSource={boxDataSource} // 数据源
targetItems={targetItems} // 右侧选中项 targetItems={targetItems} // 右侧选中项
checkedKeys={checkedKeys} // 左侧选中 checkedKeys={checkedKeys} // 左侧选中
onReset={onReset} // 重置按钮事件 onReset={onBoxChoiceReset} // 重置按钮事件
onOk={onOk} // 确定按钮事件 onOk={onBoxChoiceOk} // 确定按钮事件
onTreeCheck={onTreeCheck} // 树check选中事件 onTreeCheck={onTreeCheck} // 树check选中事件
onItemDelete={onItemDelete} // 右侧点击删除事件 onItemDelete={onItemDelete} // 右侧点击删除事件
/> />
<Space size={12} direction='vertical'> <Space size={12} direction='vertical' style={{ width: '100%' }}>
<Space> <Space size={4} style={{ width: '100%', justifyContent: 'space-between' }} >
<Input size='middle' onChange={(e) => onSearch?.(e)} placeholder='请输入盒子名称' {...searchInputProps} /> <Input size='middle' onChange={(e) => onSearch?.(e)} placeholder='请输入盒子名称' {...searchInputProps} />
<Button style={{ width: '80px' }} type='primary' ></Button> {customImport || (
<>
<Button type="text" onClick={() => handleCheckable()} icon={!isTreeCheckable ? <DiffOutlined /> : <SwitcherOutlined />} />
<Button type="text" onClick={() => onClockClick?.()} icon={<ClockCircleOutlined />} />
</>
)}
</Space> </Space>
<Space align='center'> {/* 是否显示操作按钮 */}
<ModalForm {showOptions && (
width={'600px'} <>
formRef={createFormRef} <Space align='center'>
title="新建组" <Button type='text' style={{ padding: '4px 8px' }} icon={<ImportOutlined />} ></Button>
modalProps={{ destroyOnClose: true }} <Divider type="vertical" style={{ margin: '8px 0' }} />
layout='horizontal' <ModalForm<{
labelCol={{ span: 6 }} name: string
wrapperCol={{ span: 18 }} boxList?: any[]
trigger={<Button type='link' ></Button>} }>
submitter={{ width={'600px'}
searchConfig: { formRef={createFormRef}
submitText: '确定', title="新建组"
resetText: '取消', modalProps={{ destroyOnClose: true }}
}, layout='horizontal'
}} labelCol={{ span: 6 }}
onFinish={onCreateSubmit} wrapperCol={{ span: 18 }}
> trigger={<Button type='text' style={{ padding: '4px 8px' }} icon={<FolderAddOutlined />} ></Button>}
<ProFormText submitter={{
rules={[ searchConfig: {
{ submitText: '确定',
required: true, resetText: '取消',
}, },
]} }}
width="md" onFinish={onCreateSubmit}
name="name" >
label="盒子组名称" <ProFormText
placeholder="请输入盒子名称" rules={[
/> {
<ProFormText required: true,
width="md" },
name="boxList" ]}
label="盒子选择" width="md"
placeholder="已选择0个盒子" name="name"
fieldProps={{ label="盒子组名称"
readOnly: true, placeholder="请输入盒子名称"
suffix: ( />
<Space> <ProFormText
<a onClick={() => { width="md"
createFormRef.current?.setFieldValue('boxList', null) name="boxList"
}} ></a> label="盒子选择"
<a onClick={() => setBoxChoiceOpen(true)}></a> fieldProps={{
</Space> readOnly: true,
) value: `已选择${createFormRef.current?.getFieldValue('boxList')?.length || 0}个盒子`,
}} suffix: (
/> <Space>
</ModalForm> <a onClick={() => {
<Divider type="vertical" /> createFormRef.current?.setFieldValue('boxList', null)
{/* @ts-ignore */} onBoxChoiceReset()
<Button danger type='link' disabled={treeProps?.checkedKeys?.length <= 0} onClick={onBoxBatchDelete} ></Button> }} ></a>
<Divider type="vertical" /> <a onClick={() => setBoxChoiceOpen(true)}></a>
<Button type="link" onClick={() => handleCheckable()} icon={isTreeCheckable ? <DiffOutlined /> : <SwitcherOutlined />} /> </Space>
<Button type="link" onClick={() => handleCheckable()} icon={isTreeCheckable ? <DiffOutlined /> : <SwitcherOutlined />} /> )
</Space> }}
<Divider style={{ margin: 0 }} /> />
</ModalForm>
<Divider type="vertical" style={{ margin: '8px 0' }} />
{/* @ts-ignore */}
<Button danger type='text' style={{ padding: '4px 8px' }} icon={<CloseCircleOutlined />} disabled={treeProps?.checkedKeys?.length <= 0} onClick={onBoxBatchDelete} ></Button>
</Space>
<Divider style={{ margin: 0 }} />
</>
)}
{extraBtns}
<BoxTree <BoxTree
treeCheckable={isTreeCheckable} treeCheckable={isTreeCheckable}
data={data} data={data}

View File

@ -2,10 +2,12 @@ import React, { useState } from 'react';
import { BoxSelectTree } from '@zhst/biz'; import { BoxSelectTree } from '@zhst/biz';
import { treeData, boxDataSource } from './mock' import { treeData, boxDataSource } from './mock'
import { Select, TreeProps, Modal, Checkbox } from 'antd'; import { Select, TreeProps, Modal, Checkbox } from 'antd';
import { BOX_TYPE_LIST } from '../../utils/constants';
const { Option } = Select const { Option } = Select
const demo = () => { const demo = () => {
const [activeKey, setActiveKey] = useState('1')
const [searchType, setSearchType] = useState('1') const [searchType, setSearchType] = useState('1')
const [searchVal, setSearchVal] = useState('') const [searchVal, setSearchVal] = useState('')
const [checkedKeys, setCheckedKeys] = useState<string[]>([]); const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
@ -32,18 +34,24 @@ const demo = () => {
} }
return ( return (
<div style={{ border: '1px solid #ccc', width: '320px' }}> <div style={{ border: '1px solid #ccc', width: '320px', minHeight: '900px' }}>
{contextHolder} {contextHolder}
<BoxSelectTree <BoxSelectTree
data={treeData} data={activeKey === '1' ? treeData : boxDataSource}
boxDataSource={boxDataSource} boxDataSource={boxDataSource}
onSearch={e => console.log('搜索', e)} onSearch={e => console.log('搜索', e)}
onCreateSubmit={async () => { return true }} onCreateSubmit={async (_data) => {
console.log('新建盒子', _data)
return true
}}
onItemCheck={onTreeCheck} onItemCheck={onTreeCheck}
onItemSelect={e => console.log('onItemSelect', e)} onItemSelect={e => console.log('onItemSelect', e)}
onTabChange={e => console.log('tabChange', e)} onTabChange={val => setActiveKey(val)}
onBoxDelete={data => console.log('盒子删除', data)} onBoxDelete={data => console.log('盒子删除', data)}
onBoxBatchDelete={onBoxBatchDelete} onBoxBatchDelete={onBoxBatchDelete}
tabsProps={{
activeKey,
}}
searchInputProps={{ searchInputProps={{
addonBefore: ( addonBefore: (
<Select <Select
@ -54,8 +62,9 @@ const demo = () => {
}} }}
style={{ width: '72px' }} style={{ width: '72px' }}
> >
<Option value="1"></Option> {BOX_TYPE_LIST.map(item => (
<Option value="2"></Option> <Option value={item.value}>{item.label}</Option>
))}
</Select> </Select>
), ),
onChange: e => setSearchVal(e.target.value), onChange: e => setSearchVal(e.target.value),

View File

@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { BoxSelectTree } from '@zhst/biz';
import { treeData, boxDataSource } from './mock'
import { Button } from 'antd';
const demo = () => {
const [activeKey, setActiveKey] = useState('1')
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
return (
<div style={{ border: '1px solid #ccc', width: '340px', minHeight: '900px' }}>
<BoxSelectTree
data={activeKey === '1' ? treeData : boxDataSource}
boxDataSource={boxDataSource}
showOptions={false}
extraBtns={<Button type="dashed" style={{ color: 'green' }}></Button>}
tabsProps={{
activeKey,
}}
treeProps={{
checkedKeys
}}
/>
</div>
);
};
export default demo;

View File

@ -1,6 +1,6 @@
import { TreeDataNode } from "antd"; import { TreeData } from "@zhst/biz";
export const treeData: TreeDataNode[] = [ export const boxDataSource: TreeData[] = [
{ {
title: '全部盒子', title: '全部盒子',
key: '0-0', key: '0-0',
@ -12,10 +12,12 @@ export const treeData: TreeDataNode[] = [
{ {
title: '摄像头1', title: '摄像头1',
key: '0-0-0-0', key: '0-0-0-0',
isCamera: true
}, },
{ {
title: '摄像头2', title: '摄像头2',
key: '0-0-0-1', key: '0-0-0-1',
isCamera: true
}, },
], ],
}, },
@ -25,7 +27,8 @@ export const treeData: TreeDataNode[] = [
children: [ children: [
{ {
title: '摄像头4', title: '摄像头4',
key: '0-0-1-0' key: '0-0-1-0',
isCamera: true
} }
], ],
}, },
@ -34,36 +37,18 @@ export const treeData: TreeDataNode[] = [
]; ];
export const boxDataSource: TreeDataNode[] = [ export const treeData: TreeData[] = [
{ key: '0-1-0', title: '分组0-1-0', isLeaf: true, checkable: false },
{ key: '0-1-1', title: '分组0-1-1', isLeaf: true, checkable: false },
{ key: '0-1-2', title: '分组0-1-2', isLeaf: true, checkable: false },
{ {
key: '0-0', key: '0-1-3',
title: '分组0-0', title: '分组0-1-3',
isLeaf: false,
checkable: false,
},
{
key: '0-1',
title: '分组0-1',
isLeaf: false, isLeaf: false,
children: [ children: [
{ key: '0-1-0', title: '分组0-1-0', isLeaf: true, checkable: false }, { key: '0-1-3-1', title: '分组0-1-3-1', isLeaf: true, isCamera: true },
{ key: '0-1-1', title: '分组0-1-1', isLeaf: true, checkable: false }, { key: '0-1-3-2', title: '分组0-1-3-2', isLeaf: true, isCamera: true },
{ key: '0-1-2', title: '分组0-1-2', isLeaf: true, checkable: false }, { key: '0-1-3-3', title: '分组0-1-3-3', isLeaf: true, isCamera: true },
{
key: '0-1-3',
title: '分组0-1-3',
isLeaf: false,
children: [
{ key: '0-1-3-1', title: '分组0-1-3-1', isLeaf: true },
{ key: '0-1-3-2', title: '分组0-1-3-2', isLeaf: true },
{ key: '0-1-3-3', title: '分组0-1-3-3', isLeaf: true },
],
},
], ],
}, },
{ key: '0-2', title: '分组0-2', isLeaf: false, checkable: false, },
{ key: '0-3', title: '分组0-3', isLeaf: false, checkable: false, },
{ key: '0-4', title: '分组0-4', isLeaf: false, checkable: false, },
{ key: '0-5', title: '分组0-4', isLeaf: false, checkable: false, },
{ key: '0-6', title: '分组0-4', isLeaf: false, checkable: false, },
]; ];

View File

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { BoxSelectTree } from '@zhst/biz';
import { Button, Select, TreeProps } from 'antd';
import { FilterOutlined } from '@ant-design/icons';
import { BOX_TYPE_LIST } from '../../utils/constants';
import { treeData, boxDataSource } from './mock'
const { Option } = Select
const demo = () => {
const [activeKey, setActiveKey] = useState('1')
const [searchType, setSearchType] = useState('1')
const [searchVal, setSearchVal] = useState('')
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const onTreeCheck: TreeProps['onCheck'] = (keys: any) => {
setCheckedKeys(keys)
}
return (
<div style={{ border: '1px solid #ccc', width: '340px', minHeight: '900px' }}>
<BoxSelectTree
data={activeKey === '1' ? treeData : boxDataSource}
boxDataSource={boxDataSource}
onSearch={e => console.log('搜索', e)}
onItemCheck={onTreeCheck}
onItemSelect={e => console.log('onItemSelect', e)}
onTabChange={val => setActiveKey(val)}
onBoxDelete={data => console.log('盒子删除', data)}
showOptions={false}
tabsProps={{
activeKey,
}}
customImport={<Button type="text" icon={<FilterOutlined />} />}
searchInputProps={{
addonBefore: (
<Select
value={searchType}
onChange={_type => {
setSearchType(_type)
setSearchVal('')
}}
style={{ width: '72px' }}
>
{BOX_TYPE_LIST.map(item => (
<Option value={item.value}>{item.label}</Option>
))}
</Select>
),
onChange: e => setSearchVal(e.target.value),
value: searchVal
}}
treeProps={{
checkedKeys
}}
/>
</div>
);
};
export default demo;

View File

@ -2,7 +2,7 @@
category: Components category: Components
title: BoxSelectTree 盒子树 title: BoxSelectTree 盒子树
demo: demo:
cols: 2 cols: 4
group: group:
title: 进阶组件 title: 进阶组件
order: 2 order: 2
@ -13,7 +13,10 @@ group:
## 代码演示 ## 代码演示
<code src="./demo/basic.tsx">基本用法</code> <code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/extraBtns.tsx">自定义其它按钮</code>
<code src="./demo/noOptions.tsx">不显示其它按钮</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
@ -24,3 +27,8 @@ group:
| tabsProps | Tabs组件的Props | antd的Tabs组件 | - | - | | tabsProps | Tabs组件的Props | antd的Tabs组件 | - | - |
| searchInputProps | 搜索框的Props | antd的Input组件 | - | - | | searchInputProps | 搜索框的Props | antd的Input组件 | - | - |
| onTabChange | tab切换监听 | function: (e) => void | - | - | | onTabChange | tab切换监听 | function: (e) => void | - | - |
| onBoxDelete | 盒子删除事件 | function: (e) => void | - | - |
| onBoxBatchDelete | 盒子批量删除事件 | function: (e) => void | - | - |
| onCreateSubmit | 新建提交事件 | function: (e) => void | - | - |
| showOptions | 展示其它功能按钮 | boolean | true | - |

View File

@ -1,6 +1,7 @@
export { default as BigImageModal } from './BigImageModal' export { default as BigImageModal } from './BigImageModal'
export { default as BoxSelectTree } from './boxSelectTree' export { default as BoxSelectTree } from './boxSelectTree'
export { default as Tree } from './tree' export { default as Tree } from './tree'
export type { TreeData } from './tree'
export { default as TreeTransfer } from './treeTransfer' export { default as TreeTransfer } from './treeTransfer'
export { default as TreeTransferModal } from './treeTransferModal' export { default as TreeTransferModal } from './treeTransferModal'
export { default as WarningRecordCard } from './WarningRecordCard' export { default as WarningRecordCard } from './WarningRecordCard'

View File

@ -1,16 +1,18 @@
import React, { FC } from 'react'; import React, { FC, useState } from 'react';
import { Tree, Badge, TreeDataNode, Space, TreeProps } from 'antd'; import { Tree, Badge, TreeDataNode, Space, TreeProps } from 'antd';
import theme from 'antd/es/theme'
import { CloseOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons' import { CloseOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
import { ModalForm, ProFormText } from '@ant-design/pro-components'; import { ModalForm, ProFormText } from '@ant-design/pro-components';
import './index.less' import './index.less'
const componentName = 'zhst-biz-tree' const componentName = 'zhst-biz-tree'
const { useToken } = theme
export interface BoxTreeProps extends TreeProps { export interface BoxTreeProps extends TreeProps {
data: TreeDataNode[] data: TreeDataNode[]
treeCheckable?: boolean treeCheckable?: boolean
showItemOption?: boolean showItemOption?: boolean
treeProps?: TreeProps customOptions?: any;
onItemCheck?: TreeProps['onCheck'] onItemCheck?: TreeProps['onCheck']
onItemSelect?: TreeProps['onSelect'] onItemSelect?: TreeProps['onSelect']
onItemSetting?: (_data: any) => void onItemSetting?: (_data: any) => void
@ -19,52 +21,86 @@ export interface BoxTreeProps extends TreeProps {
} }
const boxTree: FC<BoxTreeProps> = (props) => { const boxTree: FC<BoxTreeProps> = (props) => {
const { onItemSelect, onItemCheck, onItemSetting, onItemDelete, data = [], showItemOption = true, treeCheckable = false, onRenameFinish } = props const {
onItemSelect,
onItemCheck,
onItemSetting,
onItemDelete,
data = [],
showItemOption = true,
treeCheckable = false,
onRenameFinish,
customOptions
} = props
const { token } = useToken()
const [checkedItem, setCheckedItem] = useState<React.Key>('')
const cameraStatus = new Map([
['0', 'success'],
['1', 'error']
])
return ( return (
<Tree <Tree
checkable={treeCheckable} checkable={treeCheckable}
blockNode blockNode
onSelect={onItemSelect} onSelect={(selectedKeys, info) => {
setCheckedItem(selectedKeys[0])
onItemSelect?.(selectedKeys, info)
}}
onCheck={onItemCheck} onCheck={onItemCheck}
treeData={data} treeData={data}
titleRender={(_nodeData) => { titleRender={(_nodeData) => {
return ( return (
<div className={`${componentName}-item-render`}> <div className={`${componentName}-item-render`}>
{!_nodeData.children && <Badge style={{ marginRight: '6px' }} status="success" />} {/* @ts-ignore */}
{_nodeData.title as any} {!_nodeData.children && _nodeData.isCamera && <Badge style={{ marginRight: '6px' }} status={cameraStatus.get('0')} />}
{showItemOption && <Space className={`${componentName}-item-render_right`} style={{ float:'right' }} > <span
<ModalForm // @ts-ignore
title="重命名" style={(checkedItem === _nodeData.key) && _nodeData.isCamera ? {
width={600} color: token.colorPrimary
modalProps={{ destroyOnClose: true }} } : {}}
layout='horizontal' >
labelCol={{ span: 6 }} {_nodeData.title as any}
wrapperCol={{ span: 18 }} </span>
trigger={<EditOutlined />} {showItemOption && (
submitter={{ <Space className={`${componentName}-item-render_right`} style={{ float:'right' }} >
searchConfig: { {customOptions || (
submitText: '确定', <>
resetText: '取消', <ModalForm
}, title="重命名"
}} width={600}
onFinish={async (value) => onRenameFinish?.(value, _nodeData)} modalProps={{ destroyOnClose: true }}
> layout='horizontal'
<ProFormText labelCol={{ span: 6 }}
rules={[ wrapperCol={{ span: 18 }}
{ trigger={<EditOutlined />}
required: true, submitter={{
}, searchConfig: {
]} submitText: '确定',
width="md" resetText: '取消',
name="name" },
label="盒子名称" }}
placeholder="请输入盒子名称" onFinish={async (value) => onRenameFinish?.(value, _nodeData)}
/> >
</ModalForm> <ProFormText
<SettingOutlined onClick={() => onItemSetting?.(_nodeData)} /> rules={[
<CloseOutlined onClick={() => onItemDelete?.(_nodeData)} /> {
</Space>} required: true,
},
]}
width="md"
name="name"
label="盒子名称"
placeholder="请输入盒子名称"
/>
</ModalForm>
<SettingOutlined onClick={() => onItemSetting?.(_nodeData)} />
<CloseOutlined onClick={() => onItemDelete?.(_nodeData)} />
</>
)}
</Space>
)}
</div> </div>
) )
}} }}

View File

@ -0,0 +1,23 @@
import React from 'react';
import { Tree } from '@zhst/biz';
import { LoadingOutlined, PlayCircleOutlined, SettingOutlined } from '@ant-design/icons';
import { treeData } from './mock'
const demo = () => {
return (
<div style={{ width: '320px' }}>
<Tree
data={treeData}
customOptions={(
<>
<PlayCircleOutlined />
<SettingOutlined />
<LoadingOutlined />
</>
)}
/>
</div>
);
};
export default demo;

View File

@ -14,8 +14,11 @@ const demo = () => {
<div> <div>
{title} {title}
<div style={{ float: 'right' }} > <div style={{ float: 'right' }} >
<Tooltip placement="right" title={'存在0个'}> <Tooltip
<a >0</a> placement="right"
title={'存在0个'}
>
<a>0</a>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@ -14,8 +14,15 @@ group:
<code src="./demo/basic.tsx">基本用法</code> <code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/customTitleRender.tsx">自定义渲染界面</code> <code src="./demo/customTitleRender.tsx">自定义渲染界面</code>
<code src="./demo/noOption.tsx">不展示配置项</code> <code src="./demo/noOption.tsx">不展示配置项</code>
<code src="./demo/customOptions.tsx">自定义配置项</code>
## API
**额外参数可以参考 antd - Tree 组件**
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| data | 数据源 | Array[] | [] | - | | data | 数据源 | Array[] | [] | - |
| titleRender | 子项自定义 | ReactNode、 undefined | - | - |
| showItemOption | 是否显示额外配置功能 | boolean | true | - |
| customOptions | 自定义配置项 | any | - | - |

View File

@ -1,3 +1,10 @@
import { TreeDataNode } from 'antd';
import BoxTree from './boxTree'; import BoxTree from './boxTree';
export interface TreeData extends TreeDataNode {
children?: TreeDataNode['children'] & {
isCamera?: boolean
}[]
}
export default BoxTree; export default BoxTree;

View File

@ -13,6 +13,8 @@ group:
<code src="./demo/basic.tsx">基本用法</code> <code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/withModal.tsx">和Modal组合使用</code> <code src="./demo/withModal.tsx">和Modal组合使用</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| data | 数据源 | Array[] | [] | - | | data | 数据源 | Array[] | [] | - |

View File

@ -1,7 +1,7 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { Modal, ModalProps, Radio, RadioGroupProps, Select, SelectProps, TransferProps, TreeDataNode, TreeProps } from 'antd';
import TreeTransfer from '../treeTransfer'; import TreeTransfer from '../treeTransfer';
import { Modal, ModalProps, Radio, RadioGroupProps, TransferProps, TreeDataNode } from 'antd'; import { ALL_LIST, BOX_TYPE_LIST } from '../utils/constants';
import { TreeProps } from 'antd/lib';
export interface TreeTransferModalProps { export interface TreeTransferModalProps {
dataSource: TreeDataNode[] dataSource: TreeDataNode[]
@ -17,7 +17,10 @@ export interface TreeTransferModalProps {
open?: boolean open?: boolean
onCancel?: ModalProps['onCancel'] onCancel?: ModalProps['onCancel']
onRadioChange?: RadioGroupProps['onChange'] onRadioChange?: RadioGroupProps['onChange']
onSelect?: SelectProps['onSelect']
modalProps?: ModalProps modalProps?: ModalProps
radioProps?: RadioGroupProps
selectProps?: SelectProps
} }
const TreeTransferModal: FC<TreeTransferModalProps> = (props) => { const TreeTransferModal: FC<TreeTransferModalProps> = (props) => {
@ -31,11 +34,14 @@ const TreeTransferModal: FC<TreeTransferModalProps> = (props) => {
onReset, onReset,
onRadioChange, onRadioChange,
onTreeCheck, onTreeCheck,
onSelect,
targetItems, targetItems,
modalProps, modalProps,
radioProps,
selectProps,
} = props } = props
const [type, setType] = useState('box') const [type, setType] = useState('1')
return ( return (
<Modal <Modal
@ -48,17 +54,25 @@ const TreeTransferModal: FC<TreeTransferModalProps> = (props) => {
{...modalProps} {...modalProps}
> >
<div> <div>
<Radio.Group <div>
onChange={e => { <Radio.Group
setType(e.target.value) onChange={e => {
onRadioChange?.(e) setType(e.target.value)
}} onRadioChange?.(e)
style={{ marginLeft: '24px', padding: '20px 0' }} }}
value={type} style={{ marginLeft: '24px', padding: '20px 0' }}
> value={type}
<Radio value={'box'}></Radio> options={BOX_TYPE_LIST}
<Radio value={'boxGroup'}></Radio> {...radioProps}
</Radio.Group> />
<Select
defaultValue={''}
style={{ marginLeft: 200, width: 150 }}
options={ALL_LIST}
onSelect={onSelect}
{...selectProps}
/>
</div>
{type === 'box' ? {type === 'box' ?
( (
<TreeTransfer <TreeTransfer

View File

@ -7,7 +7,7 @@ import { boxDataSource } from './mock'
const App: React.FC = () => { const App: React.FC = () => {
const [targetItems, setTargetItems] = useState<TreeDataNode[]>([]); const [targetItems, setTargetItems] = useState<TreeDataNode[]>([]);
const [checkedKeys, setCheckedKeys] = useState<string[]>([]); const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const [open, setOpen] = useState(false) const [open, setOpen] = useState(true)
const onTreeCheck: TreeProps['onCheck'] = (keys: any, info) => { const onTreeCheck: TreeProps['onCheck'] = (keys: any, info) => {
let _targetItems: TreeDataNode[] = [] let _targetItems: TreeDataNode[] = []
@ -47,7 +47,7 @@ const App: React.FC = () => {
<TreeTransferModal <TreeTransferModal
open={open} open={open}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onRadioChange={() => setOpen(false)} // 顶部 radio 事件 onRadioChange={(e) => console.log('radioChange', e)} // 顶部 radio 事件
dataSource={boxDataSource} // 数据源 dataSource={boxDataSource} // 数据源
targetItems={targetItems} // 右侧选中项 targetItems={targetItems} // 右侧选中项
checkedKeys={checkedKeys} // 左侧选中 checkedKeys={checkedKeys} // 左侧选中

View File

@ -12,6 +12,8 @@ group:
<code src="./demo/basic.tsx">基本用法</code> <code src="./demo/basic.tsx">基本用法</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| data | 数据源 | Array[] | [] | - | | data | 数据源 | Array[] | [] | - |

View File

@ -25,3 +25,13 @@ export const DeviceTab = {
REAL_CAMERA_ONLYFACE: 7, REAL_CAMERA_ONLYFACE: 7,
REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8, // 只有普通摄像头,没有人脸、没有盒子、直连 REAL_CAMERA_NOFACE_NOBOX_NODIRECONNECT: 8, // 只有普通摄像头,没有人脸、没有盒子、直连
}; };
// 盒子 Tab 切换
export const BOX_TYPE_LIST = [
{ value: '1', label: '盒子' },
{ value: '2', label: '盒子组' }
]
export const ALL_LIST = [
{ value: '', label: '全部' }
]

View File

@ -1,5 +1,28 @@
# @zhst/utils # @zhst/utils
## 0.7.3
### Patch Changes
- 修改 boxSelectTree 类型提示
- Updated dependencies
- @zhst/request@0.8.2
## 0.7.2
### Patch Changes
- 优化 boxSelectTree 组件,添加可以自定义配置按钮功能
- Updated dependencies
- @zhst/request@0.8.1
## 0.7.1
### Patch Changes
- Updated dependencies
- @zhst/request@0.8.0
## 0.7.0 ## 0.7.0
### Minor Changes ### Minor Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/func", "name": "@zhst/func",
"version": "0.7.0", "version": "0.7.3",
"description": "函数合集", "description": "函数合集",
"keywords": [ "keywords": [
"hooks" "hooks"

View File

@ -1,5 +1,31 @@
# @zhst/hooks # @zhst/hooks
## 0.8.2
### Patch Changes
- 修改 boxSelectTree 类型提示
- Updated dependencies
- @zhst/func@0.7.3
## 0.8.1
### Patch Changes
- 优化 boxSelectTree 组件,添加可以自定义配置按钮功能
- Updated dependencies
- @zhst/func@0.7.2
## 0.8.0
### Minor Changes
- @zhst/biz 优化数组件
### Patch Changes
- @zhst/func@0.7.1
## 0.7.0 ## 0.7.0
### Minor Changes ### Minor Changes

View File

@ -48,4 +48,4 @@ export var useActivateState = function useActivateState() {
}); });
return isActive; return isActive;
}; };
export default useActivateWrapper; export default useActivateWrapper;

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/hooks", "name": "@zhst/hooks",
"version": "0.7.0", "version": "0.8.2",
"description": "hooks合集", "description": "hooks合集",
"keywords": [ "keywords": [
"hooks" "hooks"

View File

@ -1,5 +1,34 @@
# @zhst/utils # @zhst/utils
## 0.8.3
### Patch Changes
- 修改 boxSelectTree 类型提示
- Updated dependencies
- @zhst/hooks@0.8.2
- @zhst/func@0.7.3
- @zhst/meta@0.8.3
## 0.8.2
### Patch Changes
- 优化 boxSelectTree 组件,添加可以自定义配置按钮功能
- Updated dependencies
- @zhst/hooks@0.8.1
- @zhst/func@0.7.2
- @zhst/meta@0.8.2
## 0.8.1
### Patch Changes
- Updated dependencies
- @zhst/hooks@0.8.0
- @zhst/func@0.7.1
- @zhst/meta@0.8.1
## 0.8.0 ## 0.8.0
### Minor Changes ### Minor Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/meta", "name": "@zhst/meta",
"version": "0.8.0", "version": "0.8.3",
"description": "原子组件", "description": "原子组件",
"keywords": [ "keywords": [
"meta", "meta",

View File

@ -1,5 +1,23 @@
# @zhst/request # @zhst/request
## 0.8.2
### Patch Changes
- 修改 boxSelectTree 类型提示
## 0.8.1
### Patch Changes
- 优化 boxSelectTree 组件,添加可以自定义配置按钮功能
## 0.8.0
### Minor Changes
- @zhst/biz 优化数组件
## 0.7.0 ## 0.7.0
### Minor Changes ### Minor Changes

File diff suppressed because one or more lines are too long

View File

@ -1,155 +0,0 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
ResponseError: () => ResponseError,
User: () => User,
default: () => src_default,
doRequest: () => doRequest,
req: () => req
});
module.exports = __toCommonJS(src_exports);
var import_umi_request = require("umi-request");
var import_lodash_es = require("lodash-es");
var import_antd = require("antd");
var import_base_64 = __toESM(require("base-64"));
var User = /* @__PURE__ */ ((User2) => {
User2["TOKEN_KEY"] = "USER-TOKEN";
User2["USER_KEY"] = "USER";
return User2;
})(User || {});
var ResponseError = class extends Error {
constructor(response, text, data, request, type = "ResponseError") {
super(text || response.statusText);
this.name = "ResponseError";
this.data = data;
this.response = response;
this.request = request;
this.type = type;
}
};
var req = (0, import_umi_request.extend)({
getResponse: true,
// timeout: 1000,
parseResponse: false
});
req.use(async (ctx, next) => {
const { req: req2 } = ctx;
const { toast = true } = (req2 == null ? void 0 : req2.options) || {};
try {
await next();
const { res } = ctx;
const d = await res.text();
if (res.status === 401) {
localStorage.removeItem("USER-TOKEN" /* TOKEN_KEY */);
localStorage.removeItem("USER" /* USER_KEY */);
import_antd.message.warning("登录过期,请重新登录!");
return;
}
const isEmptyRes = d === "";
if (!res)
return;
const body = !isEmptyRes ? JSON.parse(d) : d;
if (res.status >= 200 && res.status < 300) {
ctx.res = body;
} else {
let errMsg = res.headers.get("Grpc-Metadata-Errorx-Message");
if (errMsg) {
errMsg = import_base_64.default.decode(errMsg);
} else if (!errMsg && (0, import_lodash_es.get)(body, "message")) {
errMsg = `${(0, import_lodash_es.get)(body, "message")}`;
} else {
errMsg = "您的网络发生异常,无法连接服务器";
}
toast && import_antd.message.error(errMsg);
throw new ResponseError(res, errMsg, d, req2, "CustomError");
}
} catch (error) {
if ((0, import_lodash_es.get)(error, "type") !== "CustomError") {
toast && import_antd.message.error("您的网络发生异常,无法连接服务器");
}
throw error;
}
});
var doRequest = (cgi, option) => {
const {
method,
url,
baseUrl,
data = {},
useBaseUrl = true,
originUrl = false,
refererSuffix = ""
} = cgi;
const token = localStorage.getItem("USER-TOKEN" /* TOKEN_KEY */);
let newUrl = "";
if (useBaseUrl) {
newUrl = `${baseUrl}${url}`;
} else {
newUrl = `http://10.0.0.7:32223${url}`;
}
if (originUrl) {
newUrl = url;
}
const regex = /\/:(\w+)/g;
const params = [];
let matches;
while ((matches = regex.exec(newUrl)) != null) {
if (matches[1]) {
params.push(matches[1]);
}
}
params.forEach(function(name) {
let d = data == null ? void 0 : data[name];
if (d == null) {
d = "";
}
newUrl = newUrl.replace(`:${name}`, d);
});
const newData = (0, import_lodash_es.omit)(data, params);
const paramObj = method.toLowerCase() === "get" ? { params: newData } : { data: newData };
return req(newUrl, {
method,
...paramObj,
...option,
headers: {
authorization: token,
...refererSuffix ? { zhst_referer: `${baseUrl}${refererSuffix}` } : {}
}
});
};
var src_default = doRequest;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ResponseError,
User,
doRequest,
req
});

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/request", "name": "@zhst/request",
"version": "0.7.0", "version": "0.8.2",
"description": "请求库", "description": "请求库",
"keywords": [ "keywords": [
"request", "request",