feat: 组件面板支持业务组件独立展示

This commit is contained in:
金禅 2020-09-09 21:53:44 +08:00
parent 653dc7f9c8
commit e9d8d3dbe6
18 changed files with 1518 additions and 33 deletions

View File

@ -13107,30 +13107,6 @@
}
],
"componentList": [
{
"title": "Util",
"children": [
{
"componentName": "Dropdown",
"title": "下拉菜单",
"icon": "",
"package": "@alifd/next",
"library": "Next",
"snippets": [
{
"title": "下拉菜单",
"screenshot": "",
"schema": {
"componentName": "Dropdown",
"props": { "trigger": [{ "componentName": "Button", "props": { "type": "primary" }, "children": "确定" }], "triggerType": "click"},
"children": [{ "componentName": "Menu", "props": { "style": { "width": 200 } }, "children": [{ "componentName": "Menu.Item", "props": {}, "children": "Option 1" }, { "componentName": "Menu.Item", "props": { "disabled": false }, "children": "option 2" }, { "componentName": "Menu.Item", "props": { "disabled": false }, "children": "option 3" }]}]
}
}
]
}
],
"icon": ""
},
{
"title": "DataDisplay",
"children": [
@ -15388,5 +15364,31 @@
],
"icon": ""
}
],
"bizComponentList": [
{
"title": "Util",
"children": [
{
"componentName": "Dropdown",
"title": "下拉菜单",
"icon": "",
"package": "@alifd/next",
"library": "Next",
"snippets": [
{
"title": "下拉菜单",
"screenshot": "",
"schema": {
"componentName": "Dropdown",
"props": { "trigger": [{ "componentName": "Button", "props": { "type": "primary" }, "children": "确定" }], "triggerType": "click"},
"children": [{ "componentName": "Menu", "props": { "style": { "width": 200 } }, "children": [{ "componentName": "Menu.Item", "props": {}, "children": "Option 1" }, { "componentName": "Menu.Item", "props": { "disabled": false }, "children": "option 2" }, { "componentName": "Menu.Item", "props": { "disabled": false }, "children": "option 3" }]}]
}
}
]
}
],
"icon": ""
}
]
}

View File

@ -23,7 +23,6 @@
"@ali/lowcode-designer": "^1.0.8-0",
"@ali/lowcode-editor-core": "^1.0.8-0",
"@ali/lowcode-types": "^1.0.8-0",
"@ali/ve-component-list": "^1.1.1",
"@alifd/next": "^1.19.19",
"react": "^16.8.1"
},

View File

@ -0,0 +1,270 @@
import React from "react";
import { Search, Box } from "@alifd/next";
import Button from "../button/index.js";
import Card from "../card";
import $i18n from "../../i18n/index.js";
import { searchComponent, builtinSearchMap } from "../../utils";
import "./index.less";
/**
* 配置元素的操作类型
* Draggable可拖拽
* Clickable可点击
* All可拖拽也可点击
*/
export const AdditiveType = {
Draggable: "additive-drag",
Clickable: "additive-click",
All: "additive",
};
class Base extends React.Component {
static propTypes = {
metaData: PropTypes.array,
className: PropTypes.string,
registerAdditive: PropTypes.func,
renderCustomSnippet: PropTypes.func,
actions: PropTypes.array,
getComponentInfo: PropTypes.func,
enableSearch: PropTypes.bool,
enableCard: PropTypes.bool,
enableReport: PropTypes.bool,
placeholder: PropTypes.string,
};
static defaultProps = {
metaData: [],
registerAdditive: () => {
return;
},
className: "",
renderCustomSnippet: null,
actions: [],
getComponentInfo: null,
enableSearch: false,
enableCard: true,
enableReport: true,
placeholder: "",
};
state = {
selected: "",
searchText: "",
currentCard: null,
target: null,
currentCardImage: '',
};
shell = null;
isEmpty = false;
mode = 'advance';
searchMap = {};
timer = null;
isMouseEnterCard = false;
componentWillMount() {
// TODO get remote search map
this.searchMap = builtinSearchMap;
}
hasActions() {
const { actions } = this.props;
if (!actions || !Array.isArray(actions) || !actions.length) {
return false;
}
return true;
}
onSearch(val) {
this.setState({
searchText: val,
currentCard: null, // 清空卡片
});
}
normalizeBundle(mode) {
const { metaData } = this.props;
const { searchText = "" } = this.state;
const groupList = metaData.filter((comp, index) => {
const { title = "", componentName = "", id = "" } = comp;
if (!id) {
comp.id = `${comp.componentName}_${index}`;
}
const query = searchText.toLowerCase();
return (
!![title, componentName].find(
(it) => it.toLowerCase().indexOf(query) > -1
) || !!searchComponent(title, query, this.searchMap)
);
});
if (mode === 'simple') {
return groupList;
}
let bundle = {};
// 按一定顺序排列
groupList.forEach((m) => {
const c = m.category || "Others";
if (!bundle[c]) {
bundle[c] = [];
}
bundle[c].push(m);
});
return bundle;
}
renderEmptyData() {
this.isEmpty = true;
return (
<Box direction="column" justify="center" align="center" className="ve-component-list-empty">
<img src='//g.alicdn.com/uxcore/pic/empty.png' style={{ height: 100, width: 100 }} />
<div style={{ lineHeight: 2 }}>
<div>暂无组件请在物料站点添加</div>
</div>
</Box>
);
}
renderHeader() {
const { placeholder } = this.props;
return (
<Search
placeholder={
placeholder
? placeholder
: $i18n.get({
id: "trunkPaneSearchComponent",
dm: "搜索组件",
})
}
shape="simple"
size="medium"
hasClear
defaultValue={this.state.searchText}
onChange={this.onSearch.bind(this)}
onSearch={this.onSearch.bind(this)}
/>
);
}
renderActions() {
const { actions = [] } = this.props;
if (!this.hasActions()) {
return null;
}
const len = actions.length;
// TODO:len = 1只有一个主按钮len = 2一个主按钮、一个次按钮len >=3一个主按钮、一个次按钮、其余放在按钮组里
return actions.map((action, idx) => {
return (
<Button
key={idx}
action={action}
className="btn"
type={idx === 0 ? "primary" : "outline"}
>
{action.title}
</Button>
);
});
}
renderBundle() {
const { metaData } = this.props;
if (!metaData || !Array.isArray(metaData) || !metaData.length) {
return this.renderEmptyData();
}
const bundle = this.normalizeBundle(this.mode);
if (!Object.keys(bundle).length || (Array.isArray(bundle) && bundle.length === 0)) {
return this.renderEmptyData();
}
this.isEmpty = false;
return this.getBundle ? this.getBundle(bundle) : this.renderEmptyData();
}
renderCard() {
const { currentCard, target, currentCardImage } = this.state;
const { getComponentInfo } = this.props;
if (!currentCard || !getComponentInfo) return null;
return (
<Card
componentPrototype={currentCard}
target={target}
customImage={currentCardImage}
subTitle={target && target.innerText || ''}
position="right top"
visible={!!currentCard}
showClose
getComponentInfo={getComponentInfo}
onHide={() => {
this.setState({ currentCard: null });
this.isMouseEnterCard = false;
}}
offset={{
top: 10,
left: 10,
}}
onMouseEnter={() => {
this.isMouseEnterCard = true;
}}
/>
);
}
render() {
const {
enableSearch,
className,
registerAdditive = () => {
return;
},
} = this.props;
let bodyExtraClass = "";
if (this.hasActions() && enableSearch) {
bodyExtraClass = "small";
} else if (!this.hasActions() && enableSearch) {
bodyExtraClass = "medium";
} else if (this.hasActions() && !enableSearch) {
bodyExtraClass = "large";
} else {
bodyExtraClass = "";
}
return (
<div className={`ve-component-list ${className}`}>
{enableSearch ? (
<div className="ve-component-list-head">
{this.renderHeader()}
</div>
) : null}
<div
className={`ve-component-list-body ${bodyExtraClass}`}
ref={(shell) => {
if (this.shell || !shell || this.isEmpty) {
return;
}
if (!this.shell) {
this.shell = shell;
}
registerAdditive(shell);
}}
>
{this.renderBundle()}
</div>
<div
className={`ve-component-list-foot ${
this.hasActions() ? "exist" : ""
}`}
>
{this.renderActions()}
</div>
{this.renderCard ? this.renderCard() : null}
</div>
);
}
}
export default Base;

View File

@ -0,0 +1,116 @@
@import "~@ali/ve-less-variables/index.less";
.ve-component-list {
position: absolute;
top: 45px;
right: 0;
bottom: 0;
left: 0;
.ve-component-list-head {
height: 56px;
padding: 12px;
border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
.ve-search-control {
width: 256px;
height: 32px;
margin: 0;
}
}
.ve-component-list-body {
display: flex;
height: 100%;
overflow: auto;
.ve-component-list-empty {
width: 100%;
padding: 20px;
text-align: center;
&.kuma-empty-data.normal .kuma-empty-data-icon {
width: 50px;
height: 50px;
background-size: 100% 100%;
display: inline-block;
}
}
.component-description-item-icon-group {
display: flex;
justify-content: flex-start;
align-items: center;
.component-description-item-icon {
margin-left: 4px;
&.tag {
display: flex;
justify-content: center;
align-items: center;
background: rgba(249, 189, 15, 0.1);
color: #ff6f00;
font-size: 12px;
width: 48px;
height: 24px;
}
}
icon {
position: relative;
font-size: 14px;
cursor: pointer;
color: rgba(31, 56, 88, 0.4);
transition: color @transition-duration @transition-ease;
&:hover {
color: rgba(31, 56, 88, 0.6);
}
a {
position: absolute;
left: 0;
top: 0;
display: inline-block;
width: 14px;
height: 14px;
}
}
}
&.small {
height: calc(100% - 56px - 48px);
}
&.medium {
height: calc(100% - 56px);
}
&.large {
height: calc(100% - 48px);
}
}
.ve-component-list-foot {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
bottom: 0;
.btn {
margin-right: 8px;
flex: 1;
&:last-child {
margin-right: 0;
}
}
.kuma-button-primary {
background-color: var(--color-brand, @brand-color-1);
border-color: var(--color-brand, @brand-color-1);
border-radius: @global-border-radius;
color: var(--color-text-reverse, @white-alpha-2);
transition: background-color @transition-duration @transition-ease;
&:hover {
background-color: var(--color-brand-dark, @brand-color-3);
border-color: var(--color-brand-dark, @brand-color-3);
transition: background-color @transition-duration @transition-ease;
}
}
&.exist {
width: 100%;
height: 48px;
border-top: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
background: #fff;
padding: 0 12px;
}
}
::-webkit-scrollbar {
display: none;
}
}

View File

@ -0,0 +1,22 @@
import { Button } from "@ali/vu-uxcore-legao-design";
import './index.less';
const MyButton = (props) => {
const { action, componentPrototype, type, className } = props;
return (
<Button
className={className}
type={type || action.type || "outline"}
danger={action.danger || false}
onClick={() => {
if (action.fn && typeof action.fn === "function") {
action.fn(componentPrototype);
}
}}
>
{action.title}
</Button>
);
};
export default MyButton;

View File

@ -0,0 +1,18 @@
.ve-component-list {
.kuma-button-outline {
border-color: #0079f2;
color: #0079f2;
&:hover {
border-color: #0079f2;
color: #0079f2;
background-color: #f0f7ff;
}
&.kuma-button-danger {
border-color: #f04631;
color: #f04631;
&:hover {
background-color: rgba(240, 70, 49, 0.06);
}
}
}
}

View File

@ -0,0 +1,147 @@
import React from "react";
import Layer from "@ali/vu-layer";
import { Icon } from "@alifd/next";
import $i18n from "../../i18n/index";
import Button from "../button";
import "./index.less";
export default class Card extends React.Component {
static propTypes = {};
constructor(props) {
super(props);
this.state = {
isLoading: true,
errorMsg: false,
componentInfo: {},
};
}
componentDidMount() {
if (!this.props.getComponentInfo) return;
this.loadComponentInfo(this.props.componentPrototype);
}
componentWillReceiveProps(nextProps) {
if (nextProps.componentPrototype !== this.props.componentPrototype) {
// 延迟执行数据加载和渲染,等 props 更新之后
this.loadComponentInfo(nextProps.componentPrototype);
}
}
loadComponentInfo(componentPrototype) {
if (!this.props.getComponentInfo) return;
this.setState({ isLoading: true });
this.props
.getComponentInfo(componentPrototype)
.then((componentInfo) => {
this.setState({
componentInfo,
isLoading: false,
});
})
.catch((e) => {
this.setState({ errorMsg: e.message });
console.error(e);
if (VisualEngine) {
VisualEngine.ui.Popup.error({
content: e.message,
duration: 2000,
});
}
});
}
render() {
const { componentPrototype, subTitle, customImage } = this.props;
const { componentInfo } = this.state;
const loadingContent = this.state.errorMsg ? (
<div>
<div className="ve-loading-content">{this.state.errorMsg}</div>
</div>
) : (
<div>
<Icon type="loading" className="ve-loading-icon" size="large" />
<div className="ve-loading-content">
{$i18n.get({ id: "trunkPaneLoading", dm: "加载中..." })}
</div>
</div>
);
const {
title,
version,
image = "https://img.alicdn.com/tfs/TB1XHG6ehrI8KJjy0FpXXb5hVXa-740-608.png",
desc,
detailUrl,
actions,
} = componentInfo;
let layerContent = null;
const cardTitle = subTitle && !subTitle.includes(title) ? `${title}${subTitle}` : title;
if (this.state.isLoading) {
layerContent = (
<div className="ve-card-wrapper ve-card-wrapper-loading" onMouseEnter={this.props.onMouseEnter}>
{loadingContent}
</div>
);
} else {
layerContent = (
<div className="ve-card-wrapper" onMouseEnter={this.props.onMouseEnter}>
<div className="ve-card-top">
<div className="ve-card-title">
<span className="title">{cardTitle}</span>
<span className="version">{version}</span>
</div>
<div className="ve-card-image-wrapper">
<img src={customImage && typeof customImage === 'string' ? customImage : image} alt={cardTitle} className="ve-card-image" />
</div>
</div>
<div className="ve-card-bottom">
<div className="ve-card-description">
<p>{desc}</p>
</div>
<div className="ve-operation-container">
{detailUrl ? (
<a
href={detailUrl}
target="_blank"
rel="noopener noreferrer"
className="ve-operation-item"
>
{$i18n.get({
id: "trunkPaneDetailedDocumentation",
dm: "详细文档",
})}
</a>
) : null}
<div className="actions">
{actions
? actions.map((action, idx) => {
return (
<Button
key={idx}
className="ve-card-action"
action={action}
componentPrototype={componentPrototype}
/>
);
})
: null}
</div>
</div>
</div>
</div>
);
}
return (
<Layer {...this.props} noLimitOnMaxHeight>
{layerContent}
</Layer>
);
}
}

View File

@ -0,0 +1,130 @@
@import "~@ali/ve-less-variables/index.less";
.ve-card-wrapper {
width: 320px;
max-height: 360px;
overflow: auto;
justify-content: center;
&.ve-card-wrapper-loading {
display: flex;
justify-content: center;
text-align: center;
overflow: hidden;
}
.ve-card-top {
height: 186px;
background: rgba(31, 56, 88, 0.1);
padding: 16px;
.ve-card-title {
display: flex;
align-items: center;
padding-bottom: 12px;
position: relative;
height: 31px;
color: rgba(0, 0, 0, 0.8);
.title {
line-height: 22px;
font-size: 16px;
}
.version {
margin-left: 8px;
font-size: 12px;
line-height: 22px;
color: rgba(31, 56, 88, 0.4);
}
}
.ve-card-image-wrapper {
background-color: #fff;
.ve-card-image {
max-width: 100%;
height: 120px;
margin: 0 auto;
display: block;
padding: 10px;
}
}
}
.ve-card-bottom {
padding: 8px 16px 16px 16px;
.ve-card-description {
user-select: text;
text-align: left;
line-height: 18px;
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
letter-spacing: 0;
line-height: 18px;
width: 280px;
p {
margin-bottom: 8px;
}
p:last-child {
margin-bottom: 0;
}
}
.ve-operation-container {
margin-top: 12px;
position: relative;
.actions {
margin-top: 16px;
}
}
}
.ve-operation-item {
color: @brand-color-1;
cursor: pointer;
margin-right: 8px;
}
.ve-operation-delete {
position: absolute;
top: 0;
right: 0;
}
.ve-card-more-operation {
display: flex;
justify-content: space-between;
> a {
cursor: pointer;
}
}
.ve-loading-icon {
animation: spining 0.9s infinite linear;
}
.ve-loading-content {
padding: 10px;
text-align: center;
}
.ve-card-action {
padding: 0 8px !important;
margin-right: 8px;
}
}
.ve-component-list {
.vu-layer {
border-radius: 4px !important;
.vu-layer-content {
padding-top: 0 !important;
}
}
.vu-layer .vu-layer-close {
right: 9px !important;
top: 9px !important;
}
}

View File

@ -0,0 +1,279 @@
import Base, { AdditiveType } from "../base/index.js";
import Snippet from "../snippet";
import "./index.less";
// 滚动事件触发灵敏度
const OFFSET_ACCURCY = 25;
const categoryMap = {
General: "常用",
Navigation: "导航",
DataEntry: "输入",
DataDisplay: "展示",
Feedback: "反馈",
Util: "工具",
Chart: "图表",
Others: "其他",
};
export default class ComponentList extends Base {
static displayName = "ComponentList";
descRefList = new Map();
navRefList = new Map();
descHeightList = new Map();
snippetMap = new Map();
currentScrollHeight = 0;
scrollFlag = true;
scroll;
scrollTimer;
state = {
selected: "",
searchText: "",
currentCard: null,
currentCardImage: ''
};
componentDidMount() {
setTimeout(() => {
this.calDescHeightList();
// mock 滚动结束事件
if (this.scroll) {
this.scroll.addEventListener("scroll", this.handleScrollEnd);
}
}, 20);
const bundle = this.normalizeBundle();
if (!bundle) {
return;
}
const cats = Object.keys(bundle);
if (cats.length > 0) {
const k = cats[0];
const comps = bundle[k];
if (!comps || !Array.isArray(comps) || !comps.length) {
return;
}
this.setState({ selected: comps[0].id });
}
}
componentWillUnmount() {
if (this.scroll) {
this.scroll.removeEventListener("scroll", this.handleScrollEnd);
}
}
handleScrollEnd = () => {
clearTimeout(this.scrollTimer);
this.scrollTimer = setTimeout(() => {
// 滚动结束时归位
this.scrollFlag = true;
}, 100);
};
toggleComponent(id) {
const element = this.descRefList.get(id);
// 切换组件时滚动事件回调不可用
this.scrollFlag = false;
element.scrollIntoView();
this.setState({ selected: id });
}
handleScroll(e) {
// 清空卡片
if (this.state.currentCard) {
this.setState({ currentCard: null });
}
clearTimeout(this.timer);
this.timer = null;
if (!this.scrollFlag) {
return;
}
const element = e.target;
const { scrollTop } = element;
// 延迟处理
if (Math.abs(scrollTop - this.currentScrollHeight) < OFFSET_ACCURCY) {
return;
}
this.currentScrollHeight = scrollTop;
// 处理导航块滚动高亮效果
const heightList = [...this.descHeightList.entries()];
let selected;
// 当在顶部
if (scrollTop >= 0 && scrollTop < heightList[0][1]) {
selected = heightList[0][0];
} else if (scrollTop >= heightList[heightList.length - 1][1]) {
// 底部
selected = heightList[heightList.length - 1][0];
} else {
// 当在中部
for (let i = 0; i < heightList.length - 2; i++) {
const height1 = heightList[i][1];
const height2 = heightList[i + 1][1];
if (scrollTop > height1 && scrollTop < height2) {
selected = heightList[i + 1][0];
}
}
}
this.fixSideBarView(selected);
this.setState({ selected });
}
// 导航高亮块超出视口时滚动
fixSideBarView(selected) {
const nav = this.navRefList.get(selected);
if (nav) {
nav.scrollIntoView({ block: "center" });
}
}
calDescHeightList() {
let height = 0;
this.descRefList.forEach((ele, key) => {
if (!ele) {
return;
}
height += ele.clientHeight;
this.descHeightList.set(key, height);
});
}
renderNavigator(bundle) {
return (
<div className="ve-component-list-navigator">
{Object.keys(bundle).map((c) => {
const catTitle = categoryMap[c] || c;
const catData = bundle[c];
return this.renderNavigatorGroup(catTitle, catData);
})}
</div>
);
}
renderNavigatorGroup(title, metaData) {
const { selected } = this.state;
if (!metaData) {
return null;
}
return (
<div className="navigator-group" key={title}>
<div className="navigator-group-head">
<div className="navigator-group-title">{title}</div>
</div>
{metaData.map((comp) => (
<div
className={`navigator-group-item ${AdditiveType.Draggable} ${
selected === comp.id ? "active" : ""
}`}
key={comp.id}
data-id={
(comp.snippets && comp.snippets[0] && comp.snippets[0].id) || ""
}
ref={(item) => {
this.navRefList.set(comp.id, item);
}}
onClick={() => this.toggleComponent(comp.id)}
>
<span className="navigator-group-item-title">{comp.title}</span>
</div>
))}
</div>
);
}
renderComponentDescriptionList(bundle) {
const { renderCustomSnippet = "", enableCard = true } = this.props;
return (
<div className="component-description-list">
{Object.keys(bundle).map((cat) => {
const catData = bundle[cat];
if (!catData) {
return null;
}
return catData.map((comp) => (
<div
className="component-description-item"
key={comp.id}
ref={(item) => {
this.descRefList.set(comp.id, item);
}}
>
<div className="component-description-item-header">
{comp.title ? (
<div className="component-description-item-title">
{comp.title}
</div>
) : null}
<div className="component-description-item-icon-group">
</div>
</div>
<div className="component-description-item-snippets">
{comp.snippets &&
comp.snippets.map((snippet, idx) => (
<div
key={`${comp.id}-${idx}`}
ref={(i) => {
this.snippetMap.set(`${comp.id}-${idx}`, i);
}}
onMouseEnter={() => {
if (!enableCard) {
return;
}
this.timer = setTimeout(() => {
this.setState({
currentCard: comp,
target: this.snippetMap.get(`${comp.id}-${idx}`),
currentCardImage: snippet.thumbnail
});
this.timer = null;
}, 1000);
}}
onMouseLeave={() => {
if (!enableCard) {
return;
}
clearTimeout(this.timer);
this.timer = null;
setTimeout(() => {
if (this.isMouseEnterCard) {
return;
}
this.setState({currentCard: null});
}, 200);
}}
>
<Snippet
key={idx}
snippet={snippet}
renderCustomSnippet={renderCustomSnippet}
/>
</div>
))}
</div>
</div>
));
})}
</div>
);
}
getBundle(bundle) {
return [
<div className="ve-component-list-sidebar" key="sidebar">
{this.renderNavigator(bundle)}
</div>,
<div
key="content"
className="ve-component-list-content"
onScroll={(e) => this.handleScroll(e)}
ref={(scroll) => (this.scroll = scroll)}
>
{this.renderComponentDescriptionList(bundle)}
</div>,
];
}
}

View File

@ -0,0 +1,117 @@
@import "~@ali/ve-less-variables/index.less";
@text-color-active: #0079f2;
.ve-component-list {
.ve-component-list-body {
.ve-component-list-sidebar {
flex: 0 0 110px;
width: 110px;
// height: 100%;
padding-top: 11px;
border-right: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
overflow: auto;
.ve-component-list-navigator {
.navigator-group {
margin-bottom: 8px;
& > div {
display: flex;
align-items: center;
width: 100%;
}
.navigator-group-head {
padding: 0px 12px;
color: @dark-alpha-2;
.navigator-group-title {
padding: 4px 0;
width: 100%;
line-height: 16px;
font-size: @fontSize-5;
font-weight: 700;
border-bottom: 1px solid
var(--color-line-normal, rgba(31, 56, 88, 0.1));
}
}
.navigator-group-item {
height: 32px;
padding: 8px 12px;
color: #666666;
cursor: pointer;
.navigator-group-item-title {
font-size: @fontSize-5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
&:hover {
color: @text-color-active;
}
&.active {
background: rgba(0, 121, 242, 0.1);
color: @text-color-active;
border-right: 2px solid @text-color-active;
}
}
}
}
}
.ve-component-list-content {
flex: 1;
width: 250px;
// height: 100%;
overflow-y: auto;
overflow-x: hidden;
.component-description-list {
padding: 10px 4px;
.component-description-item {
padding-bottom: 24px;
.component-description-item-header {
display: flex;
justify-content: flex-start;
align-items: center;
.component-description-item-title {
font-family: @font-family;
font-size: @fontSize-5;
font-weight: 700;
color: @text-color;
line-height: 20px;
padding-left: 8px;
}
.component-description-item-icon-group {
display: flex;
justify-content: flex-start;
align-items: center;
.component-description-item-icon {
margin-left: 4px;
}
icon {
position: relative;
font-size: 14px;
cursor: pointer;
color: rgba(31, 56, 88, 0.4);
transition: color @transition-duration @transition-ease;
&:hover {
color: rgba(31, 56, 88, 0.6);
}
a {
position: absolute;
left: 0;
top: 0;
display: inline-block;
width: 14px;
height: 14px;
}
}
}
}
.component-description-item-snippets {
display: flex;
justify-content: flex-start;
flex-flow: row wrap;
}
}
}
}
}
}

View File

@ -0,0 +1,46 @@
import { Fragment } from "react";
import { Icon } from "@alifd/next";
import { AdditiveType } from "../base";
import "./index.less";
const Snippet = (props) => {
const {
snippet,
renderCustomSnippet = "",
size = "small",
actionsInLT,
actionsInRT,
} = props;
const {
thumbnail = "https://img.alicdn.com/tfs/TB1XHG6ehrI8KJjy0FpXXb5hVXa-740-608.png",
description,
title = "未知"
} = snippet;
const snippetClassName = `component-description-item-snippet ${AdditiveType.All} ${size}`;
return (
<div className={snippetClassName} key={snippet.id} data-id={snippet.id}>
{typeof renderCustomSnippet === "function" ? (
renderCustomSnippet(snippet)
) : (
<Fragment>
<div className="snippet-thumbnail">
{typeof thumbnail === "string" && thumbnail.startsWith("http") ? (
<img alt="thumbnail" src={thumbnail} />
) : (
<Icon className="icon" type={thumbnail || "help"} />
)}
</div>
<div className="snippet-title">{description || title}</div>
<div className="engine-additive-helper left-top">
{actionsInLT ? actionsInLT : null}
</div>
<div className="engine-additive-helper right-top">
{actionsInRT ? actionsInRT : null}
</div>
</Fragment>
)}
</div>
);
};
export default Snippet;

View File

@ -0,0 +1,120 @@
@import "~@ali/ve-less-variables/index.less";
@text-color-active: #0079f2;
.component-description-item-snippet {
position: relative;
&.small {
width: 64px;
height: 66px;
.snippet-thumbnail {
height: 40px;
img,
.icon {
max-width: 56px;
max-height: 32px;
}
}
}
&.large {
width: 132px;
height: 126px;
.snippet-thumbnail {
height: 100px;
img,
.icon {
max-width: 116px;
max-height: 83px;
}
}
}
margin: 12px 8px 0 8px;
cursor: grab;
.snippet-title {
width: 100%;
padding: 0 4px;
font-size: @fontSize-5;
color: #666666;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.snippet-thumbnail {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
margin-bottom: 9px;
border-radius: @global-border-radius;
border: 1px dashed #cdd2d9;
transition: border @transition-duration @transition-ease;
img,
.icon {
width: auto;
height: auto;
}
svg {
font-size: 28px;
}
svg * {
fill: rgba(31, 56, 88, 0.6) !important;
}
}
.component-description-item-icon {
&.tag {
display: flex;
justify-content: center;
align-items: center;
background: rgba(249, 189, 15, 0.1);
color: #ff6f00;
font-size: 12px;
border-radius: 3px;
width: 48px;
height: 24px;
}
}
.engine-additive-helper {
&.left-top {
position: absolute;
left: 0;
top: 0;
}
&.right-top {
display: none;
}
icon {
position: relative;
font-size: 14px;
cursor: pointer;
color: rgba(31, 56, 88, 0.4);
transition: color @transition-duration @transition-ease;
&:hover {
color: rgba(31, 56, 88, 0.6);
}
a {
position: absolute;
left: 0;
top: 0;
display: inline-block;
width: 14px;
height: 14px;
}
}
}
&:hover {
.snippet-thumbnail {
border: 2px solid @text-color-active;
transition: border @transition-duration @transition-ease;
}
.right-top {
display: block;
position: absolute;
right: 0;
top: 0;
height: 24px;
display: flex;
align-items: center;
}
}
}

View File

@ -0,0 +1,45 @@
const { provideIntl, destroyIntl } = require('@ali/intl-universal');
const strings = require('./strings');
let intl;
const MEDUSA_APP_NAME = 'legao-designer';
const PSEUDO_LANGUAGE_TAG = 'pd-KV';
const CURRENT_LANGUAGE = (window.locale || '').replace(/_/, '-') || 'zh-CN';
function update(language) {
destroyIntl();
intl = provideIntl({
locale: language,
messagesAIO: strings,
});
}
function get(id, variable) {
if (!intl) update();
let string = '';
let key = '';
if (typeof id === 'string') {
key = id;
string = intl.formatMessage({ id }, variable);
}
if (typeof id === 'object' && id.dm) {
id.defaultMessage = id.dm;
}
key = id.id;
string = intl.formatMessage(id, variable);
if (CURRENT_LANGUAGE === PSEUDO_LANGUAGE_TAG) {
return `##@@@${key}##${MEDUSA_APP_NAME}@@@##${string}`;
}
return string;
}
if (PSEUDO_LANGUAGE_TAG === CURRENT_LANGUAGE) {
update('en-US');
} else {
update(CURRENT_LANGUAGE);
}
module.exports = {
get,
update,
};

View File

@ -0,0 +1,27 @@
{
"trunkPaneTheComponentPrototypeView": "The component prototype view does not exist and may be caused by the following reasons:",
"trunkPaneDetailedComponentInformationIs": "Detailed component information is already printed in the Console",
"trunkPaneIsTheComponentRemoved": "Is the component removed because the component cannot find the prototype view?",
"trunkPaneLoading": "Loading...",
"trunkPaneFoundComponentNameRComponentName": "Found component name: {rComponentName} has a problem",
"trunkPaneDetailedDocumentation": "Detailed documentation",
"trunkPaneComponentComponentInfoComponentNameDeletedSuccessfully": "Component {componentInfoComponentName} deleted successfully",
"trunkPaneUpdateComponent": "Update component",
"trunkPaneComponentLibrary": "Component library",
"trunkPaneSearchComponent": "Search component",
"trunkPaneCustomize": "customize",
"trunkPaneTheComponentInQuestion": "The component in question:",
"trunkPaneComponentMarket": "Component market",
"trunkPaneUncategorized": "uncategorized",
"trunkPaneNoSearchResultsYet": "No search results yet",
"trunkPaneAll": "All",
"trunkPaneClickToTheLEGO": "Click ? to the Component Center",
"trunkPaneClickToTroubleshootThe": "Click to troubleshoot the issue",
"trunkPaneComponent": "Component",
"trunkPaneComponentCenter": "Component center",
"trunkPaneClose": " Close",
"trunkPaneDetail": "Detail",
"trunkPaneEdit": "Edit",
"trunkPaneDev": "Dev",
"trunkPaneHelp": "Help"
}

View File

@ -0,0 +1,4 @@
module.exports = {
'en-US': require('./en-US.json'),
'zh-CN': require('./zh-CN.json'),
};

View File

@ -0,0 +1,26 @@
{
"trunkPaneAll": "全部",
"trunkPaneFoundComponentNameRComponentName": "发现组件名为:{rComponentName}存在问题",
"trunkPaneNoResultsYet": "暂无结果",
"trunkPaneUpdateComponent": "更新组件",
"trunkPaneClickToTroubleshootThe": "点击排查问题",
"trunkPaneClickToTheLEGO": "点击 ? 前往组件中心",
"trunkPaneComponentLibrary": "组件库",
"trunkPaneComponentCenter": "组件中心",
"trunkPaneDetailedComponentInformationIs": "详细的组件信息已经打印在 Console 中",
"trunkPaneTheComponentPrototypeView": "组件原型视图不存在,可能由以下原因导致:",
"trunkPaneLoading": "加载中...",
"trunkPaneTheComponentInQuestion": "存在问题的组件:",
"trunkPaneIsTheComponentRemoved": "由于该组件找不到原型视图,是否移除组件?",
"trunkPaneDetailedDocumentation": "详细文档",
"trunkPaneUncategorized": "未分类",
"trunkPaneComponentMarket": "组件市场",
"trunkPaneCustomize": "自定义",
"trunkPaneSearchComponent": "搜索组件",
"trunkPaneComponent": "组件",
"trunkPaneClose": " 关闭",
"trunkPaneDetail": "详情",
"trunkPaneEdit": "编辑",
"detrunkPaneDev": "开发版",
"trunkPaneHelp": "帮助"
}

View File

@ -1,5 +1,7 @@
import { Component, ReactNode } from 'react';
import ComponentList, { AdditiveType } from "@ali/ve-component-list";
import { Tab } from '@alifd/next';
import ComponentList from "./components/component-list";
import { AdditiveType } from "./components/base"
import { PluginProps } from '@ali/lowcode-types';
import { Designer } from '@ali/lowcode-designer';
@ -7,6 +9,7 @@ import './index.scss';
export interface IState {
metaData: object[];
bizComponents: object[];
}
export default class ComponentListPlugin extends Component<PluginProps, IState> {
@ -18,6 +21,7 @@ export default class ComponentListPlugin extends Component<PluginProps, IState>
super(props);
this.state = {
metaData: [],
bizComponents: [],
};
}
@ -61,9 +65,11 @@ export default class ComponentListPlugin extends Component<PluginProps, IState>
const { editor } = this.props;
const assets = editor.get('assets') || {};
const metaData = this.transformMetaData(assets.componentList);
const bizComponents = this.transformMetaData(assets.bizComponentList);
this.setState({
metaData,
bizComponents,
});
};
@ -126,15 +132,27 @@ export default class ComponentListPlugin extends Component<PluginProps, IState>
}
render(): ReactNode {
const { metaData } = this.state;
const { metaData, bizComponents } = this.state;
return (
<div className="lowcode-component-list">
<ComponentList
key="component-pane"
metaData={metaData}
registerAdditive={(shell: Element | null) => this.registerAdditive(shell)}
enableSearch
/>
<Tab>
<Tab.Item title="基础组件" key="base-components">
<ComponentList
key="component-pane"
metaData={metaData}
registerAdditive={(shell: Element | null) => this.registerAdditive(shell)}
enableSearch
/>
</Tab.Item>
<Tab.Item title="业务组件" key="biz-components">
<ComponentList
key="component-pane"
metaData={bizComponents}
registerAdditive={(shell: Element | null) => this.registerAdditive(shell)}
enableSearch
/>
</Tab.Item>
</Tab>
</div>
);
}

View File

@ -0,0 +1,99 @@
export const builtinSearchMap = {
"容器": "容器、rongqi、rq、分栏、ColumnsLayout、Columns、layout、grid",
"分栏": "分栏、ColumnsLayout、Columns、layout、grid、容器、rongqi、rq、fl、fenlan",
"卡片": "卡片、card、kapian、kp",
"选项卡": "选项卡、tab、tabs、页签、xuanxiangka、xxk、yeqian、yq",
"按钮": "按钮、button、anniu、an",
"图标": "图标、icon、tubiao、tb",
"标题": "标题、title、biaoti、bt",
"图片": "图片、image、pic、picture、tupian、tp",
"Dialog": "对话框、Dialog、弹框、弹出框、duihuakuang、dhk",
"Drawer": "抽屉、Drawer、chouti、ct、couti",
"文本": "文本、文字、text、wenzi、wz、wenben",
"链接": "链接、link、lianjie、lj",
"链接块": "链接块、链接、link、lianjie、lj、ljk",
"表单容器": "表单容器、表单、form、biaodan、bd",
"输入框": "输入框、文本框、密码框、input、shurukuang、srk、wenbenkuang、wbk",
"数字输入框": "数字输入框、数字、输入框、Number、NumberPicker、shuzi、sz、srk、shurukuang",
"单选": "单选、radio button、radio、danxuan、dx",
"多选": "复选、复选框、多选、Checkbox、check、fuxuan、fx、dx、duoxuan",
"下拉选择": "下拉选择、Select、选择器、下拉、xiala、xl、xialaxuanze、xlxz",
"开关": "开关、switch、kaiguan、kg",
"日期": "日期选择、date、DatePicker、riqi、riqixuanz、rq、rqxz",
"日期区间": "日期区间、Cascade、date、riqiqujian、rq",
"时间选择框": "时间选择、time、TimePicker、shijian、sj、shijianxuanze、sjxz、xuanzekuang",
"上传图片": "上传图片、upload、上传、shangchuan、sc、tupian",
"上传附件": "上传附件、upload、上传、shangchuan、sc、fujian",
"树形选择": "树形选择、树型选择、树选择、tree、TreeSelect、shu、sxz、shuxingxuanze",
"级联选择": "级联选择、Cascade、Cascadeselect、级联、jilian、jl、jilianxuanze、jlxz",
"地区选择": "地区选择、city、地址、address、地区、diqu、dq、diquxuanze、dqxz",
"国家选择": "国家选择、country、国家、guojia、gj、guojiaxuanze、gjxz",
"评分": "评分、Rate、Rating、星、pingfen、pf",
"明细": "明细、table、表格、表单、form、mingxi、mx",
"穿梭框": "穿梭框、transfer、chuansuokuang、csk",
"人员搜索框": "人员搜索框、employee、人员选择、选人、xuanren、xr、renyuansousuo、ryss",
"筛选": "筛选、pickable、shaixuan、sx",
"金额输入框": "金额输入框、输入框、shurukuang、srk、money、金额、jine、je",
"金额区间": "金额区间、money、金额、jine、je",
"查询": "查询、filter、chaxun、cx",
"表格": "表格、table、biaoge、bg",
"数据文本": "数据文本、Number Info、数据、shuju、sj、shujuwenben、sjwb",
"数据趋势": "数据趋势、Number Trend、数据、shuju、sj、shujuqushi、sjqs",
"勾选框": "勾选框、复选框、check box、gouxuankuang、gxk、fuxuankuang、fxk",
"图片浏览": "图片浏览、图片预览、image、pic、picture、图片、预览、tupianyulan、tupianliulan、tupian、tp、yulan、yl",
"搜索": "搜索、搜索框、查询框、查询、search、sousuo、ss",
"树形控件": "树形控件、树组件、tree、shuzujian、shuxingkongjian、shu、szj、sxkj",
"富文本框": "富文本框、RichText、fuwenben、fwb",
"步骤": "步骤、步骤条、step、steps、buzhoutiao、buzhou、bzt、bz",
"时间轴": "时间轴、时间线、timeline、shijianzhou、shijianxian、sjz、sjx",
"菜单": "菜单、menu、caidan、cd",
"气泡提示": "气泡提示、tip、tips、balloon、气泡、qipao、qp、qipaotishi、qpts",
"面包屑": "面包屑、breadcrumb、crumb、mianbaoxie、mbx",
"日历": "日历、calendar、rili、rl",
"折叠面板": "折叠面板、collapse、折叠、zhedie、zd、zhediemianban、zdmb",
"下拉菜单": "下拉菜单、dropdown、下拉、xiala、xl、xialacaidan、xlcd、菜单、caidan、cd",
"信息提示": "信息提示、message、alert、信息、提示、警示、xinxitishi、xxts、xinxi、xx、tishi、ts、消息、xiaoxi",
"进度指示器": "进度指示器、进度条、progress、jindutiao、jdt、进度、jindu、jd",
"翻页器": "翻页器、分页器、pagination、fanyeqi、fyq、分页、fenye、fy",
"轮播图": "轮播图、图片轮播、slider、轮播、lunbo、lb、lunbotu、lbt",
"底部通栏": "底部通栏、tool bar、通栏、dibutonglan、dbtl、浮动工具条、浮动、工具条、工具、fudong、gongju、toolbar、tool bar、fd、gj",
"HTML": "html",
"JSX": "jsx",
"浮动导航": "浮动导航、nav、floatNav、fudongdaohang、fddh",
"Iframe": "Iframe",
"Markdown": "Markdown",
"区段选择器": "区段选择、滑块选择、区段、滑块、选择、Range、quduan、huakuai、xuanze、qdxz、hkxz、xz"
};
/**
*
* @param {string} title 组件名
* @param {string} query 搜索词
* @param {object} map 映射关系
*/
export function searchComponent(title, query, map = {}) {
if (!title || !query || !map || !map[title]) {
return false;
}
const keys = (map[title] || '').split('、');
return !!keys.find(key => {
if (!key) {
return false;
}
return key.indexOf(query) > -1
});
}
export function debounce(func, delay) {
let timer
return function(...args) {
if (timer) {
return
}
timer = setTimeout(() => {
func.apply(this, args)
clearTimeout(timer)
timer = null
}, delay)
}
}