diff --git a/.eslintrc.js b/.eslintrc.js index c9ab2ce..e2fece9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,4 +5,8 @@ module.exports = { 'prettier/@typescript-eslint', 'prettier/react', ], + rules: { + 'react/no-multi-comp': 0, + '@typescript-eslint/member-ordering': 0, + }, }; diff --git a/README.md b/README.md index f4eb1a3..b4f7da2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,60 @@ -#### @alilc/lowcode-engine-ext +### @alilc/lowcode-engine-ext -#### 简介 -lowcode-engine-ext 是阿里低代码引擎官方提供的setter和setter必须依赖的插件集合 +### 简介 +lowcode-engine-ext 是阿里低代码引擎官方提供的 setter 和 setter 必须依赖的插件集合 -setter(设置器)是用来展示每个物料的属性,[setter使用说明手册](https://www.yuque.com/lce/doc/cl03wo_nmhznb) [官方setter列表说明](https://www.yuque.com/lce/doc/oc220p#fl46) \ No newline at end of file +setter(设置器) 是用来展示每个物料的属性,[setter使用说明手册](https://www.yuque.com/lce/doc/cl03wo_nmhznb) [官方setter列表说明](https://www.yuque.com/lce/doc/oc220p#fl46) + +### 使用方式 + +使用 CDN 方式引用,下方是官方提供的两个稳定 CDN + +#### 方式 1:alifd cdn + 1: alifd cdn +```html +https://alifd.alicdn.com/npm/@alilc/lowcode-engine-ext@1.0.5/dist/css/engine-ext.css + +https://alifd.alicdn.com/npm/@alilc/lowcode-engine-ext@1.0.5/dist/js/engine-ext.js +``` + +#### 方式 2: uipaas cdn +```html +https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine-ext/1.0.5/dist/css/engine-ext.css + +https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine-ext/1.0.5/dist/js/engine-ext.js +``` + +#### 拓展变量绑定面板 + +通过传入extraDataMap拓展属性绑定面板 + +```typescript +ctx.skeleton.add({ + area: 'centerArea', + type: 'Widget', + content: pluginMap.VariableBindDialog, + name: 'variableBindDialog', + props: { + getSchema: () => editorController.getSchema(), + // 拓展变量绑定 + extraDataMap: { + props: { + name: 'Props', // 变量组展示名 + key: 'props', // 属性名,例如 this.props + getChildren: () => [ + { + label: 'prop1', + value: 'value1', + }, + { + label: 'prop2', + children: [ + { label: 'propxxx', value: 1 } + ] + } + ], + } + } + }, +}); +``` diff --git a/build.plugin.js b/build.plugin.js index b931776..bd2137e 100644 --- a/build.plugin.js +++ b/build.plugin.js @@ -1,6 +1,6 @@ const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -module.exports = ({ onGetWebpackConfig }) => { +module.exports = ({ context,onGetWebpackConfig }) => { onGetWebpackConfig((config) => { config.resolve.plugin('tsconfigpaths').use(TsconfigPathsPlugin, [ { @@ -19,5 +19,11 @@ module.exports = ({ onGetWebpackConfig }) => { */ config.plugins.delete('hot'); config.devServer.hot(false); + if (context.command === 'start') { + config.devtool('inline-source-map'); + } + // console.log('====context',context.command) + + // config.devtool('inline-source-map'); }); }; diff --git a/package.json b/package.json index 8117a1d..6ce32c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine-ext", - "version": "1.0.2-beta.1", + "version": "1.0.6", "description": "", "files": [ "dist", @@ -11,7 +11,7 @@ "module": "es/index.js", "stylePath": "style.js", "scripts": { - "start": "build-scripts start --config build.cloud.json", + "start": "build-scripts start --config build.cloud.json --port 4008", "build": "build-scripts build && build-scripts build --config build.cloud.json", "cloud-build": "build-scripts build --config build.cloud.json", "test": "build-scripts test", @@ -21,7 +21,8 @@ "stylelint": "stylelint \"**/*.{css,scss,less}\"", "lint": "npm run eslint && npm run stylelint", "f2elint-scan": "f2elint scan -q", - "f2elint-fix": "f2elint fix" + "f2elint-fix": "f2elint fix", + "syncOss": "node ./scripts/sync-oss.js" }, "keywords": [ "ice", @@ -29,13 +30,14 @@ "component" ], "dependencies": { - "@alilc/lowcode-types": "1.0.0", - "@alilc/lowcode-utils": "1.0.0", - "react-color": "^2.19.3", + "@alilc/lowcode-plugin-base-monaco-editor": "^1.1.1", + "@alilc/lowcode-types": "^1.0.0", + "@alilc/lowcode-utils": "^1.0.0", + "antd": "^5.2.3", "classnames": "^2.2.6", "cssjson": "^2.1.3", "js-beautify": "^1.13.0", - "@alilc/lowcode-plugin-base-monaco-editor": "1.0.0" + "react-color": "^2.19.3" }, "devDependencies": { "@alib/build-scripts": "^0.1.3", @@ -47,7 +49,7 @@ "build-plugin-fusion": "^0.1.0", "build-plugin-fusion-css": "1.0.3", "build-plugin-moment-locales": "^0.1.0", - "@alilc/lowcode-editor-skeleton": "1.0.0", + "@alilc/lowcode-editor-skeleton": "^1.0.0", "build-plugin-react-app": "^1.1.2", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", @@ -70,5 +72,6 @@ "registry": "https://registry.npmjs.org/" }, "license": "MIT", - "homepage": "https://unpkg.com/@alilc/lowcode-engine-ext@1.0.2-beta.1/build/index.html" + "homepage": "https://unpkg.com/@alilc/lowcode-engine-ext@1.0.6/build/index.html", + "repository": "git@github.com:alibaba/lowcode-engine-ext.git" } diff --git a/scripts/sync-oss.js b/scripts/sync-oss.js new file mode 100644 index 0000000..407f113 --- /dev/null +++ b/scripts/sync-oss.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +const http = require('http'); +const package = require('../package.json'); +const { version, name } = package; +const options = { + method: 'PUT', + // 暂时使用 日常环境的 uipaas-node,上线后可切换成线上环境 https://uipaas-node.alibaba-inc.com + hostname: 'uipaas-node.alibaba.net', + path: '/staticAssets/cdn/packages', + headers: { + 'Content-Type': 'application/json', + Cookie: 'locale=en-us', + }, + maxRedirects: 20, +}; + +const onResponse = function (res) { + const chunks = []; + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', (chunk) => { + const body = Buffer.concat(chunks); + console.table(JSON.stringify(JSON.parse(body.toString()), null, 2)); + }); + + res.on('error', (error) => { + console.error(error); + }); +}; + +const req = http.request(options, onResponse); + +const postData = JSON.stringify({ + packages: [ + { + packageName: name, + version, + }, + ], + // 可以发布指定源的 npm 包,默认公网 npm + useTnpm: false, +}); + +req.write(postData); + +req.end(); \ No newline at end of file diff --git a/src/components/custom-icon.tsx b/src/components/custom-icon.tsx index f7f12ac..9407b4e 100644 --- a/src/components/custom-icon.tsx +++ b/src/components/custom-icon.tsx @@ -1,15 +1,17 @@ import { Icon } from '@alifd/next'; import * as React from 'react'; +import { useEffect } from 'react'; -const ICON_URL = '//at.alicdn.com/t/font_2761185_gdpwg9vnz7.js'; +const ICON_URL = '//at.alicdn.com/t/a/font_2761185_ccl8ob63gmj.js'; let CustomIcon: any; -window.onload = function () { +document.addEventListener('DOMContentLoaded', function () { + // console.log('3 seconds passed'); CustomIcon = Icon.createFromIconfontCN({ scriptUrl: ICON_URL, }); -}; +}); interface IconProps { type: string; @@ -18,8 +20,18 @@ interface IconProps { style?: any; } + + export default (props: IconProps) => { const { type, size, className = '', style = {} } = props; + useEffect(() => { + if(!CustomIcon){ + CustomIcon = Icon.createFromIconfontCN({ + scriptUrl: ICON_URL, + }); + } + + }, []); return ( <>{CustomIcon && } ); diff --git a/src/index.tsx b/src/index.tsx index 52d0520..6b3685c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,8 +23,10 @@ import TextAreaSetter from './setter/textarea-setter'; import ArraySetter from './setter/array-setter'; import ObjectSetter from './setter/object-setter'; import VariableSetter from './setter/variable-setter'; +import TitleSetter from './setter/title-setter'; import EventBindDialog from './plugin/plugin-event-bind-dialog'; import VariableBindDialog from './plugin/plugin-variable-bind-dialog'; +import SimpleVariableBindPopup from './plugin/plugin-simple-bind-popup' import './index.less'; import packagesInfo from '../package.json'; // suggest: 做成 StringSetter 的一个参数, @@ -54,7 +56,7 @@ class StringDateSetter extends Component { value={moment(value)} showTime={showTime} onChange={(val) => { - onChange(val.format()); + onChange(val ? val.format() : val); }} /> ); @@ -69,7 +71,7 @@ class StringTimePicker extends Component { { - onChange(val.format('HH:mm:ss')); + onChange(val ? val.format('HH:mm:ss') : val); }} /> ); @@ -191,6 +193,7 @@ const engineExt = { SlotSetter: DataSlotSetter, ArraySetter: DataArraySetter, ObjectSetter: DataObjectSetter, + TitleSetter, }, setterMap: { @@ -219,11 +222,13 @@ const engineExt = { SlotSetter: DataSlotSetter, ArraySetter: DataArraySetter, ObjectSetter: DataObjectSetter, + TitleSetter, }, pluginMap: { EventBindDialog, VariableBindDialog, + SimpleVariableBindPopup, }, }; engineExt.version = packagesInfo.version; diff --git a/src/plugin/plugin-event-bind-dialog/index.tsx b/src/plugin/plugin-event-bind-dialog/index.tsx index d474589..e7c32f2 100644 --- a/src/plugin/plugin-event-bind-dialog/index.tsx +++ b/src/plugin/plugin-event-bind-dialog/index.tsx @@ -9,8 +9,13 @@ import './index.less'; const defaultParams = '{\n \t "testKey":123 \n}'; // 模板变量占位 const tempPlaceHolder = '${extParams}'; +const tempPlaceHolderReg = /\$\{extParams\}/g; + +const propEventsReg = /(this\.)?props\.[a-zA-Z0-9\-_]+/; + const defaultEditorOption = { - style: { width: '100%', height: '319px' }, + height:'319px', + width:'100%', readOnly: false, automaticLayout: true, folding: true, // 默认开启折叠代码功能 @@ -76,8 +81,8 @@ export default class EventBindDialog extends Component { // }, ]; - private relatedEventName: string = ''; - private bindEventName: string = ''; + private relatedEventName = ''; + private bindEventName = ''; state: any = { visiable: false, @@ -179,7 +184,7 @@ export default class EventBindDialog extends Component { onSearchEvent = () => {}; - onChange = (checked) => { + onChange = (checked: boolean) => { this.setState({ useParams: checked, }); @@ -196,22 +201,23 @@ export default class EventBindDialog extends Component { } }; - pickupFunctionName = (codeStr) => { + pickupFunctionName = (codeStr: string) => { return codeStr.substr(0, codeStr.indexOf('(')); }; - removeSpace = (str) => { + removeSpace = (str: string) => { return str.replace(/\s*/g, ''); }; - formatTemplate = (template, eventName, useParams) => { + formatTemplate = (template: string, eventName: string, useParams: boolean) => { let formatTemp; if (template) { const functionName = this.pickupFunctionName(template); - formatTemp = template.replace(new RegExp('^s*' + functionName), eventName); + formatTemp = template.replace(new RegExp(`^s*${ functionName}`), eventName); if (useParams) { - formatTemp = formatTemp.replace(/tempPlaceHolder/g, 'extParams'); + formatTemp = formatTemp.replace(tempPlaceHolderReg, 'extParams'); + } else { const leftIndex = formatTemp.indexOf('('); const rightIndex = formatTemp.indexOf(')'); @@ -228,25 +234,20 @@ export default class EventBindDialog extends Component { // 重新join进去 formatTemp = - formatTemp.substr(0, leftIndex) + - '(' + - paramList.join(',') + - ')' + - formatTemp.substr(rightIndex + 1, formatTemp.length); - console.log(formatTemp); + `${formatTemp.substr(0, leftIndex) + }(${ + paramList.join(',') + })${ + formatTemp.substr(rightIndex + 1, formatTemp.length)}`; } } return formatTemp; }; - formatEventName = (eventName) => { - const newEventNameArr = eventName.split(''); - const index = eventName.indexOf('.'); - if (index >= 0) { - newEventNameArr[index + 1] = newEventNameArr[index + 1].toUpperCase(); - } - return newEventNameArr.join('').replace(/\./, ''); + formatEventName = (eventName: string) => { + // 去除this. + return eventName.replace(/(this\.)|(\s+)/, ''); }; onOk = () => { @@ -268,14 +269,14 @@ export default class EventBindDialog extends Component { ); // 选中的是新建事件 && 注册了sourceEditor面板 - if (this.state.selectedEventName == '') { + if (this.state.selectedEventName == '' && !propEventsReg.test(formatEventName)) { // 判断面板是否处于激活状态 skeleton.showPanel('codeEditor'); const formatTemp = this.formatTemplate(configEventData.template, formatEventName, useParams); setTimeout(() => { event.emit('codeEditor.addFunction', { functionName: formatEventName, - templete: formatTemp, + template: formatTemp, }); }, 200); } @@ -283,16 +284,15 @@ export default class EventBindDialog extends Component { this.closeDialog(); }; - onChangeEditor = (paramStr) => { + onChangeEditor = (paramStr: string) => { this.setState({ paramStr, }); - // console.log(newCode); }; render() { const { selectedEventName, eventName, visiable, paramStr, useParams } = this.state; - console.log('selectedEventName:' + selectedEventName); + // console.log('selectedEventName:' + selectedEventName); return ( {
-
事件名称
+
+ 事件名称 + {(window as any).lowcodeSetterSwitch?.enablePropsEvents && ( + + 如需绑定 props 属性,可通过 props.xxx 进行绑定 + + )} +
@@ -370,7 +377,7 @@ export default class EventBindDialog extends Component { value={paramStr} {...defaultEditorOption} {...{ language: 'json' }} - onChange={(newCode) => this.onChangeEditor(newCode)} + onChange={(newCode: string) => this.onChangeEditor(newCode)} /> {!useParams &&
}
diff --git a/src/plugin/plugin-simple-bind-popup/index.less b/src/plugin/plugin-simple-bind-popup/index.less new file mode 100644 index 0000000..41796b4 --- /dev/null +++ b/src/plugin/plugin-simple-bind-popup/index.less @@ -0,0 +1,95 @@ +.simple-dialog-body { + background-color: white; + padding: 12px; + box-shadow: 0 2px 5px rgba(0,0,0,.05); + border-radius: 4px; + + .dialog-small-title { + font-weight: 700; + margin-bottom: 8px; + color: var(--color-title, rgb(0, 0, 0)); + display: flex; + justify-content: space-between; + .error-message{ + color:var(--color-function-error, rgba(255,97,96)); + font-weight: 400; + } + } + + .dialog-right-container { + width: 400px; + position: relative; + + .event-input-container { + margin-bottom: 20px; + } + textarea { + height: 100%; + } + + .editor-type-tag { + width: 18px; + height: 18px; + background-color: var(--color-layer-mask-background, rgba(17, 105, 247, 0.18)); + border-radius: 2px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 35px; + z-index: 100; + left: 6px; + } + + .editor-context{ + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); + padding: 5px 5px 0px 15px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + } + + .editor-context-error{ + border-color:var(--color-function-error, rgba(255,97,96,0.7)); + } + } +} + +.simple-bind-dialog-bottom { + margin-top: 12px; + .bottom-left-container { + float: left; + } + .bottom-right-container { + float: right; + } +} + +.vs-variable-minimize { + width: 120px; + display: flex; + justify-content: space-between; + align-items: center; + position: absolute; + bottom: 10px; + right: 286px; + z-index: 100; + background: var(--color-block-background-normal, #fff); + padding: 8px 12px; + border-radius: 3px; + border: 1px solid var(--color-field-border, #ddd); + box-shadow: 0 0 8px #aaa; + font-size: 14px; + font-weight: 700; + cursor: pointer; + img { + width: 12px; + } +} + +.lc-code-control:hover { + border-color: transparent; +} + +.lc-code-control.ve-focused { + border-color: transparent; +} diff --git a/src/plugin/plugin-simple-bind-popup/index.tsx b/src/plugin/plugin-simple-bind-popup/index.tsx new file mode 100644 index 0000000..aa3bc62 --- /dev/null +++ b/src/plugin/plugin-simple-bind-popup/index.tsx @@ -0,0 +1,264 @@ +import React, { Component } from 'react'; +import { Button, Overlay } from '@alifd/next'; +import { PluginProps } from '@alilc/lowcode-types'; +import { event } from '@alilc/lowcode-engine'; +import MonacoEditor from '@alilc/lowcode-plugin-base-monaco-editor'; +import './index.less'; +import { adjustOverlayPosition } from './utils'; + + +const defaultEditorProps = { + width: '100%', + height: '150px', +}; + +const defaultEditorOption = { + options:{ + readOnly: false, + automaticLayout: true, + folding: false, // 默认开启折叠代码功能 + lineNumbers: 'off', + wordWrap: 'on', + formatOnPaste: true, + fontSize: 12, + tabSize: 2, + scrollBeyondLastLine: false, + fixedOverflowWidgets: false, + snippetSuggestions: 'top', + minimap: { + enabled: false, + }, + scrollbar: { + vertical: 'auto', + horizontal: 'auto', + verticalScrollbarSize:0 + }, + } +}; + +export default class SimpleVariableBindPopup extends Component { + state = { + visiable: false, + isOverFlowMaxSize:false, + jsCode: '', + field: {}, // 编辑器全局变量 + treeList: [], + minimize: false, // 是否最小化 + autoExpandParent: true, + maxTextSize:0, // 绑定变量最大字符数 + node: null as any as HTMLElement, // 触发的节点 + }; + + private editorJsRef = React.createRef(); + private nodeRef: HTMLDivElement | null = null; + private overlayRef = React.createRef(); + + componentDidMount() { + event.on('common:variableBindDialog.openDialog', ({ field, node, maxTextSize }) => { + const finalMaxTextSize = maxTextSize && typeof maxTextSize === 'number' ? maxTextSize : this.props.config?.props?.maxTextSize; + this.setState({ + field, + node: node || this.nodeRef, + maxTextSize: finalMaxTextSize, + }, () => { + this.initCode(); + this.openDialog(); + }); + }); + } + + initCode = () => { + const { field } = this.state; + const fieldValue = field.getValue(); + const jsCode = fieldValue?.value; + + this.setState({ + jsCode, + // fullScreenStatus: false, + minimize: false, // 是否最小化 + isOverFlowMaxSize:false, + }); + }; + + openDialog = () => { + this.setState({ visiable: true }); + }; + + closeDialog = () => { + this.setState({ + visiable: false, + minimize: false, + }); + }; + + updateCode = (newCode) => { + let isOverFlowMaxSize = false; + if (this.state.maxTextSize){ + isOverFlowMaxSize = newCode?.length > this.state.maxTextSize + } + + this.setState( + { + jsCode: newCode, + isOverFlowMaxSize + }, + this.autoSave, + ); + }; + + autoSave = () => { + const { autoSave } = this.props; + if (autoSave) { + this.onOk(true); + } + }; + + editorDidMount = () => { + setTimeout(() => { + this.editorNode = this.editorJsRef.current; // 记录当前dom节点; + }, 0); + }; + + onOk = (autoSave) => { + const { field, jsCode } = this.state; + if(jsCode === undefined || jsCode?.length == 0) { + return this.removeTheBinding() + } + + const fieldValue = field.getValue(); + field.setValue({ + type: 'JSExpression', + value: jsCode, + mock: + Object.prototype.toString.call(fieldValue) === '[object Object]' + ? fieldValue.mock + : fieldValue, + }); + if (autoSave !== true) { + this.closeDialog(); + } + }; + + removeTheBinding = () => { + const { field } = this.state; + const fieldValue = field.getValue(); + const value = + Object.prototype.toString.call(fieldValue) === '[object Object]' + ? fieldValue.mock + : fieldValue; + console.debug('value', value, 'fieldValue', fieldValue, field) + field.setValue(value); + this.closeDialog(); + }; + + renderBottom = () => { + const { jsCode } = this.state; + return ( +
+
+ {jsCode && jsCode.length > 0 && ( + + )} +
+ +
+ +    + +
+
+ ); + }; + + minimizeClick = (state) => { + this.setState({ + minimize: state, + visiable: !state, + }); + }; + + renderErrorMessage = () => { + const {isOverFlowMaxSize,maxTextSize} = this.state; + return ( + isOverFlowMaxSize ? 表达式文本不能超过{maxTextSize}个字符,请换成函数调用 :null + ) + } + + isBtnDisable = () => { + const { isOverFlowMaxSize } = this.state; + return isOverFlowMaxSize; + } + + + render() { + const { + visiable, + jsCode, + minimize, + isOverFlowMaxSize, + } = this.state; + + return ( +
+ {minimize ? ( +
+ this.minimizeClick(false)} + src="https://img.alicdn.com/imgextra/i2/O1CN01HzeCND1vl948xPEWm_!!6000000006212-55-tps-200-200.svg" + /> + this.minimizeClick(false)} className="vs-variable-minimize-title"> + 变量绑定 + + +
+ ) : ( + '' + )} +
this.nodeRef = ref} /> + {this.state.node && + this.state.node} + offset={[-380, 0]} + onPosition={() => { + adjustOverlayPosition(this.overlayRef.current!, [20]) + }} + > +
+
+
绑定 {this.renderErrorMessage()}
+
+
=
+ this.updateCode(newCode)} + editorDidMount={(useMonaco, editor) => { + this.editorDidMount.call(this, editor, useMonaco); + }} + /> +
+
+ {this.renderBottom()} +
+
+ } +
+ ); + } +} diff --git a/src/plugin/plugin-simple-bind-popup/utils.ts b/src/plugin/plugin-simple-bind-popup/utils.ts new file mode 100644 index 0000000..7615caf --- /dev/null +++ b/src/plugin/plugin-simple-bind-popup/utils.ts @@ -0,0 +1,34 @@ +export function adjustOverlayPosition(overlay: HTMLElement, padding: any[]) { + const rect = overlay.getBoundingClientRect(); + const style = getComputedStyle(overlay); + const windowWidth = window.innerWidth; + const [paddingTop = 0] = padding || [0]; + const [, paddingRight = paddingTop, paddingBottom = paddingTop] = padding || [0]; + const [,,, paddingLeft = paddingRight || paddingTop] = padding || [0]; + + let newTop = rect.top; + let newLeft = rect.left; + + // 检查并修正左侧和右侧 + if (rect.left < 0) { + newLeft = paddingLeft; + console.debug('超出可视区域', 'left'); + } else if (rect.right > windowWidth) { + console.debug('超出可视区域', 'right'); + newLeft = windowWidth - rect.width - paddingRight; + } + + // 检查并修正顶部和底部 + if (rect.top < 0) { + newTop = paddingTop; + console.debug('超出可视区域', 'top'); + } else if (style.bottom.includes('-')) { + console.debug('超出可视区域', 'bottom'); + // 下方超出的话则反转展示在上方。 + newTop -= parseFloat(style.height) + paddingBottom; + } + + // 应用修正后的位置 + overlay.style.left = `${newLeft}px`; + overlay.style.top = `${newTop}px`; +} \ No newline at end of file diff --git a/src/plugin/plugin-variable-bind-dialog/index.less b/src/plugin/plugin-variable-bind-dialog/index.less index cbecf8a..494b2e8 100644 --- a/src/plugin/plugin-variable-bind-dialog/index.less +++ b/src/plugin/plugin-variable-bind-dialog/index.less @@ -5,11 +5,19 @@ .dialog-small-title { font-weight: 700; margin-bottom: 8px; - color: rgb(0, 0, 0); + color: var(--color-title, rgb(0, 0, 0)); + display: flex; + justify-content: space-between; + .error-message{ + color:var(--color-function-error, rgba(255,97,96)); + font-weight: 400; + } } + + .dialog-help-tip-input { height: 180px; - border: 1px solid rgba(31, 56, 88, 0.3); + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); border-top: none; border-radius: 0 0 3px 3px; padding: 8px; @@ -27,15 +35,19 @@ float: left; .vs-variable-selector-inner { - border: 1px solid rgba(31, 56, 88, 0.3); + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); display: flex; - height: 382px; + height: 389px; border-radius: 3px; ul { list-style: none; margin: 0; padding: 0; } + .tree-container{ + padding-left: 10px; + } + .vs-variable-selector-ul { li { height: 28px; @@ -45,21 +57,22 @@ position: relative; white-space: nowrap; &:hover { - background: rgba(128, 128, 128, 0.15); + background: var(--color-block-background-light, rgba(128, 128, 128, 0.15)); } } .active { - background: rgba(128, 128, 128, 0.15); + background: var(--color-block-background-light, rgba(128, 128, 128, 0.15)); } } .vs-variable-selector-category { width: 110px; - border-right: 1px solid rgba(31, 56, 88, 0.3); + border-right: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); height: 100%; overflow: auto; } .vs-variable-selector-items-container { - width: 157px; + width: 197px; + overflow-y: auto; .ve-search-control { height: 28px; margin: 10px; @@ -75,7 +88,7 @@ .dialog-left-context { width: 270px; height: 392px; - border: 1px solid rgba(31, 56, 88, 0.3); + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); border-radius: 3px; .variable-type-container { @@ -97,7 +110,7 @@ } .select-item-active { - background: rgba(128, 128, 128, 0.15); + background: var(--color-block-background-light, rgba(128, 128, 128, 0.15)); } .event-select-container { @@ -126,7 +139,7 @@ } .variable-item:hover { - background-color: rgba(31, 56, 88, 0.06); + background-color: var(--color-block-background-light, rgba(31, 56, 88, 0.06)); } } } @@ -136,6 +149,7 @@ width: 420px; padding-left: 12px; float: left; + position: relative; .event-input-container { margin-bottom: 20px; @@ -143,6 +157,31 @@ textarea { height: 100%; } + + .editor-type-tag { + width: 18px; + height: 18px; + background-color: var(--color-layer-mask-background, rgba(17, 105, 247, 0.18)); + border-radius: 2px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 35px; + z-index: 100; + left: 17px; + } + + .editor-context{ + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.3)); + padding: 5px 5px 0px 15px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + } + + .editor-context-error{ + border-color:var(--color-function-error, rgba(255,97,96,0.7)); + } } } @@ -170,10 +209,10 @@ bottom: 10px; right: 286px; z-index: 100; - background: #fff; + background: var(--color-block-background-normal, #fff); padding: 8px 12px; border-radius: 3px; - border: 1px solid #ddd; + border: 1px solid var(--color-field-border, #ddd); box-shadow: 0 0 8px #aaa; font-size: 14px; font-weight: 700; @@ -182,3 +221,16 @@ width: 12px; } } + +.next-tree.next-node-indent .next-tree-node-inner.next-selected .next-tree-node-label { + color: var(--color-text-reverse, #fff); + background-color: var(--color-brand, #4576f5); +} + +.lc-code-control:hover { + border-color: transparent; +} + +.lc-code-control.ve-focused { + border-color: transparent; +} diff --git a/src/plugin/plugin-variable-bind-dialog/index.tsx b/src/plugin/plugin-variable-bind-dialog/index.tsx index 3b20e0f..06c7381 100644 --- a/src/plugin/plugin-variable-bind-dialog/index.tsx +++ b/src/plugin/plugin-variable-bind-dialog/index.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Dialog, Input, Button, Icon } from '@alifd/next'; +import { Dialog, Input, Button, Icon, Tree } from '@alifd/next'; import { PluginProps } from '@alilc/lowcode-types'; import { event, project } from '@alilc/lowcode-engine'; import MonacoEditor from '@alilc/lowcode-plugin-base-monaco-editor'; @@ -20,33 +20,38 @@ field: '表单Field对象' `; const defaultEditorProps = { - style: { width: '100%', height: '200px' }, + width: '100%', + height: '200px', }; const defaultEditorOption = { - readOnly: false, - automaticLayout: true, - folding: true, // 默认开启折叠代码功能 - lineNumbers: 'on', - wordWrap: 'off', - formatOnPaste: true, - fontSize: 12, - tabSize: 2, - scrollBeyondLastLine: false, - fixedOverflowWidgets: false, - snippetSuggestions: 'top', - minimap: { - enabled: false, - }, - scrollbar: { - vertical: 'auto', - horizontal: 'auto', - }, + options:{ + readOnly: false, + automaticLayout: true, + folding: false, // 默认开启折叠代码功能 + lineNumbers: 'off', + wordWrap: 'on', + formatOnPaste: true, + fontSize: 12, + tabSize: 2, + scrollBeyondLastLine: false, + fixedOverflowWidgets: false, + snippetSuggestions: 'top', + minimap: { + enabled: false, + }, + scrollbar: { + vertical: 'auto', + horizontal: 'auto', + verticalScrollbarSize:0 + }, + } }; export default class VariableBindDialog extends Component { state = { visiable: false, + isOverFlowMaxSize:false, // stateVaroableList: [], helpText: HelpText, // contextKeys: [], @@ -56,22 +61,42 @@ export default class VariableBindDialog extends Component { selParentVariable: null, // 选中的父级变量 childrenVariableList: [], // 子级变量列表 field: {}, // 编辑器全局变量 + treeList: [], minimize: false, // 是否最小化 + autoExpandParent: true, + expandedKeys: [], + maxTextSize: this.props.config?.props?.maxTextSize || 0, // 绑定变量最大字符数 }; private editorJsRef = React.createRef(); private monocoEditor: any; + private matchedKeys: null; + + get extraDataMap() { + return this.props.config.props?.extraDataMap; + } + componentDidMount() { - event.on('common:variableBindDialog.openDialog', ({ field }) => { - this.setState({ field }, () => { + event.on('common:variableBindDialog.openDialog', ({ field, maxTextSize }) => { + // 在触发事件时指定最大长度,根据传入的长度重置一下 + const finalMaxTextSize = maxTextSize && typeof maxTextSize ? maxTextSize : this.props.config?.props?.maxTextSize || 0; + this.setState({ + field, + maxTextSize: finalMaxTextSize, + }, () => { this.initCode(); this.openDialog(); }); }); } + exportSchema = () => { + // 可以定制getSchema方法 + return this.props.config?.props?.getSchema?.() || project.exportSchema(); + }; + initCode = () => { const { field } = this.state; const fieldValue = field.getValue(); @@ -86,6 +111,7 @@ export default class VariableBindDialog extends Component { selParentVariable: null, // 选中的父级变量 childrenVariableList: [], // 子级变量列表 minimize: false, // 是否最小化 + isOverFlowMaxSize:false, }); }; @@ -95,14 +121,16 @@ export default class VariableBindDialog extends Component { * @return {Array} */ getMethods(): any[] { - const schema = project.exportSchema(); - + const schema = this.exportSchema(); const methodsMap = schema.componentsTree[0]?.methods; const methods = []; - for (const key in methodsMap) { if (Object.prototype.hasOwnProperty.call(methodsMap, key) && key) { - methods.push(`${key}()`); + // methods.push(`${key}()`); + methods.push({ + label: `${key}`, + key, + }); } } @@ -115,18 +143,77 @@ export default class VariableBindDialog extends Component { * @return {Array} */ getVarableList(): any[] { - const schema = project.exportSchema(); + const schema = this.exportSchema(); const stateMap = schema.componentsTree[0]?.state; + const dataSourceMap = {}; const dataSource = []; for (const key in stateMap) { if (Object.prototype.hasOwnProperty.call(stateMap, key) && key) { dataSource.push(`this.state.${key}`); + const valueString = stateMap[key].value; + let value; + try { + value = eval(`(${valueString})`); + } catch (e) {} + + // 属性为false 或者 为"" 也显示到dialog中 + if (value || value === false || value === '') { + dataSourceMap[key] = value; + } } } + const treeList = []; + this.walkNode(dataSourceMap, -1, treeList); + // this.setState({ + // treeList + // }) + return treeList; + } - return dataSource; + /** + * 通过子节点id查找节点path + * @param tree + * @param func + * @param field + * @param path + * @returns + */ + treeFindPath(tree, func, field = '', path = []) { + if (!tree) return []; + for (const data of tree) { + field === '' ? path.push(data) : path.push(data[field]); + if (func(data)) return path; + if (data.children) { + const findChildren = this.treeFindPath(data.children, func, field, path); + if (findChildren.length) return findChildren; + } + path.pop(); + } + return []; + } + + /** + * 循环遍历节点 + * @param dataSourceMap + * @param deepNum + * @param treeList + */ + walkNode(dataSourceMap, deepNum, treeList) { + deepNum++; + let index = 0; + for (const key in dataSourceMap) { + const treeData = {}; + treeData.label = key; + // treeData.key = deepNum+'_'+index; + if (typeof dataSourceMap[key] === 'object' && !(dataSourceMap[key] instanceof Array)) { + treeData.children = []; + this.walkNode(dataSourceMap[key], deepNum, treeData.children); + } + index++; + treeList.push(treeData); + } } /** @@ -135,14 +222,18 @@ export default class VariableBindDialog extends Component { * @return {Array} */ getDataSource(): any[] { - const schema = project.exportSchema(); + const schema = this.exportSchema(); const stateMap = schema.componentsTree[0]?.dataSource; const list = stateMap?.list || []; const dataSource = []; for (const item of list) { if (item && item.id) { - dataSource.push(`this.state.${item.id}`); + // dataSource.push(`this.state.${item.id}`); + dataSource.push({ + label: `${item.id}`, + key: item.id, + }); } } @@ -184,6 +275,7 @@ export default class VariableBindDialog extends Component { const methods = this.getMethods(); const stateVaroableList = this.getVarableList(); const dataSource = this.getDataSource(); + this.setState({ variableListMap: { stateVaroableList: { @@ -198,6 +290,7 @@ export default class VariableBindDialog extends Component { name: '数据源', childrens: dataSource, }, + ...this.extraDataMap, }, }); }, @@ -227,9 +320,16 @@ export default class VariableBindDialog extends Component { }; updateCode = (newCode) => { + + let isOverFlowMaxSize = false; + if (this.state.maxTextSize) { + isOverFlowMaxSize = newCode?.length > this.state.maxTextSize + } + this.setState( { jsCode: newCode, + isOverFlowMaxSize }, this.autoSave, ); @@ -290,7 +390,7 @@ export default class VariableBindDialog extends Component {
-    @@ -302,9 +402,16 @@ export default class VariableBindDialog extends Component { ); }; - onVariableSearchChange = (val) => { + handleExpand = (keys) => { this.setState({ - searchValue: val, + expandedKeys: keys, + autoExpandParent: false, + }); + }; + + onVariableSearchChange = (value) => { + this.setState({ + searchValue: value, }); const { variableListMap, selParentVariable } = this.state; @@ -312,19 +419,56 @@ export default class VariableBindDialog extends Component { if (!selectedVariable) { return; } + value = value.trim(); + if (!value) { + this.matchedKeys = null; + return; + } - let newChildrenVariableList = []; - newChildrenVariableList = selectedVariable.childrens.filter((item) => item.indexOf(val) > -1); + const matchedKeys = []; + const loop = (data) => + data.forEach((item) => { + if (item.label.indexOf(value) > -1) { + matchedKeys.push(item.key); + } + if (item.children && item.children.length) { + loop(item.children); + } + }); + loop(selectedVariable.childrens); this.setState({ - childrenVariableList: newChildrenVariableList, + expandedKeys: [...matchedKeys], + autoExpandParent: true, }); + this.matchedKeys = matchedKeys; }; onVariableItemClick = (key: string) => { const { variableListMap } = this.state; + + let childrenVariableList; + if (this.extraDataMap?.[key] && this.extraDataMap[key]?.getChildren?.()) { + childrenVariableList = this.extraDataMap[key].getChildren(); + } else { + childrenVariableList = variableListMap[key].childrens; + } + + // const matchedKeys = []; + // const loop = data => + // data.forEach(item => { + // if (item.label.indexOf(value) > -1) { + // matchedKeys.push(item.key); + // } + // if (item.children && item.children.length) { + // loop(item.children); + // } + // }); + // loop(childrenVariableList); + this.setState({ selParentVariable: key, - childrenVariableList: variableListMap[key].childrens, + childrenVariableList, + // matchedKeys }); }; @@ -335,6 +479,34 @@ export default class VariableBindDialog extends Component { }); }; + onSelectTreeNode = (selectedKeys, extra) => { + const { selParentVariable, childrenVariableList } = this.state; + + const label = extra.selectedNodes[0]?.props?.label; + const key = extra.selectedNodes[0]?.key; + let selectLabel; + if (selParentVariable == 'stateVaroableList') { + const pathList = this.treeFindPath(childrenVariableList, (data) => data.key == key, 'label'); + selectLabel = `this.state.${pathList.join('.')}`; + } else if (selParentVariable == 'methods') { + selectLabel = `this.${label}()`; + } else if (selParentVariable == 'dataSource') { + selectLabel = `this.state.${label}`; + } else { + const fondKey = Object.keys(this.extraDataMap || {}).find((k) => k === selParentVariable); + if (fondKey) { + const propKey = this.extraDataMap[fondKey].key; + const pathList = this.treeFindPath( + childrenVariableList, + (data: any) => data.key === key, + 'label', + ); + selectLabel = `this.${propKey}.${pathList.join('.')}`; + } + } + this.onSelectItem(selectLabel); + }; + renderTitle = () => { return (
@@ -348,6 +520,21 @@ export default class VariableBindDialog extends Component { ); }; + renderErrorMessage = () => { + const {isOverFlowMaxSize,maxTextSize} = this.state; + return ( + isOverFlowMaxSize ? 表达式文本不能超过{maxTextSize}个字符,请换成函数调用 :null + + ) + } + + isBtnDisable = () => { + const {jsCode,isOverFlowMaxSize} = this.state; + + return jsCode === undefined || jsCode?.length == 0 || isOverFlowMaxSize; + } + + render() { const { visiable, @@ -358,7 +545,15 @@ export default class VariableBindDialog extends Component { jsCode, searchValue, minimize, + expandedKeys, + autoExpandParent, + isOverFlowMaxSize, } = this.state; + + const filterTreeNode = (node) => { + return this.matchedKeys && this.matchedKeys.indexOf(node.props.eventKey) > -1; + }; + return (
{minimize ? ( @@ -384,6 +579,9 @@ export default class VariableBindDialog extends Component { title={this.renderTitle()} onClose={this.closeDialog} footer={this.renderBottom()} + popupContainer={ + document.getElementById('engine-popup-container') ? 'engine-popup-container' : undefined + } >
@@ -414,21 +612,33 @@ export default class VariableBindDialog extends Component { onChange={this.onVariableSearchChange} />
-
    - {childrenVariableList && + {/*
      */} +
        + {/* {childrenVariableList && childrenVariableList.map((item) => (
      • this.onSelectItem(item)} key={item}> {item}
      • - ))} + ))} */} + +
-
绑定
-
+
绑定 {this.renderErrorMessage()}
+
+
=
= target?.path; + if (!targetPath || targetPath.length < 2) { + console.warn( + `[ArraySetter] onItemChange 接收的 target.path <${ + targetPath || 'undefined' + }> 格式非法需为 [propName, arrayIndex, key?]`, + ); + return; + } + const { field } = props; + const { path } = field; + if (path[0] !== targetPath[0]) { + console.warn( + `[ArraySetter] field.path[0] !== target.path[0] <${path[0]} !== ${targetPath[0]}>`, + ); + return; + } + try { + const fieldValue = field.getValue(); + fieldValue[index] = item.getValue(); + field?.setValue(fieldValue); + } catch (e) { + console.warn('[ArraySetter] extraProps.setValue failed :', e); + } +}; + interface ArraySetterProps { value: any[]; - field: SettingField; - itemSetter?: SetterType; - columns?: FieldConfig[]; + field: IPublicModelSettingField; + itemSetter?: IPublicTypeSetterType; + itemMaxLength?: number; + variableBind?: boolean; + columns?: IPublicTypeFieldConfig[]; multiValue?: boolean; hideDescription?: boolean; onChange?: Function; + extraProps: {renderFooter?: (options: ArraySetterProps & {onAdd: (val?: {}) => any}) => any} } export class ListSetter extends Component { @@ -33,110 +70,66 @@ export class ListSetter extends Component { constructor(props: ArraySetterProps) { super(props); - this.init(); } - init() { - const { value, field } = this.props; - const items: SettingField[] = []; + static getDerivedStateFromProps(props: ArraySetterProps, state: ArraySetterState) { + const items: IPublicModelSettingField[] = []; + const { value, field } = props; const valueLength = value && Array.isArray(value) ? value.length : 0; for (let i = 0; i < valueLength; i++) { - const item = field.createField({ - name: i, - setter: this.props.itemSetter, - forceInline: 1, - extraProps: { - defaultValue: value[i], - setValue: this.onItemChange, - }, - }); + let item = state.items[i]; + if (!item) { + item = field.createField({ + name: i.toString(), + setter: props.itemSetter, + forceInline: 1, + type: 'field', + extraProps: { + defaultValue: value[i], + setValue: (target: IPublicModelSettingField) => { + onItemChange(target, i, item, props); + }, + }, + }); + item.setValue(value[i]); + } items.push(item); } - field.setValue(value); - this.state = { items }; - } - /** - * onItemChange 用于 ArraySetter 的单个 index 下的数据发生变化, - * 因此 target.path 的数据格式必定为 [propName, arrayIndex, key?]。 - * - * @param target - * @param value - */ - onItemChange = (target: SettingField) => { - const targetPath: (string | number)[] = target?.path; - if (!targetPath || targetPath.length < 2) { - console.warn( - `[ArraySetter] onItemChange 接收的 target.path <${ - targetPath || 'undefined' - }> 格式非法需为 [propName, arrayIndex, key?]`, - ); - return; - } - const { field } = this.props; - const { items } = this.state; - const path = field.path; - if (path[0] !== targetPath[0]) { - console.warn( - `[ArraySetter] field.path[0] !== target.path[0] <${path[0]} !== ${targetPath[0]}>`, - ); - return; - } - const fieldValue = field.getValue(); - try { - const index = +targetPath[1]; - fieldValue[index] = items[index].getValue(); - field?.extraProps?.setValue?.call(field, field, fieldValue); - } catch (e) { - console.warn('[ArraySetter] extraProps.setValue failed :', e); - } - }; + return { + items, + }; + } onSort(sortedIds: Array) { - const { field } = this.props; + const { onChange, value: oldValues } = this.props; const { items } = this.state; const values: any[] = []; - const oldValues = field.getValue(); - const newItems: SettingField[] = []; + const newItems: IPublicModelSettingField[] = []; sortedIds.map((id, index) => { - const item = items[+id]; - item.setKey(index); - values[index] = oldValues[id]; - newItems[index] = item; + const itemIndex = items.findIndex(item => item.id === id); + values[index] = oldValues[itemIndex]; + newItems[index] = items[itemIndex]; return id; }); - field.setValue(values); - this.setState({ items: newItems }); + onChange?.(values); } - onAdd() { - const { items = [] } = this.state; - const { itemSetter, field } = this.props; - const values = field.getValue() || []; - const { initialValue } = itemSetter; - const defaultValue = typeof initialValue === 'function' ? initialValue(field) : initialValue; - const item = field.createField({ - name: items.length, - setter: itemSetter, - forceInline: 1, - extraProps: { - defaultValue, - setValue: this.onItemChange, - }, - }); + onAdd(newValue?: {[key: string]: any}) { + const { itemSetter, field, onChange, value = [] } = this.props; + const values = value || []; + const initialValue = (itemSetter as any)?.initialValue; + const defaultValue = newValue ? newValue : (typeof initialValue === 'function' ? initialValue(field) : initialValue); values.push(defaultValue); - - items.push(item); this.scrollToLast = true; - field?.setValue(values); - this.setState({ items }); + onChange?.(values); } - onRemove(removed: SettingField) { - const { field } = this.props; + onRemove(removed: IPublicModelSettingField) { + const { onChange, value } = this.props; const { items } = this.state; - const values = field.getValue(); + const values = value || []; let i = items.indexOf(removed); items.splice(i, 1); values.splice(i, 1); @@ -146,9 +139,8 @@ export class ListSetter extends Component { i++; } removed.remove(); - const pureValues = values.map((item: any) => Object.assign({}, item)); - field?.setValue(pureValues); - this.setState({ items }); + const pureValues = values.map((item: any) => typeof(item) === 'object' ? Object.assign({}, item):item); + onChange?.(pureValues); } componentWillUnmount() { @@ -158,16 +150,11 @@ export class ListSetter extends Component { } render() { - const { hideDescription } = this.props; - let columns: any = null; + const { hideDescription, extraProps = {}, itemMaxLength, columns } = this.props; + const { renderFooter } = extraProps; const { items } = this.state; const { scrollToLast } = this; this.scrollToLast = false; - if (this.props.columns) { - columns = this.props.columns.map((column) => ( - - )); - } const lastIndex = items.length - 1; @@ -177,7 +164,7 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> { <Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}> {items.map((field, index) => ( <ArrayItem - key={index} + key={field.id} scrollIntoView={scrollToLast && index === lastIndex} field={field} onRemove={this.onRemove.bind(this, field)} @@ -200,20 +187,34 @@ export class ListSetter extends Component<ArraySetterProps, ArraySetterState> { return ( <div className="lc-setter-list lc-block-setter"> {!hideDescription && columns && items.length > 0 ? ( - <div className="lc-setter-list-columns">{columns}</div> + <div className="lc-setter-list-columns">{ + columns.map((column) => ( + <Title key={column.name} title={column.title || (column.name as string)} /> + )) + }</div> ) : null} {content} <div className="lc-setter-list-add"> - <Button text type="primary" onClick={this.onAdd.bind(this)}> - <span>添加一项 +</span> - </Button> + { + !renderFooter + ? (itemMaxLength && items.length >= Number(itemMaxLength) + ? null + :( + <Button text type="primary" onClick={() => { + this.onAdd() + }}> + <span>添加一项 +</span> + </Button> + )) + : renderFooter({...this.props, onAdd: this.onAdd.bind(this),}) + } </div> </div> ); } } class ArrayItem extends Component<{ - field: SettingField; + field: IPublicModelSettingField; onRemove: () => void; scrollIntoView: boolean; }> { @@ -248,16 +249,14 @@ class ArrayItem extends Component<{ } } -class TableSetter extends ListSetter { - // todo: - // forceInline = 1 - // has more actions -} +class TableSetter extends ListSetter {} export default class ArraySetter extends Component<{ value: any[]; - field: SettingField; - itemSetter?: SetterType; + field: IPublicModelSettingField; + itemSetter?: IPublicTypeSetterType; + itemMaxLength?: number; + variableBind?: boolean; mode?: 'popup' | 'list'; forceInline?: boolean; multiValue?: boolean; @@ -269,16 +268,13 @@ export default class ArraySetter extends Component<{ render() { const { mode, forceInline, ...props } = this.props; const { field, itemSetter } = props; - let columns: FieldConfig[] | undefined; - if ((itemSetter as SetterConfig)?.componentName === 'ObjectSetter') { - const items: FieldConfig[] = (itemSetter as any).props?.config?.items; + let columns: IPublicTypeFieldConfig[] | undefined; + if ((itemSetter as IPublicTypeSetterConfig)?.componentName === 'ObjectSetter') { + const items: IPublicTypeFieldConfig[] = (itemSetter as any).props?.config?.items; if (items && Array.isArray(items)) { columns = items.filter( (item) => item.isRequired || item.important || (item.setter as any)?.isRequired, - ); - if (columns.length > 4) { - columns = columns.slice(0, 4); - } + )?.slice(0, 4); } } @@ -314,7 +310,7 @@ export default class ArraySetter extends Component<{ </Button> ); } else { - return <ListSetter {...props} columns={columns?.slice(0, 4)} />; + return <ListSetter {...props} columns={columns} />; } } } diff --git a/src/setter/array-setter/style.less b/src/setter/array-setter/style.less index a9f4eb4..2f86847 100644 --- a/src/setter/array-setter/style.less +++ b/src/setter/array-setter/style.less @@ -102,6 +102,13 @@ display: block; width: 100%; } + .lc-setter-mixed { + width: 100%; + + .lc-setter-actions, .lc-action-slot { + display: none; + } + } } .lc-listitem-handler { margin-left: 0; diff --git a/src/setter/behavior-setter/actions/dialog-action.tsx b/src/setter/behavior-setter/actions/dialog-action.tsx new file mode 100644 index 0000000..b807164 --- /dev/null +++ b/src/setter/behavior-setter/actions/dialog-action.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { SettingTarget } from '@alilc/lowcode-types'; +import { Select, Box } from '@alifd/next'; +import { BehaviorActionProps, BehaviorAction } from '../types'; + +interface DialogActionValue { + id?: string; +} + +const getDialogList = (field: SettingTarget) => { + const node = (field as any).getNode(); + const nodeDocument = node.document || node.page; + let nodeList = nodeDocument?.modalNodesManager?.getModalNodes?.(); + if (!nodeList || !nodeList.length) { + nodeDocument?.modalNodesManager?.setNodes?.(); + nodeList = nodeDocument?.modalNodesManager?.getModalNodes?.(); + } + + return (nodeList || []) + .filter((x: any) => x && x.propsData) + .map((x: any) => ({ + label: + x.propsData && x.propsData.title + ? `${x.propsData.title}(${x.id})` + : `${x.id}(${x.componentName})`, + value: x.propsData.ref, + })); +}; + +const DialogContent: React.FC<BehaviorActionProps> = ({ value = {}, onChange, field }) => { + const [dialogList, setDialogList] = useState([]); + useEffect(() => { + setDialogList(getDialogList(field)); + }, [field]); + return ( + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>绑定弹窗</Box> + <Box className="behavior-radio"> + <Select + size="small" + hasClear + value={value.id} + onChange={(val) => onChange({ id: val })} + dataSource={dialogList} + style={{ width: '100%', marginRight: 8 }} + /> + </Box> + </Box> + ); +}; + + +export const dialogBehaviorAction: BehaviorAction<DialogActionValue> = { + name: 'dialog', + title: '弹窗', + render: (props) => <DialogContent {...props} />, + toActionValue: (value) => (value.id ? { + type: 'JSExpression', + value: `function() { + const dialog = this.$('${value.id}'); + dialog.show(); + }`, + } : null), +}; diff --git a/src/setter/behavior-setter/actions/link-action.tsx b/src/setter/behavior-setter/actions/link-action.tsx new file mode 100644 index 0000000..4afe697 --- /dev/null +++ b/src/setter/behavior-setter/actions/link-action.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { Radio, Select, Box, Input } from '@alifd/next'; +import { BehaviorActionProps, BehaviorAction } from '../types'; + +const fetchLinkList = async (url: string) => (url ? (await fetch(url)).json() : []); + +interface LinkActionValue { + type?: 'internal' | 'external'; + target?: string; + url?: string; +} +interface LinkActionOptions { + url?: string; + responseFormatter: (dataSource: any[]) => any[]; +} + +function fillDefaultValue(value: LinkActionValue) { + if (typeof value !== 'object') { + console.warn('value passed to fillDefaultValue should be an object'); + return; + } + if (!value.target) { + value.target = '_self'; + } + if (!value.type) { + value.type = 'internal'; + } +} + +const LinkContent: React.FC<BehaviorActionProps<LinkActionValue, LinkActionOptions>> = ({ + value = {}, onChange, options = {}, +}) => { + fillDefaultValue(value); + const { url, responseFormatter } = options; + const formatter = responseFormatter; + const [data, setData] = React.useState([]); + React.useEffect(() => { + let ignore = false; + + fetchLinkList(url).then((result) => { + if (!ignore) { + setData(formatter ? formatter(result) : result); + } + }); + return () => { + ignore = true; + }; + }, [url, formatter]); + + return ( + <Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>跳转方式</Box> + <Box className="behavior-radio"> + <Radio.Group + size="small" + dataSource={[ + { label: '当前窗口打开', value: '_self' }, + { label: '新窗口打开', value: '_blank' }, + ]} + defaultValue="_self" + shape="button" + value={value.target || '_self'} + onChange={(target: any) => { + onChange({ + ...value, + target, + }); + }} + /> + </Box> + </Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>跳转类型</Box> + <Box className="behavior-radio"> + <Radio.Group + size="small" + dataSource={[ + { label: '内部页面', value: 'internal' }, + { label: '外部链接', value: 'external' }, + ]} + defaultValue="internal" + shape="button" + value={value.type || 'internal'} + onChange={(type: any) => { + onChange({ + ...value, + type, + }); + }} + /> + </Box> + </Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>跳转页面</Box> + <Box className="behavior-radio"> + {value?.type === 'external' ? ( + <Input + size="small" + hasClear + placeholder="请输入链接" + value={value.url} + onChange={(val) => { + onChange({ + type: value.type || 'internal', + url: val, + }); + }} + /> + ) : ( + <Select + size="small" + hasClear + showSearch + dataSource={data} + value={value?.url} + onChange={(val) => { + onChange({ + ...value, + url: val, + }); + }} + /> + )} + </Box> + </Box> + </Box> + ); +}; + +export const linkBehaviorAction: BehaviorAction<LinkActionValue, LinkActionOptions> = { + name: 'link', + title: '链接', + render: (props) => <LinkContent {...props} />, + toActionValue: (link) => (link.url ? { + type: 'JSExpression', + value: `function() {window.open('${link.url}', '${link.target}');}`, + } : null), +}; diff --git a/src/setter/behavior-setter/actions/message-action.tsx b/src/setter/behavior-setter/actions/message-action.tsx new file mode 100644 index 0000000..e55d40a --- /dev/null +++ b/src/setter/behavior-setter/actions/message-action.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { Select, Box, Input } from '@alifd/next'; +import { BehaviorActionProps, BehaviorAction } from '../types'; + +interface MessageActionValue { + type?: string; + content?: string; +} +interface MessageActionOptions { + defaultType?: string; + types: string[]; + library: string; + component: string; +} + +function fillDefaultValue(value: MessageActionValue) { + if (typeof value !== 'object') { + console.warn('value passed to fillDefaultValue should be an object'); + return; + } + if (!value.type) { + value.type = 'notice'; + } +} + +const MessageActionContent: React.FC<BehaviorActionProps<MessageActionValue, MessageActionOptions>> = ({ + value = {}, onChange, options, +}) => { + fillDefaultValue(value); + const { types } = options; + return ( + <Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>通知类型</Box> + <Box className="behavior-radio"> + <Select + size="small" + dataSource={types} + defaultValue="notice" + value={value?.type || 'notice'} + onChange={(val) => { + onChange({ + ...value, + type: val, + }); + }} + /> + </Box> + </Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>通知内容</Box> + <Box className="behavior-radio"> + <Input.TextArea + size="small" + placeholder="请输入内容" + value={value.content} + onChange={(content) => { + onChange({ + ...value, + content, + }); + }} + /> + </Box> + </Box> + </Box> + ); +}; + +export const messageBehaviorAction: BehaviorAction<MessageActionValue, MessageActionOptions> = { + name: 'message', + title: '提示', + render: (props) => <MessageActionContent {...props} />, + toActionValue: (value, options) => { + const { library, component, defaultType } = options; + return { + type: 'JSExpression', + value: `function() {${library}.${component}.${value.type || defaultType}('${value.content}')}`, + }; + }, +}; diff --git a/src/setter/behavior-setter/actions/tooltip-action.tsx b/src/setter/behavior-setter/actions/tooltip-action.tsx new file mode 100644 index 0000000..9d06a9d --- /dev/null +++ b/src/setter/behavior-setter/actions/tooltip-action.tsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { Box, Input, Radio } from '@alifd/next'; +import { BehaviorActionProps, BehaviorAction } from '../types'; + +const findReact = (id: string, traverseUp = 0) => { + const dom = document.querySelector(`#${id}`); + if (!dom) return; + const key = Object.keys(dom).find((item) => { + return ( + item.startsWith('__reactFiber$') || // react 17+ + item.startsWith('__reactInternalInstance$') + ); // react <17 + }); + const domFiber = dom[key]; + if (domFiber == null) return null; + + // react <16 + if (domFiber._currentElement) { + let compFiber = domFiber._currentElement._owner; + for (let i = 0; i < traverseUp; i++) { + compFiber = compFiber._currentElement._owner; + } + return compFiber._instance; + } + + // react 16+ + const GetCompFiber = (fiber) => { + // return fiber._debugOwner; // this also works, but is __DEV__ only + let parentFiber = fiber.return; + while (typeof parentFiber.type === 'string') { + parentFiber = parentFiber.return; + } + return parentFiber; + }; + let compFiber = GetCompFiber(domFiber); + for (let i = 0; i < traverseUp; i++) { + compFiber = GetCompFiber(compFiber); + } + return compFiber.stateNode; +} + +interface TooltipActionValue { + content?: string; + triggerType?: 'click' | 'hover'; +} +interface TooltipActionOptions { + id: string; +} + +const TooltipActionContent: React.FC<BehaviorActionProps<TooltipActionValue, TooltipActionOptions>> = ({ + value = {}, onChange, +}) => { + + return ( + <Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>触发方式</Box> + <Box className="behavior-radio"> + <Radio.Group + size="small" + dataSource={[ + 'click', + 'hover', + ]} + defaultValue="click" + shape="button" + value={value.triggerType || 'click'} + onChange={(triggerType: any) => { + onChange({ + ...value, + triggerType, + }); + }} + /> + </Box> + </Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>通知内容</Box> + <Box direction="row" className="behavior-radio" spacing={10}> + <Input.TextArea + size="small" + placeholder="请输入内容" + value={value.content} + onChange={(content) => { + onChange({ + ...value, + content, + }); + }} + /> + </Box> + </Box> + </Box> + ); +}; + +export const tooltipBehaviorAction: BehaviorAction<TooltipActionValue, TooltipActionOptions> = { + name: 'tooltip', + title: 'Tooltip', + render: (props) => <TooltipActionContent {...props} />, + toActionValue: (value, options) => ({ + type: 'JSExpression', + value: `function() { + const id = '${options.id}'; + const findReact = ${findReact}; + const node = findReact(id); + if (!node) return; + if (typeof node.enableTooltip === 'function') { + node.enableTooltip('${value.triggerType}', '${value.content}'); + } else if (typeof node.toggleTip === 'function') { + node.toggleTip('${value.content}'); + } + }`, + }), +}; diff --git a/src/setter/behavior-setter/index.scss b/src/setter/behavior-setter/index.scss new file mode 100644 index 0000000..aa52148 --- /dev/null +++ b/src/setter/behavior-setter/index.scss @@ -0,0 +1,92 @@ +.behavior-item { + padding: 5px 0; + .behavior-radio { + flex: 1; + .next-input { + width: 100%; + } + .next-radio-group { + white-space: nowrap; + display: flex; + flex-wrap: wrap; + flex: 1; + label { + flex: 1; + span { + text-align: center; + } + } + } + } +} + +:root { + --min-input-light-height: max(var(--form-element-small-height), 28px); +} + +.next-btn-ghost.next-btn-light { + background: #fff; + border-color: transparent; + border-color: var(--btn-ghost-light-border-color, transparent); +} + +.next-btn-text.next-medium { + border-radius: 0; + padding: 0; + height: var(--btn-text-size-m-height, 20px); + font-size: var(--btn-text-size-m-font, 14px); + border-width: 0; +} + +.next-btn-ghost.next-btn-light.active, .next-btn-ghost.next-btn-light.hover, .next-btn-ghost.next-btn-light:active, .next-btn-ghost.next-btn-light:focus, .next-btn-ghost.next-btn-light:hover { + color: #000; + color: var(--btn-ghost-light-color-hover,#000); + background: hsla(0,0%,96%,.92); + border-color: transparent; + border-color: var(--btn-ghost-light-border-color-hover,transparent); + text-decoration: none; +} + +.next-btn.next-small { + border-radius: var(--btn-size-s-corner,3px); + padding: 0 var(--btn-size-s-padding,4px); + height: var(--btn-size-s-height,28px); + font-size: var(--btn-size-s-font,12px); + border-width: var(--btn-size-s-border-width,1px); +} + +.next-input.next-small { + height: var(--min-input-light-height, 28px); + min-height: 28px; + border-radius: var(--form-element-small-corner, 3px); +} + +.next-input.next-small input { + height: calc(var(--min-input-light-height, 28px) - var(--input-border-width, 1px) * 2); + min-height: 26px; + line-height: calc(var(--min-input-light-height, 28px) - var(--input-border-width, 1px) * 2); + padding: 0 var(--input-s-padding, 8px); + font-size: var(--form-element-small-font-size, 12px); +} + +.next-input.next-small .next-input-text-field { + padding: 0 4px; + font-size: 12px; + height: calc(var(--min-input-light-height, 28px) - var(--input-border-width, 1px) * 2); + min-height: 26px; + line-height: calc(var(--min-input-light-height, 28px) - var(--input-border-width, 1px) * 2); +} + +.next-radio-button-small>label { + padding: 0 var(--radio-button-padding-small,8px); + height: var(--radio-button-height-small, 28px); + min-height: 28px; + line-height: var(--radio-button-height-small, 28px); +} + +.next-radio-button-small .next-radio-label { + height: calc(var(--radio-button-height-small, 28px) - 2px); + min-height: 26px; + line-height: calc(var(--radio-button-height-small, 28px) - 2px); + font-size: var(--radio-button-font-size-small,12px); +} diff --git a/src/setter/behavior-setter/index.tsx b/src/setter/behavior-setter/index.tsx new file mode 100644 index 0000000..bcc09db --- /dev/null +++ b/src/setter/behavior-setter/index.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { useState, ErrorInfo, useMemo } from 'react'; +import { Radio, Select, Box } from '@alifd/next'; +import { SettingTarget, CustomView } from '@alilc/lowcode-types'; + +import './index.scss'; +import { BehaviorAction } from './types'; +import { linkBehaviorAction } from './actions/link-action'; +import { dialogBehaviorAction } from './actions/dialog-action'; +import { messageBehaviorAction } from './actions/message-action'; +import { tooltipBehaviorAction } from './actions/tooltip-action'; + +interface BehaviorSetterProps { + field?: SettingTarget; + // 兼容 vision engine + prop?: SettingTarget; + onChange: Function; + actions: string[]; + type?: 'dialog' | 'link' | undefined; + value: Record<string, any>; + url?: 'string'; + responseFormatter?: Function; + extraBehaviorActions?: BehaviorAction[]; + extendedOptions?: Record<string, any>; + enableMessageAction?: boolean; + enableTooltipAction?: boolean; +} + +const defaultActionMap = { + dialog: dialogBehaviorAction, + link: linkBehaviorAction, + tooltip: tooltipBehaviorAction, + message: messageBehaviorAction, +}; +const BehaviorSetter: CustomView = ({ + value: behaviors = {}, + onChange, + field: propsField, + prop, + actions, + type: propsType, + extraBehaviorActions: propsBehaviorActions = [], + extendedOptions = {}, + url, + responseFormatter, + enableMessageAction, + enableTooltipAction, +}: BehaviorSetterProps) => { + const field = propsField || prop; + + const [currentEvent, setCurrentEvent] = useState(actions[0]); + /** 当前的行为集合 */ + + const currentBehavior = behaviors[currentEvent] || {}; + const currentBehaviorType = currentBehavior.type || propsType || 'dialog'; + const defaultActions: any = []; + defaultActions.push(defaultActionMap.dialog, defaultActionMap.link); + if (enableMessageAction) { + defaultActions.push(defaultActionMap.message); + } + if (enableTooltipAction) { + defaultActions.push(defaultActionMap.tooltip); + } + const behaviorActions = useMemo(() => [ + ...defaultActions, + ...propsBehaviorActions, + ], [propsBehaviorActions]); + + const updateCurrentEventBehavior = (type: string, newVal: any) => { + onChange({ + ...behaviors, + [currentEvent]: { + type, + [type]: newVal, + }, + }); + }; + const behaviorOption = behaviorActions.find((vo) => vo.name === currentBehaviorType); + + return ( + <div> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>事件列表</Box> + <Box className="behavior-radio"> + <Select + size="small" + dataSource={actions.map((action) => ({ + label: action, + value: action, + }))} + value={currentEvent} + onChange={(eventName) => setCurrentEvent(eventName)} + /> + </Box> + </Box> + <Box direction="row" align="center" className="behavior-item"> + <Box style={{ width: 70 }}>操作类型</Box> + <Box className="behavior-radio"> + <Radio.Group + size="small" + dataSource={behaviorActions.map((vo) => ({ + value: vo.name, + label: vo.title, + disabled: propsType && vo.name !== propsType, + }))} + value={currentBehaviorType} + shape="button" + onChange={(behaviorType: string) => updateCurrentEventBehavior(behaviorType, undefined)} + /> + </Box> + </Box> + <ErrorBoundary> + {behaviorOption && behaviorOption.render({ + field, + value: currentBehavior[currentBehaviorType], + onChange: (behaviorValue: Record<string, any>) => { + field.parent.setPropValue(currentEvent, behaviorOption.toActionValue(behaviorValue, extendedOptions[currentBehaviorType])); + updateCurrentEventBehavior(currentBehaviorType, behaviorValue); + }, + options: currentBehaviorType === 'link' ? { + ...extendedOptions[currentBehaviorType], + url, + responseFormatter, + } : extendedOptions[currentBehaviorType], + })} + </ErrorBoundary> + </div> + ); +}; + +export default BehaviorSetter; + + +// eslint-disable-next-line @iceworks/best-practices/recommend-functional-component +class ErrorBoundary extends React.Component { + // 更新 state 使下一次渲染能够显示降级后的 UI + static getDerivedStateFromError = (error: any) => ({ hasError: true, error }); + + state = { hasError: false, error: undefined as Error }; + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // 你同样可以将错误日志上报给服务器 + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // 你可以自定义降级后的 UI 并渲染 + return <div>渲染异常: {this.state.error?.message || this.state.error || ''}</div>; + } + + return this.props.children; + } +} diff --git a/src/setter/behavior-setter/types.ts b/src/setter/behavior-setter/types.ts new file mode 100644 index 0000000..d71faae --- /dev/null +++ b/src/setter/behavior-setter/types.ts @@ -0,0 +1,19 @@ +import { SettingTarget } from '@alilc/lowcode-types'; + +export interface BehaviorActionProps<Value = Record<string, any>, Options = any> { + value: Value; + onChange: (val: Value) => void; + options?: Options; + field: SettingTarget; +} +/** 自定义行为描述 */ +export interface BehaviorAction<Value = Record<string, any>, Options = Record<string, any>> { + /** 行为key */ + name: string; + /** 行为名字,显示在RadioGroup选项 */ + title: string; + /** 选项渲染 */ + render?: (props: BehaviorActionProps<Value, Options>) => React.ReactNode; + /** 值序列化为低代码协议的值 */ + toActionValue: (value: Value, options?: Options) => any; +} diff --git a/src/setter/classname-setter/index.less b/src/setter/classname-setter/index.less new file mode 100644 index 0000000..5e47a4f --- /dev/null +++ b/src/setter/classname-setter/index.less @@ -0,0 +1,23 @@ +.ClassNameSetter_Select { + display: flex; + align-items: center; +} + +.ClassNameSetter_Select .next-select-inner { + padding: 2px 0; +} + +.ClassNameSetter_Select .next-select-inner .next-input-text-field { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: 0; +} + +.ClassNameSetter_Select .next-select-inner .next-tag { + margin-bottom: 1px; +} + +.ClassNameSetter_Select .next-select-trigger-search { + height: 0; +} diff --git a/src/setter/classname-setter/index.tsx b/src/setter/classname-setter/index.tsx index 53d60b0..486b8b7 100644 --- a/src/setter/classname-setter/index.tsx +++ b/src/setter/classname-setter/index.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { Select } from '@alifd/next'; import { project } from '@alilc/lowcode-engine'; +import './index.less'; export interface PluginProps { value: string; @@ -11,55 +11,25 @@ export interface PluginProps { export default class ClassNameView extends PureComponent<PluginProps> { static display = 'ClassName'; - static propTypes = { - onChange: PropTypes.func, - value: PropTypes.string, - }; + static defaultProps = {}; - static defaultProps = { - onChange: () => {}, - value: '', + state = { + dataSource: [], + selectValue: '', }; getClassNameList = () => { - const schema = project.exportSchema(); - const css = schema.componentsTree[0].css; - const classNameList = []; - if (css) { - const re = /\.?\w+[^{]+\{[^}]*\}/g; - const list = css.match(re); - list.map((item) => { - if (item[0] === '.') { - let className = item.substring(1, item.indexOf('{')); - if (className.indexOf(':') >= 0) { - className = item.substring(1, item.indexOf(':')); - } - // 移除左右两边空格 - className = className.replace(/^\s*|\s*$/g, ''); - classNameList.push(className); - } - - return item; - }); - } + const schema = ClassNameView?.defaultProps?.getSchema?.() || project.exportSchema(); - return classNameList; + const {css} = schema.componentsTree[0]; + return css?.match(/(?<=((?<!([\d\w_-]+))\.))[\w-_\d]+(?=\s*([{,])?)/g) || []; }; - handleChange = (value) => { - const { onChange } = this.props; - onChange(value.join(' ')); - this.setState({ - selectValue: value, - }); - }; - - // eslint-disable-next-line react/no-deprecated - componentWillMount() { + setClassNameSetterData = () => { const { value } = this.props; - const classnameList = this.getClassNameList(); - const dataSource = []; - classnameList.map((item) => { + const classNameList = this.getClassNameList(); + const dataSource: Array<{ label: string; value : string}> = []; + classNameList.map((item: string) => { dataSource.push({ value: item, label: item, @@ -68,25 +38,44 @@ export default class ClassNameView extends PureComponent<PluginProps> { return item; }); - let selectValue = []; - if (value && value !== '') { - selectValue = value.split(' '); - } + const selectValue = value?.split?.(' ') || []; + selectValue.forEach(current => { + if(!classNameList.some((cls: string) => cls === current)) { + dataSource.push({ + value: current, + label: current, + }); + } + }) this.setState({ dataSource, selectValue, }); + }; + + handleChange = (value: string[]) => { + const { onChange } = this.props; + onChange(value.join(' ')); + this.setState({ + selectValue: value, + }); + }; + + componentWillMount() { + this.setClassNameSetterData(); } render() { const { dataSource, selectValue } = this.state; return ( <Select - size="small" - aria-label="tag mode" - mode="tag" + inputMode='' + className='ClassNameSetter_Select' + style={{ width: '100%' }} + mode="multiple" dataSource={dataSource} + onFocus={this.setClassNameSetterData} onChange={this.handleChange} value={selectValue} /> diff --git a/src/setter/events-setter/index.tsx b/src/setter/events-setter/index.tsx index c03fed6..4eac0f0 100644 --- a/src/setter/events-setter/index.tsx +++ b/src/setter/events-setter/index.tsx @@ -319,6 +319,10 @@ export default class EventsSetter extends Component<{ }; onRelatedEventNameClick = (eventName: string) => { + // props 事件,不需要跳转 + if (/(this\.)?props\./.test(eventName)) { + return; + } skeleton.showPanel('codeEditor'); setTimeout(() => { event.emit('codeEditor.focusByFunction', { @@ -421,7 +425,7 @@ export default class EventsSetter extends Component<{ this.state; const showEventList = lifeCycleEventList.length > 0 ? lifeCycleEventList : eventList; - console.log('eventDataList', eventDataList); + // console.log('eventDataList', eventDataList); return ( <div className="lc-block-setter event-body" onClick={this.closeEventMenu}> diff --git a/src/setter/expression-setter/index.tsx b/src/setter/expression-setter/index.tsx index 5a5c8c3..d655df3 100644 --- a/src/setter/expression-setter/index.tsx +++ b/src/setter/expression-setter/index.tsx @@ -79,16 +79,16 @@ export default class ExpressionView extends PureComponent { }; } - static getDerivedStateFromProps(props: { value: any }, state: { preValue: any }) { - const curValue = ExpressionView.getInitValue(props.value); - if (curValue !== state.preValue) { - return { - preValue: curValue, - value: curValue, - }; - } - return null; - } + // static getDerivedStateFromProps(props: { value: any }, state: { preValue: any }) { + // const curValue = ExpressionView.getInitValue(props.value); + // if (curValue !== state.preValue) { + // return { + // preValue: curValue, + // value: curValue, + // }; + // } + // return null; + // } onChange(value: string) { const realInputValue = value; @@ -132,10 +132,19 @@ export default class ExpressionView extends PureComponent { const stateMap = schema.componentsTree[0].state; const dataSource = []; + const datasourceMap = schema.componentsTree[0]?.dataSource; + const list = datasourceMap?.list || []; + for (const key in stateMap) { dataSource.push(`this.state.${key}`); } + for (const item of list) { + if (item && item.id) { + dataSource.push(`this.state.${item.id}`); + } + } + return dataSource; } @@ -166,7 +175,6 @@ export default class ExpressionView extends PureComponent { */ getContextKeys(keys: []) { const { editor } = this.props.field; - console.log(editor); const limitKeys = ['schema', 'utils', 'constants']; if (keys.length === 0) return limitKeys; if (!limitKeys.includes(keys[0])) return []; @@ -217,6 +225,7 @@ export default class ExpressionView extends PureComponent { */ filterOption(inputValue: string, item: { value: string | any[] }) { const cursorIndex = this.getInputCursorPosition(); + if (typeof(inputValue)!='string') return false; const preStr = inputValue.substr(0, cursorIndex); const lastKey: string[] = preStr.split('.').slice(-1); if (!lastKey) return true; diff --git a/src/setter/function-setter/index.tsx b/src/setter/function-setter/index.tsx index 75596d6..be636f6 100644 --- a/src/setter/function-setter/index.tsx +++ b/src/setter/function-setter/index.tsx @@ -8,7 +8,8 @@ import './index.less'; const SETTER_NAME = 'function-setter'; const defaultEditorOption = { - style: { width: '100%', height: '95%' }, + width:'100%', + height:'95%', options: { readOnly: false, automaticLayout: true, diff --git a/src/setter/i18n-setter/index.tsx b/src/setter/i18n-setter/index.tsx index c3c9c6d..6f9cecd 100644 --- a/src/setter/i18n-setter/index.tsx +++ b/src/setter/i18n-setter/index.tsx @@ -65,7 +65,7 @@ class I18nSetter extends Component { }; onSearch(value, filterValue) { - console.log('onSearch', value, filterValue); + // console.log('onSearch', value, filterValue); } showSearchPopUp = () => { diff --git a/src/setter/icon-setter/index.tsx b/src/setter/icon-setter/index.tsx index ee0976b..9cfd3d1 100644 --- a/src/setter/icon-setter/index.tsx +++ b/src/setter/icon-setter/index.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; import { Input, Icon, Balloon } from '@alifd/next'; import './index.less'; +import { intl } from './locale'; const icons = [ 'smile', @@ -82,7 +83,7 @@ export default class IconSetter extends PureComponent<IconSetterProps, IconSette defaultValue: '', hasClear: true, icons, - placeholder: '请点击选择 Icon', + placeholder: `${intl('PleaseClickToSelectIcon')}`, onChange: () => undefined, }; static displayName = 'IconSetter'; diff --git a/src/setter/icon-setter/locale/en-US.json b/src/setter/icon-setter/locale/en-US.json new file mode 100644 index 0000000..7b56098 --- /dev/null +++ b/src/setter/icon-setter/locale/en-US.json @@ -0,0 +1,3 @@ +{ + "PleaseClickToSelectIcon": "Please click to select Icon" +} diff --git a/src/setter/icon-setter/locale/index.ts b/src/setter/icon-setter/locale/index.ts new file mode 100644 index 0000000..42dc9f1 --- /dev/null +++ b/src/setter/icon-setter/locale/index.ts @@ -0,0 +1,14 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const { intl } = common?.utils?.createIntl?.({ + 'en-US': enUS, + 'zh-CN': zhCN, +}) || { + intl: (id) => { + return zhCN[id]; + } +}; + +export { intl }; diff --git a/src/setter/icon-setter/locale/zh-CN.json b/src/setter/icon-setter/locale/zh-CN.json new file mode 100644 index 0000000..f5d9e96 --- /dev/null +++ b/src/setter/icon-setter/locale/zh-CN.json @@ -0,0 +1,3 @@ +{ + "PleaseClickToSelectIcon": "请点击选择 Icon" +} diff --git a/src/setter/json-setter/index.tsx b/src/setter/json-setter/index.tsx index 2c648ce..87b5b5d 100644 --- a/src/setter/json-setter/index.tsx +++ b/src/setter/json-setter/index.tsx @@ -4,10 +4,11 @@ import { Button, Icon, Dialog } from '@alifd/next'; import MonacoEditor from '@alilc/lowcode-plugin-base-monaco-editor'; import CustomIcon from '../../components/custom-icon'; import { js_beautify } from 'js-beautify'; +import { intl } from './locale'; const defaultEditorOption = { width: '100%', - height: 400, + height: '400px', readOnly: false, automaticLayout: true, folding: true, // 默认开启折叠代码功能 @@ -58,7 +59,7 @@ export default class JsonSetter extends PureComponent<JsonSetterProps> { }; componentWillReceiveProps(nextProps) { - let nextValue = JSON.stringify(nextProps.value); + const nextValue = JSON.stringify(nextProps.value); if (nextValue !== this.state.value) { this.setState({ value: nextValue, @@ -78,12 +79,12 @@ export default class JsonSetter extends PureComponent<JsonSetterProps> { renderButton = (value) => { return !value ? ( <Button size="small" type="normal" onClick={this.openDialog}> - 绑定数据 + { intl('BindingData') } </Button> ) : ( <Button size="small" type="primary" onClick={this.openDialog}> <Icon type="edit" /> - 编辑数据 + { intl('EditData') } </Button> ); }; @@ -100,12 +101,13 @@ export default class JsonSetter extends PureComponent<JsonSetterProps> { this.closeDialog(); } catch (e) { Dialog.alert({ - title: '数据保存失败', + title: intl('FailedToSaveData'), content: e.message, }); } } else { - removeProp(); + onChange(undefined); + // removeProp(); this.closeDialog(); } }; @@ -118,7 +120,7 @@ export default class JsonSetter extends PureComponent<JsonSetterProps> { <div> <Button size="small" type="primary" onClick={this.openDialog}> <CustomIcon type="icon-ic_edit" /> - 编辑数据 + { intl('EditData') } </Button> </div> ); @@ -134,12 +136,18 @@ export default class JsonSetter extends PureComponent<JsonSetterProps> { <Dialog visible={isShowDialog} closeable={'close'} - title="数据编辑" + title={intl('EditData')} onCancel={this.closeDialog} onOk={this.onDialogOk} onClose={() => { this.closeDialog(); }} + cancelProps={{ + children: intl('Cancel') + }} + okProps={{ + children: intl('Confirm') + }} > <div style={{ width: '500px', height: '400px' }}> <MonacoEditor diff --git a/src/setter/json-setter/locale/en-US.json b/src/setter/json-setter/locale/en-US.json new file mode 100644 index 0000000..3f71baa --- /dev/null +++ b/src/setter/json-setter/locale/en-US.json @@ -0,0 +1,7 @@ +{ + "BindingData": "Binding Data", + "EditData": "Edit Data", + "FailedToSaveData": "Failed to save data", + "Cancel": "Cancel", + "Confirm": "Confirm" +} diff --git a/src/setter/json-setter/locale/index.ts b/src/setter/json-setter/locale/index.ts new file mode 100644 index 0000000..42dc9f1 --- /dev/null +++ b/src/setter/json-setter/locale/index.ts @@ -0,0 +1,14 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const { intl } = common?.utils?.createIntl?.({ + 'en-US': enUS, + 'zh-CN': zhCN, +}) || { + intl: (id) => { + return zhCN[id]; + } +}; + +export { intl }; diff --git a/src/setter/json-setter/locale/snippets.ts b/src/setter/json-setter/locale/snippets.ts deleted file mode 100644 index 2bcb0dd..0000000 --- a/src/setter/json-setter/locale/snippets.ts +++ /dev/null @@ -1,242 +0,0 @@ -export default [ - { - label: 'constants', - kind: 'Class', - insertText: 'constants', - detail: '应用全局常量', - documentation: '应用范围定义的通用常量', - }, - { - label: 'utils', - kind: 'Class', - insertText: 'utils', - detail: '应用全局公共函数', - documentation: '应用范围扩展的公共函数', - }, - { - label: 'state', - kind: 'Enum', - insertText: 'state', - detail: '当前所在容器组件内部状态', - documentation: 'React Class内部状态state', - }, - { - label: 'setState', - kind: 'Function', - insertText: 'setState({\n\t$0\n})', - insertTextRules: 'InsertAsSnippet', - detail: '设置当前所在容器组件的state数据', - documentation: '原生React方法,会自动更新组件视图', - }, - { - label: 'reloadDataSource', - kind: 'Function', - insertText: 'reloadDataSource(${1:${2:namespace}, ${3:false}, ${4:callback}})', - insertTextRules: 'InsertAsSnippet', - detail: '刷新当前所在的容器组件', - documentation: '触发当前所在的容器组件,重新发送异步请求,并用最新数据更新视图', - }, - { - label: 'location', - kind: 'Class', - insertText: 'location', - detail: '路由解析对象', - }, - { - label: 'location.query', - kind: 'Value', - insertText: 'location.query.${1:xxxx}', - insertTextRules: 'InsertAsSnippet', - detail: '从路由解析对象中获取参数信息', - }, - { - label: 'history', - kind: 'Class', - insertText: 'history', - detail: '路由历史对象', - }, - { - label: 'React', - kind: 'Keyword', - insertText: 'React', - detail: 'React对象', - }, - { - label: 'ReactDOM', - kind: 'Keyword', - insertText: 'ReactDOM', - detail: 'ReactDom对象', - }, - { - label: 'ReactDOM.findDOMNode', - kind: 'Function', - insertText: 'ReactDOM.findDOMNode(${1:this.refs.xxxx})', - insertTextRules: 'InsertAsSnippet', - detail: 'ReactDom查找真实dom node', - }, - { - label: 'Dialog.alert', - kind: 'Method', - insertText: [ - 'Dialog.alert({', - "\tcontent: '${1:Alert content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: 'alert弹框 By Fusion', - }, - { - label: 'Dialog.confirm', - kind: 'Method', - insertText: [ - 'Dialog.confirm({', - "\tcontent: '${1:Confirm content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '确认弹出框 By Fusion', - }, - { - label: 'Message.success', - kind: 'Method', - insertText: 'Message.success(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: '成功反馈提示 By Fusion', - }, - { - label: 'Message.error', - kind: 'Method', - insertText: 'Message.error(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: '错误反馈提示 By Fusion', - }, - { - label: 'Message.help', - kind: 'Method', - insertText: 'Message.help(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: '帮助反馈提示 By Fusion', - }, - { - label: 'Message.loading', - kind: 'Method', - insertText: 'Message.loading(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: 'loading反馈提示 By Fusion', - }, - { - label: 'Message.notice', - kind: 'Method', - insertText: 'Message.notice(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: '注意反馈提示 By Fusion', - }, - { - label: 'Message.waining', - kind: 'Method', - insertText: 'Message.waining(${1:content})', - insertTextRules: 'InsertAsSnippet', - detail: '警告反馈提示 By Fusion', - }, - { - label: 'Modal.confirm', - kind: 'Method', - insertText: [ - 'Modal.confirm({', - "\tcontent: '${1:Confirm content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '确认弹出框 By Antd', - }, - { - label: 'Modal.info', - kind: 'Method', - insertText: [ - 'Modal.info({', - "\tcontent: '${1:Info content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '信息弹出框 By Antd', - }, - { - label: 'Modal.success', - kind: 'Method', - insertText: [ - 'Modal.success({', - "\tcontent: '${1:Success content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '成功弹出框 By Antd', - }, - { - label: 'Modal.error', - kind: 'Method', - insertText: [ - 'Modal.error({', - "\tcontent: '${1:Error content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '错误弹出框 By Antd', - }, - { - label: 'Modal.warning', - kind: 'Method', - insertText: [ - 'Modal.warning({', - "\tcontent: '${1:Warning content}',", - "\ttitle: '${2:Title}',", - '\tonOk: () => {', - '\t\t$3', - '\t},', - '\tonCancel: () => {', - '\t\t$4', - '\t}', - '})', - ].join('\n'), - insertTextRules: 'InsertAsSnippet', - detail: '警告弹出框 By Antd', - }, -]; diff --git a/src/setter/json-setter/locale/utils.ts b/src/setter/json-setter/locale/utils.ts deleted file mode 100644 index d401ad5..0000000 --- a/src/setter/json-setter/locale/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import IntlMessageFormat from 'intl-messageformat'; - -export const isJSExpression = (obj = '') => { - if (obj && typeof obj === 'object' && obj.type === 'JSExpression') { - return true; - } - return false; -}; - -/** - * 用于构造国际化字符串处理函数 - * @param {*} locale 国际化标识,例如 zh-CN、en-US - * @param {*} messages 国际化语言包 - */ -export const generateI18n = (locale = 'zh-CN', messages = {}) => { - return function (key, values = {}) { - if (!messages || !messages[key]) return ''; - const formater = new IntlMessageFormat(messages[key], locale); - return formater.format(values); - }; -}; diff --git a/src/setter/json-setter/locale/zh-CN.json b/src/setter/json-setter/locale/zh-CN.json new file mode 100644 index 0000000..215bf44 --- /dev/null +++ b/src/setter/json-setter/locale/zh-CN.json @@ -0,0 +1,7 @@ +{ + "BindingData": "绑定数据", + "EditData": "编辑数据", + "FailedToSaveData": "数据保存失败", + "Cancel": "取消", + "Confirm": "确定" +} diff --git a/src/setter/json-setter/locale/zh-CN.ts b/src/setter/json-setter/locale/zh-CN.ts deleted file mode 100644 index eacc07c..0000000 --- a/src/setter/json-setter/locale/zh-CN.ts +++ /dev/null @@ -1,37 +0,0 @@ -export default { - // function - setting: '点击设置', - edit: '编辑', - submitConfirm: '确认提交 cmd+s', - close: '关闭 esc', - fullScreen: '全屏', - cancelFullScreen: '取消全屏', - jsonIllegal: '非json格式', - functionIllegal: '非function格式', - objectIllegal: '非object格式', - circularRef: '对象中出现循环引用的对象', - formatError: '格式错误', - saved: '已保存', - // expression - valueIllegal: - '值类型为对象类型,与当前组件属性设置的控件类型不匹配,请在属性“代码编辑模式”下进行编辑', - jsExpression: '请输入JS表达式', - // Mixin - input: '字符串Input', - textarea: '多行字符串Textarea', - expression: '变量控件Expression', - monacoEditor: '编辑器MonacoEditor', - numberPicker: '数字NumberPicker', - bool: '布尔Switch', - datePicker: '日期选择DatePicker', - select: '下拉选择Select', - radio: '单项选择RadioGroup', - date: '日期选择DatePicker', - dateYear: '年选择DatePicker', - dateMonth: '月选择DatePicker', - dateRange: '日期区间选择DatePicker', - list: '数组List', - object: '对象ObjectButton', - reactNode: '节点类型ReactNode', - typeError: 'Minix组件属性Types配置错误,存在不支持类型[{type}],请检查组件属性配置', -}; diff --git a/src/setter/mixed-setter/config.ts b/src/setter/mixed-setter/config.ts new file mode 100644 index 0000000..8662fbd --- /dev/null +++ b/src/setter/mixed-setter/config.ts @@ -0,0 +1,16 @@ +import { ReactNode } from "react" +import { SettingField } from '@alilc/lowcode-engine'; + +class MixedSetterConfig { + config: { + renderSlot?: (props: {bindCode?: string, field?: SettingField}) => ReactNode + } = {} + + setConfig(config: { + renderSlot?: (props: {bindCode?: string, field?: SettingField}) => ReactNode + }) { + this.config = config; + } +} + +export const MixedSetterController = new MixedSetterConfig() \ No newline at end of file diff --git a/src/setter/mixed-setter/index.less b/src/setter/mixed-setter/index.less index 95cca5f..b4a1cae 100644 --- a/src/setter/mixed-setter/index.less +++ b/src/setter/mixed-setter/index.less @@ -1,9 +1,9 @@ .lc-setter-mixed { min-width: 0; - margin-right: 26px; + margin-right: 40px; display: block; position: relative; - width: 100%; + width: calc(100% - 20px); > .lc-setter-actions { position: absolute; right: -4px; @@ -33,6 +33,13 @@ } } } + >.lc-action-slot { + position: absolute; + display: flex; + right: -24px; + top: 50%; + transform: translate(100%, -50%); + } .next-input, .next-date-picker, .next-month-picker { @@ -57,6 +64,11 @@ height: 32px; transform: none; } + >.lc-action-slot { + right: 12px; + top: 44px; + transform: none; + } } .lc-block-field > .lc-field-body > .lc-setter-mixed { @@ -69,4 +81,4 @@ top: 4px; transform: none; } -} +} \ No newline at end of file diff --git a/src/setter/mixed-setter/index.tsx b/src/setter/mixed-setter/index.tsx index 32a72dd..2ceeeb3 100644 --- a/src/setter/mixed-setter/index.tsx +++ b/src/setter/mixed-setter/index.tsx @@ -1,4 +1,4 @@ -import React, { Component, ComponentClass, ReactNode } from 'react'; +import React, { Component, ComponentClass } from 'react'; import classNames from 'classnames'; import { Dropdown, Menu } from '@alifd/next'; import { common, setters, SettingField } from '@alilc/lowcode-engine'; @@ -13,6 +13,7 @@ import { } from '@alilc/lowcode-types'; import { IconConvert } from './icons/convert'; import { intlNode } from './locale'; +import { MixedSetterController } from './config' import './index.less'; import { IconVariable } from './icons/variable'; @@ -31,6 +32,35 @@ export interface SetterItem { valueType: string[]; } +const dash = '_'; +function getMixedSelect(field) { + const path = field.path || []; + if(path.length) { + const key = `_unsafe_MixedSetter${dash}${path[path.length-1]}${dash}select` + const newPath = [...path]; + newPath.splice(path.length - 1, 1, key); + const newKey = field.node.getPropValue(newPath.join('.')) + if(newKey) return newKey; + // 兼容下以前的问题情况,如果捕获到,获取 oldUnsafeKey 取值并将其直接置空 + const oldUnsafeKey = `_unsafe_MixedSetter${dash}${path.join(dash)}${dash}select`; + const oldUsedSetter = field.node.getPropValue(oldUnsafeKey); + if(oldUsedSetter) { + field.node.setPropValue(newPath.join('.'), oldUsedSetter); + field.node.setPropValue(oldUnsafeKey, undefined); + } + return oldUsedSetter; + } + return undefined; +} +function setMixedSelect(field, usedSetter) { + const path = field.path || []; + if(path.length) { + const key = `_unsafe_MixedSetter${dash}${path[path.length-1]}${dash}select` + path.splice(path.length - 1, 1, key); + field.node.setPropValue(path.join('.'), usedSetter) + } +} + function nomalizeSetters( setters?: Array<string | SetterConfig | CustomView | DynamicSetter>, ): SetterItem[] { @@ -102,10 +132,18 @@ function nomalizeSetters( } return config; }); - const hasComplexSetter = formattedSetters.filter((item) => - ['ArraySetter', 'ObjectSetter'].includes(item.setter), - ).length; - return formattedSetters.map((item) => { + const uniqSetters = formattedSetters.reduce((map, s) => { + map.set(s.name, s); + return map; + }, new Map<string, any>()); + + const hasComplexSetter = formattedSetters.filter((item) => { + // 变量绑定,非切换设置器 + if (item.props?.variableBind) return false; + + return ['ArraySetter', 'ObjectSetter'].includes(item.setter); + })?.length; + return [...uniqSetters.values()].map((item) => { if (item.setter === 'VariableSetter' && hasComplexSetter) { item.setter = 'ExpressionSetter'; item.name = 'ExpressionSetter'; @@ -119,7 +157,7 @@ interface VariableSetter extends ComponentClass { } @observer -export default class MixedSetter extends Component<{ +class MixedSetter extends Component<{ field: SettingField; setters?: Array<string | SetterConfig | CustomView | DynamicSetter>; onSetterChange?: (field: SettingField, name: string) => void; @@ -127,6 +165,8 @@ export default class MixedSetter extends Component<{ value?: any; className?: string; }> { + private fromMixedSetterSelect = false; + private setters = nomalizeSetters(this.props.setters); // set name ,used in setting Transducer @@ -155,18 +195,35 @@ export default class MixedSetter extends Component<{ return firstMatched || firstDefault || this.setters[0]; } + constructor(props) { + super(props); + // TODO: use engine ext.props + const usedSetter = getMixedSelect(this.props.field); + if (usedSetter) { + this.used = usedSetter; + } + } + // dirty fix vision variable setter logic private hasVariableSetter = this.setters.some((item) => item.name === 'VariableSetter'); private useSetter = (name: string, usedName: string) => { + this.fromMixedSetterSelect = true; const { field } = this.props; + if (name === 'VariableSetter') { const setterComponent = getSetter('VariableSetter')?.component as any; if (setterComponent && setterComponent.isPopup) { - setterComponent.show({ prop: field }); - this.used = name; + setterComponent.show({ prop: field, node: this.triggerNodeRef }); + this.syncSelectSetter(name); return; } + } else { + // 变量类型直接设undefined会引起初始值变化 + if (name !== this.used ) { + // reset value + field.setValue(undefined); + } } if (name === this.used) { return; @@ -191,14 +248,20 @@ export default class MixedSetter extends Component<{ return usedItem; }); - // reset value - field.setValue(undefined); - this.used = name; + this.syncSelectSetter(name); + if (setter) { this.handleInitial(setter, fieldValue); } }; + private syncSelectSetter(name) { + // TODO: sync into engine ext.props + const { field } = this.props; + this.used = name; + setMixedSelect(field, name); + } + private handleInitial({ initialValue }: SetterItem, fieldValue: string) { const { field, onChange } = this.props; let newValue: any = initialValue; @@ -211,6 +274,7 @@ export default class MixedSetter extends Component<{ } private shell: HTMLDivElement | null = null; + private triggerNodeRef: HTMLDivElement | null = null; private checkIsBlockField() { if (this.shell) { @@ -264,6 +328,7 @@ export default class MixedSetter extends Component<{ } return createSetterContent(setterType, { + fromMixedSetterSelect: this.fromMixedSetterSelect, ...shallowIntl(setterProps), field, ...restProps, @@ -277,7 +342,6 @@ export default class MixedSetter extends Component<{ private contentsFromPolyfill(setterComponent: VariableSetter) { const { field } = this.props; - const n = this.setters.length; let setterContent: any; @@ -290,8 +354,8 @@ export default class MixedSetter extends Component<{ // =1: 原地展示<当前绑定的值,点击调用 VariableSetter.show>,icon 高亮是否->isUseVaiable,点击 VariableSetter.show setterContent = ( <a - onClick={() => { - setterComponent.show({ prop: field }); + onClick={(e) => { + setterComponent.show({ prop: field, node: e.target }); }} > {tipContent} @@ -313,8 +377,8 @@ export default class MixedSetter extends Component<{ icon: <IconVariable size={24} />, tip: tipContent, }} - onClick={() => { - setterComponent.show({ prop: field }); + onClick={(e: any) => { + setterComponent.show({ prop: field, node: e.target.parentNode }); }} /> ); @@ -327,11 +391,11 @@ export default class MixedSetter extends Component<{ if (currentSetter?.name === 'VariableSetter') { setterContent = ( <a - onClick={() => { - setterComponent.show({ prop: field }); + onClick={(e) => { + setterComponent.show({ prop: field, node: e.target }); }} > - {intlNode('Binded: {expr}', { expr: field.getValue().value })} + {intlNode('Binded: {expr}', { expr: field.getValue()?.value ?? '-' })} </a> ); } else { @@ -349,14 +413,16 @@ export default class MixedSetter extends Component<{ private renderSwitchAction(currentSetter?: SetterItem) { const usedName = currentSetter?.name || this.used; const triggerNode = ( - <Title - title={{ - tip: intlNode('Switch Setter'), - // FIXME: got a beautiful icon - icon: <IconConvert size={24} />, - }} - className="lc-switch-trigger" - /> + <div ref={ref => this.triggerNodeRef = ref}> + <Title + title={{ + tip: intlNode('Switch Setter'), + // FIXME: got a beautiful icon + icon: <IconConvert size={24} />, + }} + className="lc-switch-trigger" + /> + </div> ); return ( <Dropdown trigger={triggerNode} triggerType="click" align="tr br"> @@ -381,7 +447,7 @@ export default class MixedSetter extends Component<{ } render() { - const { className } = this.props; + const { className, field } = this.props; let contents: | { setterContent: ReactNode; @@ -413,7 +479,20 @@ export default class MixedSetter extends Component<{ > {contents.setterContent} <div className="lc-setter-actions">{contents.actions}</div> + {!!MixedSetterController.config.renderSlot && + <div className="lc-action-slot"> + {MixedSetterController.config.renderSlot?.({ + field, + bindCode: field.getValue()?.value, + })} + </div> + } </div> ); } } +interface MixedSetterType extends ComponentClass { + controller: typeof MixedSetterController; +} +export default MixedSetter as unknown as MixedSetterType +(MixedSetter as unknown as MixedSetterType).controller = MixedSetterController; diff --git a/src/setter/number-setter/index.tsx b/src/setter/number-setter/index.tsx index 365aea9..0381101 100644 --- a/src/setter/number-setter/index.tsx +++ b/src/setter/number-setter/index.tsx @@ -27,14 +27,12 @@ export default class NumberSetter extends React.PureComponent< onChange, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, - step, + step = 1, units = '', precision = 0, value, } = this.props; - console.log('value', value); - return ( <NumberPicker size="small" @@ -47,8 +45,7 @@ export default class NumberSetter extends React.PureComponent< step={step} innerAfter={units} onChange={(val: number) => { - console.log('val', val); - onChange(val); + onChange(!val ? 0 : val); }} /> ); diff --git a/src/setter/object-setter/index.tsx b/src/setter/object-setter/index.tsx index 2b763e3..12e1b1f 100644 --- a/src/setter/object-setter/index.tsx +++ b/src/setter/object-setter/index.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { Component, Fragment } from 'react'; import { Button } from '@alifd/next'; -import { common, SettingField } from '@alilc/lowcode-engine'; -import { SetterType, FieldConfig, CustomView, TitleContent } from '@alilc/lowcode-types'; +import { common } from '@alilc/lowcode-engine'; +import { IPublicTypeSetterType, IPublicModelSettingField, IPublicTypeFieldConfig, IPublicTypeCustomView, IPublicTypeTitleContent } from '@alilc/lowcode-types'; import CustomIcon from '../../components/custom-icon'; import './index.less'; @@ -12,8 +12,8 @@ const { isSettingField } = designerCabin; const { createSettingFieldView, PopupContext } = skeletonCabin; export default class ObjectSetter extends Component<{ - field: SettingField; - descriptor?: TitleContent; + field: IPublicModelSettingField; + descriptor?: IPublicTypeTitleContent; config: ObjectSetterConfig; mode?: 'popup' | 'form'; // 1: in tablerow 2: in listrow 3: in column-cell @@ -36,33 +36,35 @@ export default class ObjectSetter extends Component<{ } interface ObjectSetterConfig { - items?: FieldConfig[]; - extraSetter?: SetterType; + items?: IPublicTypeFieldConfig[]; + extraSetter?: IPublicTypeSetterType; + canCloseByOutSideClick: boolean; } interface RowSetterProps { - field: SettingField; - descriptor?: TitleContent; + value: any; + field: IPublicModelSettingField; + descriptor?: IPublicTypeTitleContent; config: ObjectSetterConfig; columns?: number; primaryButton?: boolean; } interface RowSetterState { - items: SettingField[]; - descriptor?: TitleContent; + items: IPublicModelSettingField[]; + descriptor?: IPublicTypeTitleContent; } -function getItemsFromProps(props: RowSetterProps) { +function getItemsFromProps(props: RowSetterProps, state?: RowSetterState) { const { config, field, columns } = props; const { extraProps } = field; - const items: SettingField[] = []; + const items: IPublicModelSettingField[] = []; if (columns && config?.items) { const l = Math.min(config.items.length, columns); - for (let i = 0; i < l; i++) { + for (let i = 0; i < config.items.length; i++) { const conf = config.items[i]; if (conf.isRequired || conf.important || (conf.setter as any)?.isRequired) { - const item = field.createField({ + const item = state?.items?.filter(d => d.name === conf.name)?.[0] || field.createField({ ...conf, // in column-cell forceInline: 3, @@ -76,6 +78,9 @@ function getItemsFromProps(props: RowSetterProps) { }; items.push(item); } + if (items.length >= l) { + break; + } } } return items; @@ -89,16 +94,16 @@ class RowSetter extends Component<RowSetterProps, RowSetterState> { items: [], }; - static getDerivedStateFromProps(props: RowSetterProps) { + static getDerivedStateFromProps(props: RowSetterProps, state: RowSetterState) { return { - items: getItemsFromProps(props), + items: getItemsFromProps(props, state), }; } constructor(props: RowSetterProps) { super(props); const { descriptor, field } = props; - const items: SettingField[] = getItemsFromProps(props); + const items: IPublicModelSettingField[] = getItemsFromProps(props); this.state = { items }; let firstRun = true; @@ -123,12 +128,16 @@ class RowSetter extends Component<RowSetterProps, RowSetterState> { }); } - shouldComponentUpdate(_: any, nextState: any) { + shouldComponentUpdate(nextProps: any, nextState: any) { if (this.state.descriptor !== nextState.descriptor) { return true; } + if (this.props.value !== nextProps.value) { + return true; + } return false; } + private pipe: any; render() { @@ -136,7 +145,10 @@ class RowSetter extends Component<RowSetterProps, RowSetterState> { const { field, config } = this.props; if (!this.pipe) { - this.pipe = (this.context as PopupPipe).create({ width: 320 }); + this.pipe = (this.context as PopupPipe).create({ + width: 320, + canCloseByOutSideClick: config.canCloseByOutSideClick + }); } const title = ( @@ -186,11 +198,11 @@ class RowSetter extends Component<RowSetterProps, RowSetterState> { } interface FormSetterProps { - field: SettingField; + field: IPublicModelSettingField; config: ObjectSetterConfig; } class FormSetter extends Component<FormSetterProps> { - private items: (SettingField | CustomView)[]; + private items: (IPublicModelSettingField | IPublicTypeCustomView)[]; constructor(props: RowSetterProps) { super(props); @@ -198,7 +210,7 @@ class FormSetter extends Component<FormSetterProps> { const { extraProps } = field; if (Array.isArray(field.items) && field.items.length > 0) { - field.items.forEach((item: SettingField | CustomView) => { + field.items.forEach((item: IPublicModelSettingField | IPublicTypeCustomView) => { if (isSettingField(item)) { const originalSetValue = item.extraProps.setValue; item.extraProps.setValue = (...args) => { diff --git a/src/setter/select-setter/index.tsx b/src/setter/select-setter/index.tsx index d025cb2..f7df64f 100644 --- a/src/setter/select-setter/index.tsx +++ b/src/setter/select-setter/index.tsx @@ -6,14 +6,23 @@ interface SelectSetterProps { value?: any; mode?: 'single' | 'multiple' | 'tag'; defaultValue?: any; - options: any[]; + options: any[] | Function; + /** + * 展开后是否能搜索 + */ + showSearch?: boolean; + // 是否可以清除 + hasClear?: boolean } interface SelectSetterState { setterValue: string | null; } -const formateOptions = (options: any[]) => { +const formateOptions = (options: any[] | Function) => { + if (typeof options === 'function') { + return options(); + } return options.map((item: any) => { if (item.children) { const children = item.children.map((child: any) => { @@ -52,7 +61,7 @@ export default class SelectSetter extends PureComponent<SelectSetterProps, Selec }; render() { - const { options, onChange, mode, value } = this.props; + const { options, onChange, mode, value, showSearch, hasClear } = this.props; return ( <Select autoWidth={false} @@ -64,6 +73,8 @@ export default class SelectSetter extends PureComponent<SelectSetterProps, Selec onChange && onChange(val); }} style={{ width: '100%' }} + showSearch={showSearch} + hasClear={hasClear} /> ); } diff --git a/src/setter/slot-setter/index.less b/src/setter/slot-setter/index.less index b3a28fa..f6bbfcb 100644 --- a/src/setter/slot-setter/index.less +++ b/src/setter/slot-setter/index.less @@ -1,6 +1,6 @@ .lc-setter-slot { display: flex; - align-items: start; + align-items: flex-start; .lc-slot-params { margin-top: 5px; } diff --git a/src/setter/slot-setter/index.tsx b/src/setter/slot-setter/index.tsx index e38ff57..3a37a20 100644 --- a/src/setter/slot-setter/index.tsx +++ b/src/setter/slot-setter/index.tsx @@ -116,6 +116,7 @@ export default class SlotSetter extends Component<{ switchComponent = ( <Switch autoWidth + checked={!!value} defaultChecked={isOpenSlot} onChange={(checked) => this.onChangeSwitch(checked)} size="small" @@ -133,7 +134,7 @@ export default class SlotSetter extends Component<{ <Input className="lc-slot-params" addonTextBefore="入参" - placeholder="插槽入参,以逗号风格" + placeholder="插槽入参,以逗号分隔" value={value.params!.join(',')} onChange={(val) => { val = val.trim(); diff --git a/src/setter/string-setter/index.tsx b/src/setter/string-setter/index.tsx index 349bebc..f030483 100644 --- a/src/setter/string-setter/index.tsx +++ b/src/setter/string-setter/index.tsx @@ -3,6 +3,8 @@ import { Input } from '@alifd/next'; import './index.less'; interface StringSetterProps { + isPreview?: boolean; + hasBorder?: boolean; value: string; defaultValue: string; placeholder: string; @@ -13,9 +15,11 @@ export default class StringSetter extends React.PureComponent<StringSetterProps, static displayName = 'StringSetter'; render() { - const { onChange, placeholder, value } = this.props; + const { onChange, placeholder, value, isPreview = false, hasBorder = true } = this.props; return ( <Input + isPreview={isPreview} + hasBorder={hasBorder} size="small" value={value} placeholder={placeholder || ''} diff --git a/src/setter/style-setter/components/color-input/index.tsx b/src/setter/style-setter/components/color-input/index.tsx index 5c59147..77468c7 100644 --- a/src/setter/style-setter/components/color-input/index.tsx +++ b/src/setter/style-setter/components/color-input/index.tsx @@ -9,6 +9,7 @@ interface ColorInputProps { styleData: StyleData | any; onStyleChange?: onStyleChange; inputWidth?: string; + color?:any } interface state { @@ -79,15 +80,15 @@ export default class ColorSetter extends React.Component<ColorInputProps, state> }; render() { - const { styleKey, styleData, inputWidth = '108px' } = this.props; + const { styleKey, styleData, inputWidth = '108px',color } = this.props; const InputTarget = ( <Input className="lowcode-setter-color" style={{ width: inputWidth }} hasClear - innerBefore={<div className="color-box" style={{ backgroundColor: styleData[styleKey] }} />} + innerBefore={<div className="color-box" style={{ backgroundColor: color?color:styleData[styleKey] }} />} onChange={this.inputChange} - value={styleData[styleKey]} + value={color?color:styleData[styleKey]} /> ); return ( @@ -100,7 +101,7 @@ export default class ColorSetter extends React.Component<ColorInputProps, state> triggerType="click" closable={false} > - <SketchPicker width={250} color={styleData[styleKey]} onChange={this.handleChange} /> + <SketchPicker width={250} color={color?color:styleData[styleKey]} onChange={this.handleChange} /> </Balloon> ); } diff --git a/src/setter/style-setter/components/css-code/index.tsx b/src/setter/style-setter/components/css-code/index.tsx index 742439e..88af18d 100644 --- a/src/setter/style-setter/components/css-code/index.tsx +++ b/src/setter/style-setter/components/css-code/index.tsx @@ -1,23 +1,22 @@ import * as React from 'react'; -import { Drawer } from '@alifd/next'; +import { Button } from '@alifd/next'; import { StyleData } from '../../utils/types'; import { parseToCssCode, parseToStyleData } from '../../utils'; import MonacoEditor from '@alilc/lowcode-plugin-base-monaco-editor'; +import Icon from '../../components/icon'; +import { intlLocal } from './locale'; interface CodeProps { - visible: boolean; styleData: StyleData | any; onStyleDataChange: (val: any) => void; - changeCssCodeVisiable: { - (visable: boolean): void; - }; } +const cssConfig = intlLocal(); + const defaultEditorOption = { readOnly: false, - automaticLayout: true, + // automaticLayout: true, folding: true, // 默认开启折叠代码功能 - lineNumbers: 'on', wordWrap: 'off', formatOnPaste: true, fontSize: 12, @@ -28,30 +27,31 @@ const defaultEditorOption = { minimap: { enabled: false, }, + options: { + lineNumbers: 'off', + fixedOverflowWidgets: true, + automaticLayout: true, + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + hover: { + enabled: false, + }, + }, scrollbar: { - vertical: 'auto', horizontal: 'auto', }, }; export default class CssCode extends React.Component<CodeProps> { state = { - offsetX: -300, defaultEditorProps: {}, cssCode: '', + isCanSave: true, + height: '100px', }; - shouldComponentUpdate(nextProps: CodeProps, nextState: any) { - if ( - nextProps.visible != this.props.visible || - (this.state.cssCode == '' && nextState.cssCode != '') - ) { - return true; - } - - return false; - } - componentWillReceiveProps(nextProps: CodeProps) { const cssCode = parseToCssCode(nextProps.styleData); this.setState({ @@ -69,52 +69,92 @@ export default class CssCode extends React.Component<CodeProps> { } const cssCode = parseToCssCode(styleData); - console.log('cssCode', cssCode); + // console.log('cssCode', cssCode); this.setState({ - defaultEditorProps: { - style: { - width: '100%', - height: document.body.clientHeight - 90 + 'px', - }, - }, cssCode, }); } - updateCode = (newCode: string) => { + styleSave = () => { + const { cssCode } = this.state; const { onStyleDataChange } = this.props; + const newStyleData = parseToStyleData(cssCode); + // 检查是否和原来的styleData完全相同 + if (newStyleData) { + onStyleDataChange(newStyleData); + this.setState({ + isCanSave: true, + }); + } + }; + + updateCode = (newCode: string) => { this.setState({ cssCode: newCode, }); - let newStyleData = parseToStyleData(newCode); - // 检查是否和原来的styleData完全相同 - newStyleData && onStyleDataChange(newStyleData); + const newStyleData = parseToStyleData(newCode); + if (newStyleData) { + this.setState({ + isCanSave: false, + }); + } }; - + // 高度缓存 + prevHeight = 80; render() { - const { offsetX, cssCode, defaultEditorProps } = this.state; - const { visible, changeCssCodeVisiable } = this.props; + const { cssCode, defaultEditorProps, isCanSave, height } = this.state; + const handleEditorMount = (monaco: any, editor: any) => { + editor.onDidBlurEditorWidget(() => { + this.styleSave(); + }); + editor.onDidChangeModelDecorations(() => { + updateEditorHeight(); + // 控制刷新频率 + requestAnimationFrame(updateEditorHeight); + }); + const updateEditorHeight = () => { + const padding = 20; + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const lineCount = editor.getModel()?.getLineCount() || 1; + const height = lineCount * lineHeight + padding; + if (this.prevHeight !== height) { + this.prevHeight = height; + this.setState({height: `${height}px`}) + editor.layout(); + } + }; + // 挂载时先适配高度 + updateEditorHeight() + }; return ( - <Drawer - width={360} - visible={visible} - offset={[offsetX, 0]} - hasMask={false} - title="css源码编辑" - onClose={() => { - changeCssCodeVisiable(true); - }} - > - <MonacoEditor - value={cssCode} - {...defaultEditorProps} - {...defaultEditorOption} - {...{ language: 'css' }} - onChange={(newCode: string) => this.updateCode(newCode)} - /> - </Drawer> + <div> + <div + style={{ + marginBottom: '5px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }} + > + <Icon type="icon-CSS" /> + <Button type="primary" onClick={this.styleSave} disabled={isCanSave} size="small"> + {cssConfig.save} + </Button> + </div> + {this.state.cssCode && ( + <MonacoEditor + value={cssCode} + {...defaultEditorProps} + {...defaultEditorOption} + {...{ language: 'css' }} + onChange={(newCode: string) => this.updateCode(newCode)} + editorDidMount={handleEditorMount} + height={height} + /> + )} + </div> ); } } diff --git a/src/setter/style-setter/components/css-code/locale/en-US.json b/src/setter/style-setter/components/css-code/locale/en-US.json new file mode 100644 index 0000000..d144d87 --- /dev/null +++ b/src/setter/style-setter/components/css-code/locale/en-US.json @@ -0,0 +1,3 @@ +{ + "save": "Save" +} diff --git a/src/setter/style-setter/components/css-code/locale/index.ts b/src/setter/style-setter/components/css-code/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/components/css-code/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/components/css-code/locale/zh-CN.json b/src/setter/style-setter/components/css-code/locale/zh-CN.json new file mode 100644 index 0000000..50fb480 --- /dev/null +++ b/src/setter/style-setter/components/css-code/locale/zh-CN.json @@ -0,0 +1,3 @@ +{ + "save": "保存" +} diff --git a/src/setter/style-setter/components/number/index.less b/src/setter/style-setter/components/number/index.less new file mode 100644 index 0000000..a3e02ce --- /dev/null +++ b/src/setter/style-setter/components/number/index.less @@ -0,0 +1,19 @@ +.next-select-trigger { + min-width: 1px; +} +.next-select-inner { + min-width: 1px !important; +} +.next-input-text-field { + padding: 0 2px !important; +} + +.ext-css-variable-ghost { + width: 70px; + line-height: 42px; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; + color: #666; + overflow: hidden; +} diff --git a/src/setter/style-setter/components/number/index.tsx b/src/setter/style-setter/components/number/index.tsx index 17f450e..babc142 100644 --- a/src/setter/style-setter/components/number/index.tsx +++ b/src/setter/style-setter/components/number/index.tsx @@ -1,20 +1,33 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { NumberPicker } from '@alifd/next'; +import { useEffect, useState, useMemo } from 'react'; +import { NumberPicker, Select } from '@alifd/next'; import { StyleData, onStyleChange } from '../../utils/types'; -import { addUnit, removeUnit, isEmptyValue, getPlaceholderPropertyValue } from '../../utils'; +import { + addUnit, + removeUnit, + isEmptyValue, + getPlaceholderPropertyValue, + unifyStyle, + getUnit, + isCssVarBind, +} from '../../utils'; +import './index.less'; + interface numberProps { styleKey: string; styleData: StyleData | any; onStyleChange?: onStyleChange; - unit?: string; + unit?: string | string[]; min?: number; max?: number; style?: any; className?: string; field?: any; placeholderScale?: number; + defaultPlaceholder?: string; useComputedStyle?: boolean; + onChangeFunction?: any; + multiProp?: any; // 属性值包含多项是的项序号 } export default (props: numberProps) => { @@ -28,15 +41,17 @@ export default (props: numberProps) => { style = {}, className = '', placeholderScale, + onChangeFunction, + multiProp, + defaultPlaceholder, } = props; - const [placeholder, setPlaceholder] = useState(null); - + const [placeholder, setPlaceholder] = useState(defaultPlaceholder); const onNumberChange = (styleKey: string, value: number, unit?: string) => { onStyleChange([ { styleKey, - value: unit ? addUnit(value, unit) : value, + value: unit ? addUnit(value, unit) : String(value), }, ]); }; @@ -58,16 +73,72 @@ export default (props: numberProps) => { initData(props); }, []); + let value = unit ? removeUnit(styleData[styleKey]) : styleData[styleKey]; + let curUnit = unit ? getUnit(styleData[styleKey]) || 'px' : ''; + // 不加multiprop一样,加了单独处理 + if (typeof multiProp === 'number') { + value = unifyStyle(styleData[styleKey])?.split(' ')?.[multiProp]; + if (value === null || value === undefined || value === 'auto') { + value = null; + curUnit = 'px'; + } else { + curUnit = unit ? getUnit(value) || 'px' : ''; + value = unit ? removeUnit(value) : value; + } + } + if (isNaN(value)) { + value = 0; + } + const getInnerAfter = useMemo(() => { + if (typeof unit === 'string') { + return unit; + } + if (!unit) { + return ''; + } + const options = unit?.map((item) => { + return { + value: item, + label: item, + }; + }); + return ( + <Select + defaultValue="px" + style={{ width: '24px' }} + value={curUnit || 'px'} + autoWidth={false} + hasBorder={false} + hasArrow={false} + onChange={(val) => + onChangeFunction + ? onChangeFunction(styleKey, value, val) + : onNumberChange(styleKey, value, val) + } + dataSource={options} + /> + ); + }, [unit]); + const originValue = styleData[styleKey] + if (isCssVarBind(originValue)) { + return <div className='ext-css-variable-ghost' title={originValue}> + {originValue} + </div>; + } return ( <NumberPicker style={style} className={className} - value={unit ? removeUnit(styleData[styleKey]) : styleData[styleKey]} + value={value} min={isEmptyValue(min) ? Number.MIN_SAFE_INTEGER : min} max={isEmptyValue(max) ? Number.MAX_SAFE_INTEGER : max} - onChange={(val) => onNumberChange(styleKey, val, unit)} - innerAfter={unit ? unit : ''} + onChange={(val) => + onChangeFunction + ? onChangeFunction(styleKey, val, curUnit) + : onNumberChange(styleKey, val, curUnit) + } + innerAfter={getInnerAfter} placeholder={placeholder} - ></NumberPicker> + /> ); }; diff --git a/src/setter/style-setter/components/radio-group/index.tsx b/src/setter/style-setter/components/radio-group/index.tsx index e80db04..b57a36f 100644 --- a/src/setter/style-setter/components/radio-group/index.tsx +++ b/src/setter/style-setter/components/radio-group/index.tsx @@ -27,20 +27,40 @@ export default (props: radioProps) => { ]); }; + const onRadioItemClick = (key: string, val: string | number | boolean, value: string) => { + if (value == val) { + onStyleChange([ + { + styleKey: key, + value: null, + }, + ]); + } + }; + return ( <div className="radiogroup-style"> - {value ? ( + {/* {value ? ( <RadioGroup value={value} shape="button" onChange={(val) => onRadioChange(styleKey, val)} + // onClick={(event) => { + // onRadioClick(event, styleKey, value); + // }} aria-labelledby="groupId" > {dataList && dataList.map((item: RadioItem) => ( <Balloon trigger={ - <Radio id={item.value} value={item.value}> + <Radio + id={item.value} + value={item.value} + onClick={(e) => { + onRadioItemClick(styleKey, e.currentTarget.id, value); + }} + > {item.icon ? <Icon type={item.icon} size="small"></Icon> : item.title} </Radio> } @@ -74,7 +94,38 @@ export default (props: radioProps) => { </Balloon> ))} </RadioGroup> - )} + )} */} + <RadioGroup + value={value} + shape="button" + onChange={(val) => onRadioChange(styleKey, val)} + // onClick={(event) => { + // onRadioClick(event, styleKey, value); + // }} + aria-labelledby="groupId" + > + {dataList && + dataList.map((item: RadioItem) => ( + <Balloon + trigger={ + <Radio + id={item.value} + value={item.value} + onClick={(e) => { + onRadioItemClick(styleKey, e.currentTarget.id, value); + }} + > + {item.icon ? <Icon type={item.icon} size="small"></Icon> : item.title} + </Radio> + } + triggerType="hover" + closable={false} + align="t" + > + {item.tips} + </Balloon> + ))} + </RadioGroup> </div> ); }; diff --git a/src/setter/style-setter/components/row/index.less b/src/setter/style-setter/components/row/index.less index 0558b48..eb0ed45 100644 --- a/src/setter/style-setter/components/row/index.less +++ b/src/setter/style-setter/components/row/index.less @@ -5,8 +5,15 @@ margin-bottom: 10px; .title-contaienr { - width: 60px; + width: 50px; flex-direction: row; + line-height: 1; + } + + .title-contaienr-long { + width: 55px; + flex-direction: row; + line-height: 1; } .title-text { diff --git a/src/setter/style-setter/components/row/index.tsx b/src/setter/style-setter/components/row/index.tsx index bc8889d..8c74903 100644 --- a/src/setter/style-setter/components/row/index.tsx +++ b/src/setter/style-setter/components/row/index.tsx @@ -13,19 +13,19 @@ interface rowProps { onStyleChange?: onStyleChange; value?: string; contentStyle?: any; + longTitle?: boolean } export default (props: rowProps) => { - const { title, dataList, styleKey, children, styleData, contentStyle = {} } = props; - + const { title, dataList, styleKey, children, styleData, contentStyle = {}, value, longTitle } = props; return ( <div className="row-container"> {title && ( <div className={ styleData[styleKey] - ? 'title-contaienr title-text title-text-active' - : 'title-contaienr title-text' + ? `${!longTitle?'title-contaienr':'title-contaienr-long'} title-text title-text-active` + : `${!longTitle?'title-contaienr':'title-contaienr-long'} title-text` } > {title} @@ -39,7 +39,8 @@ export default (props: rowProps) => { <RadioGroup dataList={dataList} {...props} - value={styleData ? styleData[styleKey] : null} + // 区分是style类型的值还是其他普通的值,从styleData获取的是对象 + value={typeof value != 'string' ? styleData && styleData[styleKey] : value} ></RadioGroup> )} </div> diff --git a/src/setter/style-setter/index.tsx b/src/setter/style-setter/index.tsx index b9f40c8..40135db 100644 --- a/src/setter/style-setter/index.tsx +++ b/src/setter/style-setter/index.tsx @@ -6,15 +6,27 @@ import Border from './pro/border'; import Background from './pro/background'; import CssCode from './components/css-code'; import { StyleData } from './utils/types'; -import Icon from './components/icon'; import { ConfigProvider } from '@alifd/next'; +import enUS from '@alifd/next/lib/locale/en-us'; +import zhCN from '@alifd/next/lib/locale/zh-cn'; +import { common } from '@alilc/lowcode-engine'; import './index.less'; + +const { getLocale } = common.utils.createIntl?.() || {}; +const locale: string = getLocale?.() || 'zh-CN'; +const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, +}; + interface StyleSetterProps { value: StyleData; defaultValue: string; placeholder: string; field: any; onChange: (val: any) => void; + isShowCssCode: boolean; + showModuleList: string[]; } export default class StyleSetterV2 extends React.PureComponent<StyleSetterProps> { @@ -23,6 +35,33 @@ export default class StyleSetterV2 extends React.PureComponent<StyleSetterProps> unit: 'px', // 默认计算尺寸缩放 placeholderScale: 1, + // 展示板块 + showModuleList: ['background', 'border', 'font', 'layout', 'position'], + // 是否展示css源码编辑面板 + isShowCssCode: true, + // layout 配置面板 + layoutPropsConfig: { + // display 展示列表 + showDisPlayList: ['inline', 'flex', 'block', 'inline-block', 'none'], + isShowPadding: true, + isShowMargin: true, + isShowWidthHeight: true, + }, + + fontPropsConfig: { + // fontFamily列表 + fontFamilyList: [ + { value: 'Helvetica', label: 'Helvetica' }, + { value: 'Arial', label: 'Arial' }, + { value: 'serif', label: 'serif' }, + ], + }, + + // position 配置面板 + positionPropsConfig: { + isShowFloat: true, + isShowClear: true, + }, }; state = { styleData: {}, cssCodeVisiable: false, initFlag: false }; @@ -51,9 +90,9 @@ export default class StyleSetterV2 extends React.PureComponent<StyleSetterProps> * @param styleKey * @param value */ - onStyleChange = (styleDataList: Array<StyleData>) => { + onStyleChange = (styleDataList: StyleData[]) => { const { onChange } = this.props; - let styleData: StyleData | any = Object.assign({}, this.state.styleData); + const styleData: StyleData | any = Object.assign({}, this.state.styleData); styleDataList && styleDataList.map((item) => { if (item.value == undefined || item.value == null) { @@ -81,41 +120,69 @@ export default class StyleSetterV2 extends React.PureComponent<StyleSetterProps> }; render() { + const { isShowCssCode, showModuleList } = this.props; const { styleData, cssCodeVisiable, initFlag } = this.state; - console.log('styleData', styleData); + return ( - <ConfigProvider> + <ConfigProvider locale={localeSource[locale]}> <div className="lowcode-setter-style-v2"> - <div className="top-bar"> - <div - onClick={() => this.changeCssCodeVisiable(false)} - className={cssCodeVisiable ? 'top-icon-active' : 'top-icon'} - > - <Icon type="icon-CSS"></Icon> + {isShowCssCode && ( + <div className="top-bar"> + {/* <div + onClick={() => this.changeCssCodeVisiable(false)} + className={cssCodeVisiable ? 'top-icon-active' : 'top-icon'} + > + <Icon type="icon-CSS"></Icon> + </div> */} + + <CssCode styleData={styleData} onStyleDataChange={this.onStyleDataChange} /> </div> - </div> - <Layout onStyleChange={this.onStyleChange} styleData={styleData} {...this.props}></Layout> - - <Font onStyleChange={this.onStyleChange} styleData={styleData} {...this.props}></Font> - <Background - onStyleChange={this.onStyleChange} - styleData={styleData} - {...this.props} - ></Background> - <Position - onStyleChange={this.onStyleChange} - styleData={styleData} - {...this.props} - ></Position> - <Border onStyleChange={this.onStyleChange} styleData={styleData} {...this.props}></Border> - {initFlag && ( + )} + + {showModuleList.filter((item) => item == 'layout').length > 0 && ( + <Layout + onStyleChange={this.onStyleChange} + styleData={styleData} + {...this.props} + /> + )} + + {showModuleList.filter((item) => item == 'font').length > 0 && ( + <Font onStyleChange={this.onStyleChange} styleData={styleData} {...this.props} /> + )} + + {showModuleList.filter((item) => item == 'background').length > 0 && ( + <Background + onStyleChange={this.onStyleChange} + styleData={styleData} + {...this.props} + /> + )} + + {showModuleList.filter((item) => item == 'position').length > 0 && ( + <Position + onStyleChange={this.onStyleChange} + styleData={styleData} + {...this.props} + /> + )} + + {showModuleList.filter((item) => item == 'border').length > 0 && ( + <Border + onStyleChange={this.onStyleChange} + styleData={styleData} + {...this.props} + /> + )} + + {/* {initFlag && ( <CssCode visible={cssCodeVisiable} styleData={styleData} onStyleDataChange={this.onStyleDataChange} changeCssCodeVisiable={this.changeCssCodeVisiable} ></CssCode> - )} + )} */} </div> </ConfigProvider> ); diff --git a/src/setter/style-setter/pro/background/config.json b/src/setter/style-setter/pro/background/config.json deleted file mode 100644 index f5d4e80..0000000 --- a/src/setter/style-setter/pro/background/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "backgroundType": { - "title": "背景类型", - "dataList": [ - { - "value": "color", - "tips": "颜色填充", - "icon": "icon-beijingse" - }, - { - "value": "bgImg", - "tips": "背景图片", - "icon": "icon-beijingtupian" - } - ] - } -} diff --git a/src/setter/style-setter/pro/background/constant.tsx b/src/setter/style-setter/pro/background/constant.tsx new file mode 100644 index 0000000..e46d921 --- /dev/null +++ b/src/setter/style-setter/pro/background/constant.tsx @@ -0,0 +1,5 @@ +export const backgroundSizeMap = { + "contain": "contain", + "cover": "cover", + "default": "default" +} \ No newline at end of file diff --git a/src/setter/style-setter/pro/background/index.less b/src/setter/style-setter/pro/background/index.less new file mode 100644 index 0000000..a2a6b50 --- /dev/null +++ b/src/setter/style-setter/pro/background/index.less @@ -0,0 +1,55 @@ +.inner-row-contaienr-bgsize { + display: flex; + flex-direction: row; + margin-top: 10px; + margin-bottom: 10px; + margin-left: 46px; + .row-item { + display: flex; + flex-direction: row; + align-items: center; + } + + .row-item-title { + margin-right: 4px; + } +} +.background-position-container { + display: flex; + align-items: center; + justify-content: space-between; + .background-position-container-left { + display: flex; + flex-direction: row; + align-items: center; + flex: 0 0 auto; + justify-content: space-between; + flex-wrap: wrap; + width: 60px; + height: 64px; + margin-right: 30px; + .sel-icon { + ::before { + margin-top: 2px; + background-color: #2077ff; + position: absolute; + opacity: 0.3; + width: 16px; + height: 16px; + content: ''; + } + } + .background-position-icon { + width: 16px; + height: 16px; + } + } + .background-position-container-right { + .background-position-left { + margin-top: 3px; + } + .background-position-top { + margin-top: 4px; + } + } +} diff --git a/src/setter/style-setter/pro/background/index.tsx b/src/setter/style-setter/pro/background/index.tsx index e5e795d..72a2652 100644 --- a/src/setter/style-setter/pro/background/index.tsx +++ b/src/setter/style-setter/pro/background/index.tsx @@ -1,14 +1,18 @@ -import * as React from 'react'; -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import Row from '../../components/row'; import Icon from '../../components/icon'; +import Number from '../../components/number'; import ColorInput from '../../components/color-input'; import { StyleData, onStyleChange } from '../../utils/types'; -import { Collapse, Input } from '@alifd/next'; -import borderConfig from './config.json'; -// import "./index.scss"; -const Panel = Collapse.Panel; +import { Collapse, Input, NumberPicker, Range } from '@alifd/next'; +import { addUnit, isCssVarBind, isEmptyValue, parseValue, unifyStyle } from '../../utils'; +import { intlLocal } from './locale'; +import './index.less'; +import { backgroundSizeMap } from './constant'; +const backgroundConfig = intlLocal(); + +const {Panel} = Collapse; interface fontProps { styleData: StyleData | any; onStyleChange?: onStyleChange; @@ -16,18 +20,174 @@ interface fontProps { } export default (props: fontProps) => { const { onStyleChange, styleData } = props; - const { backgroundType } = borderConfig; + const { backgroundType, backgroundSize, backgroundPosition, backgroundRepeat } = backgroundConfig; const [bgType, setBgType] = useState(null); - - const onBgTypeChange = (styleDataList: Array<StyleData>) => { + const [bgSizeType, setBgSizeType] = useState(null); + const [bgRepeatType, setBgRepeatType] = useState(null); + const [bgPositionType, setBgPositionType] = useState<string>(''); + // 背景类型切换 + const onBgTypeChange = (styleDataList: StyleData[]) => { if (styleDataList) { setBgType(styleDataList[0].value); } }; + // 背景图片切换 + const onBgImageChange = (value: string) => { + onStyleChange([ + { + styleKey: 'backgroundImage', + value: formatBgImgUrl(value), + }, + ]); + }; + // backgroundSize类型切换 + const onBgSizeTypeChange = (styleDataList: StyleData[]) => { + const backgroundSize = 'backgroundSize'; + onStyleChange([ + { + styleKey: backgroundSize, + value: null, + }, + ]); + if (styleDataList) { + const value = styleDataList[0]?.value; + setBgSizeType(value); + if (value != backgroundSizeMap.default) { + onStyleChange([ + { + styleKey: backgroundSize, + value, + }, + ]); + } + } + }; + // backgroundSize值切换 + const onBgSizeChange = ( + styleKey: string, + value: number, + unit: string, + styleData: any, + direction: string, + ) => { + const bgSizeArray = styleData[styleKey] + ? unifyStyle(styleData[styleKey])?.split(' ') + : ['auto', 'auto']; + const [width = 'auto', height = 'auto'] = bgSizeArray; + let styleDataList; + if (styleData) { + let unifiedValue = unit ? addUnit(value, unit) : value; + if (unifiedValue === null || unifiedValue === undefined) unifiedValue = 'auto'; // 空样式默认为auto + if (direction === 'width') { + styleDataList = [ + { + styleKey, + value: + unifiedValue !== 'auto' || height !== 'auto' ? `${unifiedValue } ${ height}` : null, // 都为auto则删除样式 + }, + ]; + } else { + styleDataList = [ + { + styleKey, + value: unifiedValue !== 'auto' || width !== 'auto' ? `${width } ${ unifiedValue}` : null, + }, + ]; + } + onStyleChange(styleDataList); + } + }; + // backgroundRepeat切换 + const onBgRepeatChange = (styleDataList: StyleData[]) => { + if (styleDataList) { + const value = styleDataList[0]?.value; + setBgRepeatType(value); + onStyleChange([ + { + styleKey: 'backgroundRepeat', + value, + }, + ]); + } + }; + + // backgroundPosition切换 + const onBgPositionChange = ( + styleKey: string, + value: number, + unit: string, + styleData: any, + direction: string, + ) => { + const bgSizeArray = styleData[styleKey] + ? unifyStyle(styleData[styleKey]).split(' ') + : ['auto', 'auto']; + const [width = 'auto', height = 'auto'] = bgSizeArray; + let styleDataList; + if (styleData) { + let unifiedValue = /^-?[0-9]\d*$/.test(value) ? value + unit : value; // 正则匹配非0数字并加单位 + if ( + unifiedValue === null || + unifiedValue === undefined || + unifiedValue.replace(/\s*/g, '') === '' // 空格和空字符串也为空值 + ){ + unifiedValue = 'auto'; + } + if (direction === 'horizontal') { + styleDataList = [ + { + styleKey, + value: + unifiedValue !== 'auto' || height !== 'auto' ? `${unifiedValue } ${ height}` : null, + }, + ]; + } else { + styleDataList = [ + { + styleKey, + value: unifiedValue !== 'auto' || width !== 'auto' ? `${width } ${ unifiedValue}` : null, + }, + ]; + } + onStyleChange(styleDataList); + } + }; + // 透明度切换 + const onOpacityChange = (styleKey: string, value: number, unit?: string) => { + onStyleChange([ + { + styleKey, + value: unit ? addUnit(value, unit) : value, + }, + ]); + }; + const initData = () => { + if (styleData.backgroundColor) { + setBgType('color'); + } else if (styleData.backgroundImage) { + setBgType('bgImg'); + } else { + setBgType(null); + } + setBgRepeatType(styleData.backgroundRepeat); + const bgSizeType = + styleData.backgroundSize === backgroundSizeMap.contain || + styleData.backgroundSize === backgroundSizeMap.cover + ? styleData.backgroundSize + : backgroundSizeMap.default; + setBgSizeType(bgSizeType); + const chosenItem = backgroundPosition.dataList.find((item) => { + return item.position === styleData.backgroundPosition; + }); + setBgPositionType(chosenItem?.title); + }; + useEffect(() => { + initData(); + }, [styleData]); const formatBgImgUrl = (url: string) => { if (url && url != '') { - return 'url(' + url + ')'; + return `url(${ url })`; } else { return null; } @@ -37,7 +197,7 @@ export default (props: fontProps) => { if (styleUrl) { // const reg = /^url\(.*\)/; // var result = styleUrl.match(reg); - let newUrl = styleUrl.substring(styleUrl.indexOf('(') + 1, styleUrl.indexOf(')')); + const newUrl = styleUrl.substring(styleUrl.indexOf('(') + 1, styleUrl.indexOf(')')); return newUrl; // return styleUrl.substring( @@ -48,31 +208,9 @@ export default (props: fontProps) => { return ''; } }; - - const initData = () => { - if (styleData.backgroundColor) { - setBgType('backgroundColor'); - } else if (styleData.backgroundImage) { - setBgType('backgroundImage'); - } - }; - - useEffect(() => { - initData(); - }, []); - - const onInputChange = (value: string) => { - onStyleChange([ - { - styleKey: 'backgroundImage', - value: formatBgImgUrl(value), - }, - ]); - }; - return ( <Collapse defaultExpandedKeys={['0']}> - <Panel title="背景" className="font-style-container"> + <Panel title={backgroundConfig.title} className="font-style-container"> <Row title={backgroundType.title} dataList={backgroundType.dataList} @@ -80,11 +218,11 @@ export default (props: fontProps) => { {...props} onStyleChange={onBgTypeChange} value={bgType} - ></Row> + /> {bgType == 'color' && ( <Row title={' '} styleKey="" {...props}> - <ColorInput styleKey={'backgroundColor'} {...props} inputWidth="100%"></ColorInput> + <ColorInput styleKey={'backgroundColor'} {...props} inputWidth="100%" /> </Row> )} @@ -92,37 +230,143 @@ export default (props: fontProps) => { <Row title={' '} styleKey="" {...props}> <Input innerBefore={<Icon type="icon-suffix-url" style={{ margin: 4 }} />} - placeholder="输入图片url" + placeholder={backgroundConfig.inputPlaceholder} style={{ width: '100%' }} - value={backToBgImgUrl(styleData['backgroundImage'])} - onChange={onInputChange} + value={backToBgImgUrl(styleData.backgroundImage)} + onChange={onBgImageChange} /> </Row> )} + {bgType == 'bgImg' && ( + <> + <Row + title={backgroundSize.title} + dataList={backgroundSize.dataList} + {...props} + onStyleChange={onBgSizeTypeChange} + value={bgSizeType} + /> + {bgSizeType == backgroundSizeMap.default && ( + <div className="inner-row-contaienr-bgsize"> + <div className="row-item"> + <span className="row-item-title">{backgroundConfig.width}</span> + <Number + style={{ marginRight: '4px' }} + min={0} + styleKey="backgroundSize" + {...props} + unit = {['px','%']} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBgSizeChange(styleKey, val, unit, styleData, 'width') + } + multiProp={0} + defaultPlaceholder={'auto'} + /> + </div> + <div className="row-item"> + <span className="row-item-title">{backgroundConfig.height}</span> + <Number + styleKey="backgroundSize" + min={0} + {...props} + unit = {['px','%']} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBgSizeChange(styleKey, val, unit, styleData, 'height') + } + multiProp={1} + defaultPlaceholder={'auto'} + /> + </div> + </div> + )} + <Row title={backgroundPosition.title} styleKey="border" {...props}> + <div className="background-position-container"> + <div className="background-position-container-left"> + {backgroundPosition.dataList.map((item) => { + return ( + <div + className={bgPositionType === item.title ? 'sel-icon' : ''} + onClick={() => { + setBgPositionType(item.title); + onStyleChange([ + { + styleKey: 'backgroundPosition', + value: item.position, + }, + ]); + }} + > + <Icon className="background-position-icon" type={item.icon} /> + </div> + ); + })} + </div> + + <div className="background-position-container-right"> + <div className="background-position-left"> + <span>{backgroundConfig.left}</span> + <Number + style={{ marginLeft: '10px' }} + styleKey="backgroundPosition" + {...props} + unit = {['px','%']} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBgPositionChange(styleKey, val, unit, styleData, 'horizontal') + } + multiProp={0} + defaultPlaceholder={'auto'} + /> + </div> + <div className="background-position-top"> + <span>{backgroundConfig.top}</span> + <Number + style={{ marginLeft: '10px' }} + min={-10} + styleKey="backgroundPosition" + {...props} + unit = {['px','%']} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBgPositionChange(styleKey, val, unit, styleData, 'verticle') + } + multiProp={1} + defaultPlaceholder={'auto'} + /> + + </div> + </div> + </div> + </Row> + <Row + title={backgroundRepeat.title} + dataList={backgroundRepeat.dataList} + styleKey="" + {...props} + onStyleChange={onBgRepeatChange} + value={bgRepeatType} + /> + </> + )} - {/* <Row title={"透明度"} styleKey="opacity" {...props}> + <Row title={backgroundConfig.opacity} styleKey="opacity" {...props}> <div className="opacity-container"> <Range - style={{ marginRight: "7px" }} - value={ - !isEmptyValue(styleData.opacity) ? styleData.opacity * 100 : 0 - } - onChange={(val) => onNumberChange("opacity", parseInt(val) / 100)} + disabled={isCssVarBind(styleData.opacity)} + style={{ marginLeft: '10px', marginRight: '10px', width: '104px' }} + value={!isEmptyValue(styleData.opacity) ? styleData.opacity * 100 : 0} + onChange={(val) => onOpacityChange('opacity', parseInt(val) / 100)} /> <NumberPicker value={ - !isEmptyValue(styleData.opacity) - ? Math.floor(styleData.opacity * 100) - : undefined + !isEmptyValue(styleData.opacity) && !isCssVarBind(styleData.opacity) ? Math.floor(styleData.opacity * 100) : undefined } + disabled={isCssVarBind(styleData.opacity)} max={100} min={0} - onChange={(val) => - onNumberChange("opacity", isEmptyValue(val) ? null : val / 100) - } - ></NumberPicker> + onChange={(val) => onOpacityChange('opacity', isEmptyValue(val) ? null : val / 100)} + innerAfter={'%'} + /> </div> - </Row> */} + </Row> </Panel> </Collapse> ); diff --git a/src/setter/style-setter/pro/background/locale/en-US.json b/src/setter/style-setter/pro/background/locale/en-US.json new file mode 100644 index 0000000..76354c3 --- /dev/null +++ b/src/setter/style-setter/pro/background/locale/en-US.json @@ -0,0 +1,83 @@ +{ + "title": "Background", + "inputPlaceholder": "input image url", + "width": "width", + "height": "height", + "left": "left", + "top": "top", + "opacity": "opacity", + "backgroundType": { + "title": "back ground", + "dataList": [ + { + "value": "color", + "tips": "background color", + "icon": "icon-beijingse" + }, + { + "value": "bgImg", + "tips": "background image", + "icon": "icon-beijingtupian" + } + ] + }, + "backgroundSize": { + "title": "size", + "dataList": [ + { + "value": "default", + "tips": "default", + "title": "default" + }, + { + "value": "contain", + "tips": "contain", + "title": "contain" + }, + { + "value": "cover", + "tips": "cover", + "title": "cover" + } + ] + }, + "backgroundPosition": { + "title": "position", + "dataList": [ + { "title": "leftTop", "position": "0 0", "icon": "icon-zuoshangjiao" }, + { "title": "midTop", "position": "50% 0", "icon": "icon-dingbu" }, + { "title": "rightTop", "position": "100% 0", "icon": "icon-youshangjiao" }, + { "title": "leftMid", "position": "0 50%", "icon": "icon-kaozuo" }, + { "title": "mid", "position": "50% 50%", "icon": "icon--quanbubiankuang" }, + { "title": "rightMid", "position": "100% 50%", "icon": "icon-kaoyou" }, + { "title": "leftBottom", "position": "0 100%", "icon": "icon-zuoxiajiao" }, + { "title": "midBottom", "position": "50% 100%", "icon": "icon-dibu" }, + { "title": "rightBottom", "position": "100% 100%", "icon": "icon-youxiajiao" } + ] + }, + "backgroundRepeat": { + "title": "repeat", + "dataList": [ + { + "value": "repeat", + "tips": "repeat", + "icon": "icon-jiugongge" + }, + { + "value": "repeat-x", + "tips": "reapeat-x", + "icon": "icon-hengxiangpingfen" + }, + { + "value": "repeat-y", + "tips": "repeat-y", + "icon": "icon-zongxiangpingfen" + }, + { + "value": "no-repeat", + "tips": "no-repeat", + "icon": "icon-shanchu2" + } + ] + } +} diff --git a/src/setter/style-setter/pro/background/locale/index.ts b/src/setter/style-setter/pro/background/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/pro/background/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/pro/background/locale/zh-CN.json b/src/setter/style-setter/pro/background/locale/zh-CN.json new file mode 100644 index 0000000..a59cb01 --- /dev/null +++ b/src/setter/style-setter/pro/background/locale/zh-CN.json @@ -0,0 +1,83 @@ +{ + "title": "背景", + "inputPlaceholder": "输入图片url", + "width": "宽", + "height": "高", + "left": "左", + "top": "顶", + "opacity": "透明度", + "backgroundType": { + "title": "背景", + "dataList": [ + { + "value": "color", + "tips": "颜色填充", + "icon": "icon-beijingse" + }, + { + "value": "bgImg", + "tips": "背景图片", + "icon": "icon-beijingtupian" + } + ] + }, + "backgroundSize": { + "title": "尺寸", + "dataList": [ + { + "value": "default", + "tips": "默认", + "title": "默认" + }, + { + "value": "contain", + "tips": "等比填充contain", + "title": "等比填充" + }, + { + "value": "cover", + "tips": "等比覆盖cover", + "title": "等比覆盖" + } + ] + }, + "backgroundPosition": { + "title": "定位", + "dataList": [ + { "title": "leftTop", "position": "0 0", "icon": "icon-zuoshangjiao" }, + { "title": "midTop", "position": "50% 0", "icon": "icon-dingbu" }, + { "title": "rightTop", "position": "100% 0", "icon": "icon-youshangjiao" }, + { "title": "leftMid", "position": "0 50%", "icon": "icon-kaozuo" }, + { "title": "mid", "position": "50% 50%", "icon": "icon--quanbubiankuang" }, + { "title": "rightMid", "position": "100% 50%", "icon": "icon-kaoyou" }, + { "title": "leftBottom", "position": "0 100%", "icon": "icon-zuoxiajiao" }, + { "title": "midBottom", "position": "50% 100%", "icon": "icon-dibu" }, + { "title": "rightBottom", "position": "100% 100%", "icon": "icon-youxiajiao" } + ] + }, + "backgroundRepeat": { + "title": "重复显示", + "dataList": [ + { + "value": "repeat", + "tips": "垂直方向和水平方向重复 repeat", + "icon": "icon-jiugongge" + }, + { + "value": "repeat-x", + "tips": "水平方向重复 reapeat-x", + "icon": "icon-hengxiangpingfen" + }, + { + "value": "repeat-y", + "tips": "垂直方向重复 repeat-y", + "icon": "icon-zongxiangpingfen" + }, + { + "value": "no-repeat", + "tips": "不重复 no-repeat", + "icon": "icon-shanchu2" + } + ] + } +} diff --git a/src/setter/style-setter/pro/border/index.less b/src/setter/style-setter/pro/border/index.less index 6a5c6db..6f17c89 100644 --- a/src/setter/style-setter/pro/border/index.less +++ b/src/setter/style-setter/pro/border/index.less @@ -53,3 +53,61 @@ } } } +.shadow-container { + .shadow-color-container{ + margin-left: 70px; + .shadow-color-title{ + margin-right: 16px; + } + } + .shadow-size-container { + display: flex; + flex-direction: row; + margin-top: 10px; + margin-bottom: 10px; + margin-left: 30px; + .shadow-size-x { + display: flex; + flex-direction: row; + align-items: center; + .shadow-size-x-title { + width: 15px; + margin-right: 4px; + } + } + .shadow-size-y { + display: flex; + flex-direction: row; + align-items: center; + .shadow-size-y-title { + width: 18px; + margin-left: 9px; + } + } + } + .shadow-config-container { + display: flex; + flex-direction: row; + margin-top: 10px; + margin-bottom: 10px; + margin-left: 20px; + .shadow-blur-container { + display: flex; + flex-direction: row; + align-items: center; + .shadow-blur-container-title { + width: 24px; + margin-right: 4px; + } + } + .shadow-extend-container { + display: flex; + flex-direction: row; + align-items: center; + .shadow-extend-container-title { + margin-right: 4px; + width: 24px; + } + } + } +} diff --git a/src/setter/style-setter/pro/border/index.tsx b/src/setter/style-setter/pro/border/index.tsx index 3e61b70..95c1949 100644 --- a/src/setter/style-setter/pro/border/index.tsx +++ b/src/setter/style-setter/pro/border/index.tsx @@ -6,22 +6,37 @@ import Number from '../../components/number'; import ColorInput from '../../components/color-input'; import { StyleData, onStyleChange } from '../../utils/types'; import { Collapse, Range, Select } from '@alifd/next'; -import fontConfig from './config.json'; -import { addUnit, removeUnit } from '../../utils'; +import { intlLocal } from './locale'; +import { addUnit, removeUnit, unifyStyle } from '../../utils'; import './index.less'; const Option = Select.Option; const Panel = Collapse.Panel; +const borderConfig = intlLocal(); + const BORDER_MAX = 30; +enum BorderRadiusType { + fixedBorder = "fixedBorder", + partBorder = "partBorder", +} + const BorderDirectionMap = { borderLeft: 'borderLeft', borderRight: 'borderRight', borderTop: 'borderTop', borderBottom: 'borderBottom', - border: 'border', + // border:'border' }; +const borderRadiusMap = { + borderRadius:'borderRadius', + borderTopLeftRadius:'borderTopLeftRadius', + borderTopRightRadius:'borderTopRightRadius', + borderBottomLeftRadius:'borderBottomLeftRadius', + borderBottomRightRadius:'borderBottomRightRadius', +} + interface fontProps { styleData: StyleData | any; onStyleChange?: onStyleChange; @@ -29,52 +44,77 @@ interface fontProps { } export default (props: fontProps) => { const { styleData, onStyleChange, unit } = props; - const { borderType, borderStyle } = fontConfig; + const { borderType, borderStyle, shadowType} = borderConfig; const [selBorderType, setSelBorderType] = useState(null); const [borderDirection, setBorderDirection] = useState(null); - + const [shadow, setShadow] = useState('') useEffect(() => { if (!borderDirection) { for (let key in styleData) { for (let borderDirectionKey in BorderDirectionMap) { if (key.indexOf(borderDirectionKey) >= 0) { setBorderDirection(borderDirectionKey); - return; + break; + } + if (styleData['border']){ + setBorderDirection('border'); + break; } } + } + } - // if (key.indexOf(BorderDirectionMap.borderLeft)>=0){ - // setBorderDirection('borderLeft'); - // return; - // }else if (key.indexOf('borderRight')>=0){ - // setBorderDirection('borderRight'); - // return; - // }else if (key.indexOf('borderTop')>=0){ - // setBorderDirection('borderTop'); - // return; - // }else if (key.indexOf('borderBottom')>=0){ - // setBorderDirection('borderBottom'); - // return; - // }else if (key.indexOf('border')>=0){ - // setBorderDirection('border'); - // return; - // } + // 判断圆角类型 + if (styleData[borderRadiusMap.borderRadius]){ + setSelBorderType(BorderRadiusType.fixedBorder); + }else if (styleData[borderRadiusMap.borderBottomLeftRadius] || styleData[borderRadiusMap.borderBottomRightRadius] || styleData[borderRadiusMap.borderTopLeftRadius] || styleData[borderRadiusMap.borderTopRightRadius]){ + setSelBorderType(BorderRadiusType.partBorder); + } + // 初始绑定样式 + if(styleData['boxShadow']){ + const bgSizeArray = unifyStyle(styleData['boxShadow'])?.split(' ') + if(bgSizeArray?.[0]==='inset'){ + setShadow('insetShadow') + }else{ + setShadow('outerShadow') } + }else{ + setShadow('') } }, [styleData]); const onChangeBorderType = (styleDataList: Array<StyleData>) => { if (styleDataList) { - setSelBorderType(styleDataList[0].value); + const styleKey = styleDataList[0].value; + setSelBorderType(styleKey); } }; const onRangeChange = (styleKey: string, value: string, unit?: string) => { + + // 需要清除partBorder的圆角设置,不然会冲突,容易遗漏 + onStyleChange([ { styleKey, value: unit ? addUnit(value, unit) : value, }, + { + styleKey:borderRadiusMap.borderBottomLeftRadius, + value: null + }, + { + styleKey:borderRadiusMap.borderBottomRightRadius, + value: null + }, + { + styleKey:borderRadiusMap.borderTopLeftRadius, + value: null + }, + { + styleKey:borderRadiusMap.borderTopRightRadius, + value: null + }, ]); }; @@ -82,6 +122,23 @@ export default (props: fontProps) => { setBorderDirection(styleKey); }; + const onPartBorderRadiusChange = (styleKey: string, value: number, unit: string,styleData:any) => { + let styleDataList = [ + { + styleKey, + value: unit ? addUnit(value, unit) : value, + }, + ]; + if (styleData['borderRadius']){ + styleDataList.push({ + styleKey:'borderRadius', + value:null + }) + } + onStyleChange(styleDataList); + } + + const onBorderTypeChange = (styleKey: string, value: string) => { onStyleChange([ { @@ -90,10 +147,51 @@ export default (props: fontProps) => { }, ]); }; - + const onBoxShadowChange = (styleKey: string, value: any, index?:number, unit?:string,shadowPosition?:string,isColor?:boolean) => { + const bgSizeArray = styleData[styleKey] + ? unifyStyle(styleData[styleKey])?.split(' ') + : ['0', '0', '0', '0', '#000']; + if(shadowPosition==='outerShadow'){ + if(bgSizeArray?.[0]==='inset'){ + bgSizeArray.shift() + } + }else if(shadowPosition==='insetShadow'){ + if(bgSizeArray?.[0]!=='inset'){ + bgSizeArray?.unshift('inset') + } + } + if(bgSizeArray?.[0]==='inset'){ + setShadow('insetShadow') + }else{ + setShadow('outerShadow') + } + let unifiedValue = value + if(!value&&isColor){ + unifiedValue = '#000' + } + if(unifiedValue===null||unifiedValue===undefined||!bgSizeArray) return + unifiedValue = unit? addUnit(unifiedValue,unit): String(unifiedValue) + if(index!==undefined&&index!==null){ + bgSizeArray[index] = unifiedValue + } + let curValue: String ='' + bgSizeArray.forEach((item)=>{ + curValue = curValue+item+' ' + }) + curValue=curValue.substring(0,curValue.length-1) + const styleDataList = [ + { + styleKey, + value:curValue + }, + ]; + onStyleChange(styleDataList); + } + //insetShadow会在第一位插入inset字符串,使所有阴影样式的序号+1 + const insetBoxShadowShift = shadow==='insetShadow' ? 1 : 0 return ( <Collapse defaultExpandedKeys={['0']}> - <Panel title="边框" className="border-style-container"> + <Panel title={borderConfig.title} className="border-style-container"> <Row title={borderType.title} dataList={borderType.dataList} @@ -114,7 +212,7 @@ export default (props: fontProps) => { <Number styleKey="borderRadius" - style={{ minWidth: '60px', marginLeft: '5px' }} + style={{ minWidth: '80px', marginLeft: '5px' }} {...props} max={BORDER_MAX} /> @@ -135,18 +233,20 @@ export default (props: fontProps) => { <Number max={BORDER_MAX} min={0} - styleKey="borderTopLeftRadius" + styleKey={borderRadiusMap.borderTopLeftRadius} {...props} - style={{ width: '60px' }} + style={{ width: '68px' }} + onChangeFunction = {(styleKey, val, unit)=>onPartBorderRadiusChange(styleKey, val, unit,styleData)} /> </div> <div className="row-item"> <Icon type="icon-radius-upright" className="radius-icon" /> <Number max={BORDER_MAX} - styleKey="borderTopRightRadius" + styleKey={borderRadiusMap.borderTopRightRadius} {...props} - style={{ width: '60px' }} + style={{ width: '68px' }} + onChangeFunction = {(styleKey, val, unit)=>onPartBorderRadiusChange(styleKey, val, unit,styleData)} /> </div> </Row> @@ -160,25 +260,27 @@ export default (props: fontProps) => { <Icon type="icon-radius-bottomleft" className="radius-icon" /> <Number max={BORDER_MAX} - styleKey="borderBottomLeftRadius" + styleKey={borderRadiusMap.borderBottomLeftRadius} {...props} - style={{ width: '60px' }} + style={{ width: '68px' }} + onChangeFunction = {(styleKey, val, unit)=>onPartBorderRadiusChange(styleKey, val, unit,styleData)} /> </div> <div className="row-item"> <Icon type="icon-radius-bottomright" className="radius-icon" /> <Number max={BORDER_MAX} - styleKey="borderBottomRightRadius" + styleKey={borderRadiusMap.borderBottomRightRadius} {...props} - style={{ width: '60px' }} + onChangeFunction = {(styleKey:string, val:number, unit:string)=>onPartBorderRadiusChange(styleKey, val, unit,styleData)} + style={{ width: '68px' }} /> </div> </Row> </> )} - <Row title={'边框'} styleKey="borderRadius" {...props}> + <Row title={borderConfig.title} styleKey="border" {...props}> <div className="border-container"> <div className="border-icon-container"> <div className="top-icon-container"> @@ -207,7 +309,7 @@ export default (props: fontProps) => { <div className={ - borderDirection === BorderDirectionMap.border + borderDirection === 'border' ? 'sel-icon icon-container' : 'icon-container' } @@ -268,6 +370,85 @@ export default (props: fontProps) => { </div> </div> </Row> + <Row + title={shadowType.title} + dataList={shadowType.dataList} + styleKey={'shadowType'} + {...props} + onStyleChange={(type)=>{ + onBoxShadowChange('boxShadow', type?.[0].value, undefined, undefined ,type?.[0].value ) + }} + value={shadow} + > + </Row> + <div className="shadow-container"> + <div className="shadow-color-container"> + <span className='shadow-color-title'>{borderConfig.shadowColor}</span> + <ColorInput + {...props} + color = {styleData['boxShadow']?.split(' ')?.[insetBoxShadowShift+4]} + onStyleChange = {(color)=>{ + onBoxShadowChange('boxShadow', color?.[0].value, insetBoxShadowShift+4,undefined,undefined,true ) + }} + /> + </div> + <div className="shadow-size-container"> + <div className="shadow-size-x"> + <span className="shadow-size-x-title">x</span> + <Number + style={{ marginRight: '4px' }} + styleKey="boxShadow" + {...props} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBoxShadowChange(styleKey, val, insetBoxShadowShift+0, unit ) + } + multiProp={insetBoxShadowShift+0} + defaultPlaceholder={'0'} + /> + </div> + <div className="shadow-size-y"> + <span className="shadow-size-y-title">y</span> + <Number + styleKey="boxShadow" + {...props} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBoxShadowChange(styleKey, val, insetBoxShadowShift+1, unit ) + } + multiProp={insetBoxShadowShift+1} + defaultPlaceholder={'0'} + /> + </div> + </div> + <div className="shadow-config-container"> + <div className="shadow-blur-container"> + <div className="shadow-blur-container-title">{borderConfig.blur}</div> + <Number + style={{ marginRight: '4px' }} + min={2} + styleKey="boxShadow" + {...props} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBoxShadowChange(styleKey, val, insetBoxShadowShift+2, unit ) + } + multiProp={insetBoxShadowShift+2} + defaultPlaceholder={'0'} + /> + </div> + <div className="shadow-extend-container"> + <div className="shadow-extend-container-title">{borderConfig.spread}</div> + <Number + styleKey="boxShadow" + min={3} + {...props} + onChangeFunction={(styleKey: string, val: number, unit: string) => + onBoxShadowChange(styleKey, val, insetBoxShadowShift+3, unit ) + } + multiProp={insetBoxShadowShift+3} + defaultPlaceholder={'0'} + /> + </div> + </div> + </div> </Panel> </Collapse> ); diff --git a/src/setter/style-setter/pro/border/locale/en-US.json b/src/setter/style-setter/pro/border/locale/en-US.json new file mode 100644 index 0000000..8d61acd --- /dev/null +++ b/src/setter/style-setter/pro/border/locale/en-US.json @@ -0,0 +1,63 @@ +{ + "title": "Border", + "shadowColor": "shadow color", + "blur": "blur", + "spread": "spread", + "borderType": { + "title": "radius", + "dataList": [ + { + "value": "fixedBorder", + "tips": "fixed border", + "icon": "icon-yuanjiaofangkuang" + }, + { + "value": "partBorder", + "tips": "part border", + "icon": "icon-yuanjiao" + } + ] + }, + + "shadowType": { + "title": "shadow", + "dataList": [ + { + "value": "outerShadow", + "tips": "outer shadow", + "icon": "icon-yinying" + }, + { + "value": "insetShadow", + "tips": "inset shadow", + "icon": "icon-yinying-outside" + } + ] + }, + + "borderStyle": { + "title": "border style", + "dataList": [ + { + "value": "none", + "tips": "none", + "icon": "icon-close" + }, + { + "value": "solid", + "tips": "solid", + "icon": "icon-shixian" + }, + { + "value": "dashed", + "tips": "dashed", + "icon": "icon-xuxian" + }, + { + "value": "dotted", + "tips": "dotted", + "icon": "icon-dianxian" + } + ] + } +} diff --git a/src/setter/style-setter/pro/border/locale/index.ts b/src/setter/style-setter/pro/border/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/pro/border/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/pro/border/config.json b/src/setter/style-setter/pro/border/locale/zh-CN.json similarity index 57% rename from src/setter/style-setter/pro/border/config.json rename to src/setter/style-setter/pro/border/locale/zh-CN.json index f4b26cd..bcafc08 100644 --- a/src/setter/style-setter/pro/border/config.json +++ b/src/setter/style-setter/pro/border/locale/zh-CN.json @@ -1,4 +1,8 @@ { + "title": "边框", + "shadowColor": "阴影颜色", + "blur": "模糊", + "spread": "扩展", "borderType": { "title": "圆角", "dataList": [ @@ -15,27 +19,43 @@ ] }, + "shadowType": { + "title": "阴影", + "dataList": [ + { + "value": "outerShadow", + "tips": "外阴影", + "icon": "icon-yinying" + }, + { + "value": "insetShadow", + "tips": "内阴影", + "icon": "icon-yinying-outside" + } + ] + }, + "borderStyle": { "title": "边框样式", "dataList": [ { "value": "none", - "tips": "无样式 none", + "tips": "无样式", "icon": "icon-close" }, { "value": "solid", - "tips": "实线 solid", + "tips": "实线", "icon": "icon-shixian" }, { "value": "dashed", - "tips": "虚线 dashed", + "tips": "虚线", "icon": "icon-xuxian" }, { "value": "dotted", - "tips": "点线 dotted", + "tips": "点线", "icon": "icon-dianxian" } ] diff --git a/src/setter/style-setter/pro/font/index.tsx b/src/setter/style-setter/pro/font/index.tsx index 153bc4b..593aff8 100644 --- a/src/setter/style-setter/pro/font/index.tsx +++ b/src/setter/style-setter/pro/font/index.tsx @@ -4,18 +4,33 @@ import Number from '../../components/number'; import { StyleData, onStyleChange } from '../../utils/types'; import { Collapse, NumberPicker, Select, Range } from '@alifd/next'; import ColorInput from '../../components/color-input'; -import fontConfig from './config.json'; +import { intlLocal } from './locale'; import { addUnit, isEmptyValue } from '../../utils'; import './index.less'; const Panel = Collapse.Panel; +const fontConfig = intlLocal(); + interface fontProps { styleData: StyleData | any; onStyleChange?: onStyleChange; + fontPropsConfig?: any; unit?: string; } export default (props: fontProps) => { - const { styleData, onStyleChange } = props; + const { styleData, onStyleChange, fontPropsConfig } = props; + const defaultFontPropsConfig = { + // display 展示列表 + fontFamilyList: [ + { value: 'Helvetica', label: 'Helvetica' }, + { value: 'Arial', label: 'Arial' }, + { value: 'serif', label: 'serif' }, + ], + }; + + // 配置合并 + const propsConfig = { ...defaultFontPropsConfig, ...fontPropsConfig }; + const { fontWeight, textAlign } = fontConfig; const onNumberChange = (styleKey: string, value: number, unit?: string) => { @@ -29,10 +44,10 @@ export default (props: fontProps) => { return ( <Collapse defaultExpandedKeys={['0']}> - <Panel title="文字" className="font-style-container"> + <Panel title={fontConfig.title} className="font-style-container"> <div className="inner-row-contaienr"> <div className="row-item"> - <span className="row-item-title">字号</span> + <span className="row-item-title">{fontConfig.fontSize}</span> <Number max={100} min={0} @@ -43,7 +58,7 @@ export default (props: fontProps) => { /> </div> <div className="row-item"> - <span className="row-item-title">行高</span> + <span className="row-item-title">{fontConfig.lineHeight}</span> <Number min={0} styleKey="lineHeight" @@ -54,7 +69,7 @@ export default (props: fontProps) => { </div> </div> - <Row title={'字重'} styleData={styleData} styleKey=""> + <Row title={fontConfig.fontWeight.title} styleData={styleData} styleKey=""> <Select dataSource={fontWeight.dataList} style={{ width: '100%' }} @@ -63,8 +78,17 @@ export default (props: fontProps) => { onChange={(val) => onStyleChange([{ styleKey: 'fontWeight', value: val }])} /> </Row> + <Row title={fontConfig.fontFamily} styleData={styleData} styleKey=""> + <Select + dataSource={propsConfig.fontFamilyList} + style={{ width: '100%' }} + value={styleData.fontFamily} + hasClear={true} + onChange={(val) => onStyleChange([{ styleKey: 'fontFamily', value: val }])} + /> + </Row> - <Row title={'文字颜色'} styleKey="" {...props}> + <Row title={fontConfig.color} styleKey="" {...props}> <ColorInput styleKey={'color'} {...props} inputWidth="100%"></ColorInput> </Row> @@ -75,7 +99,7 @@ export default (props: fontProps) => { {...props} /> - <Row title={'透明度'} styleKey="opacity" {...props}> + <Row title={fontConfig.opacity} styleKey="opacity" {...props}> <div className="opacity-container"> <Range style={{ marginRight: '7px' }} diff --git a/src/setter/style-setter/pro/font/locale/en-US.json b/src/setter/style-setter/pro/font/locale/en-US.json new file mode 100644 index 0000000..705b77b --- /dev/null +++ b/src/setter/style-setter/pro/font/locale/en-US.json @@ -0,0 +1,75 @@ +{ + "title": "Font", + "fontSize": "fontSize", + "lineHeight": "lineHeight", + "fontFamily": "font family", + "color": "font color", + "opacity": "opacity", + "fontWeight": { + "title": "font weight", + "dataList": [ + { + "value": 100, + "label": "100 Thin" + }, + { + "value": 200, + "label": "200 Extra Light" + }, + { + "value": 300, + "label": "300 Light" + }, + + { + "value": 400, + "label": "400 Normal" + }, + { + "value": 500, + "label": "500 Medium" + }, + { + "value": 600, + "label": "600 Semi Bold" + }, + { + "value": 700, + "label": "700 Bold" + }, + { + "value": 800, + "label": "Extra Bold" + }, + { + "value": 900, + "label": "Black" + } + ] + }, + "textAlign": { + "title": "text align", + "dataList": [ + { + "value": "left", + "tips": "left", + "icon": "icon-align-left" + }, + { + "value": "center", + "tips": "center", + "icon": "icon-align-middle" + }, + { + "value": "right", + "tips": "right", + "icon": "icon-align-right" + }, + { + "value": "justify", + "tips": "justify", + "icon": "icon-justify" + } + ] + } +} diff --git a/src/setter/style-setter/pro/font/locale/index.ts b/src/setter/style-setter/pro/font/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/pro/font/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/pro/font/config.json b/src/setter/style-setter/pro/font/locale/zh-CN.json similarity index 79% rename from src/setter/style-setter/pro/font/config.json rename to src/setter/style-setter/pro/font/locale/zh-CN.json index 95cfd6c..668c9ba 100644 --- a/src/setter/style-setter/pro/font/config.json +++ b/src/setter/style-setter/pro/font/locale/zh-CN.json @@ -1,4 +1,10 @@ { + "title": "文字", + "fontSize": "字号", + "lineHeight": "行高", + "fontFamily": "字体", + "color": "文字颜色", + "opacity": "透明度", "fontWeight": { "title": "字重", "dataList": [ @@ -41,28 +47,27 @@ } ] }, - "textAlign": { "title": "对齐", "dataList": [ { "value": "left", - "tips": "左对齐 left", + "tips": "左对齐", "icon": "icon-align-left" }, { "value": "center", - "tips": "居中对齐 center", + "tips": "居中对齐", "icon": "icon-align-middle" }, { "value": "right", - "tips": "右对齐 center", + "tips": "右对齐", "icon": "icon-align-right" }, { "value": "justify", - "tips": "两端对齐 justify", + "tips": "两端对齐", "icon": "icon-justify" } ] diff --git a/src/setter/style-setter/pro/layout/LayoutInput.tsx b/src/setter/style-setter/pro/layout/LayoutInput.tsx new file mode 100644 index 0000000..34dade9 --- /dev/null +++ b/src/setter/style-setter/pro/layout/LayoutInput.tsx @@ -0,0 +1,14 @@ +import { Input } from '@alifd/next'; +import { InputProps } from '@alifd/next/types/input'; +import React, { FC } from 'react'; +import { isCssVarBind } from '../../utils'; + +const LayoutInput: FC<InputProps> = (props) => { + const { value } = props; + if (isCssVarBind(value)) { + return null + } + return <Input {...props} />; +}; + +export default LayoutInput; diff --git a/src/setter/style-setter/pro/layout/index.less b/src/setter/style-setter/pro/layout/index.less index e8315ea..9bfc217 100644 --- a/src/setter/style-setter/pro/layout/index.less +++ b/src/setter/style-setter/pro/layout/index.less @@ -1,3 +1,5 @@ +@bindColor: #bcd1fa; + .layout-style-container { .inner-row-contaienr { display: flex; @@ -11,8 +13,8 @@ } .row-item-title { - width: 50px; - margin-right: 10px; + width: 38px; + margin-right: 3px; } } @@ -44,6 +46,10 @@ -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-top: 20px solid @bindColor; + } + .next-input.next-medium { position: absolute; left: 0; @@ -77,6 +83,9 @@ border-right: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-right: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; top: 0; @@ -116,6 +125,9 @@ border-bottom: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-bottom: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; left: 0; @@ -147,6 +159,9 @@ border-left: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-left: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; top: 0; @@ -186,6 +201,9 @@ border-top: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-top: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; left: 0; @@ -219,6 +237,9 @@ border-bottom: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-bottom: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; left: 0; @@ -252,6 +273,9 @@ border-right: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-right: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; top: 0; @@ -291,6 +315,9 @@ border-left: 20px solid #d6e4ff; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; + &.css-var-bind { + border-left: 20px solid @bindColor; + } .next-input.next-medium { position: absolute; top: 0; @@ -319,3 +346,12 @@ } } } + + +.ext-css-layout-input { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} diff --git a/src/setter/style-setter/pro/layout/index.tsx b/src/setter/style-setter/pro/layout/index.tsx index 9f298cf..06b4ad2 100644 --- a/src/setter/style-setter/pro/layout/index.tsx +++ b/src/setter/style-setter/pro/layout/index.tsx @@ -4,26 +4,45 @@ import LayoutBox from './layoutBox'; import { Collapse } from '@alifd/next'; import Number from '../../components/number'; import { StyleData, onStyleChange } from '../../utils/types'; -import layoutConfig from './config.json'; +import { intlLocal } from './locale'; const Panel = Collapse.Panel; interface layoutProps { styleData: StyleData | any; onStyleChange?: onStyleChange; + layoutPropsConfig?: any; + unit?: string } +const layoutConfig = intlLocal(); + +const defaultLayoutPropsConfig = { + // display 展示列表 + showDisPlayList: ['inline', 'flex', 'block', 'inline-block', 'none'], + isShowPadding: true, + isShowMargin: true, + isShowWidthHeight: true, +}; + export default (props: layoutProps) => { + const { onStyleChange, styleData, layoutPropsConfig, unit } = props; + + // 配置合并 + const propsConfig = { ...defaultLayoutPropsConfig, ...layoutPropsConfig }; + + // 传入配置 + const { showDisPlayList, isShowWidthHeight } = propsConfig; + // 静态配置 const { display, flexDirection, justifyContent, alignItems, flexWrap } = layoutConfig; - // const onExpand = (expandedKeys: Array<any>) => { - // getVariableValue; - // }; + const displayDataList = display.dataList.filter( + (item) => showDisPlayList.indexOf(item.value) >= 0, + ); - const { onStyleChange, styleData } = props; return ( <Collapse defaultExpandedKeys={['0']}> - <Panel title="布局" className="layout-style-container"> - <Row title={display.title} dataList={display.dataList} styleKey="display" {...props}></Row> + <Panel title={layoutConfig.title} className="layout-style-container"> + <Row title={display.title} dataList={displayDataList} styleKey="display" {...props} longTitle={true}></Row> {styleData['display'] === 'flex' && ( <> @@ -31,53 +50,66 @@ export default (props: layoutProps) => { title={flexDirection.title} dataList={flexDirection.dataList} styleKey="flexDirection" + longTitle={true} {...props} /> <Row title={justifyContent.title} dataList={justifyContent.dataList} styleKey="justifyContent" + longTitle={true} {...props} /> <Row title={alignItems.title} dataList={alignItems.dataList} styleKey="alignItems" + longTitle={true} {...props} /> <Row title={flexWrap.title} dataList={flexWrap.dataList} styleKey="flexWrap" + longTitle={true} {...props} /> </> )} - <LayoutBox styleData={styleData} onStyleChange={onStyleChange} /> + <LayoutBox + styleData={styleData} + onStyleChange={onStyleChange} + layoutPropsConfig={propsConfig} + unit={unit} + /> - <div className="inner-row-contaienr"> - <div className="row-item"> - <span className="row-item-title">宽度</span> - <Number - style={{ marginRight: '10px', width: '100%' }} - min={0} - styleKey="width" - {...props} - useComputedStyle={true} - /> - </div> - <div className="row-item"> - <span className="row-item-title">高度</span> - <Number - styleKey="height" - min={0} - {...props} - style={{ width: '100%' }} - useComputedStyle={true} - /> + {isShowWidthHeight && ( + <div className="inner-row-contaienr"> + <div className="row-item"> + <span className="row-item-title">{layoutConfig.width}</span> + <Number + style={{ marginRight: '10px', width: '100%' }} + min={0} + styleKey="width" + {...props} + unit = {['px','%']} + useComputedStyle={true} + /> + </div> + <div className="row-item"> + <span className="row-item-title">{layoutConfig.height}</span> + <Number + styleKey="height" + min={0} + {...props} + style={{ width: '100%' }} + unit = {['px','%']} + useComputedStyle={true} + /> + </div> </div> - </div> + )} </Panel> </Collapse> ); diff --git a/src/setter/style-setter/pro/layout/layoutBox.tsx b/src/setter/style-setter/pro/layout/layoutBox.tsx index 705ab20..38c9b1d 100644 --- a/src/setter/style-setter/pro/layout/layoutBox.tsx +++ b/src/setter/style-setter/pro/layout/layoutBox.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; +import cls from 'classnames'; import './index.less'; -import { Input } from '@alifd/next'; import { StyleData, onStyleChange } from '../../utils/types'; -import { addUnit, removeUnit } from '../../utils'; +import { addUnit, isCssVarBind, removeUnit as originRemoveUnit } from '../../utils'; +import LayoutInput from './LayoutInput'; const KEY_ARROW_DOWN = 'ArrowDown'; const KEY_ARROW_UP = 'ArrowUp'; @@ -10,11 +11,20 @@ const KEY_ARROW_UP = 'ArrowUp'; interface layoutBoxProps { styleData: StyleData | any; onStyleChange: onStyleChange; - unit?: 'px'; + unit?: string; + layoutPropsConfig: any; +} + +const removeUnit = (value: any) => { + if (isCssVarBind(value)) { + return value; + } + return originRemoveUnit(value); } export default (props: layoutBoxProps) => { - const { onStyleChange, unit = 'px', styleData } = props; + const { onStyleChange, unit = 'px', styleData, layoutPropsConfig } = props; + const onInputChange = (styleKey: string, value: string) => { if (value != '') { // 判断是否是数字 @@ -58,88 +68,97 @@ export default (props: layoutBoxProps) => { return ( <div className="layout-box-container"> - <div className="margin-top-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['marginTop'])} - onChange={(val) => onInputChange('marginTop', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'marginTop')} - ></Input> - </div> - <div className="margin-right-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['marginRight'])} - onChange={(val) => onInputChange('marginRight', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'marginRight')} - ></Input> - </div> - <div className="margin-bottom-div"> - <span className="help-txt">MARGIN</span> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['marginBottom'])} - onChange={(val) => onInputChange('marginBottom', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'marginBottom')} - ></Input> - </div> - <div className="margin-left-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['marginLeft'])} - onChange={(val) => onInputChange('marginLeft', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'marginLeft')} - ></Input> - </div> - <div className="padding-top-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['paddingTop'])} - onChange={(val) => onInputChange('paddingTop', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'paddingTop')} - ></Input> - </div> - <div className="padding-right-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['paddingRight'])} - onChange={(val) => onInputChange('paddingRight', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'paddingRight')} - ></Input> - </div> - <div className="padding-bottom-div"> - <span className="help-txt">PADDING</span> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['paddingBottom'])} - onChange={(val) => onInputChange('paddingBottom', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'paddingBottom')} - ></Input> - </div> - <div className="padding-left-div"> - <Input - className="next-noborder" - placeholder="0" - maxLength={3} - value={removeUnit(styleData['paddingLeft'])} - onChange={(val) => onInputChange('paddingLeft', val)} - onKeyDown={(e) => onInputKeyDown(e.key, 'paddingLeft')} - ></Input> - </div> + {layoutPropsConfig.isShowMargin && ( + <> + <div className={cls("margin-top-div", isCssVarBind(styleData.marginTop) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.marginTop)} + onChange={(val) => onInputChange('marginTop', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'marginTop')} + /> + </div> + <div className={cls("margin-right-div", isCssVarBind(styleData.marginBottom) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.marginRight)} + onChange={(val) => onInputChange('marginRight', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'marginRight')} + /> + </div> + <div className={cls("margin-bottom-div", isCssVarBind(styleData.marginBottom) && 'css-var-bind')}> + <span className="help-txt">MARGIN</span> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.marginBottom)} + onChange={(val) => onInputChange('marginBottom', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'marginBottom')} + /> + </div> + <div className={cls("margin-left-div", isCssVarBind(styleData.marginLeft) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.marginLeft)} + onChange={(val) => onInputChange('marginLeft', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'marginLeft')} + /> + </div> + </> + )} + + {layoutPropsConfig.isShowPadding && ( + <> + <div className={cls("padding-top-div", isCssVarBind(styleData.paddingTop) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.paddingTop)} + onChange={(val) => onInputChange('paddingTop', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'paddingTop')} + /> + </div> + <div className={cls("padding-right-div", isCssVarBind(styleData.paddingRight) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.paddingRight)} + onChange={(val) => onInputChange('paddingRight', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'paddingRight')} + /> + </div> + <div className={cls("padding-bottom-div", isCssVarBind(styleData.paddingBottom) && 'css-var-bind')}> + <span className="help-txt">PADDING</span> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.paddingBottom)} + onChange={(val) => onInputChange('paddingBottom', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'paddingBottom')} + /> + </div> + <div className={cls("padding-left-div", isCssVarBind(styleData.paddingLeft) && 'css-var-bind')}> + <LayoutInput + className="next-noborder" + placeholder="0" + maxLength={3} + value={removeUnit(styleData.paddingLeft)} + onChange={(val) => onInputChange('paddingLeft', val)} + onKeyDown={(e) => onInputKeyDown(e.key, 'paddingLeft')} + /> + </div> + </> + )} </div> ); }; diff --git a/src/setter/style-setter/pro/layout/locale/en-US.json b/src/setter/style-setter/pro/layout/locale/en-US.json new file mode 100644 index 0000000..565327e --- /dev/null +++ b/src/setter/style-setter/pro/layout/locale/en-US.json @@ -0,0 +1,140 @@ +{ + "title": "Layout", + "width": "width", + "height": "height", + "display": { + "title": "display", + "dataList": [ + { + "value": "inline", + "tips": "inline", + "icon": "icon-a-Displayinline" + }, + { + "value": "flex", + "tips": "flex", + "icon": "icon-a-Displayflex" + }, + { + "value": "block", + "tips": "block", + "icon": "icon-a-Displayblock" + }, + { + "value": "inline-block", + "tips": "inline-block", + "icon": "icon-a-Displayinline-block" + }, + { + "value": "none", + "tips": "none", + "icon": "icon-yincang" + } + ] + }, + "flexDirection": { + "title": "flex direction", + "dataList": [ + { + "value": "row", + "tips": "row", + "icon": "icon-a-Directionrow" + }, + { + "value": "row-reverse", + "tips": "row-reverse", + "icon": "icon-a-Directionrow-reverse" + }, + { + "value": "column", + "tips": "column", + "icon": "icon-a-Directioncolumn" + }, + { + "value": "column-reverse", + "tips": "column-reverse", + "icon": "icon-a-Directioncolumn-reverse" + } + ] + }, + "justifyContent": { + "title": "justify content", + "dataList": [ + { + "value": "flex-start", + "tips": "flex-start", + "icon": "icon-a-Justifyflex-startrow" + }, + { + "value": "flex-end", + "tips": "flex-end", + "icon": "icon-a-Justifyflex-endrow" + }, + { + "value": "center", + "tips": "center", + "icon": "icon-a-Justifycenterrow" + }, + { + "value": "space-between", + "tips": "space-between", + "icon": "icon-a-Justifyspace-betweenrow" + }, + { + "value": "space-around", + "tips": "space-around", + "icon": "icon-a-Justifyspace-aroundrow" + } + ] + }, + "alignItems": { + "title": "align items", + "dataList": [ + { + "value": "flex-start", + "tips": "flex-start", + "icon": "icon-a-Alignflex-startrow" + }, + { + "value": "flex-end", + "tips": "flex-end", + "icon": "icon-a-Alignflex-endrow" + }, + { + "value": "center", + "tips": "center", + "icon": "icon-a-Aligncenterrow" + }, + { + "value": "baseline", + "tips": "baselin", + "icon": "icon-a-Alignbaselinerow" + }, + { + "value": "stretch", + "tips": "stretch", + "icon": "icon-a-Alignstretchrow" + } + ] + }, + "flexWrap": { + "title": "flexWrap", + "dataList": [ + { + "value": "nowrap", + "tips": "nowrap", + "title": "nowrap" + }, + { + "value": "wrap", + "tips": "wrap", + "title": "wrap" + }, + { + "value": "wrap-reverse", + "tips": "wrap-reverse", + "title": "wrap-reverse" + } + ] + } +} diff --git a/src/setter/style-setter/pro/layout/locale/index.ts b/src/setter/style-setter/pro/layout/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/pro/layout/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/pro/layout/config.json b/src/setter/style-setter/pro/layout/locale/zh-CN.json similarity index 69% rename from src/setter/style-setter/pro/layout/config.json rename to src/setter/style-setter/pro/layout/locale/zh-CN.json index f892272..d01d3dc 100644 --- a/src/setter/style-setter/pro/layout/config.json +++ b/src/setter/style-setter/pro/layout/locale/zh-CN.json @@ -1,30 +1,33 @@ { + "title": "布局", + "width": "宽度", + "height": "高度", "display": { "title": "布局模式", "dataList": [ { "value": "inline", - "tips": "内联布局 inline", + "tips": "内联布局", "icon": "icon-a-Displayinline" }, { "value": "flex", - "tips": "弹性布局 flex", + "tips": "弹性布局", "icon": "icon-a-Displayflex" }, { "value": "block", - "tips": "块级布局 block", + "tips": "块级布局", "icon": "icon-a-Displayblock" }, { "value": "inline-block", - "tips": "内联块布局 inline-block", + "tips": "内联块布局", "icon": "icon-a-Displayinline-block" }, { "value": "none", - "tips": "隐藏 none", + "tips": "隐藏", "icon": "icon-yincang" } ] @@ -34,22 +37,22 @@ "dataList": [ { "value": "row", - "tips": "水平方向,起点在左侧 row", + "tips": "水平方向,起点在左侧", "icon": "icon-a-Directionrow" }, { "value": "row-reverse", - "tips": "水平方向,起点在右侧 row-reverse", + "tips": "水平方向,起点在右侧", "icon": "icon-a-Directionrow-reverse" }, { "value": "column", - "tips": "垂直方向,起点在上沿 column", + "tips": "垂直方向,起点在上沿", "icon": "icon-a-Directioncolumn" }, { "value": "column-reverse", - "tips": "垂直方向,起点在下沿 column-reverse", + "tips": "垂直方向,起点在下沿", "icon": "icon-a-Directioncolumn-reverse" } ] @@ -59,27 +62,27 @@ "dataList": [ { "value": "flex-start", - "tips": "左对齐 flex-start", + "tips": "左对齐", "icon": "icon-a-Justifyflex-startrow" }, { "value": "flex-end", - "tips": "右对齐 flex-end", + "tips": "右对齐", "icon": "icon-a-Justifyflex-endrow" }, { "value": "center", - "tips": "水平居中 center", + "tips": "水平居中", "icon": "icon-a-Justifycenterrow" }, { "value": "space-between", - "tips": "两端对齐 space-between", + "tips": "两端对齐", "icon": "icon-a-Justifyspace-betweenrow" }, { "value": "space-around", - "tips": "横向平分 space-around", + "tips": "横向平分", "icon": "icon-a-Justifyspace-aroundrow" } ] @@ -89,27 +92,27 @@ "dataList": [ { "value": "flex-start", - "tips": "起点对齐 flex-start", + "tips": "起点对齐", "icon": "icon-a-Alignflex-startrow" }, { "value": "flex-end", - "tips": "终点对齐 flex-end", + "tips": "终点对齐", "icon": "icon-a-Alignflex-endrow" }, { "value": "center", - "tips": "水平居中 center", + "tips": "水平居中", "icon": "icon-a-Aligncenterrow" }, { "value": "baseline", - "tips": "项目第一行文字的基线对齐 baseline", + "tips": "项目第一行文字的基线对齐", "icon": "icon-a-Alignbaselinerow" }, { "value": "stretch", - "tips": "沾满整个容器的高度 stretch", + "tips": "沾满整个容器的高度", "icon": "icon-a-Alignstretchrow" } ] @@ -119,17 +122,17 @@ "dataList": [ { "value": "nowrap", - "tips": "不换行 nowrap", + "tips": "不换行", "title": "不换行" }, { "value": "wrap", - "tips": "第一行在上方 wrap", + "tips": "第一行在上方", "title": "正换行" }, { "value": "wrap-reverse", - "tips": "第一行在下方 wrap-reverse", + "tips": "第一行在下方", "title": "逆换行" } ] diff --git a/src/setter/style-setter/pro/position/index.tsx b/src/setter/style-setter/pro/position/index.tsx index e274a16..5ca8d8d 100644 --- a/src/setter/style-setter/pro/position/index.tsx +++ b/src/setter/style-setter/pro/position/index.tsx @@ -4,18 +4,25 @@ import { Collapse, NumberPicker, Select } from '@alifd/next'; import { useEffect } from 'react'; import PositionBox from '../position/positionBox'; import { StyleData, onStyleChange } from '../../utils/types'; -import positionConfig from './config.json'; -const Panel = Collapse.Panel; +import { intlLocal } from './locale'; +import { isCssVarBind } from '../../utils'; + +const {Panel} = Collapse; interface layoutProps { styleData: StyleData | any; onStyleChange?: onStyleChange; + positionPropsConfig?: any; } +const positionConfig = intlLocal(); + export default (props: layoutProps) => { const { float, clear, position } = positionConfig; - const { onStyleChange, styleData } = props; + const { onStyleChange, styleData, positionPropsConfig } = props; + + const { isShowFloat, isShowClear } = positionPropsConfig; const onZIndexChange = (zIndex: number) => { onStyleChange([{ styleKey: 'zIndex', value: zIndex }]); @@ -29,43 +36,48 @@ export default (props: layoutProps) => { return ( <Collapse defaultExpandedKeys={['0']}> - <Panel title="位置"> + <Panel title={positionConfig.title}> <Row title={position.title} styleData={styleData} styleKey="position"> <Select dataSource={position.dataList} value={styleData.position} - hasClear={true} + hasClear onChange={(val) => onStyleChange([{ styleKey: 'position', value: val }])} /> </Row> - {styleData['position'] && styleData['position'] != 'static' && ( + {styleData.position && styleData.position != 'static' && ( <PositionBox styleData={styleData} onStyleChange={onStyleChange} {...props} /> )} - <Row title={'层叠顺序'} styleData={styleData} styleKey="zIndex"> + <Row title={'zIndex'} styleData={styleData} styleKey="zIndex"> <NumberPicker + disabled={isCssVarBind(styleData.zIndex)} step={1} precision={2} onChange={onZIndexChange} - value={styleData['zIndex']} + value={styleData.zIndex} /> </Row> - <Row - title={float.title} - dataList={float.dataList} - onStyleChange={onStyleChange} - styleData={styleData} - styleKey="float" - /> - <Row - title={clear.title} - dataList={clear.dataList} - onStyleChange={onStyleChange} - styleData={styleData} - styleKey="clear" - /> + {isShowFloat && ( + <Row + title={float.title} + dataList={float.dataList} + onStyleChange={onStyleChange} + styleData={styleData} + styleKey="float" + /> + )} + {isShowClear && ( + <Row + title={clear.title} + dataList={clear.dataList} + onStyleChange={onStyleChange} + styleData={styleData} + styleKey="clear" + /> + )} </Panel> </Collapse> ); diff --git a/src/setter/style-setter/pro/position/locale/en-US.json b/src/setter/style-setter/pro/position/locale/en-US.json new file mode 100644 index 0000000..73dfda0 --- /dev/null +++ b/src/setter/style-setter/pro/position/locale/en-US.json @@ -0,0 +1,103 @@ +{ + "title": "Position", + "position": { + "title": "position", + "dataList": [ + { + "value": "static", + "tips": "static", + "icon": "icon-position" + }, + { + "value": "relative", + "tips": "relative", + "icon": "icon-relative" + }, + { + "value": "absolute", + "tips": "absolute", + "icon": "icon-absolute" + }, + { + "value": "fixed", + "tips": "fixed", + "icon": "icon-fixed" + }, + { + "value": "sticky", + "tips": "sticky", + "icon": "icon-Objectpositioninterface" + } + ] + }, + "float": { + "title": "float", + "dataList": [ + { + "value": "none", + "tips": "none", + "icon": "icon-close" + }, + { + "value": "left", + "tips": "left", + "icon": "icon-formatfloatleft" + }, + { + "value": "right", + "tips": "right", + "icon": "icon-formatfloatright" + } + ] + }, + "clear": { + "title": "clear", + "dataList": [ + { + "value": "none", + "tips": "none", + "icon": "icon-close" + }, + { + "value": "left", + "tips": "left", + "icon": "icon-qingchuzuoce" + }, + { + "value": "right", + "tips": "right", + "icon": "icon-qingchuyouce" + }, + { + "value": "both", + "tips": "both", + "icon": "icon-qingchuliangce" + } + ] + }, + + "positionTemplete": { + "dataList": [ + { + "value": "topLeft", + "tips": "topLeft", + "icon": "icon-zuoshangcopy" + }, + { + "value": "topRight", + "tips": "topRight", + "icon": "icon-youshangcopy" + }, + { + "value": "bottomLeft", + "tips": "bottomLeft", + "icon": "icon-zuoxiacopy" + }, + { + "value": "bottomRight", + "tips": "bottomRight", + "icon": "icon-youxiacopy" + } + ] + } +} diff --git a/src/setter/style-setter/pro/position/locale/index.ts b/src/setter/style-setter/pro/position/locale/index.ts new file mode 100644 index 0000000..2c5b1d0 --- /dev/null +++ b/src/setter/style-setter/pro/position/locale/index.ts @@ -0,0 +1,16 @@ +import { common } from '@alilc/lowcode-engine'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const intlLocal = () => { + const { getLocale } = common.utils.createIntl?.() || {}; + const locale: string = getLocale?.() || 'zh-CN'; + const localeSource: any = { + 'en-US': enUS, + 'zh-CN': zhCN, + }; + return localeSource[locale]; +} + + +export { intlLocal }; diff --git a/src/setter/style-setter/pro/position/config.json b/src/setter/style-setter/pro/position/locale/zh-CN.json similarity index 77% rename from src/setter/style-setter/pro/position/config.json rename to src/setter/style-setter/pro/position/locale/zh-CN.json index 5cf7345..3da449f 100644 --- a/src/setter/style-setter/pro/position/config.json +++ b/src/setter/style-setter/pro/position/locale/zh-CN.json @@ -1,30 +1,31 @@ { + "title": "位置", "position": { - "title": "定位类型", + "title": "定位", "dataList": [ { "value": "static", - "tips": "无定位 static", + "tips": "无定位", "icon": "icon-position" }, { "value": "relative", - "tips": "相对定位 relative", + "tips": "相对定位", "icon": "icon-relative" }, { "value": "absolute", - "tips": "绝对定位 absolute", + "tips": "绝对定位", "icon": "icon-absolute" }, { "value": "fixed", - "tips": "固定定位 fixed", + "tips": "固定定位", "icon": "icon-fixed" }, { "value": "sticky", - "tips": "粘性定位 sticky", + "tips": "粘性定位", "icon": "icon-Objectpositioninterface" } ] @@ -34,17 +35,17 @@ "dataList": [ { "value": "none", - "tips": "不浮动 none", + "tips": "不浮动", "icon": "icon-close" }, { "value": "left", - "tips": "左浮动 left", + "tips": "左浮动", "icon": "icon-formatfloatleft" }, { "value": "right", - "tips": "右浮动 right", + "tips": "右浮动", "icon": "icon-formatfloatright" } ] @@ -54,22 +55,22 @@ "dataList": [ { "value": "none", - "tips": "不清除 none", + "tips": "不清除", "icon": "icon-close" }, { "value": "left", - "tips": "左清除 left", + "tips": "左清除", "icon": "icon-qingchuzuoce" }, { "value": "right", - "tips": "右清除 right", + "tips": "右清除", "icon": "icon-qingchuyouce" }, { "value": "both", - "tips": "两边清除 both", + "tips": "两边清除", "icon": "icon-qingchuliangce" } ] diff --git a/src/setter/style-setter/pro/position/positionBox.tsx b/src/setter/style-setter/pro/position/positionBox.tsx index ad3633a..8848399 100644 --- a/src/setter/style-setter/pro/position/positionBox.tsx +++ b/src/setter/style-setter/pro/position/positionBox.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Input } from '@alifd/next'; import { StyleData, onStyleChange } from '../../utils/types'; -import positionConfig from './config.json'; +import { intlLocal } from './locale'; import Row from '../../components/row'; import { addUnit, removeUnit } from '../../utils'; import './index.less'; @@ -14,6 +14,8 @@ interface positionBoxProps { unit?: 'px'; } +const positionConfig = intlLocal(); + export default (props: positionBoxProps) => { const { onStyleChange, styleData, unit } = props; const { positionTemplete } = positionConfig; diff --git a/src/setter/style-setter/utils/index.tsx b/src/setter/style-setter/utils/index.tsx index 391b3d7..bb35d5e 100644 --- a/src/setter/style-setter/utils/index.tsx +++ b/src/setter/style-setter/utils/index.tsx @@ -64,7 +64,7 @@ export function toLine(styleKey: string) { } export function toHump(name: String) { - return name.replace(/\-(\w)/g, function (all, letter) { + return name.replace(/\-(\w)/g, (all, letter) => { return letter.toUpperCase(); }); } @@ -84,10 +84,10 @@ export function hexify(color: string) { const g = Math.floor(a * parseInt(values[1]) + (1 - a) * 255); const b = Math.floor(a * parseInt(values[2]) + (1 - a) * 255); return ( - '#' + - ('0' + r.toString(16)).slice(-2) + - ('0' + g.toString(16)).slice(-2) + - ('0' + b.toString(16)).slice(-2) + `#${ + (`0${ r.toString(16)}`).slice(-2) + }${(`0${ g.toString(16)}`).slice(-2) + }${(`0${ b.toString(16)}`).slice(-2)}` ); } @@ -124,3 +124,44 @@ export function parseToStyleData(cssCode: string) { return styleData; } +// 多参数样式解析单个参数 +export const parseValue = (styleValue: String, valueIndex: number) => { + if (!styleValue) return; + const styleArray = styleValue?.split(' ') || []; + const value = styleArray[valueIndex]; + const unifiedValue = unifyValue(value); + return unifiedValue === 'auto' ? null : unifiedValue; +}; + +// 去除一下单位 +export const unifyValue = (value: string) => { + if (/^-?\d+px$/.test(value)) { + return value.replace('px', ''); + } + return value; +}; + +// 规范手动输入的css样式 +export const unifyStyle = (value: string) => { + if (!value) return; + // 首部空格去除 + if (value.substring(0, 1) === ' ') { + value.replace(/\s+/, ''); + } + // 多属性间重复字符串去除 + value.replace(/\s+/g, ' '); + return value; +}; + +export const getUnit = (value: string) => { + if(typeof value !== 'string') return '' + if (value != undefined && value != null){ + return value.replace(/^-?[0-9]\d*/g, '') + } +} + +export function isCssVarBind(value: any) { + if (typeof value === 'string') { + return /var\(/.test(value); + } +} diff --git a/src/setter/title-setter/index.scss b/src/setter/title-setter/index.scss new file mode 100644 index 0000000..aba0faa --- /dev/null +++ b/src/setter/title-setter/index.scss @@ -0,0 +1,22 @@ +/* write style here */ +.setter-title { + width: 100%; + + > .next-input { + flex: auto; + } +} + +.next-input.next-small { + height: var(--form-element-small-height, 28px); + min-height: 28px; + border-radius: var(--form-element-small-corner, 3px); +} + +.next-input.next-small input { + height: calc(var(--form-element-small-height, 28px) - var(--input-border-width, 1px) * 2); + min-height: 26px; + line-height: calc(var(--form-element-small-height, 28px) - var(--input-border-width, 1px) * 2) \0; + padding: 0 var(--input-s-padding, 8px); + font-size: var(--form-element-small-font-size, 12px); +} diff --git a/src/setter/title-setter/index.tsx b/src/setter/title-setter/index.tsx new file mode 100644 index 0000000..caab6b2 --- /dev/null +++ b/src/setter/title-setter/index.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import { Switch, Input, Box } from '@alifd/next'; +import { SettingTarget } from '@alilc/lowcode-types'; + +import './index.scss'; + +interface TitleSetterProps { + field?: SettingTarget; + prop?: SettingTarget; + value: string; + defaultValue: string; + defaultChecked?: boolean; + onChange: Function; +} + +const TitleSetter = (props: TitleSetterProps) => { + const { value, defaultValue, defaultChecked, onChange, field, prop } = props; + const [checked, setChecked] = useState(defaultChecked); + const target = field || prop; + const theVal = target?.getHotValue?.() || defaultValue || value; + + const handleToggle = (vis: boolean) => { + onChange?.(vis ? theVal : ''); + setChecked(vis); + }; + const handleChangeText = (text: string) => { + onChange?.(text); + }; + + return ( + <Box className="setter-title" direction="row" align="center" spacing={10}> + <Switch size="small" checked={checked} onChange={handleToggle} /> + {checked && <Input size="small" value={theVal} onChange={handleChangeText} />} + </Box> + ); +}; + +export default TitleSetter; diff --git a/src/setter/variable-setter/index.tsx b/src/setter/variable-setter/index.tsx index a5092a6..84823f3 100644 --- a/src/setter/variable-setter/index.tsx +++ b/src/setter/variable-setter/index.tsx @@ -6,8 +6,9 @@ export default class SetterVariable extends PureComponent { static displayName = 'SetterVariable'; static isPopup = true; - static show({ prop: field }) { - event.emit('variableBindDialog.openDialog', { field }); + static show(params: any) { + const { prop: field, ...res } = params; + event.emit('variableBindDialog.openDialog', { field, ...res }); } render() {