diff --git a/packages/components/card/demo/Disable.md b/packages/components/card/demo/Disable.md new file mode 100644 index 000000000..dedd6c777 --- /dev/null +++ b/packages/components/card/demo/Disable.md @@ -0,0 +1,14 @@ +--- +order: 7 +title: + zh: 禁用 + en: Disabled State +--- + +## zh + +如果 `disabled` 属性为 `true`,卡片为禁用状态。 + +## en + +If the `disabled` property is `true`, the card is disabled. diff --git a/packages/components/card/demo/Disable.vue b/packages/components/card/demo/Disable.vue new file mode 100644 index 000000000..454146f40 --- /dev/null +++ b/packages/components/card/demo/Disable.vue @@ -0,0 +1,11 @@ + diff --git a/packages/components/card/demo/Selectable.md b/packages/components/card/demo/Selectable.md new file mode 100644 index 000000000..760802b0b --- /dev/null +++ b/packages/components/card/demo/Selectable.md @@ -0,0 +1,20 @@ +--- +order: 8 +title: + zh: 配置可选 + en: Selected State +--- + +## zh + +通过 `seletable` 属性来配置卡片是否可以选中,其中,如果 `selected` 属性为 `true`,卡片被选中。 + +如果 `disabled` 属性为 `true`,卡片为禁用状态,不可变更选择状态。 + +## en + +Configure whether the card can be selected through the attribute `selectable`. + +If `selected` is `true`, it means the card is selected. + +If the `disabled` property is `true`, the card is disabled and the selection state cannot be changed. diff --git a/packages/components/card/demo/Selectable.vue b/packages/components/card/demo/Selectable.vue new file mode 100644 index 000000000..093c90f81 --- /dev/null +++ b/packages/components/card/demo/Selectable.vue @@ -0,0 +1,55 @@ + + diff --git a/packages/components/card/docs/Design.zh.md b/packages/components/card/docs/Design.zh.md index e4f01596d..6b6624196 100644 --- a/packages/components/card/docs/Design.zh.md +++ b/packages/components/card/docs/Design.zh.md @@ -10,6 +10,7 @@ | 内容区(必选) | 当内容区仅有图表,没有可快速识别内容主题的信息时,必须要设置卡头的标题 | | 外框 | 外框分为不带阴影(左图)和带阴影(右图)两种样式 | | 操作区 | 放置卡片的操作项,例如查看详情、编辑、过滤器等 | +| 角标区 | 配置卡片的选中状态,点击可以切换选中或者非选中 | ## 组件类型 @@ -21,19 +22,22 @@ - 概览指标卡 - - 结合统计数值或状态程度总体概括当前现状,让用户快速了解当前产品整体情况或对象整体情况的现状 - - 一般用于概览页中或详情/表格/列表页的总览区域中,用于展示某一个主题的核心指标 + - 结合统计数值或状态程度总体概括当前现状,让用户快速了解当前产品整体情况或对象整体情况的现状 + - 一般用于概览页中或详情/表格/列表页的总览区域中,用于展示某一个主题的核心指标 - 优先展示核心信息,例如下图将统计数据放大展示 - 分析图表卡 - - 数据内容较复杂,需要可视化呈现内容让用户直观了解细节或用户需要进行多维度关联分析 + - 数据内容较复杂,需要可视化呈现内容让用户直观了解细节或用户需要进行多维度关联分析 - 一般用于详情/表格/列表页的数据分析区域中,用于展示一个对象多维度的具体信息 - 同类信息展示卡 - 常用于卡片列表页中,对单对象信息进行展示,同类信息展示卡常常同时重复出现使用。如虚拟机管理、集群管理、应用管理等,对一系列不同的对象进行展示。相比表格页,可更加可视化、重点突出地展示数据。 -也可用于表单中做选项卡片,用户点击整个卡片即选择。 + 常用于卡片列表页中,对单对象信息进行展示,同类信息展示卡常常同时重复出现使用。如虚拟机管理、集群管理、应用管理等,对一系列不同的对象进行展示。相比表格页,可更加可视化、重点突出地展示数据。 + +- 表单选项卡 + + 可用于表单中做选项卡片,用户点击整个卡片即选择。可以设置禁用状态,来禁用选项。 ## 组件状态 @@ -42,3 +46,5 @@ | 查看详情 | 当卡片支持跳转查看详情时,可在右上方放置【查看详情】按钮,根据业务诉求可支持点击卡片全域来跳转的方式,从而增大点击区域 | | 存在多个按钮 | 表格操作较多时,根据业务场景诉求选择【按钮外显】与【收纳到“更多”按钮】,外显时使用图标按钮 | | 悬停显示内容 | 当卡片以图片展示为主时,可以鼠标悬停时展示文案描述和相关操作 | +| 选中状态 | 用户可以选中或者取消选中某个卡片表单项 | +| 禁用状态 | 用户可以置灰某个卡片表示下架或者禁用,或者置灰某个表单项,禁止切换其选中状态 | diff --git a/packages/components/card/docs/Index.zh.md b/packages/components/card/docs/Index.zh.md index 6ca70cf78..8c4873eaf 100644 --- a/packages/components/card/docs/Index.zh.md +++ b/packages/components/card/docs/Index.zh.md @@ -22,6 +22,10 @@ order: 0 | `loading` | 是否加载中状态 | `boolean` | `false` | - | 当卡片内容还在加载中时,显示占位图 | | `size` | 设置卡片大小 | `'sm' \| 'md' \| 'lg'` | `'md'` | ✅ | - | | `footer` | 自定义底部按钮 | `CardButtonProps[] \| #footer` | - | - | - | +| `selectable` | 是否开启可选中配置 | `boolean` | `false` | - | - | - | +| `v-model:selected` | 指定当前卡片是否选中 | `boolean` | `false` | - | - | - | +| `disabled` | 是否禁用当前卡片 | `boolean` | `false` | - | - | - | +| `onChange` | 选中状态发生变化后的回调 | `(selected: boolean) => void` | - | - | - | - | ```ts export interface CardCover { @@ -75,4 +79,11 @@ export interface CardButtonProps extends ButtonProps { | `@card-loading-background-size` | `600%` | - | - | | `@card-loading-transition-duration` | `2s` | - | - | | `@card-grid-width` | `25%` | - | - | +| `@card-border-color-selectable` | `@color-graphite-l20` | - | -| +| `@card-border-color-selectable-selected` | `@color-primary` | - | -| +| `@card-border-color-selectable-hover` | `@color-primary-l10` | - | -| +| `@card-icon-color` | `@color-graphite-l30` | - | -| +| `@card-icon-width` | `@font-size-xl` | - | -| +| `@card-icon-height` | `@font-size-lg` | - | -| +| `@card-box-shadow-selectable` | `0 2px 8px 0 rgba(30, 35, 43, 0.12)` | - | -| diff --git a/packages/components/card/src/Card.tsx b/packages/components/card/src/Card.tsx index 51f39599f..0c6d1667d 100644 --- a/packages/components/card/src/Card.tsx +++ b/packages/components/card/src/Card.tsx @@ -12,10 +12,13 @@ import { computed, defineComponent, isVNode, normalizeClass, provide } from 'vue import { isString } from 'lodash-es' +import { callEmit } from '@idux/cdk/utils' import { ɵHeader } from '@idux/components/_private/header' import { IxButton } from '@idux/components/button' import { useGlobalConfig } from '@idux/components/config' +import { useFormAccessor } from '@idux/components/form' import { IxCol, IxRow } from '@idux/components/grid' +import { IxIcon } from '@idux/components/icon' import { cardToken } from './token' import { cardProps } from './types' @@ -37,8 +40,29 @@ export default defineComponent({ return children.value.some(node => node.type && (node.type as any).name === 'IxCardGrid') }) + const accessor = useFormAccessor('selected') + const isSelected = computed(() => accessor.valueRef.value ?? false) + const isSelectable = computed(() => props.selectable ?? false) + const isDisabled = computed(() => props.disabled ?? accessor.disabled.value) + const handleClick = () => { + if (isDisabled.value || !isSelectable.value) { + return + } + callEmit(props.onChange, !isSelected.value) + accessor.setValue(!isSelected.value) + } + const classes = computed(() => { - const { borderless = config.borderless, loading, size = config.size, shadow } = props + const { + borderless = config.borderless, + loading, + size = config.size, + shadow, + selected, + disabled, + selectable, + } = props + const hasGridValue = hasGrid.value const prefixCls = mergedPrefixCls.value return normalizeClass({ @@ -49,23 +73,41 @@ export default defineComponent({ [`${prefixCls}-has-shadow`]: shadow, [`${prefixCls}-has-grid`]: hasGridValue, [`${prefixCls}-${size}`]: true, + [`${prefixCls}-selectable`]: selectable, + [`${prefixCls}-selected`]: selectable && selected, + [`${prefixCls}-disabled`]: disabled, }) }) return () => { const prefixCls = mergedPrefixCls.value return ( -
+
{renderCover(props, slots, prefixCls)} <ɵHeader v-slots={slots} size="sm" header={props.header} /> {renderBody(props, children, hasGrid, prefixCls)} {renderFooter(props, slots, prefixCls)} + {renderCornerMark(props, prefixCls)}
) } }, }) +const renderCornerMark = (props: CardProps, prefixCls: string) => { + const { selectable } = props + if (!selectable) { + return undefined + } + + return ( +
+
+ +
+ ) +} + const renderCover = (props: CardProps, slots: Slots, prefixCls: string) => { let coverNode: VNodeTypes | undefined if (slots.cover) { diff --git a/packages/components/card/src/types.ts b/packages/components/card/src/types.ts index 973fa6dee..a28b5f6d9 100644 --- a/packages/components/card/src/types.ts +++ b/packages/components/card/src/types.ts @@ -35,6 +35,13 @@ export const cardProps = { loading: IxPropTypes.bool.def(false), size: IxPropTypes.oneOf(['sm', 'md', 'lg']), footer: IxPropTypes.array(), + disabled: IxPropTypes.bool.def(false), + selected: IxPropTypes.bool.def(false), + selectable: IxPropTypes.bool.def(false), + + // event + 'onUpdate:selected': IxPropTypes.emit<(selected: boolean) => void>(), + onChange: IxPropTypes.emit<(selected: boolean) => void>(), } export type CardProps = ExtractInnerPropTypes diff --git a/packages/components/card/style/index.less b/packages/components/card/style/index.less index 6f86cf464..defcbbe33 100644 --- a/packages/components/card/style/index.less +++ b/packages/components/card/style/index.less @@ -117,6 +117,80 @@ } } + &-selectable { + position: relative; + overflow: hidden; + cursor: pointer; + border: @card-border-width @card-border-style @card-border-color-selectable; + box-shadow: none; + + &:hover { + border: @card-border-width @card-border-style @card-border-color-selectable-hover; + box-shadow: @card-box-shadow-selectable; + + .@{card-prefix}-mark{ + border-color: @card-border-color-selectable @card-border-color-selectable transparent transparent; + } + } + } + + &-mark-wrap { + position: absolute; + top: 0; + right: 0; + width: @card-icon-width; + height: @card-icon-height; + } + + &-mark { + width: 0; + height: 0; + border-style: @card-border-style; + border-width: (@card-icon-height / 2) (@card-icon-width / 2); + border-color: @card-icon-color @card-icon-color transparent transparent; + border-radius: 0 @card-border-width 0; + } + + &-mark-icon { + font-size: @font-size-xl; + position: absolute; + top: -6px; + right: -4px; + } + + &-selected { + border: @card-border-width @card-border-style @color-primary; + box-shadow: @card-box-shadow-selectable; + + .@{card-prefix}-mark { + border-color: @color-primary @color-primary transparent transparent; + } + + &:hover { + border: @card-border-width @card-border-style @color-primary; + box-shadow: @card-box-shadow-selectable; + + .@{card-prefix}-mark { + border-color: @color-primary @color-primary transparent transparent; + } + } + } + + &-disabled { + opacity: 0.4; + cursor: not-allowed; + + &.@{card-prefix}-selectable:hover { + border: @card-border-width @card-border-style @card-border-color-selectable; + box-shadow: none; + } + + &.@{card-prefix}-selected:hover { + border: @card-border-width @card-border-style @color-primary; + box-shadow: @card-box-shadow-selectable; + } + } + .@{idux-prefix}-header { &-content { > .@{idux-prefix}-header-suffix { diff --git a/packages/components/card/style/themes/default.variable.less b/packages/components/card/style/themes/default.variable.less index 1ba89b084..59fd38339 100644 --- a/packages/components/card/style/themes/default.variable.less +++ b/packages/components/card/style/themes/default.variable.less @@ -8,6 +8,13 @@ @card-border-color: @color-graphite-l30; @card-border-color-hover: transparent; @card-border-radius: @border-radius-sm; +@card-border-color-selectable: @color-graphite-l20; +@card-border-color-selectable-selected: @color-primary; +@card-border-color-selectable-hover: @color-primary-l10; +@card-icon-color: @color-graphite-l30; +@card-icon-width: @font-size-xl; +@card-icon-height: @font-size-lg; +@card-box-shadow-selectable: 0 2px 8px 0 rgba(30, 35, 43, 0.12); @card-box-shadow: @shadow-bottom-sm; @card-gradient-min: fade(@color-grey, 20%); @card-gradient-max: fade(@color-grey, 60%);