Commit 66635a08 authored by 陈帅's avatar 陈帅

SearchListProjects finish

parent 27d50044
import { ListItemDataType } from './data';
const titles = [ const titles = [
'Alipay', 'Alipay',
'Angular', 'Angular',
...@@ -45,7 +47,7 @@ const user = [ ...@@ -45,7 +47,7 @@ const user = [
'仲尼', '仲尼',
]; ];
function fakeList(count) { function fakeList(count: number): ListItemDataType[] {
const list = []; const list = [];
for (let i = 0; i < count; i += 1) { for (let i = 0; i < count; i += 1) {
list.push({ list.push({
...@@ -53,13 +55,17 @@ function fakeList(count) { ...@@ -53,13 +55,17 @@ function fakeList(count) {
owner: user[i % 10], owner: user[i % 10],
title: titles[i % 8], title: titles[i % 8],
avatar: avatars[i % 8], avatar: avatars[i % 8],
cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)], cover: parseInt(i / 4 + '', 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3], status: ['active', 'exception', 'normal'][i % 3] as
| 'normal'
| 'exception'
| 'active'
| 'success',
percent: Math.ceil(Math.random() * 50) + 50, percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8], logo: avatars[i % 8],
href: 'https://ant.design', href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i), updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i), createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
subDescription: desc[i % 5], subDescription: desc[i % 5],
description: description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。', '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
...@@ -93,7 +99,7 @@ function fakeList(count) { ...@@ -93,7 +99,7 @@ function fakeList(count) {
return list; return list;
} }
function getFakeList(req, res) { function getFakeList(req: { query: any }, res: { json: (arg0: any[]) => void }) {
const params = req.query; const params = req.query;
const count = params.count * 1 || 20; const count = params.count * 1 || 20;
......
@import '~antd/lib/style/themes/default.less';
.avatarList {
display: inline-block;
ul {
display: inline-block;
margin-left: 8px;
font-size: 0;
}
}
.avatarItem {
display: inline-block;
width: @avatar-size-base;
height: @avatar-size-base;
margin-left: -8px;
font-size: @font-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
}
}
}
.avatarItemLarge {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
.avatarItemSmall {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
.avatarItemMini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
import React from 'react';
import { Tooltip, Avatar } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
export declare type SizeType = number | 'small' | 'default' | 'large';
export interface AvatarItemProps {
tips: React.ReactNode;
src: string;
size?: SizeType;
style?: React.CSSProperties;
onClick?: () => void;
}
export interface AvatarListProps {
Item?: React.ReactElement<AvatarItemProps>;
size?: SizeType;
maxLength?: number;
excessItemsStyle?: React.CSSProperties;
style?: React.CSSProperties;
children: React.ReactElement<AvatarItemProps> | Array<React.ReactElement<AvatarItemProps>>;
}
const avatarSizeToClassName = (size?: SizeType) =>
classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
const Item: React.SFC<AvatarItemProps> = ({ src, size, tips, onClick = () => {} }) => {
const cls = avatarSizeToClassName(size);
return (
<li className={cls} onClick={onClick}>
{tips ? (
<Tooltip title={tips}>
<Avatar src={src} size={size} style={{ cursor: 'pointer' }} />
</Tooltip>
) : (
<Avatar src={src} size={size} />
)}
</li>
);
};
const AvatarList: React.SFC<AvatarListProps> & { Item: typeof Item } = ({
children,
size,
maxLength = 5,
excessItemsStyle,
...other
}) => {
const numOfChildren = React.Children.count(children);
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
const childrenArray = React.Children.toArray(children) as Array<
React.ReactElement<AvatarItemProps>
>;
const childrenWithProps = childrenArray.slice(0, numToShow).map(child =>
React.cloneElement(child, {
size,
})
);
if (numToShow < numOfChildren) {
const cls = avatarSizeToClassName(size);
childrenWithProps.push(
<li key="exceed" className={cls}>
<Avatar size={size} style={excessItemsStyle}>{`+${numOfChildren - maxLength}`}</Avatar>
</li>
);
}
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
AvatarList.Item = Item;
export default AvatarList;
import React from 'react';
import { FormattedMessage } from 'umi-plugin-react/locale';
import Link from 'umi/link';
import { PageHeader } from 'ant-design-pro';
import styles from './index.less';
const PageHeaderWrapper = ({ children, wrapperClassName, ...restProps }) => (
<div style={{ margin: '-24px -24px 0' }} className={wrapperClassName}>
<PageHeader
home={<FormattedMessage id="BLOCK_NAME.menu.home" defaultMessage="Home" />}
key="pageheader"
{...restProps}
linkElement={Link}
itemRender={item => {
if (item.locale) {
return <FormattedMessage id={item.locale} defaultMessage={item.title} />;
}
return item.title;
}}
/>
{children ? <div className={styles.content}>{children}</div> : null}
</div>
);
export default PageHeaderWrapper;
@import '~antd/lib/style/themes/default.less';
.content {
margin: 24px 24px 0;
}
@media screen and (max-width: @screen-sm) {
.content {
margin: 24px 0 0;
}
}
...@@ -2,7 +2,22 @@ import React from 'react'; ...@@ -2,7 +2,22 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './index.less'; import styles from './index.less';
const StandardFormRow = ({ title, children, last, block, grid, ...rest }) => { interface StandardFormRowProps {
title?: string;
last?: boolean;
block?: boolean;
grid?: boolean;
style?: React.CSSProperties;
}
const StandardFormRow: React.SFC<StandardFormRowProps> = ({
title,
children,
last,
block,
grid,
...rest
}) => {
const cls = classNames(styles.standardFormRow, { const cls = classNames(styles.standardFormRow, {
[styles.standardFormRowBlock]: block, [styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last, [styles.standardFormRowLast]: last,
......
@import '~antd/lib/style/themes/default.less';
.tagSelect {
position: relative;
max-height: 32px;
margin-left: -8px;
overflow: hidden;
line-height: 32px;
transition: all 0.3s;
user-select: none;
:global {
.ant-tag {
margin-right: 24px;
padding: 0 8px;
font-size: @font-size-base;
}
}
&.expanded {
max-height: 200px;
transition: all 0.3s;
}
.trigger {
position: absolute;
top: 0;
right: 0;
i {
font-size: 12px;
}
}
&.hasExpandTag {
padding-right: 50px;
}
}
import React, { Component } from 'react';
import classNames from 'classnames';
import { Tag, Icon } from 'antd';
import styles from './index.less';
const { CheckableTag } = Tag;
export interface TagSelectOptionProps {
value?: string | number;
style?: React.CSSProperties;
checked?: boolean;
onChange?: (value: string | number | undefined, state: boolean) => void;
}
export interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
value?: (string | number)[];
defaultValue?: (string | number)[];
style?: React.CSSProperties;
hideCheckAll?: boolean;
actionsText?: {
expandText?: React.ReactNode;
collapseText?: React.ReactNode;
selectAllText?: React.ReactNode;
};
className?: string;
Option?: TagSelectOptionProps;
children?: React.ReactElement<TagSelectOption> | Array<React.ReactElement<TagSelectOption>>;
}
const TagSelectOption: React.SFC<TagSelectOptionProps> & {
isTagSelectOption: boolean;
} = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={!!checked}
key={value}
onChange={state => onChange && onChange(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.isTagSelectOption = true;
interface TagSelectState {
expand: boolean;
value: (string | number)[];
}
class TagSelect extends Component<TagSelectProps, TagSelectState> {
static defaultProps = {
hideCheckAll: false,
actionsText: {
expandText: '展开',
collapseText: '收起',
selectAllText: '全部',
},
};
static Option: TagSelectOption = TagSelectOption;
constructor(props: TagSelectProps) {
super(props);
this.state = {
expand: false,
value: props.value || props.defaultValue || [],
};
}
static getDerivedStateFromProps(nextProps: TagSelectProps) {
if ('value' in nextProps) {
return { value: nextProps.value || [] };
}
return null;
}
onChange = (value: (string | number)[]) => {
const { onChange } = this.props;
if (!('value' in this.props)) {
this.setState({ value });
}
if (onChange) {
onChange(value);
}
};
onSelectAll = (checked: boolean) => {
let checkedTags: (string | number)[] = [];
if (checked) {
checkedTags = this.getAllTags();
}
this.onChange(checkedTags);
};
getAllTags() {
let { children } = this.props;
const childrenArray = React.Children.toArray(children) as React.ReactElement<TagSelectOption>[];
const checkedTags = childrenArray
.filter(child => this.isTagSelectOption(child))
.map(child => child.props.value);
return checkedTags || [];
}
handleTagChange = (value: string | number, checked: boolean) => {
const { value: StateValue } = this.state;
const checkedTags: (string | number)[] = [...StateValue];
const index = checkedTags.indexOf(value);
if (checked && index === -1) {
checkedTags.push(value);
} else if (!checked && index > -1) {
checkedTags.splice(index, 1);
}
this.onChange(checkedTags);
};
handleExpand = () => {
const { expand } = this.state;
this.setState({
expand: !expand,
});
};
isTagSelectOption = (node: React.ReactElement<TagSelectOption, TagSelectOption>) =>
node &&
node.type &&
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
render() {
const { value, expand } = this.state;
const { children, hideCheckAll, className, style, expandable, actionsText = {} } = this.props;
const checkedAll = this.getAllTags().length === value.length;
const { expandText = '展开', collapseText = '收起', selectAllText = '全部' } = actionsText;
const cls = classNames(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});
return (
<div className={cls} style={style}>
{hideCheckAll ? null : (
<CheckableTag checked={checkedAll} key="tag-select-__all__" onChange={this.onSelectAll}>
{selectAllText}
</CheckableTag>
)}
{value &&
children &&
React.Children.map(children, (child: React.ReactElement<TagSelectOption>) => {
if (this.isTagSelectOption(child)) {
return React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
value: child.props.value,
checked: value.indexOf(child.props.value) > -1,
onChange: this.handleTagChange,
});
}
return child;
})}
{expandable && (
<a className={styles.trigger} onClick={this.handleExpand}>
{expand ? collapseText : expandText} <Icon type={expand ? 'up' : 'down'} />
</a>
)}
</div>
);
}
}
export default TagSelect;
export interface Member {
avatar: string;
name: string;
id: string;
}
export interface ListItemDataType {
id: string;
owner: string;
title: string;
avatar: string;
cover: string;
status: 'normal' | 'exception' | 'active' | 'success';
percent: number;
logo: string;
href: string;
body?: any;
updatedAt: number;
createdAt: number;
subDescription: string;
description: string;
activeUser: number;
newUser: number;
star: number;
like: number;
message: number;
content: string;
members: Member[];
}
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'dva'; import { connect } from 'dva';
import { Row, Col, Form, Card, Select, List, Input } from 'antd'; import { Row, Col, Form, Card, Select, List, Typography } from 'antd';
import { TagSelect, AvatarList, Ellipsis } from 'ant-design-pro';
import StandardFormRow from './components/StandardFormRow'; import StandardFormRow from './components/StandardFormRow';
import TagSelect from './components/TagSelect';
import AvatarList from './components/AvatarList';
import styles from './style.less'; import styles from './style.less';
import { IStateType } from './model';
import { Dispatch } from 'redux';
import { FormComponentProps } from 'antd/lib/form';
import { ListItemDataType } from './data';
const { Option } = Select; const { Option } = Select;
const FormItem = Form.Item; const FormItem = Form.Item;
const { Paragraph } = Typography;
/* eslint react/no-array-index-key: 0 */ interface PAGE_NAME_UPPER_CAMEL_CASEProps extends FormComponentProps {
dispatch: Dispatch;
BLOCK_NAME_CAMEL_CASE: IStateType;
loading: boolean;
}
@connect(({ BLOCK_NAME_CAMEL_CASE, loading }) => ({ class PAGE_NAME_UPPER_CAMEL_CASE extends Component<PAGE_NAME_UPPER_CAMEL_CASEProps> {
BLOCK_NAME_CAMEL_CASE,
loading: loading.models.BLOCK_NAME_CAMEL_CASE,
}))
@Form.create({
onValuesChange({ dispatch }, changedValues, allValues) {
// 表单项变化时请求数据
// eslint-disable-next-line
console.log(changedValues, allValues);
// 模拟查询表单生效
dispatch({
type: 'BLOCK_NAME_CAMEL_CASE/fetch',
payload: {
count: 8,
},
});
},
})
class CoverCardList extends PureComponent {
componentDidMount() { componentDidMount() {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({ dispatch({
...@@ -51,7 +40,7 @@ class CoverCardList extends PureComponent { ...@@ -51,7 +40,7 @@ class CoverCardList extends PureComponent {
const { getFieldDecorator } = form; const { getFieldDecorator } = form;
const cardList = list ? ( const cardList = list ? (
<List <List<ListItemDataType>
rowKey="id" rowKey="id"
loading={loading} loading={loading}
grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }} grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
...@@ -65,12 +54,16 @@ class CoverCardList extends PureComponent { ...@@ -65,12 +54,16 @@ class CoverCardList extends PureComponent {
> >
<Card.Meta <Card.Meta
title={<a>{item.title}</a>} title={<a>{item.title}</a>}
description={<Ellipsis lines={2}>{item.subDescription}</Ellipsis>} description={
<Paragraph className={styles.item} ellipsis={{ rows: 2 }}>
{item.subDescription}
</Paragraph>
}
/> />
<div className={styles.cardItemContent}> <div className={styles.cardItemContent}>
<span>{moment(item.updatedAt).fromNow()}</span> <span>{moment(item.updatedAt).fromNow()}</span>
<div className={styles.avatarList}> <div className={styles.avatarList}>
<AvatarList size="mini"> <AvatarList size="small">
{item.members.map((member, i) => ( {item.members.map((member, i) => (
<AvatarList.Item <AvatarList.Item
key={`${item.id}-avatar-${i}`} key={`${item.id}-avatar-${i}`}
...@@ -93,17 +86,6 @@ class CoverCardList extends PureComponent { ...@@ -93,17 +86,6 @@ class CoverCardList extends PureComponent {
sm: { span: 16 }, sm: { span: 16 },
}, },
}; };
const mainSearch = (
<div style={{ textAlign: 'center' }}>
<Input.Search
placeholder="请输入"
enterButton="搜索"
size="large"
onSearch={this.handleFormSubmit}
style={{ width: 522 }}
/>
</div>
);
return ( return (
<div className={styles.coverCardList}> <div className={styles.coverCardList}>
...@@ -160,4 +142,28 @@ class CoverCardList extends PureComponent { ...@@ -160,4 +142,28 @@ class CoverCardList extends PureComponent {
} }
} }
export default CoverCardList; const WarpForm = Form.create({
onValuesChange({ dispatch }: PAGE_NAME_UPPER_CAMEL_CASEProps, changedValues, allValues) {
// 表单项变化时请求数据
// 模拟查询表单生效
dispatch({
type: 'BLOCK_NAME_CAMEL_CASE/fetch',
payload: {
count: 8,
},
});
},
})(PAGE_NAME_UPPER_CAMEL_CASE);
export default connect(
({
BLOCK_NAME_CAMEL_CASE,
loading,
}: {
BLOCK_NAME_CAMEL_CASE: IStateType;
loading: { models: { [key: string]: boolean } };
}) => ({
BLOCK_NAME_CAMEL_CASE,
loading: loading.models.BLOCK_NAME_CAMEL_CASE,
})
)(WarpForm);
import { queryFakeList } from './service'; import { queryFakeList } from './service';
import { ListItemDataType } from './data';
import { Reducer } from 'redux';
import { EffectsCommandMap } from 'dva';
import { AnyAction } from 'redux';
export default { export interface IStateType {
list: ListItemDataType[];
}
export type Effect = (
action: AnyAction,
effects: EffectsCommandMap & { select: <T>(func: (state: IStateType) => T) => T }
) => void;
export interface ModelType {
namespace: string;
state: IStateType;
effects: {
fetch: Effect;
};
reducers: {
queryList: Reducer<IStateType>;
};
}
const Model: ModelType = {
namespace: 'BLOCK_NAME_CAMEL_CASE', namespace: 'BLOCK_NAME_CAMEL_CASE',
state: { state: {
...@@ -26,3 +50,5 @@ export default { ...@@ -26,3 +50,5 @@ export default {
}, },
}, },
}; };
export default Model;
import request from 'umi-request'; import request from 'umi-request';
export async function queryFakeList(params) { export async function queryFakeList(params: { count: number }) {
return request(`/api/BLOCK_NAME/fake_list`, { return request(`/api/BLOCK_NAME/fake_list`, {
params, params,
}); });
......
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env PAGES_PATH='SearchListArticles/src' umi dev", "dev": "cross-env PAGES_PATH='SearchListProjects/src' umi dev",
"lint:style": "stylelint \"src/**/*.less\" --syntax less", "lint:style": "stylelint \"src/**/*.less\" --syntax less",
"lint": "eslint --ext .js src mock tests && npm run lint:style", "lint": "eslint --ext .js src mock tests && npm run lint:style",
"lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style", "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment