feat(zhst/biz): 新增无限滚动列表组件

This commit is contained in:
NICE CODE BY DEV 2024-04-28 19:44:36 +08:00
parent 57af643093
commit aea6e2ec4c
20 changed files with 553 additions and 6 deletions

4
.vscode/launch.json vendored
View File

@ -2,9 +2,9 @@
"configurations": [ "configurations": [
{ {
"type": "chrome", "type": "chrome",
"name": "http://localhost:8000/metas/big-image-preview", "name": "lambo",
"request": "launch", "request": "launch",
"url": "http://localhost:8000/metas/big-image-preview" "url": "http://localhost:8000/metas"
} }
] ]
} }

View File

@ -1,5 +1,18 @@
# @zhst/biz # @zhst/biz
## 0.17.0
### Minor Changes
- 视频添加 OD 框,查看大图首次点击修复
### Patch Changes
- Updated dependencies
- @zhst/meta@0.15.0
- @zhst/func@0.10.2
- @zhst/hooks@0.9.2
## 0.16.1 ## 0.16.1
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/biz", "name": "@zhst/biz",
"version": "0.16.1", "version": "0.17.0",
"description": "业务库", "description": "业务库",
"keywords": [ "keywords": [
"business", "business",
@ -47,6 +47,7 @@
"antd": "^5.12.5", "antd": "^5.12.5",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"rc-util": "^5.38.1" "rc-util": "^5.38.1",
"react-infinite-scroll-component": "^6.1.0"
} }
} }

View File

@ -17,4 +17,5 @@ export { default as ViewLargerImageModal, useViewLargerImageModal } from './View
export type { VideoPlayerCardProps } from './VideoPlayerCard' export type { VideoPlayerCardProps } from './VideoPlayerCard'
export { default as VideoPlayerCard } from './VideoPlayerCard' export { default as VideoPlayerCard } from './VideoPlayerCard'
export { default as RealTimeMonitor } from './RealTimeMonitor' export { default as RealTimeMonitor } from './RealTimeMonitor'
export { default as InfiniteList } from './infiniteList'
// export type { InfiniteListProps, InfiniteListRefProps } from './infiniteList'

View File

@ -0,0 +1,126 @@
/**
* Created by jiangzhixiong
*/
import React, { forwardRef, ReactNode, useContext, useImperativeHandle, useRef } from 'react'
import { ConfigProvider } from '@zhst/meta';
import { Divider, Flex } from 'antd';
import classNames from 'classnames';
import InfiniteScroll from 'react-infinite-scroll-component';
import { SearchCard, SearchCardProps } from './components';
import './index.less'
const { ConfigContext } = ConfigProvider
export interface InfiniteListProps {
type?: 'custom' | 'auto'
prefixCls?: string;
height?: number;
itemRender?: (data?: any) => React.ReactNode
loading?: boolean; //
data: any[];
targetId?: string; // 滚动列表 ID
loadMore?: (data?: any) => any;
params?: {
[key: string]: any;
}
hasMore: boolean;
endMessage?: ReactNode
loadingMessage?: ReactNode
onItemClick?: (data: any) => void;
searchCardProps?: SearchCardProps
}
export interface InfiniteListRefProps {
}
const InfiniteList = forwardRef<InfiniteListRefProps, InfiniteListProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
height,
type = 'auto',
loadingMessage = <p style={{ textAlign: 'center' }}>...</p>,
targetId = 'scrollableDiv',
itemRender,
hasMore,
onItemClick,
loadMore,
data = [],
endMessage = <Divider plain>...🤐</Divider>,
searchCardProps
} = props
const { getPrefixCls } = useContext(ConfigContext);
const componentName = getPrefixCls('biz-infinite-list', customizePrefixCls);
const listRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
}))
return (
<div
id={targetId}
className={classNames(componentName)}
ref={listRef}
style={{
height,
overflow: 'auto',
padding: 12
}}
>
{/* {loading ? (
<p>...</p>
) : (
<Flex wrap='wrap' gap="small" className={classNames(componentName + 'items')}>
{data?.list?.map((item) => (
itemRender?.(item) || (
<div className={classNames(componentName + 'items-item')}>
<SearchCard data={item} />
</div>
)
))}
</Flex>
)} */}
<InfiniteScroll
dataLength={data.length}
next={type === 'auto' ? loadMore! : () => {}}
hasMore={hasMore}
loader={loadingMessage}
endMessage={endMessage}
scrollableTarget={targetId}
>
<Flex wrap='wrap' gap="small" className={classNames(componentName + 'items')}>
{data?.map((item) => (
itemRender?.(item) || (
<div
key={item.key || item.id}
className={classNames(componentName + 'items-item')}
onClick={(e) => {
onItemClick?.(item)
}}
>
<SearchCard
data={item}
{...searchCardProps}
/>
</div>
)
))}
</Flex>
</InfiniteScroll>
{/* <div style={{ marginTop: 8 }}>
{!noMore && (
<Button onClick={loadMore} disabled={loadingMore}>
{loadingMore ? '加载中...' : '点击加载更多'}
</Button>
)}
{noMore && <span></span>}
</div> */}
</div>
)
})
export default InfiniteList

View File

@ -0,0 +1,94 @@
/**
* Created by jiangzhixiong on 2024/04/28
*/
import React, { forwardRef, useContext, useImperativeHandle } from 'react'
import { ConfigProvider, EMPTY_BASE64 } from '@zhst/meta'
import { Flex, Image } from 'antd';
import './index.less'
const { ConfigContext } = ConfigProvider
export interface SearchCardProps {
prefixCls?: string;
id?: string;
key?: string;
url?: string;
data?: {
key?: string;
url?: string;
sort?: number;
title?: string;
subtitle?: string;
}
onCreateTxt?: string;
onCreate?: (data: any) => void;
onAddTxt?: string;
onAdd?: (data: any) => void;
onRemoveTxt?: string;
onRemove?: (data: any) => void;
customOptionRender?: React.ReactNode
}
export interface SearchCardRefProps {
}
const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
url,
id,
key,
data,
onCreate,
onCreateTxt = '创建检索',
onAddTxt = '添加目标',
onRemoveTxt = '移除轨迹',
onAdd,
onRemove,
customOptionRender
} = props
const { getPrefixCls } = useContext(ConfigContext)
const componentName = getPrefixCls('biz-search-card', customizePrefixCls);
const stopBumble = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, fn: ((data: any) => void) | undefined, data: { url?: string | undefined; key?: string | undefined; title?: string | undefined; subtitle?: string | undefined; } | undefined) => {
e.stopPropagation()
fn?.(data)
}
useImperativeHandle(ref, () => ({
}))
return (
<div className={componentName} key={id || key}>
<div className={`${componentName}-main`}>
<i className={`${componentName}-main-num`}>-</i>
<Image
src={url}
preview={false}
width={'100%'}
height={'240px'}
fallback={EMPTY_BASE64}
/>
<Flex align='center' justify='space-between' className={`${componentName}-main-opt`}>
{customOptionRender || (
<>
<a onClick={(e) => stopBumble(e, onCreate, data)}>{onCreateTxt}</a>
|
<a onClick={(e) => stopBumble(e, onAdd, data)}>{onAddTxt}</a>
|
<a onClick={(e) => stopBumble(e, onRemove, data)}>{onRemoveTxt}</a>
</>
)}
</Flex>
</div>
<div className={`${componentName}-footer`}>
<p className={`${componentName}-footer-tit`}>04/28 08:21:58</p>
<p className={`${componentName}-footer-subtitle`}>西2-1</p>
</div>
</div>
)
})
export default SearchCard

View File

@ -0,0 +1,75 @@
.zhst-biz-search-card {
border: 2px solid transparent;
transition: .1s ease-in all;
cursor: pointer;
&:hover {
border: 2px solid #09f;
.zhst-biz-search-card-main-opt {
display: flex;
}
}
&-main {
position: relative;
width: 184px;
height: 240px;
&-num {
position: absolute;
right: 2px;
top: 2px;
width: 20px;
height: 20px;
text-align: center;
color: rgb(153, 153, 153);
background-color: rgba(255, 255, 255, 75%);
z-index: 1;
border-radius: 3px;
}
&-opt {
display: none;
position: absolute;
padding: 6px 3px;
left: 0;
width: 100%;
bottom: 0;
font-size: 12px;
background-color: #09f;
color: #fff;
box-sizing: border-box;
transition: .2s ease-in all;
a {
color: #fff;
&:hover {
opacity: 0.9;
}
}
}
}
&-footer {
padding: 12px;
font-size: 12px;
text-align: center;
&-tit {
margin: 0;
line-height: 20px;
}
&-subtitle {
margin: 0;
line-height: 20px;
transition: .1s ease-in all;
&:hover {
color: #09f;
}
}
}
}

View File

@ -0,0 +1,6 @@
/**
* Created by jiangzhixiong on 2024/04/28
*/
export { default as SearchCard } from './SearchCard'
export type { SearchCardProps, SearchCardRefProps } from './SearchCard'

View File

@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react'
import { InfiniteList } from '@zhst/biz'
export default () => {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const loadMoreData = () => {
if (loading) {
return;
}
setLoading(true);
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo')
.then((res) => res.json())
.then((body) => {
setData([...data, ...body.results]);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
loadMoreData();
}, []);
return (
<InfiniteList
loading={loading}
loadMore={loadMoreData}
height={300}
hasMore={data.length < 100}
data={data}
onItemClick={_data => console.log('item点击', _data)}
searchCardProps={{
onAdd: (_data) => console.log('新增', _data),
onCreate: (_data) => console.log('创建', _data),
onRemove: (_data) => console.log('删除', _data),
}}
/>
)
}

View File

@ -0,0 +1,51 @@
import React, { useEffect, useState } from 'react'
import { InfiniteList } from '@zhst/biz'
import { Button, Input, Space } from 'antd'
export default () => {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [params, setParams] = useState({})
const loadMoreData = (params?: { name: string; age?: number; sex: string; tel: number }) => {
if (loading) {
return;
}
setLoading(true);
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo')
.then((res) => res.json())
.then((body) => {
setData([...data, ...body.results]);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
loadMoreData();
}, []);
return (
<Space direction='vertical'>
<Space>
<Input placeholder='名称' onChange={(e) => setParams(pre => ({ ...pre, name: e.target.value }))} style={{ width: '120px' }} />
<Input placeholder='年龄' onChange={(e) => setParams(pre => ({ ...pre, age: e.target.value }))} style={{ width: '120px' }} />
<Input placeholder='性别' onChange={(e) => setParams(pre => ({ ...pre, sex: e.target.value }))} style={{ width: '120px' }} />
<Input placeholder='手机号' onChange={(e) => setParams(pre => ({ ...pre, tel: e.target.value }))} style={{ width: '120px' }} />
<Button onClick={() => loadMoreData(params)}></Button>
</Space>
<InfiniteList
loading={loading}
loadMore={() => loadMoreData(params)}
height={300}
hasMore={data.length < 100}
data={data}
type="custom"
loadingMessage={<Button onClick={() => loadMoreData(params)}></Button>}
onItemClick={data => console.log('item点击', data)}
/>
</Space>
)
}

View File

@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react'
import { InfiniteList } from '@zhst/biz'
import { Button, Input, Space } from 'antd'
interface IData { name: string; age?: number; sex: string; tel: number }
export default () => {
const [data, setData] = useState<any>([])
const [loading, setLoading] = useState(false)
const [params, setParams] = useState<IData>({
name: '',
tel: 123,
age: 12,
sex: '男'
})
const loadMoreData = (params?: IData) => {
if (loading) {
return;
}
setLoading(true);
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo', {
})
.then((res) => res.json())
.then((body) => {
setData([...data, ...body.results]);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
loadMoreData();
}, []);
return (
<Space direction='vertical'>
<Space>
<Input placeholder='名称' onChange={(e) => setParams(pre => ({ ...pre, name: e.target.value }))} style={{ width: '120px' }} />
<Input placeholder='年龄' onChange={(e) => setParams(pre => ({ ...pre, age: Number(e.target.value) }))} style={{ width: '120px' }} />
<Input placeholder='性别' onChange={(e) => setParams(pre => ({ ...pre, sex: e.target.value }))} style={{ width: '120px' }} />
<Input placeholder='手机号' onChange={(e) => setParams(pre => ({ ...pre, tel: Number(e.target.value) }))} style={{ width: '120px' }} />
<Button onClick={() => loadMoreData(params)}></Button>
</Space>
<InfiniteList
loading={loading}
loadMore={() => loadMoreData(params)}
height={300}
hasMore={data.length < 100}
data={data}
type="custom"
loadingMessage={<Button onClick={() => loadMoreData(params)}></Button>}
itemRender={() => {
return (
<div style={{ width: '200px', height: '200px', border: '1px solid #000' }}>
</div>
)
}}
onItemClick={data => console.log('item点击', data)}
/>
</Space>
)
}

View File

@ -0,0 +1,17 @@
const resultData = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13'];
export function getLoadMoreList(nextId: string | undefined, limit: number): Promise<any> {
let start = 0;
const end = start + limit;
const list = resultData.slice(start, end);
const nId = resultData.length >= end ? resultData[end] : undefined;
return new Promise((resolve) => {
setTimeout(() => {
resolve({
list,
nextId: nId,
isNoMore: list.length > 20
});
}, 1000);
});
}

View File

@ -0,0 +1,5 @@
.zhst-biz-infinite-list {
&::-webkit-scrollbar {
display: none;
}
}

View File

@ -0,0 +1,21 @@
---
category: Components
title: infiniteList 无限滚动列表
toc: content
group:
title: 数据展示
---
无限滚动列表
## 代码演示
<code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/custom.tsx">手动触发</code>
<code src="./demo/customItem.tsx">自定义子元素</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| data | 数据源 | Array[] | [] | - |

View File

@ -0,0 +1,5 @@
import InfinityList from './InfiniteList'
// export { InfiniteListProps, InfiniteListRefProps } from './InfiniteList'
export default InfinityList

View File

@ -1,5 +1,19 @@
# @zhst/material # @zhst/material
## 0.11.0
### Minor Changes
- 视频添加 OD 框,查看大图首次点击修复
### Patch Changes
- Updated dependencies
- @zhst/meta@0.15.0
- @zhst/biz@0.17.0
- @zhst/func@0.10.2
- @zhst/hooks@0.9.2
## 0.10.4 ## 0.10.4
### Patch Changes ### Patch Changes

View File

@ -1,6 +1,6 @@
{ {
"name": "@zhst/material", "name": "@zhst/material",
"version": "0.10.4", "version": "0.11.0",
"description": "物料库", "description": "物料库",
"keywords": [ "keywords": [
"business", "business",

View File

@ -21,6 +21,9 @@ export interface Option {
* @default: true * @default: true
*/ */
scaleAble?: boolean; scaleAble?: boolean;
width?: number;
height?: number;
backgroundColor?: string;
/* /*
* *
@ -81,7 +84,11 @@ class Viewer {
} }
build() { build() {
const { width = 300, height = 150 } = this.options
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = width || canvas.width
canvas.height = height || canvas.height
addClass(canvas, CLASS_CANVAS); addClass(canvas, CLASS_CANVAS);
this.element.appendChild(canvas); this.element.appendChild(canvas);
this.canvas = canvas; this.canvas = canvas;

View File

@ -102,3 +102,4 @@ export type { AppProps } from './app';
export { default as notification } from './notification'; export { default as notification } from './notification';
export type { ArgsProps as NotificationArgsProps } from './notification'; export type { ArgsProps as NotificationArgsProps } from './notification';
export { default as ButtonList } from './ButtonList' export { default as ButtonList } from './ButtonList'
export * from './utils'

View File

@ -0,0 +1 @@
export * from './constants'