feat(zhst/biz): 添加大图组件v2

This commit is contained in:
NICE CODE BY DEV 2024-06-11 20:25:25 +08:00
parent a501656907
commit ce6a719bc2
61 changed files with 1876 additions and 516 deletions

View File

@ -1,5 +1,15 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
});

View File

@ -61,6 +61,7 @@
"dumi": "^2.2.13",
"eslint": "^8.23.0",
"father": "^4.1.0",
"father-plugin-less": "^0.0.2",
"husky": "^8.0.1",
"lerna": "^8.0.0",
"lint-staged": "^13.0.3",

View File

@ -1,13 +1,16 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
plugins: ['father-plugin-less'],
});

View File

@ -35,6 +35,7 @@
"registry": "http://10.0.0.77:4874"
},
"devDependencies": {
"@swc/core": "^1.3.9",
"@zhst/types": "workspace:^"
},
"dependencies": {

View File

@ -0,0 +1,304 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import {
ConfigProvider,
Descriptions,
CropperImage,
AttachImage,
VideoPlayer,
RelatedImage,
Radio,
Tooltip,
Button,
Tabs
} from '@zhst/meta';
import classNames from 'classnames'
import { IconFont } from '@zhst/icon'
import { BigImageProps, BigImageRef } from './interface'
import CombineImage from './components/CombineImage'
import './index.less'
const DescriptionsItem = Descriptions.Item
export const componentPrefix = 'zhst-big-image'
const initialStyle ={
fontSize: '12px'
}
const BigImage = forwardRef<BigImageRef, BigImageProps>((props, ref) => {
const {
// ------------ 通用配置 -------------------
type,
viewHeight,
width = '100%',
children,
// ------------ 顶部按钮 -----------------
topButtonRender,
// ------------ 描述 -----------------
descriptionList = [],
showDescription,
// ------------- tab 导航 ----------------
customTabBarExtraContent,
tabProps,
onTabChange,
// ------------- 场景图 -------------
cropperImageProps = {
cropButtonList: []
},
// ----------------- 对比图 ---------------------
combineImageProps,
// ------------ 视频模式 -----------------
videoProps,
// ------------ view 操作按钮 -----------------
showModeChange,
modeButtonGroupProps,
viewTopModeButtonList = [
{
key: 'image',
label: '图片'
},
{
key: 'video',
label: '视频'
}
],
// ------------------ 翻页 ----------------------
showNavigation,
prevButtonProps,
onPrevButtonClick,
onNextButtonClick,
nextButtonProps,
// ------------------ 人脸碰撞模型 -----------------
relatedImageProps,
isRelated = false,
theme,
} = props
const combineImageRef = useRef<any>(null)
const videoPlayerRef = useRef<any>(null)
const cropperImageRef = useRef<any>(null)
cropperImageProps.cropButtonList = cropperImageProps?.cropButtonList || [
{
key: 'auto',
icon: <IconFont icon='icon-zidong' />,
label: '智能框选',
},
{
key: 'custom',
icon: <IconFont icon='icon-shoudong' />,
label: '手动框选',
}
];
// ------------------- 初始化模式 ----------------------
const initMode = (type: BigImageProps['type'] = 'normal') => {
switch (type) {
case 'compater':
return (
<CombineImage
// @ts-ignore
ref={combineImageRef}
height={viewHeight}
{...combineImageProps}
/>
)
case 'normal':
return (
<div style={{ height: viewHeight, width: '100%' }}>
<CropperImage
ref={cropperImageRef}
type='rect'
// @ts-ignore
height={viewHeight}
{...cropperImageProps}
>
{/* // ------------ 显示条件:当不为编辑状态,并且有图片值 ----------------- */}
{!cropperImageProps?.editAble && cropperImageProps?.attachImageData && (
<>
<AttachImage
data={cropperImageProps?.attachImageData}
{...cropperImageProps?.attachImageProps}
/>
<div
style={{
position: 'absolute',
right: '4px',
bottom: '4px',
fontSize: '18px',
color: 'red',
cursor: 'default'
}}
>{`人脸质量分:${(Number(cropperImageProps?.score) as number).toFixed(2)}`}</div>
</>
)}
{/* // ------------ 场景图绘制工具栏 ----------------- */}
{cropperImageProps?.showEditTools && (
<div className={`${componentPrefix}-view-cropper-btn`}>
<Radio.Group
className={`${componentPrefix}-view-cropper-btn-group`}
size="small"
buttonStyle='solid'
// @ts-ignore
onChange={cropperImageProps?.onCropperTypeChange}
{...cropperImageProps.btnGroupProps}
>
{cropperImageProps?.cropButtonList?.map(btn => (
<Tooltip key={btn.key} title={btn.label}>
{/* @ts-ignore */}
<Radio.Button value={btn.key}{...btn.props}><div className={`${componentPrefix}-view-cropper-btn-group-radio`}>{btn.icon} {btn.label}</div></Radio.Button>
</Tooltip>
))}
</Radio.Group>
<Button onClick={cropperImageProps?.onExitEdit} className={`${componentPrefix}-view-cropper-btn_close`} type="primary" size="small" icon={<IconFont icon="icon-danchuangguanbi" />} />
</div>
)}
</CropperImage>
</div>
)
case 'video':
return (
// @ts-ignore
<VideoPlayer ref={videoPlayerRef} height={viewHeight} {...videoProps} />
)
}
}
// 暴露 ref 实例
useImperativeHandle(ref, () => ({
// @ts-ignore
cropperImageRef: cropperImageRef?.current,
videoPlayerRef: videoPlayerRef?.current,
combineImageRef: combineImageRef?.current,
}));
return (
<div
className={classNames(`${componentPrefix}-view`)}
style={{ width: `${parseInt(width as string)}px` }}
>
<div className={classNames(`${componentPrefix}-view-btns`)}>
{topButtonRender}
</div>
{showDescription && (
<div
className={classNames(`${componentPrefix}-view-desc`)}
>
<ConfigProvider
theme={{
components: {
Descriptions: {
viewBg: '#f6f6f6',
colorBgLayout:'#ccc',
titleColor: 'rgba(0,0,0,0.45)',
colorTextLabel: 'rgba(0,0,0,0.45)',
contentColor: 'rgba(0,0,0,0.88)',
},
},
...theme
}}
>
{descriptionList.map(descriptions => (
<Descriptions
key={descriptions.title}
title={descriptions.showTitle && descriptions.title}
column={8}
{...descriptions.props}
>
{descriptions?.children?.map(item => (
<DescriptionsItem
key={item.key}
label={item.label}
span={1}
contentStyle={{ fontSize: initialStyle.fontSize }}
labelStyle={{ fontSize: initialStyle.fontSize }}
>{item.children}</DescriptionsItem>
))}
</Descriptions>
))}
</ConfigProvider>
</div>
)}
<Tabs
tabBarExtraContent={customTabBarExtraContent}
onChange={onTabChange}
items={[
{
label: `场景图`,
key: '1',
},
{
label: `对比图`,
key: '2',
}
]}
{...tabProps}
/>
<div
className={classNames(`${componentPrefix}-view-container`)}
>
{/* ---------------- view 视图左上角导航按钮 ------------------ */}
{showModeChange && (
<div
className={classNames(`${componentPrefix}-view-container-modeNav`)}
>
<Radio.Group
buttonStyle='solid'
size='small'
{...modeButtonGroupProps}
>
{viewTopModeButtonList?.map(btn => (
<Tooltip key={btn.key} title={btn.tooltipTxt}>
{/* @ts-ignore */}
<Radio.Button value={btn.key} {...btn.props}>{btn.icon} {btn.label}</Radio.Button>
</Tooltip>
))}
</Radio.Group>
</div>
)}
{/* --------------------------------- 视频播放模式 --------------------------------- */}
{initMode(type)}
{/* 切换按钮组件 */}
{/* ----------------------------------- 上一张按钮 ---------------------------------- */}
{showNavigation && (
<Button
className={classNames(
`${componentPrefix}-view-container-nav`,
`${componentPrefix}-view-container-nav_left`
)}
icon={<IconFont icon="icon-qiehuanzuo" />}
type="primary"
// style={{ backgroundColor: 'rgba(255,255,255,0.8)' }}
shape='circle'
onClick={onPrevButtonClick}
{...prevButtonProps}
/>
)}
{/* ----------------------------------- 下一张按钮 ---------------------------------- */}
{showNavigation && (
<Button
className={classNames(
`${componentPrefix}-view-container-nav`,
`${componentPrefix}-view-container-nav_right`
)}
icon={<IconFont icon="icon-qiehuanyou" />}
type="primary"
// style={{ backgroundColor: 'rgba(255,255,255,0.8)' }}
shape='circle'
onClick={onNextButtonClick}
{...nextButtonProps}
/>
)}
</div>
{/* ----------------------------------- 人脸碰撞组件 ---------------------------------- */}
{isRelated && (
// @ts-ignore
<RelatedImage
{...relatedImageProps}
/>
)}
{children}
</div>
)
})
export default BigImage

View File

@ -0,0 +1,3 @@
.zhst-big-image-combime-image {
background-color: #f7f7f7;
}

View File

@ -0,0 +1,82 @@
import React, { FC, useRef, forwardRef, useImperativeHandle } from 'react';
import { CompareImage, Flex, Progress, ProgressProps, CompareImageProps } from '@zhst/meta'
import './index.less'
export interface ComBineImageProps {
height?: string | number
data: {
imgSummary: string;
compaterImage: string;
imageKey: string;
score: number;
}
prevDisable?: boolean;
nextDisable?: boolean;
onPre?: () => void;
onNext?: () => void;
openRoll?: boolean
targetImageProps?: CompareImageProps
sourceImageProps?: CompareImageProps
}
const conicColors: ProgressProps['strokeColor'] = {
'0%': '#42E2AC',
'50%': '#DFAF2E',
'100%': '#F95C55',
};
const componentName = 'zhst-big-image-combime-image'
const ComBineImage: FC<ComBineImageProps> = forwardRef((props, ref) => {
const {
height,
data,
prevDisable,
nextDisable,
onNext,
onPre,
openRoll
} = props
const { imgSummary, compaterImage, score } = data
const targetImageRef = useRef(null)
const compareImageRef = useRef(null)
useImperativeHandle(ref, () => ({
compareImageRef,
targetImageRef
}));
return (
<Flex className={componentName} justify='space-between' align='center' >
<CompareImage
ref={targetImageRef}
height={height}
width="400px"
preDisable={prevDisable}
nextDisable={nextDisable}
onNext={onNext}
onPre={onPre}
showScore={false}
openRoll={openRoll}
url={imgSummary}
label="目标图"
{...props.targetImageProps}
/>
<Progress type="circle" size={72} strokeWidth={6} percent={Number(score || 0)} strokeColor={conicColors} />
<CompareImage
ref={compareImageRef}
width="400px"
height={height}
url={compaterImage}
openRoll={false}
score={score}
label="预警图"
labelColor='#FA5852'
{...props.sourceImageProps}
/>
</Flex>
)
})
export default ComBineImage

View File

@ -0,0 +1,44 @@
.zhst-image__nav {
position: absolute;
display: flex;
width: 48px;
height: 100%;
flex-shrink: 0;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0;
&>button {
& span {
display: flex;
}
}
:global {
i:hover {
color: #fff !important;
}
}
&--disable {
:global {
i {
color: #f0f0f0;
cursor: not-allowed !important;
}
}
}
&--hide {
display: none;
}
&-prev {
left: 12px;
}
&-next {
right: 12px;
}
}

View File

@ -0,0 +1,40 @@
// !! 已废弃
import * as React from 'react';
import classnames from 'classnames';
import { Button } from '@zhst/meta';
import { IconFont } from '@zhst/icon'
import './index.less';
const componentName = `zhst-image__nav`;
const Navigation: React.FC<{
show?: boolean;
onClick?: React.MouseEventHandler<HTMLElement>;
prev?: boolean;
next?: boolean;
disabled?: boolean;
className?: string;
color?: string;
hoverColor?: string;
}> = (props) => {
const { show, prev, next, disabled, onClick, className, color } = props;
return (
<div
className={classnames(
`${componentName}`,
prev && `${componentName}-prev`,
next && `${componentName}-next`,
disabled && `${componentName}--disable`,
!show && `${componentName}--hide`,
className
)}
>
<Button type="text" disabled={disabled} shape='circle' onClick={onClick}>
<IconFont size={28} color={color} icon={prev ? 'icon-qiehuanzuo' : 'icon-qiehuanyou'} />
</Button>
</div>
);
};
export default Navigation;

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -0,0 +1,71 @@
import React, { useEffect, useRef, useState } from 'react';
import { BigImage } from '@zhst/biz'
import { Space, Switch } from '@zhst/meta'
import { pick, get } from '@zhst/func'
import { BIG_IMAGE_DATA, attributeList } from '../mock'
import Img from './imgs/photo-1503185912284-5271ff81b9a8.webp'
const BigModal = (props: any) => {
const {
} = props
const [dataSource, setDataSource] = useState(BIG_IMAGE_DATA)
const [showDesc, setShowDesc] = useState(true)
const [type, setType] = useState('normal')
const modalRef = useRef(null)
return (
<div style={{ padding: '32px', border: '1px solid #09f' }}>
<Space>
<span><Switch value={showDesc} onChange={e => setShowDesc(pre => !pre)} /></span>
</Space>
<BigImage
type={type}
ref={modalRef}
width={896}
viewHeight={'440px'}
showModeChange
onTabChange={(newVal) => setType(newVal === '1'? 'normal': 'compater')}
// ------------ 属性列表 -----------------
showDescription={showDesc}
descriptionList={attributeList}
// ------------ 图片/视频切换 -----------------
viewTopModeButtonList={[
{
key: 'image',
label: '图片'
},
{
key: 'video',
label: '视频'
}
]}
modeButtonGroupProps={{
defaultValue: 'image',
onChange: e => console.log('模式切换', e)
}}
// ------------ 导航功能 -----------------
showNavigation
onPrevButtonClick={val => console.log('pre', val)}
onNextButtonClick={val => console.log('next', val)}
// ------------ 场景图功能 -----------------
cropperImageProps={{
url: Img,
score: 50,
odList: get(dataSource, 'odRect', []),
attachImageData: get(dataSource, 'attachImg', []),
}}
// ------------ 对比图模式 -----------------
combineImageProps={{
data: pick(dataSource, 'compaterImage', 'imgSummary', 'imageKey', 'score'),
}}
// ------------ 视频模块 -----------------
videoProps={{
url: get(dataSource, 'flvUrl', '')
}}
>
</BigImage>
</div>
)
}
export default BigModal

View File

@ -0,0 +1,128 @@
// @ts-nocheck
import React, { useState } from 'react';
import { BigImageModal } from '@zhst/biz'
import { Button, DescriptionsProps } from '@zhst/meta'
import { IMAGE_DATA, BIG_IMAGE_DATA } from '../mock'
import bigImageModalAdapter from '../util/bigImageModalAdapter'
import { get } from '@zhst/func';
const descriptionList: DescriptionsProps['items'] = [
{
title: '人员属性',
children: [
{
key: '1',
label: '性别',
children: '男',
},
{
key: '2',
label: '年龄',
children: '成年',
},
{
key: '3',
label: '帽子',
children: '无',
},
{
key: '4',
label: '上身颜色',
children: '灰',
},
{
key: '5',
label: '下身颜色',
children: '蓝色',
},
{
key: '6',
label: '附着物',
children: '无',
},
{
key: '7',
label: '骑行',
children: '否',
},
]
}
];
// 适配器,适配老接口
const NewImageModal = bigImageModalAdapter(BigImageModal, {
oldMode: true
})
const BigModal = (props) => {
const {
onClose,
isArchiveDetail = false,
specialTitle = '对比图2',
transformPropFunc,
screenshotButtonRender,
showLowFaceTip = false
} = props
const [visible, setVisible] = useState(false)
const [selectIndex, setSelectIndex] = useState(0)
const [dataSource, setDataSource] = useState(IMAGE_DATA.dataSource)
const [dataSources, setDataSources] = useState(IMAGE_DATA.dataSource)
const [selectItem, setSelectItem] = useState({})
return (
<div>
<Button onClick={() => setVisible(true)} ></Button>
<NewImageModal
title="查看大图"
dataSource={dataSource}
imageData={dataSource}
width={1098}
onClose={() => onClose}
descriptionConfig={{ data: descriptionList }}
visible={visible}
isArchiveDetail={isArchiveDetail}
ToolProps={{
// renderLeft: leftOperateBar({ disableBtn, onActionClick: onBigImageActionClick }),
// renderRight: rightOperateBar({
// disableBtn,
// onActionClick: onBigImageActionClick,
// isArchiveDetail,
// }),
// renderVideoBtn: !disableBtn.includes(OPT['PLAY_VIDEO']),
// disableVideo: disableVideo,
}}
selectIndex={selectIndex}
onSelectIndexChange={(index: number) => {
index > 0 && index < dataSources.length && setSelectIndex(index);
}}
// tabsFilter={tabsFilter}
specialTitle={specialTitle}
transformPropFunc={async (item: any) => {
let bigImageInfo = !!transformPropFunc && (await transformPropFunc(item));
setSelectItem({ ...bigImageInfo });
return { ...bigImageInfo };
}}
screenshotButtonRender={screenshotButtonRender}
//@ts-ignore
transformVideoPropFunc={async (item) => {
const { maxDuration: duration = 20 } = item || {};
const time = get(item, 'timestamp');
const cameraId = get(item, 'cameraId');
const { url: flvUrl } = {
url: 'url',
downloadUrl: 'url',
};
return {
flvUrl,
maxDuration: duration,
};
}}
nullDialogProp={{
emptyText: showLowFaceTip ? '目标图人脸质量较低,暂无聚类数据' : '暂无数据',
}}
/>
</div>
)
}
export default BigModal

View File

@ -0,0 +1,89 @@
import React, { useEffect, useRef, useState } from 'react';
import { BigImage } from '@zhst/biz'
import { Space, Switch, Button } from '@zhst/meta'
import { pick, get } from '@zhst/func'
import { BIG_IMAGE_DATA, attributeList } from '../mock'
import Img from './imgs/photo-1503185912284-5271ff81b9a8.webp'
const testOd = [
{
"id": "456",
"x": 0.58543766,
"y": 0.3203356,
"w": 0.052037954,
"h": 0.2664015
}
]
const BigModal = (props: any) => {
const {
} = props
const [dataSource, setDataSource] = useState(BIG_IMAGE_DATA)
const [showDesc, setShowDesc] = useState(true)
const [editAble, setEditAble] = useState(false)
const [odList, setOdList] = useState(get(dataSource, 'odRect', []))
const [type, setType] = useState('normal')
const bigImageRef = useRef(null)
return (
<div style={{ padding: '32px', border: '1px solid #09f' }}>
<Space>
<span><Switch value={showDesc} onChange={e => setShowDesc(pre => !pre)} /></span>
<span><Switch value={editAble} onChange={e => setEditAble(pre => !pre)} /></span>
<Button onClick={() => setOdList(testOd)}>Od</Button>
</Space>
<BigImage
type={type}
ref={bigImageRef}
width={896}
viewHeight={'440px'}
showModeChange
onTabChange={(newVal) => setType(newVal === '1'? 'normal': 'compater')}
// ------------ 属性列表 -----------------
showDescription={showDesc}
descriptionList={attributeList}
// ------------ 图片/视频切换 -----------------
viewTopModeButtonList={[
{
key: 'image',
label: '图片'
},
{
key: 'video',
label: '视频'
}
]}
modeButtonGroupProps={{
defaultValue: 'image',
onChange: e => console.log('模式切换', e)
}}
// ------------ 导航功能 -----------------
showNavigation
onPrevButtonClick={val => console.log('pre', val)}
onNextButtonClick={val => console.log('next', val)}
// ------------ 场景图功能 -----------------
cropperImageProps={{
editAble,
url: Img,
score: 50,
odList: odList,
showEditTools: editAble,
attachImageData: get(dataSource, 'attachImg', []),
onCropperTypeChange: v => console.log('框选模式', v),
onExitEdit: () => setEditAble(pre => !pre)
}}
// ------------ 对比图模式 -----------------
combineImageProps={{
data: pick(dataSource, 'compaterImage', 'imgSummary', 'imageKey', 'score'),
}}
// ------------ 视频模块 -----------------
videoProps={{
url: get(dataSource, 'flvUrl', '')
}}
>
</BigImage>
</div>
)
}
export default BigModal

View File

@ -0,0 +1,78 @@
import React, { useEffect, useRef, useState } from 'react';
import { BigImage } from '@zhst/biz'
import { Button, Space, Switch } from '@zhst/meta'
import { pick, get } from '@zhst/func'
import { BIG_IMAGE_DATA, attributeList, RELATED_IMAGES } from '../mock'
const BigModal = (props: any) => {
const {
} = props
const [visible, setVisible] = useState(true)
const [dataSource, setDataSource] = useState(BIG_IMAGE_DATA)
const [selectedItemKey, setSelectedItemKey] = useState()
const [showFaceModel, setShowFaceModel] = useState(true)
const [type, setType] = useState('normal')
const modalRef = useRef(null)
return (
<div style={{ padding: '32px', border: '1px solid #09f' }}>
<Space>
<span><Switch value={showFaceModel} onChange={e => setShowFaceModel(pre => !pre)} /></span>
</Space>
<BigImage
type={type}
open={visible}
ref={modalRef}
viewHeight={'440px'}
width={896}
onCancel={() => setVisible(false)}
showModeChange
onTabChange={(newVal, oldVal) => setType(newVal === '1'? 'normal': 'compater')}
// ------------ 图片/视频切换 -----------------
viewTopModeButtonList={[
{
key: 'image',
label: '图片'
},
{
key: 'video',
label: '视频'
}
]}
modeButtonGroupProps={{
defaultValue: 'image',
onChange: e => console.log('模式切换', e)
}}
// ------------ 场景图功能 -----------------
cropperImageProps={{
url: "https://gw.alipayobjects.com/zos/antfincdn/LlvErxo8H9/photo-1503185912284-5271ff81b9a8.webp",
score: 50,
odList: get(dataSource, 'odRect', []),
attachImageData: get(dataSource, 'attachImg', []),
}}
// ------------ 对比图模式 -----------------
combineImageProps={{
data: pick(dataSource, 'compaterImage', 'imgSummary', 'imageKey', 'score'),
}}
// ------------ 视频模块 -----------------
videoProps={{
url: get(dataSource, 'flvUrl', '')
}}
// ------------ 人脸碰撞模型 -----------------
isRelated={showFaceModel}
relatedImageProps={{
activeTab: 'related',
selectedItemKey,
data: RELATED_IMAGES,
onCancel: () => console.log('取消关联'),
onConnect: () => console.log('关联'),
onTabChange: (newVal, oldVal) => console.log('tab切换', newVal, oldVal),
onItemSelected: (item) => setSelectedItemKey(item.key)
}}
>
</BigImage>
</div>
)
}
export default BigModal

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

View File

@ -0,0 +1,132 @@
.zhst-big-image {
.zhst-dialog-content {
box-shadow: 0 4px 12px rgb(0 0 0 / 20%);
}
.zhst-tabs .zhst-tabs-nav-wrap {
background-color: #f6f6f6;
}
&-view {
&-btns {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
&-desc {
margin-bottom: 16px;
padding: 16px 16px 0 16px;
background-color: #f7f7f7;
.zhst-descriptions-header {
margin-bottom: 8px;
}
.zhst-descriptions-title {
font-size: 14px;
}
}
// ------------ tab -----------------
.zhst-tabs-nav {
margin-bottom: 8px;
.zhst-tabs-tab {
&:not(:first-child) {
margin-left: 24px;
}
padding: 0;
padding-bottom: 4px;
}
&::before {
display: none;
}
}
&-container {
position: relative;
min-height: 440px;
width: 100%;
margin-bottom: 16px;
font-size: 0;
&-modeNav {
padding: 2px;
position: absolute;
top: 4px;
left: 4px;
z-index: 10;
border-radius: 4px;
}
&__nav {
position: absolute;
z-index: 99;
top: 50%;
width: 40px !important;
height: 40px !important;
background: #d9d9d9;
border-radius: 100%;
cursor: pointer;
transform: translateY(-50%);
&>button {
display: flex;
align-items: center;
color: #fff !important;
}
&--disabled {
opacity: 0.3;
&>button {
color: #fff !important;
}
}
}
&__nav:hover {
background: #09f;
color: #fff !important;
}
// ------------ 导航 -----------------
&-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.8);
&:hover {
opacity: 0.8;
background-color: rgba(255, 255, 255, 0.8)!important;
}
&_left {
left: 8px;
}
&_right {
right: 8px;
}
}
}
// ------------ 场景图 -----------------
&-cropper-btn {
position: absolute;
top: 4px;
right: 4px;
&_close {
margin-left: 4px;
vertical-align: top;
}
&-group {
.zhst-radio-button-wrapper {
display: inline-flex;
font-size: 12px;
color: #191919;
background-color: rgba(255, 255, 255, 0.8)
}
&-radio {
display: inline-flex;
align-items: center;
.anticon {
margin-right: 4px;
}
}
}
}
}
}

View File

@ -0,0 +1,96 @@
---
group: 进阶组件
category: Components
subtitle: 大图组件
toc: content
title: BigImage 大图组件
---
## 大图弹框
<code src="./demo/index.tsx">基本</code>
<code src="./demo/withRelatedImage.tsx">人脸碰撞模型</code>
<code src="./demo/withEdit.tsx">编辑模式</code>
### API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| type | 当前模式 | 'compater'、'normal'、'video' | - | |
| viewHeight | 视图高度 | string、number | - | |
| width | 宽度 | string、number | - | |
| showDescription | 描述列表显示\隐藏 | boolean | false | |
| descriptionsProps | 描述列表透传 | | antd - DescriptionsProps | |
| descriptionList | 描述列表数据(见下文) | | IDescriptionList | |
| topButtonRender | 顶部按钮 | | ReactNode、string | |
| customTabBarExtraContent | 自定义tab尾部导航插槽 | | ReactNode、 string | |
| onTabChange | tab事件监听 | | antd - TabsProps['onChange'] | |
| tabProps | tab透传 | | antd - TabsProps | |
| showModeChange | 是否显示模式切换按钮 | | boolean | |
| viewTopModeButtonList | 模式切换按钮列表 | | TypeRadio[] | |
| modeButtonGroupProps | 模式切换按钮组透传 | RadioGroupProps | - | |
| isRelated | 人脸碰撞模型显示\隐藏 | boolean | | |
| relatedImageProps | 人脸碰撞模型透传 | zhst/meta - RelatedImageProps | | |
| cropperImageProps | 场景图模式透传 | ICropperImageProps | | |
| combineImageProps | 对比图模式透传 | ComBineImageProps | | |
| videoProps | 视频模式透传 | videoProps | | |
| showNavigation | 是否展示导航 | boolean | | |
| prevButtonProps | 前翻箭头透传 | prevButtonProps | | |
| onPrevButtonClick | 前翻箭头点击事件 | () => void; | | |
| nextButtonProps | 下翻箭头透传 | () => void; | | |
| onNextButtonClick | 下翻箭头点击事件 | () => void; | | |
| children | | ReactNode | | |
### IDescriptionList
```ts
interface IDescriptionList {
title: string;
showTitle?: boolean;
props?: DescriptionsProps
children: DescriptionsProps['items']
}[]
```
###
```ts
interface ICropperImageProps extends CropperImageProps {
showEditTools?: boolean // 是否展示编辑按钮
editAble?: boolean // 是否开启编辑模式
score?: string | number // 分数
btnGroupProps?: BtnGroupProps; // crop 场景图模式下的按钮拓展
cropButtonList?: TypeRadio[] // 编辑按钮列表
onCropperTypeChange?: (type: RadioProps['onChange']) => void
onExitEdit?: ButtonProps['onClick'] // 退出编辑模式
attachImageData?: AttachImageProps['data'] // 左下角预览图
attachImageProps?: AttachImageProps // 左下角预览图透传
}
```
## 设计方案
结合当下的业务场景,目前大图组件有三种模式
1. 场景图模式
-
2. 对比图模式
3. 视频模式
场景图和视频模式,支持用户编辑圈选
三种模式状态下,都会有外层模块的嵌套,目前有以下几种:
1. 描述模块
2. 顶部拓展
- 目前仅支持自定义
- 默认下边间距 16px
3. tabs 切换
- 默认下间距16px
- 支持自定义文案
- 支持自定义数量
- 支持后方插槽
4. 人脸碰撞模型
- 支持用户自定义传入数据
本来想通过插件的形式按需加载

View File

@ -0,0 +1,5 @@
import BigImage from "./BigImage";
export type { BigImageProps, BigImageRef } from './BigImage'
export default BigImage

View File

@ -0,0 +1,84 @@
import { ReactNode } from 'react'
import {
ButtonProps,
RadioProps,
RadioGroupProps,
VideoViewProps,
DescriptionsProps,
TabsProps,
RelatedImageProps,
BtnGroupProps,
CropperImageProps,
VideoViewRef,
AttachImageProps
} from '@zhst/meta'
import { ComBineImageProps } from './components/CombineImage'
export type TypeRadio = {
label?: string;
key: string;
tooltipTxt?: string;
onClick?: ButtonProps['onClick']
icon?: ReactNode | string;
props?: ButtonProps
}
type TypePlugin = 'compater' | 'normal' | 'video' | 'faceMode'
export interface BigImageProps {
type?: TypePlugin // 当前 tab
viewHeight?: string | number;
width?: string | number
typePlugins?: TypePlugin[] // TODO: 开启插件类型
// ------------ 描述列表 -----------------
showDescription?: boolean;
descriptionsProps?: DescriptionsProps
descriptionList?: {
title: string;
showTitle?: boolean;
props?: DescriptionsProps
children: DescriptionsProps['items']
}[]
// ------------------ 顶部按钮列表
topButtonRender?: ReactNode | string
// ---------------- Tabs 导航 ------------------
customTabBarExtraContent?: string | ReactNode
onTabChange?: TabsProps['onChange']
tabProps?: TabsProps
// ------------ 图片 视频切换导航 -----------------
showModeChange?: boolean
viewTopModeButtonList?: TypeRadio[]
modeButtonGroupProps?: RadioGroupProps
// ----------------- 人脸碰转 -----------------
isRelated?: boolean // 人脸碰撞功能打开
relatedImageProps?: RelatedImageProps
// -------------------------- crop 场景图模式 -----------------------
cropperImageProps?: CropperImageProps & {
showEditTools?: boolean
editAble?: boolean
score?: string | number
btnGroupProps?: BtnGroupProps; // crop 场景图模式下的按钮拓展
cropButtonList?: TypeRadio[]
onCropperTypeChange?: (type: RadioProps['onChange']) => void
onExitEdit?: ButtonProps['onClick']
attachImageData?: AttachImageProps['data']
attachImageProps?: AttachImageProps
}
// -------------------- 对比图模式 -------------------------
combineImageProps?: ComBineImageProps
// ------------ 导航 -----------------
showNavigation?: boolean // 是否展示导航箭头
prevButtonProps?: any;
onPrevButtonClick?: () => void;
nextButtonProps?: any
onNextButtonClick?: () => void;
theme?: any
children?: React.ReactNode
// ------------ 视频模式 -----------------
videoProps?: VideoViewProps
}
export interface BigImageRef {
videoPlayerRef: VideoViewRef
combineImageRef: any
}

View File

@ -0,0 +1,292 @@
import imageUrl from './demo/imgs/photo-1503185912284-5271ff81b9a8.webp'
export const IMAGE_DATA = {
"enAbleDeleteFeature": true,
"tabsFilter": [
"COMPATER",
"NORMAL"
],
"selectIndex": 4,
"disableBtn": [
0,
1,
4,
20
],
"dataSource": {
"objectId": "1742110565582518272",
"condition": {
"featureInfo": null,
"featureData": "AAAAAAAAAAAAAAAAAAAAAAA==",
"imageData": "",
"alg": "VERSION_REID_HEAD_ATTR",
"rect": {
"x": 0.271875,
"y": 0.32222223,
"w": 0.2859375,
"h": 0.67777777
},
"objectImageUrl": "singer-20240102/1/129529/1742047651878156288.jpg",
"srcImageUrl": "singer-20240102/1/129529/1742047652511496192.jpg"
},
"score": 0.7163062,
"timestamp": 1704186491979,
"deviceId": "129533",
"id": "129533",
"name": "4楼门口过道人脸",
"dirid": "0",
"status": "1",
"longitude": 120.125,
"latitude": 30.280500411987305,
"caseId": "0",
"caseGroup": "",
"isDeleted": "DEVICEMANAGER_BOOL_DEFAULT",
"objectIndex": {
"objectId": "1742110565582518272",
"solutionId": "1",
"deviceId": "129533",
"fragmentId": "0"
},
"objectType": "OBJECT_TYPE_PEDESTRAIN",
"isObjectTrack": true,
"pathId": "1742110532019697664",
"frameInfo": {
"frameId": "0",
"frameTimestamp": "1704186491979",
"width": 0,
"height": 0,
"originWidth": 1920,
"originHeight": 1080,
"offsetTime": "24714687",
"skipNumber": "0"
},
"level": 1,
"bboxInFrame": {
"x": 0.603125,
"y": 0.3314815,
"w": 0.0578125,
"h": 0.2712963
},
"bboxExtInFrame": {
"x": 0.546875,
"y": 0.2638889,
"w": 0.17135416,
"h": 0.40648147
},
"objectImageKey": "",
"objectExtImageKey": "http://10.0.0.7:30003/file/singer-20240102/1/129533/1742110565582518272.jpg",
"frameImageKey": "http://10.0.0.7:30003/file/singer-20240102/1/129533/1742110565603489792.jpg",
"confidence": 0.817271,
"sourceObjectId": "1742110565603489792",
"storeTimestamp": "0",
"gbNumber": "",
"qualityScore": 0,
"subObjectCount": 1,
"subObjectType": [
"OBJECT_TYPE_FACE"
],
"subObjectIds": [
"1742110532015503360"
],
"solutionId": "1",
"fragmentId": "0",
"contrastKey": "singer-20240102/1/129533/1742110565582518272.jpg",
"compaterImages": [
"http://10.0.0.7:30003/file/singer-20240102/1/129529/1742047651878156288.jpg"
],
"imgSummary": "singer-20240102/1/129533/1742110565582518272.jpg",
"imageKey": "http://10.0.0.7:30003/file/singer-20240102/1/129533/1742110565582518272.jpg",
"srcImageUrl": "http://10.0.0.7:30003/file/singer-20240102/1/129533/1742110565603489792.jpg",
"algorithmVersion": "VERSION_REID_HEAD_ATTR",
"cameraId": "129533",
"cameraName": "4楼门口过道人脸"
},
"isArchiveDetail": false,
"ToolProps": {
"renderVideoBtn": true,
"disableVideo": false
},
"specialTitle": ""
}
export const RELATED_IMAGES = [
{ key: '123', url: imageUrl },
{ key: '22', url: imageUrl },
{ key: '22122333', url: imageUrl },
{ key: '2212243', url: imageUrl },
{ key: '224523433', url: imageUrl },
{ key: '224235453', url: imageUrl },
{ key: '245423', url: imageUrl },
{ key: '224233543', url: imageUrl },
{ key: '22452343', url: imageUrl },
{ key: '22323243', url: imageUrl },
{ key: '2236456', url: imageUrl },
{ key: '224563', url: imageUrl },
{ key: '24565423', url: imageUrl },
{ key: '245645623', url: imageUrl },
{ key: '2456435623', url: imageUrl },
{ key: '2323', url: imageUrl }
]
export const BIG_IMAGE_DATA = {
imageKey: imageUrl,
imgSummary: imageUrl,
flvUrl: 'ws://10.0.0.120:9033/flv/HaikangNvr/45.flv?ip=10.0.2.103&stime=1712539148&etime=1712539168',
compaterImage: imageUrl,
odRect: [
{
id: '123',
"x":0.5445312,
"y":0.19166666,
"w":0.08671875,
"h":0.40138888
},
{
"id": "123",
"x": 0.5519352,
"y": 0.2965385,
"w": 0.05185461,
"h": 0.24698898,
},
{
"id": "456",
"x": 0.58543766,
"y": 0.3203356,
"w": 0.052037954,
"h": 0.2664015
}
],
attachImg: [
{
"url": imageUrl,
"label": "形体"
},{
"url": imageUrl,
"label": "人脸"
}
],
score: 60, // 人脸质量分
showScore: true, // 人脸质量分
cameraPosition: 'string', // 摄像头位置
time: '2022-01-01', // 摄像头拍摄时间
objects: [
{
"objectIndex": {
"objectId": "1746832189053474816",
"solutionId": "0",
"deviceId": "0",
"fragmentId": "0"
},
"objectType": "OBJECT_TYPE_PEDESTRAIN",
"sourceObjectId": "0",
"level": 0,
"confidence": 0.881164,
"frameInfo": {
"frameId": "0",
"frameTimestamp": "1705312223057",
"width": 0,
"height": 0,
"originWidth": 0,
"originHeight": 0,
"offsetTime": "0",
"skipNumber": "0"
},
"infoOnSource": {
"bboxInFrame": {
"bboxRatio": {
"x": 0.61418945,
"y": 0.34309354,
"w": 0.067661405,
"h": 0.34659258
},
},
"countInSource": 0,
"indexInSource": 0
},
"qualityScore": 0,
}
]
}
export const attributeList = [
{
title: '人员属性',
children: [
{
key: '1',
label: '性别',
children: '男',
},
{
key: '2',
label: '年龄',
children: '成年',
},
{
key: '3',
label: '帽子',
children: '无',
},
{
key: '4',
label: '上身颜色',
children: '灰',
},
{
key: '5',
label: '下身颜色',
children: '蓝色',
},
{
key: '6',
label: '附着物',
children: '无',
},
{
key: '7',
label: '骑行',
children: '否',
},
]
},
{
title: '社区规范',
children: [
{
key: '1',
label: '性别',
children: '男',
},
{
key: '2',
label: '年龄',
children: '成年',
},
{
key: '3',
label: '帽子',
children: '无',
},
{
key: '4',
label: '上身颜色',
children: '灰',
},
{
key: '5',
label: '下身颜色',
children: '蓝色',
},
{
key: '6',
label: '附着物',
children: '无',
},
{
key: '7',
label: '骑行',
children: '否',
},
]
}
];

View File

@ -0,0 +1,195 @@
/**
*
*/
import React, { } from 'react';
import { AlgorithmVersionStr, HumanProperty, ObjectType, Rect, IScreenshotButtonProp } from '@zhst/types'
import { VideoViewProps, ImgViewProps, VideoViewRef, ImgViewRef } from '@zhst/meta'
export type TAB_TYPE = 'COMPATER' | 'NORMAL' | 'TRACK';
export type MODEL_TYPE = 'VIDEO' | 'IMAGE';
export interface CarouselProps {
hasPre?: boolean;
hasNext?: boolean;
selectIndex: number;
setSelectIndex: React.Dispatch<React.SetStateAction<number>>;
dataSource: Array<{
key: string;
url: string;
}>;
}
export type ISelectItem = Partial<Omit<ImgViewProps, 'screenshotButtonRender'>> &
Partial<Omit<VideoViewProps, 'screenshotButtonRender'>>;
/**
* description
*/
export interface HeaderProps {
value: TAB_TYPE;
onChange: (type: TAB_TYPE) => void;
tabsFilter: TAB_TYPE[];
}
export interface ParamProps {
tab: string;
selectItem: ISelectItem;
imgViewRef: React.MutableRefObject<ImgViewRef>;
VideoViewRef: React.MutableRefObject<VideoViewRef>;
model: MODEL_TYPE;
setModel: React.Dispatch<React.SetStateAction<MODEL_TYPE>>;
/* 可观测值 */
scale$: number;
showCrop$: boolean;
}
/**
*
*/
export interface ToolProps {
renderRight?: (props: ParamProps) => React.ReactNode;
renderLeft?: (props: ParamProps) => React.ReactNode;
renderVideoBtn?: boolean;
param: ParamProps;
disableVideo: boolean;
}
export interface BigImageData {
//imageKey 小图
extendRectList: (Rect & { algorithmVersion: AlgorithmVersionStr; imageKey: string })[];
rectList: (Rect & { algorithmVersion: AlgorithmVersionStr; imageKey: string })[];
attachImg: { url: string; label: '形体' | '人脸' }[];
odRect: Rect;
compaterImages: string[] // 目标图列表
constractKey: string; // 当前比较中的目标图
frameImageKey: string; // 场景图
imageKey?: string; // 大图
imgSummary: string; // 摘要图
objectExtImageKey: string; //比对到的目标图扩展图 === imgSummary
attributeList: { label: string; list: any[] }[];
archiveImages?: any;
spaceName: string;
objectIndex?: {
deviceId: string;
fragmentId: string;
objectId: string;
solutionId: string;
}
objectType: ObjectType;
objectId: string; //这张摘要本身的Id
bodyObjectId?: string;
faceObjectId?: string; //这张摘要下的人脸Id(如果有)
sourceObjectId?: string; //这张摘要上游的形体Id(如果有)
cameraId: string;
cameraName: string;
selectIndex: number;
humanProperty: HumanProperty;
qualityScore?: number; //人脸质量分
score: number; // 相似度
timestamp: string;
bodyImageUrl: string;
faceImageUrl: string;
algorithmVersion: AlgorithmVersionStr;
bodySpaceName: string;
faceSpaceName: string;
position: {
lat: number
lng: number
}
solutionId?: string;
[index: string]: any;
}
interface IOldImageData {
visible?: boolean; // 显示隐藏弹框
defaultModel?: MODEL_TYPE; // 视频模式 | 图片模式
onClose?: () => void; // 关闭弹框
isLoading?: boolean; // 是否加载中
hasPre?: boolean; // 向前翻页
hasNext?: boolean; // 向后翻页
selectIndex?: number; // 选中的数据dataSource为数组情况下
onSelectIndexChange?: (i: number) => void; // 修改当前下标
dataSource: BigImageData[]; // 数据1
dataSources: BigImageData[]; // 数据2
relatedData?: BigImageData[]; // 数据3
transformPropFunc: (item: any) => ISelectItem; // 格式化数据方法
transformVideoPropFunc: (
item: ISelectItem
) => Promise<Omit<VideoViewProps, 'screenshotButtonRender'>>; // 视频模式格式化数据方法
screenshotButtonRender?: (screenshotButtonProp: IScreenshotButtonProp) => React.ReactElement;
showTool?: boolean; // 是否显示底部菜单
showCarousel?: boolean; //
imgViewProp?: Partial<ImgViewProps>; // 图片模式的配置
videoViewProp?: Partial<VideoViewProps>; // 视频模式的配置
ToolProps?: Partial<ToolProps>; // 底部菜单的配置
nullDialogProp?: {
emptyText?: string;
}; // 暂无数据的配置
showHeader?: boolean; // 是否显示 description
tabsFilter?: TAB_TYPE[]; // tabs 过滤
useVirtual?: boolean; // 是否显示虚拟
loadNext?: (i: number) => Promise<void>; // 下一个
loadPre?: (i: number) => Promise<void>; // 前一个
children: React.ReactNode; // 子元素
title?: string; // 标题
specialTitle?: string; // 对比图模式下标题
isRelated?: boolean;
carouselProp?: Partial<CarouselProps>;
}
export interface ImageModalDataProps {
targetData: BigImageData[]
compactData: BigImageData[]
}
export interface ModalAdapterConfigProps {
oldMode?: boolean; // 是否是老模式
}
/**
*
* @param _data
* @returns newData
*/
const translateOldImageData = (_data: IOldImageData) => {
return {
..._data,
open: _data.visible,
onCancel: _data.onClose
}
}
/**
*
* @param Cmp
* @param config
* @returns
*/
const adapter = (Cmp: any, config: ModalAdapterConfigProps): any => {
const { oldMode = false } = config
return (props: IOldImageData) => {
const newProps = oldMode ? translateOldImageData(props) : props
console.log('adapter----适配数据', props, newProps)
// 该属性已经废弃
delete newProps.visible
return (
<Cmp
{...newProps}
/>
)
}
}
export default adapter

View File

@ -0,0 +1,36 @@
export interface IBigImageModalData {
imageKey?: string // 目标图
imgSummary?: string // 大图
flvUrl?: string // 视频链接
compaterImages?: string[] // 对比图
odRect?: { // od 框数据
"x": number
"y": number
"w": number
"h": number
[key: string]: string | number; // 拓展参数
}[],
attachImg?: { // 小图,只有在场景图模式生效(人脸、形体)
"url": string
"label": string
[key: string]: string
}[],
score?: number | string // 人脸质量分
showScore?: boolean // 人脸质量分
cameraPosition?: string // 摄像头位置
time?: string // 摄像头拍摄时间
objects: { // 拓展参数、可以自由支配
objectIndex: {
[key: string]: any
},
objectType: string
sourceObjectId: string
level: number
confidence: number
infoOnSource: {
[key: string]: any
},
qualityScore: number
[key: string]: any
}[]
}

View File

@ -1,5 +1,7 @@
export { default as BigImageModal } from './BigImageModal'
export type { BigImageModalProps } from './BigImageModal'
export { default as BigImage } from './BigImage'
export type { BigImageProps, BigImageRef } from './BigImage'
export { default as BoxSelectTree } from './boxSelectTree'
export type { BoxSelectTreeProps } from './boxSelectTree'
export { default as Tree } from './tree'

View File

@ -1 +0,0 @@
export {};

View File

@ -1 +0,0 @@
export {};

View File

@ -1,13 +1,16 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
plugins: ['father-plugin-less'],
});

View File

@ -1,3 +0,0 @@
.icon:hover {
color: #6accca !important;
}

View File

@ -1,13 +1,16 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
plugins: ['father-plugin-less'],
});

View File

@ -1,7 +1,7 @@
/**
* Created by jiangzhixiong on 2024/05/21
*/
import { forwardRef, useImperativeHandle, useRef, } from 'react'
import { forwardRef, useImperativeHandle, useRef, } from 'react'
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import {
DragCircleMode,
@ -14,12 +14,11 @@ import drawRectMode from 'mapbox-gl-draw-rectangle-mode'
// @ts-ignore
import drawStaticMode from '@mapbox/mapbox-gl-draw-static-mode'
// @ts-ignore
import drawCircleMode from './Draw/drawCircleMode.draw.js'
import drawCircleMode from './mode/drawCircleMode.draw.js'
import { useControl } from 'react-map-gl';
import type { ControlPosition } from 'react-map-gl';
import { MapContextValue } from 'react-map-gl/dist/esm/components/map';
export type DrawControlProps = ConstructorParameters<typeof MapboxDraw>[0] & {
export interface DrawControlProps extends MapboxDraw {
position?: ControlPosition;
onCreate?: (evt: {features: object[]}) => void;
onUpdate?: (evt: {features: object[]; action: string}) => void;
@ -38,6 +37,7 @@ export interface DrawControlRefProps {
}
}
// @ts-ignore
const DrawControl = forwardRef<DrawControlRefProps, DrawControlProps>((props, ref) => {
const drawRef = useRef<DrawControlRefProps['drawer']>(null)
@ -45,12 +45,13 @@ const DrawControl = forwardRef<DrawControlRefProps, DrawControlProps>((props, re
() => {
let draw = new MapboxDraw(
{
// @ts-ignore
modes: {
...MapboxDraw.modes,
// draw_line_select: drawLineSelectMode,
draw_rect: drawRectMode,
drag_circle: DragCircleMode,
draw_circle : drawCircleMode,
draw_circle: drawCircleMode,
direct_select: DirectMode,
simple_select: SimpleSelectMode,
static: drawStaticMode,
@ -62,7 +63,7 @@ const DrawControl = forwardRef<DrawControlRefProps, DrawControlProps>((props, re
drawRef.current = draw
return draw
},
(context: MapContextValue) => {
(context: any) => {
const { map } = context
map.on('draw.create', e => props.onCreate?.(e));
map.on('draw.update', e => props.onUpdate?.(e));
@ -98,11 +99,4 @@ const DrawControl = forwardRef<DrawControlRefProps, DrawControlProps>((props, re
return null;
})
DrawControl.defaultProps = {
onCreate: () => {},
onUpdate: () => {},
onDelete: () => {},
};
export default DrawControl

View File

@ -0,0 +1,24 @@
const doubleClickZoom = {
enable(ctx) {
setTimeout(() => {
if (
!ctx.map ||
!ctx.map.doubleClickZoom ||
!ctx._ctx ||
!ctx._ctx.store ||
!ctx._ctx.store.getInitialConfigValue
)
return;
if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return;
ctx.map.doubleClickZoom.enable();
}, 0);
},
disable(ctx) {
setTimeout(() => {
if (!ctx.map || !ctx.map.doubleClickZoom) return;
ctx.map.doubleClickZoom.disable();
}, 0);
},
};
export default doubleClickZoom;

View File

@ -0,0 +1,69 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import doubleClickZoom from './doubleClickZoom';
import * as turf from '@turf/turf';
const { circle, distance, helpers: turfHelpers } = turf;
const drawCircleMode = { ...MapboxDraw.modes.draw_polygon };
drawCircleMode.onSetup = function () {
const polygon = this.newFeature({
type: 'Feature',
properties: {
isCircle: true,
center: [],
},
geometry: {
type: 'Polygon',
coordinates: [],
},
});
this.addFeature(polygon);
this.clearSelectedFeatures();
doubleClickZoom.disable(this);
// dragPan.disable(this);
this.updateUIClasses({ mouse: 'add' });
this.activateUIButton('Polygon');
this.setActionableState({
trash: true,
});
return {
polygon,
currentVertexPosition: 0,
};
};
drawCircleMode.onClick = drawCircleMode.onTap = function (state, e) {
const currentCenter = state.polygon.properties.center;
if (currentCenter.length === 0) {
// dragPan.disable(this)
state.polygon.properties.center = [e.lngLat.lng, e.lngLat.lat];
} else {
// dragPan.enable(this);
return this.changeMode('simple_select', { featureIds: [state.polygon.id] });
}
};
drawCircleMode.onDrag = drawCircleMode.onMouseMove = function (state, e) {
const center = state.polygon.properties.center;
if (center.length > 0) {
const distanceInKm = distance(
turfHelpers.point(center),
turfHelpers.point([e.lngLat.lng, e.lngLat.lat]),
{
units: 'kilometers',
}
);
const circleFeature = circle(center, distanceInKm);
state.polygon.incomingCoords(circleFeature.geometry.coordinates);
state.polygon.properties.radiusInKm = distanceInKm;
state.polygon.properties.lastClickCoord = [e.lngLat.lng, e.lngLat.lat];
}
};
//它决定当前 Drew 数据存储中的哪些特性将在地图上呈现。
//所有传递给“显示”的特性都将被渲染,因此可以为每个内部特性传递多个显示特性。
//有关如何制作显示特性的建议,请参阅‘ styling-pullin API.md
drawCircleMode.toDisplayFeatures = function (state, geojson, display) {
const isActivePolygon = geojson.properties.id === state.polygon.id;
geojson.properties.active = isActivePolygon ? 'true' : 'false';
display(geojson);
};
export default drawCircleMode;

View File

@ -1,13 +1,16 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
plugins: ['father-plugin-less'],
});

View File

@ -1,13 +1,16 @@
import { defineConfig } from 'father';
import { defineConfig } from 'father-plugin-less';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: {
output: 'es',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
cjs: {
output: 'lib',
ignores: ['**/demo/*', 'src/**/demo/*']
ignores: ['**/demo/*', 'src/**/demo/*'],
transformer: 'babel',
},
plugins: ['father-plugin-less'],
});

View File

@ -1,51 +0,0 @@
.zhst-image__btn-group {
// display: flex;
width: 30px;
box-shadow: 0 2px 6px 0 rgb(0 0 0 / 40%);
&__item {
display: flex;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
background: #000;
&>button {
padding: 0;
color: #fff;
&:hover {
color: #09f;
}
&:active {
color: #09f;
}
&:focus {
color: #fff;
}
&>span {
display: flex;
}
}
&--active {
&>button {
color: #09f;
}
}
}
&--circle {
background-color: none;
box-shadow: none;
}
&--circle &__item {
margin-bottom: 4px;
border-radius: 50%;
}
}

View File

@ -1,54 +0,0 @@
.zhst-image__img-view {
position: relative;
width: calc(100%);
height: 100%;
font-size: 0;
&__face-score {
position: absolute;
right: 20px;
bottom: 80px;
color: red;
font-family: 'Microsoft YaHei';
font-size: 19px;
font-weight: bold;
}
&-opt {
position: absolute;
z-index: 99;
top: 0;
}
&-crop-opt {
position: absolute;
z-index: 99;
top: 0;
right: 0;
}
&-align {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
&-main {
width: 100%;
height: 100%;
font-size: 0;
&--cursor {
& canvas {
min-height: 320px;
cursor: pointer;
}
}
}
&-screenshot {
position: absolute;
z-index: 10;
}
}

View File

@ -16,8 +16,12 @@ var CompareImage = /*#__PURE__*/forwardRef(function (props, ref) {
var customizePrefixCls = props.prefixCls,
_props$label = props.label,
label = _props$label === void 0 ? '标题' : _props$label,
_props$width = props.width,
width = _props$width === void 0 ? '400' : _props$width,
height = props.height,
_props$openRoll = props.openRoll,
openRoll = _props$openRoll === void 0 ? true : _props$openRoll,
labelColor = props.labelColor,
_props$url = props.url,
url = _props$url === void 0 ? '' : _props$url,
_props$score = props.score,
@ -33,6 +37,7 @@ var CompareImage = /*#__PURE__*/forwardRef(function (props, ref) {
getPrefixCls = _useContext.getPrefixCls;
var componentName = getPrefixCls('image__compater-view', customizePrefixCls);
var imgContainerRef = useRef(null);
var containerRef = useRef(null);
var imgInsRef = useRef(null);
var _useState = useState(0),
_useState2 = _slicedToArray(_useState, 2),
@ -47,8 +52,10 @@ var CompareImage = /*#__PURE__*/forwardRef(function (props, ref) {
setScale(get(data, 'scale', 0));
});
if (generateImg(url)) {
console.log('containerRef', containerRef);
imgInsRef.current = new Viewer(imgContainerRef.current, {
image: generateImg(url)
image: generateImg(url),
height: parseInt(height)
});
}
return function () {
@ -74,9 +81,15 @@ var CompareImage = /*#__PURE__*/forwardRef(function (props, ref) {
};
});
return /*#__PURE__*/React.createElement("div", {
className: classNames("".concat(componentName, "__container"))
className: classNames("".concat(componentName, "__container")),
style: {
width: "".concat(parseInt(width), "px")
}
}, label && /*#__PURE__*/React.createElement("div", {
className: classNames("".concat(componentName, "__label"))
className: classNames("".concat(componentName, "__label")),
style: {
backgroundColor: labelColor
}
}, label), !url ? /*#__PURE__*/React.createElement("div", {
className: classNames("".concat(componentName, "__empty"))
}, /*#__PURE__*/React.createElement("img", {

View File

@ -1,21 +0,0 @@
.zhst-image__CornerScore {
position: absolute;
right: 0;
bottom: 0;
width: 56px;
height: 22px;
line-height: 22px;
text-align: right;
padding-right: 1px;
background-size: 100%;
background-image: url('data: image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAAAsCAYAAAC9rDzHAAAByUlEQVR4Xu2cO08DMRCEJy0tlNBCSwmU0EIJLbS0UEILLTX/Fo3kk1AUHrrsjB3dWIoUnZTdvfnOZ6+1mxXGGHsADgEcA9hvnyMAvJ7xiwKrjuoQ0HmDxu8ZMxToAZDQrgAE2gxg6z9xAjwFcAvgoCDumGgKuADeA7iI6vUKqAFyE/KU12U9uMmiGuBr4Ong0bIS4A2Aa234sa4CyFzuPfLqFVABfGg5nv4OFu5BAZAbl4+F62q7fQVAJul3tjtYuCMFwEcATNozDAooAL7ltMVATngS8+kLP54UMzAAjc9VABrFVrgKQIWqRpsBaBRb4SoAFaoabQagUWyFqwBUqGq0GYBGsRWuAlChqtFmABrFVrgKQIWqRpsBaBRb4SoAFaoabVYDZLU1K9EyTApUA2RzyrMp9rgRlBWylIIlFRkmBapnIIuZ0hJmgkc3lQBTzGQEN7mqApjNSwd4VTOQr8yXFDL1IbjtDOTMY/dR1r0+/LZaA1n7yb6/wOsEb+4rlMDYdZR0oSO4uZuYtEkPAO17CP9dA3nCwll3Mlj8iw/nL4ABN/gj8hPAgBsc3KY1kJuTs7Y5yV+B7BBA5nKXrSUsKcGOgJvC/ALSJBp5mBHCFAAAAABJRU5ErkJggg==');
z-index: 99;
&>span {
padding-right: 6px;
line-height: 22px;
font-size: 12px;
vertical-align: middle;
color: rgba(255, 255, 255, 100%);
}
}

View File

@ -1,144 +0,0 @@
.zhst-image__compater-view {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
&>div:first-child {
margin-right: 25px;
}
&>div:last-child {
margin-left: 25px;
}
&__container {
font-size: 0;
position: relative;
box-sizing: content-box;
border: 1px solid #f0f0f0;
}
&__view {
min-width: 345px;
min-height: 450px;
}
&__label {
position: absolute;
z-index: 99;
top: 0;
left: 0;
display: flex;
font-size: 16px;
height: 34px;
align-items: center;
justify-content: center;
// width: 48px;
padding: 0 6px;
background: #09f;
color: #fff;
}
&__tool {
display: flex;
width: 100%;
height: 40px;
align-items: center;
justify-content: center;
background: #f9f9f9;
i,
span {
color: #333 !important;
}
i {
margin-right: 4px;
}
&>*:not(:last-child) {
margin-right: 20px;
}
&__scale {
display: inline-block;
width: 38px;
height: 16px;
// margin-left: 15px;
box-sizing: content-box;
border: 1px solid rgb(77 77 77 / 100%);
background: rgb(255 255 255 / 100%);
border-radius: 2px;
color: #4d4d4d;
cursor: default;
font-size: 12px;
line-height: 16px;
text-align: center;
}
&__line {
width: 1px;
height: 14px;
background: #e6e6e6;
}
}
&__empty {
position: absolute;
z-index: 9;
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f9f9f9;
transform: translateY(-100%);
&>img {
margin-bottom: 12px;
width: 140px;
height: 80px;
}
&--text {
color: #999;
font-size: 14px;
line-height: 22px;
}
}
&__scoll-module {
position: absolute;
padding: 12px;
bottom: 48px;
display: flex;
width: 100%;
align-items: flex-end;
justify-content: space-between;
pointer-events: none;
box-sizing: border-box;
&__btn {
width: 56px;
height: 56px;
opacity: 0.4;
pointer-events: all;
&>span {
display: flex;
align-items: center;
justify-content: center;
}
}
&__btn:hover {
opacity: 0.6;
background-color: #09f !important;
color: #fff !important;
}
}
}

View File

@ -1,53 +0,0 @@
.zhst-image__video-view__player-mask {
position: absolute;
width: 100%;
height: 100%;
z-index: 99;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgb(4 4 4 / 70%);
&--bg {
z-index: 999;
background-color: rgb(4 4 4 / 100%);
}
i {
cursor: pointer;
}
&-title {
margin-top: 12px;
color: #fff;
text-align: center;
& a {
color: #09f;
cursor: pointer;
text-decoration: underline;
}
}
}
.zhst-image__video-view__icon-wraper {
display: flex;
width: 80px;
height: 80px;
align-items: center;
justify-content: center;
background-color: rgb(255 255 255 / 10%);
border-radius: 50%;
cursor: pointer;
line-height: 80px;
text-align: center;
// &:hover {
// background: #0099ff;
// }
}

View File

@ -1,49 +0,0 @@
.zhst-image__range {
position: relative;
&--no-slider {
.next-range-slider {
display: none;
}
}
& .next-range .next-range-track {
height: 8px;
margin-top: -4px;
border-radius: 8px;
}
& .next-range .next-range-selected {
height: 8px;
margin-top: -4px;
border-radius: 8px;
}
& .next-range .next-range-slider-inner {
width: 14px;
height: 14px;
border-color: #fff;
margin-top: -7px;
margin-left: -7px;
background-color: #0098ff;
}
& .next-range .next-range-slider {
width: 14px;
height: 14px;
margin-top: -7px;
margin-left: -7px;
}
& .next-range.simulation-click>.next-range-slider-inner {
border: 2px solid #fff !important;
}
& .next-range .next-range-frag.next-range-active .next-range-slider .next-range-slider-inner {
border: 2px solid #fff !important;
}
& .next-range .next-range-slider.next-range-slider-moving .next-range-slider-inner {
border: 2px solid #fff !important;
}
}

View File

@ -1,73 +0,0 @@
.zhst-image__video-view {
position: relative;
overflow: hidden;
background-color: #333;
// &-flv {
// width: 85%;
// }
&-screenshot {
position: absolute;
z-index: 10;
}
&-crop-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&-align {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
&-opt {
position: absolute;
z-index: 99;
bottom: 0;
display: flex;
width: 100%;
height: 32px;
box-sizing: border-box;
align-items: center;
padding: 0 12px 0 24px;
background-color: rgb(0 0 0 / 80%);
line-height: 32px;
&>div:first-child {
display: flex;
align-items: center;
margin-right: 12px;
}
&>div:last-child {
display: flex;
align-items: center;
margin-left: 12px;
}
&-range {
display: flex;
height: 32px;
flex: 1;
align-items: center;
line-height: 32px;
text-align: center;
&>div:first-child {
flex: 1;
}
&>div:last-child {
width: 100px;
margin-left: 8px;
color: #fff;
}
}
}
}

View File

@ -9,6 +9,7 @@ export { default as VideoPlayer } from "./VideoPlayer";
// antd
export { default as Tabs } from "./tabs";
export { default as Tree } from "./tree";
export { default as Spin } from "./spin";
export { default as message } from "./message";
export { default as Button } from "./button";
export { default as Image } from "./image";

View File

@ -17,6 +17,9 @@ export interface CompareImageProps {
* @default "默认值"
*/
label?: string;
labelColor?: string;
width?: string;
height?: string
showTools?: boolean;
prefixCls?: string;
openRoll?: boolean; //开启翻页
@ -39,7 +42,10 @@ const CompareImage = forwardRef<CompareImageRefProps, CompareImageProps>((props,
const {
prefixCls: customizePrefixCls,
label = '标题',
width = '400',
height,
openRoll = true,
labelColor,
url = '',
score = 0,
preDisable,
@ -69,11 +75,10 @@ const CompareImage = forwardRef<CompareImageRefProps, CompareImageProps>((props,
}
);
if (generateImg(url)) {
imgInsRef.current = new Viewer(imgContainerRef.current, {
image: generateImg(url),
});
}
imgInsRef.current = new Viewer(imgContainerRef.current, {
image: url,
height: parseInt(height!),
});
return () => {
handleTransformChange?.remove();
@ -96,8 +101,8 @@ const CompareImage = forwardRef<CompareImageRefProps, CompareImageProps>((props,
}));
return (
<div className={classNames(`${componentName}__container`)}>
{label && <div className={classNames(`${componentName}__label`)}>{label}</div>}
<div className={classNames(`${componentName}__container`)} style={{ width: `${parseInt(width)}px` }}>
{label && <div className={classNames(`${componentName}__label`)} style={{ backgroundColor: labelColor }}>{label}</div>}
{!url ? (
<div className={classNames(`${componentName}__empty`)}>
<img
@ -121,7 +126,6 @@ const CompareImage = forwardRef<CompareImageRefProps, CompareImageProps>((props,
style={{ width: '56px' }}
icon={<IconFont icon="icon-qiehuanzuo" size={32} color='#fff' />}
>
</Button>
<Button
className={classNames(`${componentName}__scoll-module__btn`)}
@ -159,7 +163,6 @@ const CompareImage = forwardRef<CompareImageRefProps, CompareImageProps>((props,
<span className={classNames(`${componentName}__tool__scale`)}>
{Math.round(scale * 100) + '%'}
</span>
<div className={classNames(`${componentName}__tool__line`)}></div>
{/* </Button> */}
<Button
type="text"

View File

@ -1,7 +1,7 @@
.zhst-image__CornerScore {
position: absolute;
right: 0;
bottom: 0;
bottom: 36px;
width: 56px;
height: 22px;
line-height: 22px;

View File

@ -13,7 +13,7 @@ export const CornerScore: React.FC<ScoreProps> = (props) => {
return useMemo(
() => (
<div className={classNames(`${componentName}`)}>
<span>{~~(scoreTxt * 100)}%</span>
<span>{~~(scoreTxt)}%</span>
</div>
),
[scoreTxt]

View File

@ -19,39 +19,36 @@
border: 1px solid #f0f0f0;
}
&__view {
min-width: 345px;
min-height: 450px;
}
&__label {
padding: 4px 8px;
position: absolute;
z-index: 99;
top: 0;
left: 0;
right: 0;
display: flex;
font-size: 16px;
height: 34px;
font-size: 14px;
align-items: center;
justify-content: center;
// width: 48px;
padding: 0 6px;
background: #09f;
background: #52ACFA;
color: #fff;
border-bottom-left-radius: 8px;
}
&__tool {
position: absolute;
left: 0;
bottom: 0;
display: flex;
width: 100%;
height: 40px;
align-items: center;
font-size: 14px;
justify-content: center;
background: #f9f9f9;
background: rgba(0, 0, 0, 0.5);
i,
span {
color: #333 !important;
color: #fff !important;
}
i {
@ -63,14 +60,12 @@
}
&__scale {
padding: 2px 14px;
display: inline-block;
width: 38px;
height: 16px;
// margin-left: 15px;
box-sizing: content-box;
border: 1px solid rgb(77 77 77 / 100%);
background: rgb(255 255 255 / 100%);
border: 1px solid #fff;
border-radius: 2px;
color: #4d4d4d;
cursor: default;

View File

@ -72,6 +72,7 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
const {
prefixCls: customizePrefixCls,
url,
height = 0,
odList,
selectedItem,
onMouseDown,
@ -115,7 +116,8 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
image: url,
scaleAble,
selectAble,
height: 600,
// @ts-ignore
height: parseInt(height),
fitScaleAsMinScale: true,
dragAble: false,
});

View File

@ -80,5 +80,3 @@ demo:
| strokeWidth | 仪表盘进度条线的宽度,单位是进度条画布宽度的百分比 | number | 6 | - |
## 主题变量Design Token
<ComponentTokenTable component="Progress"></ComponentTokenTable>

View File

@ -27,7 +27,7 @@ const RelatedCarousel: FC<RelatedCarouselProps> = (_props: any) => {
onItemSelected,
label,
onClick,
width = '742px',
width = '100%',
height = '100px',
selectedKey: defaultSelectedKey
} = _props

View File

@ -25,6 +25,8 @@ module.exports = __toCommonJS(src_exports);
var import_antd = require("antd");
var import_func = require("@zhst/func");
var Slave = class {
config;
authTokenDefine;
// 设置参数
constructor() {
this.authTokenDefine = "ZHST_AUTH_TOKEN";

View File

@ -4,6 +4,7 @@
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"declarationMap": true,
"composite": true,
"jsx": "react",
"baseUrl": "./",
@ -12,9 +13,9 @@
"@zhst/*": ["packages/*/src/"]
},
"importHelpers": false,
"sourceMap": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"allowJs": true,
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"noUnusedParameters": true,