feat: 重构 noticebar (#1062)

This commit is contained in:
DiamondYuan 2024-01-25 16:03:05 +08:00 committed by GitHub
parent d7e466f37b
commit 6444cba7cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 905 additions and 402 deletions

View File

@ -6,3 +6,4 @@ src/.umi-production/**
dist/**
docs-dist/**
node_modules
compiled/

View File

@ -2,25 +2,25 @@
"env": {
"node": true
},
"globals": {
"my": true,
"App": true,
"Component": true,
"Page": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/no-empty-function": "warn",
"@typescript-eslint/no-empty-interface": "warn"
}
}
"globals": {
"wx": true,
"my": true,
"App": true,
"Component": true,
"Page": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-var-requires": "warn",
"@typescript-eslint/no-empty-function": "warn",
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/ban-ts-comment": "off"
}
}

View File

@ -36,6 +36,22 @@
</block>
</container>
<container title="可滚动通告栏(不循环)">
<block
a:for="{{ typeList }}"
a:for-index="index"
a:for-item="item">
<notice
type="{{ item }}"
style="margin-bottom: 8px"
enableMarquee="{{ true }}"
onTap="handleTapLink"
mode="link">
文本溢出时,开启循环滚动。文字不够继续添加文字凑数。
</notice>
</block>
</container>
<container title="自定义通告栏">
<notice style="margin-bottom: 8px">
不展示图标

View File

@ -3,20 +3,17 @@ Page({
typeList: ['default', 'error', 'info', 'primary'],
},
handleTapAction() {
my.showToast({
content: `点击按钮`,
duration: 1000,
});
this.showToast('点击按钮');
},
handleTapLink() {
my.showToast({
content: 'link 类型被点击了',
duration: 1000,
});
this.showToast('link 类型被点击了');
},
handleClose() {
this.showToast('点击关闭');
},
showToast(content) {
my.showToast({
content: `点击关闭`,
content: content,
duration: 1000,
});
},

View File

@ -16,9 +16,10 @@
type="SoundOutline" />
</view>
</slot>
<view class="ant-notice-bar-content ant-notice-bar-content-{{ $id }}">
<view
class="ant-notice-bar-content ant-notice-bar-content{{ $id ? '-' + $id : '' }}">
<view
class="ant-notice-bar-marquee ant-notice-bar-marquee-{{ $id }}"
class="ant-notice-bar-marquee ant-notice-bar-marquee{{ $id ? '-' + $id : '' }}"
style="{{ marqueeStyle }} display: {{ enableMarquee ? 'inline-block' : 'block' }}"
onTransitionEnd="onTransitionEnd">
<slot />

View File

@ -1,155 +1,126 @@
import { NoticeBarDefaultProps } from './props';
import { log } from '../_util/console';
import { IBoundingClientRect } from '../_util/base';
import {
useState,
useEffect,
useEvent,
usePageShow,
} from 'functional-mini/component';
import '../_util/assert-component2';
import { IBoundingClientRect } from '../_util/base';
import { mountComponent } from '../_util/component';
import { useComponentEvent } from '../_util/hooks/useComponentEvent';
import { useInstanceBoundingClientRect } from '../_util/hooks/useInstanceBoundingClientRect';
import { INoticeBarProps, NoticeBarFunctionalProps } from './props';
import { useEvent as useStableCallback } from '../_util/hooks/useEvent';
Component({
props: NoticeBarDefaultProps,
data: {
show: true,
marqueeStyle: '',
animatedWidth: 0,
overflowWidth: 0,
duration: 0,
viewWidth: 0,
},
didMount() {
const { enableMarquee } = this.props;
this.showError();
const NoticeBar = (props: INoticeBarProps) => {
const [marqueeStyle, setMarqueeStyle] = useState('');
const [show, setShow] = useState(true);
if (enableMarquee) {
this.measureText(this.startMarquee.bind(this));
const { triggerEventOnly } = useComponentEvent(props);
const startMarquee = useStableCallback((state) => {
const { loop } = props;
const leading = 500;
const { duration, overflowWidth, viewWidth } = state;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
},
const newMarqueeStyle = `transform: translate3d(${-marqueeScrollWidth}px, 0, 0); transition: ${duration}s all linear ${
typeof leading === 'number' ? `${leading / 1000}s` : '0s'
};`;
setMarqueeStyle(newMarqueeStyle);
});
didUpdate() {
const { enableMarquee } = this.props;
this.showError();
// 这里更新处理的原因是防止notice内容在动画过程中发生改变。
if (enableMarquee) {
this.measureText(this.startMarquee.bind(this));
}
},
const { getBoundingClientRectWithId } = useInstanceBoundingClientRect();
function measureText(callback) {
const fps = 40;
const { loop } = props;
// 计算文本所占据的宽度,计算需要滚动的宽度
setTimeout(async () => {
const marqueeSize: IBoundingClientRect | null =
await getBoundingClientRectWithId('.ant-notice-bar-marquee');
const contentSize: IBoundingClientRect | null =
await getBoundingClientRectWithId('.ant-notice-bar-content');
const overflowWidth =
(marqueeSize && contentSize && marqueeSize.width - contentSize.width) ||
0;
pageEvents: {
onShow() {
this.resetState();
},
},
methods: {
resetState() {
if (this.props.enableMarquee) {
this.setData(
{
marqueeStyle: '',
animatedWidth: 0,
overflowWidth: 0,
duration: 0,
viewWidth: 0,
},
() => {
this.resetMarquee();
this.measureText(this.startMarquee.bind(this));
}
);
}
},
showError() {
const { actions } = this.props;
if (!Array.isArray(actions) && typeof actions !== 'undefined') {
log.warn(
'NoticeBar',
`当前定义的 actions 的类型为 ${typeof actions},不符合属性定义,应该为数组,如:actions="{{['值', '值']}}`
);
}
},
onTap() {
const { mode, onTap } = this.props;
if (mode === 'link' && typeof onTap === 'function') {
return onTap();
}
if (mode === 'closeable' && typeof onTap === 'function') {
this.setData({
show: false,
});
return onTap();
}
},
// 文本滚动的计算
resetMarquee() {
const { loop } = this.props;
const { viewWidth } = this.data;
let showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = `${viewWidth}px`;
}
const marqueeStyle = `transform: translate3d(${showMarqueeWidth}, 0, 0); transition: 0s all linear;`;
this.setData({
marqueeStyle,
});
},
startMarquee() {
const { loop } = this.props;
const leading = 500;
const { duration, overflowWidth, viewWidth } = this.data;
const viewWidth = contentSize?.width || 0;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
const marqueeStyle = `transform: translate3d(${-marqueeScrollWidth}px, 0, 0); transition: ${duration}s all linear ${
typeof leading === 'number' ? `${leading / 1000}s` : '0s'
};`;
if (this.data.marqueeStyle !== marqueeStyle) {
this.setData({
marqueeStyle,
if (overflowWidth > 0) {
callback({
overflowWidth,
viewWidth,
duration: marqueeScrollWidth / fps,
});
}
},
onTransitionEnd() {
const { loop } = this.props;
const trailing = 200;
if (loop) {
setTimeout(() => {
this.resetMarquee();
this.measureText(this.startMarquee.bind(this));
}, trailing);
}
},
measureText(callback) {
const fps = 40;
const { loop } = this.props;
// 计算文本所占据的宽度,计算需要滚动的宽度
}, 0);
}
useEffect(() => {
const { enableMarquee } = props;
if (enableMarquee) {
measureText(startMarquee);
}
});
function resetMarquee(state) {
const { loop } = props;
const { viewWidth } = state;
let showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = `${viewWidth}px`;
}
const marqueeStyle = `transform: translate3d(${showMarqueeWidth}, 0, 0); transition: 0s all linear;`;
setMarqueeStyle(marqueeStyle);
}
useEvent('onTransitionEnd', () => {
const { loop } = props;
const trailing = 200;
if (loop) {
setTimeout(() => {
my.createSelectorQuery()
.select(`.ant-notice-bar-marquee-${this.$id}`)
.boundingClientRect()
.select(`.ant-notice-bar-content-${this.$id}`)
.boundingClientRect()
.exec((ret) => {
// eslint-disable-next-line max-len
const overflowWidth =
(ret &&
ret[0] &&
ret[1] &&
(<IBoundingClientRect>ret[0]).width -
(<IBoundingClientRect>ret[1]).width) ||
0;
const viewWidth = (<IBoundingClientRect>ret[1])?.width || 0;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
if (overflowWidth > 0) {
this.setData({
overflowWidth,
viewWidth,
duration: marqueeScrollWidth / fps,
});
callback();
}
});
}, 0);
},
},
});
measureText((state) => {
resetMarquee(state);
startMarquee(state);
});
}, trailing);
}
});
useEvent('onTap', () => {
const { mode } = props;
if (mode === 'link') {
triggerEventOnly('tap');
}
if (mode === 'closeable') {
if (typeof props.onTap !== 'function') {
return;
}
setShow(false);
triggerEventOnly('tap');
}
});
usePageShow(() => {
if (props.enableMarquee) {
setMarqueeStyle('');
resetMarquee({
overflowWidth: 0,
duration: 0,
viewWidth: 0,
});
measureText(startMarquee);
}
});
return {
marqueeStyle,
show,
};
};
mountComponent(NoticeBar, NoticeBarFunctionalProps);

View File

@ -7,10 +7,6 @@ import { IBaseProps } from '../_util/base';
*/
export interface INoticeBarProps extends IBaseProps {
/**
*
*/
show?: boolean;
/**
* @description
*/
@ -24,16 +20,7 @@ export interface INoticeBarProps extends IBaseProps {
/**
* @description link closeable x
*/
mode: 'link' | 'closeable';
/**
* @description action和mode可以同时搭配使用
*/
actions: string[];
/**
* @description
*/
marqueeStyle?: boolean;
mode?: 'link' | 'closeable';
/**
* @description
* @default false
@ -64,3 +51,11 @@ export const NoticeBarDefaultProps: Partial<INoticeBarProps> = {
loop: false,
type: 'default',
};
export const NoticeBarFunctionalProps: Partial<INoticeBarProps> = {
icon: '',
type: 'default',
mode: null,
enableMarquee: false,
loop: false,
};

View File

@ -0,0 +1,22 @@
import { useComponent } from 'functional-mini/component';
import { getInstanceBoundingClientRect } from '../jsapi/get-instance-bounding-client-rect';
export const useInstanceBoundingClientRect = () => {
const instance = useComponent();
function getInstance() {
if (instance.$id) {
return my;
}
return instance;
}
async function getBoundingClientRectWithId(prefix: string) {
return await getInstanceBoundingClientRect(
getInstance(),
`${prefix}${instance.$id ? `-${instance.$id}` : ''}`
);
}
return {
getBoundingClientRectWithId,
};
};

View File

@ -1,5 +1,6 @@
{
"pages": [
"demo/pages/NoticeBar/index",
"demo/pages/ProgressCircle/index",
"demo/pages/ProgressLine/index",
"demo/pages/Empty/index",

View File

@ -0,0 +1,20 @@
Page({
data: {
typeList: ['default', 'error', 'info', 'primary'],
},
handleTapAction: function () {
this.showToast('点击按钮');
},
handleTapLink: function () {
this.showToast('link 类型被点击了');
},
handleClose: function () {
this.showToast('点击关闭');
},
showToast: function (content) {
//@ts-ignore
wx.showToast({
title: content,
});
},
});

View File

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "Notice",
"usingComponents": {
"notice": "../../../src/NoticeBar/index",
"container": "../../../src/Container/index",
"am-icon": "../../../src/Icon/index"
}
}

View File

@ -0,0 +1,89 @@
<container title="基础用法">
<block
wx:for="{{ typeList }}"
wx:for-index="index"
wx:for-item="item">
<notice
style="margin-bottom: 8px"
type="{{ item }}">
{{ item }}
</notice>
</block>
</container>
<container title="可关闭通告栏">
<notice
bind:tap="handleClose"
mode="closeable">
这条通知可以关闭
</notice>
</container>
<container title="可滚动通告栏">
<block
wx:for="{{ typeList }}"
wx:for-index="index"
wx:for-item="item">
<notice
type="{{ item }}"
style="margin-bottom: 8px"
enableMarquee="{{ true }}"
loop="{{ true }}"
bind:tap="handleTapLink"
mode="link">
文本溢出时,开启循环滚动。文字不够继续添加文字凑数。
</notice>
</block>
</container>
<container title="可滚动通告栏(不循环)">
<block
wx:for="{{ typeList }}"
wx:for-index="index"
wx:for-item="item">
<notice
type="{{ item }}"
style="margin-bottom: 8px"
enableMarquee="{{ true }}"
bind:tap="handleTapLink"
mode="link">
文本溢出时,开启循环滚动。文字不够继续添加文字凑数。
</notice>
</block>
</container>
<container title="自定义通告栏">
<notice style="margin-bottom: 8px">
不展示图标
<view slot="icon" />
</notice>
<notice
type="primary"
icon="GlobalOutline"
style="margin-bottom: 8px"
mode="link">
自定义左侧图标
</notice>
<notice
type="primary"
icon="https://gw.alipayobjects.com/mdn/rms_ce4c6f/afts/img/A*XMCgSYx3f50AAAAAAAAAAABkARQnAQ"
style="margin-bottom: 8px"
mode="link">
自定义左侧图标图片
</notice>
<notice
mode="link"
style="margin-bottom: 8px"
bind:tap="handleTapLink">
自定义右侧按钮
<view
slot="extra"
class="extra">
<view bind:tap="handleTapAction">不再提示</view>
<view bind:tap="handleTapAction">查看详情</view>
</view>
</notice>
</container>

View File

@ -0,0 +1,7 @@
.extra {
display: flex;
}
.extra > view {
white-space: nowrap;
margin-left: 8rpx;
}

View File

@ -4,7 +4,7 @@
"async-validator": "^4.0.7",
"dayjs": "^1.11.10",
"fast-deep-equal": "3.1.3",
"functional-mini": "^0.16.0",
"functional-mini": "^0.17.0",
"tslib": "2.5.0"
},
"repository": "git@github.com:ant-design/ant-design-mini.git"

View File

@ -0,0 +1,148 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { useState, useEffect, useEvent, usePageShow, } from 'functional-mini/component';
import '../_util/assert-component2';
import { mountComponent } from '../_util/component';
import { useComponentEvent } from '../_util/hooks/useComponentEvent';
import { useInstanceBoundingClientRect } from '../_util/hooks/useInstanceBoundingClientRect';
import { NoticeBarFunctionalProps } from './props';
import { useEvent as useStableCallback } from '../_util/hooks/useEvent';
var NoticeBar = function (props) {
var _a = useState(''), marqueeStyle = _a[0], setMarqueeStyle = _a[1];
var _b = useState(true), show = _b[0], setShow = _b[1];
var triggerEventOnly = useComponentEvent(props).triggerEventOnly;
var startMarquee = useStableCallback(function (state) {
var loop = props.loop;
var leading = 500;
var duration = state.duration, overflowWidth = state.overflowWidth, viewWidth = state.viewWidth;
var marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
var newMarqueeStyle = "transform: translate3d(".concat(-marqueeScrollWidth, "px, 0, 0); transition: ").concat(duration, "s all linear ").concat(typeof leading === 'number' ? "".concat(leading / 1000, "s") : '0s', ";");
setMarqueeStyle(newMarqueeStyle);
});
var getBoundingClientRectWithId = useInstanceBoundingClientRect().getBoundingClientRectWithId;
function measureText(callback) {
var _this = this;
var fps = 40;
var loop = props.loop;
// 计算文本所占据的宽度,计算需要滚动的宽度
setTimeout(function () { return __awaiter(_this, void 0, void 0, function () {
var marqueeSize, contentSize, overflowWidth, viewWidth, marqueeScrollWidth;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getBoundingClientRectWithId('.ant-notice-bar-marquee')];
case 1:
marqueeSize = _a.sent();
return [4 /*yield*/, getBoundingClientRectWithId('.ant-notice-bar-content')];
case 2:
contentSize = _a.sent();
overflowWidth = (marqueeSize && contentSize && marqueeSize.width - contentSize.width) ||
0;
viewWidth = (contentSize === null || contentSize === void 0 ? void 0 : contentSize.width) || 0;
marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
if (overflowWidth > 0) {
callback({
overflowWidth: overflowWidth,
viewWidth: viewWidth,
duration: marqueeScrollWidth / fps,
});
}
return [2 /*return*/];
}
});
}); }, 0);
}
useEffect(function () {
var enableMarquee = props.enableMarquee;
if (enableMarquee) {
measureText(startMarquee);
}
});
function resetMarquee(state) {
var loop = props.loop;
var viewWidth = state.viewWidth;
var showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = "".concat(viewWidth, "px");
}
var marqueeStyle = "transform: translate3d(".concat(showMarqueeWidth, ", 0, 0); transition: 0s all linear;");
setMarqueeStyle(marqueeStyle);
}
useEvent('onTransitionEnd', function () {
var loop = props.loop;
var trailing = 200;
if (loop) {
setTimeout(function () {
measureText(function (state) {
resetMarquee(state);
startMarquee(state);
});
}, trailing);
}
});
useEvent('onTap', function () {
var mode = props.mode;
if (mode === 'link') {
triggerEventOnly('tap');
}
if (mode === 'closeable') {
setShow(false);
triggerEventOnly('tap');
}
});
usePageShow(function () {
if (props.enableMarquee) {
setMarqueeStyle('');
resetMarquee({
overflowWidth: 0,
duration: 0,
viewWidth: 0,
});
measureText(startMarquee);
}
});
return {
marqueeStyle: marqueeStyle,
show: show,
};
};
mountComponent(NoticeBar, NoticeBarFunctionalProps);

View File

@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"icon": "../Icon/index",
"image-icon": "../ImageIcon/index"
}
}

View File

@ -0,0 +1,35 @@
---
nav:
path: /components
group:
title: 引导提示
order: 14
toc: 'content'
---
# NoticeBar 通告栏
<code src="../../docs/components/compatibility.tsx" inline="true"></code>
展示一组消息通知
## 何时使用
用于当前页面内信息的通知,是一种较醒目的页面内通知方式
## 代码示例
<code src='pages/NoticeBar/index'></code>
## API
| 属性 | 说明 | 类型 | 默认值 |
| -----|-----|-----|-----|
| className | 类名 | string | - |
| enableMarquee | 是否开启滚动动画 | boolean | false |
| extra | 自定义右侧内容 | slot | - |
| icon | 左侧icon,支持所有内置 iconType 和自定义链接,也支持自定义slot | slot \| string | - |
| loop | 是否循环滚动,enableMarquee 为 true 时有效 | boolean | false |
| mode | 通告类型,`link` 表示连接,整行可点;`closeable` 表示点击 x 可以关闭;不填时表示你右侧没有图标 | string | - |
| style | 样式 | string | - |
| title | 标题 | string\|slot | - |
| type | 类型,可选 `default`, `error`, `primary`, `info` | string | default |
| onTap | 点击通知栏右侧的图标(箭头或者叉),触发回调 | ()=>void | - |

View File

@ -0,0 +1,39 @@
<view
wx:if="{{ show }}"
class="ant-notice-bar {{ className || '' }} {{ type ? 'ant-notice-bar-' + type : '' }}"
style="{{ style }}">
<view class="ant-notice-bar-icon">
<image-icon
wx:if="{{ icon }}"
image="{{ icon }}"
className="ant-notice-bar-icon-image" />
<icon
wx:elif="{{ type === 'error' }}"
type="InformationCircleOutline" />
<icon
wx:else
type="SoundOutline" />
</view>
<view
class="ant-notice-bar-content ant-notice-bar-content{{ $id ? '-' + $id : '' }}">
<view
class="ant-notice-bar-marquee ant-notice-bar-marquee{{ $id ? '-' + $id : '' }}"
style="{{ marqueeStyle }} display: {{ enableMarquee ? 'inline-block' : 'block' }}"
onTransitionEnd="onTransitionEnd">
<slot />
</view>
</view>
<view class="ant-notice-bar-operation">
<slot name="extra" />
<icon
wx:if="{{ mode === 'link' }}"
className="ant-notice-bar-operation-icon"
type="RightOutline"
bind:tap="onTap" />
<icon
wx:if="{{ mode === 'closeable' }}"
className="ant-notice-bar-operation-icon"
type="CloseOutline"
bind:tap="onTap" />
</view>
</view>

View File

@ -0,0 +1,68 @@
.ant-notice-bar {
position: relative;
display: flex;
height: 37.5px;
align-items: center;
overflow: hidden;
padding: 8px 12px;
font-size: 15px;
color: #ff6010;
background-color: #fff9ed;
box-sizing: border-box;
}
.ant-notice-bar-error {
color: #ffffff;
background-color: #ff3141;
}
.ant-notice-bar-error-scroll-left,
.ant-notice-bar-error-scroll-right {
background: #ff3141;
}
.ant-notice-bar-primary {
color: #1677ff;
background-color: #d0e4ff;
}
.ant-notice-bar-primary-scroll-left,
.ant-notice-bar-primary-scroll-right {
background: #d0e4ff;
}
.ant-notice-bar-info {
color: #ffffff;
background: #666666;
}
.ant-notice-bar-info-scroll-left,
.ant-notice-bar-info-scroll-right {
background: #666666;
}
.ant-notice-bar-icon {
margin-right: 8px;
font-size: 18px;
}
.ant-notice-bar-icon-image-image {
width: 18px;
height: 18px;
}
.ant-notice-bar-content {
position: relative;
z-index: 2;
flex: 1 100%;
overflow: hidden;
vertical-align: middle;
line-height: 1.4;
}
.ant-notice-bar-marquee {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
.ant-notice-bar-operation {
display: flex;
align-items: center;
}
.ant-notice-bar-operation-icon {
margin-left: 12px;
}
.ant-icon-size-x-small {
font-size: 18px;
}

View File

@ -0,0 +1,12 @@
export var NoticeBarDefaultProps = {
enableMarquee: false,
loop: false,
type: 'default',
};
export var NoticeBarFunctionalProps = {
icon: '',
type: 'default',
mode: null,
enableMarquee: false,
loop: false,
};

View File

@ -0,0 +1,60 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
import { useComponent } from 'functional-mini/component';
import { getInstanceBoundingClientRect } from '../jsapi/get-instance-bounding-client-rect';
export var useInstanceBoundingClientRect = function () {
var instance = useComponent();
function getInstance() {
if (instance.$id) {
return my;
}
return instance;
}
function getBoundingClientRectWithId(prefix) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getInstanceBoundingClientRect(getInstance(), "".concat(prefix).concat(instance.$id ? "-".concat(instance.$id) : ''))];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
}
return {
getBoundingClientRectWithId: getBoundingClientRectWithId,
};
};

View File

@ -1,5 +1,6 @@
{
"pages": [
"pages/NoticeBar",
"pages/ProgressLine",
"pages/ProgressCircle",
"pages/Empty",
@ -107,6 +108,7 @@
"Pagination",
"Badge",
"TabBar",
"Progress"
"Progress",
"NoticeBar"
]
}

View File

@ -33,6 +33,20 @@ export default ({ typeList }: InternalData) => (
))}
</Container>
<Container title="可滚动通告栏(不循环)">
{typeList.map((item) => (
<Notice
type={item}
style="margin-bottom: 8px"
enableMarquee={true}
onTap="handleTapLink"
mode="link"
>
</Notice>
))}
</Container>
<Container title="自定义通告栏">
<Notice style="margin-bottom: 8px">

View File

@ -3,21 +3,27 @@ Page({
typeList: ['default', 'error', 'info', 'primary'],
},
handleTapAction() {
my.showToast({
content: `点击按钮`,
duration: 1000,
});
this.showToast('点击按钮');
},
handleTapLink() {
my.showToast({
content: 'link 类型被点击了',
duration: 1000,
});
this.showToast('link 类型被点击了');
},
handleClose() {
this.showToast('点击关闭');
},
showToast(content: string) {
/// #if ALIPAY
my.showToast({
content: `点击关闭`,
content: content,
duration: 1000,
});
/// #endif
/// #if WECHAT
//@ts-ignore
wx.showToast({
title: content,
});
/// #endif
},
});

View File

@ -35,7 +35,7 @@
"async-validator": "^4.0.7",
"dayjs": "^1.11.3",
"fast-deep-equal": "3.1.3",
"functional-mini": "^0.16.0",
"functional-mini": "^0.17.0",
"tslib": "2.5.0"
},
"overrides": {
@ -123,4 +123,4 @@
],
"license": "MIT",
"homepage": "https://github.com/ant-design/ant-design-mini"
}
}

View File

@ -1,19 +1,22 @@
import { Slot, TSXMLProps, View, Component } from 'tsxml';
import { Slot, TSXMLProps, View, Component, InternalData } from 'tsxml';
import Icon from '../Icon/index.axml';
import ImageIcon from '../ImageIcon/index.axml';
import { INoticeBarProps } from './props';
export default ({
className,
style,
type,
icon,
mode,
enableMarquee,
show,
marqueeStyle,
$id,
}: TSXMLProps<INoticeBarProps>) => (
export default (
{
className,
style,
type,
icon,
mode,
enableMarquee,
$id,
}: TSXMLProps<INoticeBarProps>,
{ marqueeStyle, show }: InternalData
) => (
<Component>
{show && (
<View
@ -22,7 +25,10 @@ export default ({
}`}
style={style}
>
{/* #if ALIPAY */}
<Slot name="icon">
{/* #endif */}
<View class="ant-notice-bar-icon">
{icon ? (
<ImageIcon image={icon} className="ant-notice-bar-icon-image" />
@ -32,10 +38,18 @@ export default ({
<Icon type="SoundOutline" />
)}
</View>
{/* #if ALIPAY */}
</Slot>
<View class={`ant-notice-bar-content ant-notice-bar-content-${$id}`}>
{/* #endif */}
<View
class={`ant-notice-bar-content ant-notice-bar-content${
$id ? '-' + $id : ''
}`}
>
<View
class={`ant-notice-bar-marquee ant-notice-bar-marquee-${$id}`}
class={`ant-notice-bar-marquee ant-notice-bar-marquee${
$id ? '-' + $id : ''
}`}
style={`${marqueeStyle} display: ${
enableMarquee ? 'inline-block' : 'block'
}`}

View File

@ -1,155 +1,128 @@
import { NoticeBarDefaultProps } from './props';
import { log } from '../_util/console';
import { IBoundingClientRect } from '../_util/base';
import {
useState,
useEffect,
useEvent,
usePageShow,
} from 'functional-mini/component';
import '../_util/assert-component2';
import { IBoundingClientRect } from '../_util/base';
import { mountComponent } from '../_util/component';
import { useComponentEvent } from '../_util/hooks/useComponentEvent';
import { useInstanceBoundingClientRect } from '../_util/hooks/useInstanceBoundingClientRect';
import { INoticeBarProps, NoticeBarFunctionalProps } from './props';
import { useEvent as useStableCallback } from '../_util/hooks/useEvent';
Component({
props: NoticeBarDefaultProps,
data: {
show: true,
marqueeStyle: '',
animatedWidth: 0,
overflowWidth: 0,
duration: 0,
viewWidth: 0,
},
didMount() {
const { enableMarquee } = this.props;
this.showError();
const NoticeBar = (props: INoticeBarProps) => {
const [marqueeStyle, setMarqueeStyle] = useState('');
const [show, setShow] = useState(true);
if (enableMarquee) {
this.measureText(this.startMarquee.bind(this));
const { triggerEventOnly } = useComponentEvent(props);
const startMarquee = useStableCallback((state) => {
const { loop } = props;
const leading = 500;
const { duration, overflowWidth, viewWidth } = state;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
},
const newMarqueeStyle = `transform: translate3d(${-marqueeScrollWidth}px, 0, 0); transition: ${duration}s all linear ${
typeof leading === 'number' ? `${leading / 1000}s` : '0s'
};`;
setMarqueeStyle(newMarqueeStyle);
});
didUpdate() {
const { enableMarquee } = this.props;
this.showError();
// 这里更新处理的原因是防止notice内容在动画过程中发生改变。
if (enableMarquee) {
this.measureText(this.startMarquee.bind(this));
}
},
const { getBoundingClientRectWithId } = useInstanceBoundingClientRect();
function measureText(callback) {
const fps = 40;
const { loop } = props;
// 计算文本所占据的宽度,计算需要滚动的宽度
setTimeout(async () => {
const marqueeSize: IBoundingClientRect | null =
await getBoundingClientRectWithId('.ant-notice-bar-marquee');
const contentSize: IBoundingClientRect | null =
await getBoundingClientRectWithId('.ant-notice-bar-content');
const overflowWidth =
(marqueeSize && contentSize && marqueeSize.width - contentSize.width) ||
0;
pageEvents: {
onShow() {
this.resetState();
},
},
methods: {
resetState() {
if (this.props.enableMarquee) {
this.setData(
{
marqueeStyle: '',
animatedWidth: 0,
overflowWidth: 0,
duration: 0,
viewWidth: 0,
},
() => {
this.resetMarquee();
this.measureText(this.startMarquee.bind(this));
}
);
}
},
showError() {
const { actions } = this.props;
if (!Array.isArray(actions) && typeof actions !== 'undefined') {
log.warn(
'NoticeBar',
`当前定义的 actions 的类型为 ${typeof actions},不符合属性定义,应该为数组,如:actions="{{['值', '值']}}`
);
}
},
onTap() {
const { mode, onTap } = this.props;
if (mode === 'link' && typeof onTap === 'function') {
return onTap();
}
if (mode === 'closeable' && typeof onTap === 'function') {
this.setData({
show: false,
});
return onTap();
}
},
// 文本滚动的计算
resetMarquee() {
const { loop } = this.props;
const { viewWidth } = this.data;
let showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = `${viewWidth}px`;
}
const marqueeStyle = `transform: translate3d(${showMarqueeWidth}, 0, 0); transition: 0s all linear;`;
this.setData({
marqueeStyle,
});
},
startMarquee() {
const { loop } = this.props;
const leading = 500;
const { duration, overflowWidth, viewWidth } = this.data;
const viewWidth = contentSize?.width || 0;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
const marqueeStyle = `transform: translate3d(${-marqueeScrollWidth}px, 0, 0); transition: ${duration}s all linear ${
typeof leading === 'number' ? `${leading / 1000}s` : '0s'
};`;
if (this.data.marqueeStyle !== marqueeStyle) {
this.setData({
marqueeStyle,
if (overflowWidth > 0) {
callback({
overflowWidth,
viewWidth,
duration: marqueeScrollWidth / fps,
});
}
},
onTransitionEnd() {
const { loop } = this.props;
const trailing = 200;
if (loop) {
setTimeout(() => {
this.resetMarquee();
this.measureText(this.startMarquee.bind(this));
}, trailing);
}
},
measureText(callback) {
const fps = 40;
const { loop } = this.props;
// 计算文本所占据的宽度,计算需要滚动的宽度
}, 0);
}
useEffect(() => {
const { enableMarquee } = props;
if (enableMarquee) {
measureText(startMarquee);
}
});
function resetMarquee(state) {
const { loop } = props;
const { viewWidth } = state;
let showMarqueeWidth = '0px';
if (loop) {
showMarqueeWidth = `${viewWidth}px`;
}
const marqueeStyle = `transform: translate3d(${showMarqueeWidth}, 0, 0); transition: 0s all linear;`;
setMarqueeStyle(marqueeStyle);
}
useEvent('onTransitionEnd', () => {
const { loop } = props;
const trailing = 200;
if (loop) {
setTimeout(() => {
my.createSelectorQuery()
.select(`.ant-notice-bar-marquee-${this.$id}`)
.boundingClientRect()
.select(`.ant-notice-bar-content-${this.$id}`)
.boundingClientRect()
.exec((ret) => {
// eslint-disable-next-line max-len
const overflowWidth =
(ret &&
ret[0] &&
ret[1] &&
(<IBoundingClientRect>ret[0]).width -
(<IBoundingClientRect>ret[1]).width) ||
0;
const viewWidth = (<IBoundingClientRect>ret[1])?.width || 0;
let marqueeScrollWidth = overflowWidth;
if (loop) {
marqueeScrollWidth = overflowWidth + viewWidth;
}
if (overflowWidth > 0) {
this.setData({
overflowWidth,
viewWidth,
duration: marqueeScrollWidth / fps,
});
callback();
}
});
}, 0);
},
},
});
measureText((state) => {
resetMarquee(state);
startMarquee(state);
});
}, trailing);
}
});
useEvent('onTap', () => {
const { mode } = props;
if (mode === 'link') {
triggerEventOnly('tap');
}
if (mode === 'closeable') {
/// #if ALIPAY
if (typeof props.onTap !== 'function') {
return;
}
/// #endif
setShow(false);
triggerEventOnly('tap');
}
});
usePageShow(() => {
if (props.enableMarquee) {
setMarqueeStyle('');
resetMarquee({
overflowWidth: 0,
duration: 0,
viewWidth: 0,
});
measureText(startMarquee);
}
});
return {
marqueeStyle,
show,
};
};
mountComponent(NoticeBar, NoticeBarFunctionalProps);

View File

@ -7,10 +7,6 @@ import { IBaseProps } from '../_util/base';
*/
export interface INoticeBarProps extends IBaseProps {
/**
*
*/
show?: boolean;
/**
* @description
*/
@ -24,16 +20,7 @@ export interface INoticeBarProps extends IBaseProps {
/**
* @description link closeable x
*/
mode: 'link' | 'closeable';
/**
* @description action和mode可以同时搭配使用
*/
actions: string[];
/**
* @description
*/
marqueeStyle?: boolean;
mode?: 'link' | 'closeable';
/**
* @description
* @default false
@ -64,3 +51,11 @@ export const NoticeBarDefaultProps: Partial<INoticeBarProps> = {
loop: false,
type: 'default',
};
export const NoticeBarFunctionalProps: Partial<INoticeBarProps> = {
icon: '',
type: 'default',
mode: null,
enableMarquee: false,
loop: false,
};

View File

@ -0,0 +1,22 @@
import { useComponent } from 'functional-mini/component';
import { getInstanceBoundingClientRect } from '../jsapi/get-instance-bounding-client-rect';
export const useInstanceBoundingClientRect = () => {
const instance = useComponent();
function getInstance() {
if (instance.$id) {
return my;
}
return instance;
}
async function getBoundingClientRectWithId(prefix: string) {
return await getInstanceBoundingClientRect(
getInstance(),
`${prefix}${instance.$id ? `-${instance.$id}` : ''}`
);
}
return {
getBoundingClientRectWithId,
};
};

View File

@ -13,4 +13,5 @@
"header": "",
"radius": false,
},
"rootEvents": {},
}

View File

@ -20,4 +20,5 @@
"showDivider": true,
"title": "",
},
"rootEvents": {},
}

View File

@ -50,13 +50,9 @@ describe('modal onClose', () => {
});
await sleep(30);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 2.5,
'marqueeStyle':
'transform: translate3d(-100px, 0, 0); transition: 2.5s all linear 0.5s;',
'overflowWidth': 100,
'show': true,
'viewWidth': 100,
});
handleQuery.mockImplementation((id: string, index: number) => {
return {
@ -71,13 +67,9 @@ describe('modal onClose', () => {
});
await sleep(30);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 5,
'marqueeStyle':
'transform: translate3d(-200px, 0, 0); transition: 5s all linear 0.5s;',
'overflowWidth': 200,
'show': true,
'viewWidth': 100,
});
});
@ -96,13 +88,9 @@ describe('modal onClose', () => {
});
await sleep(30);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 5,
'marqueeStyle':
'transform: translate3d(-200px, 0, 0); transition: 5s all linear 0.5s;',
'overflowWidth': 100,
'show': true,
'viewWidth': 100,
});
instance.callMethod('onTransitionEnd');
handleQuery.mockImplementation(async (id: string, index: number) => {
@ -116,23 +104,15 @@ describe('modal onClose', () => {
});
await sleep(250);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 5,
'marqueeStyle':
'transform: translate3d(100px, 0, 0); transition: 0s all linear;',
'overflowWidth': 100,
'show': true,
'viewWidth': 100,
});
await sleep(300);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 5,
'marqueeStyle':
'transform: translate3d(-200px, 0, 0); transition: 5s all linear 0.5s;',
'overflowWidth': 100,
'show': true,
'viewWidth': 100,
});
await sleep(500);
expect(instance.getData()).toEqual({
'marqueeStyle':
'transform: translate3d(-200px, 0, 0); transition: 5s all linear 0.5s;',
'show': true,
});
});
@ -151,12 +131,8 @@ describe('modal onClose', () => {
});
await sleep(30);
expect(instance.getData()).toEqual({
'animatedWidth': 0,
'duration': 0,
'marqueeStyle': '',
'overflowWidth': 0,
'show': true,
'viewWidth': 0,
});
});
});

View File

@ -13,4 +13,5 @@
"message": "",
"title": "",
},
"rootEvents": {},
}

View File

@ -23,4 +23,5 @@
"size": "medium",
"uncheckedText": "",
},
"rootEvents": {},
}