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,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;

View 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;
}

View 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;

View File

@@ -0,0 +1,15 @@
.component {
display: flex;
justify-content: space-between;
:global {
.ant-select {
width: 100%;
}
.name, .code {
width: 48%;
}
}
}

View 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;

View 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;
}

View File

@@ -0,0 +1,3 @@
import { Empty } from 'antd';
export default Empty;

View 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;

View 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;

View 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;
}

View 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;

View 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;
}
}
}
}

View 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;

View 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;
}
}
}

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;
}

View 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;

View 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;

View 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;
}

View 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;

View 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);
}