SOURCE

console 命令行工具 X clear

                    
>
console
Vue.component('q-element', {
	props: [ 'props' ],
	computed: {
		style() {
			let p = this.props, w = parseInt(p.$width), h = parseInt(p.$height);
			return {
				transform: "translate(" + p.x + "px," + p.y + "px)",
				width: isNaN(w) ? 'auto' : (w + "px"),
				height: isNaN(h) ? 'auto' : (h + "px")
			};
		}
	},
	template: "<div class='widget' :style='style' :current='$root.current==this'><component :is='props.component' :props='props'></component><div class='mask'></div></div>"
});
Vue.component('q-input', {
	props: [ 'props' ],
	editor: {
		placeholder: { template: '<i-input v-model="$root.current.props.placeholder"></i-input>' },
		prefix: { template: selectIconTemplate('$root.current.props.prefix') }
	},
	template: '<i-input :placeholder="props.placeholder" :prefix="props.prefix"></i-input>'
});
function selectTemplate(model, options) {
	options = JSON.stringify(options);
	return `<i-select v-model='${model}'><i-option v-for='o in ${options}' :value='o' :label='o'></i-option></i-select>`;
}
function selectIconTemplate(model) {
	var options = JSON.stringify('md-checkmark md-close md-arrow-forward md-arrow-back md-person md-key'.split(' '));
	return `<i-select v-model="${model}" clearable><i-option v-for='o in ${options}' :value='o' :label='o'>{{o}}<Icon :type='o'></Icon></i-option></i-select>`;
}

Vue.component('q-button', {
	props: [ 'props' ],
	editor: {
		type: { template: selectTemplate('$root.current.props.type',  'default primary dashed text info success warning error'.split(' ')) },
		icon: { template: selectIconTemplate('$root.current.props.icon') },
		text: { template: '<i-input v-model="$root.current.props.text"></i-input>' }
	},
	template: '<i-button :type="props.type" :icon="props.icon" @click="props.click">{{props.text}}</i-button>'
});
Vue.component('q-select', {
	props: [ 'props' ],
	editor: {
		options: { template: '<i-input type="textarea" v-model="$root.current.props.options"></i-input>' }
	},
	computed: {
		options() {
			return this.props.options.split(/\s+/);
		}
	},
	methods: {
		change() {
			console.log(this);
		}
	},
	template: '<i-select @change="change"><i-option v-for="o in options" :value="o" :label="o"></i-option></i-select>'
});

var actions = {
	ALERT(s) {
		return () => alert(s);
	}
};

new Vue({
	el: '#app',
	data: {
		mode: 'edit',
		current: null,
		widgets: [
			{ x: 16, y: 48, $width: '', $height: '', component: 'q-input', placeholder: 'username', prefix: 'md-person' },
			{ x: 16, y: 80, $width: '', $height: '', component: 'q-input', placeholder: 'password', prefix: 'md-key' },
			{ x: 16, y: 112, $width: '', $height: '', component: 'q-button', type: 'primary', text: 'Login', icon: 'md-checkmark', click: actions.ALERT('Morgen!') },
			{ x: 16, y: 16, $width: '', $height: '', component: 'q-select', options: 'A\nB\nC' }
		]
	},
	methods: {
		clickContent(event) {
			if (event.target.className == 'mask') {
				this.current = event.target.parentElement.__vue__;
			} else {
				this.current = null;
			}
		},
		currentEditor() {
			let s = this.current;
			if (s.$root) {
				let c = s.$root.$options.components[s.props.component];
				return c.extendOptions.editor;
			}
		}
	}
});

function snap(v) {
	return Math.round(v / 16) * 16;
}
function move(event) {
	let t = event.target.parentElement, p = t.__vue__.props;
	p.x += event.dx;
	p.y += event.dy;
}
function resizemove(event) {
	let t = event.target.parentElement, p = t.__vue__.props;
	p.x += event.deltaRect.left;
	p.y += event.deltaRect.top;
	p.$width = event.rect.width;
	p.$height = event.rect.height;
}

interact('.mask').draggable({ onmove: move }).resizable({ edges: { left: true, right: true, top: false, bottom: false } }).on('resizemove', resizemove);
<div id="app">
<header>mock<font style='color:#f55'>UI</font>
<Icon type='md-checkmark' title='保存' class='OK'></Icon>
<Icon type='md-text' title='审阅' :class='{active:mode=="review"}' @click='mode="review"'></Icon>
<Icon type='md-play' title='运行' :class='{active:mode=="run"}' @click='mode="run"'></Icon>
<Icon type='md-create' title='编辑' :class='{active:mode=="edit"}' @click='mode="edit"'></Icon>
</header>
<aside><Icon type='md-folder-open' title='目录'></Icon><Icon type='md-apps' title='组件'></Icon><Icon type='md-options' title='属性'></Icon><Icon type='md-flash' title='事件'></Icon>
<i-form label-position='top' v-if='current' :model='current.props'>
<form-item label='左'><i-input :placeholder='name' v-model='current.props.x'></i-input></form-item>
<form-item label='上'><i-input :placeholder='name' v-model='current.props.y'></i-input></form-item>
<form-item label='宽'><i-input :placeholder='name' v-model='current.props.$width' clearable></i-input></form-item>
<form-item label='高'><i-input :placeholder='name' v-model='current.props.$height' clearable></i-input></form-item>
<form-item v-for='(value,name) in currentEditor()' :label='name'><component :is='value' :name='name'></component></form-item>
</i-form></aside>
<content :edit='mode=="edit"' @mousedown='clickContent'><q-element v-for='widget in widgets' :props='widget'></q-element></content>
</div>
content {
	background-color: #fff;
	position: fixed;
	left: 0;
	right: 0;
	top: 32px;
	bottom: 0;
	overflow: auto;
	transition: left 0.5s;
}
content[edit] {
	background-image: linear-gradient(#eee 1px, transparent 0), linear-gradient(90deg, #eee 1px, transparent 0);
	background-size: 16px 16px, 16px 16px;
	left: 192px;
}
aside {
	background-color: #eee;
	position: fixed;
	left: 0;
	width: 192px;
	top: 32px;
	bottom: 0;
	-padding: 16px;
}
aside > i {
	background-color: #ccc;
	color: #000;
	display: inline-block;
	font-size: 20px;
	padding: 6px 14px;
	transition: background-color 0.5s;
}
aside > i:hover {
	background-color: #eee;
}
aside form {
	height: 100%;
	overflow: auto;
	padding: 16px;
}
aside li > i {
	float: right;
}
header {
	background-color: #334;
	color: #fff;
	font-family: Ink Free;
	font-size: 20px;
	line-height: 32px;
	padding-left: 16px;
}
header > i {
	display: inline-block;
	float: right;
	margin-right: 16px;
	padding: 6px;
	transition: background-color 0.5s;
}
header > i:hover {
	background-color: #f55;
}
header > i.active {
	background-color: #f55;
}
.widget {
	display: inline-block;
	position: absolute;
	user-select: none;
}
.widget > * {
	width: 100%;
	height: 100%;
}
.mask {
	display: none;
	position: absolute;
	left: 0;
	top: 0;
	touch-action: none;
	box-sizing: border-box;
}
content[edit] .mask {
	display: block;
}
[current] > .mask {
	animation: blinking 2s infinite;
	opacity: 0.5;
}
@keyframes blinking {
	0% {
		border: 4px solid transparent;
	}
	50% {
		border: 4px solid red;
	}
	100% {
		border: 4px solid transparent;
	}
}

本项目引用的自定义外部资源