Commit d5e2284c authored by 张晨辰's avatar 张晨辰

feat: 视图编辑面板

parent 1dc2e5de
......@@ -9,6 +9,7 @@
"dependencies": {
"cookie": "^0.4.0",
"core-js": "^2.6.5",
"duiba-draggable-resizable": "^1.0.9",
"element-ui": "^2.4.5",
"moment": "^2.24.0",
"path": "^0.12.7",
......
// import { set, lensPath } from 'ramda';
import properties from '../../../utils/properties';
export default {
component: require('./index.vue'),
properties: {
...properties().node,
...properties().image
},
props: {
type: 'image'
},
title: 'image'
};
<template>
<div class="zero-custom-picture" :style="`background-image:url(${properties.source.value}); background-size: contain;`"></div>
</template>
<style>
.zero-custom-picture {
height: 100%;
background-position: center;
}
</style>
<script>
export default {
name: 'customImage',
props: {
properties: {
type: Object,
default: () => {}
}
}
};
</script>
// import { set, lensPath } from 'ramda';
import properties from '../../../utils/properties';
export default {
component: require('./index.vue'),
properties: {
...properties().node,
...properties().label
},
props: {
type: 'label'
},
title: 'label'
};
<template>
<div
class="zero-custom-text"
:class="showPlaceholder && 'placeholder'"
v-html="addNBSP + selfText"
></div>
</template>
<style>
.zero-custom-text {
font-size: 14px;
}
.zero-custom-text.placeholder:after {
content: '请输入';
font-style: italic;
font-size: 12px;
}
</style>
<script>
export default {
name: 'customLabel',
props: {
properties: {
type: Object,
default: () => {}
},
isTyping: Boolean
},
data() {
return {
selfText: '文字'
};
},
computed: {
showPlaceholder() {
return (
!this.isTyping &&
typeof this.properties.text.value !== 'undefined' &&
!this.properties.text.value.replace(/\s|(&nbsp;)/g, '')
);
},
addNBSP() {
return this.selfText.replace(/\s/g, '') ? '' : '&nbsp;';
}
},
created() {
this.selfText = this.properties.text.value || this.selfText;
}
};
</script>
// import { set, lensPath } from 'ramda';
import properties from '../../../utils/properties';
export default {
component: require('./index.vue'),
properties: {
...properties().node
},
props: {
type: 'node'
},
title: 'node'
};
<template>
<div class="zero-custom-cmp zero-custom-node"></div>
</template>
<style>
</style>
<script>
export default {
name: 'customNode',
props: {
properties: {
type: Object,
default: () => {}
}
}
};
</script>
// import { set, lensPath } from 'ramda';
import properties from '../../../utils/properties';
export default {
component: require('./index.vue'),
properties: {
...properties().node,
...properties().rect
},
props: {
type: 'rect'
},
title: 'rect'
};
<template>
<div class="zero-custom-shape-rect-blue" :style="`background-color: ${properties.fillColor.value};border-width: ${properties.strokeWidth.value}px; border-color: ${properties.strokeColor.value};`"></div>
</template>
<style>
.zero-custom-shape-rect-blue {
background-color: #5396da;
height: 100%;
}
</style>
<script>
export default {
name: 'customRect',
props: {
properties: {
type: Object,
default: () => {}
}
// fillColor: {
// type: Object,
// default: () => {
// return {
// title: '填充色',
// type: 'colorPicker',
// value: '#fff'
// };
// }
// },
// strokeColor: {
// type: Object,
// default: () => {
// return {
// title: '边框颜色',
// type: 'colorPicker',
// value: '#000'
// };
// }
// },
// strokeWidth: {
// type: Object,
// default: () => {
// return {
// title: '边框宽度',
// type: 'inputNumber',
// value: 1
// };
// }
// }
}
};
</script>
......@@ -3,11 +3,7 @@
*/
export const API_HOST = 'http://10.10.94.31:7777';
<<<<<<< Updated upstream
//export const API_HOST = 'http://localhost:3002';
=======
// export const API_HOST = 'http://localhost:3002';
>>>>>>> Stashed changes
export const ASSETS_BASE = 'http://0.0.0.0:4002/assets';
//文件类型图标 t表示展示缩略图
......
......@@ -10,6 +10,9 @@ import './assets/style.css'
import './plugins/element.js'
import './themes/light/index.scss'
import VueDraggableResizable from 'duiba-draggable-resizable';
Vue.component('vue-draggable-resizable', VueDraggableResizable);
new Vue({
router,
store,
......
......@@ -3,6 +3,74 @@
*/
import { projectApi } from "../../api";
import { compoleteComponentData } from '../../utils/compoleteCmpData';
let testData = {
views: [{
name: '视图1', type: 'node', children: [{
name: 'image',
type: 'image',
properties: {
width: 100,
height: 100,
left: 1,
top: 1,
source: 'http://yun.duiba.com.cn/images/201909/ogzik0c3hk.png'
}
}, {
name: 'label',
type: 'label',
properties: {
width: 110,
height: 110,
left: 100,
top: 100,
text: 'textlabel',
color: '#fff',
size: 12,
align: 'left'
}
}, {
name: 'rect',
type: 'rect',
properties: {
fillColor: '#fff',
strokeColor: '#000',
strokeWidth: 1,
width: 120,
height: 120,
left: 200,
top: 200
},
children: [{
name: 'label2',
type: 'label',
properties: {
text: 'textlabel2',
color: '#fff',
size: 12,
align: 'left',
width: 130,
height: 130,
left: 300,
top: 300
}
}]
}]
}]
}
// let testData = {
// views: [{
// name: 'rect',
// type: 'rect',
// properties: {
// fillColor: '#fff',
// strokeColor: '#000',
// strokeWidth: 1
// }
// }]
// }
export const projectStore = {
state: {
......@@ -10,57 +78,108 @@ export const projectStore = {
name: '',
creator: '',
data: {
views: [{ name: 'body', children: [] }],
views: [],
assets: [],
dataMapping: [],
}
},
activeComponent: {},
activeIdList: []
},
mutations: {
/**
* 更新state中的data
* @param {*} state
* @param {*} project
*/
updateProject(state, project) {
const {id, name, creator, data} = project;
const { id, name, creator, data } = project;
state.id = id;
state.name = name;
state.creator = creator;
if(data){
if (data) {
const localData = state.data;
const {views, assets, dataMapping} = JSON.parse(data);
if(!localData.views || localData.views.length === 0){
const { views, assets, dataMapping } = JSON.parse(data);
if (!localData.views || localData.views.length === 0) {
localData.views = views || [];
}
if(!localData.assets || localData.assets.length === 0){
if (!localData.assets || localData.assets.length === 0) {
localData.assets = assets || [];
}
if(!localData.dataMapping || localData.dataMapping.length === 0){
if (!localData.dataMapping || localData.dataMapping.length === 0) {
localData.dataMapping = dataMapping || [];
};
} else {
state.data.views = testData.views;
}
compoleteComponentData(state.data.views);
},
createView(state) {
state.data.views.push({
name: '未命名',
children: []
})
console.log(state.data.views);
/**
* 激活组件
* @param {*} state
* @param {*} id
*/
activeComponent(state, id) {
// todo drag
// if (state.cmpListDragging) {
// state.cmpListDragging = false;
// return;
// }
if (id !== state.activeComponent.id) {
const _active = this.getters.componentList.find(cmp => cmp.id === id);
state.activeComponent = _active || state.activeComponent;
}
state.activeComponent = id;
state.activeIdList = [id];
console.log('mutations activeComponent', state);
}
},
getters: {
views: state => {
return state.views
/**
* 当前激活的组件
*/
activeComponent: state => {
return state.activeComponent || {}
},
/**
* 当前激活的组件ID
*/
activeComponentId: state => {
return (state.activeComponent || {}).id;
},
/**
* 扁平化所有节点
*/
componentList: state => {
const flatten = arr => {
return arr.reduce((flat, toFlat) => {
return flat.concat(toFlat.children ? flatten(toFlat.children).concat([toFlat]) : [toFlat]);
}, []);
};
let result = flatten(state.data.views).map(v => {
delete v.children;
return v;
});
// let result = state.data.views.flatMap(v => [v, v.children || []])
console.log('componentList', result);
return result;
}
},
actions: {
async updateProject({commit}, projectID) {
async updateProject({ commit }, projectID) {
const project = await projectApi.getData(projectID);
commit('updateProject', project);
},
/**
* 新建视图
* 选中节点
* @param {*} param0
* @param {*} data
*/
async createView({ commit }) {
commit('createView');
activeComponent({ commit }, data) {
console.log('actions activeComponent', data);
commit('activeComponent', data);
}
},
};
......@@ -6,6 +6,7 @@
@import "./base.scss";
@import "./home.scss";
@import "./editor.scss";
@import "./playground.scss";
@import "~element-ui/packages/theme-chalk/src/index.scss";
......
.zero-playground-body-center{
position: relative;
width: 375px;
margin: 10px auto;
height: 100%;
max-height: 600px;
background-color: transparent;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.zero-playground-draw-panel{
height: 600px;
background: url(data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAiUlEQVQ4jZ1TSxLFIAhLOp7C+19NjyFvlQ7lUUvLRgXzgVGOMQyb6L3/5cwMJAEAxw6sy3FP8tw/EkhJqp7klQMBtKpWcpC1oVp7IoiqviXg4xBFRPL7EM/6WsviYDxwzrkVaBHsz5U4MlWfKxHEySpXIdk6qLRzcXBHVnKQfZRKCy1Ty979XfwApOBe0rB0KiIAAAAASUVORK5CYII=);
background-repeat: repeat;
overflow-y: auto;
overflow-x: hidden;
}
.zero-draw-panel-container {
position: relative;
// border: 5px #000 solid;
width: 375px; // 谨慎修改,要和drawPanel宽度保持一致
min-height: 600px;
}
.zero-draw-panel-container.scroll {
height: 1200px;
}
/* 重置background相关属性*/
.zero-draw-panel-container * {
background-repeat: no-repeat;
}
.zero-draw-panel-body {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
background: transparent;
}
.zero-components-container {
width: 100%;
height: 100%;
}
.zero-components-container.playingAnime {
position: absolute;
z-index: 2;
}
.zero-custom-wrapper {
position: absolute;
cursor: default;
user-select: none;
}
.active {
border: 2px dashed rgb(20, 100, 206);
}
......@@ -2,7 +2,7 @@
* Created by rockyl on 2019-09-19.
*/
import {Message, Loading} from "element-ui";
import { Message, Loading } from "element-ui";
import i18n from './i18n'
export function messageError(e) {
......@@ -26,3 +26,16 @@ export function playWaiting(promise, text) {
loading.close();
})
}
export function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
// querySelector method uses CSS3 selectors for querying the DOM and CSS3
// doesn't support ID selectors that start with a digit
// for more:
// https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document
return `xy-${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
import { guid } from './index';
import { getComposedComponents } from './getComposedComponents';
const composedComponents = getComposedComponents();
export const compoleteComponentData = (views) => {
views.forEach(view => {
view.id = view.id || guid();
let _composedCmp = composedComponents[view.type];
view.component = _composedCmp.component.default;
let _composedProps = {..._composedCmp.properties}; // 组件预设的属性,Object类型
let _viewProps = view.properties || {}; // 组件对象的具体属性,key:value
let _keys = Object.keys(_viewProps);
_keys.forEach(k => {
_composedProps[k].value = _viewProps[k]
});
view.properties = _composedProps;
if (view.children) {
compoleteComponentData(view.children);
}
});
}
\ No newline at end of file
export const getComposedComponents = () => {
// 去中心化;
const context = require.context(
'../components/customElement',
true,
/index\.js$/
);
const composedComponents = {};
context.keys().forEach((key) => {
const componentName = key.slice(2, -9);
const component = context(key);
composedComponents[componentName] = component.default;
});
console.log('composedComponents', composedComponents);
return composedComponents;
}
/**
* Created by rockyl on 2019-09-19.
*/
import { Message, Loading } from "element-ui";
import i18n from '../i18n'
export function messageError(e) {
Message({
dangerouslyUseHTMLString: true,
message: `<p style="margin-bottom: 5px;"><strong>${i18n.t(e.message)}</strong></p><p>${e.name}</p>`,
type: 'error'
})
}
export function playWaiting(promise, text) {
const loading = Loading.service({
lock: true,
text: text || i18n.t('In processing'),
});
return promise.catch(e => {
messageError(e);
throw e;
}).finally(() => {
loading.close();
})
}
export function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
// querySelector method uses CSS3 selectors for querying the DOM and CSS3
// doesn't support ID selectors that start with a digit
// for more:
// https://stackoverflow.com/questions/37270787/uncaught-syntaxerror-failed-to-execute-queryselector-on-document
return `xy-${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
// 属性
export default () => ({
node: {
groupName: '基础',
left: {
title: 'X坐标',
type: 'inputNumber',
value: 50
},
top: {
title: 'Y坐标',
type: 'inputNumber',
value: 50
},
width: {
title: '宽度',
type: 'inputNumber',
value: 100
},
height: {
title: '高度',
type: 'inputNumber',
value: 50
},
rotate: {
title: '旋转',
type: 'slider',
props: {
min: -180,
max: 180
},
value: 0
},
scaleX: {
title: 'X轴缩放',
type: 'inputNumber',
value: 1
},
scaleY: {
title: 'Y轴缩放',
type: 'inputNumber',
value: 1
},
visible: {
title: '是否可见',
type: 'switch',
value: true
}
},
label: {
groupName: '文本',
text: {
title: '文本内容',
type: 'input',
value: ''
},
color: {
title: '颜色',
type: 'colorPicker',
value: '#fff'
},
size: {
title: '字体大小',
type: 'swSelect',
props: {
optionType: 'fontSize'
},
value: 12
},
align: {
title: '文本对齐',
type: 'select',
options: [
{ label: '靠左', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '靠右', value: 'right' }
],
value: 'left'
}
},
image: {
groupName: '来源',
source: {
title: '来源',
type: 'source',
value: ''
}
},
rect: {
groupName: '形状',
fillColor: {
title: '填充色',
type: 'colorPicker',
value: '#fff'
},
strokeColor: {
title: '边框颜色',
type: 'colorPicker',
value: '#000'
},
strokeWidth: {
title: '边框宽度',
type: 'inputNumber',
value: 1,
props: {
min: 0
}
}
}
});
<template>
<pane icon="el-icon-s-operation" :title="$t('panes.Inspector')">
<div class="inspec-test" style="background-color:#fff; width: 100%;height: 100px;" @click.native="testGetter">
{{JSON.stringify(activeComponent.properties)}}
</div>
<el-button @click="testGetter">默认按钮</el-button>
</pane>
</template>
<script>
import Pane from "../../components/Pane";
export default {
name: "Inspector",
components: {Pane}
import { mapState, mapActions, mapGetters } from 'vuex';
import Pane from '../../components/Pane';
export default {
name: 'Inspector',
components: { Pane },
computed: {
...mapGetters([
'activeComponent'
])
},
methods: {
testGetter() {
console.log('testGetter', this.activeComponent);
debugger;
this.$store;
debugger;
}
}
};
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<pane icon="el-icon-s-open" :title="$t('panes.Playground')">
<div class="zero-playground-body-center">
<div class="zero-playground-draw-panel">
<draw-panel></draw-panel>
</div>
</div>
</pane>
</template>
<script>
import Pane from "../../components/Pane";
export default {
name: "Playground",
components: {Pane}
}
import Pane from '../../components/Pane';
import DrawPanel from './components/drawPanel';
export default {
name: 'Playground',
components: { Pane, DrawPanel }
};
</script>
<style scoped>
</style>
\ No newline at end of file
......@@ -11,6 +11,8 @@
:props="defaultProps"
:expand-on-click-node="false"
highlight-current
:default-expand-all="true"
@node-click="handleNodeClick"
empty-text=""
/>
</el-scrollbar>
......@@ -19,28 +21,35 @@
</template>
<script>
import {mapState, mapActions} from 'vuex'
import Pane from "../../components/Pane";
import { mapState, mapActions } from 'vuex';
import Pane from '../../components/Pane';
export default {
name: "Views",
components: {Pane},
export default {
name: 'Views',
components: { Pane },
data() {
return {
defaultProps: {
children: 'children',
label: 'name'
},
}
};
},
computed: {
...mapState({
views: state => state.project.data.views
}),
})
},
methods: {
/**
* 点击左侧视图列表
*/
handleNodeClick(data) {
this.$store.commit('activeComponent', data);
}
}
};
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div class="zero-draw-panel-container" >
<div class="zero-components-container" >
<div
v-if="item.id"
:key="item.id"
:id="item.id"
v-for="item in componentList"
@click.exact="activeComponent(item.id)"
@click.shift.exact="changeActiveIdList(item.id)"
@contextmenu.prevent="activeComponent(item.id)"
>
<wrapper :component-data="item"/>
</div>
</div>
<div class="zero-draw-panel-body"></div>
</div>
</template>
<style lang="less">
</style>
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import wrapper from './wrapper';
export default {
components: { wrapper },
methods: {
activeComponent(id) {
this.$store.dispatch('activeComponent', id);
},
changeActiveIdList(id) {
this.$store.commit('changeActiveIdList', id);
}
},
computed: {
...mapGetters(['componentList'])
}
};
</script>
<template>
<vue-draggable-resizable
:minw="1"
:minh="1"
:cmp-id="cmpId"
:z="99"
:otherstatus="true"
:class="[active ? 'choosed-cmp' : 'unchoosed-cmp', isTyping && 'isTyping']"
v-bind="position"
@dragging="handleDragging"
@resizing="handleResize"
@deactivated="handleDeactivated"
>
<div
class="sword-compomnent-content-wrapper"
:contenteditable="false"
@dblclick="handleEnableInput"
@input="handleInput"
@keyup.delete.prevent="changeEditRange"
>
<component :is="componentData.component" :is-typing="false" :properties="componentData.properties" />
</div>
</vue-draggable-resizable>
</template>
<script>
export default {
props: {
from: {
type: String
},
componentData: {
type: Object,
require: true
}
},
methods: {
handleEnableInput() {
// 处于编辑状态的组件不可进行 删除、移动、缩放操作
this.$store.dispatch('changeEditaleStatus', this.canTyping);
},
handleDeactivated() {
this.$store.dispatch('changeEditaleStatus', false);
},
changeEditRange(e) {
// this.isTyping && setEditRange(e.target);
},
handleInput(e) {
const textContent = e.target.innerHTML
.replace(/<br>/g, '')
.replace(/<\/.+?>/g, '')
.replace(/<.+?>/g, '<br>')
.replace(/^<br>/, '');
// this.$store.dispatch('modifyComponent', {
// key: ['props', 'text'],
// value: textContent || ' ',
// id: this.cmpId
// });
},
handleResize(left, top, w, h) {
const id = this.cmpId;
// this.$store.dispatch('batchModifyComponent', [
// {
// key: ['bindings', 'outline', 'position', 'top', 'v'],
// value: top,
// id
// },
// {
// key: ['bindings', 'outline', 'position', 'left', 'v'],
// value: left,
// id
// },
// {
// key: ['bindings', 'outline', 'position', 'width', 'v'],
// value: w,
// id
// },
// {
// key: ['bindings', 'outline', 'position', 'height', 'v'],
// value: h,
// id
// }
// ]);
},
handleDragging(left, top) {
// 文本编辑状态 与 位置锁定状态无法拖动
// if (
// this.isTyping ||
// path(
// ['bindings', 'outline', 'position', 'fixed', 'v'],
// this.componentData
// )
// ) {
// return -1;
// }
// const leftKeys = ['bindings', 'outline', 'position', 'left', 'v'];
// const topKeys = ['bindings', 'outline', 'position', 'top', 'v'];
// if (this.$store.state.activeIdList.length > 1) {
// // 多组件移动
// this.$store.dispatch('modifyComponentListPosition', {
// leftDistance: left - path(leftKeys, this.componentData),
// topDistance: top - path(topKeys, this.componentData)
// });
// } else {
// // console.log('active component change---------');
// // 单组件移动
// const id = this.cmpId;
// this.$store.dispatch('activeComponent', id);
// this.$store.dispatch('modifyComponent', {
// key: leftKeys,
// value: left,
// id
// });
// this.$store.dispatch('modifyComponent', {
// key: topKeys,
// value: top,
// id
// });
// }
// return 0;
}
},
computed: {
cmpId() {
console.log('this.componentData', this.componentData);
return this.componentData.id;
},
active() {
return this.$store.state.project.activeIdList.indexOf(this.cmpId) !== -1
},
isTyping() {
return this.componentData.editable;
},
canTyping() {
// 只有文字才可以编辑
return this.componentData.group === 'text';
},
styleObject() {
return styles.getStyleObject(this.componentData);
},
position() {
const componentData = this.componentData;
return {
x: componentData.properties.left.value,
y: componentData.properties.top.value,
w: componentData.properties.width.value,
h: componentData.properties.height.value
}
// return !componentData
// ? {}
// : {
// x: path(
// ['bindings', 'outline', 'position', 'left', 'v'],
// componentData
// ),
// y: path(
// ['bindings', 'outline', 'position', 'top', 'v'],
// componentData
// ),
// w: path(
// ['bindings', 'outline', 'position', 'width', 'v'],
// componentData
// ),
// h: path(
// ['bindings', 'outline', 'position', 'height', 'v'],
// componentData
// )
// };
}
}
};
</script>
<style>
.vdr {
box-sizing: content-box;
border: 1px solid transparent;
}
.vdr.active {
border-color: transparent;
}
.vdr.choosed-cmp {
border-color: rgba(157, 172, 255, 0.9);
cursor: move;
}
.vdr.isTyping {
cursor: text;
}
/* .handle {
border-radius: 50%;
} */
.vdr.choosed-cmp > .handle {
display: block !important;
}
.vdr.unchoosed-cmp > .handle {
display: none !important;
}
.otherstatus.active {
border-color: transparent;
}
.sword-compomnent-content-wrapper {
height: 100%;
border: none;
}
</style>
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