feat(biz无限滚动组件): 添加屏幕自适应撑开

This commit is contained in:
NICE CODE BY DEV 2024-05-21 10:37:47 +08:00
parent 1f6c7ccb18
commit 570f183382
13 changed files with 247 additions and 146 deletions

View File

@ -2,14 +2,14 @@
* Created by jiangzhixiong on 2024/04/28
*/
import React, { forwardRef, useContext, useImperativeHandle } from 'react'
import React, { forwardRef, MouseEventHandler, ReactNode, useContext, useImperativeHandle } from 'react'
import { ConfigProvider, EMPTY_BASE64 } from '@zhst/meta'
import { Flex, Image } from 'antd';
import './index.less'
const { ConfigContext } = ConfigProvider
export interface Idata {
export interface IData {
id?: string | number;
url?: string;
sort?: number;
@ -17,24 +17,25 @@ export interface Idata {
subtitle?: string;
}
export interface SearchCardProps extends Idata {
export interface CommonCardProps extends IData {
prefixCls?: string;
data?: Idata
data?: IData
width?: string;
height?: string;
onCreateTxt?: string;
onCreate?: (data: any) => void;
onCreate?: () => void;
onAddTxt?: string;
onAdd?: (data: any) => void;
onRemoveTxt?: string;
onRemove?: (data: any) => void;
customOptionRender?: React.ReactNode
actions?: ReactNode[]
onItemClick?: (data: any, e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}
export interface SearchCardRefProps {
export interface CommonCardRefProps {
}
const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref) => {
const CommonCard = forwardRef<CommonCardRefProps, CommonCardProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
url,
@ -43,26 +44,29 @@ const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref)
subtitle,
sort,
data,
onCreate,
onCreateTxt = '创建检索',
onAddTxt = '添加目标',
onRemoveTxt = '移除轨迹',
onAdd,
onRemove,
customOptionRender,
actions = [],
width = '184px',
height = '100%'
height = '100%',
onItemClick
} = props
const { getPrefixCls } = useContext(ConfigContext)
const componentName = getPrefixCls('biz-search-card', customizePrefixCls);
const stopBumble = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, fn?: ((data: Idata) => void), data?: Idata) => {
e.stopPropagation()
fn?.(data!)
const optionListRender = (_actions: ReactNode[]) => {
return _actions.map((action, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={`${componentName}-main-opt-action-${i}`}>
{action}
{i !== _actions.length - 1 && <em className={`${componentName}-main-opt-action-split`} />}
</li>
))
}
const handleItemClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, _data: IData | undefined) => {
onItemClick?.(data, e)
}
useImperativeHandle(ref, () => ({
}))
return (
@ -72,9 +76,10 @@ const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref)
width,
height
}}
onClick={e => handleItemClick(e, data)}
>
<div className={`${componentName}-main`}>
<i className={`${componentName}-main-num`}>{id || sort}</i>
<i className={`${componentName}-main-num`}>{sort || id}</i>
<Image
className={`${componentName}-main-img`}
src={url || data?.url}
@ -82,17 +87,9 @@ const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref)
preview={false}
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>
<ul className={`${componentName}-main-opt`}>
{optionListRender(actions)}
</ul>
</div>
<div className={`${componentName}-footer`}>
<p className={`${componentName}-footer-tit`}>{title || data?.title}</p>
@ -102,4 +99,4 @@ const SearchCard = forwardRef<SearchCardRefProps, SearchCardProps>((props, ref)
)
})
export default SearchCard
export default CommonCard

View File

@ -1,18 +1,23 @@
.zhst-biz-search-card {
display: inline-block;
border: 2px solid transparent;
transition: .1s ease-in all;
cursor: pointer;
overflow: hidden;
&:hover {
transition: .5s ease all;
border: 2px solid #09f;
.zhst-biz-search-card-main-opt {
display: flex;
align-items: center;
}
}
&-main {
position: relative;
overflow: hidden;
&-num {
position: absolute;
@ -32,11 +37,17 @@
&-img {
width: 100%;
height: 240px;
overflow: hidden;
&:hover {
transition: .5s ease-in all;
transform: scale(1.05);
}
}
&-opt {
display: none;
margin: 0;
position: absolute;
padding: 6px 3px;
left: 0;
@ -48,11 +59,28 @@
box-sizing: border-box;
transition: .2s ease-in all;
li {
position: relative;
list-style: none;
flex: 1;
text-align: center;
}
&-action-split {
position: absolute;
top: 50%;
right: 0;
width: 1px;
height: 80%;
transform: translateY(-50%);
background-color: #fff;
}
a {
color: #fff;
&:hover {
opacity: 0.9;
opacity: 0.88;
}
}
}

View File

@ -0,0 +1,5 @@
import CommonCard from './CommonCard'
export type { CommonCardProps, CommonCardRefProps } from './CommonCard'
export default CommonCard

View File

@ -0,0 +1,42 @@
import React, { useEffect, useState } from 'react'
import { CommonCard } from '@zhst/biz'
import { uniqueId } from '@zhst/func'
export default () => {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo')
.then((res) => res.json())
.then((body) => {
let res = body.results.map((o, index) => {
return {
id: uniqueId(),
sort: index + 1,
title: o.name.first,
subtitle: o.name.last,
url: o.picture.large
}
})
setData(res);
})
}, [])
return (
<div>
{data?.map(item => (
<CommonCard
key={item.id}
sort={item.sort}
data={item}
width="184px"
actions={[
<a></a>,
<a></a>,
<a></a>
]}
/>
))}
</div>
)
}

View File

@ -0,0 +1,31 @@
---
category: Components
title: CustomCard 定制化卡片
toc: content
group:
title: 数据展示
---
定制化卡片
## 代码演示
<code src="./demo/commonCard.tsx">基本卡片</code>
## API
### CommonCardProps
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| data | 数据源 | IData[] | [] | - |
| prefixCls | 数据源 | string | [] | - |
| width | 宽度 | string | [] | - |
| height | 高度 | string | [] | - |
| onCreateTxt | 创建方法文字 | string | [] | - |
| onCreate | 创建方法 | () => void | [] | - |
| onAddTxt | 数据源 | string | [] | - |
| onAdd | 数据源 | () => void | [] | - |
| onRemoveTxt | 数据源 | string | [] | - |
| onRemove | 数据源 | () => void | [] | - |
| customOptionRender | 数据源 | React.ReactNode | - | - |

View File

@ -0,0 +1,6 @@
/**
* Created by jiangzhixiong on 2024/04/28
*/
export { default as CommonCard } from './components/commonCard'
export type { CommonCardProps, CommonCardRefProps } from './components/commonCard'

View File

@ -9,6 +9,11 @@ export type { TreeTransferProps } from './treeTransfer'
export { default as TreeTransferModal } from './treeTransferModal'
export type { TreeTransferModalProps } from './treeTransferModal'
export { default as WarningRecordCard } from './WarningRecordCard'
export { CommonCard } from './CustomCard'
export type {
CommonCardProps,
CommonCardRefProps
} from './CustomCard'
export type { IRecord, WarningRecordCardProps } from './WarningRecordCard'
export { default as OdModal } from './odModal'
export type { ODModalProps } from './odModal'

View File

@ -2,24 +2,30 @@
* Created by jiangzhixiong
*/
import React, { forwardRef, ReactNode, useContext, useImperativeHandle, useRef } from 'react'
import React, { forwardRef, ReactNode, useContext, useEffect, 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 { Spin, SpinProps } from 'antd';
import { useSize } from '@zhst/hooks';
import './index.less'
import { Idata } from './components/SearchCard';
const { ConfigContext } = ConfigProvider
export interface InfiniteListProps {
export interface IData {
key: string;
title: string;
index?: number;
[k: string]: any;
}
export interface InfiniteListProps extends React.HtmlHTMLAttributes<HTMLDivElement> {
type?: 'custom' | 'auto'
prefixCls?: string;
height?: number;
itemRender?: (data?: any) => React.ReactNode
itemRender?: (data?: IData, index?: number) => React.ReactNode
loading?: boolean; //
data: Idata[];
data: IData[];
targetId?: string; // 滚动列表 ID
loadMore?: (data?: any) => any;
params?: {
@ -28,101 +34,79 @@ export interface InfiniteListProps {
hasMore: boolean;
endMessage?: ReactNode
loadingMessage?: ReactNode
onItemClick?: (data: any) => void;
searchCardProps?: SearchCardProps
loadingProps?: SpinProps
}
export interface InfiniteListRefProps {
scrollViewSize?: { width: number; height: number }
listSize?: { width: number; height: number }
}
const InfiniteList = forwardRef<InfiniteListRefProps, InfiniteListProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
height,
height = 600,
loading,
type = 'auto',
loadingMessage = <p style={{ textAlign: 'center' }}>...</p>,
targetId = 'scrollableDiv',
itemRender,
itemRender = (data) => <div>{data?.title}</div>,
hasMore,
onItemClick,
loadMore,
data = [],
endMessage = <Divider plain>...🤐</Divider>,
searchCardProps
endMessage = <div style={{ textAlign: 'center' }} >...🤐</div>,
style,
loadingProps,
className
} = props
const { getPrefixCls } = useContext(ConfigContext);
const componentName = getPrefixCls('biz-infinite-list', customizePrefixCls);
const listRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollViewSize = useSize(listRef.current) || { width: 0, height: 0 }; // 无限滚动视窗大小
// @ts-ignore
const listSize = useSize(scrollRef.current?._infScroll) || { width: 0, height: 0 } // 无限滚动列表大小
useEffect(() => {
// 当数据不够一屏时继续加载
if (listSize.height < scrollViewSize.height) {
loadMore?.()
}
}, [listSize.height])
useImperativeHandle(ref, () => ({
scrollViewSize,
listSize
}))
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}
<Spin spinning={loading} {...loadingProps}>
<div
id={targetId}
className={classNames(componentName, className)}
ref={listRef}
style={{
height,
overflow: 'auto',
...style
}}
>
<Flex wrap='wrap' gap="small" className={classNames(componentName + 'items')}>
<InfiniteScroll
// @ts-ignore
ref={scrollRef}
dataLength={data.length}
next={type === 'auto' ? loadMore! : () => {}}
hasMore={hasMore}
loader={loadingMessage}
endMessage={endMessage}
scrollableTarget={targetId}
>
{data?.map((item, idx) => (
itemRender?.(item) || (
<div
key={idx}
className={classNames(componentName + 'items-item')}
onClick={() => {
onItemClick?.(item)
}}
>
<SearchCard
id={idx + 1}
data={item}
width="184px"
{...searchCardProps}
/>
</div>
)
itemRender?.({ ...item, index: idx})
))}
</Flex>
</InfiniteScroll>
{/* <div style={{ marginTop: 8 }}>
{!noMore && (
<Button onClick={loadMore} disabled={loadingMore}>
{loadingMore ? '加载中...' : '点击加载更多'}
</Button>
)}
{noMore && <span></span>}
</div> */}
</div>
</InfiniteScroll>
</div>
</Spin>
)
})

View File

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

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'
import { InfiniteList } from '@zhst/biz'
import { InfiniteList, CommonCard } from '@zhst/biz'
import { uniqueId } from '@zhst/func'
export default () => {
const [data, setData] = useState([])
@ -13,8 +14,10 @@ export default () => {
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo')
.then((res) => res.json())
.then((body) => {
let res = body.results.map(o => {
let res = body.results.map((o, index) => {
return {
id: uniqueId(),
sort: index + 1,
title: o.name.first,
subtitle: o.name.last,
url: o.picture.large
@ -36,14 +39,23 @@ export default () => {
<InfiniteList
loading={loading}
loadMore={loadMoreData}
height={300}
hasMore={data.length < 100}
height={1200}
hasMore={data.length < 60}
data={data}
onItemClick={_data => console.log('item点击', _data)}
searchCardProps={{
onAdd: (_data) => console.log('新增', _data),
onCreate: (_data) => console.log('创建', _data),
onRemove: (_data) => console.log('删除', _data),
itemRender={(item) => {
return (
<CommonCard
key={item.id}
sort={item.sort}
data={item}
width="184px"
actions={[
<a></a>,
<a></a>,
<a></a>
]}
/>
)
}}
/>
)

View File

@ -3,11 +3,11 @@ import { InfiniteList } from '@zhst/biz'
import { Button, Input, Space } from 'antd'
export default () => {
const [data, setData] = useState([])
const [data, setData] = useState<any>([])
const [loading, setLoading] = useState(false)
const [params, setParams] = useState({})
const loadMoreData = (params?: { name: string; age?: number; sex: string; tel: number }) => {
const loadMoreData = (params?: any) => {
if (loading) {
return;
}
@ -15,7 +15,7 @@ export default () => {
fetch('https://randomuser.me/api/?results=10&inc=id,key,name,gender,email,nat,picture&noinfo')
.then((res) => res.json())
.then((body) => {
let res = body.results.map(o => {
let res = body.results.map((o: { name: { first: any; last: any }; picture: { large: any } }) => {
return {
title: o.name.first,
subtitle: o.name.last,
@ -35,7 +35,7 @@ export default () => {
}, []);
return (
<Space direction='vertical'>
<Space direction='vertical' size={10} style={{ padding: '12px', border: '1px solid #ccc' }}>
<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' }} />
@ -49,9 +49,7 @@ export default () => {
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

@ -18,21 +18,20 @@ group:
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| data | 数据源 | Idata[] | [] | - |
| data | 数据源 | IData[] | [] | - |
| height | 无限滚动列表可视区高度 | number | 600 | - |
| loading | 数据源 | Array[] | [] | - |
| data | 数据源 | Array[] | [] | - |
| data | 数据源 | Array[] | [] | - |
| data | 数据源 | Array[] | [] | - |
| data | 数据源 | Array[] | [] | - |
| data | 数据源 | Array[] | [] | - |
| dataLength | 数据数量 | number | [] | - |
| next | 下一页方法 | function | () => {} | - |
| hasMore | 是否还有更多 | boolean | false | - |
| loadingProps | 参考 antd-spin | spinProps | [] | - |
| itemRender | 自定义渲染项 | (IData) => ReactNode | - | - |
## Idata
## 设计思路
```js
interface Idata {
id?: string | number;
url?: string; // 链接
title?: string; // 标题
subtitle?: string; // 副标题
}
```
无限滚动,同时支持:
1. 自动、主动加载更多
2. 一屏没加载完,继续加载,直到填满屏幕:
- 需要第二次加载的内容是否为空,为空则停止加载
- 通过整体的page-height 和 浏览器可视区域

View File