feat:Steps组件 (#1784)

* fix: Form horizontal水平模式下 label 显示问题

* feat:Steps组件

* feat:Steps组件

* feat:Steps组件

* feat:Steps组件

* feat:Steps组件 文档

* feat:Steps组件 文档

Co-authored-by: yupeng12 <yupeng12@baidu.com>
This commit is contained in:
Allen 2021-04-13 13:21:10 +08:00 committed by GitHub
parent 5c9a7dac27
commit c6bebeb50d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 551 additions and 2 deletions

View File

@ -0,0 +1,249 @@
---
title: Steps 步骤条
description:
type: 0
group: ⚙ 组件
menuName: Steps
icon:
order: 68
---
步骤条组件
## 基本用法
```schema
{
"type": "page",
"body": [
{
"type": "steps",
"value": 1,
"steps": [
{
"title": "First",
"subTitle": "this is subTitle",
"description": "this is description"
},
{
"title": "Second"
},
{
"title": "Last"
}
]
}
]
}
```
## 设置状态
```schema
{
"type": "page",
"body": {
"type": "steps",
"value": 1,
"status": "error",
"steps": [
{
"title": "First"
},
{
"title": "Second",
"subTitle": "this is subTitle",
"description": "this is description"
},
{
"title": "Last"
}
]
}
}
```
## 数据映射
```schema
{
"type": "page",
"data": {
"step": 1,
"status": "error"
},
"body": [
{
"type": "steps",
"status": "${status}",
"value": "${step}",
"steps": [
{
"title": "First",
"subTitle": "this is subTitle",
"description": "this is description"
},
{
"title": "Second"
},
{
"title": "Last"
}
]
}
]
}
```
## 接口映射
```schema
{
"type": "page",
"initApi": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/steps/get",
"body": [
{
"type": "steps",
"value": "${step}",
"status": "${status}",
"steps": [
{
"title": "First",
"subTitle": "this is subTitle",
"description": "this is description"
},
{
"title": "Secord"
},
{
"title": "Last"
}
]
}
]
}
```
## Form 中静态展示
```schema
{
"type": "page",
"body": {
"type": "form",
"initApi": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/steps/steps",
"controls": [
{
"type": "steps",
"source": "${steps}",
"value": "${current}"
}
]
}
}
```
## 远程拉取
除了可以通过数据映射获取当前数据域中的变量以外source 还支持配置接口,格式为 API用于动态返回选项组。
```schema
{
"type": "page",
"body": {
"type": "form",
"controls": [
{
"type": "steps",
"name": "steps",
"source": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/steps/steps"
}
]
}
}
```
远程拉取接口时,返回的数据结构除了需要满足 amis 接口要求的基本数据结构 以外,必须用"steps"作为选项组的 key 值,如下:
```json
{
"status": 0,
"msg": "",
"data": {
"steps": [
{
"title": "First",
"subTitle": "this is sub title",
"value": "first"
},
{
"title": "Secord",
"description": "this is description",
"value": "secord"
},
{
"title": "Last",
"value": "last"
}
]
}
}
```
## 自定义不同步骤以及状态
```schema
{
"type": "page",
"body": {
"type": "steps",
"value": "b",
"status": {
"a": "finish",
"b": "error",
"c": "wait"
},
"steps": [
{
"title": "First",
"value": "a"
},
{
"title": "Second",
"subTitle": "this is subTitle",
"description": "this is description",
"value": "b"
},
{
"title": "Third",
"value": "c"
}
]
}
}
```
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | --------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------- |
| type | `string` | | `"steps"` 指定为 步骤条 渲染器 |
| steps | Array<[step](#step)> | [] | 数组,配置步骤信息 |
| source | [API](../../../docs/types/api) 或 [数据映射](../../../docs/concepts/data-mapping) | | 选项组源,可通过数据映射获取当前数据域变量、或者配置 API 对象 |
| value | `string` \| `number` | `-` | 指定当前步骤,如果是`string`需要在 step 中配置 value |
| status | [StepStatus](#StepStatus) \| {[propName: string]: stepStatus;} | `-` | 状态 |
| className | `string` | `-` | 自定义类名 |
### step
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | -------- | ------ | --------------------------------------- |
| title | `string` | `-` | 标题 |
| subTitle | `string` | `-` | 子标题 |
| icon | `string` | | icon 名,支持 fontawesome v4 或使用 url |
| value | `string` | | value |
| className | `string` | `-` | 自定义类名 |
### StepStatus
`wait` | `process` | `finish` | `error`

View File

@ -984,6 +984,15 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'Steps 步骤条',
path: '/zh-CN/components/steps',
getComponent: () =>
// @ts-ignore
import('../../docs/zh-CN/components/steps.md').then(
makeMarkdownRenderer
)
},
{
label: 'Property 属性表',
path: '/zh-CN/components/property',

View File

@ -52,6 +52,7 @@
--borderColor: #{$borderColor};
--borderColorLight: #{lighten($borderColor, 5%)};
--borderColorDarken: #{darken($borderColor, 10%)};
--borderRadius: 0.142rem;
--borderRadiusMd: 0.285rem;
--borderRadiusLg: 0.428rem;
@ -1334,4 +1335,15 @@
--AnchorNav-links-container-borderRight: #{px2rem(1px)} solid #d3dae0;
--AnchorNav-onActive-borderWidth: 0 #{px2rem(2px)} 0 0;
--Steps-bg: var(--borderColorDarken);
--Steps-status-success: var(--info);
--Steps-status-error: var(--danger);
--Steps-icon-fontsize: var(--fontSizeLg);
--Steps-title-fontsize: var(--fontSizeLg);
--Steps-title-color: var(--text--loud-color);
--Steps-sub-title-fontsize: var(--fontSizeBase);
--Steps-sub-title-color: var(--text-color);
--Steps-line-bg: var(--Steps-bg);
--Steps-line-success-bg: var(--Steps-status-success);
}

101
scss/components/_steps.scss Normal file
View File

@ -0,0 +1,101 @@
.#{$ns}Steps {
display: flex;
list-style: none;
padding: 0;
.#{$ns}StepsItem {
flex: 1;
overflow: hidden;
padding: 0 px2rem(20px);
height: auto;
line-height: px2rem(32px);
&-container {
&Icon {
vertical-align: top;
display: inline-block;
padding-right: px2rem(10px);
.#{$ns}StepsItem-icon {
display: inline-block;
width: px2rem(30px);
height: px2rem(30px);
line-height: px2rem(28px);
border-radius: 50%;
font-size: var(--Steps-icon-fontsize);
text-align: center;
}
}
&Wrapper {
display: inline-block;
position: relative;
.#{$ns}StepsItem-body {
.#{$ns}StepsItem-title {
font-size: var(--Steps-title-fontsize);
color: var(--Steps-title-color);
padding-right: px2rem(20px);
position: relative;
display: inline-block;
> span {
display: inline-block;
}
&:after {
content: '';
position: absolute;
right: 0;
top: px2rem(16px);
height: 1px;
left: 100%;
width: 9999px;
padding-right: 10px;
background-color: var(--Steps-line-bg);
}
}
.#{$ns}StepsItem-title.is-success {
&:after {
background-color: var(--Steps-line-success-bg);
}
}
.#{$ns}StepsItem-subTitle {
padding-left: px2rem(10px);
font-size: var(--Steps-sub-title-fontsize);
color: var(--Steps-sub-title-color);
}
.#{$ns}StepsItem-description {
max-width: px2rem(140px);
white-space: nowrap;
}
}
}
}
&:last-child {
flex: none;
.#{$ns}StepsItem-title {
&:after {
display: none;
}
}
}
}
.#{$ns}StepsItem.is-wait {
.#{$ns}StepsItem-icon {
background-color: var(--white);
color: var(--Steps-bg);
border: 1px solid var(--Steps-bg);
}
}
.#{$ns}StepsItem.is-error {
.#{$ns}StepsItem-icon {
background-color: var(--Steps-status-error);
color: var(--white);
border: 1px solid var(--Steps-status-error);
}
.#{$ns}StepsItem-title, .#{$ns}StepsItem-subTitle, .#{$ns}StepsItem-description {
color: var(--Steps-status-error);
}
}
.#{$ns}StepsItem.is-finish, .#{$ns}StepsItem.is-process {
.#{$ns}StepsItem-icon {
background-color: var(--Steps-status-success);
color: var(--white);
border: 1px solid var(--Steps-status-success);
}
}
}

View File

@ -64,6 +64,7 @@
@import '../components/log';
@import '../components/json';
@import '../components/icon';
@import '../components/steps';
@import '../components/form/fieldset';
@import '../components/form/group';

View File

@ -54,6 +54,7 @@ import {PaginationWrapperSchema} from './renderers/PaginationWrapper';
import {PaginationSchema} from './renderers/Pagination';
import {AnchorNavSchema} from './renderers/AnchorNav';
import {AvatarSchema} from './renderers/Avatar';
import {StepsSchema} from './renderers/Steps';
// 每加个类型,这补充一下。
export type SchemaType =
@ -137,7 +138,8 @@ export type SchemaType =
| 'video'
| 'wizard'
| 'wrapper'
| 'anchor-nav';
| 'anchor-nav'
| 'steps';
export type SchemaObject =
| PageSchema
@ -195,7 +197,8 @@ export type SchemaObject =
| WizardSchema
| WrapperSchema
| FormSchema
| AnchorNavSchema;
| AnchorNavSchema
| StepsSchema;
export type SchemaCollection =
| SchemaObject

View File

@ -165,6 +165,7 @@ import './renderers/Icon';
import './renderers/Carousel';
import './renderers/AnchorNav';
import './renderers/Form/AnchorNav';
import './renderers/Steps';
import Scoped, {ScopedContext} from './Scoped';
import {FormItem, registerFormItem} from './renderers/Form/Item';

173
src/renderers/Steps.tsx Normal file
View File

@ -0,0 +1,173 @@
import React from 'react';
import {Renderer, RendererProps} from '../factory';
import {BaseSchema} from '../Schema';
import {Icon} from '../components/icons';
import {RemoteOptionsProps, withRemoteConfig} from '../components/WithRemoteConfig';
import {resolveVariable} from '../utils/tpl-builtin';
enum StepStatus {
wait = 'wait',
process = 'process',
finish = 'finish',
error = 'error'
};
export type StepSchema = {
/**
*
*/
title: string;
/**
*
*/
subTitle?: string;
/**
*
*/
icon?: string;
value?: string | number;
/**
*
*/
description?: string;
} & Omit<BaseSchema, 'type'>;
export interface StepsSchema extends BaseSchema {
/**
* Steps
*/
type: 'steps';
/**
*
*/
steps?: Array<StepSchema>;
/**
* API
*/
source?: string;
/**
*
*/
value?: number | string;
/**
*
*/
name?: string;
status?: StepStatus | {
[propName: string]: StepStatus;
}
/**
*
*/
mode?: 'horizontal' | 'vertical';
}
export interface StepsProps extends RendererProps, Omit<StepsSchema, 'className'> {}
export function Steps(props: StepsProps) {
const {className, classnames: cx, steps, value = 0, status, data, source, config} = props;
const stepsRow = resolveVariable(source, data) as Array<StepSchema> || config || steps || [];
const resolveValue = typeof value === 'string' && isNaN(+value)
? resolveVariable(value, data) as string || +value : +value;
const valueIndex = stepsRow.findIndex(item => item.value && item.value === resolveValue);
const currentValue = valueIndex !== -1 ? valueIndex : resolveValue;
const FINISH_ICON = 'check';
const ERROR_ICON = 'close';
function getStepStatus(step: StepSchema, i: number): {stepStatus: StepStatus, icon?: string} {
let stepStatus = StepStatus.wait;
let icon = step.icon;
if (i < currentValue) {
stepStatus = StepStatus.finish;
!icon && (icon = FINISH_ICON);
}
else if (i === currentValue) {
stepStatus = StepStatus.process;
}
if (typeof status === 'string') {
if (i === currentValue) {
const resolveStatus = resolveVariable(status, data);
stepStatus = resolveStatus || status || StepStatus.process;
stepStatus === StepStatus.error && !icon && (icon = ERROR_ICON);
}
}
else if (typeof status === 'object') {
const key = step.value;
key && status[key] && (stepStatus = status[key]);
}
return {
stepStatus,
icon
}
}
return (
<ul className={cx('Steps', className)}>
{stepsRow.map((step, i) => {
const {stepStatus, icon} = getStepStatus(step, i);
return (
<li key={i} className={cx('StepsItem', `is-${stepStatus}`, step.className)}>
<div className={cx('StepsItem-container')}>
<div className={cx('StepsItem-containerIcon')}>
<span className={cx('StepsItem-icon')}>
{
icon ? <Icon icon={icon} className="icon" /> : (i + 1)
}
</span>
</div>
<div className={cx('StepsItem-containerWrapper')}>
<div className={cx('StepsItem-body')}>
<div className={cx('StepsItem-title', i < currentValue && 'is-success')}>
<span>{step.title}</span>
<span className={cx('StepsItem-subTitle')}>{step.subTitle || step.value}</span>
</div>
<div className={cx('StepsItem-description')}>{step.description}</div>
</div>
</div>
</div>
</li>
)
})}
</ul>
)
}
const StepsWithRemoteConfig = withRemoteConfig({
adaptor: data => data.steps || data
})(
class extends React.Component<
RemoteOptionsProps & React.ComponentProps<typeof Steps>
> {
render() {
const {config, ...rest} = this.props;
return (
<Steps config={config} {...rest} />
);
}
}
)
@Renderer({
test: /(^|\/)steps$/,
name: 'steps'
})
export class StepsRenderer extends React.Component<StepsProps> {
render() {
return <StepsWithRemoteConfig {...this.props} />;
}
}