feat:dropDownButton新增隐藏下拉图标属性hideCaret;badge组件支持横幅类型;nav组件新增角标配置、更多操作配置、拖拽排序 (#2800)

* feat:dropDownButton新增隐藏下拉图标属性hideCaret;nav组件新增角标、更多操作配置、支持图片拽排序

* feat:dropDownButton新增隐藏下拉图标属性hideCaret;badge组件支持横幅类型;nav组件新增角标配置、更多操作配置、拖拽排序

* chore:优化nav角标相关配置

* fix:修复badge组件ribbon高度遮挡问题

Co-authored-by: Qin,Haoyan <qinhaoyan@baidu.com>
This commit is contained in:
qinhaoyan 2021-11-01 10:14:25 +08:00 committed by GitHub
parent b202b4e636
commit 168a3635ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 482 additions and 104 deletions

View File

@ -68,7 +68,18 @@ order: 30
"badge": {
"position": "top-left"
}
},
{
"type": "divider"
},
{
"type": "action",
"label": "按钮",
"badge": {
"mode": "ribbon",
"text": "HOT"
}
},
]
```
@ -345,7 +356,7 @@ order: 30
| 属性名 | 类型 | 默认值 | 说明 |
| ------------- | ------------------- | ---------- | ----------------------------------------------------------------|
| mode | `string` | dot | 角标类型,可以是 dot/text |
| mode | `string` | dot | 角标类型,可以是 dot/text/ribbon |
| text | `text`、`number` | | 角标文案支持字符串和数字在mode='dot'下设置无效 |
| size | `number` | | 角标大小 |
| level | `string` | | 角标级别, 可以是info/success/warning/danger, 设置之后角标背景颜色不同 |

View File

@ -83,3 +83,4 @@ order: 44
| defaultIsOpened | `boolean` | | 默认是否打开 |
| closeOnOutside | `boolean` | | 点击外侧区域是否收起 |
| trigger | `click``hover` | `click` | 触发方式 |
| hideCaret | `boolean` | false | 隐藏下拉图标 |

View File

@ -26,7 +26,12 @@ order: 58
},
{
"label": "Nav 2",
"to": "/docs/api"
"to": "/docs/api",
"badge": {
"mode": "ribbon",
"text": "HOT",
"position": "top-left"
}
},
{
"label": "Nav 3",
@ -150,6 +155,43 @@ order: 58
}
```
## 更多操作
```schema: scope="body"
{
"type": "nav",
"stacked": true,
"className": "w-md",
"draggable": true,
"saveOrderApi": "/api/options/nav",
"source": "/api/options/nav?parentId=${value}",
"itemActions": [
{
"type": "icon",
"icon": "cloud",
"visibleOn": "this.to === '?cat=1'"
},
{
"type": "dropdown-button",
"level": "link",
"icon": "fa fa-ellipsis-h",
"hideCaret": true,
"buttons": [
{
"type": "button",
"label": "编辑",
},
{
"type": "button",
"label": "删除"
}
]
}
]
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
@ -159,8 +201,13 @@ order: 58
| stacked | `boolean` | `true` | 设置成 false 可以以 tabs 的形式展示 |
| source | `string` 或 [API](../../docs/types/api) | | 可以通过变量或 API 接口动态创建导航 |
| deferApi | [API](../../docs/types/api) | | 用来延时加载选项详情的接口,可以不配置,不配置公用 source 接口。 |
| itemActions | [SchemaNode](../../docs/types/schemanode) | | 更多操作相关配置 |
| draggable | `boolean` | | 是否支持拖拽排序 |
| saveOrderApi | `string` 或 [API](../../docs/types/api) | |保存排序的 api |
| badge | [BadgeSchema](../../components/badge)| | 角标 |
| links | `Array` | | 链接集合 |
| links[x].label | `string` | | 名称 |
| links[x].badge | [BadgeSchema](../../components/badge)| | 角标,会覆盖全局角标配置 |
| links[x].to | [模板](../../docs/concepts/template) | | 链接地址 |
| links[x].target | `string` | 链接关系 | |
| links[x].icon | `string` | | 图标 |

View File

@ -3,7 +3,8 @@
position: relative;
&-text,
&-dot {
&-dot,
&-ribbon {
background: var(--danger);
position: absolute;
top: 0;
@ -70,6 +71,56 @@
border-radius: 50%;
}
// 横幅
&-ribbon-out {
overflow: hidden;
position: absolute;
top: 0;
bottom: 0;
right: 0
}
&-ribbon {
color: var(--Badge-color);
height: var(--Badge-size);
line-height: var(--Badge-size);
transform: translateX(calc(50% - 5px)) rotate(45deg) scale(.7);
transform-origin: 50% 0;
border-radius: 0;
text-align: center;
width: px2rem(1000px);
top: 5px;
}
&-ribbon-out--top-left, &-ribbon-out--bottom-left {
left: 0;
right: auto
}
&-ribbon--top-left {
transform: translateX(calc(-50% + 5px)) rotate(-45deg) scale(.7);
left: 0;
right: auto;
}
&-ribbon--bottom-left {
transform: translateX(calc(-50% + 5px)) rotate(45deg) scale(.7);
transform-origin: 50% 100%;
left: 0;
right: auto;
bottom: 5px;
top: auto;
}
&-ribbon--bottom-right {
transform: translateX(calc(50% - 5px)) rotate(-45deg) scale(.7);
transform-origin: 50% 100%;
left: auto;
right: 0;
bottom: 5px;
top: auto;
}
// 小红点的动画
@keyframes badgeDotAnimation {
0% {

View File

@ -20,7 +20,7 @@
.#{$ns}Nav-item {
margin-bottom: calc(var(--Tabs-borderWidth) * -1);
display: inline-block;
position: relative;
> a {
font-size: var(--Tabs-linkFontSize);
display: block;
@ -31,7 +31,7 @@
color: var(--Tabs-color);
text-decoration: none;
margin-right: px2rem(2px);
padding: var(--gap-sm) var(--gap-base);
padding: var(--gap-sm) var(--gap-xl);
cursor: pointer;
}
@ -62,57 +62,129 @@
&--stacked {
min-height: px2rem(50px);
.#{$ns}Nav-item {
.#{$ns}Nav-item, .#{$ns}Badge {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
> a {
display: block;
.#{$ns}Nav-itemDrager {
cursor: move;
position: absolute;
left: 0;
top: px2rem(11px);
display: none;
> a, > .#{$ns}Badge > a {
color: var(--icon-color);
&:hover {
color: var(--icon-onHover-color);
}
}
svg {
width: px2rem(16px);
height: px2rem(16px);
}
}
> .#{$ns}Nav-item-badgeText {
position: absolute;
top: 0;
bottom: 0;
width: px2rem(35px);
overflow: hidden;
> span {
position: absolute;
top: px2rem(2px);
left: px2rem(-13px);
transform: rotate(-45deg);
width: px2rem(50px);
font-size: 12px;
text-align: center;
color: var(--white);
background: var(--success)
}
}
> .#{$ns}Nav-item-atcions {
display: flex;
align-items: center;
}
> a,
> .#{$ns}Badge > a {
display: flex;
align-items: center;
outline: none;
color: var(--Nav-item-color);
text-decoration: none;
padding: var(--gap-sm) var(--gap-base);
padding: var(--gap-sm) var(--gap-sm);
cursor: pointer;
background: var(--Nav-item-bg);
border-radius: var(--Nav-item-borderRadius);
text-overflow: ellipsis;
flex: 1;
&::after {
border-left: var(--Nav-item-onActive-borderLeft);
position: absolute;
left: 0;
top: 0;
content: '';
width: 1px;
height: 100%;
transform: scaleY(0.0001);
transition: transform 0.15s cubic-bezier(0.215, 0.61, 0.355, 1),
opacity 0.15s cubic-bezier(0.215, 0.61, 0.355, 1);
}
}
&.has-sub > a {
padding-right: calc(var(--gap-base) + var(--gap-sm));
// &::after {
// border-left: var(--Nav-item-onActive-borderLeft);
// position: absolute;
// left: 0;
// top: 0;
// content: '';
// width: 1px;
// height: 100%;
// transform: scaleY(0.0001);
// transition: transform 0.15s cubic-bezier(0.215, 0.61, 0.355, 1),
// opacity 0.15s cubic-bezier(0.215, 0.61, 0.355, 1);
// }
}
> a:hover,
> a:focus {
> a:focus,
> a:hover + .#{$ns}Nav-item-atcions,
> a:focus + .#{$ns}Nav-item-atcions,
> .#{$ns}Badge > a:hover,
> .#{$ns}Badge > a:focus,
> .#{$ns}Badge > a:hover + .#{$ns}Nav-item-atcions,
> .#{$ns}Badge > a:focus + .#{$ns}Nav-item-atcions
{
border-color: var(--Nav-item-onHover-color);
text-decoration: none;
background: var(--Nav-item-onHover-bg);
z-index: 9999;
}
> a:hover > .#{$ns}Nav-itemDrager,
> a:focus > .#{$ns}Nav-itemDrager,
> .#{$ns}Badge > a:hover > .#{$ns}Nav-itemDrager,
> .#{$ns}Badge > a:focus > .#{$ns}Nav-itemDrager {
display: block;
}
&.disabled > a,
&.is-disabled > a {
&.is-disabled > a,
&.disabled > .#{$ns}Badge > a,
&.is-disabled > .#{$ns}Badge > a {
color: var(--Nav-item-onDisabled-color);
background: transparent;
pointer-events: none;
}
&.active,
&.is-active {
background: var(--Nav-item-onActive-bg) !important;
}
&.active > a,
&.is-active > a {
&.is-active > .#{$ns}Nav-item-atcions,
&.is-active > a,
&.active > .#{$ns}Badge > a,
&.is-active > .#{$ns}Badge > .#{$ns}Nav-item-atcions,
&.is-active > .#{$ns}Badge > a {
color: var(--Nav-item-onActive-color);
background: var(--Nav-item-onActive-bg);
padding-left: px2rem(12px);
position: relative;
&::after {
@ -120,8 +192,8 @@
}
}
&.is-unfolded > {
.#{$ns}Nav-itemToggler {
&.is-unfolded > , &.is-unfolded > .#{$ns}Badge > {
a .#{$ns}Nav-itemToggler {
transform: rotate(180deg) scale(0.8);
}
@ -138,13 +210,14 @@
}
.#{$ns}Nav-itemToggler {
position: absolute;
right: 0;
top: px2rem(3px);
width: px2rem(30px);
height: px2rem(30px);
// position: absolute;
// left: 0;
// top: px2rem(3px);
float: left;
width: px2rem(24px);
height: px2rem(24px);
text-align: center;
line-height: px2rem(30px);
line-height: px2rem(24px);
vertical-align: middle;
cursor: pointer;
transform: scale(0.8);
@ -162,6 +235,7 @@
display: none;
padding-left: 0;
list-style: none;
width: 100%;
}
.#{$ns}Nav-item {

View File

@ -27,7 +27,7 @@ export interface BadgeSchema extends BaseSchema {
/**
*
*/
mode?: 'text' | 'dot';
mode?: 'text' | 'dot' | 'ribbon';
/**
* position
@ -80,6 +80,62 @@ export class Badge extends React.Component<BadgeProps, object> {
super(props);
}
renderBadge(
text: any,
size: number,
position: any,
offsetStyle: any,
sizeStyle: any,
animationElement: any
) {
const {classnames: cx, badge} = this.props;
const {
mode = 'dot',
level = 'danger',
style
} = badge as BadgeSchema;
switch(mode) {
case 'dot':
return (
<span
className={cx('Badge-dot', `Badge--${position}`, `Badge--${level}`)}
style={{...offsetStyle, ...sizeStyle, ...style}}
>
{animationElement}
</span>
);
case 'text':
return (
<span
className={cx('Badge-text', `Badge--${position}`, `Badge--${level}`)}
style={{...offsetStyle, ...sizeStyle, ...style}}
>
{text}
{animationElement}
</span>
);
case 'ribbon':
const outSize = size * Math.sqrt(2) + 5;
return (
<div
className={cx('Badge-ribbon-out', `Badge-ribbon-out--${position}`)}
style={{width: outSize, height: outSize}}
>
<span
className={cx('Badge-ribbon', `Badge-ribbon--${position}`, `Badge--${level}`)}
style={{...sizeStyle, ...style}}
>
{text}
{animationElement}
</span>
</div>
);
default:
return null;
}
}
render() {
const badge = this.props.badge;
if (!badge) {
@ -101,8 +157,7 @@ export class Badge extends React.Component<BadgeProps, object> {
overflowCount = 99,
visibleOn,
className,
animation,
level = 'danger'
animation
} = badge;
if (visibleOn) {
@ -117,6 +172,8 @@ export class Badge extends React.Component<BadgeProps, object> {
if (typeof size === 'undefined') {
if (mode === 'dot') {
size = 6;
} else if (mode === 'ribbon'){
size = 12;
} else {
size = 16;
}
@ -145,6 +202,14 @@ export class Badge extends React.Component<BadgeProps, object> {
sizeStyle = {width: size, height: size};
}
if (mode === 'ribbon') {
sizeStyle = {
height: size,
lineHeight: size + 'px',
fontSize: size
};
}
let offsetStyle = {};
// 如果设置了offset属性offset在position为'top-right'的基础上进行translate定位
if (offset && offset.length) {
@ -180,23 +245,14 @@ export class Badge extends React.Component<BadgeProps, object> {
return (
<div className={cx('Badge', className)}>
{children}
{isDisplay ? (
mode === 'dot' ? (
<span
className={cx('Badge-dot', `Badge--${position}`, `Badge--${level}`)}
style={{...offsetStyle, ...sizeStyle, ...style}}
>
{animationElement}
</span>
) : (
<span
className={cx('Badge-text', `Badge--${position}`, `Badge--${level}`)}
style={{...offsetStyle, ...sizeStyle, ...style}}
>
{text}
{animationElement}
</span>
)
{isDisplay ?
this.renderBadge(
text,
size,
position,
offsetStyle,
sizeStyle,
animationElement
) : null}
</div>
);

View File

@ -81,6 +81,10 @@ export interface DropdownButtonSchema extends BaseSchema {
* click
*/
trigger?: 'click' | 'hover';
/**
*
*/
hideCaret?: boolean;
}
export interface DropDownButtonProps
@ -272,7 +276,8 @@ export default class DropDownButton extends React.Component<
icon,
isActived,
trigger,
data
data,
hideCaret
} = this.props;
return (
@ -325,9 +330,13 @@ export default class DropDownButton extends React.Component<
)
) : null}
{typeof label === 'string' ? filter(label, data) : label}
<span className={cx('DropDown-caret')}>
{
!hideCaret
? <span className={cx('DropDown-caret')} >
<Icon icon="caret" className="icon" />
</span>
: null
}
</button>
</TooltipWrapper>
{this.state.isOpened ? this.renderOuter() : null}

View File

@ -1,8 +1,10 @@
import React from 'react';
import Sortable from 'sortablejs';
import {Renderer, RendererEnv, RendererProps} from '../factory';
import getExprProperties from '../utils/filter-schema';
import {filter, evalExpression} from '../utils/tpl';
import {
guid,
autobind,
createObject,
findTree,
@ -22,6 +24,9 @@ import {
} from '../components/WithRemoteConfig';
import {Payload} from '../types';
import Spinner from '../components/Spinner';
import cloneDeep from 'lodash/cloneDeep';
import {isEffectiveApi} from '../utils/api';
import {Badge, BadgeSchema} from '../components/Badge';
export type NavItemSchema = {
/**
@ -45,6 +50,11 @@ export type NavItemSchema = {
deferApi?: SchemaApi;
children?: Array<NavItemSchema>;
/**
*
*/
badge?: BadgeSchema
} & Omit<BaseSchema, 'type'>;
/**
@ -81,6 +91,26 @@ export interface NavSchema extends BaseSchema {
* true false tabs
*/
stacked?: boolean;
/**
*
*/
itemActions?: SchemaCollection;
/**
*
*/
draggable?: boolean;
/**
* api
*/
saveOrderApi?: SchemaApi;
/**
*
*/
badge?: BadgeSchema;
}
export interface Link {
@ -97,6 +127,7 @@ export interface Link {
loading?: boolean;
loaded?: boolean;
[propName: string]: any;
badge?: BadgeSchema
}
export interface Links extends Array<Link> {}
@ -113,7 +144,9 @@ export interface NavigationProps
togglerClassName?: string;
links?: Array<Link>;
loading?: boolean;
render: RendererProps['render']
render: RendererProps['render'];
env: RendererEnv;
reload?: any;
}
export class Navigation extends React.Component<
@ -123,6 +156,9 @@ export class Navigation extends React.Component<
static defaultProps = {
indentSize: 24
};
sortable: Sortable[] = [];
id: string;
dragRef?: HTMLElement;
@autobind
handleClick(link: Link) {
@ -134,15 +170,85 @@ export class Navigation extends React.Component<
this.props.onToggle?.(target);
}
@autobind
dragRefFn(ref: any) {
const {draggable} = this.props;
if (ref && draggable) {
this.id = guid();
this.initDragging(ref);
}
}
initDragging(ref: HTMLElement) {
const ns = this.props.classPrefix;
this.sortable.push(new Sortable(
ref,
{
group: `nav-${this.id}`,
animation: 150,
handle: `.${ns}Nav-itemDrager`,
ghostClass: `${ns}Nav-item--dragging`,
onEnd: async (e: any) => {
// 没有移动
if (e.newIndex === e.oldIndex) {
return;
}
const id = e.item.getAttribute('data-id');
const parentNode = e.to
if (
e.newIndex < e.oldIndex &&
e.oldIndex < parentNode.childNodes.length - 1
) {
parentNode.insertBefore(e.item, parentNode.childNodes[e.oldIndex + 1]);
} else if (e.oldIndex < parentNode.childNodes.length - 1) {
parentNode.insertBefore(e.item, parentNode.childNodes[e.oldIndex]);
} else {
parentNode.appendChild(e.item);
}
const links = cloneDeep(this.props.links) as Link[];
let parent = links;
someTree(links, (item: Link, key, level, paths: Link[]) => {
if (item.id === id) {
const len = paths.length - 1;
parent = (~len ? paths[len].children : links) as Link[];
return true;
}
return false;
});
parent.splice(e.newIndex, 0, parent.splice(e.oldIndex, 1)[0]);
const {saveOrderApi, env} = this.props;
if (saveOrderApi && isEffectiveApi(saveOrderApi)) {
await env.fetcher(saveOrderApi as SchemaApi, {data: links}, {method: 'post'});
this.props.reload();
} else {
console.warn('请配置saveOrderApi');
}
}
}
));
}
renderItem(link: Link, index: number, depth = 1) {
if (link.hidden === true || link.visible === false) {
return null;
}
const isActive: boolean = !!link.active;
const {disabled, togglerClassName, classnames: cx, indentSize} = this.props;
const {
disabled,
togglerClassName,
classnames: cx,
indentSize,
render,
itemActions,
draggable,
links,
badge: defaultBadge
} = this.props;
const hasSub =
(link.defer && !link.loaded) || (link.children && link.children.length);
const id = guid();
link.id = id;
const badge = defaultBadge ? Object.assign(defaultBadge, link.badge) : link.badge;
return (
<li
key={index}
@ -152,19 +258,23 @@ export class Navigation extends React.Component<
'is-unfolded': link.unfolded,
'has-sub': hasSub
})}
data-id={id}
>
<Badge classnames={cx} badge={badge} data={link}>
<a
onClick={this.handleClick.bind(this, link)}
style={{paddingLeft: depth * (parseInt(indentSize as any, 10) ?? 24)}}
>
{generateIcon(cx, link.icon, 'Nav-itemIcon')}
{
link.label && (typeof link.label === 'string'
? link.label
: this.props.render('inline', link.label as SchemaCollection))
}
{!disabled && draggable && links && links.length > 1 ? (
<div className={cx('Nav-itemDrager')} >
<a
key="drag"
data-position="bottom"
>
<Icon icon="drag-bar" className="icon" />
</a>
</div>
) : null}
{link.loading ? (
<Spinner
size="sm"
@ -180,14 +290,30 @@ export class Navigation extends React.Component<
<Icon icon="caret" className="icon" />
</span>
) : null}
{generateIcon(cx, link.icon, 'Nav-itemIcon')}
{
link.label && (typeof link.label === 'string'
? link.label
: render('inline', link.label as SchemaCollection))
}
</a>
{
// 更多操作
itemActions
? <div className={cx('Nav-item-atcions')}>
{
render('inline', itemActions, {data: link})
}
</div> : null
}
{Array.isArray(link.children) && link.children.length ? (
<ul className={cx('Nav-subItems')}>
<ul className={cx('Nav-subItems')} ref={this.dragRefFn}>
{link.children.map((link, index) =>
this.renderItem(link, index, depth + 1)
)}
</ul>
) : null}
</Badge>
</li>
);
}
@ -198,6 +324,7 @@ export class Navigation extends React.Component<
return (
<ul
className={cx('Nav', className, stacked ? 'Nav--stacked' : 'Nav--tabs')}
ref={this.dragRefFn}
>
{Array.isArray(links)
? links.map((item, index) => this.renderItem(item, index))
@ -310,6 +437,7 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
data?: any;
unfoldedField?: string;
foldedField?: string;
reload?: any;
}
> {
constructor(props: any) {
@ -442,6 +570,7 @@ export class NavigationRenderer extends React.Component<RendererProps> {
return (
<ConditionBuilderWithRemoteOptions
{...rest}
reload={this.reload}
remoteConfigRef={this.remoteConfigRef}
/>
);