Merge branch 'master' into chore-table904

This commit is contained in:
liaoxuezhi 2023-09-06 21:37:08 +08:00 committed by 2betop
commit 77a0e57278
178 changed files with 13645 additions and 9299 deletions

View File

@ -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` | | 分割线的旋转角度 |

View File

@ -45,6 +45,7 @@ order: 38
"name": "range",
"label": "range",
"multiple": true,
"joinValues": false,
"value": {
"min": 10,
"max": 50

View File

@ -453,6 +453,7 @@ order: 56
| focus | `[name]: string` 组件的值 | 输入框获取焦点时触发 |
| blur | `[name]: string` 组件的值 | 输入框失去焦点时触发 |
| change | `[name]: string` 组件的值 | 值变化时触发 |
| clear | `[name]: string` 组件的值 | 点击清除按钮时触发 |
## 动作表

View File

@ -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` 禁用插件列表

View File

@ -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',

View File

@ -5,5 +5,5 @@
"packages/amis-ui",
"packages/amis"
],
"version": "3.4.0"
"version": "3.4.1-alpha.0"
}

View File

@ -138,4 +138,4 @@
"printBasicPrototype": false
}
}
}
}

View File

@ -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",

View File

@ -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;
}

View File

@ -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;

View File

@ -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
) {

View File

@ -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}
/>

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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('');

View File

@ -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) => {

View File

@ -3,7 +3,7 @@
*
* @param string
*/
export const keyToPath = (string: string) => {
export const keyToPath = (string: string = '') => {
const result = [];
if (string.charCodeAt(0) === '.'.charCodeAt(0)) {

View File

@ -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();
}
}
}

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -73,6 +73,11 @@
> li {
margin-right: 10px;
cursor: pointer;
&.is-loading {
display: flex;
cursor: unset;
}
}
&--fullscreen {
> a {

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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);
});
}
}

View File

@ -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> {

View File

@ -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,

View File

@ -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;

View File

@ -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>
)}

View File

@ -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)
}
/>
)
}

View File

@ -73,7 +73,7 @@ export function makeWrapper(
// 查找父数据域,将当前组件数据域追加上去,使其形成父子关系
if (
rendererConfig.storeType &&
(rendererConfig.storeType || info.isListComponent) &&
!manager.dataSchema.hasScope(`${info.id}-${info.type}`)
) {
let from = parent;

View File

@ -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';

View File

@ -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

View File

@ -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 '假数据';

View File

@ -36,7 +36,7 @@ export interface RegionConfig {
/**
*
*/
placeholder?: string;
placeholder?: string | JSX.Element;
/**
*
@ -200,6 +200,12 @@ export interface RendererInfo extends RendererScaffoldInfo {
isBaseComponent?: boolean;
/**
*
* listeachcards
*/
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 (

View File

@ -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;
},
// 判断当前元素是否是根节点

View File

@ -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;
}

View File

@ -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 = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

@ -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",

File diff suppressed because it is too large Load Diff

View 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);
};

View 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;
}
}

View 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';

View File

@ -0,0 +1,7 @@
export * from './type';
export * from './constants';
export * from './utils';
export * from './DSBuilder';
export * from './DSBuilderManager';
import './ApiDSBuilder';

View 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>;

View 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;
};

View 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

View 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

View 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

View 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

View File

@ -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};

View File

@ -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';

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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);

View 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);

View 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);

View 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}
]
};

View 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)
])
);
}
};

View File

@ -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(

View File

@ -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')
])
}
])
];

View File

@ -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;
}
}
]
}
])
},

View File

@ -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']})
])
},
{

View File

@ -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: '显隐',

View File

@ -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;
}
}

View File

@ -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

View File

@ -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`

View File

@ -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',

View File

@ -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'
}),

View File

@ -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')
])
}
]);

View File

@ -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')
])
}
]);

View File

@ -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 ? '上方' : '左侧'}插入列级容器`]
) {

View File

@ -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: '在新窗口打开'

View File

@ -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 {

View 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);

View File

@ -58,7 +58,6 @@ export class OperationPlugin extends BasePlugin {
{
children: (
<Button
size="sm"
block
className="m-b-sm ae-Button--enhance"
onClick={() => {

View File

@ -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")'
})
]
})
])
]
},

View File

@ -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')
]
}
])
},

View File

@ -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;

View File

@ -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);

View File

@ -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']})
])
}
])

View File

@ -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

View File

@ -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: '模式',

View File

@ -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: '浮层样式'
})
]
}
})
])
}
])

View File

@ -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'
})
]
})
])
},

View File

@ -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'; // 卡片

View File

@ -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;

View File

@ -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>

View 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