From cecaff25d840657aab160e991e2ac16da2bb0c0b Mon Sep 17 00:00:00 2001 From: anlyyao Date: Wed, 6 Apr 2022 15:27:45 +0800 Subject: [PATCH] feat(notice-bar): update props --- example/app.json | 6 +- example/pages/home/data/display.ts | 4 + example/pages/notice-bar/notice-bar.json | 5 + example/pages/notice-bar/notice-bar.less | 27 +++ example/pages/notice-bar/notice-bar.ts | 60 +++++ example/pages/notice-bar/notice-bar.wxml | 156 ++++++++++++ src/common/utils.ts | 15 +- src/notice-bar/README.md | 176 ++++++++++++++ src/notice-bar/notice-bar.json | 7 + src/notice-bar/notice-bar.less | 91 +++++++ src/notice-bar/notice-bar.ts | 287 +++++++++++++++++++++++ src/notice-bar/notice-bar.wxml | 34 +++ 12 files changed, 862 insertions(+), 6 deletions(-) create mode 100644 example/pages/notice-bar/notice-bar.json create mode 100644 example/pages/notice-bar/notice-bar.less create mode 100644 example/pages/notice-bar/notice-bar.ts create mode 100644 example/pages/notice-bar/notice-bar.wxml create mode 100644 src/notice-bar/README.md create mode 100644 src/notice-bar/notice-bar.json create mode 100644 src/notice-bar/notice-bar.less create mode 100644 src/notice-bar/notice-bar.ts create mode 100644 src/notice-bar/notice-bar.wxml diff --git a/example/app.json b/example/app.json index 9d09356c6..aabc045f0 100644 --- a/example/app.json +++ b/example/app.json @@ -54,7 +54,8 @@ "pages/search/search", "pages/home/navigateFail/navigateFail", "pages/navbar/navbar", - "pages/date-time-picker/date-time-picker" + "pages/date-time-picker/date-time-picker", + "pages/notice-bar/notice-bar" ], "usingComponents": { "t-demo": "../../components/demo-block/index", @@ -118,7 +119,8 @@ "t-image": "tdesign-miniprogram/image/image", "t-search": "tdesign-miniprogram/search/search", "t-navbar": "tdesign-miniprogram/navbar/navbar", - "t-date-time-picker": "tdesign-miniprogram/date-time-picker/date-time-picker" + "t-date-time-picker": "tdesign-miniprogram/date-time-picker/date-time-picker", + "t-notice-bar": "tdesign-miniprogram/notice-bar/notice-bar" }, "window": { "backgroundTextStyle": "light", diff --git a/example/pages/home/data/display.ts b/example/pages/home/data/display.ts index bf378b109..d57ff7cc0 100644 --- a/example/pages/home/data/display.ts +++ b/example/pages/home/data/display.ts @@ -78,6 +78,10 @@ const display = { name: 'Sticky', label: '吸顶', }, + { + name: 'NoticeBar', + label: '公告栏', + }, ], }; diff --git a/example/pages/notice-bar/notice-bar.json b/example/pages/notice-bar/notice-bar.json new file mode 100644 index 000000000..ff3d083fb --- /dev/null +++ b/example/pages/notice-bar/notice-bar.json @@ -0,0 +1,5 @@ +{ + "navigationBarTitleText": "NoticeBar", + "navigationBarBackgroundColor": "#fff", + "usingComponents": {} +} diff --git a/example/pages/notice-bar/notice-bar.less b/example/pages/notice-bar/notice-bar.less new file mode 100644 index 000000000..c3cb0fb28 --- /dev/null +++ b/example/pages/notice-bar/notice-bar.less @@ -0,0 +1,27 @@ +.demo { + .demo-section__desc { + margin: 0 32rpx 32rpx; + } + + .box { + margin-bottom: 32rpx; + + .t-class { + color: #ffffff; + background-color: #a6a6a6; + } + .extre { + display: inline-block; + font-weight: 700; + border-bottom: 2rpx solid; + } + + .swiper-wrap { + width: 494rpx; + display: inline-block; + .swiper { + height: 44rpx; + } + } + } +} diff --git a/example/pages/notice-bar/notice-bar.ts b/example/pages/notice-bar/notice-bar.ts new file mode 100644 index 000000000..6a4f54fa5 --- /dev/null +++ b/example/pages/notice-bar/notice-bar.ts @@ -0,0 +1,60 @@ +const swiperList = [ + { + message: 'swiper item0 描述信息', + }, + { + message: 'swiper item1 描述信息', + }, + { + message: 'swiper item2 描述信息', + }, + { + message: 'swiper item3 描述信息', + }, +]; +Page({ + data: { + visible: true, + marquee1: { + speed: 80, + loop: -1, + delay: 0, + }, + marquee2: { + speed: 60, + loop: -1, + delay: 0, + }, + swiperList, + }, + + onReady() { + /** + * notice-bar组件的滚动动画依赖自身样式数据。 + * 页面中有多个滚动notice-bar时,建议用wx:if手动控制,需要显示时渲染组件,保证组件能够成功初始化。 + * */ + }, + + handleExtreText(e) { + console.log('click extre text', e); + }, + + handleSuffixIconCloseDemo(e) { + console.log('click suffix-icon close', e); + this.setData({ + visible: false, + }); + }, + + handleSuffixIconClose(e) { + console.log('click suffix-icon close', e); + }, + + handleSuffixIconLink(e) { + console.log('click suffix-icon link', e); + }, + + clickDetail() { + console.log('click detail text'); + }, +}); diff --git a/example/pages/notice-bar/notice-bar.wxml b/example/pages/notice-bar/notice-bar.wxml new file mode 100644 index 000000000..c6cb043ad --- /dev/null +++ b/example/pages/notice-bar/notice-bar.wxml @@ -0,0 +1,156 @@ + + NoticeBar 公告栏 + 在导航栏下方,用于给用户显示提示消息 + + + + + + + 带图标静态公告栏 + + + + + + + + + 带操作公告栏 + + + + + + 提示文字描述提示文字描述 + 详情 + + + + 滚动公告栏 + + + + + + + + + + + + 自定义样式 + + + + + + + + + + + + + + + + + + + 自定义样式 + + + + + + + + + + + + + + 提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述提示文字描述 + 详情 + + + + + + + + diff --git a/src/common/utils.ts b/src/common/utils.ts index 56a3fb308..7e30fb48e 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -58,6 +58,16 @@ export const requestAnimationFrame = function (cb: Function) { }); }; +export const getRect = function (context: any, selector: string) { + return new Promise((resolve) => { + wx.createSelectorQuery() + .in(context) + .select(selector) + .boundingClientRect() + .exec((rect = []) => resolve(rect[0])); + }); +}; + const isDef = function (value: any): boolean { return value !== undefined && value !== null; }; @@ -80,10 +90,7 @@ export const addUnit = function (value?: string | number): string | undefined { * @param maxCharacter 规定最大字符串长度 * @returns 当没有传入maxCharacter时返回字符串字符长度,当传入maxCharacter时返回截取之后的字符串和长度。 */ -export const getCharacterLength = ( - str: string, - maxCharacter?: number, -): { length: number; characters: string } => { +export const getCharacterLength = (str: string, maxCharacter?: number): { length: number; characters: string } => { const hasMaxCharacter = typeof maxCharacter === 'number'; if (!str || str.length === 0) { if (hasMaxCharacter) { diff --git a/src/notice-bar/README.md b/src/notice-bar/README.md new file mode 100644 index 000000000..66f4418b9 --- /dev/null +++ b/src/notice-bar/README.md @@ -0,0 +1,176 @@ +--- +title: NoticeBar 公告栏 +description: 在导航栏下方,用于给用户显示提示消息。 +spline: notice-bar +isComponent: true +--- + +## 引入 + +全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。 + +```json +"usingComponents": { + "t-notice-bar": "tdesign-miniprogram/notice-bar/notice-bar" +} +``` + +## 代码演示 + +### 基础静态公告栏 +```xml + + +``` + +### 带图标静态公告栏 +```xml + + + + + + +``` + +### 带操作公告栏 +```xml + + + + 带关闭和详情的公告栏 + 详情 + +``` + +### 滚动公告栏 + +```xml + + + +``` + +### 自定义样式 +```xml + +``` + +### 不同状态的公告栏 +公告栏类型有普通(info)、警示(warning)、成功(success)、错误(error) + +#### 默认状态公告栏 +```xml + +``` + +#### 警示状态公告栏 +```xml + +``` + +#### 成功状态公告栏 +```xml + +``` + +#### 错误状态公告栏 +```xml + +``` + +### 多行文字消息栏 +```xml + + + + + + + + + 带suffix-icon操作的多行文字消息栏,slot实现自定义content内容 + + 详情 + + + + + + +``` + +## API + +### Message Props + +| 名称 | 类型 | 默认值 | 说明 | 必传 | +| ---------------- | ----------------------- | ------ | ---------------------------------------------------- | ---- | +| align | String | middle | 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom | N | +| content | String / Slot | - | 用于自定义公告栏内容 | N | +| marquee | Boolean / Object | false | 跑马灯效果。speed 指速度控制;loop 指循环播放次数,值为 -1 表示循环播放,值为 0 表示不循环播放;delay 表示延迟多久开始播放。 | N | +| theme | String | info | 消息组件风格。可选项:info/success/warning/error。 | N | +| visible | Boolean | false | 是否显示,隐藏时默认销毁组 | N | +| icon | String / Boolean / Slot | true | 消息提醒前面的图标。值为 true 则根据 theme 显示对应的图标,值为 false 则不显示图标。也可以完全自定义图标节点。| N | +| extre | String/Slot | - | 右侧extre内容。 +| suffixIcon | String / Boolean / Slot | false | 消息提醒后缀图标。值为 true 则根据 theme 显示对应的图标,值为 false 则不显示图标。也可以完全自定义图标节点。 | N | +| wrapable | Boolean | false | 是否开启文本换行,只在禁用marquee时有效| N | +| z-index | Number | - | 元素层级,样式默认为 5000 | N | +| external-classes | Array | - | 样式类名,分别用于设置 组件外层、消息内容、左侧图标、右侧图标等元素类名。`['t-class', 't-class-icon','t-class-content','t-class-extre','t-alcas-suffix-icon']` | N | + + +### Message Events + +| 名称 | 参数 | 描述 | +| ----------- | ---- | --------------------------- | +| extre | - | 点击`extre`内容触发`extre` | +| suffix-icon | - | 点击后缀图标触发`suffix-icon` | +| duration-end| - | 计时结束后触发 | diff --git a/src/notice-bar/notice-bar.json b/src/notice-bar/notice-bar.json new file mode 100644 index 000000000..ef9e100f2 --- /dev/null +++ b/src/notice-bar/notice-bar.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "t-icon": "../icon/icon", + "t-button": "../button/button" + } +} diff --git a/src/notice-bar/notice-bar.less b/src/notice-bar/notice-bar.less new file mode 100644 index 000000000..fb777c97b --- /dev/null +++ b/src/notice-bar/notice-bar.less @@ -0,0 +1,91 @@ +@import '../common/style/index.less'; + +@notice-bar-height: 92rpx; +@notice-bar-font-size: 28rpx; +@notice-bar-horizen-padding: 32rpx; +@notice-bar-vertical-padding: 24rpx; +@notice-bar-line-height: 44rpx; +@notice-bar-icon-size: 44rpx; + +// color +@notice-bar-info-background-color: #ecf2fe; +@notice-bar-info-color: #0052d9; +@notice-bar-success-background-color: #e8f8f2; +@notice-bar-success-color: #00a870; +@notice-bar-warning-background-color: #fef3e6; +@notice-bar-warning-color: #ed7b2f; +@notice-bar-error-background-color: #fdecee; +@notice-bar-error-color: #e34d59; +@notice-bar-z-index: 15000; +@notice-bar: ~'@{prefix}-notice-bar'; + +.@{notice-bar} { + display: flex; + padding: @notice-bar-vertical-padding @notice-bar-horizen-padding; + font-size: @notice-bar-font-size; + z-index: @notice-bar-z-index; + + &__content-wrap { + flex: 1; + overflow-x: hidden; + line-height: @notice-bar-line-height; + } + + &__content { + display: inline-block; + white-space: nowrap; + } + + // 多行文本 + &__content-wrapable { + white-space: normal; + } + + &__icon, + &__suffix-icon { + font-size: 44rpx; + } + + &__icon:not(:empty) { + margin-right: 16rpx; + } + + &__extre:not(:empty), + &__suffix-icon:not(:empty) { + margin-left: 16rpx; + } + + // theme + &--info { + color: @notice-bar-info-color; + background-color: @notice-bar-info-background-color; + } + + &--success { + color: @notice-bar-success-color; + background-color: @notice-bar-success-background-color; + } + + &--warning { + color: @notice-bar-warning-color; + background-color: @notice-bar-warning-background-color; + } + + &--error { + color: @notice-bar-error-color; + background-color: @notice-bar-error-background-color; + } + + // align + &--middle { + align-items: center; + } + + &--top { + align-items: flex-start; + } + + &--bottom { + align-items: flex-end; + } +} diff --git a/src/notice-bar/notice-bar.ts b/src/notice-bar/notice-bar.ts new file mode 100644 index 000000000..447a8fe37 --- /dev/null +++ b/src/notice-bar/notice-bar.ts @@ -0,0 +1,287 @@ +import { SuperComponent, wxComponent } from '../common/src/index'; +import { getRect, requestAnimationFrame } from '../common/utils'; +import config from '../common/config'; + +const { prefix } = config; +const name = `${prefix}-notice-bar`; + +@wxComponent() +export default class NoticeBar extends SuperComponent { + externalClasses = [ + `${prefix}-class`, + `${prefix}-class-content`, + `${prefix}-class-icon`, + `${prefix}-class-extre`, + `${prefix}-class-suffix-icon`, + ]; + + options = { + styleIsolation: 'apply-shared' as const, + multipleSlots: true, + }; + + properties = { + /** 内容的对齐方式,默认居中对齐。可选项:top/middle/bottom */ + align: { + type: String, + value: 'middle', + }, + + /** 用于自定义消息内容 */ + content: { + type: String, + }, + + /** 消息内置计时器,计时到达时会触发 duration-end 事件。单位:毫秒。值为 0 则表示没有计时器。 */ + duration: { + type: Number, + value: 3000, + }, + + /** 跑马灯效果。speed 指速度控制;loop 指循环播放次数,值为 -1 表示循环播放,值为 0 表示不循环播放;delay 表示延迟多久开始播放 */ + marquee: { + type: Boolean, + optionalTypes: [Object], + value: false, + }, + + /** 公告栏组件风格。 可选项:'info' | 'success' | 'warning' | 'error' */ + theme: { + type: String, + value: 'info', + }, + + /** 是否显示,隐藏时默认销毁组件 */ + visible: { + type: Boolean, + value: false, + }, + + /** 消息提醒前面的图标。值为 true 则根据 theme 显示对应的图标,值为 false 则不显示图标。值为 'info' 或 'bell' 则显示组件内置图标。也可以完全自定义图标节点 */ + icon: { + type: String, + optionalTypes: [Boolean], + value: true, + }, + + extre: { + type: String, + }, + + /** 通知栏模式,可选值为 closeable link */ + suffixIcon: { + type: String, + }, + + /** 是否开启文本换行,只在禁用marquee时生效 */ + wrapable: { + type: Boolean, + value: false, + }, + + /** 元素层级,样式默认为 5000 */ + zIndex: { + type: Number, + value: 5000, + }, + }; + + data = { + prefix, + classPrefix: name, + icon: '', + loop: -1, + }; + + observers = { + marquee(val) { + if (JSON.stringify(val) === '{}' || JSON.stringify(val) === 'true') { + this.setData({ + marquee: { + speed: 50, + loop: -1, + delay: 0, + }, + }); + } + }, + + visible(visible) { + if (!visible) { + this.reset(); + } + }, + }; + + ready() { + this.show(); + } + + created() { + this.resetAnimation = wx.createAnimation({ + duration: 0, + timingFunction: 'linear', + }); + } + + detached() { + this.reset(); + } + + methods = { + initAnimation() { + // 获取外部容器和滚动内容的宽度 + const warpID = `.${name}__content-wrap`; + const nodeID = `.${name}__content`; + requestAnimationFrame(() => { + Promise.all([getRect(this, nodeID), getRect(this, warpID)]).then(([nodeRect, wrapRect]) => { + const { marquee } = this.properties; + const speeding = marquee.speed; + const delaying = marquee.delay ? marquee.delay : 0; + const loops = marquee.loop - 1; + + if (nodeRect == null || wrapRect == null || !nodeRect.width || !wrapRect.width) { + return; + } + + const animationDuration = ((wrapRect.width + nodeRect.width) / speeding) * 1000; + const firstanimationDuration = (nodeRect.width / speeding) * 1000; + + this.setData({ + wrapWidth: Number(wrapRect.width), + nodeWidth: Number(nodeRect.width), + animationDuration: animationDuration, + delay: delaying, + loop: loops, + firstanimationDuration: firstanimationDuration, + }); + + this.startScrollAnimation(true); + }); + }); + }, + + startScrollAnimation(isFirstScroll = false) { + if (this.nextAnimationContext) { + this.clearNoticeBarAnimation(); + } + + const { wrapWidth, nodeWidth, firstanimationDuration, animationDuration, delay } = this.data; + + const delayTime = isFirstScroll ? delay : 0; + const durationTime = isFirstScroll ? firstanimationDuration : animationDuration; + + // 滚动内容: 初始位置 + this.setData({ + animationData: this.resetAnimation + .translateX(isFirstScroll ? 0 : wrapWidth) + .step() + .export(), + }); + + requestAnimationFrame(() => { + // 滚动内容: 最终位置 + this.setData({ + // FIXME: 增加delay + // 问题: 加入delay会导致首次滚动在durationTime时间内,content内容消失 + animationData: wx + .createAnimation({ duration: durationTime, timingFunction: 'linear', delay: delayTime }) + .translateX(-nodeWidth) + .step() + .export(), + }); + }); + + // 滚动一次完成, 开启下一次的滚动 + this.nextAnimationContext = setTimeout(() => { + if (this.data.loop > 0) { + this.data.loop -= 1; + this.startScrollAnimation(); + } else if (this.data.loop === 0) { + // 动画回到初始位置 + this.setData({ animationData: this.resetAnimation.translateX(0).step().export() }); + } else if (this.data.loop < 0) { + this.startScrollAnimation(); + } + }, durationTime); + }, + + show() { + this.reset(); + this.setIcon(); + const { marquee, duration } = this.properties; + + if (marquee) { + this.initAnimation(); + } + + // 计时到达,触发 duration-end 事件 + if (duration && duration > 0) { + this.closeTimeoutContext = setTimeout(() => { + this.hide(); + this.triggerEvent('duration-end', { self: this }); + }, duration) as unknown as number; + } + }, + + hide() { + this.reset(); + this.setData({ + visible: false, + }); + }, + + // 重置定时器 + reset() { + if (this.nextAnimationContext) { + this.clearNoticeBarAnimation(); + } + this.closeTimeoutContext && clearTimeout(this.closeTimeoutContext); + this.closeTimeoutContext = null; + }, + + /** 清除动画 */ + clearNoticeBarAnimation() { + this.nextAnimationContext && clearTimeout(this.nextAnimationContext); + this.nextAnimationContext = null; + }, + + /** icon 值设置 */ + setIcon(icon = this.properties.icon) { + // 使用空值 + if (!icon) { + this.setData({ iconName: '' }); + return; + } + // 固定值 + if (typeof icon === 'string') { + this.setData({ + iconName: `${icon}`, + }); + return; + } + + // 使用默认值 + if (icon) { + let nextValue = 'notification'; + const { theme } = this.properties; + const themeMessage = { + info: 'error-circle-filled', + success: 'check-circle-filled', + warning: 'error-circle-filled', + error: 'close-circle-filled', + }; + nextValue = themeMessage[theme]; + this.setData({ iconName: nextValue }); + } + }, + + handleSuffixIcon() { + this.triggerEvent('suffix-icon', { self: this }); + }, + + handleExtre() { + this.triggerEvent('extre', { self: this }); + }, + }; +} diff --git a/src/notice-bar/notice-bar.wxml b/src/notice-bar/notice-bar.wxml new file mode 100644 index 000000000..6a8fea95c --- /dev/null +++ b/src/notice-bar/notice-bar.wxml @@ -0,0 +1,34 @@ + + + + + + + + + + + {{content}} + + + + + + + {{extre}} + + + + + + + + +