diff --git a/devui/badge/__tests__/badge.spec.ts b/devui/badge/__tests__/badge.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f18aa0e254a397e99319bf19d2d8c69741810dfb --- /dev/null +++ b/devui/badge/__tests__/badge.spec.ts @@ -0,0 +1,50 @@ +import { mount } from '@vue/test-utils' +import DBadge from '../src/badge' + +const SLOT = 'This is a slot test' + +describe('badge', () => { + it('badge base', () => { + const wrapper = mount(DBadge, { + props: { count: 80 }, + slots: { default: SLOT } + }) + expect(wrapper.vm.count).toEqual(80) + }) + + it('badge dot', () => { + const wrapper = mount(DBadge, { + props: { showDot: true }, + slots: { default: SLOT } + }) + expect(wrapper.find('.devui-badge-content.devui-badge-content-dot').exists()).toBe(true) + }) + + it('badge max', () => { + const wrapper = mount(DBadge, { + props: { count: 100 } + }) + expect(wrapper.find('.devui-badge-content').text()).toBe('99+') + + const wrapper2 = mount(DBadge, { + props: { count: 100, maxCount: 1000 } + }) + expect(wrapper2.find('.devui-badge-content').text()).toBe('100') + }) + + it('badge bgColor', () => { + const wrapper = mount(DBadge, { + props: { bgColor: 'red' }, + slots: { default: SLOT } + }) + expect(wrapper.find('.devui-badge-content').attributes().style).toBe('background: red;') + }) + + it('badge offsetXY', () => { + const wrapper = mount(DBadge, { + props: { offsetXY: [-10, 10], badgePos: 'top-right' }, + slots: { default: SLOT } + }) + expect(wrapper.find('.devui-badge-content').attributes().style).toBe('top: 10px; right: -10px;') + }) +}) diff --git a/devui/badge/index.ts b/devui/badge/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..65f1391a9b2540a770d1f8657dcd61efa50baf92 --- /dev/null +++ b/devui/badge/index.ts @@ -0,0 +1,16 @@ +import type { App } from 'vue' +import Badge from './src/badge' + +Badge.install = function (app: App) { + app.component(Badge.name, Badge) +} + +export { Badge } + +export default { + title: 'Badge 徽标', + category: '数据展示', + install(app: App): void { + app.use(Badge as any) + } +} diff --git a/devui/badge/src/badge-types.ts b/devui/badge/src/badge-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0a5ba2fdf16423c9c7d9c949cef2e977eaa4239 --- /dev/null +++ b/devui/badge/src/badge-types.ts @@ -0,0 +1,41 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +type BadgeStatusType = PropType<'danger' | 'warning' | 'waiting' | 'success' | 'info'> +type BadgePositionType = PropType<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'> + +const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info'] +const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] + +export const badgeProps = { + count: { + type: [Number, String] + }, + maxCount: { + type: Number, + default: 99 + }, + showDot: { + type: Boolean, + default: false + }, + status: { + type: String as BadgeStatusType, + validator: (val: string) => badgeStatusType.includes(val) + }, + badgePos: { + type: String as BadgePositionType, + default: 'top-right', + validator: (val: string) => badgePositionType.includes(val) + }, + offsetXY: { + type: Array + }, + bgColor: { + type: String + }, + textColor: { + type: String + } +} + +export type BadgeProps = ExtractPropTypes diff --git a/devui/badge/src/badge.scss b/devui/badge/src/badge.scss new file mode 100644 index 0000000000000000000000000000000000000000..9f72576e39863b6417ab074cc7425a3f5c320a33 --- /dev/null +++ b/devui/badge/src/badge.scss @@ -0,0 +1,76 @@ +@import '../../style/theme/color'; +@import '../../style/theme/font'; + +.devui-badge { + position: relative; + display: inline-block; + + .devui-badge-content { + font-size: $devui-font-size; + color: $devui-light-text; + + &-count { + padding: 0 4px; + min-width: 16px; + height: 16px; + line-height: 16px; + border-radius: 8px; + background: $devui-brand; + text-align: center; + } + + &-danger { + background: $devui-danger; + } + + &-warning { + background: $devui-warning; + } + + &-waiting { + background: $devui-waiting; + } + + &-success { + background: $devui-success; + } + + &-info { + background: $devui-info; + } + + &-top-left { + left: 0; + top: 0; + transform: translate(-50%, -50%); + } + + &-top-right { + right: 0; + top: 0; + transform: translate(50%, -50%); + } + + &-bottom-left { + left: 0; + bottom: 0; + transform: translate(-50%, 50%); + } + + &-bottom-right { + right: 0; + bottom: 0; + transform: translate(50%, 50%); + } + + &-fixed { + position: absolute; + } + + &-dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + } +} diff --git a/devui/badge/src/badge.tsx b/devui/badge/src/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ad6024bd19b25e1ff888002fe2bd261115625e86 --- /dev/null +++ b/devui/badge/src/badge.tsx @@ -0,0 +1,65 @@ +import './badge.scss' + +import { defineComponent, computed } from 'vue' +import { badgeProps, BadgeProps } from './badge-types' + +export default defineComponent({ + name: 'DBadge', + props: badgeProps, + emits: [], + setup(props: BadgeProps, ctx) { + const className = computed(() => { + const base = 'devui-badge-content' + return [ + base, + props.showDot ? `${base}-dot` : `${base}-count`, + props.status && `${base}-${props.status}`, + ctx.slots.default && props.badgePos && `${base}-${props.badgePos}`, + ctx.slots.default && `${base}-fixed` + ].join(' ') + }) + + const style = computed(() => { + const styleMap = { + bgColor: 'background', + textColor: 'color' + } + const ret = Object.keys(styleMap).reduce((ret, key) => { + if (props[key]) { + ret[styleMap[key]] = props[key] + } + return ret + }, {}) + // 偏移量 + if (ctx.slots.default && props.offsetXY) { + const [x, y]: Array = props.offsetXY as Array + const [yName, xName] = (props.badgePos as string).split('-') + ret[yName] = y + 'px' + ret[xName] = x + 'px' + } + + return ret + }) + + const text = computed(() => { + if (props.showDot) { + return + } + if (typeof props.count === 'number' && typeof props.maxCount === 'number') { + return props.count > props.maxCount ? `${props.maxCount}+` : props.count + } + return props.count + }) + + return () => { + return ( +
+ {ctx.slots.default?.()} +
+ {text.value} +
+
+ ) + } + } +}) diff --git a/sites/components/badge/index.md b/sites/components/badge/index.md new file mode 100644 index 0000000000000000000000000000000000000000..f903556b5c3cdbd6adbc34af7522f748b683709a --- /dev/null +++ b/sites/components/badge/index.md @@ -0,0 +1,178 @@ +# Badge 徽标 + +图标右上角的圆形徽标数字。 + +### 何时使用 + +出现在图标右上角或列表项右方,通过不同的状态色加数字提示用户有消息需要处理时。 + +### 基本徽章 + +基本徽章类型,当有包裹元素时在右上角显示徽章和数目。 + +未读消息 +未读消息 +未读消息 +未读消息 + +```html +未读消息 +未读消息 +未读消息 +未读消息 +``` + +### 点状徽章 + +点状徽章类型,当有包裹元素且 showDot 参数为 true 时为点状徽章,默认在右上角展示小点不显示数目。 + +未读消息 +未读消息 + + + + + + + +```html +未读消息 +未读消息 + + + + + + +``` + +### 计数徽章 + +当徽章独立使用且不包裹任何元素时,只展示徽章状态色和数目。 + +
    +
  • + 系统消息 + +
  • +
  • + 个人消息 + +
  • +
+ +```html +
    +
  • + 系统消息 + +
  • +
  • + 个人消息 + +
  • +
+``` + +### 状态徽章 + +当徽章独立使用、不包裹任何元素且 showDot 参数为 true 时为状态徽章,不同状态展示不同色点。 + +  danger
+  warning
+  waiting
+  info
+  success
+ +```html +  danger
+  warning
+  waiting
+  info
+  success
+``` + +### 徽章位置 + +通过 badgePos 参数设置徽章位置。 + +未读消息 +未读消息 + + + + + + +```html +未读消息 +未读消息 + + + + + + +``` + +### 自定义 + +通过 bgColor 参数设置徽章展示状态色(此时 status 参数设置的徽章状态色失效),通过 offsetXY 参数可设置相对于 badgePos 的徽章偏移量。通过 textColor、bgColor 自定义文字、背景颜色。 + + + + + + + +未读消息 +未读消息 + + +```html + + + + + + +未读消息 +未读消息 + +``` + +### API + +| 参数 | 类型 | 默认 | 说明 | +| :-------: | :-----------------: | :---------: | :--------------------------------------------------------------------------------------------------------------------------- | +| count | `Number` | -- | 可选,设置基本徽章和计数徽章中显示的数目 | +| maxCount | `Number` | 99 | 可选,设置基本徽章和计数徽章最大可显示数目,当 count > maxCount 时显示 maxCount+ | +| showDot | `Boolean` | false | 可选,true 时为点状徽章(有包裹)或状态徽章(无包裹),false 时为基本徽章(有包裹)或计数徽章(无包裹) | +| status | `BadgeStatusType` | -- | 可选,状态色 danger\| warning \| waiting \| success \| info | +| badgePos | `BadgePositionType` | 'top-right' | 可选,徽标位置 top-left\| top-right \| bottom-left \| bottom-right | +| bgColor | `String` | -- | 可选,自定义徽标色,此时 status 参数设置的徽章状态色失效 | +| textColor | `String` | -- | 可选, 可自定义徽标文字颜色 | +| offsetXY | `[number, number] ` | -- | 可选,可选,有包裹时徽标位置偏移量,格式为[x,y],单位为 px。x 为相对 right 或 left 的偏移量,y 为相对 top 或 bottom 的偏移量 | + +