diff --git a/SearchListProjects/src/_mock.js b/SearchListProjects/src/_mock.ts similarity index 89% rename from SearchListProjects/src/_mock.js rename to SearchListProjects/src/_mock.ts index c6903658fb8dfc249d8bb6f1abf8ad7a371ab41f..2de3a07283660e12a635d79e81170a751f6f2527 100644 --- a/SearchListProjects/src/_mock.js +++ b/SearchListProjects/src/_mock.ts @@ -1,3 +1,5 @@ +import { ListItemDataType } from './data'; + const titles = [ 'Alipay', 'Angular', @@ -45,7 +47,7 @@ const user = [ '仲尼', ]; -function fakeList(count) { +function fakeList(count: number): ListItemDataType[] { const list = []; for (let i = 0; i < count; i += 1) { list.push({ @@ -53,13 +55,17 @@ function fakeList(count) { owner: user[i % 10], title: titles[i % 8], avatar: avatars[i % 8], - cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)], - status: ['active', 'exception', 'normal'][i % 3], + cover: parseInt(i / 4 + '', 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)], + status: ['active', 'exception', 'normal'][i % 3] as + | 'normal' + | 'exception' + | 'active' + | 'success', percent: Math.ceil(Math.random() * 50) + 50, logo: avatars[i % 8], href: 'https://ant.design', - updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i), - createdAt: 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).getTime(), subDescription: desc[i % 5], description: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。', @@ -93,7 +99,7 @@ function fakeList(count) { return list; } -function getFakeList(req, res) { +function getFakeList(req: { query: any }, res: { json: (arg0: any[]) => void }) { const params = req.query; const count = params.count * 1 || 20; diff --git a/SearchListProjects/src/components/AvatarList/index.less b/SearchListProjects/src/components/AvatarList/index.less new file mode 100644 index 0000000000000000000000000000000000000000..45904ce6ae8bc31c48c4512c87163757ec010e76 --- /dev/null +++ b/SearchListProjects/src/components/AvatarList/index.less @@ -0,0 +1,50 @@ +@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; + } + } + } +} diff --git a/SearchListProjects/src/components/AvatarList/index.tsx b/SearchListProjects/src/components/AvatarList/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c37efcf58a12048dc02cc2e6f7971bd8fd4e1c06 --- /dev/null +++ b/SearchListProjects/src/components/AvatarList/index.tsx @@ -0,0 +1,85 @@ +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; + size?: SizeType; + maxLength?: number; + excessItemsStyle?: React.CSSProperties; + style?: React.CSSProperties; + children: React.ReactElement | Array>; +} + +const avatarSizeToClassName = (size?: SizeType) => + classNames(styles.avatarItem, { + [styles.avatarItemLarge]: size === 'large', + [styles.avatarItemSmall]: size === 'small', + [styles.avatarItemMini]: size === 'mini', + }); + +const Item: React.SFC = ({ src, size, tips, onClick = () => {} }) => { + const cls = avatarSizeToClassName(size); + + return ( +
  • + {tips ? ( + + + + ) : ( + + )} +
  • + ); +}; + +const AvatarList: React.SFC & { 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 + >; + const childrenWithProps = childrenArray.slice(0, numToShow).map(child => + React.cloneElement(child, { + size, + }) + ); + + if (numToShow < numOfChildren) { + const cls = avatarSizeToClassName(size); + + childrenWithProps.push( +
  • + {`+${numOfChildren - maxLength}`} +
  • + ); + } + + return ( +
    +
      {childrenWithProps}
    +
    + ); +}; + +AvatarList.Item = Item; + +export default AvatarList; diff --git a/SearchListProjects/src/components/PageHeaderWrapper/index.js b/SearchListProjects/src/components/PageHeaderWrapper/index.js deleted file mode 100644 index 1a40e25dfdc97a4e69407cbf1516e15ad65bd00b..0000000000000000000000000000000000000000 --- a/SearchListProjects/src/components/PageHeaderWrapper/index.js +++ /dev/null @@ -1,25 +0,0 @@ -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 }) => ( -
    - } - key="pageheader" - {...restProps} - linkElement={Link} - itemRender={item => { - if (item.locale) { - return ; - } - return item.title; - }} - /> - {children ?
    {children}
    : null} -
    -); - -export default PageHeaderWrapper; diff --git a/SearchListProjects/src/components/PageHeaderWrapper/index.less b/SearchListProjects/src/components/PageHeaderWrapper/index.less deleted file mode 100644 index 39a449657a98b039c29e6654fd117267cbb5283a..0000000000000000000000000000000000000000 --- a/SearchListProjects/src/components/PageHeaderWrapper/index.less +++ /dev/null @@ -1,11 +0,0 @@ -@import '~antd/lib/style/themes/default.less'; - -.content { - margin: 24px 24px 0; -} - -@media screen and (max-width: @screen-sm) { - .content { - margin: 24px 0 0; - } -} diff --git a/SearchListProjects/src/components/StandardFormRow/index.js b/SearchListProjects/src/components/StandardFormRow/index.tsx similarity index 67% rename from SearchListProjects/src/components/StandardFormRow/index.js rename to SearchListProjects/src/components/StandardFormRow/index.tsx index 8cb0e444e6d488ae4cdfeceb8eb94ee4adbb9248..01a8bb5dabd9f232d14b6186c74909de407dfcb0 100644 --- a/SearchListProjects/src/components/StandardFormRow/index.js +++ b/SearchListProjects/src/components/StandardFormRow/index.tsx @@ -2,7 +2,22 @@ import React from 'react'; import classNames from 'classnames'; 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 = ({ + title, + children, + last, + block, + grid, + ...rest +}) => { const cls = classNames(styles.standardFormRow, { [styles.standardFormRowBlock]: block, [styles.standardFormRowLast]: last, diff --git a/SearchListProjects/src/components/TagSelect/index.less b/SearchListProjects/src/components/TagSelect/index.less new file mode 100644 index 0000000000000000000000000000000000000000..93694653133b449b2e6447d0b4aa20ae6d2823b4 --- /dev/null +++ b/SearchListProjects/src/components/TagSelect/index.less @@ -0,0 +1,33 @@ +@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; + } +} diff --git a/SearchListProjects/src/components/TagSelect/index.tsx b/SearchListProjects/src/components/TagSelect/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d1f8f878930a9dfa7b8dc4414484017be21668e7 --- /dev/null +++ b/SearchListProjects/src/components/TagSelect/index.tsx @@ -0,0 +1,170 @@ +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 | Array>; +} + +const TagSelectOption: React.SFC & { + isTagSelectOption: boolean; +} = ({ children, checked, onChange, value }) => ( + onChange && onChange(value, state)} + > + {children} + +); + +TagSelectOption.isTagSelectOption = true; + +interface TagSelectState { + expand: boolean; + value: (string | number)[]; +} + +class TagSelect extends Component { + 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[]; + 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) => + 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 ( +
    + {hideCheckAll ? null : ( + + {selectAllText} + + )} + {value && + children && + React.Children.map(children, (child: React.ReactElement) => { + 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 && ( + + {expand ? collapseText : expandText} + + )} +
    + ); + } +} + +export default TagSelect; diff --git a/SearchListProjects/src/data.d.ts b/SearchListProjects/src/data.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a4a88ce1a0c1b9d32dd8a5925828858d27cc0b4 --- /dev/null +++ b/SearchListProjects/src/data.d.ts @@ -0,0 +1,29 @@ +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[]; +} diff --git a/SearchListProjects/src/index.js b/SearchListProjects/src/index.tsx similarity index 76% rename from SearchListProjects/src/index.js rename to SearchListProjects/src/index.tsx index f5869fb2e1bbc80e8f3557c6048b886d735ab5db..07599c0e7f537e1b06d8e79c89825562f7060feb 100644 --- a/SearchListProjects/src/index.js +++ b/SearchListProjects/src/index.tsx @@ -1,37 +1,26 @@ -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import moment from 'moment'; import { connect } from 'dva'; -import { Row, Col, Form, Card, Select, List, Input } from 'antd'; - -import { TagSelect, AvatarList, Ellipsis } from 'ant-design-pro'; - +import { Row, Col, Form, Card, Select, List, Typography } from 'antd'; import StandardFormRow from './components/StandardFormRow'; +import TagSelect from './components/TagSelect'; +import AvatarList from './components/AvatarList'; 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 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 }) => ({ - 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 { +class PAGE_NAME_UPPER_CAMEL_CASE extends Component { componentDidMount() { const { dispatch } = this.props; dispatch({ @@ -51,7 +40,7 @@ class CoverCardList extends PureComponent { const { getFieldDecorator } = form; const cardList = list ? ( - rowKey="id" loading={loading} grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }} @@ -65,12 +54,16 @@ class CoverCardList extends PureComponent { > {item.title}} - description={{item.subDescription}} + description={ + + {item.subDescription} + + } />
    {moment(item.updatedAt).fromNow()}
    - + {item.members.map((member, i) => ( - -
    - ); return (
    @@ -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); diff --git a/SearchListProjects/src/model.js b/SearchListProjects/src/model.js deleted file mode 100644 index ead65a8df48a91a8b103af60429206eb865c7230..0000000000000000000000000000000000000000 --- a/SearchListProjects/src/model.js +++ /dev/null @@ -1,28 +0,0 @@ -import { queryFakeList } from './service'; - -export default { - namespace: 'BLOCK_NAME_CAMEL_CASE', - - state: { - list: [], - }, - - effects: { - *fetch({ payload }, { call, put }) { - const response = yield call(queryFakeList, payload); - yield put({ - type: 'queryList', - payload: Array.isArray(response) ? response : [], - }); - }, - }, - - reducers: { - queryList(state, action) { - return { - ...state, - list: action.payload, - }; - }, - }, -}; diff --git a/SearchListProjects/src/model.ts b/SearchListProjects/src/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..24c2230c35d4fbff940fed19f6231cb21faee61b --- /dev/null +++ b/SearchListProjects/src/model.ts @@ -0,0 +1,54 @@ +import { queryFakeList } from './service'; +import { ListItemDataType } from './data'; +import { Reducer } from 'redux'; +import { EffectsCommandMap } from 'dva'; +import { AnyAction } from 'redux'; + +export interface IStateType { + list: ListItemDataType[]; +} + +export type Effect = ( + action: AnyAction, + effects: EffectsCommandMap & { select: (func: (state: IStateType) => T) => T } +) => void; + +export interface ModelType { + namespace: string; + state: IStateType; + effects: { + fetch: Effect; + }; + reducers: { + queryList: Reducer; + }; +} + +const Model: ModelType = { + namespace: 'BLOCK_NAME_CAMEL_CASE', + + state: { + list: [], + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(queryFakeList, payload); + yield put({ + type: 'queryList', + payload: Array.isArray(response) ? response : [], + }); + }, + }, + + reducers: { + queryList(state, action) { + return { + ...state, + list: action.payload, + }; + }, + }, +}; + +export default Model; diff --git a/SearchListProjects/src/service.js b/SearchListProjects/src/service.ts similarity index 61% rename from SearchListProjects/src/service.js rename to SearchListProjects/src/service.ts index f6d5bc70b96dc475843315338bd05e84667a7398..cd095a09d78ffce2f91a22c3127b3a9aa1db7545 100644 --- a/SearchListProjects/src/service.js +++ b/SearchListProjects/src/service.ts @@ -1,6 +1,6 @@ import request from 'umi-request'; -export async function queryFakeList(params) { +export async function queryFakeList(params: { count: number }) { return request(`/api/BLOCK_NAME/fake_list`, { params, }); diff --git a/package.json b/package.json index a02159444aadd9dcf73276703f737d46b6fe826b..31303af9186fdf1e2eab7db4ef9895b3d8d424d0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "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": "eslint --ext .js src mock tests && npm run lint:style", "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",