mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
Merge branch 'master' into chore-table904
This commit is contained in:
commit
77a0e57278
@ -32,8 +32,11 @@ order: 42
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ---------- | ----------------------------------- |
|
||||
| type | `string` | | `"divider"` 指定为 分割线 渲染器 |
|
||||
| className | `string` | | 外层 Dom 的类名 |
|
||||
| lineStyle | `string` | `"dashed"` | 分割线的样式,支持`dashed`和`solid` |
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ------------ | ------------------------------------------ |
|
||||
| type | `string` | | `"divider"` 指定为 分割线 渲染器 |
|
||||
| className | `string` | | 外层 Dom 的类名 |
|
||||
| lineStyle | `string` | `dashed` | 分割线的样式,支持`dashed`和`solid` |
|
||||
| direction | `string` | `horizontal` | 分割线的方向,支持`horizontal`和`vertical` |
|
||||
| color | `string` | | 分割线的颜色 |
|
||||
| rotate | `number` | | 分割线的旋转角度 |
|
||||
|
@ -45,6 +45,7 @@ order: 38
|
||||
"name": "range",
|
||||
"label": "range",
|
||||
"multiple": true,
|
||||
"joinValues": false,
|
||||
"value": {
|
||||
"min": 10,
|
||||
"max": 50
|
||||
|
@ -453,6 +453,7 @@ order: 56
|
||||
| focus | `[name]: string` 组件的值 | 输入框获取焦点时触发 |
|
||||
| blur | `[name]: string` 组件的值 | 输入框失去焦点时触发 |
|
||||
| change | `[name]: string` 组件的值 | 值变化时触发 |
|
||||
| clear | `[name]: string` 组件的值 | 点击清除按钮时触发 |
|
||||
|
||||
## 动作表
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: 可视化编辑器
|
||||
|
||||
目前 amis 可视化编辑器也作为单独的 npm 包发布了出来,可以通过 npm 安装使用。
|
||||
|
||||
在线体验:https://aisuda.github.io/amis-editor-demo
|
||||
在线体验:https://aisuda.github.io/amis-editor-demo
|
||||
示例代码:https://github.com/aisuda/amis-editor-demo
|
||||
|
||||
## 使用
|
||||
@ -46,7 +46,7 @@ render() {
|
||||
- `className?: string` 额外加个 css 类名,辅助样式定义。
|
||||
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
|
||||
- `theme?: string` amis 主题
|
||||
- `schemaFilter?: (schema: any) => any` 配置过滤器。可以用来实现 api proxy,比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
|
||||
- `schemaFilter?: (schema: any, isPreview?: boolean) => any` 配置过滤器。可以用来实现 api proxy,比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
|
||||
- `amisEnv?: any` 这是是给 amis 的 Env 对象,具体请前往 [env 说明](../start/getting-started#env)
|
||||
- `disableBultinPlugin?: boolean` 是否禁用内置插件
|
||||
- `disablePluginList?: Array<string> | (id: string, plugin: PluginClass) => boolean` 禁用插件列表
|
||||
|
@ -807,13 +807,13 @@ export const components = [
|
||||
import('../../docs/zh-CN/components/table.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
// {
|
||||
// label: 'Table2 表格',
|
||||
// path: '/zh-CN/components/table2',
|
||||
// component: React.lazy(() =>
|
||||
// import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
|
||||
// )
|
||||
// },
|
||||
{
|
||||
label: 'Table2 表格',
|
||||
path: '/zh-CN/components/table2',
|
||||
component: React.lazy(() =>
|
||||
import('../../docs/zh-CN/components/table2.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Table View 表格视图',
|
||||
path: '/zh-CN/components/table-view',
|
||||
|
@ -5,5 +5,5 @@
|
||||
"packages/amis-ui",
|
||||
"packages/amis"
|
||||
],
|
||||
"version": "3.4.0"
|
||||
"version": "3.4.1-alpha.0"
|
||||
}
|
||||
|
@ -138,4 +138,4 @@
|
||||
"printBasicPrototype": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-core",
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.1-alpha.0",
|
||||
"description": "amis-core",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -1,29 +1,42 @@
|
||||
import {useEffect} from 'react';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import type {RendererEnv} from '../env';
|
||||
import type {CustomStyleClassName} from '../utils/style-helper';
|
||||
import {insertCustomStyle} from '../utils/style-helper';
|
||||
import type {InsertCustomStyle} from '../utils/style-helper';
|
||||
import {StyleDom} from '../utils/style-helper';
|
||||
|
||||
interface CustomStyleProps {
|
||||
config: {
|
||||
themeCss: any;
|
||||
classNames: CustomStyleClassName[];
|
||||
id?: string;
|
||||
defaultData?: any;
|
||||
};
|
||||
wrapperCustomStyle?: any;
|
||||
componentId?: string;
|
||||
} & InsertCustomStyle;
|
||||
env: RendererEnv;
|
||||
}
|
||||
|
||||
export default function (props: CustomStyleProps) {
|
||||
const {themeCss, classNames, id, defaultData} = props.config;
|
||||
const {themeCss, classNames, id, defaultData, wrapperCustomStyle} =
|
||||
props.config;
|
||||
if (!themeCss && !wrapperCustomStyle) {
|
||||
return null;
|
||||
}
|
||||
const styleDom = useRef(new StyleDom(id || '')).current;
|
||||
|
||||
useEffect(() => {
|
||||
insertCustomStyle(
|
||||
styleDom.insertCustomStyle({
|
||||
themeCss,
|
||||
classNames,
|
||||
id,
|
||||
defaultData,
|
||||
props.env?.customStyleClassPrefix
|
||||
);
|
||||
customStyleClassPrefix: props.env?.customStyleClassPrefix
|
||||
});
|
||||
return () => {
|
||||
styleDom.removeCustomStyle();
|
||||
};
|
||||
}, [props.config.themeCss]);
|
||||
|
||||
useEffect(() => {
|
||||
styleDom.insertEditCustomStyle(wrapperCustomStyle);
|
||||
return () => {
|
||||
styleDom.removeCustomStyle('wrapperCustomStyle');
|
||||
};
|
||||
}, [props.config.wrapperCustomStyle]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import type {RendererEvent} from './utils/renderer-event';
|
||||
import type {ListenerContext} from './actions/Action';
|
||||
import type {ICmptAction} from './actions/CmptAction';
|
||||
|
||||
export interface wsObject {
|
||||
export interface WsObject {
|
||||
url: string;
|
||||
responseKey?: string;
|
||||
body?: any;
|
||||
@ -32,7 +32,7 @@ export interface RendererEnv {
|
||||
fetcher: (api: Api, data?: any, options?: object) => Promise<Payload>;
|
||||
isCancel: (val: any) => boolean;
|
||||
wsFetcher: (
|
||||
ws: wsObject,
|
||||
ws: WsObject,
|
||||
onMessage: (data: any) => void,
|
||||
onError: (error: any) => void
|
||||
) => void;
|
||||
|
@ -100,24 +100,24 @@ export interface RenderSchemaFilter {
|
||||
(schema: Schema, renderer: RendererConfig, props?: any): Schema;
|
||||
}
|
||||
|
||||
export interface wsObject {
|
||||
export interface WsObject {
|
||||
url: string;
|
||||
responseKey?: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
export interface FetcherConfig {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'jsonp' | 'js';
|
||||
data?: any;
|
||||
config?: any;
|
||||
}
|
||||
|
||||
export interface RenderOptions
|
||||
extends Partial<Omit<RendererEnv, 'fetcher' | 'theme'>> {
|
||||
session?: string;
|
||||
theme?: string;
|
||||
fetcher?: (config: fetcherConfig) => Promise<fetcherResult>;
|
||||
}
|
||||
|
||||
export interface fetcherConfig {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'jsonp' | 'js';
|
||||
data?: any;
|
||||
config?: any;
|
||||
fetcher?: (config: FetcherConfig) => Promise<fetcherResult>;
|
||||
}
|
||||
|
||||
const renderers: Array<RendererConfig> = [];
|
||||
@ -238,7 +238,7 @@ export const defaultOptions: RenderOptions = {
|
||||
},
|
||||
// 使用 WebSocket 来实时获取数据
|
||||
wsFetcher(
|
||||
ws: wsObject,
|
||||
ws: WsObject,
|
||||
onMessage: (data: any) => void,
|
||||
onError: (error: any) => void
|
||||
) {
|
||||
|
@ -954,7 +954,9 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
mobileUI,
|
||||
translate: __,
|
||||
static: isStatic,
|
||||
staticClassName
|
||||
staticClassName,
|
||||
id,
|
||||
wrapperCustomStyle
|
||||
} = props;
|
||||
|
||||
// 强制不渲染 label 的话
|
||||
@ -980,7 +982,10 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
[`is-error`]: model && !model.valid,
|
||||
[`is-required`]: required
|
||||
},
|
||||
model?.errClassNames
|
||||
model?.errClassNames,
|
||||
wrapperCustomStyle
|
||||
? `wrapperCustomStyle-${id?.replace('u:', '')}-item`
|
||||
: ''
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
@ -1493,6 +1498,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
id,
|
||||
labelClassName,
|
||||
descriptionClassName,
|
||||
wrapperCustomStyle,
|
||||
env
|
||||
} = this.props;
|
||||
const mode = this.props.mode || formMode;
|
||||
@ -1532,22 +1538,14 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
{
|
||||
key: 'labelClassName',
|
||||
value: labelClassName
|
||||
}
|
||||
],
|
||||
id: id + '-label'
|
||||
}}
|
||||
env={env}
|
||||
/>
|
||||
<CustomStyle
|
||||
config={{
|
||||
themeCss: themeCss || css,
|
||||
classNames: [
|
||||
},
|
||||
{
|
||||
key: 'descriptionClassName',
|
||||
value: descriptionClassName
|
||||
}
|
||||
],
|
||||
id: id + '-description'
|
||||
wrapperCustomStyle,
|
||||
id: id && id + '-item'
|
||||
}}
|
||||
env={env}
|
||||
/>
|
||||
|
@ -635,7 +635,8 @@ export function wrapControl<
|
||||
|
||||
if (pipeOut) {
|
||||
const oldValue = this.model.value;
|
||||
value = callStrFunction(
|
||||
value = callStrFunction.call(
|
||||
this,
|
||||
pipeOut,
|
||||
['value', 'oldValue', 'data'],
|
||||
value,
|
||||
@ -760,7 +761,8 @@ export function wrapControl<
|
||||
} = this.props;
|
||||
|
||||
if (pipeOut) {
|
||||
value = callStrFunction(
|
||||
value = callStrFunction.call(
|
||||
this,
|
||||
pipeOut,
|
||||
['value', 'oldValue', 'data'],
|
||||
value,
|
||||
@ -783,7 +785,8 @@ export function wrapControl<
|
||||
let value: any = this.model ? this.model.tmpValue : control.value;
|
||||
|
||||
if (control.pipeIn) {
|
||||
value = callStrFunction(
|
||||
value = callStrFunction.call(
|
||||
this,
|
||||
control.pipeIn,
|
||||
['value', 'data'],
|
||||
value,
|
||||
|
@ -662,6 +662,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore')
|
||||
return createObject(superData, {
|
||||
total: self.total,
|
||||
page: self.page,
|
||||
perPage: self.perPage,
|
||||
items: self.items.concat(),
|
||||
selectedItems: self.selectedItems.concat(),
|
||||
unSelectedItems: self.unSelectedItems.concat()
|
||||
|
@ -361,7 +361,12 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
inputGroupControl?.name != null &&
|
||||
(self.inputGroupControl = inputGroupControl);
|
||||
|
||||
if (typeof rules !== 'undefined' || self.required) {
|
||||
if (
|
||||
typeof rules !== 'undefined' ||
|
||||
self.required ||
|
||||
typeof minLength === 'number' ||
|
||||
typeof maxLength === 'number'
|
||||
) {
|
||||
rules = {
|
||||
...rules,
|
||||
isRequired: self.required || rules?.isRequired
|
||||
|
@ -4,19 +4,13 @@ import React from 'react';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
|
||||
export type ClassValue =
|
||||
| ClassValue[]
|
||||
| Record<string, any>
|
||||
| string
|
||||
| number
|
||||
| ClassDictionary
|
||||
| ClassArray
|
||||
| undefined
|
||||
| boolean
|
||||
| null
|
||||
| boolean;
|
||||
|
||||
interface ClassDictionary {
|
||||
[id: string]: any;
|
||||
}
|
||||
|
||||
interface ClassArray extends Array<ClassValue> {}
|
||||
| undefined;
|
||||
|
||||
export type ClassNamesFn = (...classes: ClassValue[]) => string;
|
||||
|
||||
@ -29,12 +23,10 @@ interface ThemeConfig {
|
||||
[propName: string]: any;
|
||||
};
|
||||
|
||||
[propsName: string]: any;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
const themes: {
|
||||
[propName: string]: ThemeConfig;
|
||||
} = {
|
||||
const themes: Record<string, ThemeConfig> = {
|
||||
default: {},
|
||||
cxd: {
|
||||
classPrefix: 'cxd-'
|
||||
@ -48,16 +40,15 @@ export function theme(name: string, config: Partial<ThemeConfig>) {
|
||||
};
|
||||
}
|
||||
|
||||
const fns: {
|
||||
[propName: string]: (...classes: ClassValue[]) => string;
|
||||
} = {};
|
||||
const fns: Record<string, (...classes: ClassValue[]) => string> = {};
|
||||
|
||||
export function makeClassnames(ns?: string) {
|
||||
if (ns && fns[ns]) {
|
||||
return fns[ns];
|
||||
}
|
||||
|
||||
const fn = (...classes: ClassValue[]) => {
|
||||
const str = cx(...(classes as any));
|
||||
const str = cx(...classes);
|
||||
return str && ns
|
||||
? str
|
||||
.replace(/(^|\s)([A-Z])/g, '$1' + ns + '$2')
|
||||
@ -69,11 +60,11 @@ export function makeClassnames(ns?: string) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
export type ThemeInstance = ThemeConfig & {
|
||||
export interface ThemeInstance extends ThemeConfig {
|
||||
getRendererConfig: (name?: string) => any;
|
||||
getComponentConfig: (name?: string) => any;
|
||||
classnames: ClassNamesFn;
|
||||
};
|
||||
}
|
||||
|
||||
export function hasTheme(theme: string): boolean {
|
||||
return !!themes[theme];
|
||||
@ -121,26 +112,17 @@ export function getTheme(theme: string): ThemeInstance {
|
||||
}
|
||||
|
||||
export interface ThemeProps {
|
||||
classnames: ClassNamesFn;
|
||||
classPrefix: string;
|
||||
className?: string;
|
||||
theme?: string;
|
||||
mobileUI?: boolean;
|
||||
style?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
classPrefix: string;
|
||||
classnames: ClassNamesFn;
|
||||
theme?: string;
|
||||
mobileUI?: boolean;
|
||||
}
|
||||
|
||||
export interface ThemeOutterProps {
|
||||
theme?: string;
|
||||
className?: string;
|
||||
mobileUI?: boolean;
|
||||
style?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
classPrefix?: string;
|
||||
classnames?: ClassNamesFn;
|
||||
}
|
||||
export interface ThemeOutterProps extends Partial<ThemeProps> {}
|
||||
|
||||
export let defaultTheme: string = 'cxd';
|
||||
export const ThemeContext = React.createContext('');
|
||||
|
@ -1,6 +1,6 @@
|
||||
import omit from 'lodash/omit';
|
||||
import {Api, ApiObject, EventTrack, fetcherResult, Payload} from '../types';
|
||||
import {fetcherConfig} from '../factory';
|
||||
import {FetcherConfig} from '../factory';
|
||||
import {tokenize, dataMapping, escapeHtml} from './tpl-builtin';
|
||||
import {evalExpression} from './tpl';
|
||||
import {
|
||||
@ -94,7 +94,7 @@ export function buildApi(
|
||||
) as any;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!data || api.data instanceof FormData) {
|
||||
return api;
|
||||
} else if (
|
||||
data instanceof FormData ||
|
||||
@ -463,7 +463,7 @@ export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
|
||||
}
|
||||
|
||||
export function wrapFetcher(
|
||||
fn: (config: fetcherConfig) => Promise<fetcherResult>,
|
||||
fn: (config: FetcherConfig) => Promise<fetcherResult>,
|
||||
tracker?: (eventTrack: EventTrack, data: any) => void
|
||||
) {
|
||||
// 避免重复处理
|
||||
@ -597,7 +597,7 @@ export function wrapAdaptor(
|
||||
* @returns
|
||||
*/
|
||||
export function jsFetcher(
|
||||
fetcher: (config: fetcherConfig) => Promise<fetcherResult>,
|
||||
fetcher: (config: FetcherConfig) => Promise<fetcherResult>,
|
||||
api: ApiObject
|
||||
): Promise<fetcherResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -3,7 +3,7 @@
|
||||
*
|
||||
* @param string 要转换的字符串
|
||||
*/
|
||||
export const keyToPath = (string: string) => {
|
||||
export const keyToPath = (string: string = '') => {
|
||||
const result = [];
|
||||
|
||||
if (string.charCodeAt(0) === '.'.charCodeAt(0)) {
|
||||
|
@ -1,6 +1,9 @@
|
||||
import {PlainObject} from '../types';
|
||||
import {uuid} from './helper';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import isObject from 'lodash/isObject';
|
||||
import map from 'lodash/map';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
export const valueMap: PlainObject = {
|
||||
'marginTop': 'margin-top',
|
||||
@ -40,7 +43,7 @@ interface extra {
|
||||
suf?: string;
|
||||
}
|
||||
|
||||
export function findOrCreactStyle(id: string) {
|
||||
export function findOrCreateStyle(id: string) {
|
||||
let varStyleTag = document.getElementById(id);
|
||||
if (!varStyleTag) {
|
||||
varStyleTag = document.createElement('style');
|
||||
@ -51,14 +54,18 @@ export function findOrCreactStyle(id: string) {
|
||||
}
|
||||
|
||||
export function insertStyle(style: string, id: string) {
|
||||
const varStyleTag = findOrCreactStyle(id);
|
||||
const varStyleTag = findOrCreateStyle(id);
|
||||
|
||||
// bca-disable-line
|
||||
varStyleTag.innerHTML = style;
|
||||
|
||||
if (!style) {
|
||||
varStyleTag.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function addStyle(style: string, id: string) {
|
||||
const varStyleTag = findOrCreactStyle(id);
|
||||
const varStyleTag = findOrCreateStyle(id);
|
||||
// bca-disable-line
|
||||
varStyleTag.innerHTML += style;
|
||||
}
|
||||
@ -100,7 +107,7 @@ function handleInheritData(statusMap: any, data: any) {
|
||||
|
||||
export function formatStyle(
|
||||
themeCss: any,
|
||||
classNames: {
|
||||
classNames?: {
|
||||
key: string;
|
||||
value?: string;
|
||||
weights?: {
|
||||
@ -113,7 +120,7 @@ export function formatStyle(
|
||||
id?: string,
|
||||
defaultData?: any
|
||||
) {
|
||||
if (!themeCss) {
|
||||
if (!themeCss || !classNames) {
|
||||
return {value: '', origin: []};
|
||||
}
|
||||
const res = [];
|
||||
@ -135,12 +142,11 @@ export function formatStyle(
|
||||
|
||||
list?.forEach(n => {
|
||||
if (
|
||||
/(\S*[C|c]lassName-\S*)/.test(n) &&
|
||||
n.includes('lassName-') &&
|
||||
!!~n.indexOf(
|
||||
id
|
||||
?.replace('u:', '')
|
||||
.replace('-label', '')
|
||||
.replace('-description', '')
|
||||
.replace('-item', '')
|
||||
.replace('-addOn', '')
|
||||
.replace('-icon', '')
|
||||
.replace('-inner', '') || ''
|
||||
@ -160,7 +166,7 @@ export function formatStyle(
|
||||
disabled: {}
|
||||
};
|
||||
for (let key in body) {
|
||||
if (key === '$$id') {
|
||||
if (key === '$$id' || body[key] === '') {
|
||||
continue;
|
||||
}
|
||||
if (!!~key.indexOf(':default')) {
|
||||
@ -263,8 +269,8 @@ export interface CustomStyleClassName {
|
||||
}
|
||||
|
||||
export function insertCustomStyle(
|
||||
themeCss: any,
|
||||
classNames: CustomStyleClassName[],
|
||||
themeCss?: any,
|
||||
classNames?: CustomStyleClassName[],
|
||||
id?: string,
|
||||
defaultData?: any,
|
||||
customStyleClassPrefix?: string
|
||||
@ -300,3 +306,116 @@ export function getValueByPath(path: string, data: any) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理嵌套的样式,转化成一维对象
|
||||
function traverseStyle(style: any, path: string, result: any) {
|
||||
for (let key in style) {
|
||||
if (style.hasOwnProperty(key)) {
|
||||
if (key === '$$id') {
|
||||
continue;
|
||||
}
|
||||
if (isObject(style[key])) {
|
||||
const nowPath = path ? `${path} ${key}` : key;
|
||||
traverseStyle(style[key], nowPath, result);
|
||||
} else if (path === '') {
|
||||
!result[key] && (result[key] = {});
|
||||
result[key] = style[key];
|
||||
} else {
|
||||
!result[path] && (result[path] = {});
|
||||
result[path][key] = style[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置源码编辑自定义样式
|
||||
*/
|
||||
export function insertEditCustomStyle(customStyle: any, id?: string) {
|
||||
let styles: any = {};
|
||||
traverseStyle(customStyle, '', styles);
|
||||
|
||||
let content = '';
|
||||
if (!isEmpty(styles)) {
|
||||
const className = `wrapperCustomStyle-${id?.replace('u:', '')}`;
|
||||
for (let key in styles) {
|
||||
if (styles.hasOwnProperty(key)) {
|
||||
if (!isObject(styles[key])) {
|
||||
content += `\n.${className} {\n ${key}: ${styles[key]}\n}`;
|
||||
} else if (key === 'root') {
|
||||
const res = map(styles[key], (value, key) => `${key}: ${value};`);
|
||||
content += `\n.${className} {\n ${res.join('\n ')}\n}`;
|
||||
} else if (/^root:/.test(key)) {
|
||||
const res = map(styles[key], (value, key) => `${key}: ${value};`);
|
||||
const nowKey = key.replace('root', '');
|
||||
content += `\n.${className} ${nowKey} {\n ${res.join('\n ')}\n}`;
|
||||
} else {
|
||||
const res = map(styles[key], (value, key) => `${key}: ${value};`);
|
||||
content += `\n.${className} ${key} {\n ${res.join('\n ')}\n}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
insertStyle(
|
||||
content,
|
||||
'wrapperCustomStyle-' + (id?.replace('u:', '') || uuid())
|
||||
);
|
||||
}
|
||||
|
||||
export interface InsertCustomStyle {
|
||||
themeCss?: any;
|
||||
classNames?: CustomStyleClassName[];
|
||||
id?: string;
|
||||
defaultData?: any;
|
||||
customStyleClassPrefix?: string;
|
||||
}
|
||||
|
||||
export class StyleDom {
|
||||
id: string;
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
/**
|
||||
* 插入自定义样式
|
||||
*
|
||||
* @param {InsertCustomStyle} params - 插入自定义样式的参数
|
||||
* @param {string} params.themeCss - 主题样式
|
||||
* @param {string} params.classNames - 自定义样式类名
|
||||
* @param {string} params.defaultData - 默认数据
|
||||
* @param {string} params.customStyleClassPrefix - 自定义样式类名前缀
|
||||
*/
|
||||
insertCustomStyle({
|
||||
themeCss,
|
||||
classNames,
|
||||
defaultData,
|
||||
customStyleClassPrefix
|
||||
}: InsertCustomStyle) {
|
||||
insertCustomStyle(
|
||||
themeCss,
|
||||
classNames,
|
||||
this.id,
|
||||
defaultData,
|
||||
customStyleClassPrefix
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入外层自定义样式
|
||||
*
|
||||
* @param wrapperCustomStyle 自定义样式
|
||||
*/
|
||||
insertEditCustomStyle(wrapperCustomStyle: any) {
|
||||
insertEditCustomStyle(wrapperCustomStyle, this.id);
|
||||
}
|
||||
/**
|
||||
* 移除自定义样式
|
||||
*/
|
||||
removeCustomStyle(type?: string) {
|
||||
const style = document.getElementById(
|
||||
(type ? type + '-' : '') + this.id.replace('u:', '')
|
||||
);
|
||||
if (style) {
|
||||
style.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-editor-core",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2-alpha.0",
|
||||
"description": "amis 可视化编辑器",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -4,3 +4,28 @@
|
||||
border: 1px solid rgba(#23b7e5, 1);
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
.ae-Editor-list {
|
||||
.ae-Editor-listItem,
|
||||
.ae-Editor-eachItem {
|
||||
position: relative !important;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
background: rgba(22, 40, 60, 0.2) url(../static/indication.png) repeat;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ae-Editor-eachItem:first-child::after,
|
||||
.cards-items > div:first-child > div::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
&-content {
|
||||
@include flexBox();
|
||||
align-items: stretch;
|
||||
|
||||
.ae-ApiControl-input {
|
||||
background: var(--Form-input-bg);
|
||||
@ -44,6 +45,10 @@
|
||||
width: 100%;
|
||||
height: calc(var(--Form-input-lineHeight) * var(--Form-input-fontSize));
|
||||
}
|
||||
|
||||
.ae-ApiControl-setting-button {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
163
packages/amis-editor-core/scss/control/_crud2-control.scss
Normal file
163
packages/amis-editor-core/scss/control/_crud2-control.scss
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @file crud2-control.scss
|
||||
* @desc CRUD2相关控件及样式
|
||||
*/
|
||||
|
||||
.ae-CRUDConfigControl {
|
||||
margin-bottom: var(--Form-item-gap);
|
||||
|
||||
&-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e9eb;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
padding: 0 var(--gap-sm);
|
||||
|
||||
&.is-draggable:hover {
|
||||
background-color: #f9f9f9;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
&-dragger {
|
||||
cursor: move;
|
||||
margin: 0 var(--gap-sm) 0 0;
|
||||
color: rgba(232, 233, 235, 1);
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
max-width: 140px;
|
||||
|
||||
& > span {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
color: #151b26;
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& > button {
|
||||
color: #151b26;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
width: px2rem(16px);
|
||||
height: px2rem(16px);
|
||||
|
||||
&.icon-share-link {
|
||||
width: px2rem(14px);
|
||||
height: px2rem(14px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tag {
|
||||
cursor: auto;
|
||||
background-color: transparent;
|
||||
border: 1px solid #2468f2;
|
||||
color: #2468f2;
|
||||
border-radius: 2px;
|
||||
line-height: #{px2rem(18px)};
|
||||
height: #{px2rem(20px)};
|
||||
margin-right: #{px2rem(8px)};
|
||||
scale: 0.9;
|
||||
max-width: 80px;
|
||||
|
||||
&--cascading {
|
||||
color: #531dab;
|
||||
border-color: #531dab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
color: #b4b6ba;
|
||||
padding-top: px2rem(10px);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& > span:nth-child(1) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&-switch {
|
||||
margin-right: var(--gap-sm);
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 4px;
|
||||
background-color: #dfdfdf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
/* margin-bottom: var(--Form-mode-default-labelGap); */
|
||||
|
||||
& > button {
|
||||
color: #4c5664;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-weight: bold;
|
||||
|
||||
& > svg {
|
||||
width: px2rem(14px);
|
||||
height: px2rem(14px);
|
||||
margin-right: var(--gap-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
flex-flow: row-reverse nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ae-CRUDConfigControl-modal {
|
||||
&-btn-loading {
|
||||
--Spinner-color: #fff;
|
||||
}
|
||||
}
|
@ -8,13 +8,23 @@
|
||||
display: flex;
|
||||
height: 30px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
:not(:last-child) {
|
||||
margin-right: px2rem(8px);
|
||||
}
|
||||
|
||||
&-go {
|
||||
&-content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
& > .ae-FeatureControlItem-go {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
@ -42,6 +52,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-dragBar {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 3px;
|
||||
cursor: move;
|
||||
|
||||
& > svg {
|
||||
fill: #e7e7e7;
|
||||
color: #e7e7e7;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-dragBar {
|
||||
& > svg {
|
||||
color: transparent;
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-action {
|
||||
@ -55,24 +86,9 @@
|
||||
}
|
||||
|
||||
&--menus {
|
||||
width: calc(100% - 12px);
|
||||
margin-left: 6px;
|
||||
/* width: calc(100% - 12px);
|
||||
margin-left: 6px; */
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-dragBar {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover &-dragBar {
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
@ -15,9 +15,9 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba($color: #000000, $alpha: 0.4);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background-color: rgba($color: #000000, $alpha: 0.55);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file table-column-width-control.scss
|
||||
* @desc 表格列宽控件
|
||||
*/
|
||||
|
||||
.ae-columnWidthControl {
|
||||
&-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
@ -73,6 +73,11 @@
|
||||
> li {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-loading {
|
||||
display: flex;
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
&--fullscreen {
|
||||
> a {
|
||||
|
@ -41,6 +41,8 @@
|
||||
@import './control/_status';
|
||||
@import './control/_icon-button-group-control';
|
||||
@import './control/_flex-setting-control';
|
||||
@import './control/table-column-width-control.scss';
|
||||
@import './control/crud2-control';
|
||||
|
||||
/* 样式控件 */
|
||||
@import './style-control/box-model';
|
||||
@ -1233,7 +1235,7 @@
|
||||
|
||||
[data-region] {
|
||||
position: relative;
|
||||
min-height: 34px;
|
||||
min-height: 10px;
|
||||
|
||||
&:empty {
|
||||
min-width: 20px;
|
||||
@ -1768,15 +1770,27 @@ div.ae-DragImage {
|
||||
width: px2rem(700px);
|
||||
@include panel-sm-content();
|
||||
|
||||
--radio-default-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Table-thead-fontSize: #{$Editor-right-panel-font-size};
|
||||
--button-size-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--checkbox-checkbox-default-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-active-fontSize: #{$Editor-right-panel-font-size};
|
||||
--Tabs--vertical-hover-fontSize: #{$Editor-right-panel-font-size};
|
||||
|
||||
.ae-Steps {
|
||||
margin: auto;
|
||||
max-width: px2rem(350px);
|
||||
--Steps-title-fontsize: #{px2rem(14px)};
|
||||
|
||||
&-Icon {
|
||||
display: flex !important;
|
||||
width: px2rem(22px) !important;
|
||||
height: px2rem(22px) !important;
|
||||
margin-top: px2rem(5px);
|
||||
font-size: px2rem(12px) !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,17 @@
|
||||
.ThemeCssCode {
|
||||
position: relative;
|
||||
&-button {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
&-icon {
|
||||
cursor: pointer;
|
||||
color: #5c5f66;
|
||||
&:hover {
|
||||
color: #151b26;
|
||||
}
|
||||
}
|
||||
.is-group {
|
||||
overflow: auto;
|
||||
@ -68,6 +78,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&-wrap {
|
||||
border: 1px solid #e8e9eb;
|
||||
.cxd-MonacoEditor-placeholder {
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editorPanel-inner {
|
||||
.Theme-FontEditor {
|
||||
|
@ -1,602 +0,0 @@
|
||||
/**
|
||||
* API数据源处理器
|
||||
*/
|
||||
|
||||
import {toast} from 'amis';
|
||||
import {
|
||||
DSBuilder,
|
||||
DSFeature,
|
||||
DSFeatureType,
|
||||
DSGrain,
|
||||
registerDSBuilder
|
||||
} from './DSBuilder';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {getEnv} from 'mobx-state-tree';
|
||||
import type {ButtonSchema} from 'amis';
|
||||
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
|
||||
|
||||
import type {DSSourceSettingFormConfig} from './DSBuilder';
|
||||
import {getSchemaTpl, tipedLabel} from '../tpl';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
|
||||
class APIBuilder extends DSBuilder {
|
||||
static type = 'api';
|
||||
|
||||
static accessable = (controlType: string, propKey: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
name = '接口';
|
||||
|
||||
order = 0;
|
||||
|
||||
public match = (value: any, schema?: SchemaObject) => {
|
||||
// https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api
|
||||
if (
|
||||
(typeof value === 'string' &&
|
||||
/^(get|post|put|delete|option):/.test(value)) ||
|
||||
(typeof value === 'object' && value.url)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public features: Array<DSFeatureType> = [
|
||||
'List',
|
||||
'Insert',
|
||||
'View',
|
||||
'Edit',
|
||||
'Delete',
|
||||
'BulkEdit',
|
||||
'BulkDelete',
|
||||
'Import',
|
||||
'Export',
|
||||
'SimpleQuery',
|
||||
'FuzzyQuery'
|
||||
];
|
||||
|
||||
public makeSourceSettingForm(
|
||||
config: DSSourceSettingFormConfig
|
||||
): SchemaObject[] {
|
||||
let {name, label, feat, inCrud, inScaffold} = config;
|
||||
|
||||
if (['Import', 'Export', 'SimpleQuery', 'FuzzyQuery'].includes(feat)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
label =
|
||||
label ??
|
||||
(inCrud && feat !== 'List' ? DSFeature[feat].label + '接口' : '接口');
|
||||
name = name ?? (inScaffold ? DSFeature[feat].value + 'Api' : 'api');
|
||||
|
||||
let sampleBuilder = null;
|
||||
let apiDesc = null;
|
||||
switch (feat) {
|
||||
case 'Insert':
|
||||
(label as any) = tipedLabel(
|
||||
label,
|
||||
`用来保存数据, 表单提交后将数据传入此接口。 <br/>
|
||||
接口响应体要求(如果data中有数据,该数据将被合并到表单上下文中):<br/>
|
||||
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
|
||||
);
|
||||
break;
|
||||
|
||||
case 'List':
|
||||
(label as any) = tipedLabel(
|
||||
label,
|
||||
`接口响应体要求:<br/>
|
||||
${JSON.stringify(
|
||||
{status: 0, msg: '', items: {}, page: 0, total: 0},
|
||||
null,
|
||||
'<br/>'
|
||||
)}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return [
|
||||
getSchemaTpl('apiControl', {
|
||||
label,
|
||||
name,
|
||||
sampleBuilder,
|
||||
apiDesc
|
||||
})
|
||||
]
|
||||
.concat(
|
||||
feat === 'Edit' && !inCrud
|
||||
? getSchemaTpl('apiControl', {
|
||||
label: tipedLabel(
|
||||
'初始化接口',
|
||||
`接口响应体要求:<br/>
|
||||
${JSON.stringify({status: 0, msg: '', data: {}}, null, '<br/>')}`
|
||||
),
|
||||
name: 'initApi'
|
||||
})
|
||||
: null
|
||||
)
|
||||
.concat(
|
||||
feat === 'List' && inCrud && inScaffold
|
||||
? this.makeFieldsSettingForm({
|
||||
feat,
|
||||
setting: true
|
||||
})
|
||||
: null
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
public async getContextFileds(config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
}) {
|
||||
return config.schema.__fields;
|
||||
}
|
||||
|
||||
public async getAvailableContextFileds(
|
||||
config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
},
|
||||
target: EditorNodeType
|
||||
) {
|
||||
// API类目前没有,增加API中心的出参入参后,可以在这里提供绑定字段
|
||||
// return {
|
||||
// type: 'ae-SimpleDataBindingPanel',
|
||||
// fields: [
|
||||
// {
|
||||
// label: '可用字段',
|
||||
// children: [
|
||||
// {label: '名称', value: 'name'},
|
||||
// {label: '年级', value: 'grade'}
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// } as any;
|
||||
}
|
||||
|
||||
onFieldsInit(value: any, form: any) {
|
||||
this.features.forEach(feat => {
|
||||
const key = `${DSFeature[feat].value}Fields`;
|
||||
const currentData = form.getValueByName(key);
|
||||
|
||||
const result = cloneDeep(value || []).map((field: any) => {
|
||||
const exist = currentData?.find((f: any) => f.name === field.name);
|
||||
|
||||
return {
|
||||
...field,
|
||||
checked: exist ? exist.checked : true
|
||||
};
|
||||
});
|
||||
form.setValueByName(key, result);
|
||||
});
|
||||
}
|
||||
|
||||
public makeFieldsSettingForm(config: {
|
||||
sourceKey?: string;
|
||||
feat: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
setting?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}) {
|
||||
let {sourceKey, feat, inCrud, setting, inScaffold} = config;
|
||||
if (
|
||||
inScaffold === false ||
|
||||
['Import', 'Export', 'FuzzyQuery'].includes(feat)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
sourceKey = sourceKey ?? `${DSFeature[feat].value}Api`;
|
||||
const key = setting ? '__fields' : `${DSFeature[feat].value}Fields`;
|
||||
const hasInputType =
|
||||
['Edit', 'Insert'].includes(feat) || (inCrud && feat === 'List');
|
||||
const hasType = ['View', 'List'].includes(feat);
|
||||
|
||||
return ([] as any)
|
||||
.concat(
|
||||
inCrud && feat !== 'List'
|
||||
? this.makeSourceSettingForm({
|
||||
feat,
|
||||
inScaffold,
|
||||
inCrud
|
||||
})
|
||||
: null
|
||||
)
|
||||
.concat([
|
||||
{
|
||||
type: 'combo',
|
||||
className: 'mb-0 ae-Fields-Setting',
|
||||
joinValues: false,
|
||||
name: key,
|
||||
label: inCrud ? `${DSFeature[feat].label}字段` : '字段',
|
||||
multiple: true,
|
||||
draggable: true,
|
||||
addable: false,
|
||||
removable: false,
|
||||
itemClassName: 'ae-Fields-Setting-Item',
|
||||
// CRUD的脚手架面板,基于现有字段进行选择
|
||||
hidden: setting || !inCrud || ['Delete', 'BulkDelete'].includes(feat),
|
||||
items: {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
name: 'checked',
|
||||
label: false,
|
||||
mode: 'inline',
|
||||
className: 'm-0 ml-1',
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
className: 'ae-Fields-Setting-Item-label',
|
||||
tpl: '${label}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input-table',
|
||||
label: '字段',
|
||||
className: 'mb-0',
|
||||
name: key,
|
||||
// 非crud,都是定义字段的模式,只有crud,有统一定义字段,因此是选择字段
|
||||
visible: setting ?? !inCrud,
|
||||
removable: true,
|
||||
columnsTogglable: false,
|
||||
needConfirm: false,
|
||||
onChange: (value: any, oldValue: any, model: any, form: any) =>
|
||||
this.onFieldsInit(value, form),
|
||||
columns: [
|
||||
{
|
||||
type: 'switch',
|
||||
name: 'checked',
|
||||
value: true,
|
||||
label: '隐藏,默认选中',
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'label',
|
||||
label: '标题'
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'name',
|
||||
label: '绑定字段'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
label: '类型',
|
||||
visible: hasType,
|
||||
value: 'tpl',
|
||||
options: [
|
||||
{
|
||||
value: 'tpl',
|
||||
label: '文本',
|
||||
typeKey: 'tpl'
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: '图片',
|
||||
typeKey: 'src'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: '日期',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'progress',
|
||||
label: '进度',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'mapping',
|
||||
label: '映射',
|
||||
typeKey: 'value'
|
||||
}
|
||||
],
|
||||
autoFill: {
|
||||
typeKey: '${typeKey}'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'inputType',
|
||||
label: '输入类型',
|
||||
visible: hasInputType,
|
||||
value: 'input-text',
|
||||
options: [
|
||||
{
|
||||
label: '输入框',
|
||||
value: 'input-text'
|
||||
},
|
||||
{
|
||||
label: '多行文本',
|
||||
value: 'textarea'
|
||||
},
|
||||
{
|
||||
label: '数字输入',
|
||||
value: 'input-number'
|
||||
},
|
||||
{
|
||||
label: '单选框',
|
||||
value: 'radios'
|
||||
},
|
||||
{
|
||||
label: '勾选框',
|
||||
value: 'checkbox'
|
||||
},
|
||||
{
|
||||
label: '复选框',
|
||||
value: 'checkboxes'
|
||||
},
|
||||
{
|
||||
label: '下拉框',
|
||||
value: 'select'
|
||||
},
|
||||
{
|
||||
label: '开关',
|
||||
value: 'switch'
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
value: 'input-date'
|
||||
},
|
||||
{
|
||||
label: '表格',
|
||||
value: 'input-table'
|
||||
},
|
||||
{
|
||||
label: '文件上传',
|
||||
value: 'input-file'
|
||||
},
|
||||
{
|
||||
label: '图片上传',
|
||||
value: 'input-image'
|
||||
},
|
||||
{
|
||||
label: '富文本编辑器',
|
||||
value: 'input-rich-text'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
visible: setting ?? !inCrud,
|
||||
label: '',
|
||||
body: [
|
||||
{
|
||||
type: 'grid',
|
||||
columns: [
|
||||
{
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '添加字段',
|
||||
target: key,
|
||||
className: 'ae-Button--link',
|
||||
level: 'link',
|
||||
icon: 'plus',
|
||||
actionType: 'add'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
columnClassName: 'text-right',
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '基于接口自动生成字段',
|
||||
visible: feat === 'Edit' || feat === 'List',
|
||||
className: 'ae-Button--link',
|
||||
level: 'link',
|
||||
// className: 'm-t-xs m-b-xs',
|
||||
// 列表 或者 不在CRUD中的查看接口等
|
||||
onClick: async (e: Event, props: any) => {
|
||||
const data = props.data;
|
||||
const schemaFilter = getEnv(
|
||||
(window as any).editorStore
|
||||
).schemaFilter;
|
||||
const apiKey =
|
||||
feat === 'Edit' && !inCrud ? 'initApi' : sourceKey;
|
||||
let api: any = data[apiKey!];
|
||||
// 主要是给爱速搭中替换 url
|
||||
if (schemaFilter) {
|
||||
api = schemaFilter({
|
||||
api
|
||||
}).api;
|
||||
}
|
||||
if (!api) {
|
||||
toast.warning('请先填写接口');
|
||||
}
|
||||
|
||||
const result = await props.env.fetcher(api, data);
|
||||
|
||||
let autoFillKeyValues: Array<any> = [];
|
||||
let itemExample;
|
||||
if (feat === 'List') {
|
||||
const items = result.data?.rows || result.data?.items;
|
||||
itemExample = items?.[0];
|
||||
} else {
|
||||
itemExample = result.data;
|
||||
}
|
||||
|
||||
if (itemExample) {
|
||||
Object.entries(itemExample).forEach(
|
||||
([key, value]) => {
|
||||
autoFillKeyValues.push({
|
||||
label: key,
|
||||
type: 'tpl',
|
||||
inputType:
|
||||
typeof value === 'number'
|
||||
? 'input-number'
|
||||
: 'input-text',
|
||||
name: key
|
||||
});
|
||||
}
|
||||
);
|
||||
props.formStore.setValues({
|
||||
[key]: autoFillKeyValues
|
||||
});
|
||||
this.onFieldsInit(autoFillKeyValues, props.formStore);
|
||||
} else {
|
||||
toast.warning(
|
||||
'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]) as SchemaObject[];
|
||||
}
|
||||
|
||||
public async makeFieldFilterSetting(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey: string;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public resolveSourceSchema(config: {
|
||||
schema: SchemaObject;
|
||||
setting: any;
|
||||
name?: string;
|
||||
feat?: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
}): void {
|
||||
let {name, setting, schema, feat} = config;
|
||||
name = name ?? 'api';
|
||||
// @ts-ignore
|
||||
schema[name] = setting[feat ? `${DSFeature[feat].value}Api` : 'api'];
|
||||
|
||||
// form中需要初始化接口和编辑接口
|
||||
if (feat === 'Edit') {
|
||||
(schema as FormSchema).initApi = setting.initApi;
|
||||
}
|
||||
}
|
||||
|
||||
public resolveViewSchema(config: {
|
||||
setting: any;
|
||||
feat?: DSFeatureType;
|
||||
}): SchemaObject[] {
|
||||
let {setting, feat = 'Edit'} = config;
|
||||
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
|
||||
return fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.type,
|
||||
[field.typeKey || 'value']: '${' + field.name + '}'
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveTableSchema(config: {schema: any; setting: any}): void {
|
||||
let {schema, setting} = config;
|
||||
const fields = setting.listFields.filter((i: any) => i.checked) || [];
|
||||
schema.columns = this.makeTableColumnsByFields(fields);
|
||||
}
|
||||
|
||||
public makeTableColumnsByFields(fields: any[]) {
|
||||
return fields.map((field: any) => ({
|
||||
type: field.type,
|
||||
title: field.label,
|
||||
name: field.name,
|
||||
[field.typeKey || 'value']: '${' + field.name + '}'
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveCreateSchema(config: {
|
||||
schema: FormSchema;
|
||||
setting: any;
|
||||
feat: 'Insert' | 'Edit' | 'BulkEdit';
|
||||
name?: string;
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}): void {
|
||||
let {schema, setting, feat, name} = config;
|
||||
const fields = setting[`${DSFeature[feat].value}Fields`] || [];
|
||||
// @ts-ignore
|
||||
schema[name ?? 'api'] = setting[DSFeature[feat].value + 'Api'];
|
||||
schema.initApi = setting['initApi'];
|
||||
schema.body = fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.inputType,
|
||||
name: field.name,
|
||||
label: field.label
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveDeleteSchema(config: {
|
||||
schema: ButtonSchema;
|
||||
setting: any;
|
||||
feat: 'BulkDelete' | 'Delete';
|
||||
name?: string | undefined;
|
||||
}) {
|
||||
const {schema, setting, feat} = config;
|
||||
schema.onEvent = Object.assign(schema.onEvent ?? {}, {
|
||||
click: {
|
||||
actions: []
|
||||
}
|
||||
});
|
||||
|
||||
const api = {
|
||||
...(setting[`${DSFeature[feat].value}Api`] || {})
|
||||
};
|
||||
if (feat === 'Delete') {
|
||||
api.data = {
|
||||
id: '${item.id}'
|
||||
};
|
||||
} else {
|
||||
api.data = {
|
||||
ids: '${ARRAYMAP(selectedItems, item=> item.id)}'
|
||||
};
|
||||
}
|
||||
|
||||
schema.onEvent.click.actions.push({
|
||||
actionType: 'ajax',
|
||||
args: {api}
|
||||
});
|
||||
}
|
||||
|
||||
public resolveSimpleFilterSchema(config: {
|
||||
setting: any;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
|
||||
}) {
|
||||
const {setting, size} = config;
|
||||
const fields = setting.simpleQueryFields || [];
|
||||
return fields
|
||||
.filter((i: any) => i.checked)
|
||||
.map((field: any) => ({
|
||||
type: field.inputType,
|
||||
name: field.name,
|
||||
label: field.label,
|
||||
size
|
||||
}));
|
||||
}
|
||||
|
||||
public resolveAdvancedFilterSchema(config: {setting: any}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
registerDSBuilder(APIBuilder);
|
@ -1,381 +0,0 @@
|
||||
/**
|
||||
* 数据源构造器,可用于对接当前amis中的扩展数据源
|
||||
*/
|
||||
|
||||
import type {ButtonSchema} from 'amis';
|
||||
import type {CRUD2Schema} from 'amis';
|
||||
import type {FormSchema, SchemaCollection, SchemaObject} from 'amis';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
|
||||
/**
|
||||
* 数据源所需操作,目前是因为schema从后端来
|
||||
*/
|
||||
export enum DSBehavior {
|
||||
create = 'create',
|
||||
view = 'view',
|
||||
update = 'update',
|
||||
table = 'table',
|
||||
filter = 'filter'
|
||||
}
|
||||
|
||||
export interface DSField {
|
||||
value: string;
|
||||
label: string;
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
export interface DSFieldGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
children: DSField[];
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持数据源配置的一些属性名
|
||||
*/
|
||||
export enum DSGrain {
|
||||
entity = 'entity',
|
||||
list = 'list',
|
||||
piece = 'piece'
|
||||
}
|
||||
|
||||
export const DSFeature = {
|
||||
List: {
|
||||
value: 'list',
|
||||
label: '列表'
|
||||
},
|
||||
Insert: {
|
||||
value: 'insert',
|
||||
label: '新增'
|
||||
},
|
||||
View: {
|
||||
value: 'view',
|
||||
label: '详情'
|
||||
},
|
||||
Edit: {
|
||||
value: 'edit',
|
||||
label: '编辑'
|
||||
},
|
||||
Delete: {
|
||||
value: 'delete',
|
||||
label: '删除'
|
||||
},
|
||||
BulkEdit: {
|
||||
value: 'bulkEdit',
|
||||
label: '批量编辑'
|
||||
},
|
||||
BulkDelete: {
|
||||
value: 'bulkDelete',
|
||||
label: '批量删除'
|
||||
},
|
||||
Import: {
|
||||
value: 'import',
|
||||
label: '导入'
|
||||
},
|
||||
Export: {
|
||||
value: 'export',
|
||||
label: '导出'
|
||||
},
|
||||
SimpleQuery: {
|
||||
value: 'simpleQuery',
|
||||
label: '简单查询'
|
||||
},
|
||||
FuzzyQuery: {
|
||||
value: 'fuzzyQuery',
|
||||
label: '模糊查询'
|
||||
},
|
||||
AdvancedQuery: {
|
||||
value: 'advancedQuery',
|
||||
label: '高级查询'
|
||||
}
|
||||
};
|
||||
|
||||
export type DSFeatureType = keyof typeof DSFeature;
|
||||
|
||||
export interface DSSourceSettingFormConfig {
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 数据源字段标题 */
|
||||
label?: string;
|
||||
/** 所需要配置的数据粒度 */
|
||||
grain?: DSGrain;
|
||||
/** 数据源所被使用的功能场景 */
|
||||
feat: DSFeatureType;
|
||||
/** 渲染器类型 */
|
||||
renderer?: string;
|
||||
/**
|
||||
* @deprecated 待废弃,使用renderer字段代替
|
||||
* 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置
|
||||
* */
|
||||
inCrud?: boolean;
|
||||
/** 是否在脚手架中 */
|
||||
inScaffold?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源选择构造器
|
||||
*/
|
||||
export abstract class DSBuilder {
|
||||
/**
|
||||
* 数据源名字,中文,可以覆盖同名
|
||||
*/
|
||||
public static type: string;
|
||||
|
||||
public name: string;
|
||||
|
||||
// 数字越小排序越靠前
|
||||
public order: number;
|
||||
|
||||
/**
|
||||
* 数据源schema运行前转换
|
||||
*/
|
||||
public static schemaFilter?: (schema: any) => any;
|
||||
|
||||
/**
|
||||
* 根据组件、属性名判断是否可以使用这个数据源
|
||||
*/
|
||||
public static accessable: (controlType: string, propKey: string) => boolean;
|
||||
|
||||
public features: Array<keyof typeof DSFeature>;
|
||||
|
||||
/**
|
||||
* 根据值内容和schema配置状态,看是否是当前数据源
|
||||
*/
|
||||
public abstract match(value: any, schema?: SchemaObject): boolean;
|
||||
|
||||
/**
|
||||
* 生成数据源的配置表单
|
||||
*/
|
||||
public abstract makeSourceSettingForm(
|
||||
config: DSSourceSettingFormConfig
|
||||
): SchemaObject[];
|
||||
|
||||
public abstract makeFieldsSettingForm(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey?: string;
|
||||
feat: DSFeatureType;
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
/** 初次设置字段还是选择字段 */
|
||||
setting?: boolean;
|
||||
}): SchemaObject[];
|
||||
|
||||
/**
|
||||
* 生成字段的筛选配置表单
|
||||
*/
|
||||
public abstract makeFieldFilterSetting(config: {
|
||||
/** 数据源字段名 */
|
||||
sourceKey: string;
|
||||
schema: any;
|
||||
fieldName: string;
|
||||
}): Promise<SchemaObject[]>;
|
||||
|
||||
/**
|
||||
* 数据源schema生成
|
||||
*/
|
||||
abstract resolveSourceSchema(config: {
|
||||
/** schema */
|
||||
schema: SchemaObject;
|
||||
/** 数据源配置结果 */
|
||||
setting: any;
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
feat?: DSFeatureType;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
inScaffold?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 数据删除schema生成
|
||||
*/
|
||||
abstract resolveDeleteSchema(config: {
|
||||
schema: ButtonSchema;
|
||||
setting: any;
|
||||
feat: 'BulkDelete' | 'Delete';
|
||||
name?: string;
|
||||
}): any;
|
||||
|
||||
/**
|
||||
* 生成数据创建表单schema
|
||||
*/
|
||||
abstract resolveCreateSchema(config: {
|
||||
/** schema */
|
||||
schema: FormSchema;
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
feat: 'Insert' | 'Edit' | 'BulkEdit';
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 生成数据表格列
|
||||
*/
|
||||
abstract resolveTableSchema(config: {
|
||||
/** schema */
|
||||
schema: CRUD2Schema;
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
/** 数据源字段名 */
|
||||
name?: string;
|
||||
/** 是否是在CRUD场景下,有的数据源在CRUD中可以统一设置 */
|
||||
inCrud?: boolean;
|
||||
}): void;
|
||||
|
||||
/**
|
||||
* 生成数据表格列
|
||||
*/
|
||||
abstract resolveViewSchema(config: {
|
||||
/** 脚手架配置数据 */
|
||||
setting: any;
|
||||
feat?: DSFeatureType;
|
||||
}): SchemaObject[];
|
||||
|
||||
abstract resolveSimpleFilterSchema(config: {
|
||||
setting: any;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
|
||||
}): SchemaObject[];
|
||||
|
||||
abstract resolveAdvancedFilterSchema(config: {
|
||||
setting: any;
|
||||
}): SchemaObject | void;
|
||||
|
||||
abstract makeTableColumnsByFields(fields: any[]): SchemaObject[];
|
||||
|
||||
/**
|
||||
* 当前上下文中使用的字段
|
||||
*/
|
||||
abstract getContextFileds(config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
}): Promise<DSField[] | void>;
|
||||
|
||||
/**
|
||||
* 上下文可以使用的字段
|
||||
*/
|
||||
abstract getAvailableContextFileds(
|
||||
config: {
|
||||
schema: any;
|
||||
sourceKey: string;
|
||||
feat: DSFeatureType;
|
||||
scopeNode?: EditorNodeType;
|
||||
},
|
||||
target: EditorNodeType
|
||||
): Promise<SchemaCollection | void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有的数据源构造器
|
||||
*/
|
||||
const __builders: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
export const registerDSBuilder = (builderKClass: any) => {
|
||||
__builders[builderKClass.type] = builderKClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造器管理工具,便于更好的缓存
|
||||
*/
|
||||
export class DSBuilderManager {
|
||||
/** 所有可用的数据源构造器实例 */
|
||||
builders: {
|
||||
[key: string]: DSBuilder;
|
||||
} = {};
|
||||
|
||||
get builderNum() {
|
||||
return Object.keys(this.builders).length;
|
||||
}
|
||||
|
||||
constructor(type: string, propKey: string) {
|
||||
Object.values(__builders)
|
||||
.filter(builder => builder.accessable?.(type, propKey) ?? true)
|
||||
.forEach(Builder => {
|
||||
this.builders[Builder.type] = new Builder();
|
||||
});
|
||||
}
|
||||
|
||||
resolveBuilderBySetting(setting: any) {
|
||||
return this.builders[setting.dsType] || Object.values(this.builders)[0];
|
||||
}
|
||||
|
||||
resolveBuilderBySchema(schema: any, propKey: string) {
|
||||
const builders = Object.values(this.builders);
|
||||
return (
|
||||
builders.find(builder => builder.match(schema[propKey])) || builders[0]
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultBuilderName() {
|
||||
// 先返回第一个,之后可以加一些order之类的
|
||||
const builderOptions = Object.entries(this.builders)
|
||||
.map(([key, builder]) => {
|
||||
return {
|
||||
value: key,
|
||||
order: builder.order
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
return builderOptions[0].value;
|
||||
}
|
||||
|
||||
getDSSwitch(setting: any = {}) {
|
||||
const multiSource = this.builderNum > 1;
|
||||
const builderOptions = Object.entries(this.builders).map(
|
||||
([key, builder]) => ({
|
||||
label: builder.name,
|
||||
value: key,
|
||||
order: builder.order
|
||||
})
|
||||
);
|
||||
builderOptions.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
return {
|
||||
type: 'radios',
|
||||
label: '数据来源',
|
||||
name: 'dsType',
|
||||
visible: multiSource,
|
||||
selectFirst: true,
|
||||
options: builderOptions,
|
||||
...setting
|
||||
};
|
||||
}
|
||||
|
||||
// getDSSwitchFormForPanel(
|
||||
// propKey: string,
|
||||
// label: string
|
||||
// ) {
|
||||
// return Object.keys(this.builders).length > 1 ? {
|
||||
// type: Object.keys(this.builders).length > 3 ? 'select' : 'button-group-select',
|
||||
// options: Object.keys(this.builders).map(name => ({
|
||||
// label: name,
|
||||
// value: name
|
||||
// })),
|
||||
// name: propKey,
|
||||
// label: label,
|
||||
// pipeIn: (value: string) => {
|
||||
// const builders = Object.entries(this.builders);
|
||||
// return (builders.find(([, builder]) => {
|
||||
// return builder.match(value);
|
||||
// }) || builders[0])[0];
|
||||
// },
|
||||
// pipeOut: (value: string) => {
|
||||
// return this.builders[value].defaultSchema || {};
|
||||
// }
|
||||
// } : null;
|
||||
// }
|
||||
|
||||
collectFromBuilders(
|
||||
callee: (builder: DSBuilder, builderName: string) => any
|
||||
) {
|
||||
return Object.entries(this.builders).map(([name, builder]) => {
|
||||
return callee(builder, name);
|
||||
});
|
||||
}
|
||||
}
|
@ -39,6 +39,8 @@ export interface EditorProps extends PluginEventListener {
|
||||
amisDocHost?: string;
|
||||
superEditorData?: any;
|
||||
withSuperDataSchema?: boolean;
|
||||
/** 当前 Editor 为 SubEditor 时触发的宿主节点 */
|
||||
hostNode?: EditorNodeType;
|
||||
dataBindingChange?: (
|
||||
value: string,
|
||||
data: any,
|
||||
@ -49,7 +51,7 @@ export interface EditorProps extends PluginEventListener {
|
||||
* Preview 预览前可以修改配置。
|
||||
* 比如把api地址替换成 proxy 地址。
|
||||
*/
|
||||
schemaFilter?: (schema: any, preview?: boolean) => any;
|
||||
schemaFilter?: (schema: any, isPreview?: boolean) => any;
|
||||
amisEnv?: RenderOptions;
|
||||
|
||||
/**
|
||||
@ -126,6 +128,8 @@ export interface EditorProps extends PluginEventListener {
|
||||
) => Promise<void | boolean>;
|
||||
|
||||
getHostNodeDataSchema?: () => Promise<any>;
|
||||
|
||||
getAvaiableContextFields?: (node: EditorNodeType) => Promise<any>;
|
||||
}
|
||||
|
||||
export default class Editor extends Component<EditorProps> {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {RendererProps} from 'amis-core';
|
||||
import {RendererProps, isObject} from 'amis-core';
|
||||
import {observer} from 'mobx-react';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import merge from 'lodash/merge';
|
||||
import {RendererInfo} from '../plugin';
|
||||
import {EditorNodeType} from '../store/node';
|
||||
import {autobind} from '../util';
|
||||
import {autobind, isEmpty} from '../util';
|
||||
|
||||
export interface NodeWrapperProps extends RendererProps {
|
||||
$$editor: RendererInfo; // 当前节点信息(info)
|
||||
@ -71,6 +72,14 @@ export class NodeWrapper extends React.Component<NodeWrapperProps> {
|
||||
rest = $$editor.filterProps.call($$editor.plugin, rest, $$node);
|
||||
}
|
||||
|
||||
// 自动合并假数据
|
||||
if (
|
||||
isObject(rest.editorSetting?.mock) &&
|
||||
!isEmpty(rest.editorSetting.mock)
|
||||
) {
|
||||
rest = merge({}, rest, rest.editorSetting.mock);
|
||||
}
|
||||
|
||||
if ($$editor.renderRenderer) {
|
||||
return $$editor.renderRenderer.call(
|
||||
$$editor.plugin,
|
||||
|
@ -10,7 +10,7 @@ import {EditorNodeContext, EditorNodeType} from '../store/node';
|
||||
export interface RegionWrapperProps {
|
||||
name: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
placeholder?: string | JSX.Element;
|
||||
preferTag?: string;
|
||||
wrapperResolve?: (dom: HTMLElement) => HTMLElement;
|
||||
editorStore: EditorStoreType;
|
||||
|
@ -45,7 +45,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
body = [
|
||||
{
|
||||
type: 'steps',
|
||||
name: '__steps',
|
||||
name: '__step',
|
||||
className: 'ae-Steps',
|
||||
steps: body.map((step, index) => ({
|
||||
title: step.title,
|
||||
@ -133,7 +133,6 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
|
||||
@autobind
|
||||
goToNextStep() {
|
||||
// 不能更新props的data,控制amis不重新渲染,否则数据会重新初始化
|
||||
const store = this.props.store;
|
||||
const form = this.amisScope?.getComponents()[0].props.store;
|
||||
const step = store.scaffoldFormStep + 1;
|
||||
@ -178,13 +177,14 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
true
|
||||
);
|
||||
|
||||
this.handleConfirm([values]);
|
||||
await this.handleConfirm([values]);
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
store.setScaffoldError(e.message);
|
||||
} finally {
|
||||
store.setScaffoldBuzy(false);
|
||||
}
|
||||
|
||||
store.setScaffoldBuzy(false);
|
||||
store.setScaffoldStep(0);
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -197,8 +197,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
const {store, theme, manager} = this.props;
|
||||
const scaffoldFormContext = store.scaffoldForm;
|
||||
const cx = getTheme(theme || 'cxd').classnames;
|
||||
|
||||
const isStepBody = !!scaffoldFormContext?.stepsBody;
|
||||
const canSkip = !!scaffoldFormContext?.canSkip;
|
||||
const isLastStep =
|
||||
isStepBody &&
|
||||
store.scaffoldFormStep === scaffoldFormContext!.body.length - 1;
|
||||
@ -210,7 +210,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
size={scaffoldFormContext?.size || 'md'}
|
||||
contentClassName={scaffoldFormContext?.className}
|
||||
show={!!scaffoldFormContext}
|
||||
onHide={store.closeScaffoldForm}
|
||||
onHide={this.handleCancelClick}
|
||||
className="ae-scaffoldForm-Modal"
|
||||
closeOnEsc={!store.scaffoldFormBuzy}
|
||||
>
|
||||
@ -218,7 +218,7 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
{!store.scaffoldFormBuzy ? (
|
||||
<a
|
||||
data-position="left"
|
||||
onClick={store.closeScaffoldForm}
|
||||
onClick={this.handleCancelClick}
|
||||
className={cx('Modal-close')}
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
@ -233,7 +233,8 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
{
|
||||
data: store.scaffoldData,
|
||||
onValidate: scaffoldFormContext.validate,
|
||||
scopeRef: this.scopeRef
|
||||
scopeRef: this.scopeRef,
|
||||
manager
|
||||
},
|
||||
{
|
||||
...manager.env,
|
||||
@ -257,13 +258,29 @@ export class ScaffoldModal extends React.Component<SubEditorProps> {
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{isStepBody && canSkip && isFirstStep && (
|
||||
<Button
|
||||
onClick={this.handleConfirmClick}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
跳过向导
|
||||
</Button>
|
||||
)}
|
||||
{isStepBody && !isFirstStep && (
|
||||
<Button level="primary" onClick={this.goToPrevStep}>
|
||||
<Button
|
||||
level="primary"
|
||||
onClick={this.goToPrevStep}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{isStepBody && !isLastStep && (
|
||||
<Button level="primary" onClick={this.goToNextStep}>
|
||||
<Button
|
||||
level="primary"
|
||||
onClick={this.goToNextStep}
|
||||
disabled={store.scaffoldFormBuzy}
|
||||
>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
|
@ -146,6 +146,7 @@ export class SubEditor extends React.Component<SubEditorProps> {
|
||||
ref={store.subEditorRef}
|
||||
onChange={onChange}
|
||||
data={store.subEditorContext?.data}
|
||||
hostNode={store.subEditorContext?.hostNode}
|
||||
superEditorData={superEditorData}
|
||||
schemaFilter={manager.config.schemaFilter}
|
||||
theme={manager.env.theme}
|
||||
@ -184,6 +185,9 @@ export class SubEditor extends React.Component<SubEditorProps> {
|
||||
getHostNodeDataSchema={() =>
|
||||
manager.getContextSchemas(manager.store.activeId)
|
||||
}
|
||||
getAvaiableContextFields={node =>
|
||||
manager.getAvailableContextFields(node)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export function makeWrapper(
|
||||
|
||||
// 查找父数据域,将当前组件数据域追加上去,使其形成父子关系
|
||||
if (
|
||||
rendererConfig.storeType &&
|
||||
(rendererConfig.storeType || info.isListComponent) &&
|
||||
!manager.dataSchema.hasScope(`${info.id}-${info.type}`)
|
||||
) {
|
||||
let from = parent;
|
||||
|
@ -22,8 +22,6 @@ export * from './manager';
|
||||
export * from './plugin';
|
||||
export * from './icons/index';
|
||||
export * from './mocker';
|
||||
export * from './builder/DSBuilder';
|
||||
import './builder/ApiBuilder';
|
||||
import {BasicEditor, RendererEditor} from './compat';
|
||||
import MiniEditor from './component/MiniEditor';
|
||||
import CodeEditor from './component/Panel/AMisCodeEditor';
|
||||
|
@ -3,12 +3,13 @@
|
||||
* 编辑器非 UI 相关的东西应该放在这。
|
||||
*/
|
||||
import {reaction} from 'mobx';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import {parse, stringify} from 'json-ast-comments';
|
||||
import debounce from 'lodash/debounce';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import omit from 'lodash/omit';
|
||||
import {openContextMenus, toast, alert, DataScope, DataSchema} from 'amis';
|
||||
import {getRenderers, RenderOptions, mapTree} from 'amis-core';
|
||||
import {getRenderers, RenderOptions, mapTree, isEmpty} from 'amis-core';
|
||||
import {
|
||||
PluginInterface,
|
||||
BasicPanelItem,
|
||||
@ -229,6 +230,37 @@ export class EditorManager {
|
||||
// 自动加载预先注册的自定义组件
|
||||
autoPreRegisterEditorCustomPlugins();
|
||||
|
||||
/** 在顶层对外部注册的Plugin和builtInPlugins合并去重 */
|
||||
const externalPlugins = (config?.plugins || []).forEach(external => {
|
||||
if (
|
||||
Array.isArray(external) ||
|
||||
!external.priority ||
|
||||
!Number.isInteger(external.priority)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = builtInPlugins.findIndex(
|
||||
builtIn =>
|
||||
!Array.isArray(builtIn) &&
|
||||
!Array.isArray(external) &&
|
||||
builtIn.id === external.id &&
|
||||
builtIn?.prototype instanceof BasePlugin
|
||||
);
|
||||
|
||||
if (~idx) {
|
||||
const current = builtInPlugins[idx] as PluginClass;
|
||||
const currentPriority =
|
||||
current.priority && Number.isInteger(current.priority)
|
||||
? current.priority
|
||||
: 0;
|
||||
/** 同ID Plugin根据优先级决定是否替换掉Builtin中的Plugin */
|
||||
if (external.priority > currentPriority) {
|
||||
builtInPlugins.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.plugins = (config.disableBultinPlugin ? [] : builtInPlugins) // 页面设计器注册的插件列表
|
||||
.concat(this.normalizeScene(config?.plugins))
|
||||
.filter(p => {
|
||||
@ -986,6 +1018,29 @@ export class EditorManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前节点是否可以添加同级节点
|
||||
*/
|
||||
canAppendSiblings() {
|
||||
const store = this.store;
|
||||
const id = store.activeId;
|
||||
const node = store.getNodeById(id)!; // 当前选中节点
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const regionNode = node.parent as EditorNodeType; // 父级节点
|
||||
if (
|
||||
regionNode &&
|
||||
!regionNode.region &&
|
||||
!regionNode.schema.body &&
|
||||
regionNode.schema?.type !== 'flex'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前选中元素追加新的schema
|
||||
* 备注:目前主要用在复制&粘贴快捷功能键中
|
||||
@ -1701,7 +1756,7 @@ export class EditorManager {
|
||||
patchList(node.uniqueChildren);
|
||||
}
|
||||
|
||||
if (!node.isRegion) {
|
||||
if (isAlive(node) && !node.isRegion) {
|
||||
node.patch(this.store, force);
|
||||
}
|
||||
});
|
||||
@ -1845,7 +1900,6 @@ export class EditorManager {
|
||||
return;
|
||||
}
|
||||
const plugin = node.info.plugin!;
|
||||
|
||||
const store = this.store;
|
||||
const context: PopOverFormContext = {
|
||||
node,
|
||||
@ -1918,42 +1972,74 @@ export class EditorManager {
|
||||
}
|
||||
|
||||
let nearestScope;
|
||||
let listScope = [];
|
||||
|
||||
// 更新组件树中的所有上下文数据声明为最新数据
|
||||
while (scope) {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const node = this.store.getNodeById(id, type);
|
||||
const [nodeId, type] = scope.id.split('-');
|
||||
const scopeNode = this.store.getNodeById(nodeId, type);
|
||||
|
||||
// 拿非重复组件id的父组件作为主数据域展示,如CRUD,不展示表格,只展示增删改查信息,避免变量面板出现两份数据
|
||||
if (!nearestScope && node && !node.isSecondFactor) {
|
||||
if (!nearestScope && scopeNode && !scopeNode.isSecondFactor) {
|
||||
nearestScope = scope;
|
||||
}
|
||||
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
|
||||
node,
|
||||
region,
|
||||
trigger,
|
||||
node
|
||||
);
|
||||
|
||||
const jsonschema = await scopeNode?.info?.plugin?.buildDataSchemas?.(
|
||||
scopeNode,
|
||||
region,
|
||||
trigger
|
||||
);
|
||||
if (jsonschema) {
|
||||
scope.removeSchema(jsonschema.$id);
|
||||
scope.addSchema(jsonschema);
|
||||
}
|
||||
|
||||
// 记录each列表等组件顺序
|
||||
if (scopeNode?.info?.isListComponent) {
|
||||
listScope.unshift(scope);
|
||||
|
||||
// 如果当前节点是list类型节点,当前scope从父节点上取
|
||||
if (nodeId === id) {
|
||||
nearestScope = scope.parent;
|
||||
}
|
||||
}
|
||||
|
||||
scope = withoutSuper ? undefined : scope.parent;
|
||||
}
|
||||
|
||||
// each列表类型嵌套时需要从上到下获取数据,重新执行一遍
|
||||
if (listScope.length > 1) {
|
||||
for (let scope of listScope) {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const node = this.store.getNodeById(id, type);
|
||||
const jsonschema = await node?.info?.plugin?.buildDataSchemas?.(
|
||||
node,
|
||||
region,
|
||||
trigger
|
||||
);
|
||||
if (jsonschema) {
|
||||
scope.removeSchema(jsonschema.$id);
|
||||
scope.addSchema(jsonschema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 存在当前行时,找到最底层(todo:暂不考虑table套service+table的场景)
|
||||
const nearestScopeId = Object.keys(this.dataSchema.idMap).find(
|
||||
key =>
|
||||
/\-currentRow$/.test(key) &&
|
||||
!this.dataSchema.idMap[key].children?.length
|
||||
);
|
||||
const nearestScopeId =
|
||||
Object.keys(this.dataSchema.idMap).find(
|
||||
key =>
|
||||
/\-currentRow$/.test(key) &&
|
||||
!this.dataSchema.idMap[key].children?.length
|
||||
) || nearestScope?.id;
|
||||
|
||||
if (nearestScopeId) {
|
||||
this.dataSchema.switchTo(nearestScopeId);
|
||||
} else if (nearestScope?.id) {
|
||||
this.dataSchema.switchTo(nearestScope.id);
|
||||
}
|
||||
|
||||
// 如果当前容器是list非数据组件,scope从父scope开始
|
||||
if (node.info.isListComponent) {
|
||||
let lastScope = listScope[listScope.length - 1];
|
||||
this.dataSchema.switchTo(lastScope.parent!);
|
||||
}
|
||||
|
||||
return withoutSuper
|
||||
@ -1964,7 +2050,7 @@ export class EditorManager {
|
||||
/**
|
||||
* 获取可用上下文待绑定字段
|
||||
*/
|
||||
async getAvailableContextFields(node: EditorNodeType) {
|
||||
async getAvailableContextFields(node: EditorNodeType): Promise<any> {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
@ -1993,6 +2079,10 @@ export class EditorManager {
|
||||
}
|
||||
|
||||
if (!scope) {
|
||||
/** 如果在子编辑器中,继续去上层编辑器查找,不过这里可能受限于当前层的数据映射 */
|
||||
if (!from && this.store.isSubEditor) {
|
||||
return this.config?.getAvaiableContextFields?.(node);
|
||||
}
|
||||
return from?.info.plugin.getAvailableContextFields?.(from, node);
|
||||
}
|
||||
|
||||
@ -2000,7 +2090,7 @@ export class EditorManager {
|
||||
const [id, type] = scope.id.split('-');
|
||||
const scopeNode = this.store.getNodeById(id, type);
|
||||
|
||||
if (scopeNode) {
|
||||
if (scopeNode && !scopeNode.info?.isListComponent) {
|
||||
return scopeNode?.info.plugin.getAvailableContextFields?.(
|
||||
scopeNode,
|
||||
node
|
||||
|
@ -26,6 +26,8 @@ export function mockValue(schema: any) {
|
||||
return placeholderImage;
|
||||
} else if (schema.type === 'images' || schema.type === 'static-images') {
|
||||
return [placeholderImage];
|
||||
} else if (schema.type === 'number' || schema.type === 'input-number') {
|
||||
return (Math.random() * 1000).toFixed(schema.precision ?? 0);
|
||||
}
|
||||
|
||||
return '假数据';
|
||||
|
@ -36,7 +36,7 @@ export interface RegionConfig {
|
||||
/**
|
||||
* 区域占位字符,用于提示
|
||||
*/
|
||||
placeholder?: string;
|
||||
placeholder?: string | JSX.Element;
|
||||
|
||||
/**
|
||||
* 对于复杂的控件需要用到这个配置。
|
||||
@ -200,6 +200,12 @@ export interface RendererInfo extends RendererScaffoldInfo {
|
||||
|
||||
isBaseComponent?: boolean;
|
||||
|
||||
/**
|
||||
* 是否列表类型组件,自身没数据但是绑定了数据源里面的数组字段
|
||||
* 子组件需要能获取到单项字段,如list、each、cards
|
||||
*/
|
||||
isListComponent?: boolean;
|
||||
|
||||
disabledRendererPlugin?: boolean;
|
||||
|
||||
/**
|
||||
@ -342,7 +348,8 @@ export interface ScaffoldForm extends PopOverForm {
|
||||
* value 是具体错误信息。
|
||||
*/
|
||||
validate?: (
|
||||
values: any
|
||||
values: any,
|
||||
formStore: any
|
||||
) =>
|
||||
| void
|
||||
| {[propName: string]: string}
|
||||
@ -820,6 +827,9 @@ export interface PluginInterface
|
||||
region?: EditorNodeType
|
||||
) => Promise<SchemaCollection | void>;
|
||||
|
||||
/** 配置面板表单的 pipeOut function */
|
||||
panelFormPipeOut?: (value: any) => any;
|
||||
|
||||
/**
|
||||
* @deprecated 用 panelBodyCreator
|
||||
*/
|
||||
@ -1035,6 +1045,7 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
scaffoldForm: plugin.scaffoldForm,
|
||||
disabledRendererPlugin: plugin.disabledRendererPlugin,
|
||||
isBaseComponent: plugin.isBaseComponent,
|
||||
isListComponent: plugin.isListComponent,
|
||||
rendererName: plugin.rendererName
|
||||
};
|
||||
}
|
||||
@ -1082,6 +1093,18 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
plugin
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId,
|
||||
pipeOut: plugin.panelFormPipeOut?.bind?.(plugin)
|
||||
};
|
||||
|
||||
panels.push({
|
||||
key: 'config',
|
||||
icon: plugin.panelIcon || plugin.icon || 'fa fa-cog',
|
||||
@ -1092,27 +1115,13 @@ export abstract class BasePlugin implements PluginInterface {
|
||||
const panelBody = await (body as Promise<SchemaCollection>);
|
||||
|
||||
return this.manager.makeSchemaFormRender({
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
body: panelBody,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId
|
||||
...baseProps,
|
||||
body: panelBody
|
||||
});
|
||||
}, omit(plugin.async, 'enable'))
|
||||
: this.manager.makeSchemaFormRender({
|
||||
definitions: plugin.panelDefinitions,
|
||||
submitOnChange: plugin.panelSubmitOnChange,
|
||||
api: plugin.panelApi,
|
||||
body: body as SchemaCollection,
|
||||
controls: plugin.panelControlsCreator
|
||||
? plugin.panelControlsCreator(context)
|
||||
: plugin.panelControls!,
|
||||
justify: plugin.panelJustify,
|
||||
panelById: store.activeId
|
||||
...baseProps,
|
||||
body: body as SchemaCollection
|
||||
})
|
||||
});
|
||||
} else if (
|
||||
|
@ -233,7 +233,7 @@ export const MainStore = types
|
||||
// 给预览状态时的
|
||||
get filteredSchemaForPreview() {
|
||||
const schema = JSONPipeOut(self.schema);
|
||||
return getEnv(self).schemaFilter?.(schema) ?? schema;
|
||||
return getEnv(self).schemaFilter?.(schema, true) ?? schema;
|
||||
},
|
||||
|
||||
// 判断当前元素是否是根节点
|
||||
|
@ -544,6 +544,7 @@ export const EditorNode = types
|
||||
if (node.id === 'root') {
|
||||
return;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,6 +565,10 @@ export const EditorNode = types
|
||||
break;
|
||||
}
|
||||
|
||||
if (cursor.id === 'root') {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
cursor = cursor.parent;
|
||||
}
|
||||
|
||||
|
@ -211,9 +211,9 @@ export function JSONPipeOut(
|
||||
* 如果存在themeCss属性,则给对应的className加上name
|
||||
*/
|
||||
export function addStyleClassName(obj: Schema) {
|
||||
const themeCss = obj.themeCss || obj.css;
|
||||
const themeCss = obj.type === 'page' ? obj.themeCss : obj.themeCss || obj.css;
|
||||
// page暂时不做处理
|
||||
if (!themeCss || obj.type === 'page') {
|
||||
if (!themeCss) {
|
||||
return obj;
|
||||
}
|
||||
let toUpdate: any = {};
|
||||
|
BIN
packages/amis-editor-core/static/indication.png
Normal file
BIN
packages/amis-editor-core/static/indication.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 B |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-editor",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2-alpha.0",
|
||||
"description": "amis 可视化编辑器",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
1440
packages/amis-editor/src/builder/ApiDSBuilder.ts
Normal file
1440
packages/amis-editor/src/builder/ApiDSBuilder.ts
Normal file
File diff suppressed because it is too large
Load Diff
286
packages/amis-editor/src/builder/DSBuilder.ts
Normal file
286
packages/amis-editor/src/builder/DSBuilder.ts
Normal file
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* @file DSBuilder.ts
|
||||
* @desc 数据源配置构建器
|
||||
*/
|
||||
|
||||
import {EditorManager} from 'amis-editor-core';
|
||||
import {getFeatValueByKey, getFeatLabelByKey} from './utils';
|
||||
|
||||
import type {EditorNodeType} from 'amis-editor-core';
|
||||
import type {
|
||||
DSFeatureType,
|
||||
GenericSchema,
|
||||
CRUDScaffoldConfig,
|
||||
FormScaffoldConfig
|
||||
} from './type';
|
||||
|
||||
export interface DSBuilderBaseOptions {
|
||||
/** 渲染器类型 */
|
||||
renderer: string;
|
||||
/** Form应用场景 */
|
||||
feat?: DSFeatureType;
|
||||
/** CRUD应用场景 */
|
||||
feats?: DSFeatureType[];
|
||||
/** 当前组件的 Schema */
|
||||
schema?: GenericSchema;
|
||||
/** 数据源字段名 */
|
||||
sourceKey?: string;
|
||||
/** 是否在脚手架环境中 */
|
||||
inScaffold?: boolean;
|
||||
/** 如果为列表类容器,则会返回对应的节点 */
|
||||
scopeNode?: EditorNodeType;
|
||||
/** 数据源控件配置项 */
|
||||
sourceSettings?: Record<string, any>;
|
||||
/** 字段控件配置项 */
|
||||
fieldSettings?: Record<string, any>;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface DSBuilderInterface<
|
||||
T extends DSBuilderBaseOptions = DSBuilderBaseOptions
|
||||
> {
|
||||
/** 数据源中文名称,主要用于前端展示 */
|
||||
readonly name: string;
|
||||
|
||||
/** 构造器排序权重,数字越小排序越靠前,支持负数 */
|
||||
readonly order: number;
|
||||
|
||||
/** 数据源支持的功能场景 */
|
||||
readonly features: DSFeatureType[];
|
||||
|
||||
/** 是否为默认 */
|
||||
isDefault?: boolean;
|
||||
|
||||
/** 实例获取数据源的key */
|
||||
key: string;
|
||||
|
||||
/** 是否禁用 */
|
||||
disabledOn?: () => boolean;
|
||||
|
||||
/** 获取功能场景的value */
|
||||
getFeatValueByKey(feat: DSFeatureType): string;
|
||||
|
||||
/** 获取功能场景的label */
|
||||
getFeatLabelByKey(feat: DSFeatureType): string;
|
||||
|
||||
/** 按照功能场景过滤 */
|
||||
filterByFeat(feat: any): boolean;
|
||||
|
||||
/** 根据schema,判断是否匹配当前数据源 */
|
||||
match(schema?: any): boolean;
|
||||
|
||||
/** 当前上下文中使用的字段 */
|
||||
getContextFields(options: T): Promise<any>;
|
||||
|
||||
/** 当前上下文可以使用的字段 */
|
||||
getAvailableContextFields(
|
||||
options: Omit<T, 'renderer'>,
|
||||
target: EditorNodeType
|
||||
): Promise<any>;
|
||||
|
||||
/** 获取CRUD列表字段 */
|
||||
getCRUDListFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 获取CRUD简单查询字段 */
|
||||
getCRUDSimpleQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建简单查询表单项 */
|
||||
buildSimpleQueryCollectionSchema?: (
|
||||
options: T
|
||||
) => Promise<GenericSchema[] | undefined>;
|
||||
|
||||
/** 获取CRUD高级查询字段 */
|
||||
getCRUDAdvancedQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建高级查询 */
|
||||
buildAdvancedQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
|
||||
|
||||
/** 获取CRUD模糊查询字段 */
|
||||
getCRUDFuzzyQueryFields?: <F extends Record<string, any>>(
|
||||
options: T
|
||||
) => Promise<F[]>;
|
||||
|
||||
/** 构建模糊查询 */
|
||||
buildFuzzyQuerySchema?: (options: T) => Promise<GenericSchema | undefined>;
|
||||
|
||||
/** 构造数据源的可视化配置表单 */
|
||||
makeSourceSettingForm(options: T): any[];
|
||||
|
||||
/** 构造数据源字段的可视化配置表单 */
|
||||
makeFieldsSettingForm(options: T): any[];
|
||||
|
||||
/** 新建数据 */
|
||||
buildInsertSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 编辑数据 */
|
||||
buildEditSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 批量编辑数据 */
|
||||
buildBulkEditSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 查看详情数据 */
|
||||
buildViewSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 删除数据 */
|
||||
buildCRUDDeleteSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 批量删除数据 */
|
||||
buildCRUDBulkDeleteSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 构建 CRUD 的顶部工具栏 */
|
||||
buildCRUDHeaderToolbar?: (
|
||||
options: T,
|
||||
componentId?: string
|
||||
) => Promise<GenericSchema>;
|
||||
|
||||
/** 表格的表头查询 */
|
||||
buildCRUDFilterSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 表格单列 */
|
||||
buildCRUDColumn?: (
|
||||
field: Record<string, any>,
|
||||
options: T,
|
||||
componentId?: string
|
||||
) => Promise<any>;
|
||||
|
||||
/** 表格操作列 */
|
||||
buildCRUDOpColumn?: (options: T, componentId?: string) => Promise<any>;
|
||||
|
||||
/** 表格列 */
|
||||
buildCRUDColumnsSchema(options: T, componentId?: string): Promise<any>;
|
||||
|
||||
/** 表格构建 */
|
||||
buildCRUDSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表单构建 */
|
||||
buildFormSchema(options: T): Promise<any>;
|
||||
|
||||
/** 基于 schema 还原CRUD脚手架配置 */
|
||||
guessCRUDScaffoldConfig<T extends CRUDScaffoldConfig<any, any>>(options: {
|
||||
schema: GenericSchema;
|
||||
[propName: string]: any;
|
||||
}): Promise<T> | T;
|
||||
|
||||
/** 基于 schema 还原Form脚手架配置 */
|
||||
guessFormScaffoldConfig<T extends FormScaffoldConfig<any, any>>(options: {
|
||||
schema: GenericSchema;
|
||||
[propName: string]: any;
|
||||
}): Promise<T> | T;
|
||||
|
||||
/** 重新构建 API 配置 */
|
||||
buildApiSchema(options: T): Promise<any>;
|
||||
}
|
||||
|
||||
export abstract class DSBuilder<T extends DSBuilderBaseOptions>
|
||||
implements DSBuilderInterface<T>
|
||||
{
|
||||
static key: string;
|
||||
readonly name: string;
|
||||
readonly order: number;
|
||||
/** 是否为默认 */
|
||||
readonly isDefault?: boolean;
|
||||
|
||||
features: DSFeatureType[];
|
||||
|
||||
constructor(readonly manager: EditorManager) {}
|
||||
|
||||
/** 实例获取数据源的key */
|
||||
get key() {
|
||||
return (this.constructor as typeof DSBuilder<T>).key;
|
||||
}
|
||||
|
||||
/** 获取功能场景的value */
|
||||
getFeatValueByKey(feat: DSFeatureType) {
|
||||
return getFeatValueByKey(feat);
|
||||
}
|
||||
|
||||
/** 获取功能场景的label */
|
||||
getFeatLabelByKey(feat: DSFeatureType) {
|
||||
return getFeatLabelByKey(feat);
|
||||
}
|
||||
|
||||
filterByFeat(feat: any) {
|
||||
return feat && this.features.includes(feat);
|
||||
}
|
||||
|
||||
abstract match(schema?: any): boolean;
|
||||
|
||||
abstract getContextFields(options: T): Promise<any>;
|
||||
|
||||
abstract getAvailableContextFields(
|
||||
options: Omit<T, 'renderer'>,
|
||||
target: EditorNodeType
|
||||
): Promise<any>;
|
||||
|
||||
abstract makeSourceSettingForm(options: T): any[];
|
||||
|
||||
abstract makeFieldsSettingForm(options: T): any[];
|
||||
|
||||
/** 新建数据 */
|
||||
abstract buildInsertSchema(options: T): Promise<any>;
|
||||
|
||||
/** 查看详情数据 */
|
||||
abstract buildViewSchema(options: T): Promise<any>;
|
||||
|
||||
/** 编辑数据 */
|
||||
abstract buildEditSchema(options: T): Promise<any>;
|
||||
|
||||
/** 批量编辑数据 */
|
||||
abstract buildBulkEditSchema(options: T): Promise<any>;
|
||||
|
||||
/** 删除数据 */
|
||||
abstract buildCRUDDeleteSchema(options: T): Promise<any>;
|
||||
|
||||
/** 批量删除数据 */
|
||||
abstract buildCRUDBulkDeleteSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格的表头查询 */
|
||||
abstract buildCRUDFilterSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格列 */
|
||||
abstract buildCRUDColumnsSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表格 */
|
||||
abstract buildCRUDSchema(options: T): Promise<any>;
|
||||
|
||||
/** 表单 */
|
||||
abstract buildFormSchema(options: T): Promise<any>;
|
||||
|
||||
/** 基于 schema 还原CRUD脚手架配置 */
|
||||
abstract guessCRUDScaffoldConfig<
|
||||
T extends CRUDScaffoldConfig<any, any>
|
||||
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
|
||||
|
||||
/** 基于 schema 还原Form脚手架配置 */
|
||||
abstract guessFormScaffoldConfig<
|
||||
T extends FormScaffoldConfig<any, any>
|
||||
>(options: {schema: GenericSchema; [propName: string]: any}): Promise<T> | T;
|
||||
|
||||
abstract buildApiSchema(options: T): Promise<any>;
|
||||
}
|
||||
|
||||
export interface DSBuilderClass {
|
||||
new (manager: EditorManager): DSBuilderInterface;
|
||||
/** 数据源类型,使用英文,可以覆盖同名 */
|
||||
key: string;
|
||||
}
|
||||
|
||||
export const builderFactory = new Map<string, DSBuilderClass>();
|
||||
|
||||
/** 注册数据源构造器 */
|
||||
export const registerDSBuilder = (klass: DSBuilderClass) => {
|
||||
if (builderFactory.has(klass.key)) {
|
||||
console.warn(
|
||||
`[amis-editor][DSBuilder] duplicate DSBuilder「${klass.key}」`
|
||||
);
|
||||
}
|
||||
|
||||
/** 重名覆盖 */
|
||||
builderFactory.set(klass.key, klass);
|
||||
};
|
115
packages/amis-editor/src/builder/DSBuilderManager.ts
Normal file
115
packages/amis-editor/src/builder/DSBuilderManager.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @file DSBuilderManager
|
||||
* @desc 数据源构造管理器
|
||||
*/
|
||||
|
||||
import {builderFactory, DSBuilderInterface} from './DSBuilder';
|
||||
import {EditorManager} from 'amis-editor-core';
|
||||
|
||||
export class DSBuilderManager {
|
||||
private builders: Map<string, DSBuilderInterface>;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
this.builders = new Map();
|
||||
|
||||
builderFactory.forEach((Builder, key) => {
|
||||
this.builders.set(key, new Builder(manager));
|
||||
});
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.builders.size;
|
||||
}
|
||||
|
||||
getBuilderByKey(key: string) {
|
||||
return this.builders.get(key);
|
||||
}
|
||||
|
||||
getBuilderByScaffoldSetting(scaffoldConfig: any) {
|
||||
return this.builders.get(scaffoldConfig.dsType);
|
||||
}
|
||||
|
||||
getBuilderBySchema(schema: any) {
|
||||
let builder: DSBuilderInterface | undefined;
|
||||
|
||||
for (let [key, value] of Array.from(this.builders.entries())) {
|
||||
if (value.match(schema)) {
|
||||
builder = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder ? builder : this.getDefaultBuilder();
|
||||
}
|
||||
|
||||
getDefaultBuilderKey() {
|
||||
const collections = Array.from(this.builders.entries()).filter(
|
||||
([_, builder]) => builder?.disabledOn?.() !== true
|
||||
);
|
||||
const [defaultKey, _] =
|
||||
collections.find(([_, builder]) => builder.isDefault === true) ??
|
||||
collections.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
})?.[0] ??
|
||||
[];
|
||||
|
||||
return defaultKey;
|
||||
}
|
||||
|
||||
getDefaultBuilder() {
|
||||
const collections = Array.from(this.builders.entries()).filter(
|
||||
([_, builder]) => builder?.disabledOn?.() !== true
|
||||
);
|
||||
const [_, defaultBuilder] =
|
||||
collections.find(([_, builder]) => builder.isDefault === true) ??
|
||||
collections.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
})?.[0] ??
|
||||
[];
|
||||
|
||||
return defaultBuilder;
|
||||
}
|
||||
|
||||
getAvailableBuilders() {
|
||||
return Array.from(this.builders.entries())
|
||||
.filter(([_, builder]) => builder?.disabledOn?.() !== true)
|
||||
.sort((lhs, rhs) => {
|
||||
return (lhs[1].order ?? 0) - (rhs[1].order ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
getDSSelectorSchema(patch: Record<string, any>) {
|
||||
const builders = this.getAvailableBuilders();
|
||||
const options = builders.map(([key, builder]) => ({
|
||||
label: builder.name,
|
||||
value: key
|
||||
}));
|
||||
|
||||
return {
|
||||
type: 'radios',
|
||||
label: '数据来源',
|
||||
name: 'dsType',
|
||||
visible: options.length > 0,
|
||||
selectFirst: true,
|
||||
options: options,
|
||||
...patch
|
||||
};
|
||||
}
|
||||
|
||||
buildCollectionFromBuilders(
|
||||
callback: (
|
||||
builder: DSBuilderInterface,
|
||||
builderKey: string,
|
||||
index: number
|
||||
) => any
|
||||
) {
|
||||
const builders = this.getAvailableBuilders();
|
||||
const collection = builders
|
||||
.map(([key, builder], index) => {
|
||||
return callback(builder, key, index);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return collection;
|
||||
}
|
||||
}
|
130
packages/amis-editor/src/builder/constants.ts
Normal file
130
packages/amis-editor/src/builder/constants.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @file constants.ts
|
||||
* @desc builder 相关常量
|
||||
*/
|
||||
|
||||
import {FormOperatorValue, FormOperator} from './type';
|
||||
|
||||
/**
|
||||
* 数据源所需操作,目前是因为schema从后端来
|
||||
*/
|
||||
export enum DSBehavior {
|
||||
/** 创建操作 */
|
||||
create = 'create',
|
||||
/** 查询操作 */
|
||||
view = 'view',
|
||||
/** 更新操作 */
|
||||
update = 'update',
|
||||
table = 'table',
|
||||
filter = 'filter'
|
||||
}
|
||||
|
||||
/** 数据粒度 */
|
||||
export enum DSGrain {
|
||||
/** 实体 */
|
||||
entity = 'entity',
|
||||
/** 多条数据 */
|
||||
list = 'list',
|
||||
/** 单条数据 */
|
||||
piece = 'piece'
|
||||
}
|
||||
|
||||
/** 数据源所使用的功能场景 */
|
||||
export const DSFeature = {
|
||||
List: {
|
||||
value: 'list',
|
||||
label: '列表'
|
||||
},
|
||||
Insert: {
|
||||
value: 'insert',
|
||||
label: '新增'
|
||||
},
|
||||
View: {
|
||||
value: 'view',
|
||||
label: '详情'
|
||||
},
|
||||
Edit: {
|
||||
value: 'edit',
|
||||
label: '编辑'
|
||||
},
|
||||
Delete: {
|
||||
value: 'delete',
|
||||
label: '删除'
|
||||
},
|
||||
BulkEdit: {
|
||||
value: 'bulkEdit',
|
||||
label: '批量编辑'
|
||||
},
|
||||
BulkDelete: {
|
||||
value: 'bulkDelete',
|
||||
label: '批量删除'
|
||||
},
|
||||
Import: {
|
||||
value: 'import',
|
||||
label: '导入'
|
||||
},
|
||||
Export: {
|
||||
value: 'export',
|
||||
label: '导出'
|
||||
},
|
||||
SimpleQuery: {
|
||||
value: 'simpleQuery',
|
||||
label: '简单查询'
|
||||
},
|
||||
FuzzyQuery: {
|
||||
value: 'fuzzyQuery',
|
||||
label: '模糊查询'
|
||||
},
|
||||
AdvancedQuery: {
|
||||
value: 'advancedQuery',
|
||||
label: '高级查询'
|
||||
}
|
||||
};
|
||||
|
||||
export enum DSFeatureEnum {
|
||||
List = 'List',
|
||||
Insert = 'Insert',
|
||||
View = 'View',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
BulkEdit = 'BulkEdit',
|
||||
BulkDelete = 'BulkDelete',
|
||||
Import = 'Import',
|
||||
Export = 'Export',
|
||||
SimpleQuery = 'SimpleQuery',
|
||||
FuzzyQuery = 'FuzzyQuery',
|
||||
AdvancedQuery = 'AdvancedQuery'
|
||||
}
|
||||
|
||||
export const DSFeatureList = Object.keys(
|
||||
DSFeature
|
||||
) as (keyof typeof DSFeature)[];
|
||||
|
||||
export const FormOperatorMap: Record<FormOperatorValue, FormOperator> = {
|
||||
cancel: {
|
||||
label: '取消',
|
||||
value: 'cancel',
|
||||
order: 0,
|
||||
schema: {
|
||||
level: 'default'
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
label: '重置',
|
||||
value: 'reset',
|
||||
order: 1,
|
||||
schema: {
|
||||
level: 'default'
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
label: '提交',
|
||||
value: 'submit',
|
||||
order: 2,
|
||||
schema: {
|
||||
level: 'primary'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ModelDSBuilderKey = 'model-entity';
|
7
packages/amis-editor/src/builder/index.ts
Normal file
7
packages/amis-editor/src/builder/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './type';
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
export * from './DSBuilder';
|
||||
export * from './DSBuilderManager';
|
||||
|
||||
import './ApiDSBuilder';
|
122
packages/amis-editor/src/builder/type.ts
Normal file
122
packages/amis-editor/src/builder/type.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @file type.ts
|
||||
* @desc builder 相关声明
|
||||
*/
|
||||
|
||||
import {DSFeature} from './constants';
|
||||
import type {BaseApiObject} from 'amis-core';
|
||||
|
||||
export interface DSField {
|
||||
value: string;
|
||||
label: string;
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
/** 数据源字段集合 */
|
||||
export interface DSFieldGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
children: DSField[];
|
||||
[propKey: string]: any;
|
||||
}
|
||||
|
||||
export type DSFeatureType = keyof typeof DSFeature;
|
||||
|
||||
export type GenericSchema = Record<string, any>;
|
||||
|
||||
export type DSRendererType = 'form' | 'crud' | 'service';
|
||||
|
||||
export interface ScaffoldField {
|
||||
/** 标题 */
|
||||
label: string;
|
||||
/** 字段名 */
|
||||
name: string;
|
||||
/** 展示控件类型 */
|
||||
displayType: string;
|
||||
/** 输入控件类型 */
|
||||
inputType: string;
|
||||
typeKey?: string;
|
||||
/** 是否启用 */
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
/** 表单操作 */
|
||||
export type ApiConfig = string | BaseApiObject;
|
||||
|
||||
/** 表单操作 */
|
||||
export type FormOperatorValue = 'cancel' | 'reset' | 'submit';
|
||||
|
||||
/** 表单操作按钮 */
|
||||
export interface FormOperator {
|
||||
label: string;
|
||||
value: FormOperatorValue;
|
||||
order: number;
|
||||
schema: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ScaffoldConfigBase {
|
||||
/** 数据源类型 */
|
||||
dsType: string;
|
||||
/** 重新构建时用户的原始 Schema */
|
||||
__pristineSchema?: Record<string, any>;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface FormScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> extends ScaffoldConfigBase {
|
||||
/** Form功能场景 */
|
||||
feat?: DSFeatureType;
|
||||
/** 表单初始化接口 */
|
||||
initApi?: API;
|
||||
insertApi?: API;
|
||||
editApi?: API;
|
||||
bulkEditApi?: API;
|
||||
insertFields?: Fields[];
|
||||
editFields?: Fields[];
|
||||
bulkEditFields?: Fields[];
|
||||
operators?: FormOperator[];
|
||||
}
|
||||
|
||||
export interface CRUDScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> extends ScaffoldConfigBase {
|
||||
/** 工具栏 */
|
||||
tools?: Extract<DSFeatureType, 'Insert' | 'BulkDelete' | 'BulkEdit'>[];
|
||||
/** 数据操作 */
|
||||
operators?: Extract<DSFeatureType, 'View' | 'Edit' | 'Delete'>[];
|
||||
/** 条件查询 */
|
||||
filters?: Extract<
|
||||
DSFeatureType,
|
||||
'FuzzyQuery' | 'SimpleQuery' | 'AdvancedQuery'
|
||||
>[];
|
||||
/** 表格 list 接口 */
|
||||
listApi?: API;
|
||||
viewApi?: API;
|
||||
editApi?: API;
|
||||
/** 编辑表单的初始化接口 */
|
||||
initApi?: API;
|
||||
bulkEditApi?: API;
|
||||
deleteApi?: API;
|
||||
bulkDeleteApi?: API;
|
||||
insertApi?: API;
|
||||
listFields?: Fields[];
|
||||
insertFields?: Fields[];
|
||||
viewFields?: Fields[];
|
||||
editFields?: Fields[];
|
||||
bulkEditFields?: Fields[];
|
||||
fuzzyQueryFields?: Fields[];
|
||||
simpleQueryFields?: Fields[];
|
||||
advancedQueryFields?: Fields[];
|
||||
importFields?: Fields[];
|
||||
exportFields?: Fields[];
|
||||
/** 表格脚手架时的主键 */
|
||||
primaryField?: string;
|
||||
}
|
||||
|
||||
export type ScaffoldConfig<
|
||||
Fields extends Record<string, any> = ScaffoldField,
|
||||
API extends any = ApiConfig
|
||||
> = FormScaffoldConfig<Fields, API> | CRUDScaffoldConfig<Fields, API>;
|
95
packages/amis-editor/src/builder/utils.ts
Normal file
95
packages/amis-editor/src/builder/utils.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @file utils
|
||||
* @desc builder用到的 utils
|
||||
*/
|
||||
|
||||
import isObjectLike from 'lodash/isObjectLike';
|
||||
import {DSFeature} from './constants';
|
||||
import type {DSFeatureType} from './type';
|
||||
|
||||
export const getFeatValueByKey = (feat: DSFeatureType) => {
|
||||
return `${DSFeature?.[feat]?.value}`;
|
||||
};
|
||||
|
||||
export const getFeatLabelByKey = (feat: DSFeatureType) => {
|
||||
return `${DSFeature?.[feat]?.label}`;
|
||||
};
|
||||
|
||||
const _traverseSchemaDeep = (
|
||||
schema: Record<string, any>,
|
||||
mapper: (originKey: string, originValue: any, origin: any) => any[],
|
||||
cache = new WeakMap()
|
||||
) => {
|
||||
const target: Record<string, any> = {};
|
||||
|
||||
if (cache.has(schema)) {
|
||||
return cache.get(schema);
|
||||
}
|
||||
|
||||
cache.set(schema, target);
|
||||
|
||||
const mapArray = (arr: any[]): any =>
|
||||
arr.map((item: any) => {
|
||||
return isObjectLike(item)
|
||||
? _traverseSchemaDeep(item, mapper, cache)
|
||||
: item;
|
||||
});
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return mapArray(schema);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
const result = mapper(key, value, schema);
|
||||
|
||||
let [updatedKey, updatedValue] = result;
|
||||
|
||||
if (updatedKey === '__proto__') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isObjectLike(updatedValue)) {
|
||||
updatedValue = Array.isArray(updatedValue)
|
||||
? mapArray(updatedValue)
|
||||
: _traverseSchemaDeep(updatedValue, mapper, cache);
|
||||
}
|
||||
|
||||
target[updatedKey] = updatedValue;
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
export const traverseSchemaDeep = (
|
||||
schema: Record<string, any>,
|
||||
mapper: (originKey: string, originValue: any, origin: any) => any[]
|
||||
) => {
|
||||
if (!isObjectLike(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
return _traverseSchemaDeep(schema, mapper);
|
||||
};
|
||||
|
||||
/** CRUD列类型转 Form 表单类型 */
|
||||
export const displayType2inputType = (inputType: string): string => {
|
||||
if (!inputType || typeof inputType !== 'string') {
|
||||
return inputType;
|
||||
}
|
||||
|
||||
const map: Record<string, string> = {
|
||||
tpl: 'input-text',
|
||||
image: 'input-image',
|
||||
date: 'input-date',
|
||||
progress: 'input-number',
|
||||
status: 'tag',
|
||||
mapping: 'tag',
|
||||
list: 'input-table'
|
||||
};
|
||||
|
||||
return map.hasOwnProperty(inputType) ? map[inputType] : inputType;
|
||||
};
|
1
packages/amis-editor/src/icons/crud/column-add.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-add.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M2.66699 8L13.3337 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><path d="M8 2.66699L8 13.3337" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>
|
After Width: | Height: | Size: 315 B |
1
packages/amis-editor/src/icons/crud/column-delete.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-delete.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6.19561 6.20513L14.5925 14.4728M5.83984 14.3673L14.2367 6.09961" stroke="currentColor" stroke-width="1.6"></path></svg>
|
After Width: | Height: | Size: 190 B |
1
packages/amis-editor/src/icons/crud/column-setting.svg
Normal file
1
packages/amis-editor/src/icons/crud/column-setting.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class=""><path d="M3 6.5C2.17157 6.5 1.5 7.17157 1.5 8C1.5 8.82843 2.17157 9.5 3 9.5C3.82843 9.5 4.5 8.82843 4.5 8C4.5 7.17157 3.82843 6.5 3 6.5Z" fill="currentColor"></path><path d="M6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8Z" fill="currentColor"></path><path d="M13 6.5C12.1716 6.5 11.5 7.17157 11.5 8C11.5 8.82843 12.1716 9.5 13 9.5C13.8284 9.5 14.5 8.82843 14.5 8C14.5 7.17157 13.8284 6.5 13 6.5Z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 615 B |
2
packages/amis-editor/src/icons/crud/share-link.svg
Normal file
2
packages/amis-editor/src/icons/crud/share-link.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2532" width="64" height="64"><path d="M938.666667 469.333333c-23.466667 0-42.666667 19.2-42.666667 42.666667l0 85.333333 0 320c0 12.8-8.533333 21.333333-21.333333 21.333333L106.666667 938.666667c-12.8 0-21.333333-8.533333-21.333333-21.333333l0-106.666667L85.333333 234.666667c0-12.8 8.533333-21.333333 21.333333-21.333333l362.666667 0c23.466667 0 42.666667-19.2 42.666667-42.666667 0-23.466667-19.2-42.666667-42.666667-42.666667L85.333333 128c-46.933333 0-85.333333 38.4-85.333333 85.333333l0 341.333333 0 256 0 128c0 46.933333 38.4 85.333333 85.333333 85.333333l810.666667 0c46.933333 0 85.333333-38.4 85.333333-85.333333l0-128L981.333333 597.333333l0-85.333333C981.333333 488.533333 962.133333 469.333333 938.666667 469.333333zM1011.2 162.133333l-149.333333-149.333333C855.466667 4.266667 844.8 0 832 0c-23.466667 0-42.666667 19.2-42.666667 42.666667 0 12.8 4.266667 23.466667 12.8 29.866667l83.2 83.2C554.666667 202.666667 298.666667 488.533333 298.666667 832c0 23.466667 19.2 42.666667 42.666667 42.666667s42.666667-19.2 42.666667-42.666667c0-290.133333 206.933333-533.333333 484.266667-586.666667l-66.133333 66.133333C793.6 317.866667 789.333333 328.533333 789.333333 341.333333c0 23.466667 19.2 42.666667 42.666667 42.666667 12.8 0 23.466667-4.266667 29.866667-12.8l149.333333-149.333333C1019.733333 215.466667 1024 204.8 1024 192 1024 179.2 1019.733333 168.533333 1011.2 162.133333z" p-id="2533"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -183,6 +183,12 @@ import jSpaceAround from './display/jSpaceAround.svg';
|
||||
// 主题
|
||||
import themeCss from './theme/css.svg';
|
||||
|
||||
// CRUD相关
|
||||
import ColumnSetting from './crud/column-setting.svg';
|
||||
import ColumnDelete from './crud/column-delete.svg';
|
||||
import ColumnAdd from './crud/column-add.svg';
|
||||
import ShareLink from './crud/share-link.svg';
|
||||
|
||||
// 功能类组件 icon x 11
|
||||
registerIcon('audio-plugin', audio);
|
||||
registerIcon('custom-plugin', custom);
|
||||
@ -351,4 +357,10 @@ registerIcon('jSpaceAround', jSpaceAround);
|
||||
// 主题
|
||||
registerIcon('theme-css', themeCss);
|
||||
|
||||
// CRUD相关
|
||||
registerIcon('column-setting', ColumnSetting);
|
||||
registerIcon('column-delete', ColumnDelete);
|
||||
registerIcon('column-add', ColumnAdd);
|
||||
registerIcon('share-link', ShareLink);
|
||||
|
||||
export {Icon};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'amis';
|
||||
import './locale/index';
|
||||
export * from 'amis-editor-core';
|
||||
export * from './builder';
|
||||
import './tpl/index';
|
||||
export * from './plugin';
|
||||
|
||||
@ -40,6 +41,11 @@ import './renderer/TransferTableControl';
|
||||
import './renderer/style-control/ThemeCssCode';
|
||||
import './renderer/ButtonGroupControl';
|
||||
import './renderer/FlexSettingControl';
|
||||
import './renderer/FieldSetting';
|
||||
import './renderer/TableColumnWidthControl';
|
||||
import './renderer/crud2-control/CRUDColumnControl';
|
||||
import './renderer/crud2-control/CRUDToolbarControl';
|
||||
import './renderer/crud2-control/CRUDFiltersControl';
|
||||
import 'amis-theme-editor/lib/locale/zh-CN';
|
||||
import 'amis-theme-editor/lib/locale/en-US';
|
||||
import 'amis-theme-editor/lib/renderers/Border';
|
||||
|
@ -168,7 +168,7 @@ export class ButtonPlugin extends BasePlugin {
|
||||
visibleOn: visibleOn,
|
||||
editorThemePath: `button1.size.\${size}.body.border`
|
||||
}),
|
||||
getSchemaTpl('theme:size', {
|
||||
getSchemaTpl('theme:select', {
|
||||
label: '图标尺寸',
|
||||
name: `themeCss.iconClassName.iconSize:${state}`,
|
||||
visibleOn: visibleOn,
|
||||
@ -364,7 +364,7 @@ export class ButtonPlugin extends BasePlugin {
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '自定义样式',
|
||||
title: '基本样式',
|
||||
body: [
|
||||
{
|
||||
type: 'select',
|
||||
|
File diff suppressed because it is too large
Load Diff
1295
packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx
Normal file
1295
packages/amis-editor/src/plugin/CRUD2/BaseCRUD.tsx
Normal file
File diff suppressed because it is too large
Load Diff
149
packages/amis-editor/src/plugin/CRUD2/CRUDCards.tsx
Normal file
149
packages/amis-editor/src/plugin/CRUD2/CRUDCards.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @file CRUDCards.tsx
|
||||
* @desc 卡片模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDCardsPlugin extends BaseCRUDPlugin {
|
||||
static id = 'CardsCRUDPlugin';
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
name = '卡片列表';
|
||||
|
||||
panelTitle: '卡片列表';
|
||||
|
||||
icon = 'fa fa-window-maximize';
|
||||
|
||||
panelIcon = 'fa fa-table';
|
||||
|
||||
subPanelIcon = 'fa fa-table';
|
||||
|
||||
pluginIcon = 'cards-plugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'围绕卡片列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
|
||||
|
||||
order = -1000;
|
||||
|
||||
$schema = '/schemas/CRUD2CardsSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('cards');
|
||||
|
||||
scaffold: any = this.generateScaffold('cards');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
/** 先写入动态控件 */
|
||||
this.dynamicControls = {
|
||||
/** 列配置 */
|
||||
columns: context => this.renderColumnsControl(context),
|
||||
/** 工具栏配置 */
|
||||
toolbar: context => this.renderToolbarCollapse(context),
|
||||
/** 搜索栏 */
|
||||
filters: context => this.renderFiltersCollapse(context)
|
||||
};
|
||||
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
|
||||
@autobind
|
||||
renderColumnsControl(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
title: '列设置',
|
||||
order: 5,
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-column-control',
|
||||
name: 'columns',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderToolbarCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
order: 20,
|
||||
title: '工具栏',
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-toolbar-control',
|
||||
name: 'headerToolbar',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFiltersCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
const collection: any[] = [];
|
||||
|
||||
builder.features.forEach(feat => {
|
||||
if (/Query$/.test(feat)) {
|
||||
collection.push({
|
||||
type: 'ae-crud-filters-control',
|
||||
name:
|
||||
feat === DSFeatureEnum.SimpleQuery ||
|
||||
feat === DSFeatureEnum.AdvancedQuery
|
||||
? 'filter'
|
||||
: feat === DSFeatureEnum.FuzzyQuery
|
||||
? 'headerToolbar'
|
||||
: undefined,
|
||||
label:
|
||||
feat === DSFeatureEnum.SimpleQuery
|
||||
? '简单查询'
|
||||
: feat === DSFeatureEnum.AdvancedQuery
|
||||
? '高级查询'
|
||||
: '模糊查询',
|
||||
nodeId: context.id,
|
||||
feat: feat,
|
||||
builder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return collection.length > 0
|
||||
? {
|
||||
order: 10,
|
||||
title: '搜索设置',
|
||||
body: collection
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// registerEditorPlugin(CRUDCardsPlugin);
|
149
packages/amis-editor/src/plugin/CRUD2/CRUDList.tsx
Normal file
149
packages/amis-editor/src/plugin/CRUD2/CRUDList.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @file CRUDList.tsx
|
||||
* @desc 列表模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager, DSFeatureEnum} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDListPlugin extends BaseCRUDPlugin {
|
||||
static id = 'ListCRUDPlugin';
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
name = '列表';
|
||||
|
||||
panelTitle: '列表';
|
||||
|
||||
icon = 'fa fa-list';
|
||||
|
||||
panelIcon = 'fa fa-list';
|
||||
|
||||
subPanelIcon = 'fa fa-list';
|
||||
|
||||
pluginIcon = 'list-plugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'围绕列表的数据增删改查. 负责数据的拉取,分页,单条操作,批量操作,排序,快速编辑等等功能,集成查询条件。';
|
||||
|
||||
order = -1000;
|
||||
|
||||
$schema = '/schemas/CRUD2ListSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('list');
|
||||
|
||||
scaffold: any = this.generateScaffold('list');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
/** 先写入动态控件 */
|
||||
this.dynamicControls = {
|
||||
/** 列配置 */
|
||||
columns: context => this.renderColumnsControl(context),
|
||||
/** 工具栏配置 */
|
||||
toolbar: context => this.renderToolbarCollapse(context),
|
||||
/** 搜索栏 */
|
||||
filters: context => this.renderFiltersCollapse(context)
|
||||
};
|
||||
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
|
||||
@autobind
|
||||
renderColumnsControl(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
title: '列设置',
|
||||
order: 5,
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-column-control',
|
||||
name: 'columns',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderToolbarCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
|
||||
return {
|
||||
order: 20,
|
||||
title: '工具栏',
|
||||
body: [
|
||||
{
|
||||
type: 'ae-crud-toolbar-control',
|
||||
name: 'headerToolbar',
|
||||
nodeId: context.id,
|
||||
builder
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFiltersCollapse(context: BuildPanelEventContext) {
|
||||
const builder = this.dsManager.getBuilderBySchema(context.node.schema);
|
||||
const collection: any[] = [];
|
||||
|
||||
builder.features.forEach(feat => {
|
||||
if (/Query$/.test(feat)) {
|
||||
collection.push({
|
||||
type: 'ae-crud-filters-control',
|
||||
name:
|
||||
feat === DSFeatureEnum.SimpleQuery ||
|
||||
feat === DSFeatureEnum.AdvancedQuery
|
||||
? 'filter'
|
||||
: feat === DSFeatureEnum.FuzzyQuery
|
||||
? 'headerToolbar'
|
||||
: undefined,
|
||||
label:
|
||||
feat === DSFeatureEnum.SimpleQuery
|
||||
? '简单查询'
|
||||
: feat === DSFeatureEnum.AdvancedQuery
|
||||
? '高级查询'
|
||||
: '模糊查询',
|
||||
nodeId: context.id,
|
||||
feat: feat,
|
||||
builder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return collection.length > 0
|
||||
? {
|
||||
order: 10,
|
||||
title: '搜索设置',
|
||||
body: collection
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// registerEditorPlugin(CRUDListPlugin);
|
58
packages/amis-editor/src/plugin/CRUD2/CRUDTable.tsx
Normal file
58
packages/amis-editor/src/plugin/CRUD2/CRUDTable.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @file CRUDTable.tsx
|
||||
* @desc 表格模式的 CRUD2
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {autobind} from 'amis';
|
||||
import {
|
||||
EditorManager,
|
||||
JSONPipeIn,
|
||||
BuildPanelEventContext,
|
||||
EditorNodeType,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {
|
||||
DSBuilder,
|
||||
DSBuilderManager,
|
||||
DSFeatureEnum,
|
||||
DSFeatureType
|
||||
} from '../../builder';
|
||||
import {Table2RenderereEvent, Table2RendererAction} from '../Table2';
|
||||
import {BaseCRUDPlugin} from './BaseCRUD';
|
||||
|
||||
export class CRUDTablePlugin extends BaseCRUDPlugin {
|
||||
static id = 'TableCRUDPlugin';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
multifactor = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
description =
|
||||
'用来实现对数据的增删改查,用来展示表格数据,可以配置列信息,然后关联数据便能完成展示。支持嵌套、超级表头、列固定、表头固顶、合并单元格等等。';
|
||||
|
||||
order = -950;
|
||||
|
||||
$schema = '/schemas/CRUD2TableSchema.json';
|
||||
|
||||
docLink = '/amis/zh-CN/components/crud2';
|
||||
|
||||
previewSchema: Record<string, any> = this.generatePreviewSchema('table2');
|
||||
|
||||
scaffold: any = this.generateScaffold('table2');
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager, Table2RenderereEvent, Table2RendererAction);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
/** 非实体数据源走默认构建 */
|
||||
panelBodyCreator = (context: BuildPanelEventContext) => {
|
||||
return this.baseCRUDPanelBody(context);
|
||||
};
|
||||
}
|
||||
|
||||
registerEditorPlugin(CRUDTablePlugin);
|
51
packages/amis-editor/src/plugin/CRUD2/constants.ts
Normal file
51
packages/amis-editor/src/plugin/CRUD2/constants.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file constants.ts
|
||||
* @desc CRUD 配置相关常量
|
||||
*/
|
||||
|
||||
import {DSFeatureEnum} from '../../builder/constants';
|
||||
|
||||
export const ToolsConfig = {
|
||||
groupName: 'tools',
|
||||
options: [
|
||||
{
|
||||
label: '新增记录',
|
||||
value: 'Insert',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
label: '批量编辑',
|
||||
value: 'BulkEdit',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
value: 'BulkDelete',
|
||||
align: 'left',
|
||||
icon: 'fa fa-layer-group',
|
||||
order: 30
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const FiltersConfig = {
|
||||
groupName: 'filters',
|
||||
options: [
|
||||
{label: '模糊查询', value: 'FuzzyQuery', icon: 'fa fa-search', order: 10},
|
||||
{label: '简单查询', value: 'SimpleQuery', icon: 'fa fa-search', order: 20},
|
||||
{label: '高级查询', value: 'AdvancedQuery', icon: 'fa fa-search', order: 30}
|
||||
]
|
||||
};
|
||||
|
||||
export const OperatorsConfig = {
|
||||
groupName: 'operators',
|
||||
options: [
|
||||
{label: '查看详情', value: 'View', icon: 'fa fa-database', order: 10},
|
||||
{label: '编辑记录', value: 'Edit', icon: 'fa fa-database', order: 20},
|
||||
{label: '删除记录', value: 'Delete', icon: 'fa fa-database', order: 30}
|
||||
]
|
||||
};
|
194
packages/amis-editor/src/plugin/CRUD2/utils.ts
Normal file
194
packages/amis-editor/src/plugin/CRUD2/utils.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isObject from 'lodash/isObject';
|
||||
import remove from 'lodash/remove';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
export function findAndUpdate<T = any>(
|
||||
arr: T[],
|
||||
compareFn: (item: T) => boolean,
|
||||
target?: T
|
||||
) {
|
||||
if (!target) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const result = cloneDeep(arr);
|
||||
const idx = result.findIndex(item => compareFn(item));
|
||||
|
||||
if (~idx) {
|
||||
result.splice(idx, 1, target);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 深度删除 */
|
||||
export const deepRemove = (
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
checkAll: boolean = false
|
||||
): any => {
|
||||
const waitProcess = [obj];
|
||||
let find = false;
|
||||
|
||||
while (waitProcess.length) {
|
||||
if (find) {
|
||||
break;
|
||||
}
|
||||
|
||||
let item: any = waitProcess.pop();
|
||||
if (Array.isArray(item)) {
|
||||
remove(item, (val: any) => {
|
||||
const res = predicate(val);
|
||||
|
||||
if (res && !checkAll) {
|
||||
find = true;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
waitProcess.push(...item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObject(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
if (isObject(value) && predicate(value)) {
|
||||
delete item[key];
|
||||
checkAll || (find = true);
|
||||
}
|
||||
waitProcess.push(value);
|
||||
});
|
||||
}
|
||||
|
||||
return find;
|
||||
};
|
||||
|
||||
export const findObj = (
|
||||
obj: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
stop?: (obj: any) => boolean
|
||||
): any | void => {
|
||||
const waitProcess = [obj];
|
||||
|
||||
while (waitProcess.length) {
|
||||
let item: any = waitProcess.shift();
|
||||
if (Array.isArray(item)) {
|
||||
waitProcess.push(...item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isObject(item) || (stop && stop(item))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (predicate(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
waitProcess.push(
|
||||
...Object.values(
|
||||
pickBy(item, (val: any, key: string) => !String(key).startsWith('__'))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** schema 中查找 */
|
||||
export const findSchema = (
|
||||
schema: any,
|
||||
predicate: (obj: any) => boolean,
|
||||
...scope: string[]
|
||||
) => {
|
||||
if (scope.length === 0) {
|
||||
return findObj(schema, predicate);
|
||||
}
|
||||
let region = null;
|
||||
while ((region = scope.shift())) {
|
||||
const res = findObj(schema[region], predicate);
|
||||
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** headerToolbar 和 footerToolbar 布局换成 flex 包裹 container */
|
||||
export const addSchema2Toolbar = (
|
||||
schema: any,
|
||||
content: any,
|
||||
position: 'header' | 'footer',
|
||||
align: 'left' | 'right'
|
||||
) => {
|
||||
const region = `${position}Toolbar`;
|
||||
const buildFlex = (items: any[] = []) => ({
|
||||
type: 'flex',
|
||||
items,
|
||||
style: {
|
||||
position: 'static'
|
||||
},
|
||||
direction: 'row',
|
||||
justify: 'flex-start',
|
||||
alignItems: 'stretch'
|
||||
});
|
||||
const buildContainer = (align?: 'left' | 'right', body: any[] = []) => ({
|
||||
type: 'container',
|
||||
body,
|
||||
wrapperBody: false,
|
||||
style: {
|
||||
flexGrow: 1,
|
||||
flex: '1 1 auto',
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexBasis: 'auto',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'stretch',
|
||||
...(align
|
||||
? {
|
||||
justifyContent: align === 'left' ? 'flex-start' : 'flex-end'
|
||||
}
|
||||
: {})
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
!schema[region] ||
|
||||
isEmpty(schema[region]) ||
|
||||
!Array.isArray(schema[region])
|
||||
) {
|
||||
const isArr = Array.isArray(schema[region]);
|
||||
const newSchema = buildFlex([
|
||||
buildContainer('left', isArr || !schema[region] ? [] : [schema[region]]),
|
||||
buildContainer('right')
|
||||
]);
|
||||
|
||||
(isArr && schema[region].push(newSchema)) || (schema[region] = [newSchema]);
|
||||
}
|
||||
|
||||
// 尝试放到左面第一个,否则只能放外头了
|
||||
try {
|
||||
// 优先判断没有右边列的情况,避免都走到catch里造成嵌套层数过多的问题
|
||||
if (align === 'right' && schema[region][0].items.length < 2) {
|
||||
schema[region][0].items.push(buildContainer('right'));
|
||||
}
|
||||
|
||||
schema[region][0].items[
|
||||
align === 'left' ? 0 : schema[region][0].items.length - 1
|
||||
].body.push(content);
|
||||
} catch (e) {
|
||||
const olds = [...schema[region]];
|
||||
schema[region].length = 0;
|
||||
schema[region].push(
|
||||
buildFlex([
|
||||
buildContainer('left', olds),
|
||||
buildContainer('right', content)
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import {Button} from 'amis';
|
||||
import {Button, JSONValueMap, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -16,7 +16,8 @@ import {
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
|
||||
import {resolveArrayDatasource} from '../util';
|
||||
import set from 'lodash/set';
|
||||
import {escapeFormula, resolveArrayDatasource} from '../util';
|
||||
|
||||
export class CardsPlugin extends BasePlugin {
|
||||
static id = 'CardsPlugin';
|
||||
@ -28,6 +29,7 @@ export class CardsPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '卡片列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description =
|
||||
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/cards';
|
||||
@ -36,36 +38,330 @@ export class CardsPlugin extends BasePlugin {
|
||||
pluginIcon = 'cards-plugin';
|
||||
scaffold = {
|
||||
type: 'cards',
|
||||
data: {
|
||||
items: [
|
||||
{a: 1, b: 2},
|
||||
{a: 3, b: 4}
|
||||
]
|
||||
},
|
||||
columnsCount: 2,
|
||||
columnsCount: 4,
|
||||
card: {
|
||||
type: 'card',
|
||||
className: 'm-b-none',
|
||||
header: {
|
||||
title: '标题',
|
||||
subTitle: '副标题'
|
||||
},
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
name: 'a',
|
||||
label: 'A'
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
label: 'B'
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: 'fa fa-check',
|
||||
vendor: '',
|
||||
themeCss: {
|
||||
className: {
|
||||
'font': {
|
||||
color: 'var(--colors-brand-6)',
|
||||
fontSize: '20px'
|
||||
},
|
||||
'padding-and-margin:default': {
|
||||
marginRight: '10px'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '流水线任务实例 ',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
editorSetting: {
|
||||
mock: {}
|
||||
},
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
},
|
||||
{
|
||||
type: 'flex',
|
||||
className: 'p-1',
|
||||
items: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '12/',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '19',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-6)',
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block',
|
||||
flex: '0 0 auto',
|
||||
marginTop: 'var(--sizes-size-0)',
|
||||
marginRight: 'var(--sizes-size-0)',
|
||||
marginBottom: 'var(--sizes-size-0)',
|
||||
marginLeft: 'var(--sizes-size-0)'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '单元测试',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: '0 0 auto'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '100%',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '通过率',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '99.9%',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)',
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontWeight: 'var(--fonts-weight-3)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '任务实例',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'relative'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '报告',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '2023-01-01 12:00',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
color: 'var(--colors-neutral-text-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '20px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'block',
|
||||
'overflowY': 'auto',
|
||||
'overflowX': 'auto',
|
||||
'paddingTop': '10px',
|
||||
'paddingRight': '10px',
|
||||
'paddingBottom': '10px',
|
||||
'paddingLeft': '10px',
|
||||
'radius': {
|
||||
'top-left-border-radius': '6px',
|
||||
'top-right-border-radius': '6px',
|
||||
'bottom-left-border-radius': '6px',
|
||||
'bottom-right-border-radius': '6px'
|
||||
},
|
||||
'top-border-width': 'var(--borders-width-4)',
|
||||
'left-border-width': 'var(--borders-width-2)',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'bottom-border-width': 'var(--borders-width-2)',
|
||||
'top-border-style': 'var(--borders-style-2)',
|
||||
'left-border-style': 'var(--borders-style-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'bottom-border-style': 'var(--borders-style-2)',
|
||||
'top-border-color': 'var(--colors-brand-6)',
|
||||
'left-border-color': 'var(--colors-brand-10)',
|
||||
'right-border-color': 'var(--colors-brand-10)',
|
||||
'bottom-border-color': 'var(--colors-brand-10)',
|
||||
'flex': '0 0 150px',
|
||||
'marginRight': '15px',
|
||||
'flexBasis': '100%'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
onEvent: {
|
||||
click: {
|
||||
weight: 0,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'button'
|
||||
}
|
||||
]
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
width: '1000%',
|
||||
overflowX: 'auto',
|
||||
margin: '0',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
wrapperBody: false
|
||||
},
|
||||
placeholder: '',
|
||||
name: '',
|
||||
style: {
|
||||
gutterX: 15,
|
||||
gutterY: 15
|
||||
}
|
||||
};
|
||||
previewSchema = {
|
||||
@ -74,6 +370,7 @@ export class CardsPlugin extends BasePlugin {
|
||||
};
|
||||
|
||||
panelTitle = '卡片集';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const curPosition = context?.schema?.style?.position;
|
||||
@ -82,200 +379,119 @@ export class CardsPlugin extends BasePlugin {
|
||||
return [
|
||||
getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '常规',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
}),
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
children: (
|
||||
<div className="m-b">
|
||||
<Button
|
||||
level="primary"
|
||||
size="sm"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置单项信息
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
getSchemaTpl('title'),
|
||||
|
||||
isCRUDBody
|
||||
? null
|
||||
: {
|
||||
name: 'source',
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
label: '数据源',
|
||||
pipeIn: defaultValue('${items}'),
|
||||
description: '绑定当前环境变量',
|
||||
test: !isCRUDBody
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: [
|
||||
getSchemaTpl('switch', {
|
||||
name: 'showHeader',
|
||||
label: '是否显示头部',
|
||||
pipeIn: defaultValue(true)
|
||||
}),
|
||||
|
||||
getSchemaTpl('switch', {
|
||||
name: 'showFooter',
|
||||
label: '是否显示底部',
|
||||
pipeIn: defaultValue(true)
|
||||
}),
|
||||
|
||||
getSchemaTpl('className', {
|
||||
label: 'CSS 类名'
|
||||
}),
|
||||
getSchemaTpl('className', {
|
||||
name: 'headerClassName',
|
||||
label: '头部 CSS 类名'
|
||||
}),
|
||||
getSchemaTpl('className', {
|
||||
name: 'footerClassName',
|
||||
label: '底部 CSS 类名'
|
||||
}),
|
||||
getSchemaTpl('className', {
|
||||
name: 'itemsClassName',
|
||||
label: '内容 CSS 类名'
|
||||
}),
|
||||
getSchemaTpl('className', {
|
||||
pipeIn: defaultValue('Grid-col--sm6 Grid-col--md4 Grid-col--lg3'),
|
||||
name: 'itemClassName',
|
||||
label: '卡片 CSS 类名'
|
||||
}),
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 0,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行显示个数',
|
||||
description: '不设置时,由卡片 CSS 类名决定'
|
||||
title: '组件',
|
||||
body: [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 0,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行个数',
|
||||
description: '不设置时,由卡片 CSS 类名决定'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '左右间距',
|
||||
name: 'style.gutterX'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '上下间距',
|
||||
name: 'style.gutterY'
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'masonryLayout',
|
||||
label: '启用瀑布流'
|
||||
}),
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'masonryLayout',
|
||||
label: '启用瀑布流'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '显隐',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
];
|
||||
};
|
||||
|
||||
editDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(id);
|
||||
const value = store.getValueOf(id);
|
||||
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'cards',
|
||||
type: 'object',
|
||||
title: '当前列表项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
node &&
|
||||
value &&
|
||||
this.manager.openSubEditor({
|
||||
title: '配置成员渲染器',
|
||||
value: {
|
||||
type: 'card',
|
||||
...value.card
|
||||
},
|
||||
slot: {
|
||||
type: 'container',
|
||||
body: '$$'
|
||||
},
|
||||
typeMutable: false,
|
||||
onChange: newValue => {
|
||||
newValue = {...value, card: newValue};
|
||||
manager.panelChangeValue(newValue, diff(value, newValue));
|
||||
},
|
||||
data: {
|
||||
item: 'mocked data',
|
||||
index: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
buildEditorToolbar(
|
||||
{id, info, schema}: BaseEventContext,
|
||||
toolbars: Array<BasicToolbarItem>
|
||||
) {
|
||||
if (
|
||||
info.renderer.name === 'cards' ||
|
||||
(info.renderer.name === 'crud' && schema.mode === 'cards')
|
||||
) {
|
||||
toolbars.push({
|
||||
icon: 'fa fa-expand',
|
||||
order: 100,
|
||||
tooltip: '配置成员渲染器',
|
||||
onClick: this.editDetail.bind(this, id)
|
||||
// 列表添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
) {
|
||||
if (selections.length || info?.plugin !== this) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
info.renderer.name === 'cards' ||
|
||||
(info.renderer.name === 'crud' && schema.mode === 'cards')
|
||||
) {
|
||||
menus.push('|', {
|
||||
label: '配置成员渲染器',
|
||||
onSelect: this.editDetail.bind(this, id)
|
||||
});
|
||||
}
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
filterProps(props: any) {
|
||||
const data = {
|
||||
...props.defaultData,
|
||||
...props.data
|
||||
};
|
||||
const arr = resolveArrayDatasource({
|
||||
value: props.value,
|
||||
data,
|
||||
source: props.source
|
||||
});
|
||||
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
const mockedData: any = {
|
||||
id: 666,
|
||||
title: '假数据',
|
||||
description: '假数据',
|
||||
a: '假数据',
|
||||
b: '假数据'
|
||||
};
|
||||
|
||||
props.value = repeatArray(mockedData, 1).map((item, index) => ({
|
||||
// 编辑时显示两行假数据
|
||||
const count = (props.columnsCount || 3) * 2;
|
||||
props.value = repeatArray({}, count).map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
id: index + 1
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
|
||||
if (props.card && !props.card.className?.includes('listItem')) {
|
||||
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
|
||||
}
|
||||
|
||||
const {$schema, ...rest} = props;
|
||||
// 列表类型内的文本元素显示原始公式
|
||||
props = escapeFormula(props);
|
||||
|
||||
return {
|
||||
...JSONPipeOut(rest),
|
||||
$schema
|
||||
};
|
||||
return props;
|
||||
}
|
||||
|
||||
getRendererInfo(
|
||||
|
@ -216,131 +216,160 @@ export class CarouselPlugin extends BasePlugin {
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: [
|
||||
getSchemaTpl('switch', {
|
||||
name: 'auto',
|
||||
label: '自动轮播',
|
||||
pipeIn: defaultValue(true)
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-number'
|
||||
},
|
||||
name: 'interval',
|
||||
label: '动画间隔(ms)',
|
||||
valueType: 'number',
|
||||
pipeIn: defaultValue(5000)
|
||||
}),
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'input-number',
|
||||
label: '动画时长(ms)',
|
||||
min: 100,
|
||||
step: 10,
|
||||
size: 'sm',
|
||||
pipeIn: defaultValue(500)
|
||||
},
|
||||
{
|
||||
name: 'animation',
|
||||
label: '动画效果',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('fade'),
|
||||
options: [
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('switch', {
|
||||
name: 'auto',
|
||||
label: '自动轮播',
|
||||
pipeIn: defaultValue(true)
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-number'
|
||||
},
|
||||
name: 'interval',
|
||||
label: '动画间隔(ms)',
|
||||
valueType: 'number',
|
||||
pipeIn: defaultValue(5000)
|
||||
}),
|
||||
{
|
||||
label: 'fade',
|
||||
value: 'fade'
|
||||
name: 'duration',
|
||||
type: 'input-number',
|
||||
label: '动画时长(ms)',
|
||||
min: 100,
|
||||
step: 10,
|
||||
size: 'sm',
|
||||
pipeIn: defaultValue(500)
|
||||
},
|
||||
{
|
||||
label: 'slide',
|
||||
value: 'slide'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'controlsTheme',
|
||||
label: '控制按钮主题',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('light'),
|
||||
options: [
|
||||
{
|
||||
label: 'light',
|
||||
value: 'light'
|
||||
name: 'animation',
|
||||
label: '动画效果',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('fade'),
|
||||
options: [
|
||||
{
|
||||
label: 'fade',
|
||||
value: 'fade'
|
||||
},
|
||||
{
|
||||
label: 'slide',
|
||||
value: 'slide'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'dark',
|
||||
value: 'dark'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'controls',
|
||||
label: '控制显示',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('dots,arrows'),
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
label: '底部圆点',
|
||||
value: 'dots'
|
||||
name: 'controlsTheme',
|
||||
label: '控制按钮主题',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('light'),
|
||||
options: [
|
||||
{
|
||||
label: 'light',
|
||||
value: 'light'
|
||||
},
|
||||
{
|
||||
label: 'dark',
|
||||
value: 'dark'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '左右箭头',
|
||||
value: 'arrows'
|
||||
}
|
||||
]
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'alwaysShowArrow',
|
||||
label: '箭头一直显示',
|
||||
clearValueOnHidden: true,
|
||||
hiddenOn: '!~this.controls.indexOf("arrows")',
|
||||
pipeIn: defaultValue(false)
|
||||
}),
|
||||
{
|
||||
type: 'ae-switch-more',
|
||||
bulk: true,
|
||||
mode: 'normal',
|
||||
name: 'multiple',
|
||||
label: '多图展示',
|
||||
formType: 'extend',
|
||||
form: {
|
||||
body: [
|
||||
{
|
||||
name: 'multiple.count',
|
||||
label: '数量',
|
||||
type: 'input-number',
|
||||
min: 2,
|
||||
step: 1
|
||||
name: 'controls',
|
||||
label: '控制显示',
|
||||
type: 'button-group-select',
|
||||
pipeIn: defaultValue('dots,arrows'),
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
label: '底部圆点',
|
||||
value: 'dots'
|
||||
},
|
||||
{
|
||||
label: '左右箭头',
|
||||
value: 'arrows'
|
||||
}
|
||||
]
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'alwaysShowArrow',
|
||||
label: '箭头一直显示',
|
||||
clearValueOnHidden: true,
|
||||
hiddenOn: '!~this.controls.indexOf("arrows")',
|
||||
pipeIn: defaultValue(false)
|
||||
}),
|
||||
{
|
||||
type: 'ae-switch-more',
|
||||
bulk: true,
|
||||
mode: 'normal',
|
||||
name: 'multiple',
|
||||
label: '多图展示',
|
||||
formType: 'extend',
|
||||
form: {
|
||||
body: [
|
||||
{
|
||||
name: 'multiple.count',
|
||||
label: '数量',
|
||||
type: 'input-number',
|
||||
min: 2,
|
||||
step: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'width',
|
||||
type: 'input-text',
|
||||
label: '宽度',
|
||||
validations: 'isNumeric',
|
||||
addOn: {
|
||||
type: 'button',
|
||||
label: 'px'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'height',
|
||||
type: 'input-text',
|
||||
label: '高度',
|
||||
validations: 'isNumeric',
|
||||
addOn: {
|
||||
type: 'button',
|
||||
label: 'px'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'width',
|
||||
type: 'input-text',
|
||||
label: '宽度',
|
||||
validations: 'isNumeric',
|
||||
addOn: {
|
||||
type: 'button',
|
||||
label: 'px'
|
||||
}
|
||||
title: '显隐',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
},
|
||||
getSchemaTpl('theme:base', {
|
||||
title: '轮播图'
|
||||
}),
|
||||
{
|
||||
name: 'height',
|
||||
type: 'input-text',
|
||||
label: '高度',
|
||||
validations: 'isNumeric',
|
||||
addOn: {
|
||||
type: 'button',
|
||||
label: 'px'
|
||||
}
|
||||
title: '其他',
|
||||
body: [
|
||||
{
|
||||
name: 'themeCss.baseControlClassName.--image-images-prev-icon',
|
||||
label: '左切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
{
|
||||
name: 'themeCss.baseControlClassName.--image-images-next-icon',
|
||||
label: '右切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
getSchemaTpl('theme:select', {
|
||||
label: '切换图标大小',
|
||||
name: 'themeCss.galleryControlClassName.width:default'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('className')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '显隐',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
getSchemaTpl('theme:cssCode')
|
||||
])
|
||||
}
|
||||
])
|
||||
];
|
||||
|
@ -1,3 +1,4 @@
|
||||
import update from 'lodash/update';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -10,24 +11,47 @@ import {
|
||||
|
||||
export class ColumnToggler extends BasePlugin {
|
||||
static id = 'ColumnToggler';
|
||||
// 关联渲染器名字
|
||||
|
||||
rendererName = 'column-toggler';
|
||||
$schema = '/schemas/ColumnToggler.json';
|
||||
|
||||
// 组件名称
|
||||
name = '自定义显示列';
|
||||
isBaseComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
|
||||
|
||||
tags = ['自定义显示列'];
|
||||
icon = 'fa fa-square';
|
||||
|
||||
panelTitle = '自定义显示列';
|
||||
|
||||
icon = 'fa fa-square';
|
||||
|
||||
tags = ['自定义显示列'];
|
||||
|
||||
$schema = '/schemas/ColumnTogglerSchema.json';
|
||||
|
||||
description = '用来展示表格的自定义显示列按钮,你可以配置不同的展示样式。';
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
isBaseComponent = true;
|
||||
|
||||
disabledRendererPlugin = true;
|
||||
|
||||
crudInfo: {id: any; columns: any[]; schema: any};
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const crud = context?.node?.getClosestParentByType('crud2');
|
||||
|
||||
if (crud) {
|
||||
this.crudInfo = {
|
||||
id: crud.id,
|
||||
columns: crud.schema.columns || [],
|
||||
schema: crud.schema
|
||||
};
|
||||
}
|
||||
|
||||
const columns = (this.crudInfo?.schema?.columns ?? []).map(
|
||||
(item: any, index: number) => ({
|
||||
label: item.title,
|
||||
value: index
|
||||
})
|
||||
);
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
@ -53,6 +77,64 @@ export class ColumnToggler extends BasePlugin {
|
||||
label: '按钮图标'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '列默认显示',
|
||||
body: [
|
||||
{
|
||||
name: `__toggled`,
|
||||
value: '',
|
||||
type: 'checkboxes',
|
||||
// className: 'b-a p-sm',
|
||||
label: false,
|
||||
inline: false,
|
||||
joinValues: false,
|
||||
extractValue: true,
|
||||
options: columns,
|
||||
// style: {
|
||||
// maxHeight: '200px',
|
||||
// overflow: 'auto'
|
||||
// },
|
||||
pipeIn: (value: any, form: any) => {
|
||||
const showColumnIndex: number[] = [];
|
||||
this.crudInfo?.schema?.columns?.forEach(
|
||||
(item: any, index: number) => {
|
||||
if (item.toggled !== false) {
|
||||
showColumnIndex.push(index);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return showColumnIndex;
|
||||
},
|
||||
onChange: (value: number[]) => {
|
||||
if (!this.crudInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newColumns = this.crudInfo.schema.columns;
|
||||
|
||||
newColumns = newColumns.map((item: any, index: number) => ({
|
||||
...item,
|
||||
toggled: value.includes(index) ? undefined : false
|
||||
}));
|
||||
|
||||
const updatedSchema = update(
|
||||
this.crudInfo.schema,
|
||||
'columns',
|
||||
(origin: any) => {
|
||||
return newColumns;
|
||||
}
|
||||
);
|
||||
|
||||
this.manager.store.changeValueById(
|
||||
this.crudInfo.id,
|
||||
updatedSchema
|
||||
);
|
||||
this.crudInfo.schema = updatedSchema;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -304,11 +304,7 @@ export class ContainerPlugin extends LayoutBasePlugin {
|
||||
title: '外观',
|
||||
className: 'p-none',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']}),
|
||||
{
|
||||
title: '自定义 CSS 类名',
|
||||
body: [getSchemaTpl('className')]
|
||||
}
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
},
|
||||
{
|
||||
|
@ -30,14 +30,94 @@ export class DividerPlugin extends BasePlugin {
|
||||
panelBody = getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '外观',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
getSchemaTpl('layout:width:v2', {
|
||||
visibleOn:
|
||||
'data.style && data.style.position && (data.style.position === "fixed" || data.style.position === "absolute")'
|
||||
}),
|
||||
getSchemaTpl('className')
|
||||
]
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基本样式',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
getSchemaTpl('layout:width:v2', {
|
||||
visibleOn:
|
||||
'data.style && data.style.position && (data.style.position === "fixed" || data.style.position === "absolute")'
|
||||
}),
|
||||
{
|
||||
mode: 'horizontal',
|
||||
type: 'button-group-select',
|
||||
label: '类型',
|
||||
name: 'lineStyle',
|
||||
value: 'dashed',
|
||||
options: [
|
||||
{
|
||||
value: 'dashed',
|
||||
label: '虚线'
|
||||
},
|
||||
{
|
||||
value: 'solid',
|
||||
label: '实线'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
mode: 'horizontal',
|
||||
type: 'button-group-select',
|
||||
label: '方向',
|
||||
name: 'direction',
|
||||
value: 'horizontal',
|
||||
options: [
|
||||
{
|
||||
value: 'horizontal',
|
||||
label: '水平'
|
||||
},
|
||||
{
|
||||
value: 'vertical',
|
||||
label: '垂直'
|
||||
}
|
||||
]
|
||||
},
|
||||
getSchemaTpl('theme:select', {
|
||||
mode: 'horizontal',
|
||||
label: '长度',
|
||||
name: 'style.width',
|
||||
placeholder: '100%',
|
||||
visibleOn: 'direction !== "vertical"',
|
||||
clearValueOnHidden: true
|
||||
}),
|
||||
getSchemaTpl('theme:select', {
|
||||
mode: 'horizontal',
|
||||
label: '长度',
|
||||
name: 'style.height',
|
||||
placeholder: 'var(--sizes-base-15)',
|
||||
visibleOn: 'direction === "vertical"',
|
||||
clearValueOnHidden: true
|
||||
}),
|
||||
getSchemaTpl('theme:select', {
|
||||
mode: 'horizontal',
|
||||
label: '宽度',
|
||||
name: 'style.borderWidth',
|
||||
placeholder: '1px'
|
||||
}),
|
||||
|
||||
getSchemaTpl('theme:colorPicker', {
|
||||
mode: 'horizontal',
|
||||
label: '颜色',
|
||||
name: 'color',
|
||||
placeholder: 'var(--colors-neutral-line-8)',
|
||||
labelMode: 'input',
|
||||
needGradient: true
|
||||
}),
|
||||
getSchemaTpl('theme:paddingAndMargin', {
|
||||
name: 'style',
|
||||
hidePadding: true
|
||||
}),
|
||||
{
|
||||
mode: 'horizontal',
|
||||
type: 'input-number',
|
||||
label: '角度',
|
||||
name: 'rotate',
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '显隐',
|
||||
|
@ -1,16 +1,10 @@
|
||||
import {Button} from 'amis';
|
||||
import {isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
BasicToolbarItem,
|
||||
ContextMenuEventContext,
|
||||
ContextMenuItem
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut} from 'amis-editor-core';
|
||||
import {schemaToArray} from '../util';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {BaseEventContext, BasePlugin} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {escapeFormula} from '../util';
|
||||
import {set} from 'lodash';
|
||||
|
||||
export class EachPlugin extends BasePlugin {
|
||||
static id = 'EachPlugin';
|
||||
@ -22,132 +16,355 @@ export class EachPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '循环 Each';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description = '功能渲染器,可以基于现有变量循环输出渲染器。';
|
||||
tags = ['功能'];
|
||||
icon = 'fa fa-repeat';
|
||||
pluginIcon = 'each-plugin';
|
||||
scaffold = {
|
||||
type: 'each',
|
||||
name: 'arr',
|
||||
name: '',
|
||||
items: {
|
||||
type: 'tpl',
|
||||
tpl: '<%= data.index + 1 %>. 内容:<%= data.item %>',
|
||||
wrapperComponent: '',
|
||||
inline: false
|
||||
}
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'icon',
|
||||
icon: 'fa fa-plane',
|
||||
vendor: '',
|
||||
themeCss: {
|
||||
className: {
|
||||
'padding-and-margin:default': {
|
||||
marginRight: '4px'
|
||||
},
|
||||
'font': {
|
||||
color: '#2856ad',
|
||||
fontSize: '20px'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
style: {
|
||||
fontWeight: 'var(--fonts-weight-3)',
|
||||
fontSize: '16px',
|
||||
color: 'var(--colors-brand-6)'
|
||||
},
|
||||
tpl: '回访数量TOP1',
|
||||
inline: true,
|
||||
wrapperComponent: ''
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginBottom: '6px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '北京分公司',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
'fontSize': 'var(--fonts-size-4)',
|
||||
'color': 'var(--colors-neutral-text-2)',
|
||||
'fontWeight': 'var(--fonts-weight-3)',
|
||||
'font-family': '-apple-system'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block'
|
||||
},
|
||||
wrapperBody: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'block',
|
||||
flex: '0 0 150px',
|
||||
marginRight: '20px',
|
||||
paddingTop: '20px',
|
||||
paddingRight: '15px',
|
||||
paddingBottom: '20px',
|
||||
paddingLeft: '15px',
|
||||
flexBasis: '250px',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
boxShadow: ' 0px 0px 8px 0px rgba(3, 3, 3, 0.1)',
|
||||
radius: {
|
||||
'top-left-border-radius': 'var(--borders-radius-3)',
|
||||
'top-right-border-radius': 'var(--borders-radius-3)',
|
||||
'bottom-left-border-radius': 'var(--borders-radius-3)',
|
||||
'bottom-right-border-radius': 'var(--borders-radius-3)'
|
||||
}
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false
|
||||
},
|
||||
placeholder: '',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
marginTop: '10px',
|
||||
marginBottom: '10px'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
size: 'none'
|
||||
};
|
||||
|
||||
previewSchema = {
|
||||
...this.scaffold,
|
||||
value: ['a', 'b', 'c']
|
||||
};
|
||||
|
||||
panelTitle = '循环';
|
||||
panelJustify = true;
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
return [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'name',
|
||||
label: '关联字段',
|
||||
placeholder: 'varname',
|
||||
description:
|
||||
'如果所在容器有下发 value 则不需要配置,如果没有请配置变量名,支持多层级如:a.b,表示关联a对象下的b属性。目标变量可以是数组,也可以是对象。'
|
||||
},
|
||||
const curRendererSchema = context?.schema;
|
||||
const isFreeContainer = curRendererSchema?.isFreeContainer || false;
|
||||
const isFlexItem = this.manager?.isFlexItem(context?.id);
|
||||
const isFlexColumnItem = this.manager?.isFlexColumnItem(context?.id);
|
||||
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
size="sm"
|
||||
level="primary"
|
||||
className="m-b"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置成员渲染器
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
const displayTpl = [
|
||||
getSchemaTpl('layout:display'),
|
||||
|
||||
getSchemaTpl('placeholder', {
|
||||
label: '占位符',
|
||||
pipeIn: defaultValue('暂无内容'),
|
||||
description:
|
||||
'当没有关联变量,或者目标变量不是数组或者对象时显示此占位信息'
|
||||
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('className')
|
||||
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: 'input-text',
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名',
|
||||
paramType: 'output'
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-number',
|
||||
min: 1
|
||||
},
|
||||
name: 'maxLength',
|
||||
label: '最大显示个数',
|
||||
valueType: 'number'
|
||||
}),
|
||||
getSchemaTpl('valueFormula', {
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
},
|
||||
name: 'placeholder',
|
||||
label: '空数据提示'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '布局',
|
||||
body: [
|
||||
getSchemaTpl('layout:padding'),
|
||||
getSchemaTpl('layout:position', {
|
||||
visibleOn: '!data.stickyStatus'
|
||||
}),
|
||||
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")'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-grow', {
|
||||
visibleOn:
|
||||
'data.style && data.style.flex === "1 1 auto" && (data.style.position === "static" || data.style.position === "relative")'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '弹性高度' : '弹性宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "1 1 auto"'
|
||||
})
|
||||
: null,
|
||||
isFlexItem
|
||||
? getSchemaTpl('layout:flex-basis', {
|
||||
label: isFlexColumnItem ? '固定高度' : '固定宽度',
|
||||
visibleOn:
|
||||
'data.style && (data.style.position === "static" || data.style.position === "relative") && data.style.flex === "0 0 150px"'
|
||||
})
|
||||
: null,
|
||||
|
||||
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']})
|
||||
])
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
filterProps(props: any) {
|
||||
props = JSONPipeOut(props);
|
||||
// 列表类型内的文本元素显示{{公式}}或者自定义展位,不显示实际值
|
||||
props = escapeFormula(props);
|
||||
// 循环编辑态显示2个元素
|
||||
props.value = [{}, {}];
|
||||
|
||||
// 至少显示一个成员,否则啥都不显示。
|
||||
if (!props.value) {
|
||||
props.value = [
|
||||
{
|
||||
item: 'mocked data'
|
||||
}
|
||||
];
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
if (props.items && !props.items.className?.includes('listItem')) {
|
||||
props.items.className = `${
|
||||
props.items.className || ''
|
||||
} ae-Editor-eachItem`;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
buildEditorToolbar(
|
||||
{id, info}: BaseEventContext,
|
||||
toolbars: Array<BasicToolbarItem>
|
||||
) {
|
||||
if (info.renderer.name === 'each') {
|
||||
toolbars.push({
|
||||
icon: 'fa fa-expand',
|
||||
order: 100,
|
||||
tooltip: '配置成员渲染器',
|
||||
onClick: this.editDetail.bind(this, id)
|
||||
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'each',
|
||||
type: 'object',
|
||||
title: '当前循环项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 循环添加索引方便渲染序号
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
) {
|
||||
if (selections.length || info?.plugin !== this) {
|
||||
return;
|
||||
}
|
||||
if (info.renderer.name === 'each') {
|
||||
menus.push('|', {
|
||||
label: '配置成员渲染器',
|
||||
onSelect: this.editDetail.bind(this, id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
const node = store.getNodeById(id);
|
||||
const value = store.getValueOf(id);
|
||||
|
||||
node &&
|
||||
value &&
|
||||
this.manager.openSubEditor({
|
||||
title: '配置成员渲染器',
|
||||
value: schemaToArray(value.items),
|
||||
slot: {
|
||||
type: 'container',
|
||||
body: '$$'
|
||||
},
|
||||
typeMutable: true,
|
||||
onChange: newValue => {
|
||||
newValue = {...value, items: newValue};
|
||||
manager.panelChangeValue(newValue, diff(value, newValue));
|
||||
},
|
||||
data: {
|
||||
item: 'mocked data',
|
||||
index: 0
|
||||
}
|
||||
});
|
||||
return dataSchema;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -11,11 +12,9 @@ import {
|
||||
RegionConfig,
|
||||
getI18nEnabled,
|
||||
EditorNodeType,
|
||||
EditorManager,
|
||||
DSBuilderManager
|
||||
EditorManager
|
||||
} from 'amis-editor-core';
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
|
||||
import {DSBuilderManager} from '../../builder/DSBuilderManager';
|
||||
import {ValidatorTag} from '../../validator';
|
||||
import {
|
||||
getArgsWrapper,
|
||||
@ -240,11 +239,11 @@ export class ComboControlPlugin extends BasePlugin {
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
dsBuilderManager: DSBuilderManager;
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsBuilderManager = new DSBuilderManager('combo', 'api');
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
@ -750,14 +749,11 @@ export class ComboControlPlugin extends BasePlugin {
|
||||
(target.parent.isRegion && target.parent.region === 'items')
|
||||
) {
|
||||
scope = scopeNode.parent.parent;
|
||||
builder = this.dsBuilderManager.resolveBuilderBySchema(
|
||||
scope.schema,
|
||||
'api'
|
||||
);
|
||||
builder = this.dsManager.getBuilderBySchema(scope.schema);
|
||||
}
|
||||
|
||||
if (builder && scope.schema.api) {
|
||||
return builder.getAvailableContextFileds(
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scope.schema,
|
||||
sourceKey: 'api',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -459,7 +459,7 @@ export class ImageControlPlugin extends BasePlugin {
|
||||
[
|
||||
getSchemaTpl('style:formItem', {renderer: context.info.renderer}),
|
||||
{
|
||||
title: '自定义样式',
|
||||
title: '基本样式',
|
||||
body: [
|
||||
{
|
||||
type: 'select',
|
||||
@ -498,12 +498,12 @@ export class ImageControlPlugin extends BasePlugin {
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
getSchemaTpl('theme:size', {
|
||||
getSchemaTpl('theme:select', {
|
||||
name: `${IconCssClassName}.font-size`,
|
||||
label: '图标大小',
|
||||
editorThemePath: `${editorPath}.default.body.icon-size`
|
||||
}),
|
||||
getSchemaTpl('theme:size', {
|
||||
getSchemaTpl('theme:select', {
|
||||
name: `${IconCssClassName}.margin-bottom`,
|
||||
label: '图标底边距',
|
||||
editorThemePath: `${editorPath}.default.body.icon-margin`
|
||||
|
@ -15,10 +15,10 @@ import {
|
||||
repeatArray,
|
||||
mockValue,
|
||||
EditorNodeType,
|
||||
EditorManager,
|
||||
DSBuilderManager
|
||||
EditorManager
|
||||
} from 'amis-editor-core';
|
||||
import {getTreeAncestors, setVariable, someTree} from 'amis-core';
|
||||
import {setVariable, someTree} from 'amis-core';
|
||||
import {DSBuilderManager} from '../../builder/DSBuilderManager';
|
||||
import {ValidatorTag} from '../../validator';
|
||||
import {
|
||||
getEventControlConfig,
|
||||
@ -814,11 +814,11 @@ export class TableControlPlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
dsBuilderManager: DSBuilderManager;
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsBuilderManager = new DSBuilderManager('input-table', 'api');
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
@ -1064,12 +1064,14 @@ export class TableControlPlugin extends BasePlugin {
|
||||
filterProps(props: any) {
|
||||
const arr = resolveArrayDatasource(props);
|
||||
|
||||
/** 可 */
|
||||
if (!Array.isArray(arr) || !arr.length) {
|
||||
const mockedData: any = {};
|
||||
|
||||
if (Array.isArray(props.columns)) {
|
||||
props.columns.forEach((column: any) => {
|
||||
if (column.name) {
|
||||
/** 可编辑状态下不写入 Mock 数据,避免误导用户 */
|
||||
if (column.name && !props.editable) {
|
||||
setVariable(mockedData, column.name, mockValue(column));
|
||||
}
|
||||
});
|
||||
@ -1197,14 +1199,11 @@ export class TableControlPlugin extends BasePlugin {
|
||||
(target.parent.isRegion && target.parent.region === 'columns')
|
||||
) {
|
||||
scope = scopeNode.parent.parent;
|
||||
builder = this.dsBuilderManager.resolveBuilderBySchema(
|
||||
scope.schema,
|
||||
'api'
|
||||
);
|
||||
builder = this.dsManager.getBuilderBySchema(scope.schema);
|
||||
}
|
||||
|
||||
if (builder && scope.schema.api) {
|
||||
return builder.getAvailableContextFileds(
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scope.schema,
|
||||
sourceKey: 'api',
|
||||
|
@ -126,9 +126,9 @@ export class IconPlugin extends BasePlugin {
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '自定义样式',
|
||||
title: '基本样式',
|
||||
body: [
|
||||
getSchemaTpl('theme:size', {
|
||||
getSchemaTpl('theme:select', {
|
||||
label: '尺寸',
|
||||
name: 'themeCss.className.font.fontSize'
|
||||
}),
|
||||
|
@ -90,6 +90,14 @@ export class ImagePlugin extends BasePlugin {
|
||||
label: '缩略图地址',
|
||||
description: '如果已绑定字段名,可以不用设置,支持用变量。'
|
||||
}),
|
||||
|
||||
getSchemaTpl('backgroundImageUrl', {
|
||||
name: 'editorSetting.mock.src',
|
||||
label: tipedLabel(
|
||||
'假数据图片',
|
||||
'只在编辑区显示的模拟图片,运行时将显示图片实际内容'
|
||||
)
|
||||
}),
|
||||
{
|
||||
type: 'ae-switch-more',
|
||||
mode: 'normal',
|
||||
@ -139,11 +147,17 @@ export class ImagePlugin extends BasePlugin {
|
||||
// name: 'showDimensions',
|
||||
// label: '显示图片尺寸'
|
||||
// }),
|
||||
getSchemaTpl('layout:display', {
|
||||
flexHide: true,
|
||||
value: 'inline-block',
|
||||
label: '显示类型'
|
||||
}),
|
||||
|
||||
{
|
||||
name: 'thumbMode',
|
||||
visibleOn: 'imageMode === "thumb"',
|
||||
type: 'select',
|
||||
label: '缩略图展示模式',
|
||||
label: '展示模式',
|
||||
mode: 'horizontal',
|
||||
labelAlign: 'left',
|
||||
horizontal: {
|
||||
@ -173,49 +187,63 @@ export class ImagePlugin extends BasePlugin {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'thumbRatio',
|
||||
type: 'button-group-select',
|
||||
label: '缩略图比率',
|
||||
size: 'sm',
|
||||
pipeIn: defaultValue('1:1'),
|
||||
options: [
|
||||
{
|
||||
label: '1:1',
|
||||
value: '1:1'
|
||||
},
|
||||
|
||||
{
|
||||
label: '4:3',
|
||||
value: '4:3'
|
||||
},
|
||||
|
||||
{
|
||||
label: '16:9',
|
||||
value: '16:9'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'CSS类名',
|
||||
body: [
|
||||
getSchemaTpl('className', {
|
||||
label: '外层'
|
||||
}),
|
||||
|
||||
getSchemaTpl('className', {
|
||||
name: 'imageClassName',
|
||||
label: '图片'
|
||||
}),
|
||||
|
||||
getSchemaTpl('className', {
|
||||
name: 'thumbClassName',
|
||||
label: '缩略图'
|
||||
getSchemaTpl('theme:size', {
|
||||
label: '尺寸',
|
||||
name: 'themeCss.imageControlClassName.size:default'
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'imageControlClassName',
|
||||
title: '图片'
|
||||
}),
|
||||
{
|
||||
title: '其他',
|
||||
body: [
|
||||
getSchemaTpl('theme:font', {
|
||||
label: '标题文字',
|
||||
name: 'themeCss.titleControlClassName.font',
|
||||
editorThemePath: 'image.image.default.normal.body.font'
|
||||
}),
|
||||
getSchemaTpl('theme:paddingAndMargin', {
|
||||
label: '标题边距',
|
||||
name: 'themeCss.titleControlClassName.padding-and-margin'
|
||||
}),
|
||||
getSchemaTpl('theme:font', {
|
||||
label: '描述文字',
|
||||
name: 'themeCss.desControlClassName.font',
|
||||
editorThemePath: 'image.image.default.description.body.font'
|
||||
}),
|
||||
getSchemaTpl('theme:paddingAndMargin', {
|
||||
label: '描述边距',
|
||||
name: 'themeCss.desControlClassName.padding-and-margin'
|
||||
}),
|
||||
{
|
||||
name: 'themeCss.iconControlClassName.--image-image-normal-icon',
|
||||
label: '放大图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
{
|
||||
name: 'themeCss.galleryControlClassName.--image-images-prev-icon',
|
||||
label: '左切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
{
|
||||
name: 'themeCss.galleryControlClassName.--image-images-next-icon',
|
||||
label: '右切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
getSchemaTpl('theme:select', {
|
||||
label: '切换图标大小',
|
||||
name: 'themeCss.galleryControlClassName.--image-images-item-size'
|
||||
})
|
||||
]
|
||||
},
|
||||
getSchemaTpl('theme:cssCode')
|
||||
])
|
||||
}
|
||||
]);
|
||||
|
@ -211,20 +211,32 @@ export class ImagesPlugin extends BasePlugin {
|
||||
}
|
||||
]
|
||||
},
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'imagesControlClassName',
|
||||
title: '图片集'
|
||||
}),
|
||||
{
|
||||
title: 'CSS类名',
|
||||
title: '其他',
|
||||
body: [
|
||||
getSchemaTpl('className', {
|
||||
autoComplete: false,
|
||||
label: '外层'
|
||||
}),
|
||||
|
||||
getSchemaTpl('className', {
|
||||
name: 'listClassName',
|
||||
label: '图片列表'
|
||||
{
|
||||
name: 'themeCss.galleryControlClassName.--image-images-prev-icon',
|
||||
label: '左切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
{
|
||||
name: 'themeCss.galleryControlClassName.--image-images-next-icon',
|
||||
label: '右切换图标',
|
||||
type: 'icon-select',
|
||||
returnSvg: true
|
||||
},
|
||||
getSchemaTpl('theme:select', {
|
||||
label: '切换图标大小',
|
||||
name: 'themeCss.galleryControlClassName.--image-images-item-size'
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
getSchemaTpl('theme:cssCode')
|
||||
])
|
||||
}
|
||||
]);
|
||||
|
@ -208,11 +208,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
title: '外观',
|
||||
className: 'p-none',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']}),
|
||||
{
|
||||
title: '自定义 CSS 类名',
|
||||
body: [getSchemaTpl('className')]
|
||||
}
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
@ -236,6 +232,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
const isFlexItem = this.manager?.isFlexItem(id);
|
||||
const isFlexColumnItem = this.manager?.isFlexColumnItem(id);
|
||||
const newColumnSchema = defaultFlexColumnSchema('新的一列');
|
||||
const canAppendSiblings = this.manager?.canAppendSiblings();
|
||||
|
||||
const toolbarsTooltips: any = {};
|
||||
toolbars.forEach(toolbar => {
|
||||
@ -249,7 +246,8 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
(info.renderer?.name === 'flex' || info.renderer?.name === 'container') &&
|
||||
!isFlexItem && // 备注:如果是列级元素就不需要显示了
|
||||
!draggableContainer &&
|
||||
!schema?.isFreeContainer
|
||||
!schema?.isFreeContainer &&
|
||||
canAppendSiblings
|
||||
) {
|
||||
// 非特殊布局元素(fixed、absolute)支持前后插入追加布局元素功能icon
|
||||
if (!toolbarsTooltips['上方插入布局容器']) {
|
||||
@ -298,7 +296,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFlexItem && !draggableContainer) {
|
||||
if (isFlexItem && !draggableContainer && canAppendSiblings) {
|
||||
if (
|
||||
!toolbarsTooltips[`${isFlexColumnItem ? '上方' : '左侧'}插入列级容器`]
|
||||
) {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {BasePlugin, RegionConfig, RendererInfo} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {tipedLabel} from 'amis-editor-core';
|
||||
import {
|
||||
registerEditorPlugin,
|
||||
BasePlugin,
|
||||
getSchemaTpl,
|
||||
tipedLabel
|
||||
} from 'amis-editor-core';
|
||||
|
||||
export class LinkPlugin extends BasePlugin {
|
||||
static id = 'LinkPlugin';
|
||||
@ -37,15 +39,23 @@ export class LinkPlugin extends BasePlugin {
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
{
|
||||
getSchemaTpl('valueFormula', {
|
||||
name: 'href',
|
||||
type: 'input-text',
|
||||
label: tipedLabel(
|
||||
'目标地址',
|
||||
'支持取变量,如果已绑定字段名,可以不用设置'
|
||||
)
|
||||
),
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
}
|
||||
}),
|
||||
{
|
||||
label: tipedLabel('内容', '不填写时,自动使用目标地址值'),
|
||||
type: 'ae-textareaFormulaControl',
|
||||
mode: 'normal',
|
||||
pipeIn: (value: any, data: any) => value || (data && data.html),
|
||||
name: 'body'
|
||||
},
|
||||
getSchemaTpl('inputBody'),
|
||||
getSchemaTpl('switch', {
|
||||
name: 'blank',
|
||||
label: '在新窗口打开'
|
||||
|
@ -1,6 +1,10 @@
|
||||
import {Button} from 'amis';
|
||||
import {Button, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {getI18nEnabled, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
EditorNodeType,
|
||||
getI18nEnabled,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
@ -13,6 +17,7 @@ import {
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {diff, JSONPipeOut, repeatArray} from 'amis-editor-core';
|
||||
import set from 'lodash/set';
|
||||
import {
|
||||
schemaArrayFormat,
|
||||
resolveArrayDatasource,
|
||||
@ -28,6 +33,8 @@ export class ListPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description =
|
||||
'展示一个列表,可以自定标题、副标题,内容及按钮组部分。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/list';
|
||||
@ -64,7 +71,7 @@ export class ListPlugin extends BasePlugin {
|
||||
panelTitle = '列表';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const isCRUDBody = ['crud', 'crud2'].includes(context.schema.type);
|
||||
const i18nEnabled = getI18nEnabled();
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
@ -73,21 +80,21 @@ export class ListPlugin extends BasePlugin {
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
level="primary"
|
||||
size="sm"
|
||||
block
|
||||
onClick={this.editDetail.bind(this, context.id)}
|
||||
>
|
||||
配置成员渲染器
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
// {
|
||||
// children: (
|
||||
// <Button
|
||||
// level="primary"
|
||||
// size="sm"
|
||||
// block
|
||||
// onClick={this.editDetail.bind(this, context.id)}
|
||||
// >
|
||||
// 配置成员详情
|
||||
// </Button>
|
||||
// )
|
||||
// },
|
||||
// {
|
||||
// type: 'divider'
|
||||
// },
|
||||
{
|
||||
name: 'title',
|
||||
type: i18nEnabled ? 'input-text-i18n' : 'input-text',
|
||||
@ -95,8 +102,8 @@ export class ListPlugin extends BasePlugin {
|
||||
},
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('sourceBindControl', {
|
||||
label: '数据源'
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
{
|
||||
name: 'placeholder',
|
||||
@ -210,7 +217,8 @@ export class ListPlugin extends BasePlugin {
|
||||
const {$schema, ...rest} = props;
|
||||
|
||||
return {
|
||||
...JSONPipeOut(rest),
|
||||
// ...JSONPipeOut(rest),
|
||||
...rest,
|
||||
$schema
|
||||
};
|
||||
}
|
||||
@ -326,6 +334,36 @@ export class ListPlugin extends BasePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
buildDataSchemas(node: EditorNodeType, region?: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'each',
|
||||
type: 'object',
|
||||
title: '当前循环项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 循环添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '序号'
|
||||
});
|
||||
}
|
||||
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
buildEditorContextMenu(
|
||||
{id, schema, region, info, selections}: ContextMenuEventContext,
|
||||
menus: Array<ContextMenuItem>
|
||||
@ -352,7 +390,7 @@ export class ListPlugin extends BasePlugin {
|
||||
const {renderer, schema} = context;
|
||||
if (
|
||||
!schema.$$id &&
|
||||
schema.$$editor?.renderer.name === 'crud' &&
|
||||
['crud', 'crud2'].includes(schema.$$editor?.renderer.name) &&
|
||||
renderer.name === 'list'
|
||||
) {
|
||||
return {
|
||||
|
459
packages/amis-editor/src/plugin/List2.tsx
Normal file
459
packages/amis-editor/src/plugin/List2.tsx
Normal file
@ -0,0 +1,459 @@
|
||||
import {Button, JSONValueMap, isObject} from 'amis';
|
||||
import React from 'react';
|
||||
import {EditorNodeType, registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
BasicRendererInfo,
|
||||
PluginInterface,
|
||||
RendererInfoResolveEventContext
|
||||
} from 'amis-editor-core';
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
import {repeatArray} from 'amis-editor-core';
|
||||
import set from 'lodash/set';
|
||||
import {escapeFormula, resolveArrayDatasource} from '../util';
|
||||
|
||||
export class List2Plugin extends BasePlugin {
|
||||
static id = 'List2Plugin';
|
||||
static scene = ['layout'];
|
||||
// 关联渲染器名字
|
||||
rendererName = 'cards';
|
||||
$schema = '/schemas/CardsSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '列表';
|
||||
isBaseComponent = true;
|
||||
isListComponent = true;
|
||||
description =
|
||||
'功能类似于表格,但是用一个个小卡片来展示数据。当前组件需要配置数据源,不自带数据拉取,请优先使用 「CRUD」 组件。';
|
||||
docLink = '/amis/zh-CN/components/cards';
|
||||
tags = ['展示'];
|
||||
icon = 'fa fa-window-maximize';
|
||||
pluginIcon = 'cards-plugin';
|
||||
scaffold = {
|
||||
type: 'cards',
|
||||
columnsCount: 1,
|
||||
card: {
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'flex',
|
||||
items: [
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '01',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
color: 'var(--colors-neutral-text-2)',
|
||||
fontSize: 'var(--fonts-size-3)',
|
||||
fontWeight: 'var(--fonts-weight-5)',
|
||||
marginRight: '10px'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '/',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
marginRight: '10px',
|
||||
fontSize: 'var(--fonts-size-3)',
|
||||
color: '#cccccc'
|
||||
},
|
||||
id: 'u:95d2a3ac3e70'
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '3月',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '2023',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-6)'
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'flex',
|
||||
'flex': '1 1 auto',
|
||||
'flexGrow': 0,
|
||||
'flexBasis': 'auto',
|
||||
'flexWrap': 'nowrap',
|
||||
'justifyContent': 'flex-start',
|
||||
'alignItems': 'center',
|
||||
'paddingLeft': '20px',
|
||||
'paddingRight': '40px',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'right-border-color': '#ececec',
|
||||
'marginRight': '40px'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '列表标题',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
style: {
|
||||
fontSize: 'var(--fonts-size-5)',
|
||||
color: 'var(--colors-neutral-text-4)',
|
||||
fontWeight: 'var(--fonts-weight-4)',
|
||||
marginBottom: '10px'
|
||||
},
|
||||
maxLine: 1,
|
||||
id: 'u:105ca9cda3ef'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '这是内容简介,可以设置显示行数',
|
||||
inline: true,
|
||||
wrapperComponent: '',
|
||||
maxLine: 1,
|
||||
style: {
|
||||
fontSize: '13px',
|
||||
color: 'var(--colors-neutral-text-5)'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 1,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false
|
||||
},
|
||||
{
|
||||
type: 'container',
|
||||
body: [
|
||||
{
|
||||
type: 'button',
|
||||
label: '查看详情',
|
||||
onEvent: {
|
||||
click: {
|
||||
actions: []
|
||||
}
|
||||
},
|
||||
level: 'default',
|
||||
size: 'default',
|
||||
editorState: 'default',
|
||||
themeCss: {
|
||||
className: {
|
||||
'border:default': {
|
||||
'top-border-width': 'var(--borders-width-2)',
|
||||
'left-border-width': 'var(--borders-width-2)',
|
||||
'right-border-width': 'var(--borders-width-2)',
|
||||
'bottom-border-width': 'var(--borders-width-2)',
|
||||
'top-border-style': 'var(--borders-style-2)',
|
||||
'left-border-style': 'var(--borders-style-2)',
|
||||
'right-border-style': 'var(--borders-style-2)',
|
||||
'bottom-border-style': 'var(--borders-style-2)',
|
||||
'top-border-color': 'var(--colors-brand-6)',
|
||||
'left-border-color': 'var(--colors-brand-6)',
|
||||
'right-border-color': 'var(--colors-brand-6)',
|
||||
'bottom-border-color': 'var(--colors-brand-6)'
|
||||
},
|
||||
'padding-and-margin:default': {
|
||||
paddingLeft: '20px',
|
||||
paddingRight: '20px'
|
||||
},
|
||||
'radius:default': {
|
||||
'top-left-border-radius': '20px',
|
||||
'top-right-border-radius': '20px',
|
||||
'bottom-left-border-radius': '20px',
|
||||
'bottom-right-border-radius': '20px'
|
||||
},
|
||||
'font:default': {
|
||||
color: 'var(--colors-brand-6)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'xs',
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexGrow: 0,
|
||||
flexBasis: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: false,
|
||||
id: 'u:77cb3edb2288'
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
],
|
||||
size: 'none',
|
||||
style: {
|
||||
'position': 'static',
|
||||
'display': 'block',
|
||||
'overflowY': 'auto',
|
||||
'overflowX': 'auto',
|
||||
'paddingTop': '10px',
|
||||
'paddingRight': '10px',
|
||||
'paddingBottom': '10px',
|
||||
'paddingLeft': '10px',
|
||||
'radius': {
|
||||
'top-left-border-radius': '6px',
|
||||
'top-right-border-radius': '6px',
|
||||
'bottom-left-border-radius': '6px',
|
||||
'bottom-right-border-radius': '6px'
|
||||
},
|
||||
'top-border-width': 'var(--borders-width-1)',
|
||||
'left-border-width': 'var(--borders-width-1)',
|
||||
'right-border-width': 'var(--borders-width-1)',
|
||||
'bottom-border-width': 'var(--borders-width-1)',
|
||||
'top-border-style': 'var(--borders-style-1)',
|
||||
'left-border-style': 'var(--borders-style-1)',
|
||||
'right-border-style': 'var(--borders-style-1)',
|
||||
'bottom-border-style': 'var(--borders-style-1)',
|
||||
'top-border-color': '#3be157',
|
||||
'left-border-color': '#3be157',
|
||||
'right-border-color': '#3be157',
|
||||
'bottom-border-color': '#3be157',
|
||||
'flex': '0 0 150px',
|
||||
'marginRight': '15px',
|
||||
'flexBasis': '100%',
|
||||
'boxShadow': ' 0px 0px 10px 0px var(--colors-neutral-line-8)'
|
||||
},
|
||||
wrapperBody: false,
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
onEvent: {
|
||||
click: {
|
||||
weight: 0,
|
||||
actions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
style: {
|
||||
position: 'static',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
overflowX: 'visible',
|
||||
margin: '0',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
isFixedHeight: false,
|
||||
isFixedWidth: true,
|
||||
wrapperBody: false
|
||||
},
|
||||
placeholder: '',
|
||||
name: 'items',
|
||||
style: {
|
||||
gutterY: 10
|
||||
}
|
||||
};
|
||||
|
||||
previewSchema = {
|
||||
...this.scaffold,
|
||||
className: 'text-left '
|
||||
};
|
||||
|
||||
panelTitle = '列表';
|
||||
panelJustify = true;
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const curPosition = context?.schema?.style?.position;
|
||||
const isAbsolute = curPosition === 'fixed' || curPosition === 'absolute';
|
||||
|
||||
return [
|
||||
getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
label: '组件名称',
|
||||
name: 'editorSetting.displayName'
|
||||
},
|
||||
isCRUDBody
|
||||
? null
|
||||
: getSchemaTpl('formItemName', {
|
||||
label: '绑定字段名'
|
||||
}),
|
||||
getSchemaTpl('cardsPlaceholder')
|
||||
]
|
||||
},
|
||||
getSchemaTpl('status')
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
{
|
||||
title: '组件',
|
||||
body: [
|
||||
{
|
||||
name: 'columnsCount',
|
||||
type: 'input-range',
|
||||
visibleOn: '!this.leftFixed',
|
||||
min: 1,
|
||||
max: 12,
|
||||
step: 1,
|
||||
label: '每行个数'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '左右间距',
|
||||
name: 'style.gutterX',
|
||||
visibleOn: 'this.columnsCount > 1'
|
||||
},
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '上下间距',
|
||||
name: 'style.gutterY'
|
||||
},
|
||||
getSchemaTpl('layout:originPosition', {
|
||||
visibleOn: isAbsolute ? isAbsolute : undefined,
|
||||
value: 'left-top'
|
||||
})
|
||||
]
|
||||
},
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
];
|
||||
};
|
||||
|
||||
buildDataSchemas(node: EditorNodeType, region: EditorNodeType) {
|
||||
let dataSchema: any = {
|
||||
$id: 'cards',
|
||||
type: 'object',
|
||||
title: '当前列表项',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
let match =
|
||||
node.schema.source && String(node.schema.source).match(/{([\w-_]+)}/);
|
||||
let field = node.schema.name || match?.[1];
|
||||
const scope = this.manager.dataSchema.getScope(`${node.id}-${node.type}`);
|
||||
const schema = scope?.parent?.getSchemaByPath(field);
|
||||
if (isObject(schema?.items)) {
|
||||
dataSchema = {
|
||||
...dataSchema,
|
||||
...(schema!.items as any)
|
||||
};
|
||||
|
||||
// 列表添加序号方便处理
|
||||
set(dataSchema, 'properties.index', {
|
||||
type: 'number',
|
||||
title: '索引'
|
||||
});
|
||||
}
|
||||
|
||||
return dataSchema;
|
||||
}
|
||||
|
||||
filterProps(props: any) {
|
||||
// 编辑时显示两行假数据
|
||||
const count = (props.columnsCount || 3) * 2;
|
||||
props.value = repeatArray({}, count).map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
id: index + 1
|
||||
};
|
||||
});
|
||||
|
||||
props.className = `${props.className || ''} ae-Editor-list`;
|
||||
props.itemsClassName = `${props.itemsClassName || ''} cards-items`;
|
||||
if (props.card && !props.card.className?.includes('listItem')) {
|
||||
props.card.className = `${props.card.className || ''} ae-Editor-listItem`;
|
||||
}
|
||||
|
||||
// 列表类型内的文本元素显示原始公式
|
||||
props = escapeFormula(props);
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
getRendererInfo(
|
||||
context: RendererInfoResolveEventContext
|
||||
): BasicRendererInfo | void {
|
||||
const plugin: PluginInterface = this;
|
||||
const {renderer, schema} = context;
|
||||
if (
|
||||
!schema.$$id &&
|
||||
schema.$$editor?.renderer.name === 'crud' &&
|
||||
renderer.name === 'cards'
|
||||
) {
|
||||
return {
|
||||
...({id: schema.$$editor.id} as any),
|
||||
name: plugin.name!,
|
||||
regions: plugin.regions,
|
||||
patchContainers: plugin.patchContainers,
|
||||
vRendererConfig: plugin.vRendererConfig,
|
||||
wrapperProps: plugin.wrapperProps,
|
||||
wrapperResolve: plugin.wrapperResolve,
|
||||
filterProps: plugin.filterProps,
|
||||
$schema: plugin.$schema,
|
||||
renderRenderer: plugin.renderRenderer
|
||||
};
|
||||
}
|
||||
|
||||
return super.getRendererInfo(context);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorPlugin(List2Plugin);
|
@ -58,7 +58,6 @@ export class OperationPlugin extends BasePlugin {
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
size="sm"
|
||||
block
|
||||
className="m-b-sm ae-Button--enhance"
|
||||
onClick={() => {
|
||||
|
@ -293,7 +293,40 @@ export class PagePlugin extends BasePlugin {
|
||||
className: 'p-none',
|
||||
body: [
|
||||
getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
...getSchemaTpl('theme:common', {
|
||||
exclude: ['layout'],
|
||||
classname: 'baseControlClassName',
|
||||
baseTitle: '基本样式',
|
||||
extra: [
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'bodyControlClassName',
|
||||
title: '内容区样式',
|
||||
hiddenOn: 'data.regions && !data.regions.includes("body")'
|
||||
}),
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'headerControlClassName',
|
||||
title: '标题栏样式',
|
||||
extra: [
|
||||
getSchemaTpl('theme:font', {
|
||||
label: '文字',
|
||||
name: 'font'
|
||||
})
|
||||
],
|
||||
hiddenOn: 'data.regions && !data.regions.includes("header")'
|
||||
}),
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'toolbarControlClassName',
|
||||
title: '工具栏样式',
|
||||
hiddenOn:
|
||||
'data.regions && !data.regions.includes("toolbar")'
|
||||
}),
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'asideControlClassName',
|
||||
title: '边栏样式',
|
||||
hiddenOn: 'data.regions && !data.regions.includes("aside")'
|
||||
})
|
||||
]
|
||||
})
|
||||
])
|
||||
]
|
||||
},
|
||||
|
@ -1,15 +1,14 @@
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
BasePlugin,
|
||||
RegionConfig,
|
||||
BaseEventContext,
|
||||
tipedLabel
|
||||
tipedLabel,
|
||||
defaultValue,
|
||||
getSchemaTpl,
|
||||
registerEditorPlugin
|
||||
} from 'amis-editor-core';
|
||||
import {ValidatorTag} from '../validator';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
import {RendererPluginEvent} from 'amis-editor-core';
|
||||
|
||||
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
|
||||
|
||||
export class PaginationPlugin extends BasePlugin {
|
||||
static id = 'PaginationPlugin';
|
||||
@ -20,7 +19,6 @@ export class PaginationPlugin extends BasePlugin {
|
||||
// 组件名称
|
||||
name = '分页组件';
|
||||
isBaseComponent = true;
|
||||
disabledRendererPlugin = true;
|
||||
description = '分页组件,可以对列表进行分页展示,提高页面性能';
|
||||
tags = ['容器'];
|
||||
icon = 'fa fa-window-minimize';
|
||||
@ -109,7 +107,7 @@ export class PaginationPlugin extends BasePlugin {
|
||||
'启用功能',
|
||||
'选中表示启用该项,可以拖拽排序调整功能的顺序'
|
||||
),
|
||||
visibleOn: 'data.mode === "normal"',
|
||||
visibleOn: '!data.mode || data.mode === "normal"',
|
||||
mode: 'normal',
|
||||
multiple: true,
|
||||
multiLine: false,
|
||||
@ -124,7 +122,8 @@ export class PaginationPlugin extends BasePlugin {
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'checked',
|
||||
className: 'm-t-n-xxs'
|
||||
className: 'm-t-n-xxs',
|
||||
inputClassName: 'p-t-none'
|
||||
},
|
||||
{
|
||||
type: 'tpl',
|
||||
@ -133,15 +132,31 @@ export class PaginationPlugin extends BasePlugin {
|
||||
}
|
||||
],
|
||||
pipeIn: (value: any) => {
|
||||
if (!value) {
|
||||
value = this.lastLayoutSetting;
|
||||
} else if (typeof value === 'string') {
|
||||
if (typeof value === 'string') {
|
||||
value = (value as string).split(',');
|
||||
} else if (!value || !Array.isArray(value)) {
|
||||
value = this.lastLayoutSetting;
|
||||
}
|
||||
return this.layoutOptions.map(v => ({
|
||||
...v,
|
||||
checked: value.includes(v.value)
|
||||
}));
|
||||
|
||||
return sortBy(
|
||||
this.layoutOptions.map(op => ({
|
||||
...op,
|
||||
checked: value.includes(op.value)
|
||||
})),
|
||||
[
|
||||
item => {
|
||||
const idx = value.findIndex(
|
||||
(v: string) => v === item.value
|
||||
);
|
||||
return ~idx ? idx : Infinity;
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// return this.layoutOptions.map(v => ({
|
||||
// ...v,
|
||||
// checked: value.includes(v.value)
|
||||
// }));
|
||||
},
|
||||
pipeOut: (value: any[]) => {
|
||||
this.lastLayoutSetting = value
|
||||
@ -163,7 +178,7 @@ export class PaginationPlugin extends BasePlugin {
|
||||
type: 'combo',
|
||||
label: '每页条数选项',
|
||||
visibleOn:
|
||||
'data.mode === "normal" && data.layout && data.layout.includes("perPage")',
|
||||
'(!data.mode || data.mode === "normal") && data.layout && data.layout.includes("perPage")',
|
||||
mode: 'normal',
|
||||
multiple: true,
|
||||
multiLine: false,
|
||||
@ -185,15 +200,18 @@ export class PaginationPlugin extends BasePlugin {
|
||||
return value?.map(v => ({value: v})) || [10];
|
||||
},
|
||||
pipeOut: (value: any[]) => {
|
||||
return value.map(v => v.value);
|
||||
const pages = value.map(v => v.value);
|
||||
return pages.map(
|
||||
page => page || Math.max(...pages.filter(Boolean)) + 5
|
||||
);
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'perPage',
|
||||
type: 'input-text',
|
||||
type: 'input-number',
|
||||
label: '默认每页条数',
|
||||
visibleOn:
|
||||
'data.mode === "normal" && data.layout?.includes("perPage")'
|
||||
'(!data.mode || data.mode === "normal") && data.layout?.includes("perPage")'
|
||||
},
|
||||
{
|
||||
name: 'maxButtons',
|
||||
@ -205,13 +223,17 @@ export class PaginationPlugin extends BasePlugin {
|
||||
min: 5,
|
||||
max: 20,
|
||||
pipeOut: (value: any) => value || 5,
|
||||
visibleOn: 'data.mode === "normal"'
|
||||
visibleOn: '!data.mode || data.mode === "normal"'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
body: [getSchemaTpl('disabled')]
|
||||
body: [
|
||||
getSchemaTpl('disabled'),
|
||||
getSchemaTpl('hidden'),
|
||||
getSchemaTpl('visible')
|
||||
]
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import {registerEditorPlugin} from 'amis-editor-core';
|
||||
import {
|
||||
registerEditorPlugin,
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
RendererPluginEvent
|
||||
RendererPluginEvent,
|
||||
RendererPluginAction
|
||||
} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
@ -18,6 +19,7 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
|
||||
// 组件名称
|
||||
name = '搜索框';
|
||||
searchKeywords = '搜索框、searchbox';
|
||||
isBaseComponent = true;
|
||||
description =
|
||||
'用于展示一个简单搜索框,通常需要搭配其他组件使用。比如 page 配置 initApi 后,可以用来实现简单数据过滤查找,name keywords 会作为参数传递给 page 的 initApi。';
|
||||
@ -28,6 +30,7 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
|
||||
scaffold: Schema = {
|
||||
type: 'search-box',
|
||||
name: 'keyword',
|
||||
body: {
|
||||
type: 'tpl',
|
||||
tpl: '搜索框',
|
||||
@ -137,6 +140,19 @@ export class SearchBoxPlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
actions: RendererPluginAction[] = [
|
||||
{
|
||||
actionType: 'clear',
|
||||
actionLabel: '清空',
|
||||
description: '清空输入框'
|
||||
},
|
||||
{
|
||||
actionType: 'setValue',
|
||||
actionLabel: '更新数据',
|
||||
description: '更新数据'
|
||||
}
|
||||
];
|
||||
|
||||
notRenderFormZone = true;
|
||||
panelTitle = '搜索框';
|
||||
panelJustify = true;
|
||||
|
@ -1,53 +1,81 @@
|
||||
import {Button} from 'amis';
|
||||
import React from 'react';
|
||||
import {render as amisRender} from 'amis';
|
||||
import flattenDeep from 'lodash/flattenDeep';
|
||||
import {
|
||||
EditorNodeType,
|
||||
JSONPipeOut,
|
||||
jsonToJsonSchema,
|
||||
registerEditorPlugin
|
||||
registerEditorPlugin,
|
||||
BaseEventContext,
|
||||
BasePlugin,
|
||||
RegionConfig,
|
||||
getSchemaTpl,
|
||||
tipedLabel
|
||||
} from 'amis-editor-core';
|
||||
import {BaseEventContext, BasePlugin, RegionConfig} from 'amis-editor-core';
|
||||
import {getSchemaTpl} from 'amis-editor-core';
|
||||
import {DSBuilderManager} from '../builder/DSBuilderManager';
|
||||
import {DSFeatureEnum, ModelDSBuilderKey} from '../builder';
|
||||
import {getEventControlConfig} from '../renderer/event-control/helper';
|
||||
|
||||
import type {RendererPluginAction, RendererPluginEvent} from 'amis-editor-core';
|
||||
import type {
|
||||
EditorManager,
|
||||
RendererPluginAction,
|
||||
RendererPluginEvent
|
||||
} from 'amis-editor-core';
|
||||
|
||||
export class ServicePlugin extends BasePlugin {
|
||||
static id = 'ServicePlugin';
|
||||
// 关联渲染器名字
|
||||
rendererName = 'service';
|
||||
|
||||
name = '服务Service';
|
||||
|
||||
panelTitle = '服务Service';
|
||||
|
||||
icon = 'fa fa-server';
|
||||
|
||||
pluginIcon = 'service-plugin';
|
||||
|
||||
panelIcon = 'service-plugin';
|
||||
|
||||
$schema = '/schemas/ServiceSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '服务 Service';
|
||||
isBaseComponent = true;
|
||||
|
||||
order = -850;
|
||||
|
||||
description =
|
||||
'功能性容器,可以用来加载数据或者加载渲染器配置。加载到的数据在容器可以使用。';
|
||||
|
||||
docLink = '/amis/zh-CN/components/service';
|
||||
|
||||
tags = ['数据容器'];
|
||||
icon = 'fa fa-server';
|
||||
pluginIcon = 'service-plugin';
|
||||
|
||||
scaffold = {
|
||||
type: 'service',
|
||||
/** region 区域的 placeholder 会撑开内容区 */
|
||||
body: []
|
||||
};
|
||||
previewSchema = {
|
||||
type: 'service',
|
||||
body: [
|
||||
{
|
||||
type: 'tpl',
|
||||
tpl: '内容',
|
||||
wrapperComponent: '',
|
||||
inline: false
|
||||
tpl: '内容区域',
|
||||
inline: false,
|
||||
className: 'bg-light wrapper'
|
||||
}
|
||||
]
|
||||
};
|
||||
previewSchema = {
|
||||
type: 'tpl',
|
||||
wrapperComponent: '',
|
||||
tpl: '功能性组件,用于数据拉取。'
|
||||
};
|
||||
|
||||
regions: Array<RegionConfig> = [
|
||||
{
|
||||
key: 'body',
|
||||
label: '内容区'
|
||||
label: '内容区',
|
||||
placeholder: amisRender({
|
||||
type: 'wrapper',
|
||||
size: 'lg',
|
||||
body: {type: 'tpl', tpl: '内容区域'}
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
@ -149,9 +177,76 @@ export class ServicePlugin extends BasePlugin {
|
||||
}
|
||||
];
|
||||
|
||||
panelTitle = '服务';
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const dsManager = this.dsManager;
|
||||
/** 数据来源选择器 */
|
||||
const dsTypeSelect = () =>
|
||||
dsManager.getDSSelectorSchema({
|
||||
type: 'select',
|
||||
mode: 'horizontal',
|
||||
horizontal: {
|
||||
justify: true,
|
||||
left: 'col-sm-4'
|
||||
},
|
||||
onChange: (value: any, oldValue: any, model: any, form: any) => {
|
||||
if (value !== oldValue) {
|
||||
const data = form.data;
|
||||
Object.keys(data).forEach(key => {
|
||||
if (
|
||||
key?.toLowerCase()?.endsWith('fields') ||
|
||||
key?.toLowerCase().endsWith('api')
|
||||
) {
|
||||
form.deleteValueByName(key);
|
||||
}
|
||||
});
|
||||
form.deleteValueByName('__fields');
|
||||
form.deleteValueByName('__relations');
|
||||
form.setValueByName('api', undefined);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
/** 数据源配置 */
|
||||
const dsSetting = dsManager.buildCollectionFromBuilders(
|
||||
(builder, builderKey) => {
|
||||
return {
|
||||
type: 'container',
|
||||
visibleOn: `this.dsType == null || this.dsType === '${builderKey}'`,
|
||||
body: flattenDeep([
|
||||
builder.makeSourceSettingForm({
|
||||
feat: 'View',
|
||||
renderer: 'service',
|
||||
inScaffold: false,
|
||||
sourceSettings: {
|
||||
name: 'api',
|
||||
label: '接口配置',
|
||||
mode: 'horizontal',
|
||||
...(builderKey === 'api' || builderKey === 'apicenter'
|
||||
? {
|
||||
horizontalConfig: {
|
||||
labelAlign: 'left',
|
||||
horizontal: {
|
||||
justify: true,
|
||||
left: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
useFieldManager: builderKey === ModelDSBuilderKey
|
||||
}
|
||||
})
|
||||
])
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return getSchemaTpl('tabs', [
|
||||
{
|
||||
title: '属性',
|
||||
@ -162,112 +257,56 @@ export class ServicePlugin extends BasePlugin {
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('layout:originPosition', {value: 'left-top'}),
|
||||
getSchemaTpl('name'),
|
||||
{
|
||||
children: (
|
||||
<Button
|
||||
level="info"
|
||||
size="sm"
|
||||
className="m-b-sm"
|
||||
block
|
||||
onClick={() => {
|
||||
// this.manager.showInsertPanel('body', context.id);
|
||||
this.manager.showRendererPanel('');
|
||||
}}
|
||||
>
|
||||
添加内容
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '数据接口',
|
||||
body: [
|
||||
getSchemaTpl('apiControl', {
|
||||
name: 'api',
|
||||
label: '数据接口',
|
||||
messageDesc:
|
||||
'设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主'
|
||||
}),
|
||||
{
|
||||
name: 'ws',
|
||||
type: 'input-text',
|
||||
label: 'WebSocket 实时更新接口'
|
||||
},
|
||||
/** initFetchOn可以通过api的sendOn属性控制 */
|
||||
getSchemaTpl('switch', {
|
||||
name: 'initFetch',
|
||||
label: '数据接口初始加载',
|
||||
visibleOn: 'this.api'
|
||||
}),
|
||||
{
|
||||
name: 'interval',
|
||||
label: '定时刷新间隔',
|
||||
visibleOn: 'this.api',
|
||||
type: 'input-number',
|
||||
step: 500,
|
||||
description: '设置后将自动定时刷新,单位 ms'
|
||||
},
|
||||
getSchemaTpl('switch', {
|
||||
name: 'silentPolling',
|
||||
label: '静默加载',
|
||||
visibleOn: '!!data.interval',
|
||||
description: '设置自动定时刷新是否显示加载动画'
|
||||
}),
|
||||
{
|
||||
name: 'stopAutoRefreshWhen',
|
||||
label: '停止定时刷新检测',
|
||||
type: 'input-text',
|
||||
visibleOn: '!!data.interval',
|
||||
description:
|
||||
'定时刷新一旦设置会一直刷新,除非给出表达式,条件满足后则不刷新了。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Schema接口',
|
||||
body: [
|
||||
getSchemaTpl('apiControl', {
|
||||
name: 'schemaApi',
|
||||
label: '内容 Schema 接口'
|
||||
}),
|
||||
getSchemaTpl('switch', {
|
||||
name: 'initFetchSchema',
|
||||
label: 'Schema接口初始加载',
|
||||
visibleOn: 'this.schemaApi'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '全局配置',
|
||||
body: [
|
||||
getSchemaTpl('loadingConfig', {}, {context}),
|
||||
getSchemaTpl('data'),
|
||||
{
|
||||
type: 'js-editor',
|
||||
allowFullscreen: true,
|
||||
name: 'dataProvider',
|
||||
label: '自定义函数获取数据',
|
||||
description: '将会传递 data 和 setData 两个参数'
|
||||
},
|
||||
{
|
||||
label: '默认消息信息',
|
||||
type: 'combo',
|
||||
name: 'messages',
|
||||
multiLine: true,
|
||||
description:
|
||||
'设置 service 默认提示信息,当 service 没有返回 msg 信息时有用,如果 service 返回携带了 msg 值,则还是以 service 返回为主',
|
||||
items: [
|
||||
getSchemaTpl('fetchSuccess'),
|
||||
getSchemaTpl('fetchFailed')
|
||||
]
|
||||
}
|
||||
dsTypeSelect(),
|
||||
...dsSetting
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
body: [getSchemaTpl('ref'), getSchemaTpl('visible')]
|
||||
body: [getSchemaTpl('hidden')]
|
||||
},
|
||||
{
|
||||
title: '高级',
|
||||
body: [
|
||||
getSchemaTpl('combo-container', {
|
||||
type: 'input-kv',
|
||||
mode: 'normal',
|
||||
name: 'data',
|
||||
label: '初始化静态数据'
|
||||
}),
|
||||
getSchemaTpl('apiControl', {
|
||||
name: 'schemaApi',
|
||||
label: tipedLabel(
|
||||
'Schema数据源',
|
||||
'配置schemaApi后,可以实现动态渲染页面内容'
|
||||
)
|
||||
}),
|
||||
getSchemaTpl('initFetch', {
|
||||
name: 'initFetchSchema',
|
||||
label: '是否Schema初始加载',
|
||||
visibleOn:
|
||||
'typeof this.schemaApi === "string" ? this.schemaApi : this.schemaApi && this.schemaApi.url'
|
||||
}),
|
||||
{
|
||||
name: 'ws',
|
||||
type: 'input-text',
|
||||
label: tipedLabel(
|
||||
'WebSocket接口',
|
||||
'Service 支持通过WebSocket(ws)获取数据,用于获取实时更新的数据。'
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'js-editor',
|
||||
allowFullscreen: true,
|
||||
name: 'dataProvider',
|
||||
label: tipedLabel(
|
||||
'自定义函数获取数据',
|
||||
'对于复杂的数据获取情况,可以使用外部函数获取数据'
|
||||
),
|
||||
placeholder:
|
||||
'/**\n * @param data 上下文数据\n * @param setData 更新数据的函数\n * @param env 环境变量\n */\ninterface DataProvider {\n (data: any, setData: (data: any) => void, env: any): void;\n}\n'
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
]
|
||||
@ -289,6 +328,29 @@ export class ServicePlugin extends BasePlugin {
|
||||
]);
|
||||
};
|
||||
|
||||
panelFormPipeOut = async (schema: any) => {
|
||||
const entity = schema?.api?.entity;
|
||||
|
||||
if (!entity || schema?.dsType !== ModelDSBuilderKey) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const builder = this.dsManager.getBuilderBySchema(schema);
|
||||
|
||||
try {
|
||||
const updatedSchema = await builder.buildApiSchema({
|
||||
schema,
|
||||
renderer: 'service',
|
||||
sourceKey: 'api'
|
||||
});
|
||||
return updatedSchema;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
async buildDataSchemas(
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType,
|
||||
@ -331,6 +393,25 @@ export class ServicePlugin extends BasePlugin {
|
||||
scope?.addSchema(jsonschema);
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailableContextFields(
|
||||
scopeNode: EditorNodeType,
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType
|
||||
) {
|
||||
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
|
||||
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: DSFeatureEnum.List
|
||||
},
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorPlugin(ServicePlugin);
|
||||
|
@ -68,11 +68,7 @@ export class SparklinePlugin extends BasePlugin {
|
||||
{
|
||||
title: '外观',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']}),
|
||||
{
|
||||
title: '自定义 CSS 类名',
|
||||
body: [getSchemaTpl('className')]
|
||||
}
|
||||
...getSchemaTpl('theme:common', {exclude: ['layout']})
|
||||
])
|
||||
}
|
||||
])
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
RendererPluginEvent
|
||||
} from 'amis-editor-core';
|
||||
import {findTree, setVariable, someTree} from 'amis-core';
|
||||
|
||||
import {registerEditorPlugin, repeatArray, diff} from 'amis-editor-core';
|
||||
import {
|
||||
BasePlugin,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
InsertEventContext,
|
||||
ScaffoldForm
|
||||
} from 'amis-editor-core';
|
||||
import {DSBuilderManager} from '../builder/DSBuilderManager';
|
||||
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
|
||||
import {mockValue} from 'amis-editor-core';
|
||||
import {EditorNodeType} from 'amis-editor-core';
|
||||
@ -34,6 +34,8 @@ import {
|
||||
} from '../util';
|
||||
import {reaction} from 'mobx';
|
||||
|
||||
import type {EditorManager} from 'amis-editor-core';
|
||||
|
||||
export class TablePlugin extends BasePlugin {
|
||||
static id = 'TablePlugin';
|
||||
// 关联渲染器名字
|
||||
@ -41,7 +43,7 @@ export class TablePlugin extends BasePlugin {
|
||||
$schema = '/schemas/TableSchema.json';
|
||||
|
||||
// 组件名称
|
||||
name = '表格';
|
||||
name = '原子表格';
|
||||
tags = ['展示'];
|
||||
isBaseComponent = true;
|
||||
description =
|
||||
@ -433,7 +435,16 @@ export class TablePlugin extends BasePlugin {
|
||||
description: '开启表格拖拽排序功能'
|
||||
}
|
||||
];
|
||||
|
||||
panelJustify = true;
|
||||
|
||||
dsManager: DSBuilderManager;
|
||||
|
||||
constructor(manager: EditorManager) {
|
||||
super(manager);
|
||||
this.dsManager = new DSBuilderManager(manager);
|
||||
}
|
||||
|
||||
panelBodyCreator = (context: BaseEventContext) => {
|
||||
const isCRUDBody = context.schema.type === 'crud';
|
||||
const i18nEnabled = getI18nEnabled();
|
||||
@ -888,6 +899,38 @@ export class TablePlugin extends BasePlugin {
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableContextFields(
|
||||
scopeNode: EditorNodeType,
|
||||
node: EditorNodeType,
|
||||
region?: EditorNodeType
|
||||
) {
|
||||
if (node?.info?.renderer?.name === 'table-cell') {
|
||||
if (
|
||||
scopeNode.parent?.type === 'service' &&
|
||||
scopeNode.parent?.parent?.path?.endsWith('service')
|
||||
) {
|
||||
return scopeNode.parent.parent.info.plugin.getAvailableContextFields?.(
|
||||
scopeNode.parent.parent,
|
||||
node,
|
||||
region
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const builder = this.dsManager.getBuilderBySchema(scopeNode.schema);
|
||||
|
||||
if (builder && scopeNode.schema.api) {
|
||||
return builder.getAvailableContextFields(
|
||||
{
|
||||
schema: scopeNode.schema,
|
||||
sourceKey: 'api',
|
||||
feat: 'List'
|
||||
},
|
||||
node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editHeaderDetail(id: string) {
|
||||
const manager = this.manager;
|
||||
const store = manager.store;
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -189,7 +189,13 @@ export class TagPlugin extends BasePlugin {
|
||||
{
|
||||
title: '基本',
|
||||
body: [
|
||||
getSchemaTpl('label'),
|
||||
getSchemaTpl('valueFormula', {
|
||||
name: 'label',
|
||||
label: '标签内容',
|
||||
rendererSchema: {
|
||||
type: 'input-text'
|
||||
}
|
||||
}),
|
||||
{
|
||||
type: 'button-group-select',
|
||||
label: '模式',
|
||||
|
@ -265,19 +265,20 @@ export class TooltipWrapperPlugin extends BasePlugin {
|
||||
title: '外观',
|
||||
className: 'p-none',
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('style:common'),
|
||||
{
|
||||
title: 'CSS 类名',
|
||||
body: [
|
||||
getSchemaTpl('className', {
|
||||
label: '内容区CSS类名'
|
||||
}),
|
||||
getSchemaTpl('className', {
|
||||
label: '浮层CSS类名',
|
||||
name: 'tooltipClassName'
|
||||
...getSchemaTpl('theme:common', {
|
||||
layoutExtra: [
|
||||
getSchemaTpl('theme:size', {
|
||||
label: '尺寸',
|
||||
name: 'themeCss.baseControlClassName.size:default'
|
||||
})
|
||||
],
|
||||
extra: [
|
||||
getSchemaTpl('theme:base', {
|
||||
classname: 'tooltipControlClassName',
|
||||
title: '浮层样式'
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
])
|
||||
|
@ -230,7 +230,23 @@ export class TplPlugin extends BasePlugin {
|
||||
pipeIn: defaultValue(true),
|
||||
hiddenOn: 'data.wrapperComponent !== ""'
|
||||
}),
|
||||
{
|
||||
type: 'input-number',
|
||||
label: '最大显示行数',
|
||||
name: 'maxLine',
|
||||
min: 0
|
||||
},
|
||||
getSchemaTpl('tpl:content'),
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'editorSetting.mock.tpl',
|
||||
mode: 'vertical',
|
||||
label: tipedLabel(
|
||||
'填充假数据',
|
||||
'只在编辑区显示的假数据文本,运行时将显示文本实际内容'
|
||||
),
|
||||
pipeOut: (value: any) => (value === '' ? undefined : value)
|
||||
},
|
||||
getSchemaTpl('tpl:rich-text')
|
||||
]
|
||||
},
|
||||
@ -242,7 +258,12 @@ export class TplPlugin extends BasePlugin {
|
||||
body: getSchemaTpl('collapseGroup', [
|
||||
...getSchemaTpl('theme:common', {
|
||||
exclude: ['layout'],
|
||||
include: ['font']
|
||||
baseExtra: [
|
||||
getSchemaTpl('theme:font', {
|
||||
label: '文字',
|
||||
name: 'font'
|
||||
})
|
||||
]
|
||||
})
|
||||
])
|
||||
},
|
||||
|
@ -12,12 +12,7 @@ export * from './Tabs'; // 选项卡
|
||||
|
||||
// 数据容器
|
||||
export * from './CRUD'; // 增删改查
|
||||
export {
|
||||
TableCRUDPlugin,
|
||||
ListCRUDPlugin,
|
||||
CardsCRUDPlugin,
|
||||
CRUDPlugin as CRUD2Plugin
|
||||
} from './CRUD2';
|
||||
export * from './CRUD2/CRUDTable'; // 增删改查v2.0
|
||||
export * from './Form/Form'; // 表单
|
||||
export * from './Service'; // 服务service
|
||||
|
||||
@ -91,6 +86,7 @@ export * from './Tpl'; // 文字
|
||||
export * from './Icon'; // 图标
|
||||
export * from './Link'; // 链接
|
||||
export * from './List'; // 列表
|
||||
export * from './List2'; // 列表
|
||||
export * from './Mapping'; // 映射
|
||||
export * from './Avatar'; // 头像
|
||||
export * from './Card'; // 卡片
|
||||
|
@ -15,11 +15,12 @@ import debounce from 'lodash/debounce';
|
||||
import remove from 'lodash/remove';
|
||||
import React from 'react';
|
||||
import {EditorManager, EditorNodeType, autobind} from 'amis-editor-core';
|
||||
import type {DSField, DSFieldGroup} from 'amis-editor-core';
|
||||
import {matchSorter} from 'match-sorter';
|
||||
import type {SchemaCollection} from 'amis';
|
||||
import {default as cx} from 'classnames';
|
||||
|
||||
import type {SchemaCollection} from 'amis';
|
||||
import type {DSField, DSFieldGroup} from '../builder';
|
||||
|
||||
export interface DataBindingProps extends FormControlProps {
|
||||
node: EditorNodeType;
|
||||
manager: EditorManager;
|
||||
|
@ -4,15 +4,18 @@
|
||||
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import Sortable from 'sortablejs';
|
||||
import cx from 'classnames';
|
||||
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
|
||||
|
||||
import clone from 'lodash/clone';
|
||||
import remove from 'lodash/remove';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {FormItem, Button, Icon, FormControlProps, autobind} from 'amis';
|
||||
import {Checkbox} from 'amis-ui';
|
||||
import {evalExpression} from 'amis-core';
|
||||
import {GoConfigControl} from './GoConfigControl';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const klass = 'ae-FeatureControl';
|
||||
|
||||
export type FeatureOption = {
|
||||
label: string;
|
||||
value: any;
|
||||
@ -28,9 +31,16 @@ interface FeatureControlProps extends FormControlProps {
|
||||
addable?: boolean;
|
||||
addText?: string;
|
||||
sortable?: boolean;
|
||||
checkable?: boolean;
|
||||
checkableOn?: string;
|
||||
features: Array<FeatureOption> | ((schema: any) => Array<FeatureOption>);
|
||||
goFeatureComp?: (item: FeatureOption) => string; // 去子组件
|
||||
onSort?: (value: FeatureOption[]) => void;
|
||||
goFeatureComp?: (item: FeatureOption, index: number) => string; // 去子组件
|
||||
onSort?: (data: any, value: {oldIndex: number; newIndex: number}) => void;
|
||||
// 自定义添加内容,按钮变成普通按钮
|
||||
customAction?: (props: {schema: any; onBulkChange: any}) => any;
|
||||
onItemCheck?: (checked: boolean, index: number, schema: any) => void;
|
||||
// 所有都添加完成后,隐藏添加按钮
|
||||
hideAddWhenAll?: boolean;
|
||||
}
|
||||
|
||||
interface FeatureControlState {
|
||||
@ -97,7 +107,6 @@ export default class FeatureControl extends React.Component<
|
||||
handleRemove(item: FeatureOption, index: number) {
|
||||
const {removeFeature, data, onBulkChange} = this.props;
|
||||
const {inUseFeat, unUseFeat} = this.state;
|
||||
|
||||
item.remove?.(data);
|
||||
removeFeature?.(item, data);
|
||||
onBulkChange?.(data);
|
||||
@ -108,6 +117,12 @@ export default class FeatureControl extends React.Component<
|
||||
this.setState({inUseFeat, unUseFeat});
|
||||
}
|
||||
|
||||
handleSort(e: any) {
|
||||
const {data, onBulkChange, onSort} = this.props;
|
||||
onSort?.(data, e);
|
||||
onBulkChange?.(data);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAdd(item: any) {
|
||||
const {addFeature, data, onBulkChange} = this.props;
|
||||
@ -173,7 +188,10 @@ export default class FeatureControl extends React.Component<
|
||||
const value = this.state.inUseFeat.concat();
|
||||
value[e.oldIndex] = value.splice(e.newIndex, 1, value[e.oldIndex])[0];
|
||||
this.setState({inUseFeat: value}, () => {
|
||||
this.props.onSort?.(value);
|
||||
this.handleSort({
|
||||
oldIndex: e.oldIndex,
|
||||
newIndex: e.newIndex
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -187,8 +205,24 @@ export default class FeatureControl extends React.Component<
|
||||
this.sortable && this.sortable.destroy();
|
||||
}
|
||||
|
||||
renderItem(item: FeatureOption, index: number) {
|
||||
const {sortable, goFeatureComp, node, manager} = this.props;
|
||||
@autobind
|
||||
handleCheck(res: boolean, index: number) {
|
||||
const {data, onBulkChange, onItemCheck} = this.props;
|
||||
const schema = clone(data);
|
||||
onItemCheck?.(res, index, schema);
|
||||
onBulkChange?.(schema);
|
||||
}
|
||||
|
||||
renderItem(item: FeatureOption, index: number, checkable: boolean) {
|
||||
const {
|
||||
sortable,
|
||||
goFeatureComp,
|
||||
node,
|
||||
manager,
|
||||
onItemCheck,
|
||||
isItemChecked,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
let content = null;
|
||||
|
||||
@ -199,7 +233,7 @@ export default class FeatureControl extends React.Component<
|
||||
className={cx(`${klass}Item-go`)}
|
||||
label={item.label}
|
||||
manager={manager}
|
||||
compId={() => goFeatureComp(item)}
|
||||
compId={() => goFeatureComp(item, index)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@ -208,12 +242,21 @@ export default class FeatureControl extends React.Component<
|
||||
|
||||
return (
|
||||
<li className={klass + 'Item'} key={index}>
|
||||
{sortable && (
|
||||
<a className={klass + 'Item-dragBar'}>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
{checkable && onItemCheck && (
|
||||
<Checkbox
|
||||
checked={isItemChecked(item, index, data)}
|
||||
onChange={(val: any) => this.handleCheck(val, index)}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
|
||||
<div className={klass + 'Item-content'}>
|
||||
{sortable && (
|
||||
<a className={klass + 'Item-dragBar'}>
|
||||
<Icon icon="drag-bar" className="icon" />
|
||||
</a>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
<Button
|
||||
className={klass + 'Item-action'}
|
||||
onClick={() => this.handleRemove(item, index)}
|
||||
@ -225,11 +268,31 @@ export default class FeatureControl extends React.Component<
|
||||
}
|
||||
|
||||
renderAction() {
|
||||
const {addable, addText, render} = this.props;
|
||||
const {
|
||||
addable,
|
||||
addText,
|
||||
render,
|
||||
customAction,
|
||||
data,
|
||||
onBulkChange,
|
||||
hideAddWhenAll
|
||||
} = this.props;
|
||||
if (!addable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (customAction && typeof customAction === 'function') {
|
||||
const schema = customAction({onBulkChange, schema: clone(data)});
|
||||
|
||||
if (isPlainObject(schema) && typeof schema.type === 'string') {
|
||||
return render('custom-action', schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (hideAddWhenAll && !this.state.unUseFeat.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return render('action', {
|
||||
type: 'dropdown-button',
|
||||
closeOnClick: true,
|
||||
@ -252,13 +315,21 @@ export default class FeatureControl extends React.Component<
|
||||
}
|
||||
|
||||
render() {
|
||||
const {className} = this.props;
|
||||
const {className, checkable, checkableOn, data} = this.props;
|
||||
|
||||
let isCheckable = false;
|
||||
|
||||
if (checkable !== undefined) {
|
||||
isCheckable = checkable;
|
||||
} else if (checkableOn) {
|
||||
isCheckable = evalExpression(checkableOn, data) === true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('ae-FeatureControl', className)}>
|
||||
<ul className={cx('ae-FeatureControl-features')} ref={this.dragRef}>
|
||||
{this.state.inUseFeat.map((item, index) =>
|
||||
this.renderItem(item, index)
|
||||
this.renderItem(item, index, isCheckable)
|
||||
)}
|
||||
</ul>
|
||||
|
||||
|
585
packages/amis-editor/src/renderer/FieldSetting.tsx
Normal file
585
packages/amis-editor/src/renderer/FieldSetting.tsx
Normal file
@ -0,0 +1,585 @@
|
||||
/**
|
||||
* @file FieldSetting.tsx
|
||||
* @desc 脚手架中字段管理
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {reaction} from 'mobx';
|
||||
import pick from 'lodash/pick';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {
|
||||
FormItem,
|
||||
FormControlProps,
|
||||
autobind,
|
||||
isValidApi,
|
||||
normalizeApi
|
||||
} from 'amis-core';
|
||||
import {
|
||||
Form,
|
||||
InputTable,
|
||||
Controller,
|
||||
InputBox,
|
||||
Select,
|
||||
Button,
|
||||
toast
|
||||
} from 'amis-ui';
|
||||
|
||||
import type {IReactionDisposer} from 'mobx';
|
||||
import type {InputTableColumnProps} from 'amis-ui';
|
||||
import type {DSFeatureType, ScaffoldField} from '../builder/type';
|
||||
|
||||
interface FieldSettingProps extends FormControlProps {
|
||||
/** 脚手架渲染类型 */
|
||||
renderer?: string;
|
||||
feat: DSFeatureType;
|
||||
config: {
|
||||
showInputType?: boolean;
|
||||
showDisplayType?: boolean;
|
||||
};
|
||||
onAutoGenerateFields: (params: {
|
||||
api: any;
|
||||
props: FieldSettingProps;
|
||||
setState: (state: any) => void;
|
||||
}) => Promise<any[]>;
|
||||
}
|
||||
|
||||
interface RowData extends ScaffoldField {}
|
||||
|
||||
export class FieldSetting extends React.Component<
|
||||
FieldSettingProps,
|
||||
{loading: boolean}
|
||||
> {
|
||||
static defaultProps = {
|
||||
config: {
|
||||
showInputType: true,
|
||||
showDisplayType: true
|
||||
}
|
||||
};
|
||||
|
||||
static validator = (items: RowData[], isInternal?: boolean) => {
|
||||
const cache: Record<string, boolean> = {};
|
||||
const fields = items ?? [];
|
||||
let error: string | boolean = false;
|
||||
|
||||
for (let [index, item] of fields.entries()) {
|
||||
/** 提交时再校验 */
|
||||
if (!item.name && isInternal !== true) {
|
||||
error = `序号「${index + 1}」的字段名称不能为空`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!cache.hasOwnProperty(item.name)) {
|
||||
cache[item.name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
error = `序号「${index + 1}」的字段名称「${item.name}」不唯一`;
|
||||
break;
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
reaction: IReactionDisposer;
|
||||
|
||||
dom: HTMLElement;
|
||||
|
||||
formRef = React.createRef<{submit: () => Promise<Record<string, any>>}>();
|
||||
|
||||
tableRef = React.createRef<any>();
|
||||
|
||||
scaffold: RowData = {
|
||||
label: '',
|
||||
name: '',
|
||||
displayType: 'tpl',
|
||||
inputType: 'input-text'
|
||||
};
|
||||
|
||||
constructor(props: FieldSettingProps) {
|
||||
super(props);
|
||||
this.state = {loading: false};
|
||||
this.reaction = reaction(
|
||||
() => {
|
||||
const ctx = props?.store?.data;
|
||||
const initApi = ctx?.initApi;
|
||||
const listApi = ctx?.listApi;
|
||||
return `${initApi}${listApi}`;
|
||||
},
|
||||
() => this.forceUpdate()
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dom = findDOMNode(this) as HTMLElement;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.reaction?.();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleColumnBlur() {
|
||||
this?.formRef?.current?.submit();
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSubmit(data: {items: RowData[]}) {
|
||||
const {value} = this.props;
|
||||
const items = (data?.items ?? []).map((field: RowData) => {
|
||||
const item = value?.find((f: RowData) => f.name === field.name);
|
||||
return {
|
||||
...pick(
|
||||
{
|
||||
...item,
|
||||
...field
|
||||
},
|
||||
['label', 'name', 'displayType', 'inputType']
|
||||
),
|
||||
checked: true
|
||||
};
|
||||
});
|
||||
|
||||
this.handleFieldsChange(items);
|
||||
}
|
||||
|
||||
@autobind
|
||||
async handleGenerateFields(e: React.MouseEvent<any>) {
|
||||
const {
|
||||
store,
|
||||
renderer,
|
||||
feat,
|
||||
env,
|
||||
manager,
|
||||
data: ctx,
|
||||
onAutoGenerateFields
|
||||
} = this.props;
|
||||
const scaffoldData = store?.data;
|
||||
let api =
|
||||
renderer === 'form'
|
||||
? scaffoldData?.initApi
|
||||
: renderer === 'crud'
|
||||
? scaffoldData?.listApi
|
||||
: '';
|
||||
|
||||
if (!api || (renderer === 'form' && feat !== 'Edit')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
let fields: RowData[] = [];
|
||||
|
||||
if (onAutoGenerateFields && typeof onAutoGenerateFields === 'function') {
|
||||
try {
|
||||
fields = await onAutoGenerateFields({
|
||||
api: api,
|
||||
props: this.props,
|
||||
setState: this.setState
|
||||
});
|
||||
} catch (error) {
|
||||
toast.warning(
|
||||
error.message ?? 'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const schemaFilter = manager?.store?.schemaFilter;
|
||||
|
||||
if (schemaFilter) {
|
||||
api = schemaFilter({
|
||||
api
|
||||
}).api;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await env?.fetcher(api, ctx);
|
||||
|
||||
if (!result.ok) {
|
||||
toast.warning(
|
||||
result.defaultMsg ??
|
||||
result.msg ??
|
||||
'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
this.setState({loading: false});
|
||||
return;
|
||||
}
|
||||
|
||||
let sampleRow: Record<string, any>;
|
||||
if (feat === 'List') {
|
||||
const items = result.data?.rows || result.data?.items || result.data;
|
||||
sampleRow = items?.[0];
|
||||
} else {
|
||||
sampleRow = result.data;
|
||||
}
|
||||
|
||||
if (sampleRow) {
|
||||
Object.entries(sampleRow).forEach(([key, value]) => {
|
||||
fields.push({
|
||||
label: key,
|
||||
name: key,
|
||||
displayType: 'tpl',
|
||||
inputType:
|
||||
typeof value === 'number' ? 'input-number' : 'input-text',
|
||||
checked: true
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.warning(
|
||||
error.message ?? 'API返回格式不正确,请查看接口响应格式要求'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields && fields.length > 0) {
|
||||
this.handleFieldsChange(fields);
|
||||
}
|
||||
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleFieldsChange(fields: RowData[]) {
|
||||
const {
|
||||
onChange,
|
||||
onBulkChange,
|
||||
submitOnChange,
|
||||
renderer,
|
||||
data: ctx
|
||||
} = this.props;
|
||||
const isFirstStep = ctx?.__step === 0;
|
||||
|
||||
if (renderer === 'form') {
|
||||
onChange?.(fields, submitOnChange, true);
|
||||
} else {
|
||||
if (isFirstStep) {
|
||||
onBulkChange?.(
|
||||
{
|
||||
listFields: fields,
|
||||
editFields: fields,
|
||||
bulkEditFields: fields,
|
||||
insertFields: fields,
|
||||
viewFields: fields,
|
||||
simpleQueryFields: fields
|
||||
},
|
||||
submitOnChange
|
||||
);
|
||||
} else {
|
||||
onChange?.(fields, submitOnChange, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFooter() {
|
||||
const {renderer, store, data: ctx, feat} = this.props;
|
||||
const scaffoldData = store?.data;
|
||||
const {initApi, listApi} = scaffoldData || {};
|
||||
const {loading} = this.state;
|
||||
const fieldApi =
|
||||
renderer === 'form' ? initApi : renderer === 'crud' ? listApi : '';
|
||||
const isApiValid = isValidApi(normalizeApi(fieldApi)?.url);
|
||||
const showAutoGenBtn =
|
||||
(renderer === 'form' && feat === 'Edit') ||
|
||||
(renderer === 'crud' && feat === 'List' && ctx?.__step === 0);
|
||||
|
||||
return showAutoGenBtn ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
level="link"
|
||||
loading={loading}
|
||||
disabled={!isApiValid}
|
||||
disabledTip={{
|
||||
content:
|
||||
renderer === 'form' ? '请先填写初始化接口' : '请先填写接口',
|
||||
tooltipTheme: 'dark'
|
||||
}}
|
||||
onClick={e => this.handleGenerateFields(e)}
|
||||
>
|
||||
<span>基于接口自动生成字段</span>
|
||||
</Button>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
classnames: cx,
|
||||
value: formValue,
|
||||
defaultValue: formDefaultValue,
|
||||
env,
|
||||
renderer,
|
||||
config,
|
||||
data: ctx,
|
||||
feat
|
||||
} = this.props;
|
||||
const {showDisplayType, showInputType} = config || {};
|
||||
const isForm = renderer === 'form';
|
||||
const defaultValue = Array.isArray(formDefaultValue)
|
||||
? {items: formDefaultValue}
|
||||
: {items: []};
|
||||
const value = Array.isArray(formValue) ? {items: formValue} : undefined;
|
||||
const popOverContainer = env?.getModalContainer?.() ?? this.dom;
|
||||
const isFirstStep = ctx?.__step === 0;
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={cx('ae-FieldSetting')}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
autoSubmit={false}
|
||||
// onChange={this.handleTableChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
ref={this.formRef}
|
||||
>
|
||||
{({control}: any) => (
|
||||
<>
|
||||
<InputTable
|
||||
ref={this.tableRef}
|
||||
name="items"
|
||||
label={false}
|
||||
labelAlign="left"
|
||||
mode="horizontal"
|
||||
horizontal={{left: 4}}
|
||||
control={control}
|
||||
scaffold={this.scaffold}
|
||||
addable={true}
|
||||
removable={true}
|
||||
isRequired={false}
|
||||
rules={{
|
||||
validate: (values: any[]) =>
|
||||
FieldSetting.validator(values, true)
|
||||
}}
|
||||
addButtonText="添加字段"
|
||||
addButtonProps={{level: 'link'}}
|
||||
scroll={{y: '315.5px'}}
|
||||
footer={this.renderFooter}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
title: '序号',
|
||||
tdRender: (
|
||||
{control}: any,
|
||||
index: number,
|
||||
rowIndex: number
|
||||
) => {
|
||||
return (
|
||||
<Controller
|
||||
name="index"
|
||||
control={control}
|
||||
render={({field, fieldState}) => (
|
||||
<span>{rowIndex + 1}</span>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '字段名称',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={renderProps => {
|
||||
const {field, fieldState} = renderProps;
|
||||
return (
|
||||
<InputBox
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
this.handleColumnBlur();
|
||||
}}
|
||||
hasError={!!fieldState.error}
|
||||
className={cx('ae-FieldSetting-input')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="label"
|
||||
control={control}
|
||||
render={renderProps => {
|
||||
const {field, fieldState} = renderProps;
|
||||
return (
|
||||
<InputBox
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
field.onBlur();
|
||||
this.handleColumnBlur();
|
||||
}}
|
||||
hasError={!!fieldState.error}
|
||||
className={cx('ae-FieldSetting-input')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
showInputType &&
|
||||
!(renderer === 'crud' && feat === 'List' && !isFirstStep)
|
||||
? {
|
||||
title: '输入类型',
|
||||
tdRender: ({control}: any, index: number) => {
|
||||
return (
|
||||
<Controller
|
||||
name="inputType"
|
||||
control={control}
|
||||
isRequired
|
||||
render={({field, fieldState}) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={'w-full'}
|
||||
hasError={!!fieldState.error}
|
||||
searchable
|
||||
disabled={false}
|
||||
clearable={false}
|
||||
popOverContainer={popOverContainer}
|
||||
options={[
|
||||
{
|
||||
label: '单行文本框',
|
||||
value: 'input-text'
|
||||
},
|
||||
{
|
||||
label: '多行文本',
|
||||
value: 'textarea'
|
||||
},
|
||||
{
|
||||
label: '数字输入',
|
||||
value: 'input-number'
|
||||
},
|
||||
{
|
||||
label: '单选框',
|
||||
value: 'radios'
|
||||
},
|
||||
{
|
||||
label: '勾选框',
|
||||
value: 'checkbox'
|
||||
},
|
||||
{
|
||||
label: '复选框',
|
||||
value: 'checkboxes'
|
||||
},
|
||||
{
|
||||
label: '下拉框',
|
||||
value: 'select'
|
||||
},
|
||||
{
|
||||
label: '开关',
|
||||
value: 'switch'
|
||||
},
|
||||
{
|
||||
label: '日期',
|
||||
value: 'input-date'
|
||||
},
|
||||
{
|
||||
label: '表格编辑',
|
||||
value: 'input-table'
|
||||
},
|
||||
{
|
||||
label: '组合输入',
|
||||
value: 'combo'
|
||||
},
|
||||
{
|
||||
label: '文件上传',
|
||||
value: 'input-file'
|
||||
},
|
||||
{
|
||||
label: '图片上传',
|
||||
value: 'input-image'
|
||||
},
|
||||
{
|
||||
label: '富文本编辑器',
|
||||
value: 'input-rich-text'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
showDisplayType
|
||||
? {
|
||||
title: '展示类型',
|
||||
tdRender: ({control}: any) => {
|
||||
return (
|
||||
<Controller
|
||||
name="displayType"
|
||||
control={control}
|
||||
isRequired
|
||||
render={({field, fieldState}) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={'w-full'}
|
||||
hasError={!!fieldState.error}
|
||||
searchable
|
||||
disabled={false}
|
||||
clearable={false}
|
||||
popOverContainer={popOverContainer}
|
||||
options={[
|
||||
{
|
||||
value: 'tpl',
|
||||
label: '文本',
|
||||
typeKey: 'tpl'
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
label: '图片',
|
||||
typeKey: 'src'
|
||||
},
|
||||
{
|
||||
value: 'date',
|
||||
label: '日期',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'progress',
|
||||
label: '进度',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'mapping',
|
||||
label: '映射',
|
||||
typeKey: 'value'
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: '列表',
|
||||
typeKey: 'value'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
].filter(
|
||||
(f): f is Exclude<typeof f, null | undefined> => f != null
|
||||
) as InputTableColumnProps[]
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@FormItem({type: 'ae-field-setting'})
|
||||
export default class FieldSettingRenderer extends FieldSetting {}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user