feat: add Badge component (#7602)

This commit is contained in:
neverland 2020-11-22 10:26:59 +08:00 committed by GitHub
parent cda5e0869e
commit 28a04148f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 467 additions and 0 deletions

77
src/badge/README.md Normal file
View File

@ -0,0 +1,77 @@
# Badge
### Install
```js
import Vue from 'vue';
import { Badge } from 'vant';
Vue.use(Badge);
```
## Usage
### Basic Usage
```html
<van-badge content="5">
<div class="child" />
</van-badge>
<van-badge dot>
<div class="child" />
</van-badge>
<style>
.child {
width: 36px;
height: 36px;
background: #f2f3f5;
border-radius: 4px;
}
</style>
```
### Max
```html
<van-badge content="20" max="9">
<div class="child" />
</van-badge>
<van-badge content="200" max="99">
<div class="child" />
</van-badge>
```
### Custom Color
```html
<van-badge content="5" color="#1989fa">
<div class="child" />
</van-badge>
<van-badge dot color="#1989fa">
<div class="child" />
</van-badge>
```
### Standaline
```html
<van-badge content="200" max="99" />
```
## API
### Props
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| content | Badge content | _number \| string_ | - |
| color | Background color | _string_ | `#ee0a24` |
| dot | Whether to show dot | _boolean_ | `false` |
| max | Max valueshow `{max}+` when exceedonly works when content is number | _number \| string_ | - |
### Slots
| Name | Description |
| ------- | ------------ |
| default | Default slot |

90
src/badge/README.zh-CN.md Normal file
View File

@ -0,0 +1,90 @@
# Badge 徽标
### 介绍
在右上角展示徽标数字或小红点。
### 引入
```js
import Vue from 'vue';
import { Badge } from 'vant';
Vue.use(Badge);
```
## 代码演示
### 基础用法
设置 `content` 属性后Badge 会在子元素的右上角显示对应的徽标,也可以通过 `dot` 来显示小红点。
```html
<van-badge :content="5">
<div class="child" />
</van-badge>
<van-badge dot>
<div class="child" />
</van-badge>
<style>
.child {
width: 36px;
height: 36px;
background: #f2f3f5;
border-radius: 4px;
}
</style>
```
### 最大值
设置 `max` 属性后,当 `content` 的数值超过最大值时,会自动显示为 `{max}+`
```html
<van-badge :content="20" max="9">
<div class="child" />
</van-badge>
<van-badge :content="200" max="99">
<div class="child" />
</van-badge>
```
### 自定义颜色
通过 `color` 属性来设置徽标的颜色。
```html
<van-badge :content="5" color="#1989fa">
<div class="child" />
</van-badge>
<van-badge dot color="#1989fa">
<div class="child" />
</van-badge>
```
### 独立展示
当 Badge 没有子元素时,会作为一个独立的元素进行展示。
```html
<van-badge :content="200" max="99" />
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| content | 徽标内容 | _number \| string_ | - |
| color | 徽标背景颜色 | _string_ | `#ee0a24` |
| dot | 是否展示为小红点 | _boolean_ | `false` |
| max | 最大值,超过最大值会显示 `{max}+`,仅当 content 为数字时有效 | _number \| string_ | - |
### Slots
| 名称 | 说明 |
| ------- | ---------------- |
| default | 徽标包裹的子元素 |
| content | 自定义徽标内容 |

78
src/badge/demo/index.vue Normal file
View File

@ -0,0 +1,78 @@
<template>
<demo-section>
<demo-block :title="t('basicUsage')">
<van-badge content="5">
<div class="child" />
</van-badge>
<van-badge dot>
<div class="child" />
</van-badge>
</demo-block>
<demo-block :title="t('max')">
<van-badge content="20" max="9">
<div class="child" />
</van-badge>
<van-badge content="200" max="99">
<div class="child" />
</van-badge>
</demo-block>
<demo-block :title="t('customColor')">
<van-badge content="5" :color="BLUE">
<div class="child" />
</van-badge>
<van-badge dot :color="BLUE">
<div class="child" />
</van-badge>
</demo-block>
<demo-block :title="t('standalone')">
<van-badge content="200" max="99" style="margin-left: 16px;" />
</demo-block>
</demo-section>
</template>
<script>
import { BLUE } from '../../utils/constant';
export default {
i18n: {
'zh-CN': {
max: '最大值',
standalone: '独立展示',
customColor: '自定义颜色',
},
'en-US': {
max: 'Max',
standalone: 'Standalone',
customColor: 'Custom Color',
},
},
data() {
return {
BLUE,
};
},
};
</script>
<style lang="less">
@import '../../style/var';
.demo-badge {
background-color: @white;
.van-badge__wrapper {
margin-left: @padding-md;
}
.child {
width: 36px;
height: 36px;
background: @gray-2;
border-radius: 4px;
}
}
</style>

69
src/badge/index.js Normal file
View File

@ -0,0 +1,69 @@
import { isDef, createNamespace } from '../utils';
import { isNumeric } from '../utils/validate/number';
const [createComponent, bem] = createNamespace('badge');
export default createComponent({
props: {
dot: Boolean,
max: [Number, String],
color: String,
content: [Number, String],
tag: {
type: String,
default: 'div',
},
},
methods: {
hasContent() {
return !!(
this.$scopedSlots.content ||
(isDef(this.content) && this.content !== '')
);
},
renderContent() {
const { dot, max, content } = this;
if (!dot && this.hasContent()) {
if (this.$scopedSlots.content) {
return this.$scopedSlots.content();
}
if (isDef(max) && isNumeric(content) && +content > max) {
return `${max}+`;
}
return content;
}
},
renderBadge() {
if (this.hasContent() || this.dot) {
return (
<div
class={bem({ dot: this.dot, fixed: !!this.$scopedSlots.default })}
style={{ background: this.color }}
>
{this.renderContent()}
</div>
);
}
},
},
render() {
if (this.$scopedSlots.default) {
const { tag } = this;
return (
<tag class={bem('wrapper')}>
{this.$scopedSlots.default()}
{this.renderBadge()}
</tag>
);
}
return this.renderBadge();
},
});

38
src/badge/index.less Normal file
View File

@ -0,0 +1,38 @@
@import '../style/var';
.van-badge {
display: inline-block;
box-sizing: border-box;
min-width: @badge-size;
padding: @badge-padding;
color: @badge-color;
font-weight: @badge-font-weight;
font-size: @badge-font-size;
font-family: @badge-font-family;
line-height: 1.2;
text-align: center;
background-color: @badge-background-color;
border: @badge-border-width solid @white;
border-radius: @border-radius-max;
&--fixed {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
transform-origin: 100%;
}
&--dot {
width: @badge-dot-size;
min-width: 0;
height: @badge-dot-size;
background-color: @badge-dot-color;
border-radius: 100%;
}
&__wrapper {
position: relative;
display: inline-block;
}
}

View File

@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders demo correctly 1`] = `
<div>
<div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--fixed">5</div>
</div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--dot van-badge--fixed"></div>
</div>
</div>
<div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--fixed">9+</div>
</div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--fixed">99+</div>
</div>
</div>
<div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--fixed" style="background: rgb(25, 137, 250);">5</div>
</div>
<div class="van-badge__wrapper">
<div class="child"></div>
<div class="van-badge van-badge--dot van-badge--fixed" style="background: rgb(25, 137, 250);"></div>
</div>
</div>
<div>
<div class="van-badge" style="margin-left: 16px;">99+</div>
</div>
</div>
`;

View File

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render content slot correctly 1`] = `<div class="van-badge">Custom Content</div>`;
exports[`should render nothing when content is empty string 1`] = `undefined`;
exports[`should render nothing when content is undefined 1`] = `undefined`;
exports[`should render nothing when content is zero 1`] = `<div class="van-badge">0</div>`;

View File

@ -0,0 +1,4 @@
import Demo from '../demo';
import { snapshotDemo } from '../../../test/demo';
snapshotDemo(Demo);

View File

@ -0,0 +1,42 @@
import Badge from '..';
import { mount } from '@vue/test-utils';
test('should render nothing when content is empty string', () => {
const wrapper = mount(Badge, {
propsData: {
content: '',
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render nothing when content is undefined', () => {
const wrapper = mount(Badge, {
propsData: {
content: undefined,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render nothing when content is zero', () => {
const wrapper = mount(Badge, {
propsData: {
content: 0,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
test('should render content slot correctly', () => {
const wrapper = mount(Badge, {
scopedSlots: {
content: () => 'Custom Content',
},
});
expect(wrapper.html()).toMatchSnapshot();
});

View File

@ -113,6 +113,18 @@
@address-list-item-radio-icon-color: @red;
@address-list-edit-icon-size: 20px;
// Badge
@badge-size: 16px;
@badge-color: @white;
@badge-padding: 0 3px;
@badge-font-size: @font-size-sm;
@badge-font-weight: @font-weight-bold;
@badge-border-width: @border-width-base;
@badge-background-color: @red;
@badge-dot-color: @red;
@badge-dot-size: 8px;
@badge-font-family: -apple-system-font, Helvetica Neue, Arial, sans-serif;
// Button
@button-mini-height: 24px;
@button-mini-font-size: @font-size-xs;

1
types/index.d.ts vendored
View File

@ -32,6 +32,7 @@ export function install(vue: typeof Vue): void;
export class ActionSheet extends VanComponent {}
export class AddressList extends VanComponent {}
export class Badge extends VanComponent {}
export class Button extends VanComponent {}
export class Card extends VanComponent {}
export class Cell extends VanComponent {}

View File

@ -239,6 +239,10 @@ module.exports = {
{
title: '展示组件',
items: [
{
path: 'badge',
title: 'Badge 徽标',
},
{
path: 'circle',
title: 'Circle 环形进度条',
@ -606,6 +610,10 @@ module.exports = {
{
title: 'Display Components',
items: [
{
path: 'badge',
title: 'Badge',
},
{
path: 'circle',
title: 'Circle',