Reinit project
This commit is contained in:
35
src/components/AccountItem/index.tsx
Normal file
35
src/components/AccountItem/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { Icon } from 'antd';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface AccountItemProps {
|
||||
title: string;
|
||||
message?: string;
|
||||
preview?: string;
|
||||
onClick?: () => any;
|
||||
className?: string;
|
||||
showGo?: boolean;
|
||||
}
|
||||
|
||||
const AccountItem: React.FC<AccountItemProps> = ({
|
||||
title,
|
||||
message = '',
|
||||
preview = '',
|
||||
onClick,
|
||||
className = '',
|
||||
showGo = true,
|
||||
}) => {
|
||||
const titleText = formatMessage({ id: title });
|
||||
return (
|
||||
<div className={`${styles.item} ${className}`} onClick={onClick}>
|
||||
<span className={styles.title}>{titleText}</span>
|
||||
<span className={styles.message}>{message}</span>
|
||||
<span className={styles.preview}>{preview}</span>
|
||||
{showGo && <Icon className={styles.go} type="right" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountItem;
|
||||
43
src/components/AccountItem/style.less
Normal file
43
src/components/AccountItem/style.less
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
@import '../../global.less';
|
||||
|
||||
.item {
|
||||
background-color: #fff;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
transition: background-color .2s;
|
||||
user-select: none;
|
||||
|
||||
&:active {
|
||||
background-color: @bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
@titleWidth: 80px;
|
||||
// flex-basis: @titleWidth;
|
||||
min-width: @titleWidth;
|
||||
}
|
||||
|
||||
.message {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
margin: 0 10px;
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
margin: 0 10px;
|
||||
color: @hint-color;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.go {
|
||||
flex-shrink: 0;
|
||||
color: lightgrey;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
70
src/components/AreaSelection/index.tsx
Normal file
70
src/components/AreaSelection/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { AreaCodes, AreaCode } from '@/types/common';
|
||||
import styles from './style.less';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
interface AreaSelectionProps {
|
||||
areacodes: AreaCodes;
|
||||
areacode?: AreaCode;
|
||||
onChange?: (areacode: AreaCode) => void;
|
||||
className?: string;
|
||||
size?: 'large' | 'default' | 'small',
|
||||
}
|
||||
|
||||
const AreaSelection: React.FC<AreaSelectionProps> = ({
|
||||
areacode,
|
||||
areacodes,
|
||||
onChange,
|
||||
className = '',
|
||||
size,
|
||||
}) => {
|
||||
const [value, setValue] = useState(areacodes[0]);
|
||||
|
||||
const onValueChange = (id: string) => {
|
||||
const target = _.find(areacodes, { id });
|
||||
if (target) {
|
||||
if (onChange) { onChange(target); }
|
||||
if (!areacode) { setValue(target); }
|
||||
}
|
||||
};
|
||||
|
||||
const actual = areacode || value;
|
||||
return (
|
||||
<div className={`${styles.component} ${className}`}>
|
||||
<div className="name">
|
||||
<Select
|
||||
value={actual && actual.id}
|
||||
onChange={onValueChange}
|
||||
size={size}
|
||||
>
|
||||
{areacodes.map(it => (
|
||||
<Option
|
||||
key={it.id}
|
||||
value={it.id}
|
||||
>{it.areaname}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="code">
|
||||
<Select
|
||||
value={actual && actual.id}
|
||||
onChange={onValueChange}
|
||||
size={size}
|
||||
>
|
||||
{areacodes.map(it => (
|
||||
<Option
|
||||
key={it.id}
|
||||
value={it.id}
|
||||
>+{it.areacode}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AreaSelection;
|
||||
15
src/components/AreaSelection/style.less
Normal file
15
src/components/AreaSelection/style.less
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.component {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
:global {
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name, .code {
|
||||
width: 48%;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/components/ChoiceModal/index.tsx
Normal file
46
src/components/ChoiceModal/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import { ModalProps } from 'antd/lib/modal';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface ChoiceItem {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ChoiceModal {
|
||||
data: ChoiceItem[],
|
||||
selectedKey: string;
|
||||
onItemSelected?: (key: string) => any;
|
||||
}
|
||||
|
||||
const ChoiceModal: React.FC<ChoiceModal & ModalProps> = ({
|
||||
data,
|
||||
selectedKey,
|
||||
onItemSelected,
|
||||
...props
|
||||
}) => {
|
||||
const renderItem = (it: ChoiceItem) => (
|
||||
<div
|
||||
key={it.key}
|
||||
className={`${styles.item} ${it.key === selectedKey ? styles.active : ''}`}
|
||||
onClick={() => { onItemSelected && onItemSelected(it.key); }}
|
||||
>
|
||||
{it.value}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
footer={null}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{data.map(renderItem)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
export default ChoiceModal;
|
||||
19
src/components/ChoiceModal/style.less
Normal file
19
src/components/ChoiceModal/style.less
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
@import '../../global.less';
|
||||
@item-height: 48px;
|
||||
|
||||
.list {
|
||||
max-height: @item-height * 6;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: @item-height;
|
||||
text-align: center;
|
||||
line-height: @item-height;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: lighten(@primary-color, 20%);
|
||||
color: white;
|
||||
}
|
||||
3
src/components/Empty/index.tsx
Normal file
3
src/components/Empty/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Empty } from 'antd';
|
||||
|
||||
export default Empty;
|
||||
12
src/components/Failure/index.tsx
Normal file
12
src/components/Failure/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import failureIcon from '@/assets/failure.png';
|
||||
|
||||
// import styles from './style.less';
|
||||
|
||||
const Failure: React.FC = () => {
|
||||
return (
|
||||
<img style={{ width: 160 }} src={failureIcon} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Failure;
|
||||
48
src/components/Header/index.tsx
Normal file
48
src/components/Header/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Icon } from 'antd';
|
||||
import { dispatch } from '@/utils/utils';
|
||||
|
||||
import styles from './style.less';
|
||||
import { routerRedux } from 'dva';
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
title?: string | React.ReactNode;
|
||||
showReturn?: boolean;
|
||||
rightContent?: string | React.ReactNode;
|
||||
onReturn?: () => any;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
className = '',
|
||||
title,
|
||||
showReturn = true,
|
||||
rightContent,
|
||||
onReturn,
|
||||
}) => {
|
||||
const onClickBack = () => {
|
||||
if (onReturn) {
|
||||
onReturn();
|
||||
} else {
|
||||
dispatch(routerRedux.goBack());
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={`${styles.header} ${className}`}>
|
||||
<div
|
||||
onClick={onClickBack}
|
||||
className={styles.left}
|
||||
>
|
||||
{showReturn && <Icon type="left" />}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
{rightContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
48
src/components/Header/style.less
Normal file
48
src/components/Header/style.less
Normal file
@@ -0,0 +1,48 @@
|
||||
@import '../../global.less';
|
||||
@action-size: 50px;
|
||||
|
||||
.header {
|
||||
height: @header-height;
|
||||
position: relative;
|
||||
background-color: rgba(255, 255, 255, .8);
|
||||
z-index: 10;
|
||||
color: @primary-color;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: @action-size;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
line-height: @header-height;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&:active {
|
||||
background-color: @bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 @action-size;
|
||||
text-align: center;
|
||||
line-height: @header-height;
|
||||
z-index: 1;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: @action-size;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
line-height: @header-height;
|
||||
}
|
||||
51
src/components/Info/index.tsx
Normal file
51
src/components/Info/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import Label from '../Label';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface InfoProps {
|
||||
className?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const Info: React.FC<InfoProps> = ({
|
||||
className = '',
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${styles.header} ${className}`}>
|
||||
<Label className="group-title" name={title} />
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface InfoItemProps {
|
||||
className?: string;
|
||||
title: string | React.ReactNode;
|
||||
info: string | React.ReactNode;
|
||||
|
||||
/**
|
||||
* info 内容的主色调
|
||||
*/
|
||||
type?: 'normal' | 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
export const InfoItem: React.FC<InfoItemProps> = ({
|
||||
className = '',
|
||||
title,
|
||||
info,
|
||||
type = 'normal',
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${className} ${styles.item}`}>
|
||||
<div className="left">{title}</div>
|
||||
<div className={`right ${type}`}>{info}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
39
src/components/Info/style.less
Normal file
39
src/components/Info/style.less
Normal file
@@ -0,0 +1,39 @@
|
||||
@import '../../global.less';
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
.group-title {
|
||||
padding: 14px 10px 6px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
padding: 6px 20px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
:global {
|
||||
.left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
|
||||
&.primary {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: @secondary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/components/Label/index.tsx
Normal file
20
src/components/Label/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface LabelProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const Label: React.FC<LabelProps> = ({
|
||||
className = '',
|
||||
name,
|
||||
}) => (
|
||||
<div className={`${styles.label} ${className}`}>
|
||||
<div className="indicator" />
|
||||
<span className="name">{name}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Label;
|
||||
24
src/components/Label/style.less
Normal file
24
src/components/Label/style.less
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
.label {
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
|
||||
:global {
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 0.8rem;
|
||||
border-radius: 2px;
|
||||
background-color: @primary-color;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
font-weight: lighter;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/components/List/index.tsx
Normal file
267
src/components/List/index.tsx
Normal 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);
|
||||
62
src/components/List/style.less
Normal file
62
src/components/List/style.less
Normal 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;
|
||||
}
|
||||
16
src/components/PageLoading/index.tsx
Normal file
16
src/components/PageLoading/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd'; // loading components from code split
|
||||
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
|
||||
|
||||
const PageLoading = () => (
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 100,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PageLoading;
|
||||
30
src/components/SettingItem/index.tsx
Normal file
30
src/components/SettingItem/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
import { Icon } from 'antd';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface SettingItemProps {
|
||||
logo: string;
|
||||
title: string;
|
||||
onClick?: () => any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SettingItem: React.FC<SettingItemProps> = ({
|
||||
logo,
|
||||
title,
|
||||
onClick,
|
||||
className = '',
|
||||
}) => {
|
||||
const msg = formatMessage({ id: title });
|
||||
return (
|
||||
<div className={`${styles.item} ${className}`} onClick={onClick}>
|
||||
<img className={styles.logo} src={logo} alt={msg} />
|
||||
<span className={styles.title}>{msg}</span>
|
||||
<Icon className={styles.go} type="right" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingItem;
|
||||
34
src/components/SettingItem/style.less
Normal file
34
src/components/SettingItem/style.less
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
@import '../../global.less';
|
||||
|
||||
.item {
|
||||
background-color: #fff;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid lighten(@primary-color, 40%);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
transition: background-color .2s;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
|
||||
&:active {
|
||||
background-color: @bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.go {
|
||||
flex-shrink: 0;
|
||||
color: lightgrey;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
50
src/components/Tab/index.tsx
Normal file
50
src/components/Tab/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi-plugin-locale';
|
||||
|
||||
import styles from './style.less';
|
||||
|
||||
export interface TabItemModel {
|
||||
key: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
iconSelected?: string;
|
||||
}
|
||||
|
||||
export type TabItemModels = TabItemModel[];
|
||||
|
||||
export interface TabProps {
|
||||
items: TabItemModels;
|
||||
selectedKey?: string;
|
||||
onChange?: (key: string) => void;
|
||||
}
|
||||
|
||||
const Tab: React.FC<TabProps> = ({
|
||||
items,
|
||||
selectedKey,
|
||||
onChange,
|
||||
}) => {
|
||||
const onItemClick = (key: string) => {
|
||||
if (onChange) {
|
||||
onChange(key);
|
||||
}
|
||||
};
|
||||
|
||||
const itemWidth = { width: `${100 / (items.length || 1)}%` };
|
||||
return (
|
||||
<div className={styles.tab}>
|
||||
{items.map(item => (
|
||||
<div
|
||||
onClick={() => onItemClick(item.key)}
|
||||
style={itemWidth}
|
||||
className={`${styles.item} ${item.key === selectedKey ? styles.active : ''}`}
|
||||
key={item.key}
|
||||
>
|
||||
{item.icon && item.iconSelected && <img src={item.key === selectedKey ? item.iconSelected : item.icon} />}
|
||||
<span>{formatMessage({ id: item.name })}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default Tab;
|
||||
33
src/components/Tab/style.less
Normal file
33
src/components/Tab/style.less
Normal file
@@ -0,0 +1,33 @@
|
||||
@import '../../global.less';
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 100%;
|
||||
transition: all .18s;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 0.6rem;
|
||||
color: @hint-color;
|
||||
|
||||
:global {
|
||||
img {
|
||||
@size: 20px;
|
||||
width: @size;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: @primary-color;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
Reference in New Issue
Block a user