Merge pull request #6567 from baidu/editor-style

editor:合并主题外观修改
This commit is contained in:
qkiroc 2023-04-10 20:12:58 +08:00 committed by GitHub
commit a390db8634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3943 additions and 532 deletions

View File

@ -114,6 +114,7 @@
"amis-core": "*",
"amis-formula": "*",
"amis-ui": "*",
"amis-theme-editor-helper": "*",
"i18n-runtime": "*",
"react": ">=16.8.6",
"react-dom": ">=16.8.6"

View File

@ -48,7 +48,7 @@
@import './style-control/background';
@import './style-control/size';
@import './style-control/style-common';
@import './style-control/theme_classname';
@import './style-control/theme-css-code';
@keyframes growing {
0% {

View File

@ -0,0 +1,85 @@
.ThemeCssCode {
position: relative;
&-button {
width: 100%;
}
.is-group {
overflow: auto;
}
}
.icon-theme-css {
margin-right: 10px;
}
.ThemeCssCode-editor {
height: auto;
width: px2rem(460px);
padding: px2rem(16px);
position: relative;
&-title {
font-size: 14px;
margin-bottom: px2rem(16px);
}
&-close {
position: absolute;
top: px2rem(14px);
right: px2rem(16px);
button {
height: px2rem(16px);
padding: 0;
margin: 0;
}
}
&-content {
height: px2rem(500px);
width: 100%;
&-header {
display: flex;
margin-bottom: px2rem(10px);
border-bottom: 1px solid #e8e9eb;
&-title {
margin-right: px2rem(30px);
cursor: pointer;
padding-bottom: px2rem(10px);
}
&-title--active {
color: #2468f2;
border-bottom: 2px solid #2468f2;
}
}
&-main {
overflow: auto;
height: px2rem(460px);
}
&-body--hidden {
display: none;
}
&-body {
margin-bottom: px2rem(10px);
&-title {
margin-bottom: px2rem(10px);
margin-left: px2rem(16px);
font-size: 12px;
}
&-editor {
height: 200px;
margin-bottom: px2rem(10px);
border-bottom: 1px solid #e8e9eb;
}
}
}
}
.editorPanel-inner {
.Theme-FontEditor {
.Theme-Wrapper-header-left {
font-weight: 400;
color: #5c5f66;
}
}
.Theme-ShadowEditor {
.Theme-Wrapper-header-left {
font-weight: 400;
color: #5c5f66;
}
}
}

View File

@ -1,48 +0,0 @@
.ThemeClassName {
position: relative;
&-button {
position: absolute;
left: px2rem(-38px);
}
.is-group {
overflow: auto;
}
}
.ThemeClassName-editor {
height: auto;
width: px2rem(400px);
padding: px2rem(16px);
position: relative;
&-title {
font-size: 14px;
margin-bottom: px2rem(16px);
}
&-close {
position: absolute;
top: px2rem(14px);
right: px2rem(16px);
button {
height: px2rem(16px);
padding: 0;
margin: 0;
}
}
&-content {
height: px2rem(500px);
width: 100%;
}
}
.editorPanel-inner {
.Theme-FontEditor {
.Theme-Wrapper-header-left {
font-weight: 400;
color: #5c5f66;
}
}
.Theme-ShadowEditor {
.Theme-Wrapper-header-left {
font-weight: 400;
color: #5c5f66;
}
}
}

View File

@ -18,6 +18,7 @@ import {
needDefaultWidth,
guid,
addStyleClassName,
setThemeDefaultData,
appTranslate
} from '../../src/util';
import {
@ -528,7 +529,8 @@ export const MainStore = types
getValueOf(id: string) {
const schema = JSONGetById(self.schema, id);
const res = JSONPipeOut(schema, false);
const data = JSONPipeOut(schema, false);
const res = setThemeDefaultData(data);
return res;
},
@ -546,7 +548,8 @@ export const MainStore = types
key !== '$$commonSchema') ||
typeof props === 'function' || // pipeIn 和 pipeOut
key.substring(0, 2) === '__' ||
key === 'css' ||
key === 'themeCss' ||
key === 'editorPath' ||
key === 'editorState') // 样式不需要出现做json中,
);
},

View File

@ -9,6 +9,8 @@ import isPlainObject from 'lodash/isPlainObject';
import isNumber from 'lodash/isNumber';
import type {Schema} from 'amis';
import {SchemaObject} from 'amis/lib/Schema';
import {assign, cloneDeep} from 'lodash';
import {getGlobalData} from 'amis-theme-editor-helper';
const {
guid,
@ -39,6 +41,8 @@ export {
createObject
};
export let themeConfig: any = {};
export function __uri(id: string) {
return id;
}
@ -198,15 +202,16 @@ export function JSONPipeOut(
}
/**
* css属性className加上name
* themeCss属性className加上name
*/
export function addStyleClassName(obj: Schema) {
const css = obj.css;
if (!obj.css) {
const themeCss = obj.themeCss || obj.css;
// page暂时不做处理
if (!themeCss || obj.type === 'page') {
return obj;
}
let toUpdate: any = {};
Object.keys(css).forEach(key => {
Object.keys(themeCss).forEach(key => {
if (key !== '$$id') {
let classname = `${key}-${obj.id.replace('u:', '')}`;
if (!obj[key]) {
@ -695,18 +700,23 @@ export function filterSchemaForEditor(schema: any): any {
mapped[key] = filtered;
// 组件切换状态修改classname
if (/[C|c]lassName/.test(key) && schema.editorState) {
mapped[key] = mapped[key]
? mapped[key] + ' ' + schema.editorState
: schema.editorState;
modified = true;
}
// TODO:切换状态暂时先不改变组件的样式
// if (/[C|c]lassName/.test(key) && schema.editorState) {
// mapped[key] = mapped[key]
// ? mapped[key] + ' ' + schema.editorState
// : schema.editorState;
// modified = true;
// }
if (filtered !== value) {
modified = true;
}
});
return modified ? mapped : schema;
const finalSchema = modified ? mapped : schema;
if (finalSchema?.type) {
return setThemeDefaultData(finalSchema);
}
return finalSchema;
}
return schema;
@ -1083,3 +1093,15 @@ export function needFillPlaceholder(curProps: any) {
}
return needFillPlaceholder;
}
// 设置主题数据
export function setThemeConfig(config: any) {
themeConfig = config;
}
// 将主题数据传入组件的schema
export function setThemeDefaultData(data: any) {
const schemaData = cloneDeep(data);
schemaData.themeConfig = themeConfig;
assign(schemaData, getGlobalData(themeConfig));
return schemaData;
}

View File

@ -175,6 +175,9 @@ import jFlexEnd from './display/jFlexEnd.svg';
import jSpaceBetween from './display/jSpaceBetween.svg';
import jSpaceAround from './display/jSpaceAround.svg';
// 主题
import themeCss from './theme/css.svg';
// 功能类组件 icon x 11
registerIcon('audio-plugin', audio);
registerIcon('custom-plugin', custom);
@ -336,4 +339,7 @@ registerIcon('jFlexEnd', jFlexEnd);
registerIcon('jSpaceBetween', jSpaceBetween);
registerIcon('jSpaceAround', jSpaceAround);
// 主题
registerIcon('theme-css', themeCss);
export {Icon};

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2778" ><path d="M173.320441 64.959477l0 17.833157 0 855.984379 0 17.833157 17.833157 0 641.98854 0 17.833157 0 0-17.833157L850.975295 332.454788l0-7.245008-5.015224-5.572926L596.296895 69.975724l-5.572926-5.015224-7.245008 0L191.153598 64.960501 173.320441 64.960501zM208.986755 100.625792l356.660072 0L565.646827 332.454788l0 17.833157 17.833157 0 231.828996 0 0 570.655911L208.986755 920.943856 208.986755 100.625792zM601.312118 126.260635l188.360995 188.360995L601.312118 314.621631 601.312118 126.260635zM367.287107 467.88527c23.945365 0 42.979884 11.665691 54.952566 24.866341l-14.429641 16.270569c-10.744715-11.358699-23.638373-18.726503-40.215934-18.726503-38.375006 0-63.85533 31.314193-63.85533 81.353866 0 50.653657 24.252357 82.581834 62.627363 82.581834 19.033495 0 33.155121-7.982812 46.048779-22.410406l14.429641 15.656585c-15.656585 18.419512-35.612079 29.164227-61.092403 29.164227-50.653657 0-88.413656-38.680974-88.413656-104.378256C277.337469 507.48722 315.712475 467.88527 367.287107 467.88527zM456.316793 629.058043c14.429641 15.042601 34.384112 25.174356 55.56655 25.174356 26.402323 0 42.058908-13.20065 42.058908-32.849152 0-20.569478-14.735609-27.015284-33.769105-35.612079l-28.858258-12.586666c-18.726503-7.982812-40.829917-22.410406-40.829917-51.882648 0-30.699186 27.015284-53.416584 63.548338-53.416584 23.945365 0 45.128827 10.130731 59.250452 24.866341l-13.507642 16.57756c-12.279674-11.665691-27.322275-19.033495-45.74281-19.033495-22.717398 0-37.759999 11.358699-37.759999 29.471219 0 19.340487 17.805528 26.708292 33.463136 33.463136l28.857235 12.279674c23.331381 10.129708 41.443901 23.945365 41.443901 53.723575 0 31.928177-26.4013 57.408501-68.766177 57.408501-28.244274 0-52.803623-11.664667-70.302159-29.77821L456.316793 629.058043zM614.416578 629.058043c14.429641 15.042601 34.384112 25.174356 55.56655 25.174356 26.402323 0 42.058908-13.20065 42.058908-32.849152 0-20.569478-14.735609-27.015284-33.769105-35.612079l-28.858258-12.586666c-18.726503-7.982812-40.829917-22.410406-40.829917-51.882648 0-30.699186 27.015284-53.416584 63.548338-53.416584 23.945365 0 45.128827 10.130731 59.250452 24.866341l-13.507642 16.57756c-12.279674-11.665691-27.322275-19.033495-45.74281-19.033495-22.717398 0-37.759999 11.358699-37.759999 29.471219 0 19.340487 17.805528 26.708292 33.463136 33.463136l28.857235 12.279674c23.331381 10.129708 41.443901 23.945365 41.443901 53.723575 0 31.928177-26.4013 57.408501-68.766177 57.408501-28.244274 0-52.803623-11.664667-70.302159-29.77821L614.416578 629.058043z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -178,7 +178,7 @@ import './renderer/FeatureControl';
import './renderer/event-control/index';
import './renderer/TreeOptionControl';
import './renderer/TransferTableControl';
import './renderer/style-control/ThemeClassName';
import './renderer/style-control/ThemeCssCode';
import './renderer/ButtonGroupControl';
import './renderer/FlexSettingControl';
import 'amis-theme-editor/lib/renderers/Border';

View File

@ -116,28 +116,35 @@ export class ButtonPlugin extends BasePlugin {
return [
getSchemaTpl('theme:font', {
label: '文字',
name: `css.className.font:${state}`,
visibleOn: visibleOn
name: `themeCss.className.font:${state}`,
visibleOn: visibleOn,
editorThemePath: [
`button1.type.\${level}.${state}.body.font-color`,
`button1.size.\${size}.body.font`
]
}),
getSchemaTpl('theme:colorPicker', {
label: '背景',
name: `css.className.background:${state}`,
name: `themeCss.className.background:${state}`,
labelMode: 'input',
needGradient: true,
visibleOn: visibleOn
visibleOn: visibleOn,
editorThemePath: `button1.type.\${level}.${state}.body.bg-color`
}),
getSchemaTpl('theme:border', {
name: `css.className.border:${state}`,
visibleOn: visibleOn
name: `themeCss.className.border:${state}`,
visibleOn: visibleOn,
editorThemePath: `button1.type.\${level}.${state}.body.border`
}),
getSchemaTpl('theme:paddingAndMargin', {
name: `css.className.padding-and-margin:${state}`,
visibleOn: visibleOn
name: `themeCss.className.padding-and-margin:${state}`,
visibleOn: visibleOn,
editorThemePath: `button1.size.\${size}.body.padding-and-margin`
}),
getSchemaTpl('theme:radius', {
name: `css.className.radius:${state}`,
visibleOn: visibleOn
name: `themeCss.className.radius:${state}`,
visibleOn: visibleOn,
editorThemePath: `button1.size.\${size}.body.border`
})
];
};
@ -358,7 +365,14 @@ export class ButtonPlugin extends BasePlugin {
...buttonStateFunc("${editorState == 'active'}", 'active')
]
},
getSchemaTpl('theme:classNames', {isFormItem: false})
getSchemaTpl('theme:cssCode', {
themeClass: [
{
value: '',
state: ['default', 'hover', 'active']
}
]
})
])
},
{

View File

@ -235,16 +235,7 @@ export class ContainerPlugin extends LayoutBasePlugin {
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', [
...getSchemaTpl('style:common', ['layout']),
getSchemaTpl('style:classNames', {
isFormItem: false,
schema: [
getSchemaTpl('className', {
name: 'bodyClassName',
label: '内容区'
})
]
})
...getSchemaTpl('theme:common', ['layout'])
])
}
]);

View File

@ -17,6 +17,7 @@ import {
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {ValidatorTag} from '../../validator';
import {getEventControlConfig} from '../../renderer/event-control/helper';
import {inputStateTpl} from '../../renderer/style-control/helper';
export class NumberControlPlugin extends BasePlugin {
// 关联渲染器名字
@ -272,7 +273,28 @@ export class NumberControlPlugin extends BasePlugin {
}
]
}),
getSchemaTpl('style:classNames')
getSchemaTpl('theme:form-label'),
getSchemaTpl('theme:form-description'),
{
title: '数字输入框样式',
body: [
...inputStateTpl(
'themeCss.inputControlClassName',
'inputNumber.base.base'
)
]
},
getSchemaTpl('theme:cssCode', {
themeClass: [
{
name: '数字输入框',
value: '',
className: 'inputControlClassName',
state: ['default', 'hover', 'active']
}
],
isFormItem: true
})
],
{...context?.schema, configTitle: 'style'}
)

View File

@ -9,6 +9,7 @@ import {
import {defaultValue, getSchemaTpl, tipedLabel} from 'amis-editor-core';
import {ValidatorTag} from '../../validator';
import {getEventControlConfig} from '../../renderer/event-control/helper';
import {inputStateTpl} from '../../renderer/style-control/helper';
const isText = 'data.type === "input-text"';
const isPassword = 'data.type === "input-password"';
@ -154,36 +155,6 @@ export class TextControlPlugin extends BasePlugin {
panelBodyCreator = (context: BaseEventContext) => {
const renderer: any = context.info.renderer;
const inputStateFunc = (visibleOn: string, state: string) => {
return [
getSchemaTpl('theme:font', {
label: '文字',
name: `css.inputControlClassName.font:${state}`,
visibleOn: visibleOn
}),
getSchemaTpl('theme:colorPicker', {
label: '背景',
name: `css.inputControlClassName.background:${state}`,
labelMode: 'input',
needGradient: true,
visibleOn: visibleOn
}),
getSchemaTpl('theme:border', {
name: `css.inputControlClassName.border:${state}`,
visibleOn: visibleOn
}),
getSchemaTpl('theme:paddingAndMargin', {
name: `css.inputControlClassName.padding-and-margin:${state}`,
visibleOn: visibleOn
}),
getSchemaTpl('theme:radius', {
name: `css.inputControlClassName.radius:${state}`,
visibleOn: visibleOn
})
];
};
return getSchemaTpl('tabs', [
{
title: '属性',
@ -389,32 +360,10 @@ export class TextControlPlugin extends BasePlugin {
{
title: '输入框样式',
body: [
{
type: 'select',
name: 'editorState',
label: '状态',
selectFirst: true,
options: [
{
label: '常规',
value: 'default'
},
{
label: '悬浮',
value: 'hover'
},
{
label: '点击',
value: 'active'
}
]
},
...inputStateFunc(
"${editorState == 'default' || !editorState}",
'default'
),
...inputStateFunc("${editorState == 'hover'}", 'hover'),
...inputStateFunc("${editorState == 'active'}", 'active')
...inputStateTpl(
'themeCss.inputControlClassName',
'input.base.default'
)
]
},
{
@ -423,28 +372,28 @@ export class TextControlPlugin extends BasePlugin {
body: [
getSchemaTpl('theme:font', {
label: '文字',
name: 'css.addOnClassName.font'
name: 'themeCss.addOnClassName.font:default'
}),
getSchemaTpl('theme:paddingAndMargin', {
name: 'css.addOnClassName.padding-and-margin'
name: 'themeCss.addOnClassName.padding-and-margin:default'
})
]
},
getSchemaTpl('theme:classNames', {
schema: [
getSchemaTpl('theme:cssCode', {
themeClass: [
{
type: 'theme-classname',
label: '输入框',
name: 'inputControlClassName'
name: '输入框',
value: '',
className: 'inputControlClassName',
state: ['default', 'hover', 'active']
},
{
type: 'theme-classname',
name: 'addOnClassName',
suffix: 'addOn',
label: 'AddOn',
visibleOn: 'this.addOn && this.addOn.type === "text"'
name: 'addOn',
value: 'addOn',
className: 'addOnClassName'
}
]
],
isFormItem: true
})
],
{...context?.schema, configTitle: 'style'}

View File

@ -76,10 +76,7 @@ export class IFramePlugin extends BasePlugin {
})
]
},
getSchemaTpl('style:classNames', {
isFormItem: false
}),
...getSchemaTpl('style:common', [], 'border')
...getSchemaTpl('theme:common', ['layout'])
])
]
}

View File

@ -210,13 +210,7 @@ export class FlexPluginBase extends LayoutBasePlugin {
{
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', [
...getSchemaTpl('style:common', []),
{
title: 'CSS 类名',
body: [getSchemaTpl('className', {label: '外层CSS类名'})]
}
])
body: getSchemaTpl('collapseGroup', [...getSchemaTpl('theme:common')])
}
])
];

View File

@ -275,30 +275,7 @@ export class PagePlugin extends BasePlugin {
className: 'p-none',
body: [
getSchemaTpl('collapseGroup', [
...getSchemaTpl('theme:common', ['layout']),
getSchemaTpl('style:classNames', {
isFormItem: false,
schema: [
getSchemaTpl('className', {
name: 'headerClassName',
label: '顶部'
}),
getSchemaTpl('className', {
name: 'bodyClassName',
label: '内容区'
}),
getSchemaTpl('className', {
name: 'asideClassName',
label: '边栏'
}),
getSchemaTpl('className', {
name: 'toolbarClassName',
label: '工具栏'
})
]
})
...getSchemaTpl('theme:common', ['layout'])
])
]
},

View File

@ -105,7 +105,29 @@ export class StepsPlugin extends BasePlugin {
},
{
title: '外观',
body: [getSchemaTpl('className')]
body: [
{
name: 'mode',
type: 'select',
label: '模式',
value: 'horizontal',
options: [
{
label: '水平',
value: 'horizontal'
},
{
label: '竖直',
value: 'vertical'
},
{
label: '简单',
value: 'simple'
}
]
},
getSchemaTpl('className')
]
},
{
title: '显隐',

View File

@ -245,13 +245,7 @@ export class TableViewPlugin extends BasePlugin {
{
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', [
...getSchemaTpl('style:common'),
{
title: 'CSS 类名',
body: [getSchemaTpl('className')]
}
])
body: getSchemaTpl('collapseGroup', [...getSchemaTpl('theme:common')])
},
{
title: '状态',
@ -446,7 +440,7 @@ export class TableViewPlugin extends BasePlugin {
{
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', getSchemaTpl('style:common'))
body: getSchemaTpl('collapseGroup', getSchemaTpl('theme:common'))
}
])
];
@ -476,7 +470,7 @@ export class TableViewPlugin extends BasePlugin {
{
title: '外观',
className: 'p-none',
body: getSchemaTpl('collapseGroup', getSchemaTpl('style:common'))
body: getSchemaTpl('collapseGroup', getSchemaTpl('theme:common'))
}
])
];

View File

@ -219,10 +219,7 @@ export class TplPlugin extends BasePlugin {
{
title: '外观',
body: getSchemaTpl('collapseGroup', [
...getSchemaTpl('style:common', ['layout']),
getSchemaTpl('style:classNames', {
isFormItem: false
})
...getSchemaTpl('theme:common', ['layout'], ['font'])
])
},
{

View File

@ -409,7 +409,7 @@ export default class FormulaControl extends React.Component<
'kilobitSeparator',
'value',
'inputControlClassName',
'css'
'themeCss'
];
// 当前组件要剔除的字段

View File

@ -1,237 +0,0 @@
/**
* +
*/
import React, {useEffect, useRef, useState} from 'react';
import {Button, Editor, Icon, Overlay, PopOver} from 'amis-ui';
import {FormControlProps, FormItem, render} from 'amis-core';
import {parse as cssParse} from 'amis-postcss';
import {PlainObject} from './types';
import {debounce} from 'lodash';
const valueMap: PlainObject = {
'margin-top': 'marginTop',
'margin-right': 'marginRight',
'margin-bottom': 'marginBottom',
'margin-left': 'marginLeft',
'padding-top': 'paddingTop',
'padding-right': 'paddingRight',
'padding-bottom': 'paddingBottom',
'padding-left': 'paddingLeft',
'border-top-width': 'top-border-width',
'border-right-width': 'right-border-width',
'border-bottom-width': 'bottom-border-width',
'border-left-width': 'left-border-width',
'border-top-style': 'top-border-style',
'border-right-style': 'right-border-style',
'border-bottom-style': 'bottom-border-style',
'border-left-style': 'left-border-style',
'border-top-color': 'top-border-color',
'border-right-color': 'right-border-color',
'border-bottom-color': 'bottom-border-color',
'border-left-color': 'left-border-color',
'font-size': 'fontSize',
'font-weight': 'fontWeight',
'line-height': 'lineHeight'
};
const fontStyle = [
'color',
'font-weight',
'font-size',
'font-style',
'text-decoration',
'text-align',
'vertical-align',
'font-family',
'line-height'
];
function AmisStyleCodeEditor(props: FormControlProps) {
const {name, value: classname, suffix} = props;
const [value, setValue] = useState('');
function getCssAndSetValue(
classname?: string,
name?: string,
suffix?: string
) {
try {
const id =
classname?.replace(name + '-', '') + (suffix ? '-' + suffix : '');
const dom = document.getElementById(id || '') || null;
const content = dom?.innerHTML || '';
const ast = cssParse(content);
const nodes: any[] = [];
ast.nodes.forEach((node: any) => {
const selector = node.selector;
if (!selector.endsWith('.hover') && !selector.endsWith('.active')) {
nodes.push(node);
}
});
ast.nodes = nodes;
const css = nodes
.map(node => {
const style = node.nodes.map((n: any) => `${n.prop}: ${n.value};`);
return `${node.selector} {\n ${style.join('\n ')}\n}`;
})
.join('\n\n');
setValue(css);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
getCssAndSetValue(classname, name, suffix);
}, []);
function handleChange(value: string) {
setValue(value);
editorChange(value);
}
const editorChange = debounce((value: string) => {
try {
const ast = cssParse(value);
const {data, onBulkChange, name} = props;
const sourceCss = data.css || {};
const className: PlainObject = {};
ast.nodes.forEach((node: any) => {
const nodes = node.nodes;
const selector = node.selector;
let state = 'default';
if (!!~selector.indexOf(':hover:active')) {
state = 'active';
} else if (!!~selector.indexOf(':hover')) {
state = 'hover';
}
nodes.forEach((item: any) => {
const prop = item.prop;
const cssValue = item.value;
if (!!~prop.indexOf('radius')) {
const type = 'radius:' + state;
!className[type] && (className[type] = {});
const radius = cssValue.split(' ');
className[type]['top-left-border-radius'] = radius[0];
className[type]['top-right-border-radius'] = radius[1];
className[type]['bottom-right-border-radius'] = radius[2];
className[type]['bottom-left-border-radius'] = radius[3];
} else if (!!~prop.indexOf('border')) {
!className['border:' + state] &&
(className['border:' + state] = {});
className['border:' + state][valueMap[prop] || prop] = cssValue;
} else if (!!~prop.indexOf('padding') || !!~prop.indexOf('margin')) {
!className['padding-and-margin:' + state] &&
(className['padding-and-margin:' + state] = {});
className['padding-and-margin:' + state][valueMap[prop] || prop] =
cssValue;
} else if (fontStyle.includes(prop)) {
!className['font:' + state] && (className['font:' + state] = {});
className['font:' + state][valueMap[prop] || prop] = cssValue;
} else {
className[(valueMap[prop] || prop) + ':' + state] = cssValue;
}
});
});
const newCss = {
...sourceCss,
[name!]: className
};
onBulkChange &&
onBulkChange({
css: newCss
});
} catch (error) {
console.error(error);
}
});
return (
<div className="ThemeClassName-editor">
<div className="ThemeClassName-editor-title"></div>
<div className="ThemeClassName-editor-close">
<Button onClick={props.onHide} level="link">
<Icon icon="close" className="icon" />
</Button>
</div>
<div className="ThemeClassName-editor-content">
<Editor
value={value}
language="css"
onChange={handleChange}
options={{
automaticLayout: true,
lineNumbers: 'off',
glyphMargin: false,
tabSize: 2,
wordWrap: 'on',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
selectOnLineNumbers: true,
scrollBeyondLastLine: false,
folding: true,
minimap: {
enabled: false
}
}}
/>
</div>
</div>
);
}
function ThemeClassName(props: FormControlProps) {
const ref = useRef<HTMLDivElement>(null);
const {value} = props;
const [showEditor, setShowEditor] = useState(false);
function handleShowEditor() {
setShowEditor(true);
}
return (
<>
<div ref={ref} className="ThemeClassName">
<Button
onClick={handleShowEditor}
level="link"
className=":ThemeClassName-button"
>
<Icon icon="file" className="icon" />
</Button>
{render({
type: 'input-tag',
name: 'class',
placeholder: '请输入类名',
delimiter: ' ',
value: value,
onChange: (value: string) => {
props.onChange && props.onChange(value);
}
})}
</div>
<Overlay
container={document.body}
placement="left"
target={ref.current}
show={showEditor}
rootClose={false}
>
<PopOver overlay onHide={() => setShowEditor(false)}>
<AmisStyleCodeEditor {...props} onHide={() => setShowEditor(false)} />
</PopOver>
</Overlay>
</>
);
}
@FormItem({
type: 'theme-classname',
strictMode: false
})
export class BorderRenderer extends React.Component<FormControlProps> {
render() {
return <ThemeClassName {...this.props} />;
}
}

View File

@ -0,0 +1,396 @@
/**
* +
*/
import React, {useEffect, useRef, useState} from 'react';
import {Button, Editor, Overlay, PopOver} from 'amis-ui';
import {FormControlProps, FormItem, styleMap} from 'amis-core';
import {parse as cssParse} from 'amis-postcss';
import {PlainObject} from './types';
import {debounce, isEmpty} from 'lodash';
import {Icon} from '../../icons/index';
import editorFactory from './themeLanguage';
import cx from 'classnames';
const valueMap: PlainObject = {};
for (let key in styleMap) {
valueMap[styleMap[key]] = key;
}
const fontStyle = [
'color',
'font-weight',
'font-size',
'font-style',
'text-decoration',
'text-align',
'vertical-align',
'font-family',
'line-height'
];
interface CssNode {
value: string;
selector: string;
}
interface CssNodeTab {
name: string;
children: CssNode[];
}
function AmisThemeCssCodeEditor(props: FormControlProps) {
const {themeClass, data} = props;
const id = data.id.replace('u:', '');
const [cssNodes, setCssNodes] = useState<CssNodeTab[]>([]);
const [tabId, setTabId] = useState(0);
function getCssAndSetValue(themeClass: any[]) {
try {
const newCssNodes: CssNodeTab[] = [];
themeClass?.forEach(n => {
const classId = n.value ? id + '-' + n.value : id;
const state = n.state || ['default'];
const className = n.className || 'className';
const dom = document.getElementById(classId || '') || null;
const content = dom?.innerHTML || '';
const ast = cssParse(content);
const nodes: any[] = [];
ast.nodes.forEach((node: any) => {
const selector = node.selector;
if (!selector.endsWith('.hover') && !selector.endsWith('.active')) {
nodes.push(node);
}
});
const css: {selector: string; value: string; state: string}[] = [];
state.forEach((s: string) => {
css.push({
selector: `.${className}-${id}${s === 'default' ? '' : ':' + s}`,
state: s,
value: ''
});
});
nodes.forEach(node => {
const style = node.nodes.map((n: any) => `${n.prop}: ${n.value};`);
const item = css.find(c => {
if (
c.selector === node.selector ||
node.selector.endsWith(`:${c.state}`)
) {
return c;
}
return false;
})!;
item.value = style.join('\n');
});
newCssNodes.push({
name: n.name || '自定义样式',
children: css
});
});
setCssNodes(newCssNodes);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
getCssAndSetValue(themeClass);
}, []);
const editorChange = debounce((nodeTabs: CssNodeTab[]) => {
try {
const {data, onBulkChange} = props;
const sourceCss = data.themeCss || data.css || {};
const newCss: any = {};
nodeTabs.forEach(tab => {
tab.children.forEach(node => {
const nodes = cssParse(node.value)
.nodes.map(node => {
const {prop, value} = node;
return {
prop,
value
};
})
.filter(n => n.value);
const selector = node.selector;
const nameEtr = /\.(.*)\-/.exec(selector);
const cssCode: PlainObject = {};
let name = nameEtr ? nameEtr[1] : '';
let state = 'default';
if (!!~selector.indexOf(':active')) {
state = 'active';
} else if (!!~selector.indexOf(':hover')) {
state = 'hover';
}
nodes.forEach(item => {
const prop = item.prop;
const cssValue = item.value;
if (!!~prop.indexOf('radius')) {
const type = 'radius:' + state;
!cssCode[type] && (cssCode[type] = {});
const radius = cssValue.split(' ');
cssCode[type]['top-left-border-radius'] = radius[0];
cssCode[type]['top-right-border-radius'] = radius[1];
cssCode[type]['bottom-right-border-radius'] = radius[2];
cssCode[type]['bottom-left-border-radius'] = radius[3];
} else if (!!~prop.indexOf('border')) {
!cssCode['border:' + state] && (cssCode['border:' + state] = {});
cssCode['border:' + state][valueMap[prop] || prop] = cssValue;
} else if (
!!~prop.indexOf('padding') ||
!!~prop.indexOf('margin')
) {
!cssCode['padding-and-margin:' + state] &&
(cssCode['padding-and-margin:' + state] = {});
cssCode['padding-and-margin:' + state][valueMap[prop] || prop] =
cssValue;
} else if (fontStyle.includes(prop)) {
!cssCode['font:' + state] && (cssCode['font:' + state] = {});
cssCode['font:' + state][valueMap[prop] || prop] = cssValue;
} else {
cssCode[(valueMap[prop] || prop) + ':' + state] = cssValue;
}
});
if (newCss[name]) {
newCss[name] = Object.assign(newCss[name], cssCode);
} else {
newCss[name] = cssCode;
}
});
});
onBulkChange &&
onBulkChange({
themeCss: {
...sourceCss,
...newCss
}
});
} catch (error) {
console.error(error);
}
});
function handleChange(value: string, i: number, j: number) {
const newCssNodes = cssNodes;
newCssNodes[i].children[j].value = value;
setCssNodes(newCssNodes); // 好像不需要这个?
editorChange(newCssNodes);
}
function formateTitle(title: string) {
if (title.endsWith('hover')) {
return '悬浮态样式';
} else if (title.endsWith('active')) {
return '点击态样式';
} else if (title.endsWith('disabled')) {
return '禁用态样式';
}
return '常规态样式';
}
return (
<div className="ThemeCssCode-editor">
<div className="ThemeCssCode-editor-title"></div>
<div className="ThemeCssCode-editor-close">
<Button onClick={props.onHide} level="link">
<Icon icon="close" className="icon" />
</Button>
</div>
<div className="ThemeCssCode-editor-content">
<div className="ThemeCssCode-editor-content-header">
{cssNodes.map((node, index) => {
return (
<div
key={index}
onClick={() => setTabId(index)}
className={cx(
'ThemeCssCode-editor-content-header-title',
index === tabId &&
'ThemeCssCode-editor-content-header-title--active'
)}
>
{node.name}
</div>
);
})}
</div>
<div className="ThemeCssCode-editor-content-main">
{cssNodes.map((node, i) => {
const children = node.children;
return (
<div
key={i}
className={cx(
i !== tabId && 'ThemeCssCode-editor-content-body--hidden'
)}
>
{children.map((css, j) => {
return (
<div
className="ThemeCssCode-editor-content-body"
key={`${i}-${j}-${css.selector}`}
id={`${i}-${j}-${css.selector}`}
>
{children.length > 1 ? (
<div className="ThemeCssCode-editor-content-body-title">
{formateTitle(css.selector)}
</div>
) : null}
<div className="ThemeCssCode-editor-content-body-editor">
<Editor
value={css.value}
editorFactory={editorFactory}
options={{
onChange: (value: string) =>
handleChange(value, i, j)
}}
/>
</div>
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
);
}
function AmisStyleCodeEditor(props: FormControlProps) {
const {data, onBulkChange} = props;
const {style} = data;
const [value, setValue] = useState('');
function getCssAndSetValue(data: any) {
if (isEmpty(data)) {
return '';
}
let str = '';
for (let key in data) {
if (key === 'radius') {
str += `border-radius: ${
data.radius['top-left-border-radius'] +
' ' +
data.radius['top-right-border-radius'] +
' ' +
data.radius['bottom-right-border-radius'] +
' ' +
data.radius['bottom-left-border-radius']
};\n`;
} else {
str += `${styleMap[key] || key}: ${data[key]};\n`;
}
}
return str;
}
useEffect(() => {
const res = getCssAndSetValue(style);
setValue(res);
}, []);
const editorChange = debounce((value: string) => {
const newStyle: PlainObject = {};
try {
const style = cssParse(value);
style.nodes.forEach(node => {
const {prop, value} = node;
if (value) {
if (prop === 'border-radius') {
const radius = value.split(' ');
newStyle['radius'] = {
'top-left-border-radius': radius[0] || '',
'top-right-border-radius': radius[1] || '',
'bottom-right-border-radius': radius[2] || '',
'bottom-left-border-radius': radius[3] || ''
};
} else {
newStyle[valueMap[prop] || prop] = value;
}
}
});
onBulkChange &&
onBulkChange({
style: newStyle
});
} catch (error) {
console.error(error);
}
});
function handleChange(value: string) {
editorChange(value);
setValue(value);
}
return (
<div className="ThemeCssCode-editor">
<div className="ThemeCssCode-editor-title"></div>
<div className="ThemeCssCode-editor-close">
<Button onClick={props.onHide} level="link">
<Icon icon="close" className="icon" />
</Button>
</div>
<div className="ThemeCssCode-editor-content">
<Editor
value={value}
editorFactory={editorFactory}
options={{
onChange: handleChange
}}
/>
</div>
</div>
);
}
function ThemeCssCode(props: FormControlProps) {
const ref = useRef<HTMLDivElement>(null);
const [showEditor, setShowEditor] = useState(false);
function handleShowEditor() {
setShowEditor(true);
}
return (
<>
<div ref={ref} className="ThemeCssCode">
<Button onClick={handleShowEditor} className=":ThemeCssCode-button">
<Icon icon="theme-css" className="icon" />
</Button>
</div>
<Overlay
container={document.body}
placement="left"
target={ref.current}
show={showEditor}
rootClose={false}
>
<PopOver overlay onHide={() => setShowEditor(false)}>
{props.isLayout ? (
<AmisStyleCodeEditor
{...props}
onHide={() => setShowEditor(false)}
/>
) : (
<AmisThemeCssCodeEditor
{...props}
onHide={() => setShowEditor(false)}
/>
)}
</PopOver>
</Overlay>
</>
);
}
@FormItem({
type: 'theme-cssCode',
strictMode: false
})
export class ThemeCssCodeRenderer extends React.Component<FormControlProps> {
render() {
return <ThemeCssCode {...this.props} />;
}
}

View File

@ -0,0 +1,85 @@
import {getSchemaTpl} from 'amis-editor-core';
export const inputStateTpl = (className: string, path: string = '') => {
return [
{
type: 'select',
name: 'editorState',
label: '状态',
selectFirst: true,
options: [
{
label: '常规',
value: 'default'
},
{
label: '悬浮',
value: 'hover'
},
{
label: '点击',
value: 'active'
}
]
},
{
type: 'hidden',
name: 'editorPath',
value: path
},
...inputStateFunc(
"${editorState == 'default' || !editorState}",
'default',
className,
path
),
...inputStateFunc("${editorState == 'hover'}", 'hover', className, path),
...inputStateFunc("${editorState == 'active'}", 'active', className, path)
];
};
export const inputStateFunc = (
visibleOn: string,
state: string,
className: string,
path: string,
options: any = []
) => {
return [
getSchemaTpl('theme:font', {
label: '文字',
name: `${className}.font:${state}`,
visibleOn: visibleOn,
editorThemePath: `${path}.${state}.body.font`,
state
}),
getSchemaTpl('theme:colorPicker', {
label: '背景',
name: `${className}.background:${state}`,
labelMode: 'input',
needGradient: true,
visibleOn: visibleOn,
editorThemePath: `${path}.${state}.body.bg-color`,
state
}),
getSchemaTpl('theme:border', {
name: `${className}.border:${state}`,
visibleOn: visibleOn,
editorThemePath: `${path}.${state}.body.border`,
state
}),
getSchemaTpl('theme:paddingAndMargin', {
name: `${className}.padding-and-margin:${state}`,
visibleOn: visibleOn,
editorThemePath: `${path}.${state}.body.padding-and-margin`,
state
}),
getSchemaTpl('theme:radius', {
name: `${className}.radius:${state}`,
visibleOn: visibleOn,
editorThemePath: `${path}.${state}.body.border`,
state
}),
...options
];
};

File diff suppressed because it is too large Load Diff

View File

@ -427,39 +427,36 @@ export const styleTpl = {
// css类名
setSchemaTpl(
'theme:classNames',
(config: {schema: any; isFormItem: boolean; unsupportStatic?: boolean}) => {
const {
isFormItem = true,
unsupportStatic = false,
schema = []
} = config || {};
'theme:cssCode',
({
themeClass = [],
isFormItem,
isLayout
}: {
themeClass?: any[];
isFormItem?: boolean;
} = {}) => {
if (isFormItem) {
themeClass.push(
...[
{
name: 'description',
value: 'description',
className: 'descriptionClassName'
},
{name: 'label', value: 'label', className: 'labelClassName'}
]
);
}
return {
title: 'CSS 类名',
body: (isFormItem
? [
{
type: 'theme-classname',
label: 'Label',
suffix: 'label',
name: 'labelClassName'
},
{
type: 'theme-classname',
label: '描述',
suffix: 'description',
name: 'descriptionClassName',
visibleOn: 'this.description'
}
]
: [
{
type: 'theme-classname',
label: '外层',
name: 'className'
}
]
).concat(schema)
title: '样式源码',
body: [
{
type: 'theme-cssCode',
label: false,
themeClass
}
]
};
}
);
@ -471,10 +468,11 @@ setSchemaTpl('theme:form-label', () => {
body: [
getSchemaTpl('theme:font', {
label: '文字',
name: 'css.labelClassName.font'
name: 'themeCss.labelClassName.font:default',
editorThemePath: 'form.item.default.label.body.font'
}),
getSchemaTpl('theme:paddingAndMargin', {
name: 'css.labelClassName.padding-and-margin'
name: 'themeCss.labelClassName.padding-and-margin:default'
})
]
};
@ -488,10 +486,11 @@ setSchemaTpl('theme:form-description', () => {
body: [
getSchemaTpl('theme:font', {
label: '文字',
name: 'css.descriptionClassName.font'
name: 'themeCss.descriptionClassName.font:default',
editorThemePath: 'form.item.default.description.body.font'
}),
getSchemaTpl('theme:paddingAndMargin', {
name: 'css.descriptionClassName.padding-and-margin'
name: 'themeCss.descriptionClassName.padding-and-margin:default'
})
]
};
@ -503,7 +502,7 @@ setSchemaTpl('theme:font', (option: any = {}) => {
mode: 'default',
type: 'amis-theme-font-editor',
label: '文字',
name: `css.className.font`,
name: `themeCss.className.font:default`,
needColorCustom: true,
...option
};
@ -515,7 +514,7 @@ setSchemaTpl('theme:colorPicker', (option: any = {}) => {
mode: 'default',
type: 'amis-theme-color-picker',
label: '颜色',
name: `css.className.color`,
name: `themeCss.className.color:default`,
needCustom: true,
...option
};
@ -527,7 +526,7 @@ setSchemaTpl('theme:border', (option: any = {}) => {
mode: 'default',
type: 'amis-theme-border',
label: '边框',
name: `css.className.border`,
name: `themeCss.className.border:default`,
needColorCustom: true,
...option
};
@ -539,7 +538,7 @@ setSchemaTpl('theme:paddingAndMargin', (option: any = {}) => {
mode: 'default',
type: 'amis-theme-padding-and-margin',
label: '边距',
name: `css.className.padding-and-margin`,
name: `themeCss.className.padding-and-margin:default`,
...option
};
});
@ -550,7 +549,7 @@ setSchemaTpl('theme:radius', (option: any = {}) => {
mode: 'default',
type: 'amis-theme-radius',
label: '圆角',
name: `css.className.radius`,
name: `themeCss.className.radius:default`,
...option
};
});
@ -560,7 +559,7 @@ setSchemaTpl('theme:shadow', (option: any = {}) => {
return {
type: 'amis-theme-shadow-editor',
label: false,
name: `css.className.boxShadow`,
name: `themeCss.className.boxShadow:default`,
hasSenior: true,
...option
};
@ -568,15 +567,39 @@ setSchemaTpl('theme:shadow', (option: any = {}) => {
setSchemaTpl(
'theme:common',
(exclude: string[] | string, include: string[] | string) => {
(exclude: string[] | string, include: string[]) => {
// key统一转换成Kebab caseeg: boxShadow => bos-shadow
exclude = (
exclude ? (Array.isArray(exclude) ? exclude : [exclude]) : []
).map((key: string) => kebabCase(key));
include = (
include ? (Array.isArray(include) ? include : [include]) : []
).map((key: string) => kebabCase(key));
const moreStyle =
include?.map(key =>
getSchemaTpl(`theme:${key}`, {
name: 'style'
})
) || [];
const styles = moreStyle.concat([
getSchemaTpl('theme:border', {
name: 'style'
}),
getSchemaTpl('theme:radius', {
name: 'style.radius'
}),
getSchemaTpl('theme:paddingAndMargin', {
name: 'style'
}),
getSchemaTpl('theme:colorPicker', {
name: 'style.background',
label: '背景',
needCustom: true,
needGradient: true,
labelMode: 'input'
}),
getSchemaTpl('theme:shadow', {
name: 'style.boxShadow'
})
]);
return [
{
header: '布局',
@ -590,52 +613,19 @@ setSchemaTpl(
].filter(comp => !~exclude.indexOf(comp.type.replace(/^style-/i, '')))
},
{
header: '样式',
key: 'style',
title: '自定义样式',
body: styles
},
{
title: '样式源码',
body: [
getSchemaTpl('theme:border', {
name: 'style'
}),
getSchemaTpl('theme:radius', {
name: 'style.radius'
}),
getSchemaTpl('theme:paddingAndMargin', {
name: 'style'
}),
getSchemaTpl('theme:colorPicker', {
name: 'style.background',
label: '背景',
needCustom: true,
needGradient: true,
labelMode: 'input'
}),
getSchemaTpl('theme:shadow', {
name: 'style.boxShadow'
})
{
type: 'theme-cssCode',
label: false,
isLayout: true
}
]
},
{
header: '圆角',
key: 'radius',
body: []
},
{
header: '间距',
key: 'box-model',
body: []
},
{
header: '背景',
key: 'background',
body: []
},
{
header: '阴影',
key: 'box-shadow',
body: []
}
].filter(item =>
include.length ? ~include.indexOf(item.key) : !~exclude.indexOf(item.key)
);
].filter(item => !~exclude.indexOf(item.key || ''));
}
);