Reinit project

This commit is contained in:
Limo Saplf
2019-08-31 20:44:15 +08:00
commit b51ae824c7
115 changed files with 20955 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import {
VariableSizeList,
ListChildComponentProps,
ListOnScrollProps,
} from 'react-window';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { Icon } from 'antd';
import { formatMessage } from 'umi-plugin-locale';
import styles from './style.less';
import { connect } from 'dva';
const listPosition: { [key: string]: number } = {};
type ActionCallback<T = any> = () => T | Promise<T>;
export interface ListProps<T = any> {
className?: string;
distance?: number;
refreshDistance?: number;
loadDistance?: number;
itemSize?: number;
pullText?: string;
releaseText?: string;
refreshText?: string;
loadingText?: string;
loadFinishedText?: string;
refreshing?: boolean;
enableRefresh?: boolean;
loading?: boolean;
enableLoading?: boolean;
data?: T[];
onRefresh?: ActionCallback;
onLoadMore?: ActionCallback;
renderItem?: (item: T, index: number, array: T[]) => React.ReactNode;
itemKey?: (data: T) => string;
}
interface Point {
x: number;
y: number;
}
const List: React.FC<ListProps> = ({
className = '',
distance = 120,
refreshDistance = 80,
loadDistance = 200,
itemSize = 80,
pullText = 'common.pullToRefresh',
releaseText = 'common.releaseToRefresh',
refreshText = 'common.refreshing',
loadingText = 'common.loading',
loadFinishedText = 'common.loadFinished',
refreshing = false,
loading = false,
enableRefresh = true,
enableLoading = true,
data = [],
renderItem,
itemKey,
onRefresh,
onLoadMore,
...props
}) => {
if (refreshDistance > distance) {
throw Error('refreshDistance 应该小于 distance');
}
const scrollOffset = useRef(0);
const touchEnable = useRef(false);
const startPoint = useRef<Point>();
const scrollContainer = useRef<VariableSizeList>();
const [pullOffset, setPullOffset] = useState(0);
const validToRefresh = pullOffset >= refreshDistance;
const key = (props as any).location.pathname;
useEffect(() => {
if (refreshing && loading) {
throw Error('refreshing 与 loading 不能同时进行');
}
if (refreshing === loading) { // 同时为 false
setPullOffset(0);
} else {
setPullOffset(refreshing ? refreshDistance : 0);
}
}, [refreshing, refreshDistance, loading]);
useEffect(() => {
const { current } = scrollContainer;
if (current) {
current.resetAfterIndex(0, false);
}
return () => { listPosition[key] = scrollOffset.current; }
}, [data.length]);
const attachContainer = useCallback((node: VariableSizeList) => {
if (node) {
scrollContainer.current = node;
const position = listPosition[key];
if (typeof position === 'number') {
node.scrollTo(position);
}
}
}, []);
const onScroll = useCallback((e: ListOnScrollProps) => {
const { current } = scrollContainer;
scrollOffset.current = e.scrollOffset;
if (
enableLoading && !loading && !refreshing && current &&
data.length * itemSize - (+current.props.height) - e.scrollOffset < loadDistance
) {
onLoadMore && onLoadMore();
}
}, [loading, data.length, itemSize, enableLoading, key]);
const onTouchStart = useCallback((e: React.TouchEvent) => {
if (!enableRefresh || refreshing || loading) return;
const touch = e.touches[0];
if (!touch || scrollOffset.current) return;
startPoint.current = { x: touch.clientX, y: touch.clientY };
}, [enableRefresh, refreshing, loading]);
const onTouchEnd = useCallback(() => {
if (!enableRefresh || refreshing || loading) return;
touchEnable.current = false;
startPoint.current = undefined;
if (validToRefresh) {
onRefresh && onRefresh();
} else {
if (pullOffset !== 0) {
setPullOffset(0);
}
}
}, [pullOffset, validToRefresh, refreshing, enableRefresh, loading]);
const onTouchMove = useCallback((e: React.TouchEvent) => {
if (!enableRefresh || refreshing) return;
const touch = e.touches[0];
const start = startPoint.current;
if (!touch || !start) return;
if (!touchEnable.current) {
const deltaX = Math.abs(touch.clientX - start.x);
const deltaY = Math.abs(touch.clientY - start.y);
const yDirection = deltaY !== 0 && deltaX / deltaY < 0.5;
const enable = yDirection && scrollOffset.current === 0;
touchEnable.current = enable;
if (!enable) {
onTouchEnd();
}
return;
}
if (touchEnable.current && e.touches[0]) {
const { clientY } = e.touches[0];
const offset = clientY - start.y;
if (offset > 0) {
const variable = offset / distance;
if (variable <= Math.PI / 2) {
const result = distance * Math.sin(offset / distance);
setPullOffset(result);
}
}
}
}, [enableRefresh, distance]);
// 每一行的渲染
const Row: React.FC<ListChildComponentProps> = useCallback(({ style, index }) => {
let child;
if (index === data.length) {
child = (
<div className={styles.loadcontent}>
{loading && <Icon className="logo" type="loading" spin={true} />}
<span>
{formatMessage({ id: loading ? loadingText : loadFinishedText })}
</span>
</div>
);
} else {
child = renderItem && renderItem(data[index], index, data);
}
return (
<div className={styles.item} style={style}>
{child}
</div>
);
}, [data, loading]);
const calcItemSize = useCallback((index: number) => {
if (!refreshing && index === data.length) return 40;
return itemSize;
}, [data, refreshing]);
const keyGenerator = useCallback((index: number) => {
if (itemKey) {
const item = data[index];
return item ? itemKey(item) : 'loader';
}
return `${index}`;
}, [data, itemKey]);
// 列表内容
const ActualList: React.FC<Size> = useCallback(({ width, height }) => (
<VariableSizeList
ref={attachContainer}
style={{
transform: `translateY(${pullOffset}px)`,
transition: pullOffset === 0 || refreshing || loading ? 'all .2s' : undefined,
touchAction: refreshing ? 'none' : 'unset',
}}
className={styles.listcontent}
itemCount={data.length + (refreshing ? 0 : 1)}
itemSize={calcItemSize}
height={height}
width={width}
onScroll={onScroll}
itemKey={keyGenerator}
>
{Row}
</VariableSizeList>
), [pullOffset, data, itemSize, Row, refreshing]);
let pullContent = validToRefresh ? releaseText : pullText;
if (refreshing) {
pullContent = refreshText;
}
const RefreshContent = (
<div
style={{
transform: `translateY(${pullOffset / 3}px)`,
transition: pullOffset === 0 || refreshing ? 'all .2s' : undefined,
}}
className={styles.pullcontent}
>
{refreshing ? (
<Icon type="loading" className="loading" spin={true} />
) : (
<Icon
type="arrow-down"
className={`logo ${validToRefresh ? 'active' : ''}`}
/>
)}
<span className="desc">{formatMessage({ id: pullContent })}</span>
</div>
);
return (
<div
className={`${styles.container} ${className}`}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{RefreshContent}
<AutoSizer>
{ActualList}
</AutoSizer>
</div>
);
}
export default connect(
({ router } : any) => ({ ...router }),
)(List);

View File

@@ -0,0 +1,62 @@
@import '../../global.less';
.item {
height: 60px;
}
.container {
width: 100%;
height: auto;
position: relative;
overflow: hidden;
background-color: @bg-color;
}
.pullcontent {
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
color: @hint-color;
:global {
.logo, .desc {
transition: all .3s;
}
.logo, .loading {
margin-right: 6px;
}
.logo {
&.active {
transform: rotateZ(180deg);
}
}
}
}
.loadcontent {
background-color: @bg-color;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: @hint-color;
:global {
.logo {
margin-right: 6px;
}
}
}
.listcontent {
background-color: #fff;
position: relative;
z-index: 1;
}