diff --git a/components/_util/props-util.js b/components/_util/props-util.js
index a78030095..0a1cf7d6b 100644
--- a/components/_util/props-util.js
+++ b/components/_util/props-util.js
@@ -59,6 +59,9 @@ const getSlots = (ele) => {
return slots
}
const getSlotOptions = (ele) => {
+ if (ele.fnOptions) { // 函数式组件
+ return ele.fnOptions
+ }
let componentOptions = ele.componentOptions
if (ele.$vnode) {
componentOptions = ele.$vnode.componentOptions
diff --git a/components/index.js b/components/index.js
index ee1b260f1..a9779713a 100644
--- a/components/index.js
+++ b/components/index.js
@@ -60,7 +60,7 @@ import { default as InputNumber } from './input-number'
import { default as Layout } from './layout'
-// import { default as List } from './list'
+import { default as List } from './list'
import { default as LocaleProvider } from './locale-provider'
@@ -163,6 +163,9 @@ const components = [
Layout.Footer,
Layout.Sider,
Layout.Content,
+ List,
+ List.Item,
+ List.Item.Meta,
LocaleProvider,
Menu,
Menu.Item,
@@ -247,6 +250,7 @@ export {
Input,
InputNumber,
Layout,
+ List,
LocaleProvider,
Menu,
Modal,
diff --git a/components/list/Item.jsx b/components/list/Item.jsx
new file mode 100644
index 000000000..212047ff4
--- /dev/null
+++ b/components/list/Item.jsx
@@ -0,0 +1,143 @@
+import PropTypes from '../_util/vue-types'
+import classNames from 'classnames'
+import { getSlotOptions, getComponentFromProp, isEmptyElement } from '../_util/props-util'
+import { Col } from '../grid'
+import { ListGridType } from './index'
+
+export const ListItemProps = {
+ prefixCls: PropTypes.string,
+ extra: PropTypes.any,
+ actions: PropTypes.arrayOf(PropTypes.any),
+ grid: ListGridType,
+}
+
+export const ListItemMetaProps = {
+ avatar: PropTypes.any,
+ description: PropTypes.any,
+ prefixCls: PropTypes.string,
+ title: PropTypes.any,
+}
+
+export const Meta = {
+ functional: true,
+ name: 'AListItemMeta',
+ __ANT_LIST_ITEM_META: true,
+ render (h, context) {
+ const { props, slots, listeners } = context
+ const slotsMap = slots()
+ const {
+ prefixCls = 'ant-list',
+ } = props
+ const avatar = props.avatar || slotsMap.avatar
+ const title = props.title || slotsMap.title
+ const description = props.description || slotsMap.description
+ const content = (
+
+ {title &&
}
+ {description &&
{description}
}
+
+ )
+ return (
+
+ )
+ },
+
+}
+
+function getGrid (grid, t) {
+ return grid[t] && Math.floor(24 / grid[t])
+}
+
+export default {
+ name: 'AListItem',
+ Meta,
+ props: ListItemProps,
+ inject: {
+ listContext: { default: {}},
+ },
+
+ render () {
+ const { grid } = this.listContext
+ const { prefixCls = 'ant-list', $slots, $listeners } = this
+ const classString = `${prefixCls}-item`
+ const extra = getComponentFromProp(this, 'extra')
+ const actions = getComponentFromProp(this, 'actions')
+ const metaContent = []
+ const otherContent = []
+
+ ;($slots.default || []).forEach((element) => {
+ if (!isEmptyElement(element)) {
+ if (getSlotOptions(element).__ANT_LIST_ITEM_META) {
+ metaContent.push(element)
+ } else {
+ otherContent.push(element)
+ }
+ }
+ })
+
+ const contentClassString = classNames(`${prefixCls}-item-content`, {
+ [`${prefixCls}-item-content-single`]: (metaContent.length < 1),
+ })
+ const content = otherContent.length > 0 ? (
+
+ {otherContent}
+
) : null
+
+ let actionsContent
+ if (actions && actions.length > 0) {
+ const actionsContentItem = (action, i) => (
+
+ {action}
+ {i !== (actions.length - 1) && }
+
+ )
+ actionsContent = (
+
+ {actions.map((action, i) => actionsContentItem(action, i))}
+
+ )
+ }
+
+ const extraContent = (
+
+ )
+
+ const mainContent = grid ? (
+
+
+ {extra && extraContent}
+ {!extra && metaContent}
+ {!extra && content}
+ {!extra && actionsContent}
+
+
+ ) : (
+
+ {extra && extraContent}
+ {!extra && metaContent}
+ {!extra && content}
+ {!extra && actionsContent}
+
+ )
+
+ return mainContent
+ },
+}
diff --git a/components/list/__tests__/__snapshots__/demo.test.js.snap b/components/list/__tests__/__snapshots__/demo.test.js.snap
new file mode 100644
index 000000000..a23c0b0f5
--- /dev/null
+++ b/components/list/__tests__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,486 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders ./components/list/demo/basic.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/grid.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/infinite-load.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/infinite-virtualized-load.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/loadmore.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/resposive.md correctly 1`] = `
+
+`;
+
+exports[`renders ./components/list/demo/simple.md correctly 1`] = `
+
+
Default Size
+
+
+
+
+
+
Racing car sprays burning fuel into crowd.
+
+
+
Japanese princess to wed commoner.
+
+
+
Australian walks 100km after outback crash.
+
+
+
Man charged over missing wedding girl.
+
+
+
Los Angeles battles huge wildfires.
+
+
+
+
+
+
Small Size
+
+
+
+
+
+
Racing car sprays burning fuel into crowd.
+
+
+
Japanese princess to wed commoner.
+
+
+
Australian walks 100km after outback crash.
+
+
+
Man charged over missing wedding girl.
+
+
+
Los Angeles battles huge wildfires.
+
+
+
+
+
+
Large Size
+
+
+
+
+
+
Racing car sprays burning fuel into crowd.
+
+
+
Japanese princess to wed commoner.
+
+
+
Australian walks 100km after outback crash.
+
+
+
Man charged over missing wedding girl.
+
+
+
Los Angeles battles huge wildfires.
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/list/demo/vertical.md correctly 1`] = `
+
+`;
diff --git a/components/list/__tests__/demo.test.js b/components/list/__tests__/demo.test.js
new file mode 100644
index 000000000..fb75d12a3
--- /dev/null
+++ b/components/list/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest'
+
+demoTest('list')
diff --git a/components/list/demo/basic.md b/components/list/demo/basic.md
new file mode 100644
index 000000000..82560b895
--- /dev/null
+++ b/components/list/demo/basic.md
@@ -0,0 +1,53 @@
+
+#### 基础列表
+基础列表。
+
+
+
+#### Basic list
+Basic list.
+
+
+```html
+
+
+
+
+ {{item.title}}
+
+
+
+
+
+
+
+```
diff --git a/components/list/demo/grid.md b/components/list/demo/grid.md
new file mode 100644
index 000000000..6904cfc51
--- /dev/null
+++ b/components/list/demo/grid.md
@@ -0,0 +1,48 @@
+
+#### 栅格列表
+可以通过设置 `List` 的 `grid` 属性来实现栅格列表,`column` 可设置期望显示的列数。
+
+
+
+#### Grid
+Creating a grid list by setting the `grid` property of List
+
+
+```html
+
+
+
+ Card content
+
+
+
+
+
+```
diff --git a/components/list/demo/index.vue b/components/list/demo/index.vue
new file mode 100644
index 000000000..043c5e56f
--- /dev/null
+++ b/components/list/demo/index.vue
@@ -0,0 +1,57 @@
+
diff --git a/components/list/demo/infinite-load.md b/components/list/demo/infinite-load.md
new file mode 100644
index 000000000..c680a8ea2
--- /dev/null
+++ b/components/list/demo/infinite-load.md
@@ -0,0 +1,95 @@
+
+#### 滚动加载
+结合 [vue-infinite-scroll](https://github.com/ElemeFE/vue-infinite-scroll) 实现滚动自动加载列表。
+
+
+
+#### Scrolling loaded
+The example of infinite load with [vue-infinite-scroll](https://github.com/ElemeFE/vue-infinite-scroll).
+
+
+```html
+
+
+
+
+
+```
diff --git a/components/list/demo/infinite-virtualized-load.md b/components/list/demo/infinite-virtualized-load.md
new file mode 100644
index 000000000..141761665
--- /dev/null
+++ b/components/list/demo/infinite-virtualized-load.md
@@ -0,0 +1,95 @@
+
+#### 滚动加载无限长列表
+结合 [vue-virtual-scroller](https://github.com/Akryum/vue-virtual-scroller) 实现滚动加载无限长列表,带有虚拟化([virtualization](https://blog.jscrambler.com/optimizing-react-rendering-through-virtualization/))功能,能够提高数据量大时候长列表的性能。
+可以结合 [vue-infinite-scroll](https://github.com/ElemeFE/vue-infinite-scroll) 实现滚动自动加载无限长列表。
+`virtualized` 是在大数据列表中应用的一种技术,主要是为了减少不可见区域不必要的渲染从而提高性能,特别是数据量在成千上万条效果尤为明显。
+
+
+
+#### Infinite & virtualized
+An example of infinite list & virtualized loading using [vue-virtual-scroller](https://github.com/Akryum/vue-virtual-scroller).
+`Virtualized` rendering is a technique to mount big sets of data. It reduces the amount of rendered DOM nodes by tracking and hiding whatever isn't currently visible.
+
+
+```html
+
+
+
+
+
+ {{item.name.last}}
+
+
+ Content {{item.index}}
+
+
+
+
+
+
+
+```
diff --git a/components/list/demo/loadmore.md b/components/list/demo/loadmore.md
new file mode 100644
index 000000000..672f8ca55
--- /dev/null
+++ b/components/list/demo/loadmore.md
@@ -0,0 +1,86 @@
+
+#### 加载更多
+可通过 `loadMore` 属性实现加载更多功能。
+
+
+
+#### Load more
+Load more list with `loadMore` property.
+
+
+```html
+
+
+
+
+ edit
+ more
+
+ {{item.name.last}}
+
+
+ content
+
+
+
+
+
+```
diff --git a/components/list/demo/resposive.md b/components/list/demo/resposive.md
new file mode 100644
index 000000000..00a5bdd70
--- /dev/null
+++ b/components/list/demo/resposive.md
@@ -0,0 +1,55 @@
+
+#### 响应式的栅格列表
+响应式的栅格列表。尺寸与 [Layout Grid](https://vuecomponent.github.io/ant-design/components/grid-cn/#Col) 保持一致。
+
+
+
+#### Responsive grid list
+Responsive grid list. The size property is as same as [Layout Grid](https://vuecomponent.github.io/ant-design/components/grid/#Col).
+
+
+```html
+
+
+
+ Card content
+
+
+
+
+
+```
diff --git a/components/list/demo/simple.md b/components/list/demo/simple.md
new file mode 100644
index 000000000..76324fb9f
--- /dev/null
+++ b/components/list/demo/simple.md
@@ -0,0 +1,69 @@
+
+#### 简单列表
+列表拥有大、中、小三种尺寸。
+通过设置 `size` 为 `large` `small` 分别把按钮设为大、小尺寸。若不设置 `size`,则尺寸为中。
+可通过设置 `header` 和 `footer`,来自定义列表头部和尾部。
+
+
+
+#### Simple list
+Ant Design supports a default list size as well as a large and small size.
+If a large or small list is desired, set the size property to either large or small respectively. Omit the size property for a list with the default size.
+Customizing the header and footer of list by setting `header` and `footer` property.
+
+
+```html
+
+
+
Default Size
+
+ {{item}}
+ Header
+ Footer
+
+
Small Size
+
+ {{item}}
+ Header
+ Footer
+
+
Large Size
+
+ {{item}}
+ Header
+ Footer
+
+
+
+
+
+```
diff --git a/components/list/demo/vertical.md b/components/list/demo/vertical.md
new file mode 100644
index 000000000..c631b2c10
--- /dev/null
+++ b/components/list/demo/vertical.md
@@ -0,0 +1,73 @@
+
+#### 竖排列表样式
+通过设置 `itemLayout` 属性为 `vertical` 可实现竖排列表样式。
+
+
+
+#### Vertical
+Setting `itemLayout` property with `vertical` to create a vertical list.
+
+
+```html
+
+
+
+
+
+
+ {{text}}
+
+
+
+
+ {{item.title}}
+
+
+ {{item.content}}
+
+
+
+
+
+```
diff --git a/components/list/index.en-US.md b/components/list/index.en-US.md
new file mode 100644
index 000000000..76b468dbc
--- /dev/null
+++ b/components/list/index.en-US.md
@@ -0,0 +1,46 @@
+
+## API
+
+### List
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| bordered | Toggles rendering of the border around the list | boolean | false |
+| footer | List footer renderer | string\|slot | - |
+| grid | The grid type of list. You can set grid to something like {gutter: 16, column: 4} | object | - |
+| header | List header renderer | string\|slot | - |
+| itemLayout | The layout of list, default is `horizontal`, If a vertical list is desired, set the itemLayout property to `vertical` | string | - |
+| loading | Shows a loading indicator while the contents of the list are being fetched | boolean\|[object](https://vuecomponent.github.io/ant-design/components/spin/#API) | false |
+| loadMore | Shows a load more content | string\|slot | - |
+| pagination | Pagination [config](https://vuecomponent.github.io/ant-design/components/pagination/#API), hide it by setting it to false | boolean \| object | false |
+| split | Toggles rendering of the split under the list item | boolean | true |
+| renderItem | Custom item renderer, slot="renderItem" and slot-scope="item, index" | (item, index) => vNode | | - |
+
+### List grid props
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| column | column of grid | number | - |
+| gutter | spacing between grid | number | 0 |
+| size | Size of list | `default` \| `middle` \| `small` | `default` |
+| xs | `<576px` column of grid | number | - |
+| sm | `≥576px` column of grid | number | - |
+| md | `≥768px` column of grid | number | - |
+| lg | `≥992px` column of grid | number | - |
+| xl | `≥1200px` column of grid | number | - |
+| xxl | `≥1600px` column of grid | number | - |
+
+### List.Item
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| actions | The actions content of list item. If `itemLayout` is `vertical`, shows the content on bottom, otherwise shows content on the far right. | Array\\|slot | - |
+| extra | The extra content of list item. If `itemLayout` is `vertical`, shows the content on right, otherwise shows content on the far right. | string\|slot | - |
+
+### List.Item.Meta
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| avatar | The avatar of list item | slot | - |
+| description | The description of list item | string\|slot | - |
+| title | The title of list item | string\|slot | - |
diff --git a/components/list/index.jsx b/components/list/index.jsx
new file mode 100644
index 000000000..7e0c04f14
--- /dev/null
+++ b/components/list/index.jsx
@@ -0,0 +1,249 @@
+import PropTypes from '../_util/vue-types'
+import classNames from 'classnames'
+import omit from 'omit.js'
+import { SpinProps } from '../spin'
+import LocaleReceiver from '../locale-provider/LocaleReceiver'
+import defaultLocale from '../locale-provider/default'
+
+import Spin from '../spin'
+import Pagination from '../pagination'
+import { Row } from '../grid'
+
+import Item from './Item'
+import { initDefaultProps, getComponentFromProp } from '../_util/props-util'
+import { cloneElement } from '../_util/vnode'
+
+export { ListItemProps, ListItemMetaProps } from './Item'
+
+export const ColumnCount = ['', 1, 2, 3, 4, 6, 8, 12, 24]
+
+export const ColumnType = ['gutter', 'column', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']
+
+export const ListGridType = {
+ gutter: PropTypes.number,
+ column: PropTypes.oneOf(ColumnCount),
+ xs: PropTypes.oneOf(ColumnCount),
+ sm: PropTypes.oneOf(ColumnCount),
+ md: PropTypes.oneOf(ColumnCount),
+ lg: PropTypes.oneOf(ColumnCount),
+ xl: PropTypes.oneOf(ColumnCount),
+ xxl: PropTypes.oneOf(ColumnCount),
+}
+
+export const ListSize = ['small', 'default', 'large']
+
+export const ListProps = () => ({
+ bordered: PropTypes.bool,
+ dataSource: PropTypes.any,
+ extra: PropTypes.any,
+ grid: PropTypes.shape(ListGridType).loose,
+ itemLayout: PropTypes.string,
+ loading: PropTypes.oneOfType([PropTypes.bool, SpinProps()]),
+ loadMore: PropTypes.any,
+ pagination: PropTypes.any,
+ prefixCls: PropTypes.string,
+ rowKey: PropTypes.any,
+ renderItem: PropTypes.any,
+ size: PropTypes.oneOf(ListSize),
+ split: PropTypes.bool,
+ header: PropTypes.any,
+ footer: PropTypes.any,
+ locale: PropTypes.object,
+})
+
+export default {
+ Item,
+ name: 'AList',
+
+ props: initDefaultProps(ListProps(), {
+ dataSource: [],
+ prefixCls: 'ant-list',
+ bordered: false,
+ split: true,
+ loading: false,
+ pagination: false,
+ }),
+ provide () {
+ return {
+ listContext: this,
+ }
+ },
+ data () {
+ this.keys = []
+ this.defaultPaginationProps = {
+ current: 1,
+ pageSize: 10,
+ onChange: (page, pageSize) => {
+ const { pagination } = this
+ this.paginationCurrent = page
+ if (pagination && pagination.onChange) {
+ pagination.onChange(page, pageSize)
+ }
+ },
+ total: 0,
+ }
+ return {
+ paginationCurrent: 1,
+ }
+ },
+ methods: {
+ renderItem2 (item, index) {
+ const { dataSource, $scopedSlots, rowKey } = this
+ let key
+ const renderItem = this.renderItem || $scopedSlots.renderItem
+ if (typeof rowKey === 'function') {
+ key = rowKey(dataSource[index])
+ } else if (typeof rowKey === 'string') {
+ key = dataSource[rowKey]
+ } else {
+ key = dataSource.key
+ }
+
+ if (!key) {
+ key = `list-item-${index}`
+ }
+
+ this.keys[index] = key
+
+ return renderItem(item, index)
+ },
+
+ isSomethingAfterLastTtem () {
+ const { pagination } = this
+ const loadMore = getComponentFromProp(this, 'loadMore')
+ const footer = getComponentFromProp(this, 'footer')
+ return !!(loadMore || pagination || footer)
+ },
+
+ renderEmpty (contextLocale) {
+ const locale = { ...contextLocale, ...this.locale }
+ return {locale.emptyText}
+ },
+ },
+
+ render () {
+ const {
+ bordered,
+ split,
+ itemLayout,
+ pagination,
+ prefixCls,
+ grid,
+ dataSource,
+ size,
+ loading,
+ $listeners,
+ $slots,
+ paginationCurrent,
+ } = this
+ const loadMore = getComponentFromProp(this, 'loadMore')
+ const footer = getComponentFromProp(this, 'footer')
+ const header = getComponentFromProp(this, 'header')
+ const children = $slots.default || []
+ let loadingProp = loading
+ if (typeof loadingProp === 'boolean') {
+ loadingProp = {
+ spinning: loadingProp,
+ }
+ }
+ const isLoading = (loadingProp && loadingProp.spinning)
+
+ // large => lg
+ // small => sm
+ let sizeCls = ''
+ switch (size) {
+ case 'large':
+ sizeCls = 'lg'
+ break
+ case 'small':
+ sizeCls = 'sm'
+ break
+ default:
+ break
+ }
+ const classString = classNames(prefixCls, {
+ [`${prefixCls}-vertical`]: itemLayout === 'vertical',
+ [`${prefixCls}-${sizeCls}`]: sizeCls,
+ [`${prefixCls}-split`]: split,
+ [`${prefixCls}-bordered`]: bordered,
+ [`${prefixCls}-loading`]: isLoading,
+ [`${prefixCls}-grid`]: grid,
+ [`${prefixCls}-something-after-last-item`]: this.isSomethingAfterLastTtem(),
+ })
+ const paginationProps = {
+ ...this.defaultPaginationProps,
+ total: dataSource.length,
+ current: paginationCurrent,
+ ...pagination || {},
+ }
+ const largestPage = Math.ceil(
+ paginationProps.total / paginationProps.pageSize,
+ )
+ if (paginationProps.current > largestPage) {
+ paginationProps.current = largestPage
+ }
+ const { class: cls, style, onShowSizeChange = () => {}, ...restProps } = paginationProps
+ const paginationContent = pagination ? (
+
+ ) : null
+
+ let splitDataSource = [...dataSource]
+ if (pagination) {
+ if (
+ dataSource.length >
+ (paginationProps.current - 1) * paginationProps.pageSize
+ ) {
+ splitDataSource = [...dataSource].splice(
+ (paginationProps.current - 1) * paginationProps.pageSize,
+ paginationProps.pageSize,
+ )
+ }
+ }
+
+ let childrenContent
+ childrenContent = isLoading && ()
+ if (splitDataSource.length > 0) {
+ const items = splitDataSource.map((item, index) => this.renderItem2(item, index))
+ const childrenList = items.map((child, index) => cloneElement(child, {
+ key: this.keys[index],
+ }),
+ )
+
+ childrenContent = grid ? (
+ {childrenList}
+ ) : childrenList
+ } else if (!children && !isLoading) {
+ childrenContent = (
+
+ )
+ }
+ const paginationPosition = paginationProps.position || 'bottom'
+
+ return (
+
+ {(paginationPosition === 'top' || paginationPosition === 'both') && paginationContent}
+ {header && }
+
+ {childrenContent}
+ {children}
+
+ {footer && }
+ {loadMore || (paginationPosition === 'bottom' || paginationPosition === 'both') && paginationContent}
+
+ )
+ },
+}
diff --git a/components/list/index.zh-CN.md b/components/list/index.zh-CN.md
new file mode 100644
index 000000000..e79d29fa7
--- /dev/null
+++ b/components/list/index.zh-CN.md
@@ -0,0 +1,46 @@
+
+## API
+
+### List
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| bordered | 是否展示边框 | boolean | false |
+| footer | 列表底部 | string\|slot | - |
+| grid | 列表栅格配置 | object | - |
+| header | 列表头部 | string\|slot | - |
+| itemLayout | 设置 `List.Item` 布局, 设置成 `vertical` 则竖直样式显示, 默认横排 | string | - |
+| loading | 当卡片内容还在加载中时,可以用 `loading` 展示一个占位 | boolean\|[object](https://vuecomponent.github.io/ant-design/components/spin-cn/#API) | false |
+| loadMore | 加载更多 | string\|slot | - |
+| pagination | 对应的 `pagination` [配置]((https://vuecomponent.github.io/ant-design/components/pagination-cn/#API)), 设置 `false` 不显示 | boolean\|object | false |
+| size | list 的尺寸 | `default` \| `middle` \| `small` | `default` |
+| split | 是否展示分割线 | boolean | true |
+| renderItem | 自定义`Item`函数,也可使用slot="renderItem" 和 slot-scope="item, index" | (item, index) => vNode | | - |
+
+### List grid props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| column | 列数 | number | - |
+| gutter | 栅格间隔 | number | 0 |
+| xs | `<576px` 展示的列数 | number | - |
+| sm | `≥576px` 展示的列数 | number | - |
+| md | `≥768px` 展示的列数 | number | - |
+| lg | `≥992px` 展示的列数 | number | - |
+| xl | `≥1200px` 展示的列数 | number | - |
+| xxl | `≥1600px` 展示的列数 | number | - |
+
+### List.Item
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| actions | 列表操作组,根据 `itemLayout` 的不同, 位置在卡片底部或者最右侧 | Array\/|slot | - |
+| extra | 额外内容, 通常用在 `itemLayout` 为 `vertical` 的情况下, 展示右侧内容; `horizontal` 展示在列表元素最右侧 | string\|slot | - |
+
+### List.Item.Meta
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| avatar | 列表元素的图标 | slot | - |
+| description | 列表元素的描述内容 | string\|slot | - |
+| title | 列表元素的标题 | string\|slot | - |
diff --git a/components/list/style/bordered.less b/components/list/style/bordered.less
new file mode 100644
index 000000000..e3175b8a6
--- /dev/null
+++ b/components/list/style/bordered.less
@@ -0,0 +1,41 @@
+.@{list-prefix-cls}-bordered {
+ border-radius: @border-radius-base;
+ border: 1px solid @border-color-base;
+ .@{list-prefix-cls}-header {
+ padding-left: 24px;
+ padding-right: 24px;
+ }
+
+ .@{list-prefix-cls}-footer {
+ padding-left: 24px;
+ padding-right: 24px;
+ }
+
+ .@{list-prefix-cls}-item {
+ border-bottom: 1px solid @border-color-split;
+ padding-left: 24px;
+ padding-right: 24px;
+ }
+
+ .@{list-prefix-cls}-pagination {
+ margin: 16px 24px;
+ }
+
+ &.@{list-prefix-cls}-sm {
+ .@{list-prefix-cls}-item {
+ padding-left: 16px;
+ padding-right: 16px;
+ }
+ .@{list-prefix-cls}-header,
+ .@{list-prefix-cls}-footer {
+ padding: 8px 16px;
+ }
+ }
+
+ &.@{list-prefix-cls}-lg {
+ .@{list-prefix-cls}-header,
+ .@{list-prefix-cls}-footer {
+ padding: 16px 24px;
+ }
+ }
+}
diff --git a/components/list/style/index.js b/components/list/style/index.js
new file mode 100644
index 000000000..c76d8c508
--- /dev/null
+++ b/components/list/style/index.js
@@ -0,0 +1,7 @@
+import '../../style/index.less'
+import './index.less'
+
+// style dependencies
+import '../../spin/style'
+import '../../pagination/style'
+import '../../grid/style'
diff --git a/components/list/style/index.less b/components/list/style/index.less
new file mode 100644
index 000000000..11e55b6c9
--- /dev/null
+++ b/components/list/style/index.less
@@ -0,0 +1,207 @@
+@import "../../style/themes/default";
+@import "../../style/mixins/index";
+
+@list-prefix-cls: ~"@{ant-prefix}-list";
+
+.@{list-prefix-cls} {
+ .reset-component;
+ position: relative;
+ * {
+ outline: none;
+ }
+ &-pagination {
+ margin-top: 24px;
+ text-align: right;
+ }
+ &-more {
+ margin-top: 12px;
+ text-align: center;
+ button {
+ padding-left: 32px;
+ padding-right: 32px;
+ }
+ }
+ &-spin {
+ text-align: center;
+ min-height: 40px;
+ }
+ &-empty-text {
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ padding: 16px;
+ text-align: center;
+ }
+ &-item {
+ align-items: center;
+ display: flex;
+ padding-top: 12px;
+ padding-bottom: 12px;
+ &-meta {
+ align-items: flex-start;
+ display: flex;
+ flex: 1;
+ font-size: 0;
+ &-avatar {
+ margin-right: 16px;
+ }
+ &-content {
+ flex: 1 0;
+ }
+ &-title {
+ color: @text-color;
+ margin-bottom: 4px;
+ font-size: @font-size-base;
+ line-height: 22px;
+ > a {
+ color: @text-color;
+ transition: all .3s;
+ &:hover {
+ color: @primary-color;
+ }
+ }
+ }
+ &-description {
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ }
+ &-content {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ &-content-single {
+ justify-content: flex-start;
+ }
+ &-action {
+ font-size: 0;
+ flex: 0 0 auto;
+ margin-left: 48px;
+ padding: 0;
+ list-style: none;
+ & > li {
+ display: inline-block;
+ color: @text-color-secondary;
+ cursor: pointer;
+ padding: 0 8px;
+ position: relative;
+ font-size: @font-size-base;
+ line-height: 22px;
+ text-align: center;
+ }
+ & > li:first-child {
+ padding-left: 0;
+ }
+ &-split {
+ background-color: @border-color-split;
+ margin-top: -7px;
+ position: absolute;
+ top: 50%;
+ right: 0;
+ width: 1px;
+ height: 14px;
+ }
+ }
+ &-main {
+ display: flex;
+ flex: 1;
+ }
+ }
+
+ &-header,
+ &-footer {
+ padding-top: 12px;
+ padding-bottom: 12px;
+ }
+
+ &-empty {
+ color: @text-color-secondary;
+ padding: 16px 0;
+ font-size: 12px;
+ text-align: center;
+ }
+
+ &-split &-item {
+ border-bottom: 1px solid @border-color-split;
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ &-split &-header {
+ border-bottom: 1px solid @border-color-split;
+ }
+
+ &-loading &-spin-nested-loading {
+ min-height: 32px;
+ }
+
+ &-something-after-last-item &-item:last-child {
+ border-bottom: 1px solid @border-color-split;
+ }
+
+ &-lg &-item {
+ padding-top: 16px;
+ padding-bottom: 16px;
+ }
+
+ &-sm &-item {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+
+ &-vertical &-item {
+ display: block;
+ &-extra-wrap {
+ display: flex;
+ }
+ &-main {
+ display: block;
+ flex: 1;
+ }
+ &-extra {
+ margin-left: 58px;
+ }
+ &-meta {
+ margin-bottom: 16px;
+ &-avatar {
+ display: none;
+ }
+ &-title {
+ color: @heading-color;
+ margin-bottom: 12px;
+ font-size: @font-size-lg;
+ line-height: 24px;
+ }
+ }
+ &-content {
+ display: block;
+ color: @text-color;
+ font-size: @font-size-base;
+ margin-bottom: 16px;
+ }
+ &-action {
+ margin-left: auto;
+ > li {
+ padding: 0 16px;
+ &:first-child {
+ padding-left: 0;
+ }
+ }
+ }
+ }
+
+ &-grid &-item {
+ border-bottom: none;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-bottom: 20px;
+ &-content {
+ display: block;
+ }
+ }
+}
+
+@import './bordered';
+@import './responsive';
diff --git a/components/list/style/responsive.less b/components/list/style/responsive.less
new file mode 100644
index 000000000..a1eba05ca
--- /dev/null
+++ b/components/list/style/responsive.less
@@ -0,0 +1,42 @@
+@media screen and (max-width: @screen-md) {
+ .@{list-prefix-cls} {
+ &-item {
+ &-action {
+ margin-left: 24px;
+ }
+ }
+ }
+
+ .@{list-prefix-cls}-vertical {
+ .@{list-prefix-cls}-item {
+ &-extra {
+ margin-left: 24px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-xs) {
+ .@{list-prefix-cls} {
+ &-item {
+ flex-wrap: wrap;
+ &-action {
+ margin-left: 12px;
+ }
+ }
+ }
+
+ .@{list-prefix-cls}-vertical {
+ .@{list-prefix-cls}-item {
+ &-extra-wrap {
+ flex-wrap: wrap-reverse;
+ }
+ &-main {
+ min-width: 220px;
+ }
+ &-extra {
+ margin-left: 0;
+ }
+ }
+ }
+}
diff --git a/components/style.js b/components/style.js
index b2e783035..640ee4c18 100644
--- a/components/style.js
+++ b/components/style.js
@@ -45,3 +45,4 @@ import './upload/style'
import './layout/style'
import './form/style'
import './anchor/style'
+import './list/style'
diff --git a/site/components.js b/site/components.js
index 6422abf9b..8b262e351 100644
--- a/site/components.js
+++ b/site/components.js
@@ -24,7 +24,7 @@ import {
Input,
InputNumber,
Layout,
- // List,
+ List,
LocaleProvider,
message,
Menu,
@@ -100,7 +100,9 @@ Vue.component(Layout.Header.name, Layout.Header)
Vue.component(Layout.Footer.name, Layout.Footer)
Vue.component(Layout.Sider.name, Layout.Sider)
Vue.component(Layout.Content.name, Layout.Content)
-// Vue.component(List.name, List)
+Vue.component(List.name, List)
+Vue.component(List.Item.name, List.Item)
+Vue.component(List.Item.Meta.name, List.Item.Meta)
Vue.component(LocaleProvider.name, LocaleProvider)
Vue.component(Menu.name, Menu)
Vue.component(Menu.Item.name, Menu.Item)
diff --git a/site/demo.js b/site/demo.js
index 23694d32d..3e028ce0f 100644
--- a/site/demo.js
+++ b/site/demo.js
@@ -46,3 +46,4 @@ export { default as tree } from 'antd/tree/demo/index.vue'
export { default as layout } from 'antd/layout/demo/index.vue'
export { default as form } from 'antd/form/demo/index.vue'
export { default as anchor } from 'antd/anchor/demo/index.vue'
+export { default as list } from 'antd/list/demo/index.vue'
diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap
index 9adcc0cc1..be8c0de07 100644
--- a/tests/__snapshots__/index.test.js.snap
+++ b/tests/__snapshots__/index.test.js.snap
@@ -29,6 +29,7 @@ Array [
"Input",
"InputNumber",
"Layout",
+ "List",
"LocaleProvider",
"Menu",
"Modal",