diff --git a/src/badge/README.md b/src/badge/README.md new file mode 100644 index 000000000..00f8e3952 --- /dev/null +++ b/src/badge/README.md @@ -0,0 +1,77 @@ +# Badge + +### Install + +```js +import Vue from 'vue'; +import { Badge } from 'vant'; + +Vue.use(Badge); +``` + +## Usage + +### Basic Usage + +```html + +
+ + +
+ + + +``` + +### Max + +```html + +
+ + +
+ +``` + +### Custom Color + +```html + +
+ + +
+ +``` + +### Standaline + +```html + +``` + +## 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 value,show `{max}+` when exceed,only works when content is number | _number \| string_ | - | + +### Slots + +| Name | Description | +| ------- | ------------ | +| default | Default slot | diff --git a/src/badge/README.zh-CN.md b/src/badge/README.zh-CN.md new file mode 100644 index 000000000..b6c5d4fe8 --- /dev/null +++ b/src/badge/README.zh-CN.md @@ -0,0 +1,90 @@ +# Badge 徽标 + +### 介绍 + +在右上角展示徽标数字或小红点。 + +### 引入 + +```js +import Vue from 'vue'; +import { Badge } from 'vant'; + +Vue.use(Badge); +``` + +## 代码演示 + +### 基础用法 + +设置 `content` 属性后,Badge 会在子元素的右上角显示对应的徽标,也可以通过 `dot` 来显示小红点。 + +```html + +
+ + +
+ + + +``` + +### 最大值 + +设置 `max` 属性后,当 `content` 的数值超过最大值时,会自动显示为 `{max}+`。 + +```html + +
+ + +
+ +``` + +### 自定义颜色 + +通过 `color` 属性来设置徽标的颜色。 + +```html + +
+ + +
+ +``` + +### 独立展示 + +当 Badge 没有子元素时,会作为一个独立的元素进行展示。 + +```html + +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| content | 徽标内容 | _number \| string_ | - | +| color | 徽标背景颜色 | _string_ | `#ee0a24` | +| dot | 是否展示为小红点 | _boolean_ | `false` | +| max | 最大值,超过最大值会显示 `{max}+`,仅当 content 为数字时有效 | _number \| string_ | - | + +### Slots + +| 名称 | 说明 | +| ------- | ---------------- | +| default | 徽标包裹的子元素 | +| content | 自定义徽标内容 | diff --git a/src/badge/demo/index.vue b/src/badge/demo/index.vue new file mode 100644 index 000000000..c7558afc4 --- /dev/null +++ b/src/badge/demo/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/badge/index.js b/src/badge/index.js new file mode 100644 index 000000000..652e5184b --- /dev/null +++ b/src/badge/index.js @@ -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 ( +
+ {this.renderContent()} +
+ ); + } + }, + }, + + render() { + if (this.$scopedSlots.default) { + const { tag } = this; + return ( + + {this.$scopedSlots.default()} + {this.renderBadge()} + + ); + } + + return this.renderBadge(); + }, +}); diff --git a/src/badge/index.less b/src/badge/index.less new file mode 100644 index 000000000..6bbd1c791 --- /dev/null +++ b/src/badge/index.less @@ -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; + } +} diff --git a/src/badge/test/__snapshots__/demo.spec.js.snap b/src/badge/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..29e88b7b1 --- /dev/null +++ b/src/badge/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
+
+
5
+
+
+
+
+
+
+
+
+
+
9+
+
+
+
+
99+
+
+
+
+
+
+
5
+
+
+
+
+
+
+
+
99+
+
+
+`; diff --git a/src/badge/test/__snapshots__/index.spec.js.snap b/src/badge/test/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..2ed9ab2d1 --- /dev/null +++ b/src/badge/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render content slot correctly 1`] = `
Custom Content
`; + +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`] = `
0
`; diff --git a/src/badge/test/demo.spec.js b/src/badge/test/demo.spec.js new file mode 100644 index 000000000..5c70922b5 --- /dev/null +++ b/src/badge/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/src/badge/test/index.spec.js b/src/badge/test/index.spec.js new file mode 100644 index 000000000..7bf8d7d83 --- /dev/null +++ b/src/badge/test/index.spec.js @@ -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(); +}); diff --git a/src/style/var.less b/src/style/var.less index 9443f69a3..84a405520 100644 --- a/src/style/var.less +++ b/src/style/var.less @@ -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; diff --git a/types/index.d.ts b/types/index.d.ts index f7141356e..245ef676d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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 {} diff --git a/vant.config.js b/vant.config.js index d7c9046dc..c132b93ee 100644 --- a/vant.config.js +++ b/vant.config.js @@ -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',