From 565d530d85a659798d50106ac89cb9dff54a6147 Mon Sep 17 00:00:00 2001 From: realcwang Date: Fri, 16 Aug 2024 10:37:37 +0800 Subject: [PATCH] feat(list): add list component re #394 --- site/mobile/components/style/index.less | 2 + site/mobile/mobile.config.js | 5 ++ site/web/site.config.js | 12 +-- src/index.ts | 1 + src/list/_example/base.jsx | 59 ++++++++++++++ src/list/_example/err-tip.jsx | 56 +++++++++++++ src/list/_example/index.jsx | 53 ++++++++++++ src/list/_example/pull-refresh.jsx | 70 ++++++++++++++++ src/list/_example/style/index.less | 30 +++++++ src/list/index.ts | 8 ++ src/list/list.en-US.md | 13 +++ src/list/list.md | 13 +++ src/list/list.tsx | 104 ++++++++++++++++++++++++ src/list/style/css.js | 1 + src/list/style/index.js | 1 + src/list/type.ts | 34 ++++++++ 16 files changed, 456 insertions(+), 6 deletions(-) create mode 100644 src/list/_example/base.jsx create mode 100644 src/list/_example/err-tip.jsx create mode 100644 src/list/_example/index.jsx create mode 100644 src/list/_example/pull-refresh.jsx create mode 100644 src/list/_example/style/index.less create mode 100644 src/list/index.ts create mode 100644 src/list/list.en-US.md create mode 100644 src/list/list.md create mode 100644 src/list/list.tsx create mode 100644 src/list/style/css.js create mode 100644 src/list/style/index.js create mode 100644 src/list/type.ts diff --git a/site/mobile/components/style/index.less b/site/mobile/components/style/index.less index 10e578a0..fbe4680f 100644 --- a/site/mobile/components/style/index.less +++ b/site/mobile/components/style/index.less @@ -6,10 +6,12 @@ #app { min-height: 100vh; + height: 100vh; display: flex; flex-direction: column; } .tdesign-mobile-demo { flex: 1; + overflow-y: scroll; } \ No newline at end of file diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index 4bb613b7..1ff08d71 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -30,6 +30,11 @@ export default { name: 'grid', component: () => import('tdesign-mobile-react/grid/_example/base.jsx'), }, + { + title: 'List 列表', + name: 'list', + component: () => import('tdesign-mobile-react/list/_example/index.jsx'), + }, { title: 'Image 图片', name: 'image', diff --git a/site/web/site.config.js b/site/web/site.config.js index 928e5823..bafc2379 100644 --- a/site/web/site.config.js +++ b/site/web/site.config.js @@ -239,12 +239,12 @@ export default { path: '/mobile-react/components/image', component: () => import('tdesign-mobile-react/image/image.md'), }, - // { - // title: 'List 列表', - // name: 'list', - // path: '/mobile-react/components/list', - // component: () => import('tdesign-mobile-react/list/list.md'), - // }, + { + title: 'List 列表', + name: 'list', + path: '/mobile-react/components/list', + component: () => import('tdesign-mobile-react/list/list.md'), + }, // { // title: 'ImageViewer 图片预览', // name: 'image-viewer', diff --git a/src/index.ts b/src/index.ts index 8688946d..6d5fb20d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ export * from './sticky'; export * from './swiper'; export * from './swipe-cell'; export * from './tag'; +export * from './list'; /** * 消息提醒(7个) diff --git a/src/list/_example/base.jsx b/src/list/_example/base.jsx new file mode 100644 index 00000000..ef69d28b --- /dev/null +++ b/src/list/_example/base.jsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState, useRef} from 'react'; +import './style/index.less'; +import { Cell, List } from 'tdesign-mobile-react'; + +export default function ListDemo() { + const [isLoading, setIsLoading] = useState(false); + const pageSize = 20; + const stateRef = useRef([]); + const pageRef = useRef(1); + const dataSource = []; + const total = 100; + for (let i = 0; i < total; i++) { + dataSource.push({ + id: i, + content: '列表内容列表内容列表内容', + icon: 'https://tdesign.gtimg.com/list-icon.png', + title: '列表主内容', + }); + } + + // 模拟请求 + const fetchData = async (pageInfo) => { + if (isLoading) return; + setIsLoading(true); + try { + setTimeout(() => { + const { pageNum, pageSize } = pageInfo; + const newDataSource = dataSource.slice((pageNum - 1) * pageSize, pageNum * pageSize); + const newListData = stateRef.current.concat(newDataSource); + pageRef.current = pageNum + stateRef.current = newListData + setIsLoading(false); + }, 0); + } catch (err) { + stateRef.current = [] + } + }; + + const onScroll = (scrollBottom) => { + if (!scrollBottom && stateRef.current.length < total) { + fetchData({ pageNum: pageRef.current + 1, pageSize }); + } + } + + useEffect(() => { + fetchData({ pageNum: pageRef.current, pageSize }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + { + stateRef.current.map((item) => + {item.id} + ) + } + + ); +} diff --git a/src/list/_example/err-tip.jsx b/src/list/_example/err-tip.jsx new file mode 100644 index 00000000..f9b65453 --- /dev/null +++ b/src/list/_example/err-tip.jsx @@ -0,0 +1,56 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Cell, List, Loading } from 'tdesign-mobile-react'; + +export default function ListDemo() { + const listError = useRef([]) + const [loading, setLoading] = useState('') + const [showError, setShowError] = useState(false) + + const onLoadError = () => { + setLoading('loading') + + setTimeout(() => { + const newVal = [...listError.current] + for (let i = listError.current.length; i < 8; i++) { + newVal.push(`${i}`); + } + listError.current = newVal; + + setShowError(true) + setLoading('') + }, 1000); + }; + + const onLoadMore = () => { + setShowError(false) + if (listError.current.length >= 60 || loading) { + return; + } + setLoading('loading') + + setTimeout(() => { + for (let i = 0; i < 15; i++) { + listError.current.push(`${listError.current.length + 1}`) + } + setLoading('') + }, 1000); + }; + + useEffect(()=>{ + onLoadError() + }, []); + + return ( + +
请求失败,点击重新加载
+ + }> + { + listError.current.map((item) => + {item} + ) + } +
+ ); +} diff --git a/src/list/_example/index.jsx b/src/list/_example/index.jsx new file mode 100644 index 00000000..e2e11111 --- /dev/null +++ b/src/list/_example/index.jsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { Button } from 'tdesign-mobile-react'; +import TDemoBlock from '../../../site/mobile/components/DemoBlock'; +// import TDemoHeader from '../../../site/mobile/components/DemoHeader'; +import './style/index.less' + +import BaseList from './base.jsx'; +import ErrTipDemo from './err-tip.jsx'; +import PullRefreshDemo from './pull-refresh.jsx'; + + +export default function ListDemo() { + + const [currentTab, setCurrentTab] = useState('info') + + const onChangeTab = (val) => { + setCurrentTab(val); + history.pushState({}, '', '?tab=demo'); + }; + + return ( +
+
+ { currentTab === 'info' &&
+

List 列表

+

+ 瀑布流滚动加载,用于展示同一类型信息的长列表。当列表即将滚动到底部时,会触发事件并加载更多列表项。 +

+ + + + + +
} + { + currentTab === 'base' && + } + { + currentTab === 'error-tip' && + } + { + currentTab === 'pull-refresh' &&
+ +
+ } +
+
+ ); +} diff --git a/src/list/_example/pull-refresh.jsx b/src/list/_example/pull-refresh.jsx new file mode 100644 index 00000000..94846129 --- /dev/null +++ b/src/list/_example/pull-refresh.jsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Cell, List, PullDownRefresh } from 'tdesign-mobile-react'; + +export default function ListDemo() { + const [loading, setLoading] = useState('') + const [refreshing, setRefreshing] = useState(false) + + const listData = useRef([]) + + const MAX_DATA_LEN = 60; + + const loadData = (isRefresh) => { + const ONCE_LOAD_NUM = 20; + return new Promise((resolve) => { + setTimeout(() => { + const temp = []; + for (let i = 0; i < ONCE_LOAD_NUM; i++) { + if (isRefresh) { + temp.push(`${i + 1}`); + } else { + temp.push(`${listData.current.length + 1 + i}`); + } + } + + if (isRefresh) { + listData.current = temp + } else { + listData.current= [...listData.current, ...temp ] + } + setLoading(''); + setRefreshing(false); + }, 1000); + }); + }; + + const onLoadData = (isRefresh) => { + if ((listData.current.length >= MAX_DATA_LEN && !isRefresh) || loading.value) { + return; + } + setLoading('loading'); + loadData(isRefresh) + }; + + const onScroll = (scrollBottom) => { + if (scrollBottom < 50) { + onLoadData(); + } + }; + + const onRefresh = () => { + setRefreshing(true); + onLoadData(true); + }; + + useEffect(()=>{ + onLoadData(); + }, []); + + return ( + setRefreshing(val)} onRefresh={onRefresh}> + + { + listData.current.map((item) => + {item} + ) + } + + + ); +} diff --git a/src/list/_example/style/index.less b/src/list/_example/style/index.less new file mode 100644 index 00000000..e1a5a2b8 --- /dev/null +++ b/src/list/_example/style/index.less @@ -0,0 +1,30 @@ +.list-demo { + .t-list { + .cell { + width: 100%; + text-align: center; + } + .error { + text-align: center; + color: #969799; + font-size: 14px; + margin-top: 8px; + } + } + .custom-error { + font-size: 14px; + color: #969799; + text-align: center; + padding-top: 16px; + cursor: default; + + span { + color: #0052d9; + cursor: pointer; + } + } + .t-button { + margin: 0 16px 16px 16px; + width: calc(100% - 32px); + } +} \ No newline at end of file diff --git a/src/list/index.ts b/src/list/index.ts new file mode 100644 index 00000000..26d5e2a0 --- /dev/null +++ b/src/list/index.ts @@ -0,0 +1,8 @@ +import _List from './list'; + +import './style/index.js'; + +export * from './type'; + +export const List = _List; +export default List; diff --git a/src/list/list.en-US.md b/src/list/list.en-US.md new file mode 100644 index 00000000..ca82cafa --- /dev/null +++ b/src/list/list.en-US.md @@ -0,0 +1,13 @@ +:: BASE_DOC :: + +## API + +### List Props + +name | type | default | description | required +-- | -- | -- | -- | -- +asyncLoading | TNode / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +footer | TNode / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +header | TNode / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +onLoadMore | Function | | Typescript:`() => void`
| N +onScroll | Function | | Typescript:`(bottomDistance: number, scrollTop: number) => void`
| N \ No newline at end of file diff --git a/src/list/list.md b/src/list/list.md new file mode 100644 index 00000000..62ed0905 --- /dev/null +++ b/src/list/list.md @@ -0,0 +1,13 @@ +:: BASE_DOC :: + +## API + +### List Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +asyncLoading | String / TNode | - | 自定义加载中。值为空不显示加载中,值为 'loading' 显示加载中状态,值为 'load-more' 显示加载更多状态。值类型为函数,则表示自定义加载状态呈现内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +footer | String / TNode | - | 底部。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +header | String / TNode | - | 头部。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +onLoadMore | Function | | TS 类型:`() => void`
点击加载更多时触发 | N +onScroll | Function | | TS 类型:`(bottomDistance: number, scrollTop: number) => void`
列表滚动时触发,bottomDistance 表示底部距离;scrollTop 表示顶部滚动距离 | N diff --git a/src/list/list.tsx b/src/list/list.tsx new file mode 100644 index 00000000..dbc52de9 --- /dev/null +++ b/src/list/list.tsx @@ -0,0 +1,104 @@ + + +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { TdListProps } from './type'; +import useConfig from '../_util/useConfig'; + +import TLoading from '../loading'; + +export interface ListProps extends TdListProps { + required?: boolean; + readonly?: boolean; +} + +function isElement(node: Element) { + const ELEMENT_NODE_TYPE = 1; + return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE; +} + +const overflowScrollReg = /scroll|auto/i; + +function getScrollParent(el: Element, root = window ) { + let node = el; + + while (node && node !== root && isElement(node)) { + const { overflowY } = window.getComputedStyle(node); + if (overflowScrollReg.test(overflowY)) { + return node; + } + node = node.parentNode as Element; + } + + return root; +} + +const List: React.FC = (props: ListProps) => { + const { classPrefix } = useConfig(); + const { header, footer, children} = props; + const name = classPrefix; + + const LOADING_TEXT_MAP = { + loading: '加载中', // TODO: i18n + 'load-more': '加载更多', + }; + + const root = useRef(null); + + const useWindowHeight = ()=>{ + const [height, setHeight] = useState(window.innerHeight); + window.onresize = ()=>{ + const height = window.innerHeight + setHeight(height) + } + return height + } + const height = useWindowHeight(); + + const onLoadMore = () => { + if (props.asyncLoading === 'load-more') { + props.onLoadMore?.(); + } + }; + + const handleScroll = useCallback((e: WheelEvent | Event):void => { + const scrollHeight = + (e.target as HTMLElement).scrollHeight || + Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); + + const scrollTop = + (e.target as HTMLElement).scrollTop || document.documentElement.scrollTop || document.body.scrollTop; + + const offsetHeight = (e.target as HTMLElement).offsetHeight || height; + const bottomDistance = scrollHeight - (scrollTop + offsetHeight) + props.onScroll?.(bottomDistance, scrollTop); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const scorllParent = getScrollParent(root.current) + + scorllParent.addEventListener('scroll', handleScroll); + return () => { + removeEventListener('scroll', handleScroll) + } + }, [height, handleScroll]) + + return ( +
handleScroll(e)}> + {header} + {children} +
onLoadMore()}> + {typeof props.asyncLoading === 'string' && ['loading', 'load-more'].includes(props.asyncLoading) && ( + + )} +
+ {footer} +
+ ); +}; + +export default List; diff --git a/src/list/style/css.js b/src/list/style/css.js new file mode 100644 index 00000000..6a9a4b13 --- /dev/null +++ b/src/list/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/list/style/index.js b/src/list/style/index.js new file mode 100644 index 00000000..fdbefc78 --- /dev/null +++ b/src/list/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/mobile/components/list/_index.less'; diff --git a/src/list/type.ts b/src/list/type.ts new file mode 100644 index 00000000..52be6029 --- /dev/null +++ b/src/list/type.ts @@ -0,0 +1,34 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TNode } from '../common'; + +export interface TdListProps { + /** + * 自定义加载中。值为空不显示加载中,值为 'loading' 显示加载中状态,值为 'load-more' 显示加载更多状态。值类型为函数,则表示自定义加载状态呈现内容 + */ + asyncLoading?: string | TNode; + /** + * 底部 + */ + footer?: string | TNode; + /** + * 子元素 + */ + children?: string | TNode; + /** + * 头部 + */ + header?: string | TNode; + /** + * 点击加载更多时触发 + */ + onLoadMore?: () => void; + /** + * 列表滚动时触发,bottomDistance 表示底部距离;scrollTop 表示顶部滚动距离 + */ + onScroll?: (bottomDistance: number, scrollTop: number) => void; +}