mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
parent
4125f16cf7
commit
c96f9be821
235
docs/zh-CN/components/switch-container.md
Normal file
235
docs/zh-CN/components/switch-container.md
Normal file
@ -0,0 +1,235 @@
|
||||
---
|
||||
title: switch-container 状态容器
|
||||
description:
|
||||
type: 0
|
||||
group: ⚙ 组件
|
||||
menuName: switch-container 容器
|
||||
icon:
|
||||
order: 50
|
||||
---
|
||||
|
||||
switch-container 是一种特殊的容器组件,它可以根据动态数据显示条件渲染组件的某一状态。注意容器只会显示最多一种状态,只显示首个命中的状态。容器的不同状态是对应的展示配置,可通过组件搭配与嵌套设计任意展示样式。状态容器的外观与事件动作与容器组件类似,支持常见的外观设置与点击、移入、移出事件动作。
|
||||
|
||||
状态容器主要用于编辑器内统一管理复杂组件的多种状态,同时避免因为组件多状态显示而干扰设计。如果只使用 amis 引擎,也可以直接用容器加显示条件实现。
|
||||
|
||||
## 基本用法
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"title": "",
|
||||
"mode": "horizontal",
|
||||
"dsType": "api",
|
||||
"feat": "Insert",
|
||||
"body": [
|
||||
{
|
||||
"type": "button-group-select",
|
||||
"name": "state",
|
||||
"label": "切换状态",
|
||||
"inline": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "选项1",
|
||||
"value": "a"
|
||||
},
|
||||
{
|
||||
"label": "选项2",
|
||||
"value": "b"
|
||||
}
|
||||
],
|
||||
"multiple": false,
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"type": "switch-container",
|
||||
"items": [
|
||||
{
|
||||
"title": "状态1",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容1",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
],
|
||||
"visibleOn": "${state == \"a\"}"
|
||||
},
|
||||
{
|
||||
"title": "状态2",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容2",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
],
|
||||
"visibleOn": "${state == \"b\"}"
|
||||
}
|
||||
],
|
||||
"style": {
|
||||
"position": "static",
|
||||
"display": "block"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [],
|
||||
"resetAfterSubmit": true
|
||||
}
|
||||
```
|
||||
|
||||
### style
|
||||
|
||||
container 可以通过 style 来设置样式,比如背景色或背景图,注意这里的属性是使用驼峰写法,是 `backgroundColor` 而不是 `background-color`。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "switch-container",
|
||||
"style": {
|
||||
"backgroundColor": "#C4C4C4"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"title": "状态1",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容1",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| --------- | ----------------------------------------- | ------------- | ----------------------- |
|
||||
| type | `string` | `"container"` | 指定为 container 渲染器 |
|
||||
| className | `string` | | 外层 Dom 的类名 |
|
||||
| style | `Object` | | 自定义样式 |
|
||||
| items | [SchemaNode](../../docs/types/schemanode) | | 容器内容 |
|
||||
|
||||
## 事件表
|
||||
|
||||
> 3.3.0 及以上版本
|
||||
|
||||
当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细查看[事件动作](../../docs/concepts/event-action)。
|
||||
|
||||
| 事件名称 | 事件参数 | 说明 |
|
||||
| ---------- | -------- | -------------- |
|
||||
| click | - | 点击时触发 |
|
||||
| mouseenter | - | 鼠标移入时触发 |
|
||||
| mouseleave | - | 鼠标移出时触发 |
|
||||
|
||||
### click
|
||||
|
||||
鼠标点击。可以尝试通过`${event.context.nativeEvent}`获取鼠标事件对象。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "switch-container",
|
||||
"items": [
|
||||
{
|
||||
"title": "状态1",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容1",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"onEvent": {
|
||||
"click": {
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "toast",
|
||||
"args": {
|
||||
"msgType": "info",
|
||||
"msg": "${event.context.nativeEvent.type}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### mouseenter
|
||||
|
||||
鼠标移入。可以尝试通过`${event.context.nativeEvent}`获取鼠标事件对象。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "switch-container",
|
||||
"items": [
|
||||
{
|
||||
"title": "状态1",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容1",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"onEvent": {
|
||||
"mouseenter": {
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "toast",
|
||||
"args": {
|
||||
"msgType": "info",
|
||||
"msg": "${event.context.nativeEvent.type}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### mouseleave
|
||||
|
||||
鼠标移出。可以尝试通过`${event.context.nativeEvent}`获取鼠标事件对象。
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "switch-container",
|
||||
"items": [
|
||||
{
|
||||
"title": "状态1",
|
||||
"body": [
|
||||
{
|
||||
"type": "tpl",
|
||||
"tpl": "状态内容1",
|
||||
"wrapperComponent": "",
|
||||
"inline": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"onEvent": {
|
||||
"mouseleave": {
|
||||
"actions": [
|
||||
{
|
||||
"actionType": "toast",
|
||||
"args": {
|
||||
"msgType": "info",
|
||||
"msg": "${event.context.nativeEvent.type}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -145,6 +145,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置面板 combo 多行模式样式调整 */
|
||||
.cxd-Combo--ver:not(.cxd-Combo--noBorder) > .ae-Combo-items {
|
||||
margin: px2rem(8px) 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cxd-Combo--ver:not(.cxd-Combo--noBorder) > .ae-Combo-items > .cxd-Combo-item {
|
||||
margin: 0;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ae-Combo-items + div {
|
||||
padding-left: 0px !important;
|
||||
margin-bottom: px2rem(12px);
|
||||
|
@ -34,6 +34,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cxd-DropDown,
|
||||
.cxd-DropDown > .cxd-Button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1335,6 +1335,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-renderer='switch-container'],
|
||||
[data-renderer='container'] {
|
||||
min-height: 0;
|
||||
}
|
||||
|
@ -304,6 +304,7 @@ export interface RendererInfo extends RendererScaffoldInfo {
|
||||
sharedContext?: Record<string, any>;
|
||||
dialogTitle?: string; //弹窗标题用于弹窗大纲的展示
|
||||
dialogType?: string; //区分确认对话框类型
|
||||
subEditorVariable?: Array<{label: string; children: any}>; // 传递给子编辑器的组件自定义变量,如listSelect的选项名称和值
|
||||
}
|
||||
|
||||
export type BasicRendererInfo = Omit<
|
||||
@ -1049,7 +1050,8 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
isBaseComponent: plugin.isBaseComponent,
|
||||
isListComponent: plugin.isListComponent,
|
||||
rendererName: plugin.rendererName,
|
||||
memberImmutable: plugin.memberImmutable
|
||||
memberImmutable: plugin.memberImmutable,
|
||||
subEditorVariable: plugin.subEditorVariable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1209,10 +1209,16 @@ export async function resolveVariablesFromScope(node: any, manager: any) {
|
||||
(await manager?.dataSchema?.getDataPropsAsOptions()) ?? []
|
||||
);
|
||||
|
||||
// 子编辑器内读取的host节点自定义变量,非数据域方式,如listSelect的选项值
|
||||
let hostNodeVaraibles = [];
|
||||
if (manager?.store?.isSubEditor) {
|
||||
hostNodeVaraibles = manager.config?.hostNode?.info?.subEditorVariable || [];
|
||||
}
|
||||
|
||||
const variables: VariableItem[] =
|
||||
manager?.variableManager?.getVariableFormulaOptions() || [];
|
||||
|
||||
return [...dataPropsAsOptions, ...variables].filter(
|
||||
return [...hostNodeVaraibles, ...dataPropsAsOptions, ...variables].filter(
|
||||
(item: any) => item.children?.length
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<path d="M0 265.142857v713.142857h1024v-713.142857H0z m987.428571 676.571429H36.571429v-640h950.857142v640zM54.857143 228.571429h914.285714a18.285714 18.285714 0 0 0 0-36.571429H54.857143a18.285714 18.285714 0 0 0 0 36.571429zM109.714286 155.428571h804.571428a18.285714 18.285714 0 0 0 0-36.571428H109.714286a18.285714 18.285714 0 0 0 0 36.571428zM164.571429 82.285714h694.857142a18.285714 18.285714 0 0 0 0-36.571428H164.571429a18.285714 18.285714 0 0 0 0 36.571428z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 626 B |
@ -29,6 +29,7 @@ import wizard from './feat/wizard.svg';
|
||||
import anchorNav from './container/anchor-nav.svg';
|
||||
import collapse from './container/collapse.svg';
|
||||
import container from './container/container.svg';
|
||||
import swtichContainer from './container/switch-container.svg';
|
||||
import flexContainer from './container/flex-container.svg';
|
||||
import formGroup from './container/form-group.svg';
|
||||
import grid from './container/grid.svg';
|
||||
@ -207,6 +208,7 @@ registerIcon('anchor-nav-plugin', anchorNav);
|
||||
registerIcon('collapse-plugin', collapse);
|
||||
registerIcon('flex-container-plugin', flexContainer);
|
||||
registerIcon('container-plugin', container);
|
||||
registerIcon('switch-container-plugin', swtichContainer);
|
||||
registerIcon('form-group-plugin', formGroup);
|
||||
registerIcon('panel-plugin', panel);
|
||||
registerIcon('grid-plugin', grid);
|
||||
|
@ -48,6 +48,7 @@ import './renderer/crud2-control/CRUDToolbarControl';
|
||||
import './renderer/crud2-control/CRUDFiltersControl';
|
||||
import './renderer/InputRangeValueControl';
|
||||
import './renderer/FunctionEditorControl';
|
||||
import './renderer/ListItemControl';
|
||||
|
||||
import 'amis-theme-editor/lib/locale/zh-CN';
|
||||
import 'amis-theme-editor/lib/locale/en-US';
|
||||
|
@ -1,11 +1,15 @@
|
||||
import {EditorNodeType, getSchemaTpl} from 'amis-editor-core';
|
||||
import {
|
||||
EditorNodeType,
|
||||
JSONPipeIn,
|
||||
JSONPipeOut,
|
||||
getSchemaTpl
|
||||
} from 'amis-editor-core';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {BasePlugin, BaseEventContext} from 'amis-editor-core';
|
||||
|
||||
import {BasePlugin, BaseEventContext, diff} from 'amis-editor-core';
|
||||
import {formItemControl} from '../../component/BaseControl';
|
||||
import {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
|
||||
import {resolveOptionType} from '../../util';
|
||||
import type {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
|
||||
import type {Schema} from 'amis';
|
||||
import {resolveOptionType, schemaArrayFormat, schemaToArray} from '../../util';
|
||||
|
||||
export class ListControlPlugin extends BasePlugin {
|
||||
static id = 'ListControlPlugin';
|
||||
@ -105,6 +109,22 @@ export class ListControlPlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
subEditorVariable: Array<{label: string; children: any}> = [
|
||||
{
|
||||
label: '当前选项',
|
||||
children: [
|
||||
{
|
||||
label: '选项名称',
|
||||
value: 'label'
|
||||
},
|
||||
{
|
||||
label: '选项值',
|
||||
value: 'value'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
return formItemControl(
|
||||
{
|
||||
@ -119,7 +139,11 @@ export class ListControlPlugin extends BasePlugin {
|
||||
getSchemaTpl('multiple'),
|
||||
getSchemaTpl('extractValue'),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: (schema: Schema) => schema,
|
||||
// 边栏渲染不渲染自定义样式,会干扰css生成
|
||||
rendererSchema: (schema: Schema) => ({
|
||||
...(schema || {}),
|
||||
itemSchema: null
|
||||
}),
|
||||
mode: 'vertical',
|
||||
useSelectMode: true, // 改用 Select 设置模式
|
||||
visibleOn: 'this.options && this.options.length > 0'
|
||||
@ -127,7 +151,67 @@ export class ListControlPlugin extends BasePlugin {
|
||||
]
|
||||
},
|
||||
option: {
|
||||
body: [getSchemaTpl('optionControlV2')]
|
||||
body: [
|
||||
getSchemaTpl('optionControlV2'),
|
||||
{
|
||||
type: 'ae-switch-more',
|
||||
mode: 'normal',
|
||||
label: '自定义显示模板',
|
||||
bulk: false,
|
||||
name: 'itemSchema',
|
||||
formType: 'extend',
|
||||
form: {
|
||||
body: [
|
||||
{
|
||||
type: 'dropdown-button',
|
||||
label: '配置显示模板',
|
||||
level: 'enhance',
|
||||
buttons: [
|
||||
{
|
||||
type: 'button',
|
||||
block: true,
|
||||
onClick: this.editDetail.bind(
|
||||
this,
|
||||
context.id,
|
||||
'itemSchema'
|
||||
),
|
||||
label: '配置默认态模板'
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
block: true,
|
||||
onClick: this.editDetail.bind(
|
||||
this,
|
||||
context.id,
|
||||
'activeItemSchema'
|
||||
),
|
||||
label: '配置激活态模板'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
pipeIn: (value: any) => {
|
||||
return value !== undefined;
|
||||
},
|
||||
pipeOut: (value: any, originValue: any, data: any) => {
|
||||
if (value === true) {
|
||||
return {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: `\${${this.getDisplayField(value)}}`,
|
||||
wrapperComponent: '',
|
||||
inline: true
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
return value ? value : undefined;
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
status: {}
|
||||
},
|
||||
@ -184,6 +268,67 @@ export class ListControlPlugin extends BasePlugin {
|
||||
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
filterProps(props: any) {
|
||||
// 禁止选中子节点
|
||||
return JSONPipeOut(props);
|
||||
}
|
||||
|
||||
getDisplayField(data: any) {
|
||||
if (
|
||||
data.source ||
|
||||
(data.map &&
|
||||
Array.isArray(data.map) &&
|
||||
data.map[0] &&
|
||||
Object.keys(data.map[0]).length > 1)
|
||||
) {
|
||||
return data.labelField ?? 'label';
|
||||
}
|
||||
return 'label';
|
||||
}
|
||||
|
||||
editDetail(id: string, field: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(id);
|
||||
const value = store.getValueOf(id);
|
||||
let defaultItemSchema = {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: `\${${this.getDisplayField(value)}}`,
|
||||
inline: true,
|
||||
wrapperComponent: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 首次编辑激活态样式时自动复制默认态
|
||||
if (field !== 'itemSchema' && value?.itemSchema) {
|
||||
defaultItemSchema = JSONPipeIn(value.itemSchema, true);
|
||||
}
|
||||
|
||||
node &&
|
||||
value &&
|
||||
this.manager.openSubEditor({
|
||||
title: '配置显示模板',
|
||||
value: value[field] ?? defaultItemSchema,
|
||||
slot: {
|
||||
type: 'container',
|
||||
body: '$$'
|
||||
},
|
||||
onChange: (newValue: any) => {
|
||||
newValue = {...value, [field]: schemaArrayFormat(newValue)};
|
||||
manager.panelChangeValue(newValue, diff(value, newValue));
|
||||
},
|
||||
data: {
|
||||
[value.labelField || 'label']: '选项名',
|
||||
[value.valueField || 'value']: '选项值',
|
||||
item: '假数据'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorPlugin(ListControlPlugin);
|
||||
|
480
packages/amis-editor/src/plugin/SwitchContainer.tsx
Normal file
480
packages/amis-editor/src/plugin/SwitchContainer.tsx
Normal file
@ -0,0 +1,480 @@
|
||||
import {
|
||||
BaseEventContext,
|
||||
LayoutBasePlugin,
|
||||
RegionConfig,
|
||||
registerEditorPlugin,
|
||||
getSchemaTpl,
|
||||
RendererPluginEvent,
|
||||
VRendererConfig,
|
||||
VRenderer,
|
||||
RendererInfo,
|
||||
BasicToolbarItem
|
||||
} from 'amis-editor-core';
|
||||
import {RegionWrapper as Region} from 'amis-editor-core';
|
||||
import {getEventControlConfig} from '../renderer/event-control';
|
||||
import React from 'react';
|
||||
|
||||
export class SwitchContainerPlugin extends LayoutBasePlugin {
|
||||
static id = 'SwitchContainerPlugin';
|
||||
static scene = ['layout'];
|
||||
// 关联渲染器名字
|
||||
rendererName = 'switch-container';
|
||||
$schema = '/schemas/SwitchContainerSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '状态容器';
|
||||
isBaseComponent = true;
|
||||
description = '根据状态进行组件条件渲染的容器,方便设计多状态组件';
|
||||
tags = ['布局容器'];
|
||||
order = -2;
|
||||
icon = 'fa fa-square-o';
|
||||
pluginIcon = 'switch-container-plugin';
|
||||
scaffold = {
|
||||
type: 'switch-container',
|
||||
items: [
|
||||
{
|
||||
title: '状态一',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '状态一内容',
|
||||
wrapperComponent: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '状态二',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '状态二内容',
|
||||
wrapperComponent: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
}
|
||||
};
|
||||
previewSchema = {
|
||||
...this.scaffold
|
||||
};
|
||||
|
||||
regions: Array<RegionConfig> = [
|
||||
{
|
||||
key: 'body',
|
||||
label: '内容区'
|
||||
}
|
||||
];
|
||||
|
||||
panelTitle = '状态容器';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
vRendererConfig: VRendererConfig = {
|
||||
regions: {
|
||||
body: {
|
||||
key: 'body',
|
||||
label: '内容区',
|
||||
placeholder: '状态',
|
||||
wrapperResolve: (dom: HTMLElement) => dom
|
||||
}
|
||||
},
|
||||
panelTitle: '状态',
|
||||
panelJustify: true,
|
||||
panelBodyCreator: (context: BaseEventContext) => {
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基础',
|
||||
body: [
|
||||
{
|
||||
name: 'title',
|
||||
label: '状态名称',
|
||||
type: 'input-text',
|
||||
required: true
|
||||
},
|
||||
getSchemaTpl('expressionFormulaControl', {
|
||||
evalMode: false,
|
||||
label: '状态条件',
|
||||
name: 'visibleOn',
|
||||
placeholder: '\\${xxx}'
|
||||
})
|
||||
]
|
||||
}
|
||||
])
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
wrapperProps = {
|
||||
unmountOnExit: true,
|
||||
mountOnEnter: true
|
||||
};
|
||||
|
||||
stateWrapperResolve = (dom: HTMLElement) => dom;
|
||||
overrides = {
|
||||
renderBody(this: any, item: any) {
|
||||
const dom = this.super(item);
|
||||
const info: RendererInfo = this.props.$$editor;
|
||||
const items = this.props.items || [];
|
||||
const index = items.findIndex((cur: any) => cur.$$id === item.$$id);
|
||||
|
||||
if (!info || !info.plugin) {
|
||||
return dom;
|
||||
}
|
||||
|
||||
const plugin: SwitchContainerPlugin = info.plugin as any;
|
||||
const id = item.$$id;
|
||||
const region = plugin.vRendererConfig?.regions?.body;
|
||||
|
||||
return (
|
||||
<VRenderer
|
||||
type={info.type}
|
||||
plugin={info.plugin}
|
||||
renderer={info.renderer}
|
||||
multifactor
|
||||
key={id}
|
||||
//$schema="/schemas/ListBodyField.json"
|
||||
hostId={info.id}
|
||||
memberIndex={index}
|
||||
name={`${item.title || `状态${index + 1}`}`}
|
||||
id={id}
|
||||
draggable={false}
|
||||
wrapperResolve={plugin.stateWrapperResolve}
|
||||
schemaPath={`${info.schemaPath}/items/${index}`}
|
||||
path={`${this.props.$path}/${index}`}
|
||||
data={this.props.data}
|
||||
>
|
||||
{region ? (
|
||||
<Region
|
||||
key={region.key}
|
||||
preferTag={region.preferTag}
|
||||
name={region.key}
|
||||
label={region.label}
|
||||
regionConfig={region}
|
||||
placeholder={region.placeholder}
|
||||
editorStore={plugin.manager.store}
|
||||
manager={plugin.manager}
|
||||
children={dom}
|
||||
wrapperResolve={region.wrapperResolve}
|
||||
rendererName={info.renderer.name}
|
||||
/>
|
||||
) : (
|
||||
dom
|
||||
)}
|
||||
</VRenderer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 补充切换的 toolbar
|
||||
* @param context
|
||||
* @param toolbars
|
||||
*/
|
||||
buildEditorToolbar(
|
||||
context: BaseEventContext,
|
||||
toolbars: Array<BasicToolbarItem>
|
||||
) {
|
||||
if (
|
||||
context.info.plugin === this &&
|
||||
context.info.renderer.name === 'switch-container' &&
|
||||
!context.info.hostId
|
||||
) {
|
||||
const node = context.node;
|
||||
toolbars.unshift({
|
||||
icon: 'fa fa-chevron-right',
|
||||
tooltip: '下个状态',
|
||||
onClick: () => {
|
||||
const control = node.getComponent();
|
||||
if (control?.switchTo) {
|
||||
let index =
|
||||
control.state.activeIndex < 0 ? 0 : control.state.activeIndex;
|
||||
control.switchTo(index + 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
toolbars.unshift({
|
||||
icon: 'fa fa-chevron-left',
|
||||
tooltip: '上个状态',
|
||||
onClick: () => {
|
||||
const control = node.getComponent();
|
||||
if (control?.switchTo) {
|
||||
let index = control.state.activeIndex;
|
||||
control.switchTo(index - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 事件定义
|
||||
events: RendererPluginEvent[] = [
|
||||
{
|
||||
eventName: 'click',
|
||||
eventLabel: '点击',
|
||||
description: '点击时触发',
|
||||
dataSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
context: {
|
||||
type: 'object',
|
||||
title: '上下文',
|
||||
properties: {
|
||||
nativeEvent: {
|
||||
type: 'object',
|
||||
title: '鼠标事件对象'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
eventName: 'mouseenter',
|
||||
eventLabel: '鼠标移入',
|
||||
description: '鼠标移入时触发',
|
||||
dataSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
context: {
|
||||
type: 'object',
|
||||
title: '上下文',
|
||||
properties: {
|
||||
nativeEvent: {
|
||||
type: 'object',
|
||||
title: '鼠标事件对象'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
eventName: 'mouseleave',
|
||||
eventLabel: '鼠标移出',
|
||||
description: '鼠标移出时触发',
|
||||
dataSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
context: {
|
||||
type: 'object',
|
||||
title: '上下文',
|
||||
properties: {
|
||||
nativeEvent: {
|
||||
type: 'object',
|
||||
title: '鼠标事件对象'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const curRendererSchema = context?.schema;
|
||||
const isFreeContainer = curRendererSchema?.isFreeContainer || false;
|
||||
const isFlexItem = this.manager?.isFlexItem(context?.id);
|
||||
const isFlexColumnItem = this.manager?.isFlexColumnItem(context?.id);
|
||||
|
||||
const displayTpl = [
|
||||
getSchemaTpl('layout:display'),
|
||||
|
||||
getSchemaTpl('layout:flex-setting', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")',
|
||||
direction: curRendererSchema.direction,
|
||||
justify: curRendererSchema.justify,
|
||||
alignItems: curRendererSchema.alignItems
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:flex-wrap', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.display === "flex" || data.style.display === "inline-flex")'
|
||||
})
|
||||
];
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'ae-listItemControl',
|
||||
mode: 'normal',
|
||||
name: 'items',
|
||||
label: '状态列表',
|
||||
addTip: '新增组件状态',
|
||||
items: [
|
||||
{
|
||||
type: 'input-text',
|
||||
placeholder: '请输入显示文本',
|
||||
label: '状态名称',
|
||||
mode: 'horizontal',
|
||||
name: 'title'
|
||||
},
|
||||
getSchemaTpl('expressionFormulaControl', {
|
||||
name: 'visibleOn',
|
||||
mode: 'horizontal',
|
||||
label: '显示条件'
|
||||
})
|
||||
],
|
||||
scaffold: {
|
||||
title: '状态',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '状态内容',
|
||||
wrapperComponent: '',
|
||||
inline: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
className: 'p-none',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '布局',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition'),
|
||||
getSchemaTpl('layout:inset', {
|
||||
mode: 'vertical'
|
||||
}),
|
||||
|
||||
// 自由容器不需要 display 相关配置项
|
||||
...(!isFreeContainer ? displayTpl : []),
|
||||
|
||||
...(isFlexItem
|
||||
? [
|
||||
getSchemaTpl('layout:flex', {
|
||||
isFlexColumnItem,
|
||||
label: isFlexColumnItem ? '高度设置' : '宽度设置',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative")'
|
||||
}),
|
||||
getSchemaTpl('layout:flex-grow', {
|
||||
visibleOn:
|
||||
'data.style && data.style.flex === "1 1 auto" && (data.style.position === "static" || data.style.position === "relative")'
|
||||
}),
|
||||
getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '弹性高度' : '弹性宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "1 1 auto"'
|
||||
}),
|
||||
getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '固定高度' : '固定宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "0 0 150px"'
|
||||
})
|
||||
]
|
||||
: []),
|
||||
|
||||
getSchemaTpl('layout:overflow-x', {
|
||||
visibleOn: `${
|
||||
isFlexItem && !isFlexColumnItem
|
||||
} && data.style.flex === '0 0 150px'`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:isFixedHeight', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`,
|
||||
onChange: (value: boolean) => {
|
||||
context?.node.setHeightMutable(value);
|
||||
}
|
||||
}),
|
||||
getSchemaTpl('layout:height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:max-height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:min-height', {
|
||||
visibleOn: `${!isFlexItem || !isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:overflow-y', {
|
||||
visibleOn: `${
|
||||
!isFlexItem || !isFlexColumnItem
|
||||
} && (data.isFixedHeight || data.style && data.style.maxHeight) || (${
|
||||
isFlexItem && isFlexColumnItem
|
||||
} && data.style.flex === '0 0 150px')`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:isFixedWidth', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`,
|
||||
onChange: (value: boolean) => {
|
||||
context?.node.setWidthMutable(value);
|
||||
}
|
||||
}),
|
||||
getSchemaTpl('layout:width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:max-width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
getSchemaTpl('layout:min-width', {
|
||||
visibleOn: `${!isFlexItem || isFlexColumnItem}`
|
||||
}),
|
||||
|
||||
getSchemaTpl('layout:overflow-x', {
|
||||
visibleOn: `${
|
||||
!isFlexItem || isFlexColumnItem
|
||||
} && (data.isFixedWidth || data.style && data.style.maxWidth)`
|
||||
}),
|
||||
|
||||
!isFlexItem ? getSchemaTpl('layout:margin-center') : null,
|
||||
!isFlexItem && !isFreeContainer
|
||||
? getSchemaTpl('layout:textAlign', {
|
||||
name: 'style.textAlign',
|
||||
label: '内部对齐方式',
|
||||
visibleOn:
|
||||
'data.style && data.style.display !== "flex" && data.style.display !== "inline-flex"'
|
||||
})
|
||||
: null,
|
||||
getSchemaTpl('layout:z-index'),
|
||||
getSchemaTpl('layout:sticky', {
|
||||
visibleOn:
|
||||
'data.style && (data.style.position !== "fixed" && data.style.position !== "absolute")'
|
||||
}),
|
||||
getSchemaTpl('layout:stickyPosition')
|
||||
]
|
||||
},
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '事件',
|
||||
className: 'p-none',
|
||||
body: [
|
||||
getSchemaTpl('eventControl', {
|
||||
name: 'onEvent',
|
||||
...getEventControlConfig(this.manager, context)
|
||||
})
|
||||
]
|
||||
}
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
registerEditorPlugin(SwitchContainerPlugin);
|
@ -9,6 +9,7 @@ export * from './Layout/Layout_fixed'; // 悬浮容器
|
||||
export * from './CollapseGroup'; // 折叠面板
|
||||
export * from './Panel'; // 面板
|
||||
export * from './Tabs'; // 选项卡
|
||||
export * from './SwitchContainer'; // 状态容器
|
||||
|
||||
// 数据容器
|
||||
export * from './CRUD'; // 增删改查
|
||||
|
393
packages/amis-editor/src/renderer/ListItemControl.tsx
Normal file
393
packages/amis-editor/src/renderer/ListItemControl.tsx
Normal file
@ -0,0 +1,393 @@
|
||||
/**
|
||||
* @file 通用数组列表项的可视化编辑控件
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import cx from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import Sortable from 'sortablejs';
|
||||
import {FormItem, Button, Icon, render as amisRender} from 'amis';
|
||||
import {autobind} from 'amis-editor-core';
|
||||
import type {Option} from 'amis';
|
||||
import {createObject, FormControlProps} from 'amis-core';
|
||||
import type {SchemaApi} from 'amis';
|
||||
import type {PlainObject} from './style-control/types';
|
||||
|
||||
export type valueType = 'text' | 'boolean' | 'number';
|
||||
|
||||
export interface PopoverForm {
|
||||
optionLabel: string;
|
||||
optionValue: any;
|
||||
optionValueType: valueType;
|
||||
}
|
||||
|
||||
export interface OptionControlProps extends FormControlProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type SourceType = 'custom' | 'api' | 'apicenter' | 'variable';
|
||||
|
||||
export interface OptionControlState {
|
||||
items: Array<PlainObject>;
|
||||
api: SchemaApi;
|
||||
labelField: string;
|
||||
valueField: string;
|
||||
}
|
||||
|
||||
export default class ListItemControl extends React.Component<
|
||||
OptionControlProps,
|
||||
OptionControlState
|
||||
> {
|
||||
sortable?: Sortable;
|
||||
drag?: HTMLElement | null;
|
||||
target: HTMLElement | null;
|
||||
|
||||
internalProps = ['checked', 'editing'];
|
||||
|
||||
constructor(props: OptionControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
items: this.transformOptions(props),
|
||||
api: props.data.source,
|
||||
labelField: props.data.labelField || 'title',
|
||||
valueField: props.data.valueField
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据更新
|
||||
*/
|
||||
componentWillReceiveProps(nextProps: OptionControlProps) {
|
||||
const items = get(nextProps, 'items')
|
||||
? this.transformOptions(nextProps)
|
||||
: [];
|
||||
if (
|
||||
JSON.stringify(
|
||||
this.state.items.map(item => ({
|
||||
...item,
|
||||
editing: undefined
|
||||
}))
|
||||
) !== JSON.stringify(items)
|
||||
) {
|
||||
this.setState({
|
||||
items
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理填入输入框的值
|
||||
*/
|
||||
transformOptionValue(value: any) {
|
||||
return typeof value === 'undefined' || value === null
|
||||
? ''
|
||||
: typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value);
|
||||
}
|
||||
|
||||
transformOptions(props: OptionControlProps) {
|
||||
const {data: ctx, value: options} = props;
|
||||
|
||||
return Array.isArray(options)
|
||||
? options.map((item: Option) => ({
|
||||
...item,
|
||||
...(item.hidden !== undefined ? {hidden: item.hidden} : {}),
|
||||
...(item.hiddenOn !== undefined ? {hiddenOn: item.hiddenOn} : {})
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新options字段的统一出口
|
||||
*/
|
||||
onChange() {
|
||||
const {onChange} = this.props;
|
||||
onChange(this.state.items);
|
||||
return;
|
||||
}
|
||||
|
||||
@autobind
|
||||
targetRef(ref: any) {
|
||||
this.target = ref ? (findDOMNode(ref) as HTMLElement) : null;
|
||||
}
|
||||
|
||||
@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-OptionControl-content') as HTMLElement,
|
||||
{
|
||||
group: 'OptionControlGroup',
|
||||
animation: 150,
|
||||
handle: '.ae-OptionControlItem-dragBar',
|
||||
ghostClass: 'ae-OptionControlItem--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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除选项
|
||||
*/
|
||||
handleDelete(index: number) {
|
||||
const items = this.state.items.concat();
|
||||
|
||||
items.splice(index, 1);
|
||||
this.setState({items}, () => this.onChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑选项
|
||||
*/
|
||||
toggleEdit(index: number) {
|
||||
const {items} = this.state;
|
||||
items[index].editing = !items[index].editing;
|
||||
this.setState({items});
|
||||
}
|
||||
|
||||
editItem(item: PlainObject, index: number) {
|
||||
const items = this.state.items.concat();
|
||||
if (items[index]) {
|
||||
items[index] = item;
|
||||
}
|
||||
this.setState({items}, () => this.onChange());
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleEditLabel(index: number, value: string) {
|
||||
const items = this.state.items.concat();
|
||||
items.splice(index, 1, {...items[index], [this.state.labelField]: value});
|
||||
this.setState({items}, () => this.onChange());
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAdd() {
|
||||
const scaffold = this.props.scaffold;
|
||||
const {labelField} = this.state;
|
||||
const items = this.state.items.slice();
|
||||
items.push(
|
||||
scaffold
|
||||
? scaffold
|
||||
: {
|
||||
[labelField]: '新状态',
|
||||
body: {}
|
||||
}
|
||||
);
|
||||
this.setState({items}, () => {
|
||||
this.onChange();
|
||||
});
|
||||
}
|
||||
|
||||
handleValueChange(index: number, value: string) {
|
||||
const items = this.state.items.concat();
|
||||
items[index].value = value;
|
||||
this.setState({items}, () => this.onChange());
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const {render, label, labelRemark, useMobileUI, env, popOverContainer} =
|
||||
this.props;
|
||||
const classPrefix = env?.theme?.classPrefix;
|
||||
|
||||
return (
|
||||
<header className="ae-OptionControl-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 || env.getModalContainer
|
||||
})
|
||||
: null}
|
||||
</label>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
renderOption(props: any) {
|
||||
const {index, editing} = props;
|
||||
const {render, data: ctx, items = []} = this.props;
|
||||
const label = this.transformOptionValue(props[this.state.labelField]);
|
||||
|
||||
const editDom = editing ? (
|
||||
<div className="ae-OptionControlItem-extendMore">
|
||||
{render(
|
||||
'item',
|
||||
{
|
||||
type: 'form',
|
||||
title: null,
|
||||
className: 'ae-ExtendMore right mb-2 border-none',
|
||||
wrapWithPanel: false,
|
||||
labelAlign: 'left',
|
||||
horizontal: {
|
||||
left: 4,
|
||||
right: 8
|
||||
},
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
className: 'ae-OptionControlItem-closeBtn',
|
||||
label: '×',
|
||||
level: 'link',
|
||||
onClick: () => this.toggleEdit(index)
|
||||
},
|
||||
...items
|
||||
],
|
||||
onChange: (model: any) => {
|
||||
this.editItem(model, index);
|
||||
}
|
||||
},
|
||||
{data: createObject(ctx, props)}
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const operationBtn = [
|
||||
{
|
||||
type: 'button',
|
||||
className: 'ae-OptionControlItem-action',
|
||||
label: '编辑',
|
||||
onClick: () => this.toggleEdit(index)
|
||||
},
|
||||
{
|
||||
type: 'button',
|
||||
className: 'ae-OptionControlItem-action',
|
||||
label: '删除',
|
||||
onClick: () => this.handleDelete(index)
|
||||
}
|
||||
];
|
||||
|
||||
const labelField = this.state.labelField;
|
||||
|
||||
return (
|
||||
<li className="ae-OptionControlItem" key={index}>
|
||||
<div className="ae-OptionControlItem-Main">
|
||||
<a className="ae-OptionControlItem-dragBar">
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
{amisRender(
|
||||
{
|
||||
type: 'input-text',
|
||||
name: labelField,
|
||||
className: 'ae-OptionControlItem-input',
|
||||
value: label,
|
||||
placeholder: '状态名称',
|
||||
clearable: false,
|
||||
onChange: (value: string) => {
|
||||
this.handleEditLabel(index, value);
|
||||
}
|
||||
},
|
||||
{
|
||||
data: {
|
||||
[labelField]: label
|
||||
}
|
||||
}
|
||||
)}
|
||||
{render(
|
||||
'dropdown',
|
||||
{
|
||||
type: 'dropdown-button',
|
||||
className: 'ae-OptionControlItem-dropdown',
|
||||
btnClassName: 'px-2',
|
||||
icon: 'fa fa-ellipsis-h',
|
||||
hideCaret: true,
|
||||
closeOnClick: true,
|
||||
align: 'right',
|
||||
menuClassName: 'ae-OptionControlItem-ulmenu',
|
||||
buttons: operationBtn
|
||||
},
|
||||
{
|
||||
popOverContainer: null // amis 渲染挂载节点会使用 this.target
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{editDom}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {items} = this.state;
|
||||
const {className, addTip, placeholder} = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('ae-OptionControl', className)}>
|
||||
{this.renderHeader()}
|
||||
|
||||
<div className="ae-OptionControl-wrapper">
|
||||
{Array.isArray(items) && items.length ? (
|
||||
<ul className="ae-OptionControl-content" ref={this.dragRef}>
|
||||
{items.map((item, index) => this.renderOption({...item, index}))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="ae-OptionControl-placeholder">
|
||||
{placeholder || '无数据'}
|
||||
</div>
|
||||
)}
|
||||
<div className="ae-OptionControl-footer">
|
||||
<Button
|
||||
level="enhance"
|
||||
onClick={this.handleAdd}
|
||||
ref={this.targetRef}
|
||||
className="w-full"
|
||||
>
|
||||
{addTip || '添加选项'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({
|
||||
type: 'ae-listItemControl',
|
||||
renderLabel: false
|
||||
})
|
||||
export class ListItemControlRenderer extends ListItemControl {}
|
@ -206,5 +206,20 @@
|
||||
var(--listSelect-base-disabled-bottom-left-border-radius);
|
||||
background: var(--listSelect-base-disabled-bg-color);
|
||||
}
|
||||
|
||||
&.is-custom {
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,3 +136,77 @@ test('Renderer:listSelect with image option & listClassName', async () => {
|
||||
'items-wrapper'
|
||||
);
|
||||
});
|
||||
|
||||
test('Renderer:listSelect with custom list style', async () => {
|
||||
const {container, getByText} = render(
|
||||
amisRender({
|
||||
type: 'form',
|
||||
body: {
|
||||
type: 'list-select',
|
||||
name: 'select',
|
||||
label: '单选',
|
||||
listClassName: 'items-wrapper',
|
||||
value: 'b',
|
||||
options: [
|
||||
{
|
||||
label: 'OptionA',
|
||||
value: 'a',
|
||||
image:
|
||||
'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg'
|
||||
},
|
||||
{
|
||||
label: 'OptionB',
|
||||
value: 'b',
|
||||
image:
|
||||
'https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3893101144,2877209892&fm=23&gp=0.jpg'
|
||||
}
|
||||
],
|
||||
itemSchema: {
|
||||
type: 'container',
|
||||
className: 'item-default',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '${label}',
|
||||
inline: true,
|
||||
wrapperComponent: ''
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
},
|
||||
themeCss: {
|
||||
baseControlClassName: {
|
||||
'background:default': '#e06d6d'
|
||||
}
|
||||
}
|
||||
},
|
||||
activeItemSchema: {
|
||||
type: 'container',
|
||||
className: 'item-active',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '${label}',
|
||||
inline: true,
|
||||
wrapperComponent: ''
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
},
|
||||
themeCss: {
|
||||
baseControlClassName: {
|
||||
'background:default': '#38f9d4'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(container.querySelector('.item-default')).toBeInTheDocument();
|
||||
expect(container.querySelector('.item-active')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ import {CollapseSchema} from './renderers/Collapse';
|
||||
import {CollapseGroupSchema} from './renderers/CollapseGroup';
|
||||
import {ColorSchema} from './renderers/Color';
|
||||
import {ContainerSchema} from './renderers/Container';
|
||||
import {SwitchContainerSchema} from './renderers/SwitchContainer';
|
||||
import {CRUDSchema} from './renderers/CRUD';
|
||||
import {CRUD2Schema} from './renderers/CRUD2';
|
||||
import {DateSchema} from './renderers/Date';
|
||||
@ -238,6 +239,7 @@ export type SchemaType =
|
||||
| 'combo'
|
||||
| 'condition-builder'
|
||||
| 'container'
|
||||
| 'switch-container'
|
||||
| 'input-date'
|
||||
| 'input-datetime'
|
||||
| 'input-time'
|
||||
@ -380,6 +382,7 @@ export type SchemaObject =
|
||||
| CollapseGroupSchema
|
||||
| ColorSchema
|
||||
| ContainerSchema
|
||||
| SwitchContainerSchema
|
||||
| CRUDSchema
|
||||
| CRUD2Schema
|
||||
| DateSchema
|
||||
|
@ -121,6 +121,7 @@ import './renderers/Link';
|
||||
import './renderers/Wizard';
|
||||
import './renderers/Chart';
|
||||
import './renderers/Container';
|
||||
import './renderers/SwitchContainer';
|
||||
import './renderers/SearchBox';
|
||||
import './renderers/Service';
|
||||
import './renderers/SparkLine';
|
||||
|
@ -36,6 +36,11 @@ export interface ListControlSchema extends FormOptionsSchema {
|
||||
*/
|
||||
itemSchema?: SchemaCollection;
|
||||
|
||||
/**
|
||||
* 激活态自定义展示模板。
|
||||
*/
|
||||
activeItemSchema?: SchemaCollection;
|
||||
|
||||
/**
|
||||
* 支持配置 list div 的 css 类名。
|
||||
* 比如:flex justify-between
|
||||
@ -171,6 +176,7 @@ export default class ListControl extends React.Component<ListProps, any> {
|
||||
imageClassName,
|
||||
submitOnDBClick,
|
||||
itemSchema,
|
||||
activeItemSchema,
|
||||
data,
|
||||
labelField,
|
||||
listClassName,
|
||||
@ -187,7 +193,8 @@ export default class ListControl extends React.Component<ListProps, any> {
|
||||
key={key}
|
||||
className={cx(`ListControl-item`, itemClassName, {
|
||||
'is-active': ~selectedOptions.indexOf(option),
|
||||
'is-disabled': option.disabled || disabled
|
||||
'is-disabled': option.disabled || disabled,
|
||||
'is-custom': !!itemSchema
|
||||
})}
|
||||
onClick={this.handleClick.bind(this, option)}
|
||||
onDoubleClick={
|
||||
@ -197,9 +204,15 @@ export default class ListControl extends React.Component<ListProps, any> {
|
||||
}
|
||||
>
|
||||
{itemSchema
|
||||
? render(`${key}/body`, itemSchema, {
|
||||
data: createObject(data, option)
|
||||
})
|
||||
? render(
|
||||
`${key}/body`,
|
||||
~selectedOptions.indexOf(option)
|
||||
? activeItemSchema ?? itemSchema
|
||||
: itemSchema,
|
||||
{
|
||||
data: createObject(data, option)
|
||||
}
|
||||
)
|
||||
: option.body
|
||||
? render(`${key}/body`, option.body)
|
||||
: [
|
||||
|
187
packages/amis/src/renderers/SwitchContainer.tsx
Normal file
187
packages/amis/src/renderers/SwitchContainer.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Renderer,
|
||||
RendererProps,
|
||||
autobind,
|
||||
buildStyle,
|
||||
CustomStyle,
|
||||
isVisible,
|
||||
setThemeClassName
|
||||
} from 'amis-core';
|
||||
import {DndContainer as DndWrapper} from 'amis-ui';
|
||||
import {BaseSchema, SchemaCollection} from '../Schema';
|
||||
import {JSONSchema} from '../types';
|
||||
|
||||
export interface StateSchema extends Omit<BaseSchema, 'type'> {
|
||||
/**
|
||||
* 状态标题
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
body?: SchemaCollection;
|
||||
|
||||
/**
|
||||
* 显示条件
|
||||
*/
|
||||
visibleOn?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SwitchContainer 状态容器渲染器。
|
||||
* 文档:https://aisuda.bce.baidu.com/amis/zh-CN/components/state-container
|
||||
*/
|
||||
export interface SwitchContainerSchema extends BaseSchema {
|
||||
/**
|
||||
* 指定为 container 类型
|
||||
*/
|
||||
type: 'switch-container';
|
||||
|
||||
/**
|
||||
* 状态项列表
|
||||
*/
|
||||
items: Array<StateSchema>;
|
||||
|
||||
/**
|
||||
* 自定义样式
|
||||
*/
|
||||
style?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SwitchContainerProps
|
||||
extends RendererProps,
|
||||
Omit<SwitchContainerSchema, 'type' | 'className' | 'style'> {
|
||||
children?: (props: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface SwtichContainerState {
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
export default class SwitchContainer extends React.Component<
|
||||
SwitchContainerProps,
|
||||
SwtichContainerState
|
||||
> {
|
||||
static propsList: Array<string> = ['body', 'className'];
|
||||
static defaultProps = {
|
||||
className: ''
|
||||
};
|
||||
|
||||
constructor(props: SwitchContainerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeIndex: -1
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(preProps: SwitchContainerProps) {
|
||||
const items = this.props.items || [];
|
||||
if (this.state.activeIndex >= 0 && !items[this.state.activeIndex]) {
|
||||
this.setState({
|
||||
activeIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleClick(e: React.MouseEvent<any>) {
|
||||
const {dispatchEvent, data} = this.props;
|
||||
dispatchEvent(e, data);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleMouseEnter(e: React.MouseEvent<any>) {
|
||||
const {dispatchEvent, data} = this.props;
|
||||
dispatchEvent(e, data);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleMouseLeave(e: React.MouseEvent<any>) {
|
||||
const {dispatchEvent, data} = this.props;
|
||||
dispatchEvent(e, data);
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderBody(item: JSONSchema): JSX.Element | null {
|
||||
const {children, render, disabled} = this.props;
|
||||
const body = item?.body;
|
||||
|
||||
const containerBody = children
|
||||
? typeof children === 'function'
|
||||
? ((children as any)(this.props) as JSX.Element)
|
||||
: (children as any)
|
||||
: body
|
||||
? (render('body', body as any, {disabled}) as JSX.Element)
|
||||
: null;
|
||||
|
||||
return <div style={{display: 'inline'}}>{containerBody}</div>;
|
||||
}
|
||||
|
||||
@autobind
|
||||
switchTo(index: number) {
|
||||
const items = this.props.items || [];
|
||||
if (index >= 0 && index < items.length) {
|
||||
this.setState({activeIndex: index});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
items = [],
|
||||
classnames: cx,
|
||||
style,
|
||||
data,
|
||||
id,
|
||||
wrapperCustomStyle,
|
||||
env,
|
||||
themeCss
|
||||
} = this.props;
|
||||
|
||||
const activeItem =
|
||||
items[this.state.activeIndex] ??
|
||||
items.find((item: JSONSchema) => isVisible(item, data));
|
||||
|
||||
const contentDom = (
|
||||
<div
|
||||
className={cx(
|
||||
'SwitchContainer',
|
||||
className,
|
||||
setThemeClassName('baseControlClassName', id, themeCss),
|
||||
setThemeClassName('wrapperCustomStyle', id, wrapperCustomStyle)
|
||||
)}
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={buildStyle(style, data)}
|
||||
>
|
||||
{activeItem && this.renderBody(activeItem)}
|
||||
|
||||
<CustomStyle
|
||||
config={{
|
||||
wrapperCustomStyle,
|
||||
id,
|
||||
themeCss,
|
||||
classNames: [
|
||||
{
|
||||
key: 'baseControlClassName'
|
||||
}
|
||||
]
|
||||
}}
|
||||
env={env}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return contentDom;
|
||||
}
|
||||
}
|
||||
|
||||
@Renderer({
|
||||
type: 'switch-container'
|
||||
})
|
||||
export class SwitchContainerRenderer extends SwitchContainer {}
|
Loading…
Reference in New Issue
Block a user