Commit 0a0442d1 authored by rockyl's avatar rockyl

行为编辑器完成

parent bb3df274
# 库
* builtins 内建
* customs 自定义
# 库中的嵌套
builtins不能引用customs中的过程,但是customs能引用builtins中的过程
库中的过程只能使用引用,即带有脚本的过程只暴露在根中
# 索引
为方便查找,所有集合都采用id作为键名
在树状列表中用到的列表需转化一次成为可用数据结构
No preview for this file type
......@@ -12,6 +12,7 @@ const data = {
id: 'entry',
name: 'Entry',
group: 'base',
type: 'builtin',
options: {},
output: ['success'],
},
......@@ -19,6 +20,7 @@ const data = {
id: 'wait',
name: 'Wait',
group: 'base',
type: 'builtin',
options: {
duration: {type: 'number', default: 1000},
},
......@@ -27,7 +29,9 @@ const data = {
{
id: 'prefab1',
name: 'Prefab1',
type: 'builtin',
isPrefab: true,
subEntry: '1',
sub: {
1: {
uuid: '1',
......@@ -54,8 +58,10 @@ const data = {
name: 'Wave',
options: {
duration: {type: 'number', default: 1000},
type: {type: 'string', default: 'yoyo'},
ease: {type: 'string', default: 'linear'},
name: {type: 'string', default: 'hello'},
color: {type: 'color', default: '#123456'},
type: {type: ['rotate', 'jump', 'breath'], default: 'rotate'},
ease: {type: ['linear', 'cubic', 'back'], default: 'linear'},
autoPlay: {type: 'boolean', default: false},
}
}
......
......@@ -46,12 +46,12 @@ const data = {
}],
"assets": [],
"dataMapping": [],
processMap: {
main: {
processes: [
{
id: 'main',
name: 'Main',
options: {},
subEntry: '1',
subEntry: 'a1',
sub: {
a1: {
uuid: 'a1',
......@@ -83,15 +83,19 @@ const data = {
},
}
},
test: {
{
id: 'test',
name: 'Test',
options: {
text: {type: 'string', default: ''},
text: {alias: '文本', type: 'string', default: '你好'},
num: {alias: '数字', type: 'number', default: 1},
type: {alias: '类型', type: 'enum', enum: ['rotate', 'jump', 'breath'], default: 'rotate'},
autoPlay: {alias: '自动播放', type: 'boolean', default: false},
},
output: ['success', 'failed'],
script: "console.log('test');",
},
}
],
};
const data1 = {
"views": [{
......
......@@ -2,8 +2,8 @@
* Created by rockyl on 2019-09-19.
*/
export const API_HOST = 'http://10.10.95.74:7777';
//export const API_HOST = 'http://localhost:3002';
//export const API_HOST = 'http://10.10.95.74:7777';
export const API_HOST = 'http://localhost:3002';
export const UPLOAD_FILE_URL = API_HOST + '/api/uploadFile';
export const PARSE_BUNDLE_URL = API_HOST + '/api/parsePSD';
......
......@@ -11,6 +11,8 @@
"Import": "Import",
"Export": "Export",
"Upload": "Upload",
"Edit": "Edit",
"Name": "Name",
"Failed to fetch": "Network error!",
"In processing": "In processing",
"Projects": "Projects",
......@@ -28,6 +30,8 @@
"Saving": "Saving…",
"Create project": "Create project",
"Rename project": "Rename project",
"Options Editor": "Options Editor",
"Meta Editor": "Meta Editor",
"Input project name": "Input project name",
"Invalid project name": "Invalid project name",
"Creating project": "Creating project…",
......@@ -61,6 +65,8 @@
"Invalid asset name": "Invalid asset name",
"Copied field to clipboard": "Copied {field} to clipboard",
"Unsaved Alert": "You are leaving, but the project is not saved. Do you want to save it?",
"Meta is in use, can not delete": "Meta is in use, can not delete!",
"Are you sure to delete this meta": "Are you sure to delete this meta?",
"menu": {
"save": "Save",
"preview": "Preview",
......
......@@ -18,10 +18,5 @@ export default new Router({
name: 'editor',
component: () => import('./views/Editor.vue')
},
{
path: '/behavior',
name: 'behavior',
component: () => import('./views/BehaviorEditorWrapper.vue')
},
]
})
......@@ -8,17 +8,23 @@ import {envStore} from "./modules/env";
import {projectsStore} from "./modules/projects";
import {projectStore} from "./modules/project";
import SaveToLocalPlugin from "./save-to-local-plugin";
import {behaviorStore} from "./modules/behavior";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
mutations: {
},
actions: {
},
modules: {
env: envStore,
projects: projectsStore,
project: projectStore,
behavior: behaviorStore,
},
plugins: [
SaveToLocalPlugin({
......@@ -34,7 +40,8 @@ export default new Vuex.Store({
'addDataMapping',
'deleteDataMapping',
'modifyDataMapping',
'modifyActiveView'
'modifyActiveView',
'behavior_save',
]
})
]
......
/**
* Created by rockyl on 2019-10-29.
*
* 行为编辑
*/
import Vue from 'vue';
import generateUUID from "uuid/v4";
import {metaInUse, updateProcesses} from "../../utils";
export const behaviorStore = {
state: {
data: {},
currentBehavior: null,
processContext: [],
originData: null,
behaviors: null,
},
mutations: {
behavior_startEdit(state, {originData, behaviors, event}) {
state.originData = originData;
state.behaviors = behaviors;
state.data = JSON.parse(JSON.stringify(originData));
if (behaviors.length > 0) {
state.currentBehavior = behaviors[0];
} else {
let metaUUID = generateUUID();
state.currentBehavior = behaviors[0] = {
uuid: generateUUID(),
alias: event,
meta: metaUUID,
};
let subEntryUUID = generateUUID();
state.data.processes.push({
id: metaUUID,
name: 'event',
options: {},
subEntry: subEntryUUID,
sub: {
[subEntryUUID]: {
uuid: subEntryUUID,
alias: 'Entry',
meta: 'entry',
},
}
});
}
},
addCustomProcessMeta(state, meta) {
state.data.processes.push(meta);
return process;
},
behavior_save(state) {
state.originData.processes = state.data.processes;
state.behaviors[0] = state.currentBehavior;
},
updateProcesses(state, {targetMetaID, replaceMetaID}) {
for (let process of state.data.processes) {
updateProcesses(process, targetMetaID, replaceMetaID);
}
},
deleteProcessMeta(state, metaID) {
for (let i = 0, li = state.data.processes.length; i < li; i++) {
const process = state.data.processes[i];
if (process.id === metaID) {
state.data.processes.splice(i, 1);
break;
}
}
}
},
getters: {
customProcessMap: state => {
let map = {};
for (let process of state.data.processes) {
map[process.id] = process;
}
return map;
},
metaInUse: state => targetMetaID => {
let result = false;
for (let process of state.data.processes) {
if (metaInUse(process, targetMetaID)) {
result = true;
break;
}
}
return result;
},
},
actions: {
addCustomProcessMeta({commit, state}) {
let meta = {
id: generateUUID(),
name: 'Custom',
script: '',
options: {},
output: ['success', 'failed'],
};
commit('addCustomProcessMeta', meta);
return meta;
},
}
};
......@@ -28,13 +28,31 @@ export const envStore = {
prefabProcessTree: state => {
return groupProcesses(state.processes, process => process.isPrefab);
},
normalProcessTree: state => {
return groupProcesses(state.processes, process => !process.isPrefab);
builtinProcessTree: state => {
const customProcess = {
id: 'custom',
name: 'Custom',
};
const tree = groupProcesses(state.processes, process => !process.isPrefab);
tree.unshift(customProcess);
tree.push({
name: 'custom',
children: [],
});
return tree;
},
builtinsProcessMap: state => {
let map = {};
for (let process of state.processes) {
map[process.id] = process;
}
return map;
}
},
actions: {
async updateEnv({state, commit}) {
if(!state.initialized){
if (!state.initialized) {
const env = await envApi.fetchEnv();
commit('updateEnv', env);
}
......
......@@ -18,6 +18,7 @@ export const projectStore = {
views: [],
assets: [],
dataMapping: [],
processMap: {},
},
activeComponent: {},
activeComponentCopy: {}, // 当前选中节点的镜像,用来处理拖拽时数据变化频繁的问题
......@@ -40,20 +41,22 @@ export const projectStore = {
const localData = state.data;
if (data) {
const { views, assets, dataMapping } = JSON.parse(data);
const { views, assets, dataMapping, processes } = JSON.parse(data);
Vue.set(localData, 'views', views || []);
Vue.set(localData, 'assets', assets || []);
Vue.set(localData, 'dataMapping', dataMapping || []);
Vue.set(localData, 'processes', processes || []);
} else {
Vue.set(localData, 'views', []);
Vue.set(localData, 'assets', []);
Vue.set(localData, 'dataMapping', []);
Vue.set(localData, 'processes', []);
}
},
/**
* 激活组件
* @param {*} state
* @param {*} id
* @param {*} item
*/
activeComponent(state, item) {
if (item !== state.activeComponent) {
......@@ -135,6 +138,7 @@ export const projectStore = {
},
/**
* assets拖拽
* @param state
* @param {*} data
*/
assetDragStart(state, data) {
......@@ -327,7 +331,7 @@ export const projectStore = {
},
/**
* 选中节点
* @param {*} param
* @param {*} context
* @param {*} data
*/
activeComponent(context, data) {
......@@ -338,7 +342,7 @@ export const projectStore = {
} else {
return getTopView(node.parent);
}
}
};
let _view = getTopView(data.node);
if (_view && _view.data) {
......
......@@ -16,15 +16,54 @@ $dock-point-width: 9px;
}
.process-tree {
.scrollbar {
height: 100%;
.process-tree-node {
flex: 1;
flex-direction: row;
display: flex;
padding-right: 10px;
.node-name {
flex: 1;
width: 0;
text-overflow: ellipsis;
overflow: hidden;
font-size: 14px;
}
.edit-button {
visibility: hidden;
}
}
}
}
.behavior {
border: 1px solid $--border-color-base;
.background{
.background {
background-color: $--background-color-base;
}
.center {
display: flex;
flex-direction: column;
.edit-path {
padding: 5px;
height: 14px;
border-bottom: 1px solid $--border-color-light;
}
}
.board {
flex: 1;
.svg-board {
.line {
stroke: #979797;
......@@ -49,21 +88,21 @@ $dock-point-width: 9px;
user-select: none;
margin: 0 $dock-point-width;
&:hover {
/*&:hover {
border-color: $block-border-hover-background-color;
& > .header {
background-color: $block-border-hover-background-color;
}
}
}*/
&:focus {
/*&:focus {
border-color: $block-border-focus-background-color;
& > .header {
background-color: $block-border-focus-background-color;
}
}
}*/
.header {
min-height: 12px;
......@@ -73,6 +112,20 @@ $dock-point-width: 9px;
padding: 3px;
font-size: 12px;
color: white;
display: flex;
.title {
flex: 1;
}
.delete-button {
padding: 2px;
color: $--border-color-lighter;
&:hover {
color: white;
}
}
}
.body {
......@@ -85,14 +138,18 @@ $dock-point-width: 9px;
.field-item {
display: flex;
.key {
span {
flex: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
}
.key {
}
.value {
flex: 1;
text-align: right;
}
}
......@@ -143,11 +200,99 @@ $dock-point-width: 9px;
right: -$dock-point-width;
}
}
.active {
border-color: $block-border-focus-background-color;
& > .header {
background-color: $block-border-focus-background-color;
}
}
}
}
.properties {
display: flex;
flex-direction: column;
.el-input-group__prepend {
padding: 0 5px;
}
.el-button {
padding-left: 5px;
padding-right: 5px;
}
.wrapper {
padding: 5px;
flex: 1;
display: flex;
flex-direction: column;
.scrollbar {
padding-top: 5px;
flex: 1;
.el-form-item__content {
display: flex;
.el-input-number--mini {
flex: 1;
}
}
}
}
}
}
}
.meta-editor-wrapper {
height: 40vh;
display: flex;
padding: 5px;
flex-direction: column;
.info-editor {
margin-right: 5px;
}
.script-editor {
flex: 1;
}
}
.options-editor-dialog {
.scrollbar {
width: 100%;
height: 40vh;
}
.add-button {
margin-bottom: 5px;
}
.default-value {
width: 100%;
}
.operate-bar {
.el-button {
padding: 4px;
}
.edit-enum-button {
}
.delete-button {
margin-left: 3px;
}
}
}
.edit-enum-popover {
.el-select {
width: 400px;
}
}
......@@ -40,7 +40,7 @@
}
}
.bottom-bar, .toolbar {
.toolbar {
background-color: $--pane-background-color;
}
......
......@@ -27,3 +27,23 @@
padding: 6px 0;
}
.el-input__inner {
padding: 0 5px;
}
.el-input--suffix .el-input__inner{
padding-right: 5px;
}
.el-form-item--mini.el-form-item {
margin-bottom: 3px;
}
.el-textarea__inner {
height: 100%;
}
.el-tabs--border-card > .el-tabs__content{
padding: 5px;
}
.inspector-tabs {
flex: 1;
border: 0 !important;
display: flex;
flex-direction: column;
&>:last-child{
flex: 1;
}
.el-tabs__item {
height: 25px;
line-height: 25px;
}
.el-tab-pane{
height: 100%;
}
.zero-inspector-props-form {
height: 100%;
.el-input-number.el-input-number--mini, .el-select.el-select--mini {
width: 100%;
}
......@@ -23,5 +35,18 @@
.zero-slider > .el-slider__input {
width: 60px;
}
.scrollbar{
height: 100%;
}
}
.zero-inspector-behavior-form {
height: 100%;
.scrollbar{
height: 100%;
}
}
}
......@@ -59,8 +59,33 @@ export function saveAs(blob, fileName) {
link.click();
document.body.removeChild(link);
setTimeout(()=>{
setTimeout(() => {
URL.revokeObjectURL(url);
}, 500);
}
}
export function getEditorDefaultValue(property) {
return property.hasOwnProperty('default') ? property.default + '' : 'unset';
}
export function updateProcesses(process, targetMetaID, replaceMetaID) {
for (let key in process.sub) {
let subProcess = process.sub[key];
if (subProcess.meta === targetMetaID) {
subProcess.meta = replaceMetaID;
}
}
}
export function metaInUse(process, targetMetaID) {
let result = false;
for (let key in process.sub) {
let subProcess = process.sub[key];
if (subProcess.meta === targetMetaID) {
result = true;
break;
}
}
return result;
}
<template>
<behavior-editor style="width: 100%;height: 100%;display: flex;"></behavior-editor>
</template>
<script>
import BehaviorEditor from "./Editor/behavior-editor/BehaviorEditor";
export default {
name: "BehaviorEditorWrapper",
components: {BehaviorEditor}
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div class="zero-inspector-behavior-form" v-if="activeComponent.uuid">
<el-scrollbar class="scrollbar" wrap-class="wrap-x-hidden">
<el-form ref="form" size="mini" label-width="80px">
<div v-for="(evn, key) in eventsObj" :key="key">
<el-form-item label="触发事件:">
......@@ -18,6 +19,7 @@
</el-form-item>
</div>
</el-form>
</el-scrollbar>
<behavior-editor-dialog :behaviors="behaviors" @change="v => handleBehaviorsChange(v)" ref="behaviorEditorDialog"></behavior-editor-dialog>
</div>
</template>
......@@ -52,7 +54,7 @@ export default {
showBehaviorEditor(evn, key) {
this.currentEvent = key;
this.behaviors = evn.behaviors || [];
this.$refs.behaviorEditorDialog.show();
this.$refs.behaviorEditorDialog.show(this.behaviors, key);
},
/**
* 当前选中组件发生变化时,更新eventsObj的数据
......
<template>
<div class="zero-inspector-props-form" v-if="activeComponent.uuid">
<el-scrollbar class="scrollbar" wrap-class="wrap-x-hidden">
<el-form ref="form" size="mini" :model="form" label-width="80px">
<el-collapse v-model="configColl">
<el-collapse-item title="配置" name="properties">
......@@ -45,6 +46,7 @@
</el-collapse-item>
</el-collapse>
</el-form>
</el-scrollbar>
<!-- <div class="script-config-dialog" v-show="scriptDialog">
<el-tree :data="scripts" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</div> -->
......
......@@ -2,198 +2,139 @@
<div class="behavior">
<split-panes>
<split-panes splitpanes-min="20" :splitpanes-size="20" horizontal>
<process-list :data="prefabProcessTree" class="background full-size" splitpanes-min="20" :splitpanes-size="50"/>
<process-list :data="normalProcessTree" class="background full-size" splitpanes-min="20" :splitpanes-size="50"/>
<process-list @edit-meta="onEditMeta" @delete-meta="onDeleteMeta" :data="prefabProcessTree"
class="background full-size" splitpanes-min="20"
:splitpanes-size="30"/>
<process-list @edit-meta="onEditMeta" @delete-meta="onDeleteMeta" :data="normalProcessTree"
class="background full-size" splitpanes-min="20"
:splitpanes-size="70"/>
</split-panes>
<board ref="board" class="background full-size" :builtins="builtins" :mainProcess="mainProcess"
splitpanes-min="20" :splitpanes-size="60"/>
<properties class="background full-size" splitpanes-min="20" :splitpanes-size="20"/>
<div class="center full-size background" splitpanes-min="20" :splitpanes-size="70">
<edit-path :processStack="processStack" @pop="onPop"/>
<board ref="board" @select-process-node="onSelectProcessNode" @edit-process="editProcess"/>
</div>
<div class="properties background full-size" splitpanes-min="20" :splitpanes-size="30">
<properties-editor ref="properties"/>
</div>
</split-panes>
<meta-editor-dialog ref="metaEditorDialog" @input="onSaveMeta"/>
</div>
</template>
<script>
import {mapState, mapMutations, mapGetters} from 'vuex'
import {mapState, mapMutations, mapGetters, mapActions} from 'vuex'
import Board from "./Board";
import SplitPanes from 'splitpanes'
import ProcessList from "./ProcessList";
import Properties from "./Properties";
const builtins = {
entry: {
id: 'entry',
name: 'Entry',
options: {},
script: "resolve({type: 'success'});",
output: ['success'],
},
wait: {
id: 'wait',
name: 'Wait',
options: {
duration: {type: 'number', default: 1000},
},
script: "setTimeout(function(){resolve({type: 'complete'})}, options.duration || 0);",
output: ['complete'],
},
};
const mainProcess = {
uuid: '1',
alias: '主过程',
meta: {
id: 'main',
name: 'Main',
options: {},
metas: {
compare: {
id: 'compare',
name: 'Compare',
options: {
left: {type: 'any', default: ''},
right: {type: 'any', default: ''},
operator: {type: 'string', default: '=='},
},
script: `
let leftValue = typeof options.left === 'object' ? args[options.left.path] : options.left;
let rightValue = typeof options.right === 'object' ? args[options.right.path] : options.right;
let func = new Function('return '+leftValue+args.operator+rightValue);
let result = func();
resolve({type: result ? 'equal' : 'unequal'});
`,
output: ['complete'],
},
nestProc: {
id: 'nestProc',
name: 'NestProc',
metas: {
print: {
id: 'print',
name: 'Print',
options: {
text: {type: 'string', default: ''},
},
script: "console.log(options.text);resolve({type: 'success'});",
output: ['success'],
}
},
options: {},
subEntry: '1',
sub: {
1: {
uuid: '1',
meta: 'wait',
alias: '等待',
options: {
duration: 500,
},
output: ['2'],
},
2: {
uuid: '2',
alias: '打印',
meta: 'print',
options: {
text: 'hello',
},
output: [],
},
},
},
test: {
options: {
text: {type: 'string', default: ''},
},
script: "console.log(args, options);resolve({type: 'success'});",
output: ['success', 'failed'],
},
},
subEntry: '1',
sub: {
1: {
uuid: '1',
alias: '入口',
meta: 'entry',
output: {
success: ['2'],
},
design: {
x: 10,
y: 10,
},
},
2: {
uuid: '2',
alias: 'test',
meta: 'test',
options: {
text: 'hello',
},
output: {
success: ['3'],
failed: [],
},
design: {
x: 20,
y: 100,
},
},
3: {
uuid: '3',
alias: '等待',
meta: 'wait',
options: {
duration: 500,
},
output: {
complete: ['4']
},
design: {
x: 200,
y: 50,
},
},
4: {
uuid: '4',
alias: 'nestProc',
meta: 'nestProc',
options: {
text: 'hello',
},
output: [],
design: {
x: 150,
y: 200,
},
},
}
}
};
import PropertiesEditor from "./PropertiesEditor";
import EditPath from "./Board/EditPath";
import Process from "./Board/Process";
import MetaEditorDialog from "./MetaEditorDialog";
export default {
name: "BehaviorEditor",
components: {Properties, ProcessList, Board, SplitPanes,},
components: {MetaEditorDialog, PropertiesEditor, EditPath, ProcessList, Board, SplitPanes,},
props: [],
data() {
return {
builtins,
mainProcess,
processStack: [],
metaInEditing: null,
}
},
computed: {
processContext() {
const {builtinsProcessMap, customProcessMap} = this.$store.getters;
return [
builtinsProcessMap,
customProcessMap,
]
},
normalProcessTree() {
const tree = this.builtinProcessTree;
const group = tree.find(item => item.name === 'custom');
group.children = this.$store.state.behavior.data.processes;
return tree;
},
...mapState({
processes: state => state.env.processes,
behavior: state => state.behavior.currentBehavior,
}),
...mapGetters([
'prefabProcessTree',
'normalProcessTree'
'builtinProcessTree'
])
},
mounted() {
},
methods: {
measure() {
this.$refs.board.measure();
resolveProcess(id) {
for (let context of this.processContext) {
if (context[id]) {
return context[id];
}
}
},
edit() {
this.processStack.splice(0);
let process = new Process(null, this.behavior, this.resolveProcess);
this.editProcess(process);
},
onSelectProcessNode(process) {
this.$refs.properties.edit(process);
},
editProcess(process) {
this.processStack.push(process);
this.$refs.board.edit(process, this.resolveProcess);
this.$refs.properties.edit();
},
onPop(index) {
this.processStack.splice(index + 1);
let process = this.processStack[this.processStack.length - 1];
this.$refs.board.edit(process, this.resolveProcess);
this.$refs.properties.edit();
},
onEditMeta(meta) {
this.metaInEditing = meta;
this.$refs.metaEditorDialog.edit(meta);
},
onDeleteMeta(meta) {
const inUse = this.$store.getters.metaInUse(meta.id);
if (inUse) {
this.$alert(this.$t('Meta is in use, can not delete'), this.$t('Alert'))
.catch((e) => {
});
} else {
this.$confirm(this.$t('Are you sure to delete this meta'), this.$t('Alert'), {
confirmButtonText: this.$t('Delete'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.deleteProcessMeta(meta.id);
}).catch((e) => {
});
}
},
onSaveMeta(meta) {
let oldMetaID = this.metaInEditing.id;
for (let key in meta) {
this.metaInEditing[key] = meta[key];
}
this.metaInEditing = null;
if (oldMetaID !== meta.id) {
this.updateProcesses({
targetMetaID: oldMetaID,
replaceMetaID: meta.id,
});
}
this.$refs.board.updateProcessNode();
},
...mapMutations([
'updateProcesses',
'deleteProcessMeta',
]),
...mapGetters([
'metaInUse',
]),
}
}
</script>
......
<template>
<div class="board">
<svg class="svg-board full-size" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<div class="board" @dragover="onDragOver" @drop="onDrop">
<svg class="svg-board full-size" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="layer" stroke-width="2" fill="none" fill-rule="evenodd">
<link-line v-for="(line, key, index) in lines" :data="line" :key="index" @dblclick="onDeleteLine"></link-line>
<path v-show="lineDrawing.visible" class="line hover" :d="lineDrawing.path"></path>
</g>
<g id="nodes">
<process-node v-for="(process, key, index) of processMap" :process="process" :key="index"
<process-node v-for="(process, key, index) of subProcessMap" :ref="'pn_' + key" :process="process" :key="index"
@click="onClickProcessNode(process, key)"
@hover-point="onPointHover"
@leave-point="onPointLeave"
@down-point="onPointDown"
@delete="onPointDelete"
@dblclick="editSubProcess(process)"
/>
</g>
</svg>
......@@ -18,6 +22,7 @@
</template>
<script>
import {mapState, mapMutations, mapGetters, mapActions} from 'vuex'
import ProcessNode from "./Board/ProcessNode";
import Process from "./Board/Process";
import LinkLine from "./Board/LinkLine";
......@@ -25,46 +30,117 @@
import {DOCK_POINT_OFFSET} from "../../../config";
import {state} from "./Board/state";
import events from "../../../global-events";
import generateUUID from "uuid/v4";
export default {
name: "Board",
components: {ToolTip, LinkLine, ProcessNode,},
props: ['builtins', 'mainProcess'],
props: [],
data() {
let processMap = {};
let currentProcess = new Process(null, this.mainProcess, this.builtins);
for (let id in currentProcess.meta.sub) {
const subData = currentProcess.meta.sub[id];
processMap[id] = new Process(currentProcess, subData, this.builtins)
}
return {
processMap,
process: null,
subProcessMap: null,
selectedProcessNode: null,
lines: {},
lineDrawing: {
visible: false,
path: ''
path: '',
process: null,
}
}
},
mounted() {
},
computed: {},
methods: {
measure(){
...mapActions([
'addCustomProcessMeta',
]),
async edit(process, resolveProcess) {
this.selectedProcessNode = null;
this.process = process;
this.resolveProcess = resolveProcess;
if (!this.process.meta.subEntry) {
const subProcessData = await this.addSubProcessData('entry', {x: 10, y: 10,});
this.$set(this.process.meta, 'subEntry', subProcessData.uuid);
}
this.updateSubProcess();
this.measure();
},
updateSubProcess() {
this.subProcessMap = {};
for (let uuid in this.process.meta.sub) {
const subData = this.process.meta.sub[uuid];
this.addSubProcess(uuid, subData);
}
},
addSubProcess(uuid, data) {
const process = new Process(this.process, data, this.resolveProcess);
this.$set(this.subProcessMap, uuid, process);
},
async addSubProcessData(processId, pos) {
let process;
if (processId === 'custom') {
const processMeta = await this.addCustomProcessMeta();
processId = processMeta.id;
}
process = this.resolveProcess(processId);
let data = {
uuid: generateUUID(),
meta: process.id,
design: {
x: pos.x,
y: pos.y,
},
};
if (!this.process.meta.hasOwnProperty('sub')) {
this.$set(this.process.meta, 'sub', {});
}
this.$set(this.process.meta.sub, data.uuid, data);
return data;
},
onDragOver(e) {
let {types} = e.dataTransfer;
if (types.includes('process')) {
e.preventDefault();
}
},
async onDrop(e) {
const processId = e.dataTransfer.getData('process');
if (processId === 'entry') {
return;
}
const data = await this.addSubProcessData(processId, {
x: e.offsetX - 9,
y: e.offsetY,
});
this.addSubProcess(data.uuid, data);
this.$nextTick(() => {
events.$emit('update-dock-point-pos');
});
},
measure() {
const {x, y} = this.$el.getBoundingClientRect();
console.log(x, y);
state.boardOffset.x = x;
state.boardOffset.y = y;
this.$nextTick(() => {
events.$emit('update-dock-point-pos');
this.updateLines();
});
},
updateLines() {
this.lines = {};
for (let id in this.processMap) {
const process = this.processMap[id];
for (let uuid in this.subProcessMap) {
const process = this.subProcessMap[uuid];
const {output} = process.data;
for (let outputType in output) {
......@@ -77,7 +153,7 @@
}
},
addLine(process, outputID, outputType, outputIndex) {
const nextProcess = this.processMap[outputID];
const nextProcess = this.subProcessMap[outputID];
if (nextProcess) {
this.$set(this.lines, state.lineID, {
id: state.lineID,
......@@ -120,7 +196,7 @@
this.lineDrawing.visible = false;
state.drawing = false;
if(state.targetUUID){
if (state.targetUUID && state.targetUUID !== this.processDrawing.uuid) {
this.processDrawing.output[this.pointDrawing] = [state.targetUUID];
this.addLine(this.processDrawing, state.targetUUID, this.pointDrawing, 0);
......@@ -131,6 +207,48 @@
const {prev, outputType, outputIndex, id} = line;
prev.output[outputType].splice(outputIndex, 1);
this.$delete(this.lines, id);
},
onPointDelete(process) {
this.$delete(this.subProcessMap, process.uuid);
this.$delete(this.process.meta.sub, process.uuid);
for (let id in this.lines) {
const line = this.lines[id];
const {prev, next} = line;
if (prev === process || next === process) {
this.onDeleteLine(line);
}
}
},
editSubProcess(process) {
if (process.meta.type !== 'builtin' || process.meta.sub && Object.keys(process.meta.sub).length > 0) {
this.$emit('edit-process', process);
}
},
onClickProcessNode(process, uuid) {
for (let key in this.$refs) {
if (key.startsWith('pn_')) {
const processNode = this.$refs[key][0];
if (processNode) {
let hint = 'pn_' + uuid === key;
processNode.setActive(hint);
if (hint && this.selectedProcessNode !== processNode) {
this.selectedProcessNode = processNode;
this.$emit('select-process-node', process);
}
}
}
}
},
updateProcessNode() {
if (this.selectedProcessNode) {
this.$nextTick(()=>{
this.selectedProcessNode.updateSize();
this.selectedProcessNode.updateDockPointPos();
});
}
}
}
}
......
<template>
<el-breadcrumb class="edit-path" separator-class="el-icon-arrow-right">
<el-breadcrumb-item v-for="(process, index) in processStack" :key="index">
<span v-if="index === processStack.length - 1">{{parseName(process)}}</span>
<el-link v-else @click="onClickItem(process, index)">{{parseName(process)}}</el-link>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
name: "EditPath",
props: ['processStack'],
watch: {
processStack(v){
//console.log(v);
}
},
methods: {
parseName(process){
return process.data.alias || process.meta.name;
},
onClickItem(process, index){
this.$emit('pop', index);
},
}
}
</script>
<style scoped>
</style>
\ No newline at end of file
......@@ -3,28 +3,24 @@
*/
export default class Process {
constructor(parent, data, builtins) {
this._builtins = builtins;
constructor(parent, data, resolveProcess) {
this._resolveProcess = resolveProcess;
this._parent = parent;
this._data = data;
this._meta = typeof data.meta === 'string' ? this.resolveMeta(data.meta) : data.meta;
this.meta = typeof data.meta === 'string' ? this.resolveMeta(data.meta) : data.meta;
}
get data(){
return this._data;
}
get meta(){
return this._meta;
}
resolveMeta(name) {
let meta = this._meta ? this._meta.metas[name] : null;
resolveMeta(id) {
let meta = this.meta && this.meta.metas ? this.meta.metas[id] : null;
if (!meta && this._parent) {
meta = this._parent.resolveMeta(name);
meta = this._parent.resolveMeta(id);
}
if(!meta){
meta = this._builtins[name];
meta = this._resolveProcess(id);
}
return meta;
......
<template>
<foreignObject :x="data.design.x" :y="data.design.y" :width="width" :height="height">
<div ref="node" class="node" tabindex="0" @mousedown="onMouseDown" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave">
<div ref="node" :class="{active: active}" class="node" @mousedown="onMouseDown" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave" @click="onClick" @dblclick="onDblclick">
<div class="header">
<span>{{data.alias || meta.name}}</span>
<span class="title">{{data.alias || meta.name}}</span>
<i v-if="meta.id !== 'entry'" class="delete-button el-icon-delete" @click.stop="onClickDelete" @mousedown.stop.prevent></i>
</div>
<div class="body">
<div class="field-item" v-for="(param, key, index) in meta.options" :key="index">
<span class="key">{{key}}</span>:
<span class="value">{{data.options[key]}}</span>
<span class="key">{{param.alias || key}}</span>:
<span class="value">{{data.options[key] || meta.options[key].default}}</span>
</div>
</div>
<div ref="inputDock" class="dock input">
......@@ -28,30 +30,29 @@
import DockPoint from "./DockPoint";
import {state} from "./state";
import events from "../../../../global-events";
//todo 容器坐标改变影响节点坐标
export default {
name: "ProcessNode",
components: {DockPoint},
props: ['process'],
data() {
this.prepare();
const inputMeta = this.process.meta.name === 'Entry' ? [] : ['default'];
const inputMeta = this.process.meta.id === 'entry' ? [] : ['default'];
return {
width: 130,
height: 100,
inputMeta,
active: false,
}
},
created() {
},
mounted() {
let bounds = this.$refs.node.getBoundingClientRect();
this.width = bounds.width + 9;
this.height = bounds.height;
this.updateSize();
events.$on('update-dock-point-pos', this.updateDockPointPos);
},
destroyed() {
events.$off('update-dock-point-pos', this.updateDockPointPos);
},
computed: {
meta() {
return this.process.meta;
......@@ -62,26 +63,39 @@
},
watch: {
process(v) {
}
this.active = false;
this.prepare();
this.updateSize();
},
},
methods: {
prepare() {
let {design, options} = this.process.data;
let {design, options, output} = this.process.data;
if (!design) {
this.$set(this.process.data, 'design', {});
design = this.process.data.design;
}
if (!options) {
this.$set(this.process.data, 'options', {});
options = this.process.data.options;
}
if (!design.x) {
this.$set(design, 'x', 0);
}
if (!design.y) {
this.$set(design, 'y', 0);
}
if (!options) {
this.$set(this.process.data, 'options', {});
}
if (!output) {
this.$set(this.process.data, 'output', {});
}
},
onClick(e) {
this.$emit('click', e);
},
onDblclick(e) {
this.$emit('dblclick', e);
},
setActive(active){
this.active = active;
},
onMouseEnter(e) {
if (state.drawing && this.meta.id !== 'entry') {
......@@ -121,6 +135,13 @@
this.$emit('change-position', this, this);
}
},
updateSize(){
this.$nextTick(()=>{
let bounds = this.$refs.node.getBoundingClientRect();
this.width = bounds.width + 18;
this.height = bounds.height;
});
},
updateDockPointPos() {
const {x: dx, y: dy} = this.process.data.design;
const {x: offX, y: offY} = state.boardOffset;
......@@ -138,7 +159,7 @@
let dockPoint = container.children[i];
const {x, y} = dockPoint.getBoundingClientRect();
posArr.push({
x: x - dx - offX,
x: x - dx - offX + 4.5,
y: y - dy - offY,
});
}
......@@ -160,6 +181,9 @@
this.$emit('down-point', e, this.data, point);
}
},
onClickDelete() {
this.$emit('delete', this.data);
},
}
}
</script>
......
......@@ -52,5 +52,6 @@
padding: 5px;
border-radius: 5px;
pointer-events: none;
z-index: 2;
}
</style>
\ No newline at end of file
<template>
<el-dialog :title="$t('Meta Editor')" width="80%" :visible.sync="visible"
:close-on-click-modal="false"
:append-to-body="true">
<div class="meta-editor-wrapper">
<el-form :inline="true" class="info-editor" size="mini">
<template v-if="meta">
<el-form-item label="ID">
<el-input v-model="meta.id" placeholder="ID" :readonly="!editable"/>
</el-form-item>
<el-form-item label="Name">
<el-input v-model="meta.name" placeholder="Name" :readonly="!editable"/>
</el-form-item>
</template>
</el-form>
<el-form class="info-editor" size="mini">
<template v-if="meta">
<el-form-item label="Options">
<el-link :underline="false" @click="onClickEditOptions" :disabled="!editable">
<template v-if="Object.keys(meta.options).length">
<el-tag type="success" size="mini" v-for="(option, key) in meta.options" :key="key">{{key}}</el-tag>
</template>
<template v-else>Nothing</template>
</el-link>
</el-form-item>
<el-form-item label="Output">
<div style="display: flex;flex: 1;">
<el-select style="flex: 1;" v-model="meta.output" :disabled="!editable" allow-create filterable multiple
placeholder="Output"/>
</div>
</el-form-item>
</template>
</el-form>
<el-input v-if="meta" class="script-editor" :readonly=" !editable" type="textarea"
v-model="meta.script"></el-input>
</div>
<div slot="footer" class="dialog-footer">
<div class="button-bar">
<el-button size="mini" plain @click="cancel">Cancel</el-button>
<el-button size="mini" plain @click="save">Save</el-button>
</div>
</div>
<options-editor-dialog ref="optionsEditorDialog"/>
</el-dialog>
</template>
<script>
import ElFormItem from "./editors/form-item";
import OptionsEditorDialog from "./OptionsEditorDialog";
export default {
name: "MetaEditorDialog",
components: {OptionsEditorDialog, ElFormItem},
data() {
return {
visible: false,
meta: null,
optionsEditorVisible: false,
}
},
computed: {
editable() {
return this.meta && this.meta.type !== 'builtin';
},
options() {
return Object.keys(this.meta.options).join(',')
},
},
methods: {
edit(meta) {
this.visible = true;
this.meta = JSON.parse(JSON.stringify(meta));
},
onClickEditOptions() {
this.$refs.optionsEditorDialog.edit(this.meta.options);
},
save() {
this.$emit('input', this.meta);
this.visible = false;
},
cancel(){
this.visible = false;
}
}
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<el-dialog :title="$t('Options Editor')" width="80%" :visible.sync="visible"
:close-on-click-modal="false"
:append-to-body="true">
<div class="options-editor-dialog">
<el-button class="add-button" size="mini" circle icon="el-icon-plus" plain @click="addItem"/>
<el-scrollbar class="scrollbar" wrap-class="wrap-x-hidden" v-if="options"
view-class="scrollbar-view">
<div class="wrapper">
<el-table
:data="options"
style="width: 100%">
<el-table-column
label="Type"
width="100">
<template slot-scope="scope">
<el-select v-model="scope.row.option.type" size="mini">
<el-option v-for="type in types"
:key="type"
:label="type"
:value="type"
>
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column
label="Key"
width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.key" size="mini"/>
</template>
</el-table-column>
<el-table-column
label="Alias"
width="100">
<template slot-scope="scope">
<el-input v-model="scope.row.option.alias" size="mini"/>
</template>
</el-table-column>
<el-table-column
label="Default">
<template slot-scope="scope">
<el-input v-if="scope.row.option.type === 'string' || scope.row.option.type === 'enum'" class="default-value" v-model="scope.row.option.default" size="mini" placeholder="Default"/>
<el-input-number v-if="scope.row.option.type === 'number'" controls-position="right" class="default-value" v-model="scope.row.option.default" size="mini" placeholder="Default"/>
<el-switch v-if="scope.row.option.type === 'boolean'" class="default-value" v-model="scope.row.option.default" size="mini"/>
</template>
</el-table-column>
<el-table-column
label=""
width="70">
<template slot-scope="scope">
<div class="operate-bar">
<el-popover
class="edit-enum-button"
trigger="click">
<div class="edit-enum-popover">
<el-select size="mini" allow-create multiple filterable v-model="scope.row.option.enum"/>
</div>
<el-button slot="reference" class="edit-button" size="mini" circle icon="el-icon-edit" plain :disabled="scope.row.option.type!=='enum'"
@click="editEnum(scope)"/>
</el-popover>
<el-button class="delete-button" size="mini" circle icon="el-icon-minus" type="danger" plain
@click="deleteItem(scope.$index)"/>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-scrollbar>
</div>
<div slot="footer" class="dialog-footer">
<div class="button-bar">
<el-button size="mini" plain @click="cancel">Cancel</el-button>
<el-button size="mini" plain @click="save">Save</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: "OptionsEditorDialog",
data() {
return {
visible: false,
originOptions: null,
copiedOptions: null,
options: [],
types: [
'boolean',
'string',
'number',
'enum',
],
}
},
methods: {
edit(options) {
this.visible = true;
this.originOptions = options;
this.copiedOptions = JSON.parse(JSON.stringify(options));
this.options.splice(0);
for (let key in this.copiedOptions) {
let option = this.copiedOptions[key];
this.options.push({
key,
option,
})
}
},
save(){
let keys = Object.keys(this.originOptions);
for (let key of keys) {
this.$delete(this.originOptions, key);
}
for(let item of this.options){
if(item.key){
this.$set(this.originOptions, item.key, item.option);
}
}
this.visible = false;
},
cancel(){
this.visible = false;
},
editEnum(item){
},
addItem(){
this.options.push({
option: {
type: 'string',
}
})
},
deleteItem(index){
this.options.splice(index, 1);
},
},
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div>
<div class="process-tree">
<el-scrollbar class="scrollbar" wrap-class="wrap-x-hidden">
<el-tree
v-model="data"
:data="data"
:props="defaultProps"
empty-text=""
>
<div slot-scope="{ node, data }" class="process-tree-node">
<div class="node-name">
<span :draggable="draggable(data)" @dragstart.stop="dragProcessStart(data, $event)">{{data.name}}</span>
</div>
<el-dropdown v-if="data.type !== 'builtin' && !data.hasOwnProperty('children') && data.id !== 'custom'" class="more-button" size="mini" trigger="click"
@command="(command)=>{onMoreMenu(command, data, node)}">
<el-link icon="el-icon-more" :underline="false" @click.stop/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="edit">{{$t('Edit')}}</el-dropdown-item>
<el-dropdown-item command="delete">{{$t('Delete')}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-tree>
</el-scrollbar>
</div>
</template>
<script>
......@@ -14,6 +32,32 @@
props: [
'data',
],
data() {
return {
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
draggable(data) {
return !data.hasOwnProperty('children');
},
dragProcessStart(data, event) {
event.dataTransfer.setData('process', data.id);
},
onMoreMenu(command, data, node) {
switch (command) {
case 'edit':
this.$emit('edit-meta', data);
break;
case 'delete':
this.$emit('delete-meta', data);
break;
}
}
},
}
</script>
......
<template>
<div class="properties">
<span>Properties</span>
</div>
</template>
<script>
export default {
name: "Properties"
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div class="wrapper" v-if="process">
<el-input v-model="process.data.alias" clearable :placeholder="process.meta.name" size="mini">
<template slot="prepend">{{$t('Name')}}</template>
</el-input>
<el-scrollbar class="scrollbar" wrap-class="wrap-x-hidden"
view-class="scrollbar-view">
<el-form v-model="process" size="mini" label-width="80px" label-position="left" @submit.prevent>
<component v-for="(property, key) in process.meta.options"
:is="getEditor(property)"
v-model="process.data.options[key]"
:propertyName="key"
:property="property"
:container="process"
:key="key"
/>
</el-form>
</el-scrollbar>
</div>
</template>
<script>
import NumberEditor from "./editors/NumberEditor";
import StringEditor from "./editors/StringEditor";
import EnumEditor from "./editors/EnumEditor";
import BooleanEditor from "./editors/BooleanEditor";
const editorMapping = {
number: 'NumberEditor',
string: 'StringEditor',
enum: 'EnumEditor',
boolean: 'BooleanEditor',
};
export default {
name: "PropertiesEditor",
components: {BooleanEditor, EnumEditor, NumberEditor, StringEditor},
data() {
return {
process: null,
}
},
methods: {
edit(process) {
this.process = process;
},
getEditor(property) {
return editorMapping[property.type];
},
}
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<editor-wrapper :property="property" :propertyName="propertyName">
<el-switch :value="editValue" @input="onInput"
class="picker"></el-switch>
</editor-wrapper>
</template>
<script>
import EditorWrapper from "./EditorWrapper";
export default {
name: "BooleanEditor",
components: {EditorWrapper},
props: ['value', 'container', 'property', 'propertyName'],
data() {
return {}
},
computed: {
editValue() {
return this.value === undefined ? this.property.default : this.value;
},
},
methods: {
onInput(v) {
if (v !== this.value) {
this.$emit('input', v, this.container, this.propertyName, this.value);
}
}
},
}
</script>
<style scoped>
.picker {
float: right;
margin-top: 4px;
}
</style>
<template>
<editor-wrapper :property="property" class="color-editor-container">
<el-color-picker
class="picker"
:value="editValue"
@input="onInput"
show-alpha
:predefine="predefineColors">
</el-color-picker>
</editor-wrapper>
</template>
<script>
import EditorWrapper from "./EditorWrapper";
export default {
name: "ColorEditor",
components: {EditorWrapper,},
props: ['component', 'value', 'property'],
data() {
return {
predefineColors: [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577'
]
}
},
computed: {
editValue() {
return this.value === undefined ? this.property.defaultValue : this.value;
},
},
methods: {
onInput(v) {
if (v !== this.value) {
this.$emit('input', v, this.component, this.property.name, this.value);
}
}
},
}
</script>
<style scoped>
.color-editor-container {
height: 29px;
overflow: hidden;
}
.picker {
float: right;
}
</style>
<template>
<el-form-item class="editor-wrapper" :label="property.alias || propertyName" content-float="right" :content-width="contentWidth" :labelOffsetTop="labelOffsetTop">
<slot></slot>
</el-form-item>
</template>
<script>
import camelcase from 'camelcase'
import ElFormItem from "./form-item";
export default {
name: "EditorWrapper",
components: {ElFormItem},
props: {
property: Object,
propertyName: String,
contentWidth: {
type: String,
default: '65%',
},
labelOffsetTop: {
type: Number,
default: 0,
},
},
computed: {
},
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<editor-wrapper :property="property" :propertyName="propertyName">
<enum-select :value="value" @input="onInput" :property="property"></enum-select>
</editor-wrapper>
</template>
<script>
import EnumSelect from "./EnumEditor/EnumSelect";
import EditorWrapper from "./EditorWrapper";
export default {
name: "EnumEditor",
components: {EnumSelect, EditorWrapper,},
props: ['value', 'container', 'property', 'propertyName'],
methods: {
onInput(v, oldValue){
if(v !== this.value){
this.$emit('input', v, this.container, this.propertyName, oldValue);
}
}
},
}
</script>
<style scoped>
</style>
<template>
<el-select :value="editValue" @input="onInput" :placeholder="property.default" class="el-select">
<el-option
v-for="(item, key) in property.enum"
:key="item"
:label="item"
:value="item">
<span>{{item}}</span>
<span class="comment"></span>
</el-option>
</el-select>
</template>
<script>
export default {
name: "EnumSelect",
props: ['value', 'property'],
data(){
return {
}
},
computed: {
editValue(){
return this.value || this.property.default;
},
},
methods: {
onInput(v){
if(v !== this.value){
this.$emit('input', v, this.value);
}
}
},
}
</script>
<style scoped>
.el-select {
width: 100%;
}
.comment {
float: right;
color: darkgray;
font-size: 12px;
}
</style>
\ No newline at end of file
<template>
<el-form-item class="wrapper editor-event-wrapper" label-width="0">
<div class="header">
<span class="event-name">{{property.name}}</span>
<el-button icon="el-icon-plus" circle plain type="primary" class="mini-button" @click="onClickAdd"></el-button>
</div>
<div class="list" v-if="editValue">
<div class="event-item" v-for="(item, index) in editValue">
<div class="method-path">
<entity-linker class="entity-linker" v-model="item.entity" @input="onEntityChange(item, index)"></entity-linker>
<el-popover
popper-class="tooltip-popover"
class="method-name-container"
placement="top"
trigger="hover"
:disabled="getMethodName(item, index).length === 0"
:open-delay="400"
:content="getMethodName(item, index)">
<div slot="reference" class="method-menu editor-event-method-menu" @click="onShowMethodMenu(item, index)">{{getMethodName(item, index)}}
</div>
</el-popover>
</div>
<div class="params-box">
<el-form-item class="param-input" label="param" label-width="45px">
<el-input v-model="item.param"></el-input>
</el-form-item>
<el-button icon="el-icon-close" @click="onClickDelete(item, index)" circle plain class="mini-button"
type="danger"></el-button>
</div>
</div>
</div>
</el-form-item>
</template>
<script>
import ElFormItem from "./form-item";
import EntityLinker from "./EntityEditor/EntityLinker";
import {getUUIDFromLink} from "../../../../../utils";
import {getCurrentProject, getCurrentScene} from "../../../../../editor";
import {showMethodMenu} from "../../../../../menus/index";
import {getClassDeclare} from "../../../../../components-manager";
export default {
name: "EventEditor",
components: {EntityLinker, ElFormItem},
props: ['component', 'value', 'property'],
data() {
this.storeOldValue(this.value);
return {
methodName: '',
editValue: this.value,
}
},
watch: {
value() {
this.editValue = this.value;
this.storeOldValue();
},
},
methods: {
storeOldValue(v){
let value = v || this.editValue;
this.oldValue = value === undefined ? undefined : JSON.parse(JSON.stringify(value));
},
onShowMethodMenu(item, index) {
let entity = this.getEntity(item.entity);
showMethodMenu(entity, event.x, event.y, index, this.onSelectMethod);
},
onSelectMethod(index, componentIndex, method) {
let item = this.editValue[index];
if(componentIndex < 0){
item.component = null;
item.method = null;
}else{
item.component = componentIndex;
item.method = method;
}
this.emitInput();
},
onEntityChange(item, index) {
item.component = null;
item.method = null;
this.emitInput();
},
onClickAdd() {
if (this.editValue === undefined) {
this.editValue = [];
}
this.editValue.push({
entity: null,
component: null,
method: null,
});
this.emitInput();
},
onClickDelete(item, index) {
this.editValue.splice(index, 1);
this.emitInput();
},
emitInput() {
this.$emit('input', this.editValue.length > 0 ? this.editValue : undefined, this.component, this.property.name, this.oldValue);
},
getEntity(link) {
if (link) {
let {type, uuid} = getUUIDFromLink(link);
if (type === 'entity') {
let _entity = getCurrentScene().findEntityByUUID(uuid);
if (_entity) {
return _entity;
}
}
}
},
getMethodName({entity: entityLink, component, method}, index) {
let entity = this.getEntity(entityLink);
if (entity) {
let comp = entity.components[component];
if (comp) {
let declare = getClassDeclare(comp.script);
return declare.className + '.' + method;
}
return '';
} else {
return '';
}
}
}
}
</script>
<style scoped>
.wrapper {
border-radius: 3px;
width: 100%;
height: 100%;
padding: 5px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.event-name {
}
.list {
}
.event-item {
display: flex;
flex-direction: column;
margin: 3px 0;
}
.method-path {
display: flex;
align-items: flex-end;
margin-bottom: 5px;
}
.params-box{
display: flex;
align-items: center;
}
.param-input{
flex: 1;
margin-right: 5px;
}
.entity-linker {
flex: 1;
}
.method-menu {
height: 28px;
padding: 0 3px;
border-radius: 3px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.method-menu:focus {
outline: none;
}
.method-name-container {
width: 120px;
margin: 0 5px;
}
.mini-button {
padding: 3px !important;
}
</style>
\ No newline at end of file
<template>
<editor-wrapper :property="property" :propertyName="propertyName">
<el-input-number :value="editValue" @input="onInput" controls-position="right"
:placeholder="defaultValue"></el-input-number>
</editor-wrapper>
</template>
<script>
import EditorWrapper from "./EditorWrapper";
import {getEditorDefaultValue} from "../../../../utils";
export default {
name: "NumberEditor",
components: {EditorWrapper,},
props: ['value', 'container', 'property', 'propertyName'],
computed: {
editValue() {
return this.value === undefined ? this.property.default : this.value;
},
defaultValue(){
return getEditorDefaultValue(this.property);
},
},
methods: {
onInput(v) {
if(v !== this.value){
this.$emit('input', v, this.container, this.propertyName, this.value);
}
}
},
}
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<editor-wrapper :property="property">
<div class="texture-box">
<el-popover
placement="top"
popper-class="raw-editor-popover"
class="raw-name-container"
trigger="manual"
width="400"
v-model="popoverVisible">
<json-editor ref="jsonEditor" :value="editValue" @input="onInput" @cancel="onCancel"></json-editor>
<div slot="reference" class="editor-texture-name">{{rawString}}</div>
</el-popover>
<el-button-group>
<el-button :icon="editButtonIcon" @click="onClickEdit"></el-button>
<el-button icon="el-icon-delete" @click="onClickClean"></el-button>
</el-button-group>
</div>
</editor-wrapper>
</template>
<script>
import JsonEditor from "./JsonEditor";
import EditorWrapper from "./EditorWrapper";
export default {
name: "RawEditor",
components: {JsonEditor, EditorWrapper},
props: ['component', 'value', 'property'],
data() {
return {
popoverVisible: false,
}
},
computed: {
editButtonIcon() {
return this.popoverVisible ? 'el-icon-check' : 'el-icon-edit';
},
rawString() {
return this.editValue ? JSON.stringify(this.editValue) : '';
},
editValue(){
return this.value ? this.value.data : null;
}
},
watch: {
popoverVisible(v){
if(v){
this.$refs.jsonEditor.initValue();
}
}
},
methods: {
onClickEdit() {
this.popoverVisible = !this.popoverVisible;
},
onClickClean() {
this.$emit('input', undefined, this.component, this.property.name, this.value);
},
onInput(v) {
if(v !== this.value){
this.$emit('input', {
_type_: 'raw',
data: v,
}, this.component, this.property.name, this.value);
}
this.popoverVisible = false;
},
onCancel(){
this.popoverVisible = false;
}
},
}
</script>
<style scoped>
.texture-box {
width: 100%;
height: 28px;
display: flex;
font-size: 12px;
}
.editor-texture-name {
line-height: 28px;
height: 28px;
border-radius: 4px 0 0 4px;
color: gray;
padding: 0 5px;
user-select: none;
}
.raw-name-container {
flex: 1;
overflow: hidden;
}
.invalid-link {
color: red;
}
.inspector-append {
width: 45px;
}
</style>
<template>
<editor-wrapper :property="property" :propertyName="propertyName">
<div style="display: flex;flex: 1;">
<el-popover
placement="top"
popper-class="input-area-popover"
class="string-name-container"
trigger="manual"
width="400"
v-model="popoverVisible"
>
<div>
<el-input
type="textarea"
v-model="popoverEditValue"
:placeholder="defaultValue"
:rows="6">
</el-input>
<div class="bottom-bar">
<el-button @click="onClean" type="danger" plain>Clean</el-button>
<el-button-group>
<el-button @click="onCancel" plain>Cancel</el-button>
<el-button @click="onConfirm" type="primary" plain>Confirm</el-button>
</el-button-group>
</div>
</div>
<el-input clearable slot="reference" v-model="editValue" @change="onChange" :placeholder="defaultValue"/>
</el-popover>
<el-button-group>
<el-button :icon="editButtonIcon" @click="onClickEdit"></el-button>
<el-button icon="el-icon-delete" @click="onClickClean"></el-button>
</el-button-group>
</div>
</editor-wrapper>
</template>
<script>
import EditorWrapper from "./EditorWrapper";
import {getEditorDefaultValue} from "../../../../utils";
export default {
name: "StringEditor",
components: {EditorWrapper,},
props: ['value', 'container', 'property', 'propertyName'],
data() {
return {
editValue: this.value,
popoverEditValue: this.value,
popoverVisible: false,
}
},
computed: {
editButtonIcon() {
return this.popoverVisible ? 'el-icon-check' : 'el-icon-edit';
},
defaultValue() {
return getEditorDefaultValue(this.property);
}
},
watch: {
value(v) {
this.editValue = v;
},
popoverVisible(v) {
if (v) {
this.popoverEditValue = this.editValue;
}
}
},
methods: {
onClickEdit() {
this.popoverVisible = !this.popoverVisible;
},
onClickClean() {
this.$emit('input', undefined, this.container, this.propertyName, this.value);
},
onChange() {
this.$emit('input', this.editValue, this.container, this.propertyName, this.value);
},
onConfirm() {
this.editValue = this.popoverEditValue;
this.onChange();
this.popoverVisible = false;
},
onCancel() {
this.$emit('cancel');
this.popoverVisible = false;
},
onClean() {
this.popoverEditValue = '';
},
},
}
</script>
<style scoped>
.bottom-bar {
margin-top: 5px;
display: flex;
align-items: center;
justify-content: space-between;
}
.string-name-container {
flex: 1;
}
</style>
<template>
<div class="el-form-item" :class="[{
'el-form-item--feedback': elForm && elForm.statusIcon,
'is-error': validateState === 'error',
'is-validating': validateState === 'validating',
'is-success': validateState === 'success',
'is-required': isRequired || required,
'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
},
sizeClass ? 'el-form-item--' + sizeClass : ''
]">
<label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
<slot name="label">{{label + form.labelSuffix}}</slot>
</label>
<div class="el-form-item__content" :style="contentStyle">
<slot></slot>
<transition name="el-zoom-in-top">
<slot
v-if="validateState === 'error' && showMessage && form.showMessage"
name="error"
:error="validateMessage">
<div
class="el-form-item__error"
:class="{
'el-form-item__error--inline': typeof inlineMessage === 'boolean'
? inlineMessage
: (elForm && elForm.inlineMessage || false)
}"
>
{{validateMessage}}
</div>
</slot>
</transition>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator';
import emitter from 'element-ui/src/mixins/emitter';
import objectAssign from 'element-ui/src/utils/merge';
import { noop, getPropByPath } from 'element-ui/src/utils/util';
export default {
name: 'ElFormItem',
componentName: 'ElFormItem',
mixins: [emitter],
provide() {
return {
elFormItem: this
};
},
inject: ['elForm'],
props: {
label: String,
labelWidth: String,
prop: String,
required: {
type: Boolean,
default: undefined
},
rules: [Object, Array],
error: String,
validateStatus: String,
for: String,
inlineMessage: {
type: [String, Boolean],
default: ''
},
showMessage: {
type: Boolean,
default: true
},
size: String,
labelOffsetTop: {
type: Number,
default: 0,
},
},
watch: {
error: {
immediate: true,
handler(value) {
this.validateMessage = value;
this.validateState = value ? 'error' : '';
}
},
validateStatus(value) {
this.validateState = value;
}
},
computed: {
labelFor() {
return this.for || this.prop;
},
labelStyle() {
const ret = {marginTop: this.labelOffsetTop + 'px'};
if (this.form.labelPosition === 'top') return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
},
contentStyle() {
const ret = {};
const label = this.label;
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
if (!label && !this.labelWidth && this.isNested) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.marginLeft = labelWidth;
}
return ret;
},
form() {
let parent = this.$parent;
let parentName = parent.$options.componentName;
while (parentName !== 'ElForm') {
if (parentName === 'ElFormItem') {
this.isNested = true;
}
parent = parent.$parent;
parentName = parent.$options.componentName;
}
return parent;
},
fieldValue() {
const model = this.form.model;
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
return getPropByPath(model, path, true).v;
},
isRequired() {
let rules = this.getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired;
},
_formSize() {
return this.elForm.size;
},
elFormItemSize() {
return this.size || this._formSize;
},
sizeClass() {
return this.elFormItemSize || (this.$ELEMENT || {}).size;
}
},
data() {
return {
validateState: '',
validateMessage: '',
validateDisabled: false,
validator: {},
isNested: false
};
},
methods: {
validate(trigger, callback = noop) {
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger);
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
resetField() {
this.validateState = '';
this.validateMessage = '';
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
let prop = getPropByPath(model, path, true);
this.validateDisabled = true;
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
},
getRules() {
let formRules = this.form.rules;
const selfRules = this.rules;
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
getFilteredRule(trigger) {
const rules = this.getRules();
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
},
onFieldBlur() {
this.validate('blur');
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
this.validate('change');
}
},
mounted() {
if (this.prop) {
this.dispatch('ElForm', 'el.form.addField', [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
let rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
}
},
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
};
</script>
......@@ -22,22 +22,29 @@
},
computed: {
...mapState({
data: state=>state.project.data,
}),
},
methods: {
show() {
show(behaviors, event) {
this.behavior_startEdit({
originData: this.data,
behaviors,
event,
});
this.visible = true;
},
onSave() {
this.behavior_save();
this.visible = false;
this.$emit('change')
this.$emit('change');
},
onOpened() {
this.$refs.behaviorEditor.measure();
this.$refs.behaviorEditor.edit();
},
...mapMutations([
'behavior_startEdit',
'behavior_save',
]),
}
}
......
......@@ -2845,13 +2845,6 @@ dotenv@^7.0.0:
resolved "https://registry.npm.taobao.org/dotenv/download/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"
integrity sha1-or481Sc2ZzIG6KhftSEO6ilijnw=
duiba-draggable-resizable@^1.0.9:
version "1.0.9"
resolved "https://registry.npm.taobao.org/duiba-draggable-resizable/download/duiba-draggable-resizable-1.0.9.tgz#9df1bec8b7c8ddde10ecc593a5246a268cdd19e3"
integrity sha1-nfG+yLfI3d4Q7MWTpSRqJozdGeM=
dependencies:
vue "^2.5.13"
duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.npm.taobao.org/duplexer/download/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
......@@ -7885,6 +7878,11 @@ vue-cli-plugin-i18n@^0.6.0:
vue-i18n "^8.0.0"
vue-i18n-extract "^0.4.13"
vue-draggable-resizable@^2.0.1:
version "2.0.1"
resolved "https://registry.npm.taobao.org/vue-draggable-resizable/download/vue-draggable-resizable-2.0.1.tgz#fb98d0997b1cfa8e3ba90f723cf8b605012cd96d"
integrity sha1-+5jQmXsc+o47qQ9yPPi2BQEs2W0=
vue-hot-reload-api@^2.3.0:
version "2.3.3"
resolved "https://registry.npm.taobao.org/vue-hot-reload-api/download/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
......@@ -7944,7 +7942,7 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.npm.taobao.org/vue-template-es2015-compiler/download/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha1-HuO8mhbsv1EYvjNLsV+cRvgvWCU=
vue@^2.5.13, vue@^2.5.16, vue@^2.6.10:
vue@^2.5.16, vue@^2.6.10:
version "2.6.10"
resolved "https://registry.npm.taobao.org/vue/download/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha1-pysaQqTYKnIepDjRtr9V5mGVxjc=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment