diff --git a/SearchListApplications/src/_mock.ts b/SearchListApplications/src/_mock.ts index c6903658fb8dfc249d8bb6f1abf8ad7a371ab41f..2de3a07283660e12a635d79e81170a751f6f2527 100644 --- a/SearchListApplications/src/_mock.ts +++ b/SearchListApplications/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/SearchListApplications/src/components/TagSelect/index.tsx b/SearchListApplications/src/components/TagSelect/index.tsx index 96416a4b96b762e81def938dbef7c1754ea82dc6..d1f8f878930a9dfa7b8dc4414484017be21668e7 100644 --- a/SearchListApplications/src/components/TagSelect/index.tsx +++ b/SearchListApplications/src/components/TagSelect/index.tsx @@ -53,9 +53,9 @@ class TagSelect extends Component { static defaultProps = { hideCheckAll: false, actionsText: { - expandText: 'Expand', - collapseText: 'Collapse', - selectAllText: 'All', + expandText: '展开', + collapseText: '收起', + selectAllText: '全部', }, }; static Option: TagSelectOption = TagSelectOption; @@ -130,7 +130,7 @@ class TagSelect extends Component { const { value, expand } = this.state; const { children, hideCheckAll, className, style, expandable, actionsText = {} } = this.props; const checkedAll = this.getAllTags().length === value.length; - const { expandText = 'Expand', collapseText = 'Collapse', selectAllText = 'All' } = actionsText; + const { expandText = '展开', collapseText = '收起', selectAllText = '全部' } = actionsText; const cls = classNames(styles.tagSelect, className, { [styles.hasExpandTag]: expandable, @@ -145,6 +145,7 @@ class TagSelect extends Component { )} {value && + children && React.Children.map(children, (child: React.ReactElement) => { if (this.isTagSelectOption(child)) { return React.cloneElement(child, { diff --git a/SearchListApplications/src/index.tsx b/SearchListApplications/src/index.tsx index 5160d0d0cbe4e8c9083a7678ca4f07c9a6da7826..4a0c3e4785ae6706910375d89192780fd655a4da 100644 --- a/SearchListApplications/src/index.tsx +++ b/SearchListApplications/src/index.tsx @@ -1,23 +1,9 @@ import React, { Component } from 'react'; import numeral from 'numeral'; import { connect } from 'dva'; -import { - Row, - Col, - Form, - Card, - Select, - Icon, - Avatar, - List, - Tooltip, - Dropdown, - Menu, - Input, -} from 'antd'; +import { Row, Col, Form, Card, Select, Icon, Avatar, List, Tooltip, Dropdown, Menu } from 'antd'; import TagSelect from './components/TagSelect'; import StandardFormRow from './components/StandardFormRow'; -import PageHeaderWrapper from './components/PageHeaderWrapper'; import { formatWan } from './utils/utils'; import styles from './style.less'; import { IStateType } from './model'; @@ -34,18 +20,6 @@ interface PAGE_NAME_UPPER_CAMEL_CASEProps extends FormComponentProps { loading: boolean; } -@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, - }) -) class PAGE_NAME_UPPER_CAMEL_CASE extends Component { componentDidMount() { const { dispatch } = this.props; @@ -115,7 +89,7 @@ class PAGE_NAME_UPPER_CAMEL_CASE extends Component {getFieldDecorator('category')( - + 类目一 类目二 类目三 @@ -157,6 +131,7 @@ class PAGE_NAME_UPPER_CAMEL_CASE extends Component +
rowKey="id" grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }} @@ -198,7 +173,7 @@ class PAGE_NAME_UPPER_CAMEL_CASE extends Component ({ + BLOCK_NAME_CAMEL_CASE, + loading: loading.models.BLOCK_NAME_CAMEL_CASE, + }) +)(WarpForm); diff --git a/SearchListApplications/src/service.ts b/SearchListApplications/src/service.ts index f6d5bc70b96dc475843315338bd05e84667a7398..ea2853b7cfe17f628b2131aef88293e1416a2819 100644 --- a/SearchListApplications/src/service.ts +++ b/SearchListApplications/src/service.ts @@ -1,6 +1,6 @@ import request from 'umi-request'; - -export async function queryFakeList(params) { +import { ListItemDataType } from './data'; +export async function queryFakeList(params: ListItemDataType) { return request(`/api/BLOCK_NAME/fake_list`, { params, }); diff --git a/SearchListArticles/src/_mock.js b/SearchListArticles/src/_mock.ts similarity index 89% rename from SearchListArticles/src/_mock.js rename to SearchListArticles/src/_mock.ts index c6903658fb8dfc249d8bb6f1abf8ad7a371ab41f..2de3a07283660e12a635d79e81170a751f6f2527 100644 --- a/SearchListArticles/src/_mock.js +++ b/SearchListArticles/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/SearchListArticles/src/components/ArticleListContent/index.js b/SearchListArticles/src/components/ArticleListContent/index.tsx similarity index 62% rename from SearchListArticles/src/components/ArticleListContent/index.js rename to SearchListArticles/src/components/ArticleListContent/index.tsx index c4525d6a51a8e401740387f854200521891c7f46..bb1abbf19d294e2a0d35a5c08cd8f2277c27cd22 100644 --- a/SearchListArticles/src/components/ArticleListContent/index.js +++ b/SearchListArticles/src/components/ArticleListContent/index.tsx @@ -3,7 +3,19 @@ import moment from 'moment'; import { Avatar } from 'antd'; import styles from './index.less'; -const ArticleListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => ( +interface ArticleListContentProps { + data: { + content: React.ReactNode; + updatedAt: number; + avatar: string; + owner: string; + href: string; + }; +} + +const ArticleListContent: React.SFC = ({ + data: { content, updatedAt, avatar, owner, href }, +}) => (
{content}
diff --git a/SearchListArticles/src/components/PageHeaderWrapper/index.js b/SearchListArticles/src/components/PageHeaderWrapper/index.js deleted file mode 100644 index 1a40e25dfdc97a4e69407cbf1516e15ad65bd00b..0000000000000000000000000000000000000000 --- a/SearchListArticles/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/SearchListArticles/src/components/PageHeaderWrapper/index.less b/SearchListArticles/src/components/PageHeaderWrapper/index.less deleted file mode 100644 index 39a449657a98b039c29e6654fd117267cbb5283a..0000000000000000000000000000000000000000 --- a/SearchListArticles/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/SearchListArticles/src/components/StandardFormRow/index.js b/SearchListArticles/src/components/StandardFormRow/index.tsx similarity index 67% rename from SearchListArticles/src/components/StandardFormRow/index.js rename to SearchListArticles/src/components/StandardFormRow/index.tsx index 8cb0e444e6d488ae4cdfeceb8eb94ee4adbb9248..01a8bb5dabd9f232d14b6186c74909de407dfcb0 100644 --- a/SearchListArticles/src/components/StandardFormRow/index.js +++ b/SearchListArticles/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/SearchListArticles/src/components/TagSelect/index.less b/SearchListArticles/src/components/TagSelect/index.less new file mode 100644 index 0000000000000000000000000000000000000000..93694653133b449b2e6447d0b4aa20ae6d2823b4 --- /dev/null +++ b/SearchListArticles/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/SearchListArticles/src/components/TagSelect/index.tsx b/SearchListArticles/src/components/TagSelect/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d1f8f878930a9dfa7b8dc4414484017be21668e7 --- /dev/null +++ b/SearchListArticles/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/SearchListArticles/src/data.d.ts b/SearchListArticles/src/data.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a4a88ce1a0c1b9d32dd8a5925828858d27cc0b4 --- /dev/null +++ b/SearchListArticles/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/SearchListArticles/src/index.js b/SearchListArticles/src/index.tsx similarity index 86% rename from SearchListArticles/src/index.js rename to SearchListArticles/src/index.tsx index 773a81bab2656ca543a4348a03b5cb8d61306b88..4399de0c319ff42ea91e996cdad17aed0b28a587 100644 --- a/SearchListArticles/src/index.js +++ b/SearchListArticles/src/index.tsx @@ -6,31 +6,23 @@ import TagSelect from './components/TagSelect'; import StandardFormRow from './components/StandardFormRow'; import ArticleListContent from './components/ArticleListContent'; import styles from './style.less'; +import { Dispatch } from 'redux'; +import { FormComponentProps } from 'antd/lib/form'; +import { ListItemDataType } from './data'; +import { IStateType } from './model'; const { Option } = Select; const FormItem = Form.Item; const pageSize = 5; -@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: 5, - }, - }); - }, -}) -class SearchList extends Component { +interface PAGE_NAME_UPPER_CAMEL_CASEProps extends FormComponentProps { + dispatch: Dispatch; + BLOCK_NAME_CAMEL_CASE: IStateType; + loading: boolean; +} + +class PAGE_NAME_UPPER_CAMEL_CASE extends Component { componentDidMount() { const { dispatch } = this.props; dispatch({ @@ -89,7 +81,10 @@ class SearchList extends Component { }, ]; - const IconText = ({ type, text }) => ( + const IconText: React.SFC<{ + type: string; + text: React.ReactNode; + }> = ({ type, text }) => ( {text} @@ -145,8 +140,8 @@ class SearchList extends Component { - - + + {getFieldDecorator('owner', { initialValue: ['wjh', 'zxx'], })( @@ -198,7 +193,7 @@ class SearchList extends Component { bordered={false} bodyStyle={{ padding: '8px 32px 32px 32px' }} > - size="large" loading={list.length === 0 ? loading : false} rowKey="id" @@ -239,4 +234,28 @@ class SearchList extends Component { } } -export default SearchList; +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/SearchListArticles/src/model.js b/SearchListArticles/src/model.ts similarity index 53% rename from SearchListArticles/src/model.js rename to SearchListArticles/src/model.ts index c32dedec9fa8202b9afc824654377908020723e9..73fea52bb8f35153d77b7624263be7833832d739 100644 --- a/SearchListArticles/src/model.js +++ b/SearchListArticles/src/model.ts @@ -1,6 +1,32 @@ 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: (func: (state: IStateType) => T) => T } +) => void; + +export interface ModelType { + namespace: string; + state: IStateType; + effects: { + fetch: Effect; + appendFetch: Effect; + }; + reducers: { + queryList: Reducer; + appendList: Reducer; + }; +} + +const Model: ModelType = { namespace: 'BLOCK_NAME_CAMEL_CASE', state: { @@ -34,8 +60,10 @@ export default { appendList(state, action) { return { ...state, - list: state.list.concat(action.payload), + list: state!.list.concat(action.payload), }; }, }, }; + +export default Model; diff --git a/SearchListArticles/src/service.js b/SearchListArticles/src/service.js deleted file mode 100644 index f6d5bc70b96dc475843315338bd05e84667a7398..0000000000000000000000000000000000000000 --- a/SearchListArticles/src/service.js +++ /dev/null @@ -1,7 +0,0 @@ -import request from 'umi-request'; - -export async function queryFakeList(params) { - return request(`/api/BLOCK_NAME/fake_list`, { - params, - }); -} diff --git a/SearchListArticles/src/service.ts b/SearchListArticles/src/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e5730a0da741bcefc84b97348127a4602eb7849 --- /dev/null +++ b/SearchListArticles/src/service.ts @@ -0,0 +1,8 @@ +import request from 'umi-request'; +import { ListItemDataType } from './data'; + +export async function queryFakeList(params: ListItemDataType) { + return request(`/api/BLOCK_NAME/fake_list`, { + params, + }); +} diff --git a/package.json b/package.json index 093481544572d524bfd3de777d011b3585c542a1..a02159444aadd9dcf73276703f737d46b6fe826b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "dev": "cross-env PAGES_PATH='ResultFail/src' umi dev", + "dev": "cross-env PAGES_PATH='SearchListArticles/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",