feat: 表单支持flex布局 (#10286)

* feat: form-row-dnd

* form-dnd

* feat: form-flex-layout

* feat: form-flex-layout

* feat: 处理移动端独占一行的组件

* feat: 处理移动端独占一行的组件

* feat: 处理移动端独占一行的组件

* feat: 处理移动端独占一行的组件

* feat: 编辑器头部右侧提供容器

* fix: 修复移动端插入报错问题

* style: 优化form flex模式下移动端组件样式

* style: 优化flex dnd指示器样式

* chore: LabelAlign支持inherit值

* feat:  editor addElem 支持 nocode-form

* colSize

* 初始时获取第一个region

* form row 改为可选

* form 去掉 dndMode

* feat: 状态插件支持选择自定义表达式、校验插件新增清空方法、增加两种校验类型 (#10239)

* feat:基础插件优化、增加校验类型

* fix:去除默认options

---------

Co-authored-by: hezhihang <hezhihang@baidu.com>

* style: 优化flex dnd指示器样式

* fix: 修复标题位置继承错误问题

* fix: 修复colSize不实时渲染问题

* style: 优化零代码表单样式

* feat: xxxOn 支持自定义条件

* feat: xxxOn 支持自定义条件

* feat: xxxOn 支持自定义条件

* fix: 未验证的动态方法调用

* feat: 组件支持配置选中不高亮

* style: 修复input-number单个单位的样式

* fix: 导出校验相关函数 (#10263)

Co-authored-by: hezhihang <hezhihang@baidu.com>

* style: 表单组件不限制最大宽度

* feat: 支持配置面板内其他组件更新校验项时校验信息自动刷新 (#10279)

* fix(editor): 修复删除节点后未正确赋值父级region问题

* fix(editor): 修复拖拽后组件高亮异常问题

* fix(editor): 修复表单组件样式

* fix(editor): 修改文案

* 修复内置校验可关闭的问题 (#10281)

* fix: 内置校验一定是不可关闭的

* feat(editor): 编辑器支持配置mini toolbal模式,仅保留基础的菜单功能

* bugfix

* feat(editor): 编辑器可配置是否支持弹框

* 删除低码编辑器的表单flex模式

* bugfix

* fix: 优化状态中条件设置的判断逻辑 (#10294)

* fix: 导出校验相关函数

* fix: 优化条件设置逻辑

---------

Co-authored-by: hezhihang <hezhihang@baidu.com>

---------

Co-authored-by: qinhaoyan <30946345+qinhaoyan@users.noreply.github.com>
Co-authored-by: yupeng12 <yupeng12@baidu.com>
Co-authored-by: Allen <yupeng.fe@qq.com>
Co-authored-by: hzh11012 <43038692+hzh11012@users.noreply.github.com>
Co-authored-by: hezhihang <hezhihang@baidu.com>
Co-authored-by: hsm-lv <80095014+hsm-lv@users.noreply.github.com>
Co-authored-by: yangwei9012 <yangwei9012@163.com>
This commit is contained in:
qkiroc 2024-05-22 20:19:45 +08:00 committed by GitHub
parent 5c3d7bcffd
commit b427168b67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1859 additions and 248 deletions

View File

@ -1,6 +1,6 @@
import moment from 'moment';
import {
resolveCondition,
resolveConditionAsync,
guid,
registerConditionComputer,
setConditionComputeErrorHandler
@ -283,13 +283,13 @@ const conditions7 = {
};
test(`condition`, async () => {
expect(await resolveCondition(conditions1, data)).toBe(false);
expect(await resolveCondition(conditions2, data)).toBe(true);
expect(await resolveCondition(conditions3, data)).toBe(false);
expect(await resolveCondition(conditions4, data)).toBe(false);
expect(await resolveCondition(conditions5, data)).toBe(false);
expect(await resolveCondition(conditions6, data)).toBe(false);
expect(await resolveCondition(conditions7, data)).toBe(false);
expect(await resolveConditionAsync(conditions1, data)).toBe(false);
expect(await resolveConditionAsync(conditions2, data)).toBe(true);
expect(await resolveConditionAsync(conditions3, data)).toBe(false);
expect(await resolveConditionAsync(conditions4, data)).toBe(false);
expect(await resolveConditionAsync(conditions5, data)).toBe(false);
expect(await resolveConditionAsync(conditions6, data)).toBe(false);
expect(await resolveConditionAsync(conditions7, data)).toBe(false);
});
test(`condition date`, async () => {
@ -381,7 +381,7 @@ test(`condition date`, async () => {
]
};
expect(await resolveCondition(conditions, data)).toBe(true);
expect(await resolveConditionAsync(conditions, data)).toBe(true);
});
test(`condition tree`, async () => {
@ -466,9 +466,9 @@ test(`condition tree`, async () => {
]
};
expect(await resolveCondition(conditions8, data)).toBe(false);
expect(await resolveCondition(conditions9, data)).toBe(true);
expect(await resolveCondition(conditions10, data)).toBe(true);
expect(await resolveConditionAsync(conditions8, data)).toBe(false);
expect(await resolveConditionAsync(conditions9, data)).toBe(true);
expect(await resolveConditionAsync(conditions10, data)).toBe(true);
});
test(`condition register`, async () => {
@ -507,7 +507,7 @@ test(`condition register`, async () => {
]
};
expect(await resolveCondition(conditions, data)).toBe(true);
expect(await resolveConditionAsync(conditions, data)).toBe(true);
});
test(`condition conditionComputeHander`, async () => {
@ -543,5 +543,5 @@ test(`condition conditionComputeHander`, async () => {
]
};
expect(await resolveCondition(conditions, data)).toBe(true);
expect(await resolveConditionAsync(conditions, data)).toBe(true);
});

View File

@ -3,7 +3,7 @@ import {RendererProps} from '../factory';
import {ConditionGroupValue, Api, SchemaNode} from '../types';
import {createObject} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpressionWithConditionBuilder} from '../utils/tpl';
import {evalExpressionWithConditionBuilderAsync} from '../utils/tpl';
import {dataMapping} from '../utils/tpl-builtin';
import {IBreakAction} from './BreakAction';
import {IContinueAction} from './ContinueAction';
@ -247,7 +247,7 @@ export const runAction = async (
let isStop = false;
if (expression) {
isStop = !(await evalExpressionWithConditionBuilder(
isStop = !(await evalExpressionWithConditionBuilderAsync(
expression,
mergeData,
true
@ -261,7 +261,7 @@ export const runAction = async (
// 支持表达式 >=1.10.0
let preventDefault = false;
if (action.preventDefault) {
preventDefault = await evalExpressionWithConditionBuilder(
preventDefault = await evalExpressionWithConditionBuilderAsync(
action.preventDefault,
mergeData,
false
@ -362,7 +362,7 @@ export const runAction = async (
let stopPropagation = false;
if (action.stopPropagation) {
stopPropagation = await evalExpressionWithConditionBuilder(
stopPropagation = await evalExpressionWithConditionBuilderAsync(
action.stopPropagation,
mergeData,
false

View File

@ -3,7 +3,7 @@ import {normalizeApi, normalizeApiResponseData} from '../utils/api';
import {ServerError} from '../utils/errors';
import {createObject, isEmpty} from '../utils/helper';
import {RendererEvent} from '../utils/renderer-event';
import {evalExpressionWithConditionBuilder} from '../utils/tpl';
import {evalExpressionWithConditionBuilderAsync} from '../utils/tpl';
import {
RendererAction,
ListenerAction,
@ -61,7 +61,7 @@ export class AjaxAction implements RendererAction {
if (api.sendOn !== undefined) {
// 发送请求前,判断是否需要发送
const sendOn = await evalExpressionWithConditionBuilder(
const sendOn = await evalExpressionWithConditionBuilderAsync(
api.sendOn,
action.data ?? {},
false

View File

@ -1,5 +1,5 @@
import {RendererEvent} from '../utils/renderer-event';
import {evalExpressionWithConditionBuilder} from '../utils/tpl';
import {evalExpressionWithConditionBuilderAsync} from '../utils/tpl';
import {
RendererAction,
ListenerContext,
@ -27,7 +27,7 @@ export class SwitchAction implements RendererAction {
continue;
}
const isPass = await evalExpressionWithConditionBuilder(
const isPass = await evalExpressionWithConditionBuilderAsync(
branch.expression,
mergeData
);

View File

@ -232,7 +232,7 @@ export interface FormSchemaBase {
/**
*
*/
mode?: 'normal' | 'inline' | 'horizontal';
mode?: 'normal' | 'inline' | 'horizontal' | 'flex';
/**
*
@ -1735,6 +1735,7 @@ export default class Form extends React.Component<FormProps, object> {
otherProps: Partial<FormProps> = {}
): React.ReactNode {
children = children || [];
const {classnames: cx} = this.props;
if (!Array.isArray(children)) {
children = [children];
@ -1765,8 +1766,6 @@ export default class Form extends React.Component<FormProps, object> {
return null;
}
const {classnames: cx} = this.props;
return (
<div className={cx('Form-row')}>
{children.map((control, key) =>
@ -1789,6 +1788,63 @@ export default class Form extends React.Component<FormProps, object> {
);
}
if (this.props.mode === 'flex') {
let rows: any = [];
children.forEach(child => {
if (typeof child.row === 'number') {
if (rows[child.row]) {
rows[child.row].push(child);
} else {
rows[child.row] = [child];
}
} else {
// 没有 row 的,就单启一行
rows.push([child]);
}
});
return (
<>
{rows.map((children: any, index: number) => {
return (
<div className={cx('Form-flex')} role="flex-row" key={index}>
{children.map((control: any, key: number) => {
const split = control.colSize?.split('/');
const colSize =
split?.[0] && split?.[1]
? (split[0] / split[1]) * 100 + '%'
: control.colSize;
return ~['hidden', 'formula'].indexOf(
(control as any).type
) ? (
this.renderChild(control, key, otherProps)
) : (
<div
key={control.id || key}
className={cx(
`Form-flex-col`,
(control as Schema).columnClassName
)}
style={{
flex:
colSize && !['1', 'auto'].includes(colSize)
? `0 0 ${colSize}`
: ''
}}
role="flex-col"
>
{this.renderChild(control, '', {
...otherProps,
mode: 'flex'
})}
</div>
);
})}
</div>
);
})}
</>
);
}
return children.map((control, key) =>
this.renderChild(control, key, otherProps, region)
);
@ -1841,7 +1897,10 @@ export default class Form extends React.Component<FormProps, object> {
formSubmited: form.submited,
formMode: mode,
formHorizontal: horizontal,
formLabelAlign: labelAlign !== 'left' ? 'right' : labelAlign,
formLabelAlign:
!labelAlign || !['left', 'right', 'top'].includes(labelAlign)
? 'right'
: labelAlign,
formLabelWidth: labelWidth,
controlWidth,
/**

View File

@ -48,7 +48,7 @@ import CustomStyle from '../components/CustomStyle';
import classNames from 'classnames';
import isPlainObject from 'lodash/isPlainObject';
export type LabelAlign = 'right' | 'left';
export type LabelAlign = 'right' | 'left' | 'top' | 'inherit';
export interface FormBaseControl extends BaseSchemaWithoutType {
/**
@ -461,6 +461,8 @@ export interface FormBaseControl extends BaseSchemaWithoutType {
*
*/
initAutoFill?: boolean | 'fillIfNotSet';
row?: number; // flex模式下指定所在的行数
}
export interface FormItemBasicConfig extends Partial<RendererConfig> {
@ -1699,9 +1701,8 @@ export class FormItemWrap extends React.Component<FormItemProps> {
themeCss,
id
} = props;
const labelWidth = props.labelWidth || props.formLabelWidth;
description = description || desc;
const labelWidth = props.labelWidth || props.formLabelWidth;
return (
<div
data-role="form-item"
@ -1785,6 +1786,154 @@ export class FormItemWrap extends React.Component<FormItemProps> {
</ul>
) : null}
{description && renderDescription !== false
? render('description', description, {
className: cx(
`Form-description`,
descriptionClassName,
setThemeClassName({
...props,
name: 'descriptionClassName',
id,
themeCss,
extra: 'item'
})
)
})
: null}
</div>
);
},
flex: (props: FormItemProps, renderControl: () => JSX.Element) => {
let {
className,
style,
classnames: cx,
desc,
description,
label,
render,
required,
caption,
remark,
labelRemark,
env,
descriptionClassName,
captionClassName,
formItem: model,
renderLabel,
renderDescription,
hint,
data,
showErrorMsg,
mobileUI,
translate: __,
static: isStatic,
staticClassName,
wrapperCustomStyle,
themeCss,
id
} = props;
let labelAlign =
(props.labelAlign !== 'inherit' && props.labelAlign) ||
props.formLabelAlign;
const labelWidth = props.labelWidth || props.formLabelWidth;
description = description || desc;
return (
<div
data-role="form-item"
className={cx(
`Form-item Form-item--flex`,
isStatic && staticClassName ? staticClassName : className,
{
'is-error': model && !model.valid,
[`is-required`]: required
},
model?.errClassNames,
setThemeClassName({
...props,
name: 'wrapperCustomStyle',
id,
themeCss: wrapperCustomStyle,
extra: 'item'
})
)}
style={style}
>
<div
className={cx(
'Form-flexInner',
labelAlign && `Form-flexInner--label-${labelAlign}`
)}
>
{label && renderLabel !== false ? (
<label
className={cx(`Form-label`, getItemLabelClassName(props))}
style={
labelWidth != null
? {width: labelAlign === 'top' ? '100%' : labelWidth}
: undefined
}
>
<span>
{render('label', label)}
{required && (label || labelRemark) ? (
<span className={cx(`Form-star`)}>*</span>
) : null}
{labelRemark
? render('label-remark', {
type: 'remark',
icon: labelRemark.icon || 'warning-mark',
tooltip: labelRemark,
className: cx(`Form-lableRemark`),
mobileUI,
container:
props.popOverContainer || env.getModalContainer
})
: null}
</span>
</label>
) : null}
{renderControl()}
{caption
? render('caption', caption, {
className: cx(`Form-caption`, captionClassName)
})
: null}
{remark
? render('remark', {
type: 'remark',
icon: remark.icon || 'warning-mark',
className: cx(`Form-remark`),
tooltip: remark,
container: props.popOverContainer || env.getModalContainer
})
: null}
</div>
{hint && model && model.isFocused
? render('hint', hint, {
className: cx(`Form-hint`)
})
: null}
{model &&
!model.valid &&
showErrorMsg !== false &&
Array.isArray(model.errors) ? (
<ul className={cx('Form-feedback')}>
{model.errors.map((msg: string, key: number) => (
<li key={key}>{msg}</li>
))}
</ul>
) : null}
{description && renderDescription !== false
? render('description', description, {
className: cx(
@ -1940,7 +2089,12 @@ export const detectProps = [
'displayMode',
'revealPassword',
'loading',
'themeCss'
'themeCss',
'formLabelAlign',
'formLabelWidth',
'formHorizontal',
'labelAlign',
'colSize'
];
export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
@ -2060,7 +2214,10 @@ export function asFormItem(config: Omit<FormItemConfig, 'component'>) {
...rest
} = this.props;
const controlSize = size || defaultSize;
const controlSize =
size && ['xs', 'sm', 'md', 'lg', 'full'].includes(size)
? size
: defaultSize;
//@ts-ignore
const isOpened = this.state.isOpened;

View File

@ -1,4 +1,8 @@
import {evalExpression, filter} from './tpl';
import {
evalExpression,
evalExpressionWithConditionBuilder,
filter
} from './tpl';
import {PlainObject} from '../types';
import {injectPropsToObject, mapObject} from './helper';
import isPlainObject from 'lodash/isPlainObject';
@ -62,7 +66,8 @@ export function getExprProperties(
if (
value &&
typeof value === 'string' &&
(typeof value === 'string' ||
Object.prototype.toString.call(value) === '[object Object]') &&
parts?.[1] &&
(type === 'On' || type === 'Expr')
) {
@ -81,7 +86,9 @@ export function getExprProperties(
}
if (type === 'On') {
value = props?.[key] || evalExpression(value, ctx || data);
value =
props?.[key] ||
evalExpressionWithConditionBuilder(value, ctx || data);
} else {
value = filter(value, ctx || data);
}

View File

@ -2,7 +2,11 @@ import {Options} from '../types';
import isPlainObject from 'lodash/isPlainObject';
export function normalizeOptions(
options: string | {[propName: string]: string} | Array<string> | Options,
options:
| string
| {[propName: string]: string}
| Array<string | number>
| Options,
share: {
values: Array<any>;
options: Array<any>;
@ -32,8 +36,9 @@ export function normalizeOptions(
return option;
});
} else if (
Array.isArray(options as Array<string>) &&
typeof (options as Array<string>)[0] === 'string'
Array.isArray(options as Array<string | number>) &&
(typeof (options as Array<string | number>)[0] === 'string' ||
typeof (options as Array<string | number>)[0] === 'number')
) {
return (options as Array<string>).map(item => {
const idx = share.values.indexOf(item);

View File

@ -6,7 +6,7 @@ import {TreeItem, eachTree, getTree} from './helper';
import {createObject, extendObject} from './object';
import debounce from 'lodash/debounce';
import {resolveVariableAndFilterForAsync} from './resolveVariableAndFilterForAsync';
import {evalExpression, evalExpressionWithConditionBuilder} from './tpl';
import {evalExpression, evalExpressionWithConditionBuilderAsync} from './tpl';
export interface debounceConfig {
maxWait?: number;
@ -379,7 +379,7 @@ export async function getMatchedEventTargets<T extends TreeItem>(
eachTree(tree, item => {
const data = item.storeType ? item.data : item;
promies.push(async () => {
const result = await evalExpressionWithConditionBuilder(
const result = await evalExpressionWithConditionBuilderAsync(
condition,
createObject(ctx, data)
);

View File

@ -6,6 +6,7 @@ import startsWith from 'lodash/startsWith';
import {resolveVariableAndFilterForAsync} from './resolveVariableAndFilterForAsync';
import moment from 'moment';
import capitalize from 'lodash/capitalize';
import {isPureVariable, resolveVariableAndFilter} from './tpl-builtin';
const conditionResolverMap: {
[op: string]: (left: any, right: any, fieldType?: string) => boolean;
@ -18,7 +19,7 @@ let conditionComputeErrorHandler: (
defaultResult: boolean
) => boolean | Promise<boolean>;
export async function resolveCondition(
export async function resolveConditionAsync(
conditions: any,
data: any,
defaultResult: boolean = true
@ -33,7 +34,7 @@ export async function resolveCondition(
}
try {
return await computeConditions(
return await computeConditionsAsync(
conditions.children,
conditions.conjunction,
data
@ -51,7 +52,36 @@ export async function resolveCondition(
}
}
async function computeConditions(
export function resolveCondition(
conditions: any,
data: any,
defaultResult: boolean = true
) {
if (
!conditions ||
!conditions.conjunction ||
!Array.isArray(conditions.children) ||
!conditions.children.length
) {
return defaultResult;
}
try {
return computeConditions(conditions.children, conditions.conjunction, data);
} catch (e) {
// 如果函数未定义则交给handler
if (e.name === 'FormulaEvalError') {
return conditionComputeErrorHandler?.(
conditions.children,
conditions.conjunction,
data
);
}
return defaultResult;
}
}
async function computeConditionsAsync(
conditions: any[],
conjunction: 'or' | 'and' = 'and',
data: any
@ -61,8 +91,8 @@ async function computeConditions(
const item = conditions[index];
const result =
item.conjunction && Array.isArray(item.children) && item.children.length
? await computeConditions(item.children, item.conjunction, data)
: await computeCondition(item, index, data);
? await computeConditionsAsync(item.children, item.conjunction, data)
: await computeConditionAsync(item, index, data);
computeResult = !!result;
@ -76,7 +106,32 @@ async function computeConditions(
return computeResult;
}
async function computeCondition(
function computeConditions(
conditions: any[],
conjunction: 'or' | 'and' = 'and',
data: any
): boolean {
let computeResult = true;
for (let index = 0, len = conditions.length; index < len; index++) {
const item = conditions[index];
const result =
item.conjunction && Array.isArray(item.children) && item.children.length
? computeConditions(item.children, item.conjunction, data)
: computeCondition(item, index, data);
computeResult = !!result;
if (
(result && conjunction === 'or') ||
(!result && conjunction === 'and')
) {
break;
}
}
return computeResult;
}
async function computeConditionAsync(
rule: {
op: string;
left: {
@ -104,6 +159,32 @@ async function computeCondition(
return func ? func(leftValue, rightValue, rule.left.type) : DEFAULT_RESULT;
}
function computeCondition(
rule: {
op: string;
left: {
type: string;
field: string;
};
right: any;
},
index: number,
data: any
) {
const leftValue = get(data, rule.left.field);
const rightValue: any = isPureVariable(rule.right)
? resolveVariableAndFilter(rule.right, data)
: rule.right;
const func =
conditionResolverMap[`${rule.op}For${capitalize(rule.left.type)}`] ??
conditionResolverMap[rule.op];
return func && typeof func === 'function'
? func(leftValue, rightValue, rule.left.type)
: DEFAULT_RESULT;
}
function startsWithFunc(left: any, right: any) {
if (left === undefined || right === undefined) {
return DEFAULT_RESULT;

View File

@ -1,7 +1,7 @@
import {register as registerBulitin, getFilters} from './tpl-builtin';
import {register as registerLodash} from './tpl-lodash';
import {parse, evaluate} from 'amis-formula';
import {resolveCondition} from './resolveCondition';
import {resolveCondition, resolveConditionAsync} from './resolveCondition';
import {memoParse} from './tokenize';
export interface Enginer {
@ -133,14 +133,33 @@ export function evalExpression(expression: string, data?: object): boolean {
* @param data
* @returns
*/
export async function evalExpressionWithConditionBuilder(
export async function evalExpressionWithConditionBuilderAsync(
expression: any,
data?: object,
defaultResult?: boolean
): Promise<boolean> {
// 支持ConditionBuilder
if (Object.prototype.toString.call(expression) === '[object Object]') {
return await resolveCondition(expression, data, defaultResult);
return await resolveConditionAsync(expression, data, defaultResult);
}
return evalExpression(String(expression), data);
}
/**
* condition-builder
* @param expression or condition-builder对象
* @param data
* @returns
*/
export function evalExpressionWithConditionBuilder(
expression: any,
data?: object,
defaultResult?: boolean
) {
// 支持ConditionBuilder
if (Object.prototype.toString.call(expression) === '[object Object]') {
return resolveCondition(expression, data, defaultResult);
}
return evalExpression(String(expression), data);

View File

@ -4,12 +4,13 @@
background: #fff;
box-sizing: border-box;
border-bottom: 1px solid $editor-border-color;
z-index: 1000;
padding: 0 16px;
.ae-Breadcrumb-content {
left: 0;
height: 100%;
width: max-content;
padding: 0 16px;
white-space: nowrap;
height: 22px;

View File

@ -80,6 +80,7 @@
min-height: 450px;
min-width: 980px;
overflow: hidden;
user-select: none;
// 覆盖amis默认top值避免导致未垂直居中
.ae-Editor-toolbar svg.icon {
@ -1199,7 +1200,7 @@
[data-editor-id].ae-is-draging,
.ae-is-draging {
display: none !important;
opacity: 0.6;
}
[data-editor-id][data-visible='false'] {
@ -1532,6 +1533,34 @@ div.ae-DragImage {
}
}
.ae-PushHighlight-top,
.ae-PushHighlight-bottom {
position: absolute;
&::after {
content: '';
left: 0;
right: 0;
display: block;
background: $Editor-theme-color;
position: absolute;
height: 2px;
}
}
.ae-PushHighlight-left,
.ae-PushHighlight-right {
position: absolute;
&::after {
content: '';
top: 0;
bottom: 0;
display: block;
background: $Editor-theme-color;
position: absolute;
width: 2px;
}
}
.ae-DragGhost {
margin-bottom: 12px;
@ -1872,3 +1901,19 @@ div.ae-DragImage {
div[class*='Form-group']:empty {
margin-bottom: 0 !important;
}
.ae-Header {
display: flex;
justify-content: space-between;
background: #fff;
align-items: center;
border-bottom: 1px solid $editor-border-color;
.ae-Breadcrumb {
flex: 1;
max-width: 100%;
min-width: 0;
}
&-Right-Container {
z-index: 1001;
background-color: #fff;
}
}

View File

@ -152,7 +152,8 @@ export default class Breadcrumb extends React.Component<
}
const scrollLeft = this.toNumber(this.getScrollLeft());
const maxScrollLeft = scrollElem.offsetWidth - scrollContainer.offsetWidth;
const maxScrollLeft =
scrollElem.offsetWidth - scrollContainer.offsetWidth + 32;
if (scrollLeft - 50 > -maxScrollLeft) {
scrollElem.style.left = `${scrollLeft - 50}px`;

View File

@ -31,6 +31,10 @@ export interface EditorProps extends PluginEventListener {
$schemaUrl?: string;
schemas?: Array<any>;
theme?: string;
/** 工具栏模式 */
toolbarMode?: 'default' | 'mini';
/** 是否需要弹框 */
noDialog?: boolean;
/** 应用语言类型 */
appLocale?: string;
/** 是否开启多语言 */
@ -166,6 +170,8 @@ export default class Editor extends Component<EditorProps> {
{
isMobile: props.isMobile,
theme: props.theme,
toolbarMode: props.toolbarMode || 'default',
noDialog: props.noDialog,
isSubEditor,
amisDocHost: props.amisDocHost,
superEditorData,
@ -592,7 +598,13 @@ export default class Editor extends Component<EditorProps> {
<div className="ae-Main">
{!preview && (
<div className="ae-Header">
<Breadcrumb store={this.store} manager={this.manager} />
<div
id="aeHeaderRightContainer"
className="ae-Header-Right-Container"
></div>
</div>
)}
<Preview
{...previewProps}

View File

@ -238,7 +238,8 @@ export default observer(function ({
)}
data-hlbox-id={id}
style={{
display: node.w && node.h ? 'block' : 'none',
display:
node.w && node.h && !node.info.plugin.notHighlight ? 'block' : 'none',
top: node.y,
left: node.x,
width: node.w,

View File

@ -334,7 +334,7 @@ export class OutlinePanel extends React.Component<PanelProps> {
)}
</div>
</Tab>
{store.isSubEditor ? null : (
{store.isSubEditor || store.noDialog ? null : (
<Tab
className={'ae-outline-tabs-panel'}
key={'dialog-outline'}

View File

@ -635,8 +635,12 @@ class SmartPreview extends React.Component<SmartPreviewProps> {
store.outline,
item => !item.isRegion && item.clickable
);
first && isAlive(store) && store.setActiveId(first.id);
if (first && isAlive(store)) {
const region = first.childRegions.find(
(i: any) => i.region
)?.region;
store.setActiveId(first.id, region);
}
}
}, 350);
} else {

View File

@ -0,0 +1,285 @@
import {isMobile} from 'amis-core';
/**
*
*/
import findIndex from 'lodash/findIndex';
import {EditorDNDManager} from '.';
import {renderThumbToGhost} from '../component/factory';
import {EditorNodeType} from '../store/node';
import {translateSchema} from '../util';
import {DNDModeInterface} from './interface';
import findLastIndex from 'lodash/findLastIndex';
import find from 'lodash/find';
const className = 'PushHighlight';
export class FlexDNDMode implements DNDModeInterface {
readonly dndContainer: HTMLElement; // 记录当前拖拽区域
dropBeforeId?: string;
position?: 'top' | 'bottom' | 'left' | 'right';
maxRolLength = 4;
dragNode?: any;
dragId: string;
store: any;
constructor(
readonly dnd: EditorDNDManager,
readonly region: EditorNodeType,
config: any
) {
// 初始化时,默认将元素所在区域设置为当前拖拽区域
this.dndContainer = this.dnd.store
.getDoc()
.querySelector(
`[data-region="${region.region}"][data-region-host="${region.id}"]`
) as HTMLElement;
this.maxRolLength = config.regionNode.maxRolLength || 4;
}
/**
* ghost
* @param e
* @param ghost
*/
enter(e: DragEvent, ghost: HTMLElement) {
const dragEl = this.dnd.dragElement;
const list = Array.isArray(this.region.schema) ? this.region.schema : [];
const manager = this.dnd.manager;
this.store = manager.store;
// 如果区域里面没有元素ghost就渲染为真实的表单元素
if (list.length === 0) {
if (dragEl && dragEl.closest('[data-region]') === this.dndContainer) {
const child = this.getChild(this.dndContainer, dragEl);
this.dndContainer.insertBefore(ghost, child);
let innerHTML = dragEl.outerHTML
.replace('ae-is-draging', '')
.replace(/\bdata\-editor\-id=(?:'.+?'|".+?")/g, '');
ghost.innerHTML = innerHTML;
} else {
renderThumbToGhost(
ghost,
this.region,
translateSchema(this.store.dragSchema),
manager
);
this.dndContainer.appendChild(ghost);
}
} else {
ghost.innerHTML = '';
// 直接插入 ghostover的时候再去调整样式
this.dndContainer.appendChild(ghost);
}
this.dragId = this.store.dragId;
this.dragNode =
find(list, (item: any) => item.$$id === this.dragId) ||
this.store.dragSchema;
}
/**
* ghost
* @param e
* @param ghost
*/
leave(e: DragEvent, ghost: HTMLElement) {
this.dndContainer.removeChild(ghost);
this.clearGhostStyle(ghost);
}
over(e: DragEvent, ghost: HTMLElement) {
const {isMobile} = this.store;
const colTarget = (e.target as HTMLElement).closest('[role="flex-col"]');
const wrapper = this.dndContainer;
const elemSchema = this.region.schema;
const {x: wx, y: wy} = wrapper.getBoundingClientRect();
const list: Array<any> = Array.isArray(elemSchema) ? elemSchema : [];
this.clearGhostStyle(ghost);
if (colTarget && list.length) {
const {width, height, x, y} = colTarget.getBoundingClientRect();
const cx = e.clientX;
const cy = e.clientY;
const w = width / 8;
const h = height / 2;
const target = this.getTarget(colTarget);
const targetId = target.getAttribute('data-editor-id')!;
const targetIndex = findIndex(
list,
(item: any) => item.$$id === targetId
);
const targetRow = list[targetIndex].row;
const targetRowLen = list.filter(
(item: any) => item.row === targetRow
).length;
// 是否可以插入到左右
const canRL =
this.dragId !== targetId && // 拖拽和目标不能是同一个元素,才能插入到左右
this.dragNode?.$$dragMode !== 'hv' &&
list[targetIndex]?.$$dragMode !== 'hv' && // 如果拖拽元素和目标元素的拖拽模式不能是垂直,才能插入到左右
(targetRowLen < this.maxRolLength ||
this.dragNode?.row === targetRow) && // 如果当前行的元素个数小于最大行长度,或者拖拽的元素就在当前行,才能插入到当前行
!isMobile; // 移动端不支持左右拖拽
if (cx < x + w && canRL) {
ghost.classList.add(`ae-${className}-left`);
ghost.style.left = x - wx + 'px';
ghost.style.top = y - wy + 'px';
ghost.style.height = height + 'px';
this.dropBeforeId = targetId;
this.position = 'left';
} else if (cx > x + 7 * w && canRL) {
ghost.classList.add(`ae-${className}-right`);
ghost.style.left = x - wx + width + 'px';
ghost.style.top = y - wy + 'px';
ghost.style.height = height + 'px';
this.dropBeforeId =
list[
list[targetIndex + 1]?.$$id === this.dragId
? targetIndex + 2
: targetIndex + 1
]?.$$id;
this.position = 'right';
} else if (cy < y + h) {
// 移动端,独占一行的元素不能插入到一行的中间
if (
this.store.isMobile &&
(this.dragNode?.$$dragMode !== 'hv' ||
list[targetIndex]?.$$dragMode !== 'hv') &&
list[targetIndex].row === list[targetIndex - 1]?.row
) {
delete this.position;
delete this.dropBeforeId;
return;
}
ghost.classList.add(`ae-${className}-top`);
ghost.style.width = '100%';
ghost.style.top = y - wy + 'px';
if (this.store.isMobile) {
this.dropBeforeId = targetId;
} else {
const beforeIndex = findIndex(
list,
(item: any) => item.row === targetRow
);
const index =
list[beforeIndex]?.$$id === this.dragId
? beforeIndex + 1
: beforeIndex;
this.dropBeforeId = list[index]?.$$id;
}
this.position = 'top';
} else {
// 移动端,独占一行的元素不能插入到一行的中间
if (
this.store.isMobile &&
(this.dragNode?.$$dragMode !== 'hv' ||
list[targetIndex]?.$$dragMode !== 'hv') &&
list[targetIndex].row === list[targetIndex + 1]?.row
) {
delete this.position;
delete this.dropBeforeId;
return;
}
ghost.classList.add(`ae-${className}-bottom`);
ghost.style.width = '100%';
ghost.style.top = y - wy + height + 'px';
if (this.store.isMobile) {
this.dropBeforeId = list[targetIndex + 1]?.$$id;
} else {
const lastIndex = findLastIndex(
list,
(item: any) => item.row === targetRow
);
const index =
list[lastIndex + 1]?.$$id === this.dragId
? lastIndex + 2
: lastIndex + 1;
this.dropBeforeId = list[index]?.$$id;
}
this.position = 'bottom';
}
} else {
this.dropBeforeId = undefined;
if (list.length) {
const rows = wrapper.querySelectorAll('[role="flex-row"]');
const lastRow = rows[rows.length - 1];
const {y, height} = lastRow.getBoundingClientRect();
ghost.classList.add(`ae-${className}-bottom`);
ghost.style.width = '100%';
ghost.style.top = y - wy + height + 'px';
}
this.position = 'bottom';
}
}
clearGhostStyle(ghost: HTMLElement) {
// 清除ghost的样式
ghost.style.left = '';
ghost.style.top = '';
ghost.style.right = '';
ghost.style.bottom = '';
ghost.style.width = '';
ghost.style.height = '';
ghost.classList.remove(`ae-${className}-left`);
ghost.classList.remove(`ae-${className}-right`);
ghost.classList.remove(`ae-${className}-top`);
ghost.classList.remove(`ae-${className}-bottom`);
}
/**
*
*/
getDropBeforeId() {
return this.dropBeforeId;
}
/**
*
*/
getTarget(col: Element | null) {
let target = col?.querySelector('[data-editor-id]') as HTMLElement;
return target;
}
/**
*
* ghost insertBefore
* @param dom
* @param descend
*/
getChild(dom: HTMLElement, descend: HTMLElement) {
let child = descend;
while (child) {
if (child.parentElement === dom) {
break;
}
child = child.parentElement!;
}
return child;
}
/**
*
*/
dispose() {
delete this.dropBeforeId;
delete this.position;
}
getDropPosition() {
return this.position;
}
// 是否中断 drop 事件
interruptionDrop() {
// 如果没有 dropBeforeId 和 position说明没有拖拽到任何元素上中断 drop 事件
if (!this.dropBeforeId && !this.position) {
return true;
}
return false;
}
}

View File

@ -9,10 +9,16 @@ import {EditorManager} from '../manager';
import {DragEventContext, SubRendererInfo} from '../plugin';
import {EditorStoreType} from '../store/editor';
import {EditorNodeType} from '../store/node';
import {autobind, reactionWithOldValue, unitFormula} from '../util';
import {
JSONGetById,
autobind,
reactionWithOldValue,
unitFormula
} from '../util';
import {DefaultDNDMode} from './default';
import {DNDModeInterface} from './interface';
import {PositionHDNDMode} from './position-h';
import {FlexDNDMode} from './flex';
const toastWarning = debounce(msg => {
toast.warning(msg);
@ -175,15 +181,23 @@ export class EditorDNDManager {
return this.dndMode || null;
}
const mode = region.regionInfo?.dndMode;
const regionNode = JSONGetById(this.store.schema, region.id);
let Klass: new (
dnd: EditorDNDManager,
region: EditorNodeType
region: EditorNodeType,
config: any
) => DNDModeInterface = DefaultDNDMode; // todo 根据配置自动实例化不同的。
if (mode === 'position-h') {
Klass = PositionHDNDMode;
}
this.dndMode = new Klass(this, region);
if (typeof mode === 'function') {
if (mode(regionNode) === 'flex') {
Klass = FlexDNDMode;
}
}
this.dndMode = new Klass(this, region, {regionNode});
return this.dndMode;
}
@ -475,9 +489,20 @@ export class EditorDNDManager {
}
const beforeId = this.dndMode?.getDropBeforeId();
const position = this.dndMode?.getDropPosition?.();
// 如果中断 drop 事件,则直接返回
if (this.dndMode?.interruptionDrop?.()) {
return;
}
if (store.dragMode === 'move') {
this.manager.move(store.dropId, store.dropRegion, store.dragId, beforeId);
this.manager.move(
store.dropId,
store.dropRegion,
store.dragId,
beforeId,
{position}
);
} else if (store.dragMode === 'copy') {
let schema = store.dragSchema;
const dropId = store.dropId;
@ -494,11 +519,20 @@ export class EditorDNDManager {
}
}
this.manager.addChild(dropId, dropRegion, schema, beforeId, subRenderer, {
this.manager.addChild(
dropId,
dropRegion,
schema,
beforeId,
subRenderer,
{
id: store.dragId,
type: store.dragType,
data: store.dragSchema
});
data: store.dragSchema,
position: position
},
false
);
}
}

View File

@ -1,3 +1,8 @@
import {
DeleteEventContext,
InsertEventContext,
MoveEventContext
} from '../plugin';
import {EditorNodeType} from '../store/node';
import {EditorDNDManager} from './index';
@ -17,4 +22,8 @@ export interface DNDModeInterface {
getDropBeforeId: () => string | undefined;
dispose: () => void;
getDropPosition?: () => 'top' | 'bottom' | 'left' | 'right' | undefined;
interruptionDrop?: () => boolean; // 是否中断 drop 事件
}

View File

@ -0,0 +1,3 @@
import {LayoutInterface} from './interface';
export default class DefaultLayout implements LayoutInterface {}

View File

@ -0,0 +1,274 @@
import findLastIndex from 'lodash/findLastIndex';
import {
BaseEventContext,
InsertEventContext,
MoveEventContext
} from '../plugin';
import {LayoutInterface} from './interface';
import {setDefaultColSize} from '../util';
export default class FlexLayout implements LayoutInterface {
beforeInsert(context: InsertEventContext, store: any) {
const region = context.region;
const body = [...(context.schema?.[region] || [])];
let row = 0;
if (body?.length) {
const beforeId = context.beforeId;
const beforeNodeIndex = body.findIndex(
(item: any) => item.$$id === beforeId
);
const beforeNode = body[beforeNodeIndex] || body[body.length - 1];
const beforeRow = beforeNode?.row;
const position = context.dragInfo?.position || 'bottom';
row = beforeRow; // left、bottom、top使用beforeRowbottom、top后续行需要加1
if (position === 'right') {
const preNode = body[beforeNodeIndex - 1];
// 如果前一个节点的row和beforeRow不一样需要减1
if (preNode && preNode.row !== beforeRow) {
row = beforeRow - 1;
}
}
if (position === 'bottom') {
if (beforeNodeIndex < 0) {
row = beforeRow + 1;
}
}
}
return {
...context,
data: {
...context.data,
row
},
schema: {
...context.schema,
[region]: body
}
};
}
afterInsert(context: InsertEventContext, store: any) {
const {isMobile} = store;
const region = context.region;
const body = [...(context.schema?.[region] || [])];
const position = context.dragInfo?.position || 'bottom';
const currentIndex = context.regionList.findIndex(
(item: any) => item.$$id === context.data.$$id
);
let regionList = [...context.regionList];
if (position === 'top' || position === 'bottom') {
if (isMobile) {
// const currentRow = regionList[currentIndex].row;
const preBeforeIndex = body.findIndex(
(item: any) => item.$$id === context.beforeId
);
const preBeforeRow = body[preBeforeIndex]?.row;
// 插入到了一行最后一个元素的后边所以该元素独占用一行后续元素的row都加1
if (preBeforeRow !== body[preBeforeIndex - 1]?.row) {
for (let i = currentIndex + 1; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
} else {
// 插入到了一行的中间这一行的最后一个元素的row加1后续元素的row都加1
let lastIndex = findLastIndex(
regionList,
(item: any) => item.row === preBeforeRow
);
lastIndex = lastIndex === -1 ? currentIndex + 1 : lastIndex;
for (let i = lastIndex; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
}
} else {
for (let i = currentIndex + 1; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
}
context.data.$$defaultColSize &&
(regionList[currentIndex].colSize = context.data.$$defaultColSize);
} else {
regionList = regionList.map((item: any) => {
if (item.row === context.data.row) {
item = {
...item,
colSize: 'auto'
};
}
return item;
});
}
return {
...context,
regionList
};
}
afterMove(context: MoveEventContext, store: any) {
const {isMobile} = store;
const position = context.dragInfo?.position;
const region = context.region;
const body = [...(context.schema?.[region] || [])];
const preCurrentIndex = body.findIndex(
(item: any) => item.$$id === context.sourceId
);
// 如果是最后一个元素往自己的上边移动,不做处理
if (
position === 'top' &&
preCurrentIndex === body.length - 1 &&
!context.beforeId
) {
return context;
}
let regionList = [...context.regionList];
const currentIndex = regionList.findIndex(
(item: any) => item.$$id === context.sourceId
);
// 如果移动的元素是整行则需要将后续的元素的row减1
const preCurrentRow = body[preCurrentIndex].row;
if (body.filter((item: any) => item.row === preCurrentRow).length === 1) {
for (let i = preCurrentIndex; i < regionList.length; i++) {
if (regionList[i].row > preCurrentRow) {
regionList[i] = {
...regionList[i],
row: regionList[i].row - 1
};
}
}
}
const beforeIndex = regionList.findIndex(
(item: any) => item.$$id === context.beforeId
);
const beforeNode =
regionList[beforeIndex] || regionList[regionList.length - 2];
const beforeRow = beforeNode?.row;
if (typeof beforeRow !== 'number') {
return context;
}
let row = beforeRow;
if (position === 'right') {
const preNode = regionList[beforeIndex - 2];
if (preNode && preNode.row !== beforeRow) {
row = beforeRow - 1;
}
}
if (position === 'bottom') {
if (beforeIndex < 0) {
row = beforeRow + 1;
}
}
if (position === 'top' || position === 'bottom') {
if (isMobile) {
const preBeforeIndex = body.findIndex(
(item: any) => item.$$id === context.beforeId
);
// 独占一行
if (beforeRow !== body[preBeforeIndex - 1]?.row) {
for (let i = currentIndex + 1; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
} else {
const lastIndex = findLastIndex(
regionList,
(item: any) => item.row === beforeRow
);
for (let i = lastIndex; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
}
} else {
for (let i = currentIndex + 1; i < regionList.length; i++) {
regionList[i] = {
...regionList[i],
row: regionList[i].row + 1
};
}
}
}
regionList[currentIndex] = {
...regionList[currentIndex],
row
};
regionList = setDefaultColSize(regionList, row, preCurrentRow);
return {
...context,
regionList
};
}
afterDelete(context: BaseEventContext) {
let regionList = [...context.regionList];
let preRow = -1;
for (let i = 0; i < regionList.length; i++) {
const row = regionList[i].row;
if (row - preRow >= 2) {
regionList[i] = {
...regionList[i],
row: row - 1
};
}
if (regionList[i + 1]?.row !== row) {
preRow = regionList[i].row;
}
}
regionList = setDefaultColSize(regionList, -1, preRow);
return {
...context,
regionList
};
}
afterMoveDown(context: BaseEventContext) {
const regionList = [...context.regionList];
const sourceId = context.sourceId;
const currentIndex = regionList.findIndex(n => n.$$id === sourceId);
const currentItem = regionList[currentIndex];
const changeItem = regionList[currentIndex - 1];
const tempRow = currentItem.row;
currentItem.row = changeItem.row;
changeItem.row = tempRow;
return {
...context,
regionList
};
}
afterMoveUp(context: BaseEventContext) {
const regionList = [...context.regionList];
const sourceId = context.sourceId;
const currentIndex = regionList.findIndex(n => n.$$id === sourceId);
const currentItem = regionList[currentIndex];
const changeItem = regionList[currentIndex + 1];
const tempRow = currentItem.row;
currentItem.row = changeItem.row;
changeItem.row = tempRow;
return {
...context,
regionList
};
}
}

View File

@ -0,0 +1,24 @@
import {EditorNodeType} from '../store/node';
import {JSONGetById} from '../util';
import DefaultLayout from './default';
import FlexLayout from './flex';
import {LayoutInterface} from './interface';
export default function getLayoutInstance(
schema: any,
region: EditorNodeType
): LayoutInterface {
if (!region) {
return new DefaultLayout();
}
const mode = region?.regionInfo?.dndMode;
const regionNode = JSONGetById(schema, region?.id);
let Klass = DefaultLayout;
if (typeof mode === 'function') {
if (mode(regionNode) === 'flex') {
Klass = FlexLayout;
}
}
return new Klass();
}

View File

@ -0,0 +1,24 @@
import {
BaseEventContext,
InsertEventContext,
MoveEventContext
} from '../plugin';
/**
*
*/
export interface LayoutInterface {
beforeInsert?: (
context: InsertEventContext,
store: any
) => InsertEventContext;
afterInsert?: (context: InsertEventContext, store: any) => InsertEventContext;
beforeMove?: (context: MoveEventContext, store: any) => MoveEventContext;
afterMove?: (context: MoveEventContext, store: any) => MoveEventContext;
beforeDelete?: (context: BaseEventContext, store: any) => BaseEventContext;
afterDelete?: (context: BaseEventContext, store: any) => BaseEventContext;
beforeMoveDown?: (context: BaseEventContext, store: any) => BaseEventContext;
afterMoveDown?: (context: BaseEventContext, store: any) => BaseEventContext;
beforeMoveUp?: (context: BaseEventContext, store: any) => BaseEventContext;
afterMoveUp?: (context: BaseEventContext, store: any) => BaseEventContext;
}

View File

@ -56,7 +56,8 @@ import {
isLayoutPlugin,
JSONPipeOut,
scrollToActive,
JSONPipeIn
JSONPipeIn,
JSONGetById
} from './util';
import {hackIn, makeSchemaFormRender, makeWrapper} from './component/factory';
import {env} from './env';
@ -961,10 +962,6 @@ export class EditorManager {
// 当前节点是布局类容器节点
regionNodeId = curActiveId;
regionNodeRegion = 'items';
} else if (node.schema.fields && node.schema.type === 'doc-entity') {
// 当前节点是表单视图
regionNodeId = curActiveId;
regionNodeRegion = 'fields';
} else if (node.schema.body) {
// 当前节点是容器节点
regionNodeId = curActiveId;
@ -1451,12 +1448,13 @@ export class EditorManager {
sourceId: node.id,
direction: 'up',
beforeId: node.prevSibling?.id,
region: regionNode.region
region: regionNode.region,
regionNode: regionNode
};
const event = this.trigger('before-move', context);
if (!event.prevented) {
store.moveUp(node.id);
store.moveUp(context);
// this.buildToolbars();
this.trigger('after-move', context);
this.trigger('after-update', context);
@ -1482,12 +1480,13 @@ export class EditorManager {
sourceId: node.id,
direction: 'down',
beforeId: node.nextSibling?.nextSibling?.id,
region: regionNode.region
region: regionNode.region,
regionNode: regionNode
};
const event = this.trigger('before-move', context);
if (!event.prevented) {
store.moveDown(node.id);
store.moveDown(context);
// this.buildToolbars();
this.trigger('after-move', context);
this.trigger('after-update', context);
@ -1512,8 +1511,7 @@ export class EditorManager {
if (!event.prevented) {
Array.isArray(context.data) && context.data.length
? this.store.delMulti(context.data)
: this.store.del(id);
: this.store.del(context);
this.trigger('after-delete', context);
}
}
@ -1617,6 +1615,7 @@ export class EditorManager {
id: string;
type: string;
data: any;
position?: string;
},
reGenerateId?: boolean
): any | null {
@ -1665,7 +1664,8 @@ export class EditorManager {
id: string,
region: string,
sourceId: string,
beforeId?: string
beforeId?: string,
dragInfo?: any
): boolean {
const store = this.store;
@ -1673,7 +1673,8 @@ export class EditorManager {
...this.buildEventContext(id),
beforeId,
region: region,
sourceId
sourceId,
dragInfo
};
const event = this.trigger('before-move', context);

View File

@ -114,7 +114,9 @@ export interface RegionConfig {
| 'default'
| 'position-h'
| 'position-v'
| (new (dnd: EditorDNDManager) => DNDModeInterface);
| 'flex'
// | (new (dnd: EditorDNDManager) => DNDModeInterface)
| ((node: any) => string | undefined);
/**
*
@ -213,6 +215,11 @@ export interface RendererInfo extends RendererScaffoldInfo {
*/
regions?: Array<RegionConfig>;
/**
*
*/
notHighlight?: boolean;
/**
* regions
*/
@ -534,6 +541,7 @@ export interface InsertEventContext extends BaseEventContext {
id: string;
type: string;
data: any;
position?: string;
};
}
@ -820,6 +828,11 @@ export interface PluginInterface
*/
async?: AsyncLayerOptions;
/**
*
*/
dragMode?: string;
/**
*
*/

View File

@ -122,7 +122,8 @@ export class BasicToolbarPlugin extends BasePlugin {
if (
!host?.memberImmutable(regionNode.region) &&
store.panels.some(Panel => Panel.key === 'renderers')
store.panels.some(Panel => Panel.key === 'renderers') &&
store.toolbarMode === 'default'
) {
const nextId = parent[idx + 1]?.$$id;
@ -206,7 +207,7 @@ export class BasicToolbarPlugin extends BasePlugin {
onClick: this.manager.del.bind(this.manager, id)
});
}
if (store.toolbarMode === 'default') {
toolbars.push({
id: 'more',
iconSvg: 'more-btn',
@ -236,6 +237,7 @@ export class BasicToolbarPlugin extends BasePlugin {
}
}
});
}
if (info.scaffoldForm?.canRebuild ?? info.plugin.scaffoldForm?.canRebuild) {
toolbars.push({
@ -263,6 +265,7 @@ export class BasicToolbarPlugin extends BasePlugin {
if (selections.length) {
// 多选时的右键菜单
if (store.toolbarMode === 'default') {
menus.push({
id: 'copy',
label: '重复一份',
@ -270,6 +273,7 @@ export class BasicToolbarPlugin extends BasePlugin {
disabled: selections.some(item => !item.node.duplicatable),
onSelect: () => manager.duplicate(selections.map(item => item.id))
});
}
menus.push({
id: 'unselect',
@ -320,6 +324,9 @@ export class BasicToolbarPlugin extends BasePlugin {
});
}
} else {
if (store.toolbarMode === 'mini') {
return;
}
menus.push({
id: 'select',
label: `选中${first.label}`,

View File

@ -32,7 +32,7 @@ export class DataDebugPlugin extends BasePlugin {
// return;
// }
const store = comp.props.store;
if (store.toolbarMode === 'default') {
toolbars.push({
icon: 'fa fa-bug',
order: -1000,
@ -41,6 +41,7 @@ export class DataDebugPlugin extends BasePlugin {
onClick: () => this.openDebugForm(comp.props.data)
});
}
}
dataViewer = {
type: 'json',

View File

@ -34,7 +34,9 @@ import {
PanelItem,
MoveEventContext,
ScaffoldForm,
PopOverForm
PopOverForm,
DeleteEventContext,
BaseEventContext
} from '../plugin';
import {
JSONDuplicate,
@ -59,6 +61,7 @@ import {matchSorter} from 'match-sorter';
import debounce from 'lodash/debounce';
import type {DialogSchema} from '../../../amis/src/renderers/Dialog';
import type {DrawerSchema} from '../../../amis/src/renderers/Drawer';
import getLayoutInstance from '../layout';
export interface SchemaHistory {
versionId: number;
@ -152,6 +155,8 @@ export const MainStore = types
label: 'Root'
}),
theme: 'cxd', // 主题默认cxd主题
toolbarMode: 'default', // 工具栏模式默认defaultmini模式没有更多、前后插入组件、上下文数据、重复一份、合成一行、右键功能
noDialog: false, // 不需要弹框功能
hoverId: '',
hoverRegion: '',
activeId: '',
@ -1236,6 +1241,11 @@ export const MainStore = types
// 显然有错误。
return;
}
const node = self.getNodeById(id, region);
const LayoutInstance = getLayoutInstance(self.schema, node!);
const {beforeInsert, afterInsert} = LayoutInstance;
beforeInsert && (event.context = beforeInsert(event.context, self));
const child = JSONPipeIn(event.context.data);
@ -1267,13 +1277,21 @@ export const MainStore = types
arr.push(child);
}
event.context.data = child;
event.context.regionList = arr;
afterInsert && (event.context = afterInsert(event.context, self));
this.traceableSetSchema(
JSONUpdate(self.schema, id, {
[region]: arr
[region]: event.context.regionList
})
);
event.context.data = child;
child?.$$id &&
setTimeout(() => {
this.setActiveId(child.$$id);
}, 0);
return child;
},
@ -1285,12 +1303,17 @@ export const MainStore = types
if (context.sourceId === context.beforeId) {
return;
}
const region = context.region;
const node = self.getNodeById(context.id, region);
const LayoutInstance = getLayoutInstance(self.schema, node!);
const {beforeMove, afterMove} = LayoutInstance;
beforeMove && (event.context = beforeMove(event.context, self));
const source = JSONGetById(schema, context.sourceId);
schema = JSONDelete(schema, context.sourceId, undefined, true);
const region = context.region;
const json = JSONGetById(schema, context.id);
let origin = json[region];
origin = Array.isArray(origin)
@ -1300,11 +1323,10 @@ export const MainStore = types
: [];
if (context.beforeId) {
const idx = findIndex(
let idx = findIndex(
origin,
(item: any) => item.$$id === context.beforeId
);
if (!~idx) {
throw new Error('位置错误,目标位置没有找到');
}
@ -1313,9 +1335,12 @@ export const MainStore = types
origin.push(source);
}
event.context.regionList = origin;
afterMove && (event.context = afterMove(event.context, self));
this.traceableSetSchema(
JSONUpdate(schema, context.id, {
[region]: origin
[region]: event.context.regionList
})
);
},
@ -1603,35 +1628,84 @@ export const MainStore = types
}
},
moveUp(id: string) {
if (!id) {
moveUp(context: BaseEventContext) {
const {sourceId, regionNode, region, id} = context;
if (!sourceId) {
return;
}
const schema = JSONMoveUpById(self.schema, sourceId);
const LayoutInstance = getLayoutInstance(self.schema, regionNode);
this.traceableSetSchema(JSONMoveUpById(self.schema, id));
if (LayoutInstance.afterMoveUp) {
const parent = JSONGetById(schema, id);
let regionList = parent[region];
context.regionList = regionList;
context = LayoutInstance.afterMoveUp(context, self);
this.traceableSetSchema(
JSONUpdate(schema, id, {
[region]: context.regionList
})
);
} else {
this.traceableSetSchema(schema);
}
},
moveDown(id: string) {
if (!id) {
moveDown(context: BaseEventContext) {
const {sourceId, regionNode, region, id} = context;
if (!sourceId) {
return;
}
const schema = JSONMoveDownById(self.schema, sourceId);
const LayoutInstance = getLayoutInstance(self.schema, regionNode);
this.traceableSetSchema(JSONMoveDownById(self.schema, id));
if (LayoutInstance.afterMoveDown) {
const parent = JSONGetById(schema, id);
let regionList = parent[region];
context.regionList = regionList;
context = LayoutInstance.afterMoveDown(context, self);
this.traceableSetSchema(
JSONUpdate(schema, id, {
[region]: context.regionList
})
);
} else {
this.traceableSetSchema(schema);
}
},
del(id: string) {
del(context: DeleteEventContext) {
const id = context.id;
if (id === self.activeId) {
const host = self.getNodeById(id)?.host;
this.setActiveId(host ? host.id : '');
const node = self.getNodeById(id);
this.setActiveId(node?.parentId || '', node?.parentRegion);
} else if (self.activeId) {
const active = JSONGetById(self.schema, id);
// 如果当前点选的是要删的节点里面的,则改成选中当前要删的上层
if (JSONGetById(active, self.activeId)) {
const host = self.getNodeById(id)?.host;
this.setActiveId(host ? host.id : '');
const node = self.getNodeById(id);
this.setActiveId(node?.parentId || '', node?.parentRegion);
}
}
this.traceableSetSchema(JSONDelete(self.schema, id));
const schema = JSONDelete(self.schema, id);
const node = self.getNodeById(id);
const LayoutInstance = getLayoutInstance(self.schema, node?.parent);
if (LayoutInstance.afterDelete && node) {
const parent = JSONGetById(schema, node.parentId);
let regionList = parent[node.parentRegion];
context.regionList = regionList;
context = LayoutInstance.afterDelete(context, self);
this.traceableSetSchema(
JSONUpdate(schema, node.parentId, {
[node.parentRegion]: context.regionList
})
);
} else {
this.traceableSetSchema(schema);
}
},
delMulti(ids: Array<string>) {

View File

@ -16,6 +16,7 @@ import isNumber from 'lodash/isNumber';
import debounce from 'lodash/debounce';
import merge from 'lodash/merge';
import {EditorModalBody} from './store/editor';
import {filter} from 'lodash';
const {
guid,
@ -1578,3 +1579,28 @@ export function mergeDefinitions(
return schema;
}
export function setDefaultColSize(
regionList: any[],
row: number,
preRow: number
) {
const tempList = [...regionList];
const preRowNodeLength = filter(tempList, n => n.row === preRow).length;
const currentRowNodeLength = filter(tempList, n => n.row === row).length;
for (let i = 0; i < tempList.length; i++) {
const item = tempList[i];
if (item.row === row) {
item.colSize = 'auto';
}
// 原来的行只有一个节点,且有默认宽度,则设置默认宽度
if (
((preRowNodeLength === 1 && item.row === preRow) ||
(currentRowNodeLength === 1 && item.row === row)) &&
item.$$defaultColSize
) {
item.colSize = item.$$defaultColSize;
}
}
return tempList;
}

View File

@ -4,6 +4,7 @@ export * from 'amis-editor-core';
export * from './builder';
import './tpl/index';
export * from './plugin';
export * from './validator';
import './renderer/OptionControl';
import './renderer/ValueFormatControl';
@ -16,11 +17,13 @@ import './renderer/TimelineItemControl';
import './renderer/APIControl';
import './renderer/APIAdaptorControl';
import './renderer/ValidationControl';
import './renderer/ValidateApiControl';
import './renderer/ValidationItem';
import './renderer/SwitchMoreControl';
import './renderer/StatusControl';
import './renderer/FormulaControl';
import './renderer/ExpressionFormulaControl';
import './renderer/ConditionFormulaControl';
import './renderer/textarea-formula/TextareaFormulaControl';
import './renderer/TplFormulaControl';
import './renderer/DateShortCutControl';

View File

@ -24,7 +24,8 @@ export class DividerPlugin extends BasePlugin {
description = '用来展示一个分割线,可用来做视觉上的隔离。';
docLink = '/amis/zh-CN/components/divider';
scaffold = {
type: 'divider'
type: 'divider',
$$dragMode: 'hv'
};
previewSchema: any = {
type: 'divider',

View File

@ -20,7 +20,10 @@ import {
ScaffoldForm,
RegionConfig,
registerEditorPlugin,
JSONPipeOut
JSONPipeOut,
InsertEventContext,
MoveEventContext,
DeleteEventContext
} from 'amis-editor-core';
import {
DSFeatureType,

View File

@ -268,6 +268,9 @@ export class ItemPlugin extends BasePlugin {
{id, schema, region, selections}: ContextMenuEventContext,
menus: Array<ContextMenuItem>
) {
if (this.manager.store.toolbarMode === 'mini') {
return;
}
if (!selections.length || selections.length > 3) {
// 单选或者超过3个选中态时直接返回
return;

View File

@ -0,0 +1,204 @@
/**
* @file
*/
import React from 'react';
import {render as renderAmis, autobind, FormControlProps} from 'amis-core';
import cx from 'classnames';
import {FormItem, Button, PickerContainer, ConditionBuilderFields} from 'amis';
import {reaction} from 'mobx';
import {getVariables} from 'amis-editor-core';
interface ConditionFormulaControlProps extends FormControlProps {
/**
* ConditionBuilder
*/
fields?: ConditionBuilderFields;
/**
* amis数据域中取变量集合 true
*/
requiredDataPropsFields?: boolean;
[props: string]: any;
}
interface ConditionFormulaControlState {
formulaPickerValue: string;
fields: ConditionBuilderFields;
}
// 把数据域中的变量类型转换成符合ConditionBuilder的type
const PropsFieldsMapping: {
[props: string]: string;
} = {
string: 'text',
number: 'number',
boolean: 'boolean'
};
export default class ConditionFormulaControl extends React.Component<
ConditionFormulaControlProps,
ConditionFormulaControlState
> {
static defaultProps: Partial<ConditionFormulaControlProps> = {
requiredDataPropsFields: true
};
isUnmount: boolean;
unReaction: any;
appLocale: string;
appCorpusData: any;
constructor(props: ConditionFormulaControlProps) {
super(props);
this.state = {
fields: [],
formulaPickerValue: ''
};
}
async componentDidMount() {
this.initFormulaPickerValue(this.props.value);
const editorStore = (window as any).editorStore;
this.appLocale = editorStore?.appLocale;
this.appCorpusData = editorStore?.appCorpusData;
this.unReaction = reaction(
() => editorStore?.appLocaleState,
async () => {
this.appLocale = editorStore?.appLocale;
this.appCorpusData = editorStore?.appCorpusData;
}
);
const fieldsArr = await this.buildFieldsData();
this.setState({
fields: fieldsArr
});
}
async componentDidUpdate(prevProps: ConditionFormulaControlProps) {
if (prevProps.value !== this.props.value) {
this.initFormulaPickerValue(this.props.value);
}
}
componentWillUnmount() {
this.isUnmount = true;
this.unReaction?.();
}
@autobind
async buildFieldsData() {
let fieldsArr: ConditionBuilderFields = [];
const {requiredDataPropsFields, fields} = this.props;
if (requiredDataPropsFields) {
const variablesArr = await getVariables(this);
// 自身字段
const selfName = this.props?.data?.name;
const vars =
variablesArr?.filter((item: any) => item?.label === '组件上下文')?.[0]
?.children?.[0]?.children || [];
fieldsArr = vars
.map((item: any) => {
if (item && item.type && PropsFieldsMapping[item.type]) {
let obj: any = {
label: item.label,
type: PropsFieldsMapping[item.type],
name: item.value
};
if (selfName === item.value) {
obj = {
...obj,
label: item.label + 'self',
disabled: true
};
}
return obj;
}
})
?.filter((item: any) => item);
}
return fieldsArr.concat(fields || []);
}
@autobind
initFormulaPickerValue(value: string) {
this.setState({
formulaPickerValue: value
});
}
@autobind
handleConfirm(value = '') {
this.props?.onChange?.(value);
}
@autobind
async handleOnClick(
e: React.MouseEvent,
onClick: (e: React.MouseEvent) => void
) {
const fieldsArr = await this.buildFieldsData();
this.setState({
fields: fieldsArr
});
return onClick?.(e);
}
render() {
const {name, className, size} = this.props;
const {formulaPickerValue, fields} = this.state;
return (
<div className={cx('ae-ExpressionFormulaControl', className)}>
<PickerContainer
title="条件设置"
bodyRender={({
value,
onChange
}: {
onChange: (value: any) => void;
value: any;
}) => {
const condition = renderAmis(
{
type: 'condition-builder',
label: false,
name,
fields: fields
},
{
value,
onChange
}
);
return condition;
}}
value={formulaPickerValue}
onConfirm={this.handleConfirm}
size={size ?? 'lg'}
>
{({onClick}: {onClick: (e: React.MouseEvent) => any}) => (
<Button
size="sm"
className="btn-set-expression"
onClick={(e: any) => this.handleOnClick(e, onClick)}
>
</Button>
)}
</PickerContainer>
</div>
);
}
}
@FormItem({
type: 'ae-conditionFormulaControl'
})
export class ConditionFormulaControlRenderer extends ConditionFormulaControl {}

View File

@ -30,6 +30,7 @@ export interface StatusControlProps extends FormControlProps {
type StatusFormData = {
statusType: number;
expression: string;
condition: object;
};
interface StatusControlState {
@ -64,7 +65,11 @@ export class StatusControl extends React.Component<
const formData: StatusFormData = {
statusType: 1,
expression: ''
expression: '',
condition: {
conjunction: 'and',
children: []
}
};
let ctx = data;
@ -73,14 +78,29 @@ export class StatusControl extends React.Component<
ctx = noBulkChangeData;
}
if (ctx[expressionName] || ctx[expressionName] === '') {
if (
typeof ctx[expressionName] === 'string' &&
(ctx[expressionName] || ctx[expressionName] === '')
) {
formData.statusType = 2;
formData.expression = ctx[expressionName];
}
if (
typeof ctx[expressionName] === 'object' &&
ctx[expressionName] &&
ctx[expressionName].conjunction
) {
formData.statusType = 3;
formData.condition = ctx[expressionName];
}
return {
checked:
ctx[name] == trueValue ||
typeof ctx[expressionName] === 'string' ||
Object.prototype.toString.call(ctx[expressionName]) ===
'[object Object]' ||
(!!defaultTrue &&
ctx[name] == undefined &&
ctx[expressionName] == undefined),
@ -98,7 +118,7 @@ export class StatusControl extends React.Component<
@autobind
handleSwitch(value: boolean) {
const {trueValue, falseValue} = this.props;
const {expression, statusType = 1} = this.state.formData || {};
const {condition, expression, statusType = 1} = this.state.formData || {};
this.setState({checked: value == trueValue ? true : false}, () => {
const {onBulkChange, noBulkChange, onDataChange, expressionName, name} =
this.props;
@ -115,6 +135,9 @@ export class StatusControl extends React.Component<
case 2:
newData[expressionName] = expression;
break;
case 3:
newData[expressionName] = condition;
break;
}
}
!noBulkChange && onBulkChange && onBulkChange(newData);
@ -140,6 +163,9 @@ export class StatusControl extends React.Component<
case 2:
data[expressionName] = values.expression;
break;
case 3:
data[expressionName] = values.condition;
break;
}
!noBulkChange && onBulkChange && onBulkChange(data);
onDataChange && onDataChange(data);
@ -225,6 +251,11 @@ export class StatusControl extends React.Component<
name: 'expression',
placeholder: `请输入${label}条件`,
visibleOn: 'this.statusType === 2'
}),
getSchemaTpl('conditionFormulaControl', {
label: '条件设置',
name: 'condition',
visibleOn: 'this.statusType === 3'
})
]
},

View File

@ -0,0 +1,89 @@
/**
* @file
*/
import React from 'react';
import {FormItem} from 'amis';
import {getSchemaTpl, tipedLabel} from 'amis-editor-core';
import type {FormControlProps} from 'amis-core';
export interface ValidationApiControlProps extends FormControlProps {}
export default class ValidationApiControl extends React.Component<ValidationApiControlProps> {
constructor(props: ValidationApiControlProps) {
super(props);
}
renderValidateApiControl() {
const {onBulkChange, render} = this.props;
return (
<div className="ae-ValidationControl-item">
{render('validate-api-control', {
type: 'form',
wrapWithPanel: false,
className: 'w-full mb-2',
bodyClassName: 'p-none',
wrapperComponent: 'div',
mode: 'horizontal',
data: {
switchStatus: this.props.data.validateApi !== undefined
},
preventEnterSubmit: true,
submitOnChange: true,
onSubmit: ({switchStatus, validateApi}: any) => {
onBulkChange &&
onBulkChange({
validateApi: !switchStatus ? undefined : validateApi
});
},
body: [
getSchemaTpl('switch', {
label: tipedLabel(
'接口校验',
`配置校验接口,对表单项进行远程校验,配置方式与普通接口一致<br />
1. <span class="ae-ValidationControl-label-code">{status: 0}</span> <br />
2. <span class="ae-ValidationControl-label-code">{status: 422}</span> <br />
3. errors <br />
<span class="ae-ValidationControl-label-code">{status: 422, errors: '错误提示消息'}</span>
`
),
name: 'switchStatus'
}),
{
type: 'container',
className: 'ae-ExtendMore ae-ValidationControl-item-input',
bodyClassName: 'w-full',
visibleOn: 'this.switchStatus',
data: {
// 放在form中则包含的表达式会被求值
validateApi: this.props.data.validateApi
},
body: [
getSchemaTpl('apiControl', {
name: 'validateApi',
renderLabel: true,
label: '',
mode: 'normal',
className: 'w-full'
})
]
}
]
})}
</div>
);
}
render() {
return <>{this.renderValidateApiControl()}</>;
}
}
@FormItem({
type: 'ae-validationApiControl',
renderLabel: false,
strictMode: false
})
export class ValidationApiControlRenderer extends ValidationApiControl {}

View File

@ -6,9 +6,15 @@ import React, {ReactNode} from 'react';
import groupBy from 'lodash/groupBy';
import remove from 'lodash/remove';
import cx from 'classnames';
import {FormItem} from 'amis';
import {ConditionBuilderFields, FormItem} from 'amis';
import {autobind, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {
autobind,
getSchemaTpl,
getVariables,
isObjectShallowModified,
tipedLabel
} from 'amis-editor-core';
import ValidationItem, {ValidatorData} from './ValidationItem';
import type {FormControlProps} from 'amis-core';
@ -34,31 +40,85 @@ interface ValidationControlState {
defaultValidators: Record<string, Validator>;
builtInValidators: Record<string, Validator>;
};
fields: ConditionBuilderFields;
}
export default class ValidationControl extends React.Component<
ValidationControlProps,
ValidationControlState
> {
cache?: any;
constructor(props: ValidationControlProps) {
super(props);
this.state = {
avaliableValids: this.getAvaliableValids(props)
avaliableValids: this.getAvaliableValids(props),
fields: []
};
}
async componentDidMount() {
const fieldsArr = await this.buildFieldsData();
this.setState({
fields: fieldsArr
});
}
componentWillReceiveProps(nextProps: ValidationControlProps) {
if (this.props.data.type !== nextProps.data.type) {
if (
this.props.data.type !== nextProps.data.type ||
this.cache?.required !== nextProps.data.required ||
isObjectShallowModified(
this.cache?.validations,
nextProps.data.validations
) ||
isObjectShallowModified(
this.cache?.validationErrors,
nextProps.data.validationErrors
)
) {
this.setState({
avaliableValids: this.getAvaliableValids(nextProps)
});
const validators = this.transformValid(this.props.data);
this.updateValidation(validators);
// const validators = this.transformValid(this.props.data);
// this.updateValidation(validators);
}
// todo 删除不允许配置的值
}
@autobind
async buildFieldsData() {
const variablesArr = await getVariables(this);
// 自身字段
const selfName = this.props.data.name;
const vars =
variablesArr?.filter((item: any) => item?.label === '组件上下文')?.[0]
?.children?.[0]?.children || [];
const arr: ConditionBuilderFields = vars
.map((item: any) => {
if (item && item.value) {
let obj: any = {
label: item.label,
value: item.value
};
if (selfName === item.value) {
obj = {
...obj,
label: item.label + 'self',
disabled: true
};
}
return obj;
}
})
?.filter((item: any) => item);
return arr;
}
getAvaliableValids(props: ValidationControlProps) {
let {data, tag} = props;
tag = typeof tag === 'string' ? tag : tag(data);
@ -96,6 +156,7 @@ export default class ValidationControl extends React.Component<
const {onBulkChange} = this.props;
if (!validators.length) {
this.cache = undefined;
onBulkChange &&
onBulkChange({
required: undefined,
@ -121,14 +182,15 @@ export default class ValidationControl extends React.Component<
}
});
onBulkChange &&
onBulkChange({
this.cache = {
required,
validations: Object.keys(validations).length ? validations : undefined,
validationErrors: Object.keys(validationErrors).length
? validationErrors
: undefined
});
};
onBulkChange && onBulkChange({...this.cache});
}
/**
@ -254,17 +316,18 @@ export default class ValidationControl extends React.Component<
renderValidaton() {
const classPrefix = this.props?.env?.theme?.classPrefix;
let {
avaliableValids: {defaultValidators, moreValidators, builtInValidators}
avaliableValids: {defaultValidators, moreValidators, builtInValidators},
fields
} = this.state;
let validators = this.transformValid(this.props.data);
const rules: ReactNode[] = [];
validators = validators.concat();
// 优先渲染默认的顺序
Object.keys(defaultValidators).forEach((validName: string) => {
const data = remove(validators, v => v.name === validName);
rules.push(
<ValidationItem
fields={fields}
key={validName}
validator={defaultValidators[validName]}
data={data.length ? data[0] : {name: validName}}
@ -281,11 +344,12 @@ export default class ValidationControl extends React.Component<
const data = remove(validators, v => v.name === validName);
rules.push(
<ValidationItem
fields={fields}
key={validName}
validator={builtInValidators[validName]}
data={
data.length
? data[0]
? {...data[0], isBuiltIn: true}
: {name: validName, value: true, isBuiltIn: true}
}
classPrefix={classPrefix}
@ -307,6 +371,7 @@ export default class ValidationControl extends React.Component<
}
rules.push(
<ValidationItem
fields={fields}
key={valid.name}
data={valid}
classPrefix={classPrefix}
@ -323,67 +388,6 @@ export default class ValidationControl extends React.Component<
return (
<div className="ae-ValidationControl-rules" key="rules">
{rules}
{this.renderValidateApiControl()}
</div>
);
}
renderValidateApiControl() {
const {onBulkChange, render} = this.props;
return (
<div className="ae-ValidationControl-item">
{render('validate-api-control', {
type: 'form',
wrapWithPanel: false,
className: 'w-full mb-2',
bodyClassName: 'p-none',
wrapperComponent: 'div',
mode: 'horizontal',
data: {
switchStatus: this.props.data.validateApi !== undefined
},
preventEnterSubmit: true,
submitOnChange: true,
onSubmit: ({switchStatus, validateApi}: any) => {
onBulkChange &&
onBulkChange({
validateApi: !switchStatus ? undefined : validateApi
});
},
body: [
getSchemaTpl('switch', {
label: tipedLabel(
'接口校验',
`配置校验接口,对表单项进行远程校验,配置方式与普通接口一致<br />
1. <span class="ae-ValidationControl-label-code">{status: 0}</span> <br />
2. <span class="ae-ValidationControl-label-code">{status: 422}</span> <br />
3. errors <br />
<span class="ae-ValidationControl-label-code">{status: 422, errors: '错误提示消息'}</span>
`
),
name: 'switchStatus'
}),
{
type: 'container',
className: 'ae-ExtendMore ae-ValidationControl-item-input',
bodyClassName: 'w-full',
visibleOn: 'this.switchStatus',
data: {
// 放在form中则包含的表达式会被求值
validateApi: this.props.data.validateApi
},
body: [
getSchemaTpl('apiControl', {
name: 'validateApi',
renderLabel: true,
label: '',
mode: 'normal',
className: 'w-full'
})
]
}
]
})}
</div>
);
}

View File

@ -9,7 +9,7 @@ import {render, Button, Switch} from 'amis';
import {autobind, getI18nEnabled} from 'amis-editor-core';
import {Validator} from '../validator';
import {tipedLabel} from 'amis-editor-core';
import type {SchemaCollection} from 'amis';
import type {ConditionBuilderFields, SchemaCollection} from 'amis';
export type ValidatorData = {
name: string;
@ -36,6 +36,8 @@ export interface ValidationItemProps {
validator: Validator;
fields?: ConditionBuilderFields;
onEdit?: (data: ValidatorData) => void;
onDelete?: (name: string) => void;
onSwitch?: (checked: boolean, data?: ValidatorData) => void;
@ -134,6 +136,7 @@ export default class ValidationItem extends React.Component<
renderInputControl() {
const {value, message, checked} = this.state;
const {fields} = this.props;
const i18nEnabled = getI18nEnabled();
let control: any = [];
@ -186,6 +189,10 @@ export default class ValidationItem extends React.Component<
data: {value, message}
},
{
data: {
...this.props.data,
fields
},
onSubmit: this.handleEdit
}
)}

View File

@ -144,6 +144,13 @@ setSchemaTpl('expressionFormulaControl', (schema: object = {}) => {
};
});
setSchemaTpl('conditionFormulaControl', (schema: object = {}) => {
return {
type: 'ae-conditionFormulaControl',
...schema
};
});
setSchemaTpl('textareaFormulaControl', (schema: object = {}) => {
return {
type: 'ae-textareaFormulaControl',

View File

@ -558,6 +558,9 @@ setSchemaTpl(
{
type: 'ae-validationControl',
mode: 'normal',
style: {
marginBottom: '6px'
},
...config
// pipeIn: (value: any, data: any) => {
// // return reduce(value, (arr: any, item) => {
@ -586,6 +589,7 @@ setSchemaTpl(
// // }, []);
// },
},
getSchemaTpl('validationApiControl'),
getSchemaTpl('validateOnChange')
]
};
@ -593,6 +597,11 @@ setSchemaTpl(
}
);
setSchemaTpl('validationApiControl', {
type: 'ae-validationApiControl',
label: false
});
setSchemaTpl('validationControl', (value: Array<ValidationOptions> = []) => ({
type: 'ae-validationControl',
label: '校验规则',

View File

@ -65,6 +65,10 @@ export const registerValidator = (...config: Array<Validator>) => {
Validators.push(...config);
};
export const removeAllValidator = () => {
Validators.length = 0;
};
export const getValidatorsByTag = (tag: ValidatorTag) => {
const defaultValidators: Record<string, Validator> = {};
const moreValidators: Record<string, Validator> = {};
@ -117,7 +121,9 @@ export enum ValidatorTag {
File = '8',
Date = '9',
Code = '10',
Tree = '11'
Tree = '11',
Phone = '12',
Tel = '13'
}
registerValidator(
@ -133,7 +139,8 @@ registerValidator(
[ValidatorTag.Email]: ValidTagMatchType.isDefault,
[ValidatorTag.Password]: ValidTagMatchType.isDefault,
[ValidatorTag.URL]: ValidTagMatchType.isDefault,
[ValidatorTag.Tree]: ValidTagMatchType.isDefault
[ValidatorTag.Tree]: ValidTagMatchType.isDefault,
[ValidatorTag.Phone]: ValidTagMatchType.isDefault
}
},
{
@ -345,7 +352,8 @@ registerValidator(
group: ValidationGroup.Pattern,
message: '请输入合法的手机号码',
tag: {
[ValidatorTag.Text]: ValidTagMatchType.isMore
[ValidatorTag.Text]: ValidTagMatchType.isMore,
[ValidatorTag.Phone]: ValidTagMatchType.isBuiltIn
}
},
{
@ -354,7 +362,8 @@ registerValidator(
group: ValidationGroup.Pattern,
message: '请输入合法的电话号码',
tag: {
[ValidatorTag.Text]: ValidTagMatchType.isMore
[ValidatorTag.Text]: ValidTagMatchType.isMore,
[ValidatorTag.Tel]: ValidTagMatchType.isBuiltIn
}
},
{

View File

@ -408,6 +408,53 @@
// }
}
}
.#{$ns}Form-flex {
display: flex;
// flex-wrap: wrap;
align-items: flex-start;
width: 100%;
}
.#{$ns}Form-flex-col {
flex-basis: 0;
flex-grow: 1;
flex-shrink: 1;
padding: calc(var(--Form-item-gap) / 2);
}
.#{$ns}Form-flexInner {
display: flex;
flex-wrap: nowrap;
> .#{$ns}Form-label {
display: inline-block;
vertical-align: top;
padding-top: var(--Form-label-paddingTop);
margin-right: var(--Form-row-gutterWidth);
}
> .#{$ns}Form-control {
flex-basis: 0;
flex-grow: 1;
}
}
.#{$ns}Form-flexInner--label-top {
flex-direction: column;
}
.#{$ns}Form-flexInner--label-left {
> .#{$ns}Form-label {
text-align: left;
width: px2rem(76px);
}
}
.#{$ns}Form-flexInner--label-right {
> .#{$ns}Form-label {
text-align: right;
width: px2rem(76px);
}
}
}
.#{$ns}Form--debug {
@ -852,6 +899,42 @@
display: none;
}
}
.#{$ns}Form-flexInner {
display: flex;
flex-wrap: nowrap;
width: 100%;
> .#{$ns}Form-label {
display: inline-block;
vertical-align: top;
padding-top: var(--Form-label-paddingTop);
padding-right: var(--Form-row-gutterWidth);
flex: 1;
min-width: 0;
padding-top: 0;
}
> .#{$ns}Form-control {
flex: 1;
}
}
.#{$ns}Form-flexInner--label-top {
flex-direction: column;
> .#{$ns}Form-label {
margin-bottom: px2rem(10px);
}
}
.#{$ns}Form-flexInner--label-left {
> .#{$ns}Form-label {
text-align: left;
}
}
.#{$ns}Form-flexInner--label-right {
> .#{$ns}Form-label {
text-align: right;
}
}
}
.#{$ns}Autofill-popOver {

View File

@ -366,7 +366,7 @@
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
background-color: var(--inputNumber-base-unit-bg-color);
min-width: var(--inputNumber-base-default-unit-width) !important;
min-width: var(--inputNumber-base-default-unit-width);
padding: var(--inputNumber-base-default-unit-paddingTop)
var(--inputNumber-base-default-unit-paddingRight)
var(--inputNumber-base-default-unit-paddingBottom)

View File

@ -2,13 +2,7 @@ import React from 'react';
import isEqual from 'lodash/isEqual';
import pickBy from 'lodash/pickBy';
import omitBy from 'lodash/omitBy';
import {
Renderer,
RendererProps,
evalExpressionWithConditionBuilder,
filterTarget,
mapTree
} from 'amis-core';
import {Renderer, RendererProps, filterTarget, mapTree} from 'amis-core';
import {SchemaNode, Schema, ActionObject, PlainObject} from 'amis-core';
import {CRUDStore, ICRUDStore, getMatchedEventTargets} from 'amis-core';
import {

View File

@ -5,7 +5,6 @@ import {
RendererProps,
ScopedContext,
buildStyle,
evalExpressionWithConditionBuilder,
getMatchedEventTargets,
getPropValue
} from 'amis-core';

View File

@ -10,7 +10,7 @@ import {
resolveEventData,
ApiObject,
FormHorizontal,
evalExpressionWithConditionBuilder,
evalExpressionWithConditionBuilderAsync,
IFormStore,
getVariable,
IFormItemStore,
@ -2033,7 +2033,7 @@ export class ComboControlRenderer extends ComboControl {
} else if (condition !== undefined) {
for (let i = 0; i < len; i++) {
const item = items[i];
const isUpdate = await evalExpressionWithConditionBuilder(
const isUpdate = await evalExpressionWithConditionBuilderAsync(
condition,
item
);

View File

@ -24,7 +24,7 @@ import {
getRendererByName,
resolveEventData,
ListenerAction,
evalExpressionWithConditionBuilder,
evalExpressionWithConditionBuilderAsync,
mapTree,
isObject,
eachTree,
@ -1949,7 +1949,7 @@ export class TableControlRenderer extends FormTable {
const promises: Array<() => Promise<any>> = [];
everyTree(items, (item, index, paths, indexes) => {
promises.unshift(async () => {
const isUpdate = await evalExpressionWithConditionBuilder(
const isUpdate = await evalExpressionWithConditionBuilderAsync(
condition,
item
);
@ -2085,7 +2085,7 @@ export class TableControlRenderer extends FormTable {
const promises: Array<() => Promise<any>> = [];
everyTree(items, (item, index, paths, indexes) => {
promises.unshift(async () => {
const result = await evalExpressionWithConditionBuilder(
const result = await evalExpressionWithConditionBuilderAsync(
args?.condition,
item
);

View File

@ -4,7 +4,6 @@ import Sortable from 'sortablejs';
import omit from 'lodash/omit';
import {
ScopedContext,
evalExpressionWithConditionBuilder,
filterClassNameObject,
getMatchedEventTargets,
getPropValue

View File

@ -16,7 +16,6 @@ import {
SchemaExpression,
position,
animation,
evalExpressionWithConditionBuilder,
isEffectiveApi,
Renderer,
RendererProps,

View File

@ -30,7 +30,7 @@ import {
isArrayChildrenModified,
filterTarget,
changedEffect,
evalExpressionWithConditionBuilder,
evalExpressionWithConditionBuilderAsync,
normalizeApi,
getPropValue
} from 'amis-core';
@ -2182,7 +2182,7 @@ export class TableRenderer extends Table2 {
let items = [...store.data.rows];
for (let i = 0; i < len; i++) {
const item = items[i];
const isUpdate = await evalExpressionWithConditionBuilder(
const isUpdate = await evalExpressionWithConditionBuilderAsync(
condition,
item
);

View File

@ -31,7 +31,7 @@ import {
import {ActionSchema} from './Action';
import {tokenize, evalExpressionWithConditionBuilder} from 'amis-core';
import {tokenize, evalExpressionWithConditionBuilderAsync} from 'amis-core';
import {StepSchema} from './Steps';
import isEqual from 'lodash/isEqual';
import omit from 'lodash/omit';
@ -413,7 +413,7 @@ export default class Wizard extends React.Component<WizardProps, WizardState> {
const stepsLength = steps.length;
// 这里有个bug如果把第一个step隐藏表单就不会渲染
for (let i = 0; i < stepsLength; i++) {
const hiddenFlag = await evalExpressionWithConditionBuilder(
const hiddenFlag = await evalExpressionWithConditionBuilderAsync(
steps[i].hiddenOn,
values
);