refactor: card

refactor-tabs
tangjinzhou 2021-10-06 22:02:48 +08:00
parent fd27c65507
commit 28212b821e
13 changed files with 286 additions and 267 deletions

View File

@ -1,46 +1,47 @@
import type { VNodeTypes, PropType, VNode, ExtractPropTypes } from 'vue'; import type { VNodeTypes, PropType, VNode, ExtractPropTypes } from 'vue';
import { inject, isVNode, defineComponent } from 'vue'; import { isVNode, defineComponent, renderSlot } from 'vue';
import { tuple } from '../_util/type';
import Tabs from '../tabs'; import Tabs from '../tabs';
import Row from '../row'; import Row from '../row';
import Col from '../col'; import Col from '../col';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getComponent, getSlot, isEmptyElement } from '../_util/props-util'; import { flattenChildren, isEmptyElement } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin'; import BaseMixin from '../_util/BaseMixin';
import type { SizeType } from '../config-provider'; import type { SizeType } from '../config-provider';
import { defaultConfigProvider } from '../config-provider';
import isPlainObject from 'lodash-es/isPlainObject'; import isPlainObject from 'lodash-es/isPlainObject';
import useConfigInject from '../_util/hooks/useConfigInject';
import devWarning from '../vc-util/devWarning';
export interface CardTabListType { export interface CardTabListType {
key: string; key: string;
tab: VNodeTypes; tab: any;
/** @deprecated Please use `customTab` instead. */
slots?: { tab: string }; slots?: { tab: string };
disabled?: boolean; disabled?: boolean;
} }
export type CardType = 'inner'; export type CardType = 'inner';
export type CardSize = 'default' | 'small';
const { TabPane } = Tabs; const { TabPane } = Tabs;
const cardProps = () => ({ const cardProps = () => ({
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
title: PropTypes.VNodeChild, title: PropTypes.any,
extra: PropTypes.VNodeChild, extra: PropTypes.any,
bordered: PropTypes.looseBool.def(true), bordered: PropTypes.looseBool.def(true),
bodyStyle: PropTypes.style, bodyStyle: PropTypes.style,
headStyle: PropTypes.style, headStyle: PropTypes.style,
loading: PropTypes.looseBool.def(false), loading: PropTypes.looseBool.def(false),
hoverable: PropTypes.looseBool.def(false), hoverable: PropTypes.looseBool.def(false),
type: PropTypes.string, type: { type: String as PropType<CardType> },
size: PropTypes.oneOf(tuple('default', 'small')), size: { type: String as PropType<CardSize> },
actions: PropTypes.VNodeChild, actions: PropTypes.any,
tabList: { tabList: {
type: Array as PropType<CardTabListType[]>, type: Array as PropType<CardTabListType[]>,
}, },
tabBarExtraContent: PropTypes.VNodeChild, tabBarExtraContent: PropTypes.any,
activeTabKey: PropTypes.string, activeTabKey: PropTypes.string,
defaultActiveTabKey: PropTypes.string, defaultActiveTabKey: PropTypes.string,
cover: PropTypes.VNodeChild, cover: PropTypes.any,
onTabChange: { onTabChange: {
type: Function as PropType<(key: string) => void>, type: Function as PropType<(key: string) => void>,
}, },
@ -52,18 +53,10 @@ const Card = defineComponent({
name: 'ACard', name: 'ACard',
mixins: [BaseMixin], mixins: [BaseMixin],
props: cardProps(), props: cardProps(),
setup() { slots: ['title', 'extra', 'tabBarExtraContent', 'actions', 'cover', 'customTab'],
return { setup(props, { slots }) {
configProvider: inject('configProvider', defaultConfigProvider), const { prefixCls, direction, size } = useConfigInject('card', props);
}; const getAction = (actions: VNodeTypes[]) => {
},
data() {
return {
widerPadding: false,
};
},
methods: {
getAction(actions: VNodeTypes[]) {
const actionList = actions.map((action, index) => const actionList = actions.map((action, index) =>
(isVNode(action) && !isEmptyElement(action)) || !isVNode(action) ? ( (isVNode(action) && !isEmptyElement(action)) || !isVNode(action) ? (
<li style={{ width: `${100 / actions.length}%` }} key={`action-${index}`}> <li style={{ width: `${100 / actions.length}%` }} key={`action-${index}`}>
@ -72,11 +65,11 @@ const Card = defineComponent({
) : null, ) : null,
); );
return actionList; return actionList;
}, };
triggerTabChange(key: string) { const triggerTabChange = (key: string) => {
this.$emit('tabChange', key); props.onTabChange?.(key);
}, };
isContainGrid(obj: VNode[] = []) { const isContainGrid = (obj: VNode[] = []) => {
let containGrid: boolean; let containGrid: boolean;
obj.forEach(element => { obj.forEach(element => {
if (element && isPlainObject(element.type) && (element.type as any).__ANT_CARD_GRID) { if (element && isPlainObject(element.type) && (element.type as any).__ANT_CARD_GRID) {
@ -84,83 +77,64 @@ const Card = defineComponent({
} }
}); });
return containGrid; return containGrid;
}, };
},
render() { return () => {
const { const {
prefixCls: customizePrefixCls,
headStyle = {}, headStyle = {},
bodyStyle = {}, bodyStyle = {},
loading, loading,
bordered = true, bordered = true,
size = 'default',
type, type,
tabList, tabList,
hoverable, hoverable,
activeTabKey, activeTabKey,
defaultActiveTabKey, defaultActiveTabKey,
} = this.$props; tabBarExtraContent = slots.tabBarExtraContent?.(),
const { $slots } = this; title = slots.title?.(),
const children = getSlot(this); extra = slots.extra?.(),
const { getPrefixCls } = this.configProvider; actions = slots.actions?.(),
const prefixCls = getPrefixCls('card', customizePrefixCls); cover = slots.cover?.(),
} = props;
const tabBarExtraContent = getComponent(this, 'tabBarExtraContent'); const children = flattenChildren(slots.default?.());
const pre = prefixCls.value;
const classString = { const classString = {
[`${prefixCls}`]: true, [`${pre}`]: true,
[`${prefixCls}-loading`]: loading, [`${pre}-loading`]: loading,
[`${prefixCls}-bordered`]: bordered, [`${pre}-bordered`]: bordered,
[`${prefixCls}-hoverable`]: !!hoverable, [`${pre}-hoverable`]: !!hoverable,
[`${prefixCls}-contain-grid`]: this.isContainGrid(children), [`${pre}-contain-grid`]: isContainGrid(children),
[`${prefixCls}-contain-tabs`]: tabList && tabList.length, [`${pre}-contain-tabs`]: tabList && tabList.length,
[`${prefixCls}-${size}`]: size !== 'default', [`${pre}-${size.value}`]: size.value,
[`${prefixCls}-type-${type}`]: !!type, [`${pre}-type-${type}`]: !!type,
[`${pre}-rtl`]: direction.value === 'rtl',
}; };
const loadingBlockStyle = const loadingBlockStyle =
bodyStyle.padding === 0 || bodyStyle.padding === '0px' ? { padding: 24 } : undefined; bodyStyle.padding === 0 || bodyStyle.padding === '0px' ? { padding: '24px' } : undefined;
const block = <div class={`${pre}-loading-block`} />;
const loadingBlock = ( const loadingBlock = (
<div class={`${prefixCls}-loading-content`} style={loadingBlockStyle}> <div class={`${pre}-loading-content`} style={loadingBlockStyle}>
<Row gutter={8}> <Row gutter={8}>
<Col span={22}> <Col span={22}>{block}</Col>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row> </Row>
<Row gutter={8}> <Row gutter={8}>
<Col span={8}> <Col span={8}>{block}</Col>
<div class={`${prefixCls}-loading-block`} /> <Col span={15}>{block}</Col>
</Col>
<Col span={15}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row> </Row>
<Row gutter={8}> <Row gutter={8}>
<Col span={6}> <Col span={6}>{block}</Col>
<div class={`${prefixCls}-loading-block`} /> <Col span={18}>{block}</Col>
</Col>
<Col span={18}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row> </Row>
<Row gutter={8}> <Row gutter={8}>
<Col span={13}> <Col span={13}>{block}</Col>
<div class={`${prefixCls}-loading-block`} /> <Col span={9}>{block}</Col>
</Col>
<Col span={9}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row> </Row>
<Row gutter={8}> <Row gutter={8}>
<Col span={4}> <Col span={4}>{block}</Col>
<div class={`${prefixCls}-loading-block`} /> <Col span={3}>{block}</Col>
</Col> <Col span={16}>{block}</Col>
<Col span={3}>
<div class={`${prefixCls}-loading-block`} />
</Col>
<Col span={16}>
<div class={`${prefixCls}-loading-block`} />
</Col>
</Row> </Row>
</div> </div>
); );
@ -171,58 +145,61 @@ const Card = defineComponent({
[hasActiveTabKey ? 'activeKey' : 'defaultActiveKey']: hasActiveTabKey [hasActiveTabKey ? 'activeKey' : 'defaultActiveKey']: hasActiveTabKey
? activeTabKey ? activeTabKey
: defaultActiveTabKey, : defaultActiveTabKey,
tabBarExtraContent, onChange: triggerTabChange,
onChange: this.triggerTabChange, class: `${pre}-head-tabs`,
class: `${prefixCls}-head-tabs`,
}; };
let head; let head;
const tabs = const tabs =
tabList && tabList.length ? ( tabList && tabList.length ? (
<Tabs {...tabsProps}> <Tabs
{...tabsProps}
v-slots={{ rightExtra: tabBarExtraContent ? () => tabBarExtraContent : null }}
>
{tabList.map(item => { {tabList.map(item => {
const { tab: temp, slots } = item as CardTabListType; const { tab: temp, slots: itemSlots } = item as CardTabListType;
const name = slots?.tab; const name = itemSlots?.tab;
const tab = temp !== undefined ? temp : $slots[name] ? $slots[name](item) : null; devWarning(
!itemSlots,
'Card',
`tabList slots is deprecated, Please use \`customTab\` instead.`,
);
let tab = temp !== undefined ? temp : slots[name] ? slots[name](item) : null;
tab = renderSlot(slots, 'customTab', item as any, () => [tab]);
return <TabPane tab={tab} key={item.key} disabled={item.disabled} />; return <TabPane tab={tab} key={item.key} disabled={item.disabled} />;
})} })}
</Tabs> </Tabs>
) : null; ) : null;
const titleDom = getComponent(this, 'title'); if (title || extra || tabs) {
const extraDom = getComponent(this, 'extra');
if (titleDom || extraDom || tabs) {
head = ( head = (
<div class={`${prefixCls}-head`} style={headStyle}> <div class={`${pre}-head`} style={headStyle}>
<div class={`${prefixCls}-head-wrapper`}> <div class={`${pre}-head-wrapper`}>
{titleDom && <div class={`${prefixCls}-head-title`}>{titleDom}</div>} {title && <div class={`${pre}-head-title`}>{title}</div>}
{extraDom && <div class={`${prefixCls}-extra`}>{extraDom}</div>} {extra && <div class={`${pre}-extra`}>{extra}</div>}
</div> </div>
{tabs} {tabs}
</div> </div>
); );
} }
const cover = getComponent(this, 'cover'); const coverDom = cover ? <div class={`${pre}-cover`}>{cover}</div> : null;
const coverDom = cover ? <div class={`${prefixCls}-cover`}>{cover}</div> : null;
const body = ( const body = (
<div class={`${prefixCls}-body`} style={bodyStyle}> <div class={`${pre}-body`} style={bodyStyle}>
{loading ? loadingBlock : children} {loading ? loadingBlock : children}
</div> </div>
); );
const actions = getComponent(this, 'actions');
const actionDom = const actionDom =
actions && actions.length ? ( actions && actions.length ? <ul class={`${pre}-actions`}>{getAction(actions)}</ul> : null;
<ul class={`${prefixCls}-actions`}>{this.getAction(actions)}</ul>
) : null;
return ( return (
<div class={classString} ref="cardContainerRef"> <div class={classString} ref="cardContainerRef">
{head} {head}
{coverDom} {coverDom}
{children ? body : null} {children && children.length ? body : null}
{actionDom} {actionDom}
</div> </div>
); );
};
}, },
}); });

View File

@ -1,30 +1,23 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, computed } from 'vue';
import PropTypes from '../_util/vue-types'; import useConfigInject from '../_util/hooks/useConfigInject';
import { defaultConfigProvider } from '../config-provider';
import { getSlot } from '../_util/props-util';
export default defineComponent({ export default defineComponent({
name: 'ACardGrid', name: 'ACardGrid',
__ANT_CARD_GRID: true, __ANT_CARD_GRID: true,
props: { props: {
prefixCls: PropTypes.string, prefixCls: String,
hoverable: PropTypes.looseBool, hoverable: { type: Boolean, default: true },
}, },
setup() { setup(props, { slots }) {
const { prefixCls } = useConfigInject('card', props);
const classNames = computed(() => {
return { return {
configProvider: inject('configProvider', defaultConfigProvider), [`${prefixCls.value}-grid`]: true,
[`${prefixCls.value}-grid-hoverable`]: props.hoverable,
}; };
}, });
render() { return () => {
const { prefixCls: customizePrefixCls, hoverable = true } = this.$props; return <div class={classNames.value}>{slots.default?.()}</div>;
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('card', customizePrefixCls);
const classString = {
[`${prefixCls}-grid`]: true,
[`${prefixCls}-grid-hoverable`]: hoverable,
}; };
return <div class={classString}>{getSlot(this)}</div>;
}, },
}); });

View File

@ -1,43 +1,37 @@
import { defineComponent, inject } from 'vue'; import { defineComponent } from 'vue';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getComponent } from '../_util/props-util'; import { getPropsSlot } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider'; import useConfigInject from '../_util/hooks/useConfigInject';
export default defineComponent({ export default defineComponent({
name: 'ACardMeta', name: 'ACardMeta',
props: { props: {
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
title: PropTypes.VNodeChild, title: PropTypes.any,
description: PropTypes.VNodeChild, description: PropTypes.any,
avatar: PropTypes.VNodeChild, avatar: PropTypes.any,
}, },
setup() { slots: ['title', 'description', 'avatar'],
return { setup(props, { slots }) {
configProvider: inject('configProvider', defaultConfigProvider), const { prefixCls } = useConfigInject('card', props);
}; return () => {
},
render() {
const { prefixCls: customizePrefixCls } = this.$props;
const { getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('card', customizePrefixCls);
const classString = { const classString = {
[`${prefixCls}-meta`]: true, [`${prefixCls.value}-meta`]: true,
}; };
const avatar = getPropsSlot(slots, props, 'avatar');
const title = getPropsSlot(slots, props, 'title');
const description = getPropsSlot(slots, props, 'description');
const avatar = getComponent(this, 'avatar'); const avatarDom = avatar ? (
const title = getComponent(this, 'title'); <div class={`${prefixCls.value}-meta-avatar`}>{avatar}</div>
const description = getComponent(this, 'description'); ) : null;
const titleDom = title ? <div class={`${prefixCls.value}-meta-title`}>{title}</div> : null;
const avatarDom = avatar ? <div class={`${prefixCls}-meta-avatar`}>{avatar}</div> : null;
const titleDom = title ? <div class={`${prefixCls}-meta-title`}>{title}</div> : null;
const descriptionDom = description ? ( const descriptionDom = description ? (
<div class={`${prefixCls}-meta-description`}>{description}</div> <div class={`${prefixCls.value}-meta-description`}>{description}</div>
) : null; ) : null;
const MetaDetail = const MetaDetail =
titleDom || descriptionDom ? ( titleDom || descriptionDom ? (
<div class={`${prefixCls}-meta-detail`}> <div class={`${prefixCls.value}-meta-detail`}>
{titleDom} {titleDom}
{descriptionDom} {descriptionDom}
</div> </div>
@ -48,5 +42,6 @@ export default defineComponent({
{MetaDetail} {MetaDetail}
</div> </div>
); );
};
}, },
}); });

View File

@ -365,7 +365,7 @@ exports[`renders ./components/card/demo/tabs.vue correctly 1`] = `
<!----><button type="button" class="ant-tabs-nav-more" style="visibility: hidden; order: 1;" tabindex="-1" aria-hidden="true" aria-haspopup="listbox" aria-controls="rc-tabs-test-more-popup" id="rc-tabs-test-more" aria-expanded="false"><span role="img" aria-label="ellipsis" class="anticon anticon-ellipsis"><svg focusable="false" class="" data-icon="ellipsis" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"></path></svg></span></button> <!----><button type="button" class="ant-tabs-nav-more" style="visibility: hidden; order: 1;" tabindex="-1" aria-hidden="true" aria-haspopup="listbox" aria-controls="rc-tabs-test-more-popup" id="rc-tabs-test-more" aria-expanded="false"><span role="img" aria-label="ellipsis" class="anticon anticon-ellipsis"><svg focusable="false" class="" data-icon="ellipsis" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M176 511a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0zm280 0a56 56 0 10112 0 56 56 0 10-112 0z"></path></svg></span></button>
<!----> <!---->
</div> </div>
<!----> <div class="ant-tabs-extra-content"><a href="#">More</a></div>
<!----> <!---->
</div> </div>
<div class="ant-tabs-content-holder"> <div class="ant-tabs-content-holder">

View File

@ -5,7 +5,7 @@ exports[`Card should still have padding when card which set padding to 0 is load
<!----> <!---->
<!----> <!---->
<div class="ant-card-body" style="padding: 0px;"> <div class="ant-card-body" style="padding: 0px;">
<div class="ant-card-loading-content"> <div class="ant-card-loading-content" style="padding: 24px;">
<div class="ant-row" style="margin-left: -4px; margin-right: -4px;"> <div class="ant-row" style="margin-left: -4px; margin-right: -4px;">
<div class="ant-col ant-col-22" style="padding-left: 4px; padding-right: 4px;"> <div class="ant-col ant-col-22" style="padding-left: 4px; padding-right: 4px;">
<div class="ant-card-loading-block"></div> <div class="ant-card-loading-block"></div>

View File

@ -24,8 +24,8 @@ More content can be hosted
:active-tab-key="key" :active-tab-key="key"
@tabChange="key => onTabChange(key, 'key')" @tabChange="key => onTabChange(key, 'key')"
> >
<template #customRender="item"> <template #customTab="item">
<span> <span v-if="item.key === 'tab1'">
<home-outlined /> <home-outlined />
{{ item.key }} {{ item.key }}
</span> </span>
@ -63,8 +63,7 @@ export default defineComponent({
const tabList = [ const tabList = [
{ {
key: 'tab1', key: 'tab1',
// tab: 'tab1', tab: 'tab1',
slots: { tab: 'customRender' },
}, },
{ {
key: 'tab2', key: 'tab2',

View File

@ -17,22 +17,30 @@ A card can be used to display content related to a single subject. The content c
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| actions | The action list, shows at the bottom of the Card. | slots | - | |
| activeTabKey | Current TabPane's key | string | - | | | activeTabKey | Current TabPane's key | string | - | |
| headStyle | Inline style to apply to the card head | object | - | | | headStyle | Inline style to apply to the card head | object | - | |
| bodyStyle | Inline style to apply to the card content | object | - | | | bodyStyle | Inline style to apply to the card content | object | - | |
| bordered | Toggles rendering of the border around the card | boolean | `true` | | | bordered | Toggles rendering of the border around the card | boolean | `true` | |
| cover | Card cover | slot | - | |
| defaultActiveTabKey | Initial active TabPane's key, if `activeTabKey` is not set. | string | - | | | defaultActiveTabKey | Initial active TabPane's key, if `activeTabKey` is not set. | string | - | |
| extra | Content to render in the top-right corner of the card | string\|slot | - | | | extra | Content to render in the top-right corner of the card | string\|slot | - | |
| hoverable | Lift up when hovering card | boolean | false | | | hoverable | Lift up when hovering card | boolean | false | |
| loading | Shows a loading indicator while the contents of the card are being fetched | boolean | false | | | loading | Shows a loading indicator while the contents of the card are being fetched | boolean | false | |
| tabList | List of TabPane's head, Custom tabs can be created with the slots property | Array&lt;{key: string, tab: any, slots: {tab: 'XXX'}}&gt; | - | | | tabList | List of TabPane's head, Custom tabs with the customTab(v3.0) slot | Array&lt;{key: string, tab: any}&gt; | - | |
| tabBarExtraContent | Extra content in tab bar | slot | - | 1.5.0 |
| size | Size of card | `default` \| `small` | `default` | | | size | Size of card | `default` \| `small` | `default` | |
| title | Card title | string\|slot | - | | | title | Card title | string\|slot | - | |
| type | Card style type, can be set to `inner` or not set | string | - | | | type | Card style type, can be set to `inner` or not set | string | - | |
### Card Slots
| Slot Name | Description | Type |
| --- | --- | --- | --- |
| customTab | custom tabList tab | { item: tabList[number] } | |
| title | Card title | - | |
| extra | Content to render in the top-right corner of the card | - | |
| tabBarExtraContent | Extra content in tab bar | - | |
| actions | The action list, shows at the bottom of the Card. | - | |
| cover | Card cover | - | |
### events ### events
| Events Name | Description | Arguments | Version | | Events Name | Description | Arguments | Version |

View File

@ -18,22 +18,30 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/NqXt8DJhky/Card.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| actions | 卡片操作组,位置在卡片底部 | slots | - | |
| activeTabKey | 当前激活页签的 key | string | - | | | activeTabKey | 当前激活页签的 key | string | - | |
| headStyle | 自定义标题区域样式 | object | - | | | headStyle | 自定义标题区域样式 | object | - | |
| bodyStyle | 内容区域自定义样式 | object | - | | | bodyStyle | 内容区域自定义样式 | object | - | |
| bordered | 是否有边框 | boolean | true | | | bordered | 是否有边框 | boolean | true | |
| cover | 卡片封面 | slot | - | |
| defaultActiveTabKey | 初始化选中页签的 key如果没有设置 activeTabKey | string | 第一个页签 | | | defaultActiveTabKey | 初始化选中页签的 key如果没有设置 activeTabKey | string | 第一个页签 | |
| extra | 卡片右上角的操作区域 | string\|slot | - | | | extra | 卡片右上角的操作区域 | string\|slot | - | |
| hoverable | 鼠标移过时可浮起 | boolean | false | | | hoverable | 鼠标移过时可浮起 | boolean | false | |
| loading | 当卡片内容还在加载中时,可以用 loading 展示一个占位 | boolean | false | | | loading | 当卡片内容还在加载中时,可以用 loading 展示一个占位 | boolean | false | |
| tabList | 页签标题列表, 可以通过 slots 属性自定义 tab | Array&lt;{key: string, tab: any, slots: {tab: 'XXX'}}&gt; | - | | | tabList | 页签标题列表, 可以通过 customTab(v3.0) 插槽自定义 tab | Array&lt;{key: string, tab: any}&gt; | - | |
| tabBarExtraContent | tab bar 上额外的元素 | slot | 无 | 1.5.0 |
| size | card 的尺寸 | `default` \| `small` | `default` | | | size | card 的尺寸 | `default` \| `small` | `default` | |
| title | 卡片标题 | string\|slot | - | | | title | 卡片标题 | string\|slot | - | |
| type | 卡片类型,可设置为 `inner` 或 不设置 | string | - | | | type | 卡片类型,可设置为 `inner` 或 不设置 | string | - | |
### Card 插槽
| 插槽名称 | 说明 | 参数 |
| ------------------ | -------------------------- | ------------------------- | --- |
| customTab | 自定义 tabList tab 标签 | { item: tabList[number] } | |
| title | 卡片标题 | - | |
| extra | 卡片右上角的操作区域 | - | |
| tabBarExtraContent | tab bar 上额外的元素 | - | |
| actions | 卡片操作组,位置在卡片底部 | - | |
| cover | 卡片封面 | - | |
### 事件 ### 事件
| 事件名称 | 说明 | 回调参数 | 版本 | | 事件名称 | 说明 | 回调参数 | 版本 |

View File

@ -2,8 +2,7 @@
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@card-prefix-cls: ~'@{ant-prefix}-card'; @card-prefix-cls: ~'@{ant-prefix}-card';
@card-head-height: 48px; @card-hoverable-hover-border: transparent;
@card-hover-border: fade(@black, 9%);
@card-action-icon-size: 16px; @card-action-icon-size: 16px;
@gradient-min: fade(@card-skeleton-bg, 20%); @gradient-min: fade(@card-skeleton-bg, 20%);
@ -15,14 +14,17 @@
position: relative; position: relative;
background: @card-background; background: @card-background;
border-radius: @card-radius; border-radius: @card-radius;
transition: all 0.3s;
&-rtl {
direction: rtl;
}
&-hoverable { &-hoverable {
cursor: pointer; cursor: pointer;
transition: box-shadow 0.3s border-color 0.3s; transition: box-shadow 0.3s, border-color 0.3s;
&:hover { &:hover {
border-color: @card-hover-border; border-color: @card-hoverable-hover-border;
box-shadow: @card-shadow; box-shadow: @card-shadow;
} }
} }
@ -37,7 +39,7 @@
padding: 0 @card-padding-base; padding: 0 @card-padding-base;
color: @card-head-color; color: @card-head-color;
font-weight: 500; font-weight: 500;
font-size: @font-size-lg; font-size: @card-head-font-size;
background: @card-head-background; background: @card-head-background;
border-bottom: @border-width-base @border-style-base @border-color-split; border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @card-radius @card-radius 0 0; border-radius: @card-radius @card-radius 0 0;
@ -55,11 +57,18 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
> .@{ant-prefix}-typography,
> .@{ant-prefix}-typography-edit-content {
left: 0;
margin-top: 0;
margin-bottom: 0;
}
} }
.@{ant-prefix}-tabs { .@{ant-prefix}-tabs {
clear: both; clear: both;
margin-bottom: -17px; margin-bottom: @card-head-tabs-margin-bottom;
color: @text-color; color: @text-color;
font-weight: normal; font-weight: normal;
font-size: @font-size-base; font-size: @font-size-base;
@ -75,9 +84,14 @@
// https://stackoverflow.com/a/22429853/3040605 // https://stackoverflow.com/a/22429853/3040605
margin-left: auto; margin-left: auto;
padding: @card-head-padding 0; padding: @card-head-padding 0;
color: @text-color; color: @card-head-extra-color;
font-weight: normal; font-weight: normal;
font-size: @font-size-base; font-size: @font-size-base;
.@{card-prefix-cls}-rtl & {
margin-right: auto;
margin-left: 0;
}
} }
&-body { &-body {
@ -100,11 +114,16 @@
1px 1px 0 0 @border-color-split, 1px 0 0 0 @border-color-split inset, 1px 1px 0 0 @border-color-split, 1px 0 0 0 @border-color-split inset,
0 1px 0 0 @border-color-split inset; 0 1px 0 0 @border-color-split inset;
transition: all 0.3s; transition: all 0.3s;
.@{card-prefix-cls}-rtl & {
float: right;
}
&-hoverable { &-hoverable {
&:hover { &:hover {
position: relative; position: relative;
z-index: 1; z-index: 1;
box-shadow: @box-shadow-base; box-shadow: @card-shadow;
} }
} }
} }
@ -118,11 +137,18 @@
padding-bottom: 0; padding-bottom: 0;
} }
&-bordered &-cover {
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
}
&-cover { &-cover {
> * { > * {
display: block; display: block;
width: 100%; width: 100%;
} }
img { img {
border-radius: @card-radius @card-radius 0 0; border-radius: @card-radius @card-radius 0 0;
} }
@ -138,16 +164,20 @@
& > li { & > li {
float: left; float: left;
margin: 12px 0; margin: @card-actions-li-margin;
color: @text-color-secondary; color: @text-color-secondary;
text-align: center; text-align: center;
.@{card-prefix-cls}-rtl & {
float: right;
}
> span { > span {
position: relative; position: relative;
display: block; display: block;
min-width: 32px; min-width: 32px;
font-size: @font-size-base; font-size: @font-size-base;
line-height: 22px; line-height: @line-height-base;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
@ -156,7 +186,7 @@
} }
a:not(.@{ant-prefix}-btn), a:not(.@{ant-prefix}-btn),
> .anticon { > .@{iconfont-css-prefix} {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
color: @text-color-secondary; color: @text-color-secondary;
@ -168,7 +198,7 @@
} }
} }
> .anticon { > .@{iconfont-css-prefix} {
font-size: @card-action-icon-size; font-size: @card-action-icon-size;
line-height: 22px; line-height: 22px;
} }
@ -176,6 +206,11 @@
&:not(:last-child) { &:not(:last-child) {
border-right: @border-width-base @border-style-base @border-color-split; border-right: @border-width-base @border-style-base @border-color-split;
.@{card-prefix-cls}-rtl & {
border-right: none;
border-left: @border-width-base @border-style-base @border-color-split;
}
} }
} }
} }
@ -205,12 +240,18 @@
&-avatar { &-avatar {
float: left; float: left;
padding-right: 16px; padding-right: 16px;
.@{card-prefix-cls}-rtl & {
float: right;
padding-right: 0;
padding-left: 16px;
}
} }
&-detail { &-detail {
overflow: hidden; overflow: hidden;
> div:not(:last-child) { > div:not(:last-child) {
margin-bottom: 8px; margin-bottom: @margin-xs;
} }
} }

View File

@ -1,7 +0,0 @@
import '../../style/index.less';
import './index.less';
// style dependencies
import '../../tabs/style';
import '../../row/style';
import '../../col/style';

View File

@ -1,8 +1,3 @@
@card-head-height-sm: 36px;
@card-padding-base-sm: (@card-padding-base / 2);
@card-head-padding-sm: (@card-head-padding / 2);
@card-head-font-size-sm: @font-size-base;
.@{card-prefix-cls}-small { .@{card-prefix-cls}-small {
> .@{card-prefix-cls}-head { > .@{card-prefix-cls}-head {
min-height: @card-head-height-sm; min-height: @card-head-height-sm;

View File

@ -657,14 +657,24 @@
// --- // ---
@card-head-color: @heading-color; @card-head-color: @heading-color;
@card-head-background: transparent; @card-head-background: transparent;
@card-head-font-size: @font-size-lg;
@card-head-font-size-sm: @font-size-base;
@card-head-padding: 16px; @card-head-padding: 16px;
@card-head-padding-sm: (@card-head-padding / 2);
@card-head-height: 48px;
@card-head-height-sm: 36px;
@card-inner-head-padding: 12px; @card-inner-head-padding: 12px;
@card-padding-base: 24px; @card-padding-base: 24px;
@card-actions-background: @background-color-light; @card-padding-base-sm: (@card-padding-base / 2);
@card-actions-background: @component-background;
@card-actions-li-margin: 12px 0;
@card-skeleton-bg: #cfd8dc; @card-skeleton-bg: #cfd8dc;
@card-background: @component-background; @card-background: @component-background;
@card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); @card-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12),
@card-radius: @border-radius-sm; 0 5px 12px 4px rgba(0, 0, 0, 0.09);
@card-radius: @border-radius-base;
@card-head-tabs-margin-bottom: -17px;
@card-head-extra-color: @text-color;
// Comment // Comment
// --- // ---

View File

@ -32,7 +32,7 @@ Ant Design has 3 types of Tabs for different situations.
### Tabs Slots ### Tabs Slots
| 插槽名称 | 说明 | 参数 | | Slot Name | Description | Type |
| ------------ | ------------------------------ | ----------------- | --- | | ------------ | ------------------------------ | ----------------- | --- |
| renderTabBar | Replace the TabBar | { DefaultTabBar } | | | renderTabBar | Replace the TabBar | { DefaultTabBar } | |
| leftExtra | Extra content in tab bar left | - | - | | leftExtra | Extra content in tab bar left | - | - |