merge feat-timeline

Change-Id: I009b295c17ae4ddc05eb2fb247dfe7c3a55e5fea
This commit is contained in:
jiatianqi 2022-08-11 19:39:50 +08:00
commit b9645bce9d
5 changed files with 742 additions and 0 deletions

View File

@ -122,6 +122,7 @@ import './plugin/Table';
import './plugin/Tabs'; import './plugin/Tabs';
import './plugin/Tasks'; import './plugin/Tasks';
import './plugin/Time'; import './plugin/Time';
import './plugin/Timeline';
import './plugin/Tpl'; import './plugin/Tpl';
import './plugin/AnchorNav'; import './plugin/AnchorNav';
import './plugin/Video'; import './plugin/Video';
@ -135,6 +136,7 @@ import './plugin/WebComponent';
import {GridPlugin} from './plugin/Grid'; import {GridPlugin} from './plugin/Grid';
import './renderer/OptionControl'; import './renderer/OptionControl';
import './renderer/TimelineItemControl';
import './renderer/APIControl'; import './renderer/APIControl';
import './renderer/ValidationControl'; import './renderer/ValidationControl';
import './renderer/ValidationItem'; import './renderer/ValidationItem';

View File

@ -0,0 +1,133 @@
import React from 'react';
import {getEventControlConfig} from '../util';
import {tipedLabel} from '../component/BaseControl';
import {
registerEditorPlugin,
getSchemaTpl
} from 'amis-editor-core';
import { RendererEvent } from 'amis-editor-comp/dist/renderers/event-action';
import {BasePlugin, BaseEventContext} from 'amis-editor-core';
export class TimelinePlugin extends BasePlugin {
rendererName = 'timeline';
$schema = '/schemas/TimelineSchema.json';
label: '时间轴';
type: 'timeline';
name = '时间轴';
isBaseComponent = true;
icon = 'fa fa-bars';
description = '用来展示时间轴';
docLink = '/amis/zh-CN/components/timeline';
tags = ['功能'];
scaffold = {
type: 'timeline',
label: '时间轴',
name: 'timeline',
items: [
{time: '2012-12-21', title: '节点数据'},
{time: '2012-12-24', title: '节点数据'}
]
};
previewSchema = {
...this.scaffold
};
// TODO 事件定义
events: RendererEvent[] = [];
panelTitle = '时间轴';
panelJustify = true;
panelBodyCreator = (context: BaseEventContext) =>
getSchemaTpl('tabs', [
{
title: '属性',
body: getSchemaTpl('collapseGroup', [
{
title: '基本',
body: [
getSchemaTpl('formItemName', {
required: true
}),
getSchemaTpl('label'),
{
label: '排序',
name: 'reverse',
value: false,
type: 'button-group-select',
inline: false,
options: [
{label: '正序', value: false},
{label: '反序', value: true}
]
},
{
label: '时间轴方向',
name: 'direction',
value: 'vertical',
type: 'button-group-select',
inline: true,
options: [
{label: '垂直', value: 'vertical'},
{label: '水平', value: 'horizontal'}
],
},
{
label: tipedLabel(
'文字位置',
'文字相对时间轴位置'
),
name: 'mode',
value: 'right',
type: 'button-group-select',
visibleOn: 'data.direction === "vertical"',
options: [
{label: '左侧', value: 'right'},
{label: '右侧', value: 'left'},
{label: '两侧交替', value: 'alternate'},
]
}
]
},
{
title: '数据',
body: [
getSchemaTpl('timelineItemControl', {
name: 'items',
mode: 'normal',
}),
]
},
getSchemaTpl('status')
])
},
{
title: '外观',
body: getSchemaTpl('collapseGroup', [
{
title: '样式',
body: [
{
name: 'className',
label: '外层',
type: 'input-text',
placeholder: '请输入className'
},
]
}
])
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl',{
name: 'onEvent',
...getEventControlConfig(this.manager, context)
})
]
}
]);
}
registerEditorPlugin(TimelinePlugin);

View File

@ -0,0 +1,585 @@
/**
* @file Timeline组件节点的可视化编辑控件
*/
import React from 'react';
import {findDOMNode} from 'react-dom';
import cx from 'classnames';
import uniqBy from 'lodash/uniqBy';
import Sortable from 'sortablejs';
import {
render as amisRender,
FormItem,
Icon,
InputBox
} from 'amis';
import {tipedLabel} from '../component/BaseControl';
import {autobind} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import type {FormControlProps} from 'amis-core';
import {SchemaApi} from 'amis/lib/Schema';
import { isObject } from 'lodash';
type TimelineItem = {
title: string;
time: string;
detail?: string;
otherConfig?: boolean;
detailCollapsedText?: string;
detailExpandedText?: string;
color?: string | 'info' | 'success' | 'warning' | 'danger';
icon?: string;
}
export interface TimelineItemProps extends FormControlProps {
className?: string;
}
export interface TimelineItemState {
items: Array<Partial<TimelineItem>>;
source: 'custom' | 'api';
api: SchemaApi;
}
export default class TimelineItemControl extends React.Component<
TimelineItemProps,
TimelineItemState
> {
sortable?: Sortable;
drag?: HTMLElement | null;
target: HTMLElement | null;
constructor(props: TimelineItemProps) {
super(props);
this.state = {
items: props.value,
api: props.data.source,
source: props.data.source ? 'api' : 'custom'
}
}
/**
*
*/
@autobind
handleSourceChange(source: 'custom' | 'api') {
this.setState({source: source}, this.onChange);
}
@autobind
handleAPIChange(source: SchemaApi) {
this.setState({api: source}, this.onChange);
}
onChange() {
const {source} = this.state;
const {onBulkChange} = this.props;
const data: Partial<TimelineItemProps> = {
source: undefined,
items: undefined
};
if (source === 'custom') {
const {items} = this.state;
data.items = items.map(item => ({...item}))
}
if (source === 'api') {}
onBulkChange && onBulkChange(data);
}
@autobind
toggleEdit(values: TimelineItem, index: number) {
const items = this.state.items.concat();
items[index] = values;
this.setState({items}, this.onChange);
}
toggleCopy(index: number) {
const {items} = this.state;
const res = items.concat(items[index]);
this.setState({items: res}, this.onChange);
}
toggleDelete(index: number) {
const items = this.state.items.concat();
items.splice(index, 1);
this.setState({items}, this.onChange);
}
handleEditLabel(index: number, value: string, attr: 'time' | 'title') {
const items = this.state.items.concat();
items.splice(index, 1, {...items[index], [attr]: value});
this.setState({items}, () => this.onChange());
}
@autobind
handleBatchAdd(values: {batchItems: string}, action: any) {
const items = this.state.items.concat();
const addedOptions: Array<Partial<TimelineItem>> = values.batchItems
.split('\n')
.map(option => {
const item = option.trim();
if (~item.indexOf(' ')) {
let [time, title] = item.split(' ');
return {time: time.trim(), title: title.trim()};
}
return {label: item, value: item};
});
const newOptions = uniqBy([...items, ...addedOptions], 'time');
this.setState({items: newOptions}, () => this.onChange());
}
@autobind
handleAdd(values: TimelineItem) {
var {items} = this.state;
const itemsTemp = items.concat({...values});
this.setState({items: itemsTemp}, this.onChange);
}
buildAddOrEditSchema(props?: Partial<TimelineItem>) {
return [
{
type: 'input-text',
name: 'time',
required: true,
placeholder: '请输入时间',
label: '时间',
value: props?.['time']
},
{
type: 'input-text',
name: 'title',
required: true,
placeholder: '请输入标题',
label: '标题',
value: props?.['title']
},
{
type: 'textarea',
maxRows: 2,
label: '描述',
name: 'detail',
value: props?.['detail'],
placeholder: '请输入内容'
},
{
type: 'input-text',
name: 'detailCollapsedText',
value: props?.['detailCollapsedText'],
placeholder: '请输入',
label: tipedLabel(
'折叠前文案',
'无配置情况,默认显示标题'
),
},
{
type: 'input-text',
name: 'detailExpandedText',
value: props?.['detailExpandedText'],
placeholder: '请输入',
label: tipedLabel(
'折叠后文案',
'无配置情况,默认显示标题'
)
},
{
type: 'input-color',
name: 'color',
value: props?.['color'],
placeholder: '请输入',
label: '颜色'
},
{
type: 'icon-picker',
name: 'icon',
value: props?.['icon'],
placeholder: '请输入',
label: '图标',
className: 'fix-icon-picker-overflow',
pipeIn: (value: any) => value?.icon,
pipeOut: (value: any) => {
if (value) {
return {
type: 'icon',
vendor: '',
icon: value
}
}
return undefined;
}
}
]
}
buildBatchAddSchema() {
return {
type: 'action',
actionType: 'dialog',
label: '批量添加',
dialog: {
title: '批量添加选项',
headerClassName: 'font-bold',
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
body: [
{
type: 'alert',
level: 'warning',
body: [
{
type: 'tpl',
tpl: '每个选项单列一行,将所有值不重复的项加为新的选项;<br/>每行可通过空格来分别设置time和title,例:"2022-06-23 期末补考"'
}
],
showIcon: true,
className: 'mb-2.5'
},
{
type: 'form',
wrapWithPanel: false,
mode: 'normal',
wrapperComponent: 'div',
resetAfterSubmit: true,
autoFocus: true,
preventEnterSubmit: true,
horizontal: {
left: 0,
right: 12
},
body: [
{
name: 'batchItems',
type: 'textarea',
label: '',
placeholder: '请输入选项内容',
trimContents: true,
minRows: 10,
maxRows: 50,
required: true
}
]
}
]
}
};
}
buildAddSchema() {
return {
type: 'action',
actionType: 'dialog',
label: '添加选项',
active: true,
dialog: {
title: '节点配置',
headerClassName: 'font-bold',
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
body: [
{
type: 'form',
wrapWithPanel: false,
wrapperComponent: 'div',
resetAfterSubmit: true,
autoFocus: true,
preventEnterSubmit: true,
horizontal: {
justify: true,
left: 3,
right: 9
},
body: this.buildAddOrEditSchema()
}
],
},
}
}
@autobind
dragRef(ref: any) {
if (!this.drag && ref) {
this.initDragging();
} else if (this.drag && !ref) {
this.destroyDragging();
}
this.drag = ref;
}
initDragging() {
const dom = findDOMNode(this) as HTMLElement;
this.sortable = new Sortable(
dom.querySelector('.ae-TimelineItemControl-content') as HTMLElement,
{
group: 'TimelineItemControlGroup',
animation: 150,
handle: '.ae-TimelineItemControlItem-dragBar',
ghostClass: 'ae-TimelineItemControlItem--dragging',
onEnd: (e: any) => {
// 没有移动
if (e.newIndex === e.oldIndex) {
return;
}
// 换回来
const parent = e.to as HTMLElement;
if (
e.newIndex < e.oldIndex &&
e.oldIndex < parent.childNodes.length - 1
) {
parent.insertBefore(e.item, parent.childNodes[e.oldIndex + 1]);
} else if (e.oldIndex < parent.childNodes.length - 1) {
parent.insertBefore(e.item, parent.childNodes[e.oldIndex]);
} else {
parent.appendChild(e.item);
}
const items = this.state.items.concat();
items[e.oldIndex] = items.splice(
e.newIndex,
1,
items[e.oldIndex]
)[0];
this.setState({items}, () => this.onChange());
}
}
);
}
destroyDragging() {
this.sortable && this.sortable.destroy();
}
renderHeader() {
const {render, label, labelRemark, useMobileUI, env, popOverContainer} =
this.props;
const classPrefix = env?.theme?.classPrefix;
const {source} = this.state;
const optionSourceList = (
[
{
label: '自定义选项',
value: 'custom'
},
{
label: '接口获取',
value: 'api'
}
] as Array<{
label: string;
value: 'custom' | 'api';
}>
).map(item => ({
...item,
onClick: () => this.handleSourceChange(item.value)
}));
return (
<header className="ae-TimelineItemControl-header">
<label className={cx(`${classPrefix}Form-label`)}>
{label || ''}
{labelRemark
? render('label-remark', {
type: 'remark',
icon: labelRemark.icon || 'warning-mark',
tooltip: labelRemark,
className: cx(`Form-lableRemark`, labelRemark?.className),
useMobileUI,
container: popOverContainer
? popOverContainer
: env && env.getModalContainer
? env.getModalContainer
: undefined
})
: null}
</label>
<div>
{render(
'validation-control-addBtn',
{
type: 'dropdown-button',
level: 'link',
size: 'sm',
label: '${selected}',
align: 'right',
closeOnClick: true,
closeOnOutside: true,
buttons: optionSourceList
},
{
popOverContainer: null,
data: {
selected: optionSourceList.find(item => item.value === source)!
.label
}
}
)}
</div>
</header>
);
}
renderOption(props: TimelineItem & {index: number}) {
const {time, title, index} = props;
return (
<li className="ae-TimelineItemControlItem" key={index}>
<div className="ae-TimelineItemControlItem-Main">
<a className="ae-TimelineItemControlItem-dragBar">
<Icon icon="drag-bar" className="icon" />
</a>
<InputBox
className="ae-TimelineItemControlItem-input"
value={time}
placeholder="请输入显示时间"
clearable={false}
onChange={(value: string) => this.handleEditLabel(index, value, 'time')}
/>
{/* {amisRender(
{
type: "input-date",
name: "time",
value: time,
className: "ae-TimelineItemControlItem-inputDate",
label: ""
}
)} */}
{amisRender({
type: 'dropdown-button',
className: 'ae-TimelineItemControlItem-dropdown',
btnClassName: 'px-2',
icon: 'fa fa-ellipsis-h',
hideCaret: true,
closeOnClick: true,
align: 'right',
menuClassName: 'ae-TimelineItemControlItem-ulmenu',
buttons: [
{
type: 'action',
className: 'ae-TimelineItemControlItem-action',
label: '编辑',
actionType: 'dialog',
dialog: {
title: '节点配置',
headerClassName: 'font-bold',
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
body: [
{
type: 'form',
wrapWithPanel: false,
wrapperComponent: 'div',
resetAfterSubmit: true,
autoFocus: true,
preventEnterSubmit: true,
horizontal: {
justify: true,
left: 3,
right: 9
},
body: this.buildAddOrEditSchema(props),
onSubmit: (e: any) => this.toggleEdit(e, index)
},
],
},
},
{
type: 'button',
className: 'ae-TimelineItemControlItem-action',
label: '复制',
onClick: () => this.toggleCopy(index)
},
{
type: 'button',
className: 'ae-TimelineItemControlItem-action',
label: '删除',
onClick: () => this.toggleDelete(index)
}
]
})}
</div>
<div className="ae-TimelineItemControlItem-Main">
<InputBox
className="ae-TimelineItemControlItem-input-title"
value={title}
clearable={false}
placeholder="请输入标题"
onChange={(value: string) => this.handleEditLabel(index, value, 'title')}
/>
</div>
</li>
);
}
renderApiPanel() {
const {render} = this.props;
const {source, api} = this.state;
if (source !== 'api') {
return null;
}
return render(
'api',
getSchemaTpl('apiControl', {
label: '接口',
name: 'source',
className: 'ae-ExtendMore',
visibleOn: 'data.autoComplete !== false',
value: api,
onChange: this.handleAPIChange
})
);
}
render() {
const {source, items} = this.state;
const {render, className} = this.props;
return (
<div className={cx('ae-TimelineItemControl', className)}>
{this.renderHeader()}
{source === 'custom'
? <div className="ae-TimelineItemControl-wrapper">
{Array.isArray(items) && items.length ? (
<ul className="ae-TimelineItemControl-content" ref={this.dragRef} >
{items.map((item: TimelineItem, index: number) =>
this.renderOption({...item, index})
)}
</ul>
) : (
<div className="ae-TimelineItemControl-placeholder"></div>
)}
<div className="ae-TimelineItemControl-footer">
{amisRender(this.buildAddSchema(), {
onSubmit: this.handleAdd
})}
{amisRender(this.buildBatchAddSchema(), {
onSubmit: this.handleBatchAdd
})}
</div>
</div>
: null}
{this.renderApiPanel()}
</div>
)
}
}
@FormItem({type: 'ae-timelineItemControl', renderLabel: false})
export class TimelineItemControlRenderer extends React.Component<TimelineItemProps> {
render() {
return <TimelineItemControl {...this.props} />
}
}

View File

@ -1008,3 +1008,16 @@ setSchemaTpl(
}; };
} }
); );
setSchemaTpl('iconLink', (schema: {name: 'icon' | 'rightIcon', visibleOn: boolean}) => {
const {name, visibleOn} = schema;
return {
name: name,
visibleOn,
label: '图标',
type: 'icon-picker',
placeholder: '点击选择图标',
clearable: true,
description: ''
}
});

View File

@ -305,6 +305,15 @@ setSchemaTpl('optionControlV2', {
closeDefaultCheck: true // 关闭默认值设置 closeDefaultCheck: true // 关闭默认值设置
}); });
/**
*
*/
setSchemaTpl('timelineItemControl', {
label: '数据',
model: 'normal',
type: 'ae-timelineItemControl'
});
setSchemaTpl('treeOptionControl', { setSchemaTpl('treeOptionControl', {
label: '数据', label: '数据',
mode: 'normal', mode: 'normal',