feat:更换画布

This commit is contained in:
wangjiahao 2021-03-25 19:16:32 +08:00
parent 470f8cddae
commit 5db39b5855
65 changed files with 8586 additions and 427 deletions

View File

@ -18,4 +18,10 @@ public interface ViewApi {
@ApiOperation("视图树")
@PostMapping("/tree")
List<PanelViewDto> tree(BaseGridRequest request);
}

View File

@ -15,6 +15,7 @@
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
},
"dependencies": {
"core-js": "^3.6.5",
"@riophae/vue-treeselect": "0.4.0",
"axios": "^0.21.1",
"echarts": "^5.0.2",
@ -41,14 +42,17 @@
"devDependencies": {
"@babel/core": "^7.4.0-0",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-service": "3.6.0",
"babel-eslint": "10.0.1",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "10.1.0",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "5.15.3",
"eslint-plugin-vue": "5.2.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-import": "^2.20.2",
"html-webpack-plugin": "3.2.0",
"less": "^4.1.1",
"less-loader": "^8.0.0",

View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,307 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>IconFont Demo</title>
<link rel="shortcut icon" href="https://img.alicdn.com/tps/i4/TB1_oz6GVXXXXaFXpXXJDFnIXXX-64-64.ico" type="image/x-icon"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">&#xe86b;</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2373406" target="_blank" class="nav-more">查看项目</a>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe66a;</span>
<div class="name">向右旋转</div>
<div class="code-name">&amp;#xe66a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe616;</span>
<div class="name">图片</div>
<div class="code-name">&amp;#xe616;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe672;</span>
<div class="name"></div>
<div class="code-name">&amp;#xe672;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe790;</span>
<div class="name">矩形</div>
<div class="code-name">&amp;#xe790;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe652;</span>
<div class="name">文本</div>
<div class="code-name">&amp;#xe652;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe648;</span>
<div class="name">按钮</div>
<div class="code-name">&amp;#xe648;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>兼容性最好,支持 IE6+,及所有现代浏览器。</li>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持多色图标,这些多色图标在 Unicode 模式下将不能使用如果有需求建议使用symbol 的引用方式</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.eot');
src: url('iconfont.eot?#iefix') format('embedded-opentype'),
url('iconfont.woff2') format('woff2'),
url('iconfont.woff') format('woff'),
url('iconfont.ttf') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-xiangyouxuanzhuan"></span>
<div class="name">
向右旋转
</div>
<div class="code-name">.icon-xiangyouxuanzhuan
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-tupian"></span>
<div class="name">
图片
</div>
<div class="code-name">.icon-tupian
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-suo"></span>
<div class="name">
</div>
<div class="code-name">.icon-suo
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-juxing"></span>
<div class="name">
矩形
</div>
<div class="code-name">.icon-juxing
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-wenben"></span>
<div class="name">
文本
</div>
<div class="code-name">.icon-wenben
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-button"></span>
<div class="name">
按钮
</div>
<div class="code-name">.icon-button
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>兼容性良好,支持 IE8+,及所有现代浏览器。</li>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
<li>不过因为本质上还是使用的字体,所以多色图标还是不支持的。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xiangyouxuanzhuan"></use>
</svg>
<div class="name">向右旋转</div>
<div class="code-name">#icon-xiangyouxuanzhuan</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-tupian"></use>
</svg>
<div class="name">图片</div>
<div class="code-name">#icon-tupian</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-suo"></use>
</svg>
<div class="name"></div>
<div class="code-name">#icon-suo</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-juxing"></use>
</svg>
<div class="name">矩形</div>
<div class="code-name">#icon-juxing</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-wenben"></use>
</svg>
<div class="name">文本</div>
<div class="code-name">#icon-wenben</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-button"></use>
</svg>
<div class="name">按钮</div>
<div class="code-name">#icon-button</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,40 @@
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1613282476380'); /* IE9 */
src: url('iconfont.eot?t=1613282476380#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAZ4AAsAAAAAC4gAAAYrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDXgqJUIgMATYCJAMcCxAABCAFhG0HZRvpCVGUT1KO7AtsG/ZEEGZ3PDfaDAJAgQcmFqBIigUWiiI8AiAe/tvvf/vMzJVn2j7mifU6urpJNelEkiW80mmEJhYiyZo3Qvl/6nxfgFi/BKGxwByQHcd2lJPljzDSVgCceEMegddu3XAIT7tLO+RSaJSLPez0oYkqnu0TgIkcaAHSAf1bbX7eTf//38/VidWFpretrReT9//MLIkBIXFIGhIhmXgjQrPSqJVFPJNdqjAo343DCXRtMIXm6KIVG8CsYMsF4pbAF8CcMSoNzNBq6opNi3jMoE0P03Pgkf39+Af7w0xSZWzVY1cWlmHOz2Ajuu51EnZoAun27BhuRcY+oBD3Kp136bxqn0Xdqr+pdwoYaiX1z6Al1qmijfz/T7aZjQ5DrqdjkUrI+/rn1Xqoj9RzqZqfQckn8XOJpApbh3oVYlXUC8Qo8sn8GpE9XajRmwyxEcQLEP8xfGWld2GXSTFZc9tdY3xevb4/GOs2W4LNau4KIZu93+2PadM8AP/LtQ1XrmDOzY3Xr29G3rbp2jX8LLc3X726kb+FNSjRV62SerBvyI3Uqs8tPX614diVcMW1USevTz3YXq41paRFQ7Cg4noIddOwWaZLqZBbMZukQFsmWV9IaqUXFhHMjBJIERcj/kuZZaMEdF+2AKSNyICwpUeE4G5Eu2UCBEU7W2sLlWXYMqaym4raylp5tK3bOrRE8TQaGNna2K5JEXfMybhooK+8gJwHLxkaai4Tsssr5FHJIo1dMLRUZtOcQmSMge5z5k1tMPeYRoREiIwOcTPi29vRKhG3I0EXEhlS0O2HhbwijSLQPaFRSGRYtUBxu6VlaG8HmMZRi3gbaQ4Lq1Ut65pQXIHKs2vC4u0W9AACCBJFoxhA/B7keQgQop7A2wESNigFWmWqFAhRD6GiaO6fNByWXZG15F7MIakBVikRUsQyS8aleedaUmrBbJH7c4JfZ8wMShqZshQMGX7dNgSP/TMf1Xc4tdFvnn3grrPzttKyOQqFZ5gpf6NyhzpX80afmLlzge/AZJHxLejV3kW7WF0YR4N2rk6JiODxFkYmB0uRfauIgwq6sqGyF684cuKgohVwL8FBD4Hbg5DNvSGMs9aFrzfXCvTOBs4t4wZTwqpLOo9hc2Hrz+Jq74/D72sfZqDVP28JhxMSs0rmc7vu+G+FCVfmxKicNOcKjDmfrsHm3ds7D5ub9O6xebxdb65UXblizWh/sRV8an8QSud9feWC2TW+fIMLIfQ6pvnrFAYsFX7xiHWzWpvaxl3tdr626Hb8dPgSkuR2wq5waS5497VimqjHnWiYe0PbtCvAIvPBlOH+2nPMkrT898SGaCh+TFilgy25/X6Z+SoDjcOgUvT0wCudsSPT9+NzttrvMtJcEoZ9zcQOx1FpujKSH6XX4KALz1Nwf/+gWaJWgBSSGKqzymYM3FyDewUTKRXsDXofN5EYxivNHiWsPe2gXsNaYb3CVKKjjOf18xJ2a5uzNthu9tRU7cA6zQ5TJ5H6Bof3drXaOgfzH3rvdS7ga1/pmzkv4dfVR6znpt9MnrN6q4BH8Lm3e5t+eO/2Wr4+7IFfu703XDqdAOsy3V3dPGC36iFfuJpc15/nApDfpF+sX9Zdz3MAyB+1L/8qi/kx7U27XTMyE5+GcV9X7VIi347k+44hS/8YUuKQspAzU1NmnaWEyG18hskXErq6+Dt4pz5tD1+Qkf8evoiQNNYga20jFuo+qPSsoNY6AF17Ld66Z4bSidLBHgMAYewqJENvIBt7QSzUj1BZ+gm1ceig60z4dtmzIwTrfIoLDJdRsQUJJVLjhIVZXeU6zKtSgabllrUJU0WIoFw6m88sxjVMt7GG0sjnGeMQR4mMFsHRsCQRVKekikssXWGsPi2T4aoeKF0iMlgdQWEFDFaGFLVABCVEDafHZqyFn18H46kkBbSh7VxwE4xSCDNHctKyBOJiVCO1PZZ6RSNeHkviHIQblhMyZFFixySeJJB69UJVWAmTVukQr5smk4zEUVF6/7j8OE+DLnbWJlLkKFFFHU20JneEQo1vIWqzWqi1VpTfyNR6cVWdohJjVW0WaryxCdeKotFYVBkjNQAA') format('woff2'),
url('iconfont.woff?t=1613282476380') format('woff'),
url('iconfont.ttf?t=1613282476380') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiangyouxuanzhuan:before {
content: "\e66a";
}
.icon-tupian:before {
content: "\e616";
}
.icon-suo:before {
content: "\e672";
}
.icon-juxing:before {
content: "\e790";
}
.icon-wenben:before {
content: "\e652";
}
.icon-button:before {
content: "\e648";
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,51 @@
{
"id": "2373406",
"name": "visual-drag-demo",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "8229196",
"name": "向右旋转",
"font_class": "xiangyouxuanzhuan",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "2187794",
"name": "图片",
"font_class": "tupian",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "6056165",
"name": "锁",
"font_class": "suo",
"unicode": "e672",
"unicode_decimal": 58994
},
{
"icon_id": "6266248",
"name": "矩形",
"font_class": "juxing",
"unicode": "e790",
"unicode_decimal": 59280
},
{
"icon_id": "14220841",
"name": "文本",
"font_class": "wenben",
"unicode": "e652",
"unicode_decimal": 58962
},
{
"icon_id": "16859933",
"name": "按钮",
"font_class": "button",
"unicode": "e648",
"unicode_decimal": 58952
}
]
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="xiangyouxuanzhuan" unicode="&#58986;" d="M961.45 533.82V758.55l-96.93-96.93C781.45 767.11 652.62 834.84 507.58 833.43 271.24 831.15 72.15 638.22 62.9 402.06 52.86 145.61 257.78-65.45 512-65.45c166.26 0 311.09 90.52 388.83 224.73h-43.78C775.39 34.13 627.8-44.06 463.37-25.2c-187.7 21.52-339.93 174.33-360.76 362.11C75.05 585.53 268.94 796 512 796c132.91 0 250.39-63.49 325.7-161.21L736.73 533.82h224.72z" horiz-adv-x="1024" />
<glyph glyph-name="tupian" unicode="&#58902;" d="M939 858H84C39 858 2.4 824.6 2.4 779.6V-6c0-45 36.8-81.8 81.6-81.8h861.8c44.8 0 78.2 36.8 78.2 81.8V779.6c0 45-40.2 78.4-85 78.4zM331.4 741.8c69.2 0 125.2-56 125.2-125.2 0-69-56.2-125-125.2-125s-125.2 56-125.2 125 56.2 125.2 125.2 125.2z m-192-712.6c-8.8 0-17.8 3-25 9.2-16.2 13.8-18 38.2-4.2 54.4l176.8 266.6c13.2 15.4 35.8 17.8 52.2 5.8l156-116 279.8 317.8c13.2 16.6 83.4 94.8 124.6 6.2 0 0.2 0.2-117.4 0.2-237.4v-306.6c-0.6 0.4-760 0-760.4 0z" horiz-adv-x="1024" />
<glyph glyph-name="suo" unicode="&#58994;" d="M829.6-64.6h-634c-35.9 0-65.8 23.9-65.8 65.8V461.8c0 35.9 29.9 65.8 65.8 65.8h634c35.9 0 65.8-29.9 65.8-65.8v-460.5c6-41.9-23.9-65.9-65.8-65.9z m-634 532.3c-5.9 0-5.9-5.9 0 0l-6-466.5c0-6 0-6 6-6h634c6 0 6 0 6 6V461.8c0 6 0 6-6 6h-634zM590.4 306.2c0 41.9-41.9 77.8-77.8 77.8-41.9 0-71.8-41.9-71.8-77.8 0-29.9 17.9-59.8 47.8-65.8v-107.7c0-12 6-17.9 17.9-17.9h23.9c12 0 17.9 6 17.9 17.9V240.5c24.1 5.9 42.1 35.8 42.1 65.7zM763.8 503.6H704v89.7c0 101.7-83.7 179.4-179.4 179.4-101.7 0-179.4-83.7-179.4-179.4v-89.7h-59.8v89.7c0 131.6 107.7 239.2 239.2 239.2s239.2-107.7 239.2-239.2v-89.7z" horiz-adv-x="1024" />
<glyph glyph-name="juxing" unicode="&#59280;" d="M898.8 696.4v-624.8H125.2V696.4h773.6m59.5 59.5H65.7v-743.8h892.5V755.9h0.1z" horiz-adv-x="1024" />
<glyph glyph-name="wenben" unicode="&#58962;" d="M755.243 636.633a36.486 36.486 0 0 0 3.503-72.802l-3.503-0.17H268.757a36.486 36.486 0 0 0-3.503 72.802l3.503 0.17h486.486zM512 636.633a36.486 36.486 0 0 0 36.316-32.983l0.17-3.503v-413.514a36.486 36.486 0 0 0-72.802-3.527l-0.17 3.527V600.147A36.486 36.486 0 0 0 512 636.633zM852.54 831.228a97.297 97.297 0 0 0 97.298-97.297V52.85a97.297 97.297 0 0 0-97.297-97.298H171.459a97.297 97.297 0 0 0-97.297 97.298V733.93a97.297 97.297 0 0 0 97.297 97.298h681.082z m0-72.973H171.46a24.324 24.324 0 0 1-24.155-21.503l-0.17-2.797v-681.081a24.324 24.324 0 0 1 21.479-24.179l2.845-0.145h681.082a24.324 24.324 0 0 1 24.154 21.478l0.17 2.846V733.955a24.324 24.324 0 0 1-21.479 24.154l-2.845 0.146z" horiz-adv-x="1024" />
<glyph glyph-name="button" unicode="&#58952;" d="M211.98 501.92h104.44c22.43 0 39.94-5.34 52.54-16.02 12.17-10.25 18.26-24.14 18.26-41.65 0-12.81-3.2-23.71-9.61-32.68-6.62-8.54-15.49-14.85-26.59-18.9 15.16-2.99 26.59-9.08 34.28-18.26 7.69-9.19 11.53-21.57 11.53-37.16 0-22.86-7.9-39.73-23.71-50.62-13.46-8.97-32.36-13.46-56.7-13.46H211.98V501.92z m34.92-28.83v-68.24h60.23c15.8 0 27.34 2.77 34.6 8.33 7.05 5.77 10.57 14.74 10.57 26.91 0 11.53-3.52 19.86-10.57 24.99-6.84 5.34-18.16 8.01-33.96 8.01H246.9z m0-96.75v-74.32h64.71c14.31 0 25.84 2.35 34.6 7.05 10.46 6.19 15.7 15.8 15.7 28.83 0 13.46-4.06 23.28-12.17 29.47-7.9 5.98-20.29 8.97-37.16 8.97H246.9zM316.42 268.17999999999995H206.98V506.92h109.44c23.53 0 42.3-5.79 55.77-17.2 13.29-11.19 20.03-26.49 20.03-45.46 0-13.82-3.55-25.79-10.54-35.58l-0.12-0.16c-4.5-5.81-9.99-10.69-16.41-14.6 9.97-3.71 18.01-9.16 23.98-16.29 8.43-10.07 12.7-23.65 12.7-40.37 0-24.49-8.7-42.9-25.87-54.74-14.29-9.53-34.3-14.34-59.54-14.34z m-99.44 10h99.44c23.23 0 41.37 4.24 53.93 12.62 14.43 9.95 21.48 25.16 21.48 46.46 0 14.31-3.49 25.73-10.37 33.95-6.9 8.24-17.47 13.81-31.41 16.56l-17.55 3.47 16.8 6.14c10.12 3.7 18.29 9.48 24.29 17.19 5.72 8.05 8.62 18.04 8.62 29.69 0 16.11-5.39 28.48-16.48 37.82-11.63 9.85-28.22 14.84-49.32 14.84h-99.44v-218.74z m94.64 18.83H241.9v84.32h70.67c18.15 0 31.3-3.26 40.18-9.98 9.4-7.18 14.16-18.43 14.16-33.46 0-14.86-6.11-26.01-18.15-33.14l-0.18-0.1c-9.45-5.07-21.89-7.64-36.96-7.64z m-59.72 10h59.71c13.36 0 24.17 2.15 32.14 6.4 8.85 5.28 13.15 13.28 13.15 24.48 0 11.91-3.34 20.25-10.21 25.5-6.96 5.27-18.45 7.94-34.13 7.94H251.9v-64.32z m55.23 92.84H241.9v78.24h65.87c17.05 0 29.15-2.95 36.97-9.02 8.34-6.1 12.56-15.85 12.56-28.98 0-13.69-4.17-24.04-12.41-30.78l-0.13-0.1c-8.22-6.3-20.53-9.36-37.63-9.36z m-55.23 10h55.23c14.56 0 25.16 2.44 31.5 7.25 5.84 4.81 8.67 12.33 8.67 22.99 0 9.93-2.78 16.78-8.51 20.94l-0.14 0.1c-5.91 4.61-16.3 6.95-30.88 6.95H251.9v-58.23zM414.45 501.92h187.41v-29.79h-76.25v-198.95h-34.6V472.13h-76.57v29.79zM530.62 268.17999999999995h-44.6V467.13h-76.57v39.79h197.41v-39.79h-76.25v-198.95z m-34.6 10h24.6V477.13h76.25v19.79H419.45v-19.79h76.57v-198.95zM629.42 501.92h35.24l116.93-170.11h1.28V501.92h34.92v-228.74h-33.96L665.62 445.22h-1.28v-172.04h-34.92V501.92zM822.79 268.17999999999995H781.2l-111.87 162.8v-162.8h-44.92V506.92h42.87l110.58-160.88V506.92h44.92v-238.74z m-36.33 10h26.33V496.92h-24.92v-170.11h-8.91l-1.49 2.17-115.44 167.94h-27.61v-218.74h24.92V450.22h8.91l118.21-172.04zM884.08 76.32000000000005H146c-43.71 0-79.47 35.76-79.47 79.47V637.5799999999999c0 43.71 35.76 79.47 79.47 79.47h738.08c43.71 0 79.47-35.76 79.47-79.47V155.79999999999995c0-43.72-35.76-79.48-79.47-79.48z m15.4 129.88V587.1700000000001c0 36.2-29.61 65.81-65.81 65.81H196.41c-36.2 0-65.81-29.61-65.81-65.81V206.20000000000005c0-36.2 29.61-65.81 65.81-65.81h637.26c36.19 0 65.81 29.62 65.81 65.81z" horiz-adv-x="1024" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,118 @@
<template>
<div class="animation-list">
<div class="div-animation">
<el-button @click="isShowAnimation = true">添加动画</el-button>
<el-button @click="previewAnimate">预览动画</el-button>
<div>
<el-tag
v-for="(tag, index) in curComponent.animations"
:key="index"
closable
@close="removeAnimation(index)"
>
{{ tag.label }}
</el-tag>
</div>
</div>
<!-- 选择动画 -->
<Modal v-model="isShowAnimation">
<el-tabs v-model="animationActiveName">
<el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :name="item.label">
<el-scrollbar class="animate-container">
<div
class="animate"
v-for="(animate, index) in item.children"
:key="index"
@mouseover="hoverPreviewAnimate = animate.value"
@click="addAnimation(animate)"
>
<div :class="[hoverPreviewAnimate === animate.value && animate.value + ' animated']">
{{ animate.label }}
</div>
</div>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</Modal>
</div>
</template>
<script>
import Modal from '@/components/Modal'
import eventBus from '@/utils/eventBus'
import animationClassData from '@/utils/animationClassData'
import { mapState } from 'vuex'
export default {
components: { Modal },
data() {
return {
isShowAnimation: false,
hoverPreviewAnimate: '',
animationActiveName: '进入',
animationClassData,
showAnimatePanel: false,
}
},
computed: mapState([
'curComponent',
]),
methods: {
addAnimation(animate) {
this.$store.commit('addAnimation', animate)
this.isShowAnimation = false
},
previewAnimate() {
eventBus.$emit('runAnimation')
},
removeAnimation(index) {
this.$store.commit('removeAnimation', index)
},
},
}
</script>
<style lang="scss">
.animation-list {
.div-animation {
text-align: center;
& > div {
margin-top: 20px;
}
.el-tag {
display: block;
width: 50%;
margin: auto;
margin-bottom: 10px;
}
}
.el-scrollbar__view {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 10px;
.animate > div {
width: 100px;
height: 60px;
background: #f5f8fb;
display: flex;
align-items: center;
justify-content: center;
margin: 0 12px;
margin-bottom: 10px;
font-size: 12px;
color: #333;
border-radius: 3px;
user-select: none;
cursor: pointer;
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="attr-list">
<el-form>
<el-form-item v-for="(key, index) in styleKeys.filter(item => item != 'rotate')" :key="index" :label="map[key]">
<el-color-picker v-if="key == 'borderColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'color'" v-model="curComponent.style[key]"></el-color-picker>
<el-color-picker v-else-if="key == 'backgroundColor'" v-model="curComponent.style[key]"></el-color-picker>
<el-select v-else-if="key == 'textAlign'" v-model="curComponent.style[key]">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-input type="number" v-else v-model="curComponent.style[key]" />
</el-form-item>
<el-form-item label="内容" v-if="curComponent && !excludes.includes(curComponent.component)">
<el-input type="textarea" v-model="curComponent.propValue" />
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
excludes: ['Picture', 'Group'], //
options: [
{
label: '左对齐',
value: 'left',
},
{
label: '居中',
value: 'center',
},
{
label: '右对齐',
value: 'right',
},
],
map: {
left: 'x 坐标',
top: 'y 坐标',
height: '高',
width: '宽',
color: '颜色',
backgroundColor: '背景色',
borderWidth: '边框宽度',
borderColor: '边框颜色',
borderRadius: '边框半径',
fontSize: '字体大小',
fontWeight: '字体粗细',
lineHeight: '行高',
letterSpacing: '字间距',
textAlign: '对齐方式',
opacity: '透明度',
},
}
},
computed: {
styleKeys() {
return this.$store.state.curComponent? Object.keys(this.$store.state.curComponent.style) : []
},
curComponent() {
return this.$store.state.curComponent
},
},
}
</script>
<style lang="scss" scoped>
.attr-list {
overflow: auto;
padding: 20px;
padding-top: 0;
height: 100%;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="component-list" @dragstart="handleDragStart">
<div
v-for="(item, index) in componentList"
:key="index"
class="list"
draggable
:data-index="index"
>
<span class="iconfont" :class="'icon-' + item.icon" />
<span>{{ item.label }}</span>
</div>
</div>
</template>
<script>
import componentList from '@/custom-component/component-list'
export default {
data() {
return {
componentList
}
},
methods: {
handleDragStart(e) {
e.dataTransfer.setData('index', e.target.dataset.index)
}
}
}
</script>
<style lang="scss" scoped>
.component-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 10px;
.list {
width: 45%;
border: 1px solid #ddd;
cursor: grab;
margin-bottom: 10px;
text-align: center;
color: #333;
padding: 2px 5px;
display: flex;
align-items: center;
justify-content: center;
&:active {
cursor: grabbing;
}
.iconfont {
margin-right: 4px;
font-size: 20px;
}
.icon-wenben,
.icon-tupian {
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div :style="{
left: start.x + 'px',
top: start.y + 'px',
width: width + 'px',
height: height + 'px',
}" class="area"></div>
</template>
<script>
export default {
props: {
start: {
type: Object,
},
width: {
type: Number,
},
height: {
type: Number,
},
},
}
</script>
<style lang="scss" scoped>
.area {
border: 1px solid #70c0ff;
position: absolute;
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div @click="handleClick">
<component
class="component"
:is="config.component"
:style="getStyle(config.style)"
:propValue="config.propValue"
:element="config"
/>
</div>
</template>
<script>
import { getStyle } from '@/utils/style'
import runAnimation from '@/utils/runAnimation'
import { mixins } from '@/utils/events'
export default {
props: {
config: {
type: Object,
require: true,
},
},
mounted() {
runAnimation(this.$el, this.config.animations)
},
mixins: [mixins],
methods: {
getStyle,
handleClick() {
const events = this.config.events
Object.keys(events).forEach(event => {
this[event](events[event])
})
},
},
}
</script>
<style lang="scss" scoped>
.component {
position: absolute;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<div class="contextmenu" v-show="menuShow" :style="{ top: menuTop + 'px', left: menuLeft + 'px' }">
<ul @mouseup="handleMouseUp">
<template v-if="curComponent">
<template v-if="!curComponent.isLock">
<li @click="copy">复制</li>
<li @click="paste">粘贴</li>
<li @click="cut">剪切</li>
<li @click="deleteComponent">删除</li>
<li @click="lock">锁定</li>
<li @click="topComponent">置顶</li>
<li @click="bottomComponent">置底</li>
<li @click="upComponent">上移</li>
<li @click="downComponent">下移</li>
</template>
<li v-else @click="unlock">解锁</li>
</template>
<li v-else @click="paste">粘贴</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
copyData: null,
}
},
computed: mapState([
'menuTop',
'menuLeft',
'menuShow',
'curComponent',
]),
methods: {
lock() {
this.$store.commit('lock')
},
unlock() {
this.$store.commit('unlock')
},
//
handleMouseUp() {
this.$store.commit('setClickComponentStatus', true)
},
cut() {
this.$store.commit('cut')
},
copy() {
this.$store.commit('copy')
},
paste() {
this.$store.commit('paste', true)
this.$store.commit('recordSnapshot')
},
deleteComponent() {
this.$store.commit('deleteComponent')
this.$store.commit('recordSnapshot')
},
upComponent() {
this.$store.commit('upComponent')
this.$store.commit('recordSnapshot')
},
downComponent() {
this.$store.commit('downComponent')
this.$store.commit('recordSnapshot')
},
topComponent() {
this.$store.commit('topComponent')
this.$store.commit('recordSnapshot')
},
bottomComponent() {
this.$store.commit('bottomComponent')
this.$store.commit('recordSnapshot')
},
},
}
</script>
<style lang="scss" scoped>
.contextmenu {
position: absolute;
z-index: 1000;
ul {
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
box-sizing: border-box;
margin: 5px 0;
padding: 6px 0;
li {
font-size: 14px;
padding: 0 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #606266;
height: 34px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
&:hover {
background-color: #f5f7fa;
}
}
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<svg class="grid" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="smallGrid" width="7.236328125" height="7.236328125" patternUnits="userSpaceOnUse">
<path
d="M 7.236328125 0 L 0 0 0 7.236328125"
fill="none"
stroke="rgba(207, 207, 207, 0.3)"
stroke-width="1">
</path>
</pattern>
<pattern id="grid" width="36.181640625" height="36.181640625" patternUnits="userSpaceOnUse">
<rect width="36.181640625" height="36.181640625" fill="url(#smallGrid)"></rect>
<path
d="M 36.181640625 0 L 0 0 0 36.181640625"
fill="none"
stroke="rgba(186, 186, 186, 0.5)"
stroke-width="1">
</path>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)"></rect>
</svg>
</template>
<style lang="scss" scoped>
.grid {
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div class="mark-line">
<div
v-for="line in lines"
:key="line"
class="line"
:class="line.includes('x')? 'xline' : 'yline'"
:ref="line"
v-show="lineStatus[line] || false"
></div>
</div>
</template>
<script>
import eventBus from '@/utils/eventBus'
import { mapState } from 'vuex'
import { getComponentRotatedStyle } from '@/utils/style'
export default {
data() {
return {
lines: ['xt', 'xc', 'xb', 'yl', 'yc', 'yr'], // 线线
diff: 3, // dff
lineStatus: {
xt: false,
xc: false,
xb: false,
yl: false,
yc: false,
yr: false,
},
}
},
computed: mapState([
'curComponent',
'componentData',
]),
mounted() {
//
eventBus.$on('move', (isDownward, isRightward) => {
this.showLine(isDownward, isRightward)
})
eventBus.$on('unmove', () => {
this.hideLine()
})
},
methods: {
hideLine() {
Object.keys(this.lineStatus).forEach(line => {
this.lineStatus[line] = false
})
},
showLine(isDownward, isRightward) {
const lines = this.$refs
const components = this.componentData
const curComponentStyle = getComponentRotatedStyle(this.curComponent.style)
const curComponentHalfwidth = curComponentStyle.width / 2
const curComponentHalfHeight = curComponentStyle.height / 2
this.hideLine()
components.forEach(component => {
if (component == this.curComponent) return
const componentStyle = getComponentRotatedStyle(component.style)
const { top, left, bottom, right } = componentStyle
const componentHalfwidth = componentStyle.width / 2
const componentHalfHeight = componentStyle.height / 2
const conditions = {
top: [
{
isNearly: this.isNearly(curComponentStyle.top, top),
lineNode: lines.xt[0], // xt
line: 'xt',
dragShift: top,
lineShift: top,
},
{
isNearly: this.isNearly(curComponentStyle.bottom, top),
lineNode: lines.xt[0], // xt
line: 'xt',
dragShift: top - curComponentStyle.height,
lineShift: top,
},
{
//
isNearly: this.isNearly(curComponentStyle.top + curComponentHalfHeight, top + componentHalfHeight),
lineNode: lines.xc[0], // xc
line: 'xc',
dragShift: top + componentHalfHeight - curComponentHalfHeight,
lineShift: top + componentHalfHeight,
},
{
isNearly: this.isNearly(curComponentStyle.top, bottom),
lineNode: lines.xb[0], // xb
line: 'xb',
dragShift: bottom,
lineShift: bottom,
},
{
isNearly: this.isNearly(curComponentStyle.bottom, bottom),
lineNode: lines.xb[0], // xb
line: 'xb',
dragShift: bottom - curComponentStyle.height,
lineShift: bottom,
},
],
left: [
{
isNearly: this.isNearly(curComponentStyle.left, left),
lineNode: lines.yl[0], // yl
line: 'yl',
dragShift: left,
lineShift: left,
},
{
isNearly: this.isNearly(curComponentStyle.right, left),
lineNode: lines.yl[0], // yl
line: 'yl',
dragShift: left - curComponentStyle.width,
lineShift: left,
},
{
//
isNearly: this.isNearly(curComponentStyle.left + curComponentHalfwidth, left + componentHalfwidth),
lineNode: lines.yc[0], // yc
line: 'yc',
dragShift: left + componentHalfwidth - curComponentHalfwidth,
lineShift: left + componentHalfwidth,
},
{
isNearly: this.isNearly(curComponentStyle.left, right),
lineNode: lines.yr[0], // yr
line: 'yr',
dragShift: right,
lineShift: right,
},
{
isNearly: this.isNearly(curComponentStyle.right, right),
lineNode: lines.yr[0], // yr
line: 'yr',
dragShift: right - curComponentStyle.width,
lineShift: right,
},
],
}
const needToShow = []
const { rotate } = this.curComponent.style
Object.keys(conditions).forEach(key => {
//
conditions[key].forEach((condition) => {
if (!condition.isNearly) return
//
this.$store.commit('setShapeSingleStyle', {
key,
value: rotate != 0? this.translatecurComponentShift(key, condition, curComponentStyle) : condition.dragShift,
})
condition.lineNode.style[key] = `${condition.lineShift}px`
needToShow.push(condition.line)
})
})
// 线
// 线线
if (needToShow.length) {
this.chooseTheTureLine(needToShow, isDownward, isRightward)
}
})
},
translatecurComponentShift(key, condition, curComponentStyle) {
const { width, height } = this.curComponent.style
if (key == 'top') {
return Math.round(condition.dragShift - (height - curComponentStyle.height) / 2)
}
return Math.round(condition.dragShift - (width - curComponentStyle.width) / 2)
},
chooseTheTureLine(needToShow, isDownward, isRightward) {
// 线
// 线
if (isRightward) {
if (needToShow.includes('yr')) {
this.lineStatus.yr = true
} else if (needToShow.includes('yc')) {
this.lineStatus.yc = true
} else if (needToShow.includes('yl')) {
this.lineStatus.yl = true
}
} else {
// eslint-disable-next-line no-lonely-if
if (needToShow.includes('yl')) {
this.lineStatus.yl = true
} else if (needToShow.includes('yc')) {
this.lineStatus.yc = true
} else if (needToShow.includes('yr')) {
this.lineStatus.yr = true
}
}
if (isDownward) {
if (needToShow.includes('xb')) {
this.lineStatus.xb = true
} else if (needToShow.includes('xc')) {
this.lineStatus.xc = true
} else if (needToShow.includes('xt')) {
this.lineStatus.xt = true
}
} else {
// eslint-disable-next-line no-lonely-if
if (needToShow.includes('xt')) {
this.lineStatus.xt = true
} else if (needToShow.includes('xc')) {
this.lineStatus.xc = true
} else if (needToShow.includes('xb')) {
this.lineStatus.xb = true
}
}
},
isNearly(dragValue, targetValue) {
return Math.abs(dragValue - targetValue) <= this.diff
},
},
}
</script>
<style lang="scss" scoped>
.mark-line {
height: 100%;
}
.line {
background: #59c7f9;
position: absolute;
z-index: 1000;
}
.xline {
width: 100%;
height: 1px;
}
.yline {
width: 1px;
height: 100%;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<div class="bg" v-if="show">
<el-button @click="close" class="close">关闭</el-button>
<div class="canvas-container">
<div class="canvas"
:style="{
width: changeStyleWithScale(canvasStyleData.width) + 'px',
height: changeStyleWithScale(canvasStyleData.height) + 'px',
}"
>
<ComponentWrapper
v-for="(item, index) in componentData"
:key="index"
:config="item"
/>
</div>
</div>
</div>
</template>
<script>
import { getStyle } from '@/utils/style'
import { mapState } from 'vuex'
import ComponentWrapper from './ComponentWrapper'
import { changeStyleWithScale } from '@/utils/translate'
export default {
model: {
prop: 'show',
event: 'change',
},
props: {
show: {
type: Boolean,
default: false,
},
},
components: { ComponentWrapper },
computed: mapState([
'componentData',
'canvasStyleData',
]),
methods: {
changeStyleWithScale,
getStyle,
close() {
this.$emit('change', false)
},
},
}
</script>
<style lang="scss" scoped>
.bg {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: fixed;
background: rgb(0, 0, 0, .5);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: 20px;
.canvas-container {
width: calc(100% - 40px);
height: calc(100% - 120px);
overflow: auto;
.canvas {
background: #fff;
position: relative;
margin: auto;
}
}
.close {
position: absolute;
right: 20px;
top: 100px;
}
}
</style>

View File

@ -0,0 +1,388 @@
<template>
<div class="shape" :class="{ active }" @click="selectCurComponent" @mousedown="handleMouseDownOnShape">
<span v-show="isActive()" class="iconfont icon-xiangyouxuanzhuan" @mousedown="handleRotate" />
<span v-show="element.isLock" class="iconfont icon-suo" />
<div
v-for="item in (isActive()? pointList : [])"
:key="item"
class="shape-point"
:style="getPointStyle(item)"
@mousedown="handleMouseDownOnPoint(item, $event)"
/>
<slot />
</div>
</template>
<script>
import eventBus from '@/utils/eventBus'
import runAnimation from '@/utils/runAnimation'
import { mapState } from 'vuex'
import calculateComponentPositonAndSize from '@/utils/calculateComponentPositonAndSize'
import { mod360 } from '@/utils/translate'
export default {
props: {
active: {
type: Boolean,
default: false
},
element: {
require: true,
type: Object
},
defaultStyle: {
require: true,
type: Object
},
index: {
require: true,
type: [Number, String]
}
},
data() {
return {
pointList: ['lt', 't', 'rt', 'r', 'rb', 'b', 'lb', 'l'], //
initialAngle: { //
lt: 0,
t: 45,
rt: 90,
r: 135,
rb: 180,
b: 225,
lb: 270,
l: 315
},
angleToCursor: [ //
{ start: 338, end: 23, cursor: 'nw' },
{ start: 23, end: 68, cursor: 'n' },
{ start: 68, end: 113, cursor: 'ne' },
{ start: 113, end: 158, cursor: 'e' },
{ start: 158, end: 203, cursor: 'se' },
{ start: 203, end: 248, cursor: 's' },
{ start: 248, end: 293, cursor: 'sw' },
{ start: 293, end: 338, cursor: 'w' }
],
cursors: {}
}
},
computed: mapState([
'curComponent',
'editor'
]),
mounted() {
// Group
if (this.curComponent) {
this.cursors = this.getCursor() //
}
eventBus.$on('runAnimation', () => {
if (this.element == this.curComponent) {
runAnimation(this.$el, this.curComponent.animations)
}
})
},
methods: {
isActive() {
return this.active && !this.element.isLock
},
//
handleRotate(e) {
this.$store.commit('setClickComponentStatus', true)
e.preventDefault()
e.stopPropagation()
//
const pos = { ...this.defaultStyle }
const startY = e.clientY
const startX = e.clientX
const startRotate = pos.rotate
//
const rect = this.$el.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
//
const rotateDegreeBefore = Math.atan2(startY - centerY, startX - centerX) / (Math.PI / 180)
//
let hasMove = false
const move = (moveEvent) => {
hasMove = true
const curX = moveEvent.clientX
const curY = moveEvent.clientY
//
const rotateDegreeAfter = Math.atan2(curY - centerY, curX - centerX) / (Math.PI / 180)
//
pos.rotate = startRotate + rotateDegreeAfter - rotateDegreeBefore
//
this.$store.commit('setShapeStyle', pos)
}
const up = () => {
hasMove && this.$store.commit('recordSnapshot')
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
this.cursors = this.getCursor() //
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
},
getPointStyle(point) {
const { width, height } = this.defaultStyle
const hasT = /t/.test(point)
const hasB = /b/.test(point)
const hasL = /l/.test(point)
const hasR = /r/.test(point)
let newLeft = 0
let newTop = 0
//
if (point.length === 2) {
newLeft = hasL ? 0 : width
newTop = hasT ? 0 : height
} else {
//
if (hasT || hasB) {
newLeft = width / 2
newTop = hasT ? 0 : height
}
//
if (hasL || hasR) {
newLeft = hasL ? 0 : width
newTop = Math.floor(height / 2)
}
}
const style = {
marginLeft: hasR ? '-4px' : '-4px',
marginTop: '-4px',
left: `${newLeft}px`,
top: `${newTop}px`,
cursor: this.cursors[point]
}
return style
},
getCursor() {
const { angleToCursor, initialAngle, pointList, curComponent } = this
const rotate = mod360(curComponent.style.rotate) // 360
const result = {}
let lastMatchIndex = -1 //
pointList.forEach(point => {
const angle = mod360(initialAngle[point] + rotate)
const len = angleToCursor.length
while (true) {
lastMatchIndex = (lastMatchIndex + 1) % len
const angleLimit = angleToCursor[lastMatchIndex]
if (angle < 23 || angle >= 338) {
result[point] = 'nw-resize'
return
}
if (angleLimit.start <= angle && angle < angleLimit.end) {
result[point] = angleLimit.cursor + '-resize'
return
}
}
})
return result
},
handleMouseDownOnShape(e) {
this.$store.commit('setClickComponentStatus', true)
if (this.element.component != 'v-text' && this.element.component != 'rect-shape') {
e.preventDefault()
}
e.stopPropagation()
this.$store.commit('setCurComponent', { component: this.element, index: this.index })
if (this.element.isLock) return
this.cursors = this.getCursor() //
const pos = { ...this.defaultStyle }
const startY = e.clientY
const startX = e.clientX
//
const startTop = Number(pos.top)
const startLeft = Number(pos.left)
//
let hasMove = false
const move = (moveEvent) => {
hasMove = true
const curX = moveEvent.clientX
const curY = moveEvent.clientY
pos.top = curY - startY + startTop
pos.left = curX - startX + startLeft
//
this.$store.commit('setShapeStyle', pos)
//
// 使 $nextTick
this.$nextTick(() => {
// 线
//
// curY - startY > 0 true false
// curX - startX > 0 true false
eventBus.$emit('move', curY - startY > 0, curX - startX > 0)
})
}
const up = () => {
hasMove && this.$store.commit('recordSnapshot')
// 线
eventBus.$emit('unmove')
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
},
selectCurComponent(e) {
//
e.stopPropagation()
e.preventDefault()
this.$store.commit('hideContextMenu')
},
handleMouseDownOnPoint(point, e) {
this.$store.commit('setClickComponentStatus', true)
e.stopPropagation()
e.preventDefault()
const style = { ...this.defaultStyle }
//
const proportion = style.width / style.height
//
const center = {
x: style.left + style.width / 2,
y: style.top + style.height / 2
}
//
const editorRectInfo = this.editor.getBoundingClientRect()
//
const curPoint = {
x: e.clientX - editorRectInfo.left,
y: e.clientY - editorRectInfo.top
}
//
const symmetricPoint = {
x: center.x - (curPoint.x - center.x),
y: center.y - (curPoint.y - center.y)
}
//
let needSave = false
let isFirst = true
const needLockProportion = this.isNeedLockProportion()
const move = (moveEvent) => {
// move
// move
if (isFirst) {
isFirst = false
return
}
needSave = true
const curPositon = {
x: moveEvent.clientX - editorRectInfo.left,
y: moveEvent.clientY - editorRectInfo.top
}
calculateComponentPositonAndSize(point, style, curPositon, proportion, needLockProportion, {
center,
curPoint,
symmetricPoint
})
console.log('this is test:' + JSON.stringify(this.element.propValue.viewId))
this.$store.commit('setShapeStyle', style)
eventBus.$emit('resizing', this.element.propValue.viewId)
}
const up = () => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
needSave && this.$store.commit('recordSnapshot')
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
},
isNeedLockProportion() {
if (this.element.component != 'Group') return false
const ratates = [0, 90, 180, 360]
for (const component of this.element.propValue) {
if (!ratates.includes(mod360(parseInt(component.style.rotate)))) {
return true
}
}
return false
}
}
}
</script>
<style lang="scss" scoped>
.shape {
position: absolute;
&:hover {
cursor: move;
}
}
.active {
outline: 1px solid #70c0ff;
user-select: none;
}
.shape-point {
position: absolute;
background: #fff;
border: 1px solid #59c7f9;
width: 8px;
height: 8px;
border-radius: 50%;
z-index: 1;
}
.icon-xiangyouxuanzhuan {
position: absolute;
top: -34px;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
font-weight: 600;
cursor: grab;
color: #59c7f9;
font-size: 20px;
font-weight: 600;
&:active {
cursor: grabbing;
}
}
.icon-suo {
position: absolute;
top: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,307 @@
<template>
<div
id="editor"
class="editor"
:class="{ edit: isEdit }"
:style="{
width: changeStyleWithScale(canvasStyleData.width) + 'px',
height: changeStyleWithScale(canvasStyleData.height) + 'px',
}"
@contextmenu="handleContextMenu"
@mousedown="handleMouseDown"
>
<!-- 网格线 -->
<Grid />
<!--页面组件列表展示-->
<Shape
v-for="(item, index) in componentData"
:key="item.id"
:default-style="item.style"
:style="getShapeStyle(item.style)"
:active="item === curComponent"
:element="item"
:index="index"
:class="{ lock: item.isLock }"
>
<component
:is="item.component"
v-if="item.component != 'v-text'"
:id="'component' + item.id"
class="component"
:style="getComponentStyle(item.style)"
:prop-value="item.propValue"
:element="item"
/>
<component
:is="item.component"
v-else
:id="'component' + item.id"
class="component"
:style="getComponentStyle(item.style)"
:prop-value="item.propValue"
:element="item"
@input="handleInput"
/>
</Shape>
<!-- 右击菜单 -->
<ContextMenu />
<!-- 标线 -->
<MarkLine />
<!-- 选中区域 -->
<Area v-show="isShowArea" :start="start" :width="width" :height="height" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import Shape from './Shape'
import { getStyle, getComponentRotatedStyle } from '@/utils/style'
import { $ } from '@/utils/utils'
import ContextMenu from './ContextMenu'
import MarkLine from './MarkLine'
import Area from './Area'
import eventBus from '@/utils/eventBus'
import Grid from './Grid'
import { changeStyleWithScale } from '@/utils/translate'
export default {
components: { Shape, ContextMenu, MarkLine, Area, Grid },
props: {
isEdit: {
type: Boolean,
default: true
}
},
data() {
return {
editorX: 0,
editorY: 0,
start: { //
x: 0,
y: 0
},
width: 0,
height: 0,
isShowArea: false
}
},
computed: mapState([
'componentData',
'curComponent',
'canvasStyleData',
'editor'
]),
mounted() {
//
this.$store.commit('getEditor')
eventBus.$on('hideArea', () => {
this.hideArea()
})
},
methods: {
changeStyleWithScale,
handleMouseDown(e) {
// e.preventDefault() drop
if (!this.curComponent || (this.curComponent.component != 'v-text' && this.curComponent.component != 'rect-shape')) {
e.preventDefault()
}
this.hideArea()
// 便
const rectInfo = this.editor.getBoundingClientRect()
this.editorX = rectInfo.x
this.editorY = rectInfo.y
const startX = e.clientX
const startY = e.clientY
this.start.x = startX - this.editorX
this.start.y = startY - this.editorY
//
this.isShowArea = true
const move = (moveEvent) => {
debugger
this.width = Math.abs(moveEvent.clientX - startX)
this.height = Math.abs(moveEvent.clientY - startY)
if (moveEvent.clientX < startX) {
this.start.x = moveEvent.clientX - this.editorX
}
if (moveEvent.clientY < startY) {
this.start.y = moveEvent.clientY - this.editorY
}
}
const up = (e) => {
document.removeEventListener('mousemove', move)
document.removeEventListener('mouseup', up)
if (e.clientX == startX && e.clientY == startY) {
this.hideArea()
return
}
this.createGroup()
}
document.addEventListener('mousemove', move)
document.addEventListener('mouseup', up)
},
hideArea() {
this.isShowArea = 0
this.width = 0
this.height = 0
},
createGroup() {
//
const areaData = this.getSelectArea()
if (areaData.length <= 1) {
this.hideArea()
return
}
// Group
// left top right bottom
let top = Infinity; let left = Infinity
let right = -Infinity; let bottom = -Infinity
areaData.forEach(component => {
let style = {}
if (component.component == 'Group') {
component.propValue.forEach(item => {
const rectInfo = $(`#component${item.id}`).getBoundingClientRect()
style.left = rectInfo.left - this.editorX
style.top = rectInfo.top - this.editorY
style.right = rectInfo.right - this.editorX
style.bottom = rectInfo.bottom - this.editorY
if (style.left < left) left = style.left
if (style.top < top) top = style.top
if (style.right > right) right = style.right
if (style.bottom > bottom) bottom = style.bottom
})
} else {
style = getComponentRotatedStyle(component.style)
}
if (style.left < left) left = style.left
if (style.top < top) top = style.top
if (style.right > right) right = style.right
if (style.bottom > bottom) bottom = style.bottom
})
this.start.x = left
this.start.y = top
this.width = right - left
this.height = bottom - top
//
this.$store.commit('setAreaData', {
style: {
left,
top,
width: this.width,
height: this.height
},
components: areaData
})
},
getSelectArea() {
const result = []
//
const { x, y } = this.start
//
this.componentData.forEach(component => {
if (component.isLock) return
const { left, top, width, height } = component.style
if (x <= left && y <= top && (left + width <= x + this.width) && (top + height <= y + this.height)) {
result.push(component)
}
})
//
return result
},
handleContextMenu(e) {
e.stopPropagation()
e.preventDefault()
//
let target = e.target
let top = e.offsetY
let left = e.offsetX
while (target instanceof SVGElement) {
target = target.parentNode
}
while (!target.className.includes('editor')) {
left += target.offsetLeft
top += target.offsetTop
target = target.parentNode
}
this.$store.commit('showContextMenu', { top, left })
},
getShapeStyle(style) {
const result = {};
['width', 'height', 'top', 'left', 'rotate'].forEach(attr => {
if (attr != 'rotate') {
result[attr] = style[attr] + 'px'
} else {
result.transform = 'rotate(' + style[attr] + 'deg)'
}
})
return result
},
getComponentStyle(style) {
return getStyle(style, ['top', 'left', 'width', 'height', 'rotate'])
},
handleInput(element, value) {
// shape
this.$store.commit('setShapeStyle', { height: this.getTextareaHeight(element, value) })
},
getTextareaHeight(element, text) {
let { lineHeight, fontSize, height } = element.style
if (lineHeight === '') {
lineHeight = 1.5
}
const newHeight = (text.split('<br>').length - 1) * lineHeight * fontSize
return height > newHeight ? height : newHeight
}
}
}
</script>
<style lang="scss" scoped>
.editor {
position: relative;
background: #fff;
margin: auto;
.lock {
opacity: .5;
}
}
.edit {
.component {
outline: none;
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="event-list">
<div class="div-events">
<el-button @click="isShowEvent = true">添加事件</el-button>
<div>
<el-tag
v-for="event in Object.keys(curComponent.events)"
:key="event"
closable
@close="removeEvent(event)"
>
{{ event }}
</el-tag>
</div>
</div>
<!-- 选择事件 -->
<Modal v-model="isShowEvent">
<el-tabs v-model="eventActiveName">
<el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
<el-input v-if="item.key == 'redirect'" v-model="item.param" type="textarea" placeholder="请输入完整的 URL" />
<el-input v-if="item.key == 'alert'" v-model="item.param" type="textarea" placeholder="请输入要 alert 的内容" />
<el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)">确定</el-button>
</el-tab-pane>
</el-tabs>
</Modal>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Modal from '@/components/Modal'
import { eventList } from '@/utils/events'
export default {
components: { Modal },
data() {
return {
isShowEvent: false,
eventURL: '',
eventActiveName: 'redirect',
eventList,
}
},
computed: mapState([
'curComponent',
]),
methods: {
addEvent(event, param) {
this.isShowEvent = false
this.$store.commit('addEvent', { event, param })
},
removeEvent(event) {
this.$store.commit('removeEvent', event)
},
},
}
</script>
<style lang="scss" scoped>
.event-list {
.div-events {
text-align: center;
padding: 0 20px;
.el-button {
display: inline-block;
margin-bottom: 10px;
}
.el-tag {
display: block;
width: 50%;
margin: auto;
margin-bottom: 10px;
}
}
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="modal-bg" v-if="show" @click="hide">
<div class="fadeInLeft animated modal" @click="stopPropagation">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'show',
event: 'change',
},
props: {
show: {
type: Boolean,
default: false,
},
},
methods: {
hide() {
this.$emit('change')
},
stopPropagation(e) {
e.stopPropagation()
},
},
}
</script>
<style lang="scss" scoped>
.modal-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,.5);
z-index: 1001;
.modal {
width: 400px;
background: #fff;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<div>
<div class="toolbar">
<el-tooltip content="撤消">
<el-button class="el-icon-refresh-left" size="mini" circle @click="undo" />
</el-tooltip>
<el-tooltip content="重做">
<el-button class="el-icon-refresh-left" size="mini" circle @click="redo" />
</el-tooltip>
<el-tooltip content="插入图片">
<el-button class="el-icon-refresh-left" for="input" size="mini" circle />
<input id="input" type="file" hidden @change="handleFileChange">
</el-tooltip>
<el-tooltip content="预览">
<el-button class="el-icon-refresh-left" size="mini" circle @click="preview" />
</el-tooltip>
<el-tooltip content="清空">
<el-button class="el-icon-refresh-left" size="mini" circle @click="clearCanvas" />
</el-tooltip>
<el-tooltip content="保存">
<el-button class="el-icon-circle-check" size="mini" circle @click="save" />
</el-tooltip>
<el-tooltip content="预览">
<el-button class="el-icon-view" size="mini" circle @click="preview" />
</el-tooltip>
</div>
<!-- 预览 -->
<Preview v-model="isShowPreview" @change="handlePreviewChange" />
</div>
</template>
<script>
import generateID from '@/utils/generateID'
import toast from '@/utils/toast'
import { mapState } from 'vuex'
import Preview from '@/components/Editor/Preview'
import { commonStyle, commonAttr } from '@/custom-component/component-list'
import eventBus from '@/utils/eventBus'
import { deepCopy } from '@/utils/utils'
export default {
components: { Preview },
data() {
return {
isShowPreview: false,
needToChange: [
'top',
'left',
'width',
'height',
'fontSize',
'borderWidth'
],
scale: '100%',
timer: null
}
},
computed: mapState([
'componentData',
'canvasStyleData',
'areaData',
'curComponent'
]),
created() {
eventBus.$on('preview', this.preview)
eventBus.$on('save', this.save)
eventBus.$on('clearCanvas', this.clearCanvas)
this.scale = this.canvasStyleData.scale
},
methods: {
format(value) {
const scale = this.scale
return value * parseInt(scale) / 100
},
getOriginStyle(value) {
const scale = this.canvasStyleData.scale
const result = value / (parseInt(scale) / 100)
return result
},
handleScaleChange() {
clearTimeout(this.timer)
setTimeout(() => {
const componentData = deepCopy(this.componentData)
componentData.forEach(component => {
Object.keys(component.style).forEach(key => {
if (this.needToChange.includes(key)) {
//
// *
component.style[key] = this.format(this.getOriginStyle(component.style[key]))
}
})
})
this.$store.commit('setComponentData', componentData)
this.$store.commit('setCanvasStyle', {
...this.canvasStyleData,
scale: this.scale
})
}, 500)
},
lock() {
this.$store.commit('lock')
},
unlock() {
this.$store.commit('unlock')
},
compose() {
this.$store.commit('compose')
this.$store.commit('recordSnapshot')
},
decompose() {
this.$store.commit('decompose')
this.$store.commit('recordSnapshot')
},
undo() {
this.$store.commit('undo')
},
redo() {
this.$store.commit('redo')
},
handleFileChange(e) {
const file = e.target.files[0]
if (!file.type.includes('image')) {
toast('只能插入图片')
return
}
const reader = new FileReader()
reader.onload = (res) => {
const fileResult = res.target.result
const img = new Image()
img.onload = () => {
this.$store.commit('addComponent', {
component: {
...commonAttr,
id: generateID(),
component: 'Picture',
label: '图片',
icon: '',
propValue: fileResult,
style: {
...commonStyle,
top: 0,
left: 0,
width: img.width,
height: img.height
}
}
})
this.$store.commit('recordSnapshot')
}
img.src = fileResult
}
reader.readAsDataURL(file)
},
preview() {
this.isShowPreview = true
this.$store.commit('setEditMode', 'preview')
},
save() {
localStorage.setItem('canvasData', JSON.stringify(this.componentData))
localStorage.setItem('canvasStyle', JSON.stringify(this.canvasStyleData))
this.$message.success('保存成功')
},
clearCanvas() {
this.$store.commit('setComponentData', [])
this.$store.commit('recordSnapshot')
},
handlePreviewChange() {
this.$store.commit('setEditMode', 'edit')
}
}
}
</script>
<style lang="scss" scoped>
.toolbar {
height: 35px;
line-height: 35px;
background: #fff;
border-bottom: 1px solid #ddd;
.canvas-config {
display: inline-block;
margin-left: 10px;
font-size: 14px;
color: #606266;
input {
width: 50px;
margin-left: 10px;
outline: none;
padding: 0 5px;
border: 1px solid #ddd;
color: #606266;
}
span {
margin-left: 10px;
}
}
.insert {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #FFF;
border: 1px solid #DCDFE6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: 0;
margin: 0;
transition: .1s;
font-weight: 500;
padding: 9px 15px;
font-size: 12px;
border-radius: 3px;
margin-left: 10px;
&:active {
color: #3a8ee6;
border-color: #3a8ee6;
outline: 0;
}
&:hover {
background-color: #ecf5ff;
color: #3a8ee6;
}
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="group">
<div>
<template v-for="item in propValue">
<component
class="component"
:is="item.component"
:style="item.groupStyle"
:propValue="item.propValue"
:key="item.id"
:id="'component' + item.id"
:element="item"
/>
</template>
</div>
</div>
</template>
<script>
import { getStyle } from '@/utils/style'
export default {
props: {
propValue: {
type: Array,
default: () => [],
},
element: {
type: Object,
},
},
created() {
const parentStyle = this.element.style
this.propValue.forEach(component => {
// component.groupStyle top left group
// component.groupStyle
if (!Object.keys(component.groupStyle).length) {
const style = { ...component.style }
component.groupStyle = getStyle(style)
component.groupStyle.left = this.toPercent((style.left - parentStyle.left) / parentStyle.width)
component.groupStyle.top = this.toPercent((style.top - parentStyle.top) / parentStyle.height)
component.groupStyle.width = this.toPercent(style.width / parentStyle.width)
component.groupStyle.height = this.toPercent(style.height / parentStyle.height)
}
})
},
methods: {
toPercent(val) {
return val * 100 + '%'
},
},
}
</script>
<style lang="scss" scoped>
.group {
& > div {
position: relative;
width: 100%;
height: 100%;
.component {
position: absolute;
}
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<div style="overflow: hidden">
<img :src="propValue">
</div>
</template>
<script>
export default {
props: {
propValue: {
type: String,
require: true,
},
},
}
</script>
<style lang="scss" scoped>
img {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<div class="rect-shape">
<v-text :propValue="element.propValue" :element="element" />
</div>
</template>
<script>
export default {
props: {
element: {
type: Object,
},
},
}
</script>
<style lang="scss" scoped>
.rect-shape {
width: 100%;
height: 100%;
overflow: auto;
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div class="rect-shape">
<chart-component v-if="showCard" :ref="element.propValue.id" :chart-id="element.propValue.id" :chart="chart" />
</div>
</template>
<script>
import { post } from '@/api/panel/panel'
import ChartComponent from '@/views/chart/components/ChartComponent.vue'
export default {
name: 'UserView',
components: { ChartComponent },
props: {
element: {
type: Object
}
},
data() {
return {
chart: {},
showCard: false
}
},
created() {
const id = this.element.propValue.viewId
debugger
this.$nextTick(() => {
// eChar
this.getData(id)
})
},
mounted() {
},
methods: {
getData(id) {
if (id) {
post('/chart/view/getData/' + id, null).then(response => {
// echart
this.chart = response.data
this.showCard = true
})
}
}
}
}
</script>
<style lang="scss" scoped>
.rect-shape {
width: 100%;
height: 100%;
overflow: auto;
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<button class="v-button">{{ propValue }}</button>
</template>
<script>
export default {
props: {
propValue: {
type: String,
default: '',
},
},
}
</script>
<style lang="scss" scoped>
.v-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #FFF;
border: 1px solid #DCDFE6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: 0;
margin: 0;
transition: .1s;
font-weight: 500;
width: 100%;
height: 100%;
font-size: 14px;
&:active {
color: #3a8ee6;
border-color: #3a8ee6;
outline: 0;
}
&:hover {
background-color: #ecf5ff;
color: #3a8ee6;
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div v-if="editMode == 'edit'" class="v-text" @keydown="handleKeydown" @keyup="handleKeyup">
<!-- tabindex >= 0 使得双击时聚集该元素 -->
<div :contenteditable="canEdit" :class="{ canEdit }" @dblclick="setEdit" :tabindex="element.id" @paste="clearStyle"
@mousedown="handleMousedown" @blur="handleBlur" ref="text" v-html="element.propValue" @input="handleInput"
:style="{ verticalAlign: element.style.verticalAlign }"
></div>
</div>
<div v-else class="v-text">
<div v-html="element.propValue" :style="{ verticalAlign: element.style.verticalAlign }"></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { keycodes } from '@/utils/shortcutKey.js'
export default {
props: {
propValue: {
type: String,
require: true,
},
element: {
type: Object,
},
},
data() {
return {
canEdit: false,
ctrlKey: 17,
isCtrlDown: false,
}
},
computed: {
...mapState([
'editMode',
]),
},
methods: {
handleInput(e) {
this.$emit('input', this.element, e.target.innerHTML)
},
handleKeydown(e) {
if (e.keyCode == this.ctrlKey) {
this.isCtrlDown = true
} else if (this.isCtrlDown && this.canEdit && keycodes.includes(e.keyCode)) {
e.stopPropagation()
} else if (e.keyCode == 46) { // deleteKey
e.stopPropagation()
}
},
handleKeyup(e) {
if (e.keyCode == this.ctrlKey) {
this.isCtrlDown = false
}
},
handleMousedown(e) {
if (this.canEdit) {
e.stopPropagation()
}
},
clearStyle(e) {
e.preventDefault()
const clp = e.clipboardData
const text = clp.getData('text/plain') || ''
if (text !== '') {
document.execCommand('insertText', false, text)
}
this.$emit('input', this.element, e.target.innerHTML)
},
handleBlur(e) {
this.element.propValue = e.target.innerHTML || '&nbsp;'
this.canEdit = false
},
setEdit() {
this.canEdit = true
//
this.selectText(this.$refs.text)
},
selectText(element) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection.removeAllRanges()
selection.addRange(range)
},
},
}
</script>
<style lang="scss" scoped>
.v-text {
width: 100%;
height: 100%;
display: table;
div {
display: table-cell;
width: 100%;
height: 100%;
outline: none;
}
.canEdit {
cursor: text;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,104 @@
// 公共样式
export const commonStyle = {
rotate: 0,
opacity: 1
}
export const commonAttr = {
animations: [],
events: {},
groupStyle: {}, // 当一个组件成为 Group 的子组件时使用
isLock: false // 是否锁定组件
}
// 编辑器左侧组件列表
const list = [
{
component: 'v-text',
label: '文字',
propValue: '双击编辑文字',
icon: 'wenben',
style: {
width: 200,
height: 22,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: ''
}
},
{
component: 'v-button',
label: '按钮',
propValue: '按钮',
icon: 'button',
style: {
width: 100,
height: 34,
borderWidth: '',
borderColor: '',
borderRadius: '',
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: '',
color: '',
backgroundColor: ''
}
},
{
component: 'Picture',
label: '图片',
icon: 'tupian',
propValue: require('@/assets/title.jpg'),
style: {
width: 300,
height: 200,
borderRadius: ''
}
},
{
component: 'rect-shape',
label: '矩形',
propValue: '&nbsp;',
icon: 'juxing',
style: {
width: 200,
height: 200,
fontSize: 14,
fontWeight: 500,
lineHeight: '',
letterSpacing: 0,
textAlign: 'center',
color: '',
borderColor: '#000',
borderWidth: 1,
backgroundColor: '',
borderStyle: 'solid',
verticalAlign: 'middle'
}
},
{
component: 'user-view',
label: '用户视图',
propValue: '',
icon: 'juxing',
type: 'view',
style: {
width: 200,
height: 300,
borderWidth: 1
}
}
]
for (let i = 0, len = list.length; i < len; i++) {
const item = list[i]
item.style = { ...commonStyle, ...item.style }
list[i] = { ...commonAttr, ...item }
}
export default list

View File

@ -0,0 +1,16 @@
import Vue from 'vue'
import Picture from '@/custom-component/Picture'
import VText from '@/custom-component/VText'
import VButton from '@/custom-component/VButton'
import Group from '@/custom-component/Group'
import RectShape from '@/custom-component/RectShape'
import UserView from '@/custom-component/UserView'
Vue.component('Picture', Picture)
Vue.component('VText', VText)
Vue.component('VButton', VButton)
Vue.component('Group', Group)
Vue.component('RectShape', RectShape)
Vue.component('UserView', UserView)

View File

@ -15,6 +15,14 @@ import api from '@/api/index.js'
import filter from '@/filter/filter'
import directives from './directive'
import '@/custom-component' // 注册自定义组件
import '@/assets/iconfont/iconfont.css'
import '@/styles/animate.css'
import 'element-ui/lib/theme-chalk/index.css'
import '@/styles/reset.css'
Vue.config.productionTip = false
Vue.prototype.$api = api
import * as echarts from 'echarts'

View File

@ -71,6 +71,18 @@ export const constantRoutes = [
}
]
},
{
path: '/panelCanvas',
component: Layout,
redirect: '/panelCanvas/canvas',
hidden: true,
children: [
{
path: 'canvas',
component: () => import('@/views/panel/canvas')
}
]
},
{
path: '/preview',

View File

@ -0,0 +1,11 @@
export default {
mutations: {
addAnimation({ curComponent }, animation) {
curComponent.animations.push(animation)
},
removeAnimation({ curComponent }, index) {
curComponent.animations.splice(index, 1)
},
},
}

View File

@ -0,0 +1,100 @@
import store from './index'
import generateID from '@/utils/generateID'
import eventBus from '@/utils/eventBus'
import decomposeComponent from '@/utils/decomposeComponent'
import { $ } from '@/utils/utils'
import { commonStyle, commonAttr } from '@/custom-component/component-list'
export default {
state: {
areaData: { // 选中区域包含的组件以及区域位移信息
style: {
top: 0,
left: 0,
width: 0,
height: 0,
},
components: [],
},
editor: null,
},
mutations: {
getEditor(state) {
state.editor = $('#editor')
},
setAreaData(state, data) {
state.areaData = data
},
compose({ componentData, areaData, editor }) {
const components = []
areaData.components.forEach(component => {
if (component.component != 'Group') {
components.push(component)
} else {
// 如果要组合的组件中,已经存在组合数据,则需要提前拆分
const parentStyle = { ...component.style }
const subComponents = component.propValue
const editorRect = editor.getBoundingClientRect()
store.commit('deleteComponent')
subComponents.forEach(component => {
decomposeComponent(component, editorRect, parentStyle)
store.commit('addComponent', { component })
})
components.push(...component.propValue)
store.commit('batchDeleteComponent', component.propValue)
}
})
store.commit('addComponent', {
component: {
id: generateID(),
component: 'Group',
...commonAttr,
style: {
...commonStyle,
...areaData.style,
},
propValue: components,
},
})
eventBus.$emit('hideArea')
store.commit('batchDeleteComponent', areaData.components)
store.commit('setCurComponent', {
component: componentData[componentData.length - 1],
index: componentData.length - 1,
})
areaData.components = []
},
// 将已经放到 Group 组件数据删除,也就是在 componentData 中删除,因为它们已经放到 Group 组件中了
batchDeleteComponent({ componentData }, deleteData) {
deleteData.forEach(component => {
for (let i = 0, len = componentData.length; i < len; i++) {
if (component.id == componentData[i].id) {
componentData.splice(i, 1)
break
}
}
})
},
decompose({ curComponent, editor }) {
const parentStyle = { ...curComponent.style }
const components = curComponent.propValue
const editorRect = editor.getBoundingClientRect()
store.commit('deleteComponent')
components.forEach(component => {
decomposeComponent(component, editorRect, parentStyle)
store.commit('addComponent', { component })
})
},
},
}

View File

@ -0,0 +1,18 @@
export default {
state: {
menuTop: 0, // 右击菜单数据
menuLeft: 0,
menuShow: false,
},
mutations: {
showContextMenu(state, { top, left }) {
state.menuShow = true
state.menuTop = top
state.menuLeft = left
},
hideContextMenu(state) {
state.menuShow = false
},
},
}

View File

@ -0,0 +1,67 @@
import store from './index'
import toast from '@/utils/toast'
import generateID from '@/utils/generateID'
import { deepCopy } from '@/utils/utils'
export default {
state: {
copyData: null, // 复制粘贴剪切
isCut: false,
},
mutations: {
copy(state) {
if (!state.curComponent) return
state.copyData = {
data: deepCopy(state.curComponent),
index: state.curComponentIndex,
}
state.isCut = false
},
paste(state, isMouse) {
if (!state.copyData) {
toast('请选择组件')
return
}
const data = state.copyData.data
if (isMouse) {
data.style.top = state.menuTop
data.style.left = state.menuLeft
} else {
data.style.top += 10
data.style.left += 10
}
data.id = generateID()
store.commit('addComponent', { component: deepCopy(data) })
if (state.isCut) {
state.copyData = null
}
},
cut(state) {
if (!state.curComponent) {
toast('请选择组件')
return
}
if (state.copyData) {
const data = deepCopy(state.copyData.data)
const index = state.copyData.index
data.id = generateID()
store.commit('addComponent', { component: data, index })
if (state.curComponentIndex >= index) {
// 如果当前组件索引大于等于插入索引,需要加一,因为当前组件往后移了一位
state.curComponentIndex++
}
}
store.commit('copy')
store.commit('deleteComponent')
state.isCut = true
},
},
}

View File

@ -0,0 +1,11 @@
export default {
mutations: {
addEvent({ curComponent }, { event, param }) {
curComponent.events[event] = param
},
removeEvent({ curComponent }, event) {
delete curComponent.events[event]
},
},
}

View File

@ -9,9 +9,101 @@ import dataset from './modules/dataset'
import chart from './modules/chart'
import request from './modules/request'
import panel from './modules/panel'
import animation from './animation'
import compose from './compose'
import contextmenu from './contextmenu'
import copy from './copy'
import event from './event'
import layer from './layer'
import snapshot from './snapshot'
import lock from './lock'
Vue.use(Vuex)
const store = new Vuex.Store({
const data = {
state: {
...animation.state,
...compose.state,
...contextmenu.state,
...copy.state,
...event.state,
...layer.state,
...snapshot.state,
...lock.state,
editMode: 'edit', // 编辑器模式 edit preview
canvasStyleData: { // 页面全局数据
width: 1200,
height: 740,
scale: 100
},
componentData: [], // 画布组件数据
curComponent: null,
curComponentIndex: null,
// 点击画布时是否点中组件,主要用于取消选中组件用。
// 如果没点中组件,并且在画布空白处弹起鼠标,则取消当前组件的选中状态
isClickComponent: false
},
mutations: {
...animation.mutations,
...compose.mutations,
...contextmenu.mutations,
...copy.mutations,
...event.mutations,
...layer.mutations,
...snapshot.mutations,
...lock.mutations,
setClickComponentStatus(state, status) {
state.isClickComponent = status
},
setEditMode(state, mode) {
state.editMode = mode
},
setCanvasStyle(state, style) {
state.canvasStyleData = style
},
setCurComponent(state, { component, index }) {
state.curComponent = component
state.curComponentIndex = index
},
setShapeStyle({ curComponent }, { top, left, width, height, rotate }) {
if (top) curComponent.style.top = top
if (left) curComponent.style.left = left
if (width) curComponent.style.width = width
if (height) curComponent.style.height = height
if (rotate) curComponent.style.rotate = rotate
},
setShapeSingleStyle({ curComponent }, { key, value }) {
curComponent.style[key] = value
},
setComponentData(state, componentData = []) {
Vue.set(state, 'componentData', componentData)
},
addComponent(state, { component, index }) {
if (index !== undefined) {
state.componentData.splice(index, 0, component)
} else {
state.componentData.push(component)
}
},
deleteComponent(state, index) {
if (index === undefined) {
index = state.curComponentIndex
}
state.componentData.splice(index, 1)
}
},
modules: {
app,
settings,
@ -23,6 +115,6 @@ const store = new Vuex.Store({
panel
},
getters
})
}
export default store
export default new Vuex.Store(data)

View File

@ -0,0 +1,42 @@
import { swap } from '@/utils/utils'
import toast from '@/utils/toast'
export default {
mutations: {
upComponent({ componentData, curComponentIndex }) {
// 上移图层 index表示元素在数组中越往后
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, curComponentIndex + 1)
} else {
toast('已经到顶了')
}
},
downComponent({ componentData, curComponentIndex }) {
// 下移图层 index表示元素在数组中越往前
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, curComponentIndex - 1)
} else {
toast('已经到底了')
}
},
topComponent({ componentData, curComponentIndex }) {
// 置顶
if (curComponentIndex < componentData.length - 1) {
swap(componentData, curComponentIndex, componentData.length - 1)
} else {
toast('已经到顶了')
}
},
bottomComponent({ componentData, curComponentIndex }) {
// 置底
if (curComponentIndex > 0) {
swap(componentData, curComponentIndex, 0)
} else {
toast('已经到底了')
}
},
},
}

View File

@ -0,0 +1,11 @@
export default {
mutations: {
lock({ curComponent }) {
curComponent.isLock = true
},
unlock({ curComponent }) {
curComponent.isLock = false
},
},
}

View File

@ -0,0 +1,33 @@
import store from './index'
import { deepCopy } from '@/utils/utils'
export default {
state: {
snapshotData: [], // 编辑器快照数据
snapshotIndex: -1, // 快照索引
},
mutations: {
undo(state) {
if (state.snapshotIndex >= 0) {
state.snapshotIndex--
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
redo(state) {
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotIndex++
store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
}
},
recordSnapshot(state) {
// 添加新的快照
state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
// 在 undo 过程中,添加新的快照时,要将它后面的快照清理掉
if (state.snapshotIndex < state.snapshotData.length - 1) {
state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)
}
},
},
}

3614
frontend/src/styles/animate.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
body,
button,
input,
p,
li,
ol,
ul,
div,
section,
article,
td,
th,
span,
textarea,
form,
footer,
header,
nav,
main,
address,
aside,
pre,
canvas {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
}
li {
list-style: none;
}
#app {
overflow: hidden;
}
.el-tabs {
height: 100%;
}
.el-tabs__content {
height: calc(100% - 55px);
overflow: auto;
}
.el-tabs__nav-scroll {
padding-left: 20px;
}

View File

@ -0,0 +1,94 @@
export default [
{
label: '进入',
children: [
{ label: '渐显', value: 'fadeIn' },
{ label: '向右进入', value: 'fadeInLeft' },
{ label: '向左进入', value: 'fadeInRight' },
{ label: '向上进入', value: 'fadeInUp' },
{ label: '向下进入', value: 'fadeInDown' },
{ label: '向右长距进入', value: 'fadeInLeftBig' },
{ label: '向左长距进入', value: 'fadeInRightBig' },
{ label: '向上长距进入', value: 'fadeInUpBig' },
{ label: '向下长距进入', value: 'fadeInDownBig' },
{ label: '旋转进入', value: 'rotateIn' },
{ label: '左顺时针旋转', value: 'rotateInDownLeft' },
{ label: '右逆时针旋转', value: 'rotateInDownRight' },
{ label: '左逆时针旋转', value: 'rotateInUpLeft' },
{ label: '右逆时针旋转', value: 'rotateInUpRight' },
{ label: '弹入', value: 'bounceIn' },
{ label: '向右弹入', value: 'bounceInLeft' },
{ label: '向左弹入', value: 'bounceInRight' },
{ label: '向上弹入', value: 'bounceInUp' },
{ label: '向下弹入', value: 'bounceInDown' },
{ label: '光速从右进入', value: 'lightSpeedInRight' },
{ label: '光速从左进入', value: 'lightSpeedInLeft' },
{ label: '光速从右退出', value: 'lightSpeedOutRight' },
{ label: '光速从左退出', value: 'lightSpeedOutLeft' },
{ label: 'Y轴旋转', value: 'flip' },
{ label: '中心X轴旋转', value: 'flipInX' },
{ label: '中心Y轴旋转', value: 'flipInY' },
{ label: '左长半径旋转', value: 'rollIn' },
{ label: '由小变大进入', value: 'zoomIn' },
{ label: '左变大进入', value: 'zoomInLeft' },
{ label: '右变大进入', value: 'zoomInRight' },
{ label: '向上变大进入', value: 'zoomInUp' },
{ label: '向下变大进入', value: 'zoomInDown' },
{ label: '向右滑动展开', value: 'slideInLeft' },
{ label: '向左滑动展开', value: 'slideInRight' },
{ label: '向上滑动展开', value: 'slideInUp' },
{ label: '向下滑动展开', value: 'slideInDown' },
],
},
{
label: '强调',
children: [
{ label: '弹跳', value: 'bounce' },
{ label: '闪烁', value: 'flash' },
{ label: '放大缩小', value: 'pulse' },
{ label: '放大缩小弹簧', value: 'rubberBand' },
{ label: '左右晃动', value: 'headShake' },
{ label: '左右扇形摇摆', value: 'swing' },
{ label: '放大晃动缩小', value: 'tada' },
{ label: '扇形摇摆', value: 'wobble' },
{ label: '左右上下晃动', value: 'jello' },
{ label: 'Y轴旋转', value: 'flip' },
],
},
{
label: '退出',
children: [
{ label: '渐隐', value: 'fadeOut' },
{ label: '向左退出', value: 'fadeOutLeft' },
{ label: '向右退出', value: 'fadeOutRight' },
{ label: '向上退出', value: 'fadeOutUp' },
{ label: '向下退出', value: 'fadeOutDown' },
{ label: '向左长距退出', value: 'fadeOutLeftBig' },
{ label: '向右长距退出', value: 'fadeOutRightBig' },
{ label: '向上长距退出', value: 'fadeOutUpBig' },
{ label: '向下长距退出', value: 'fadeOutDownBig' },
{ label: '旋转退出', value: 'rotateOut' },
{ label: '左顺时针旋转', value: 'rotateOutDownLeft' },
{ label: '右逆时针旋转', value: 'rotateOutDownRight' },
{ label: '左逆时针旋转', value: 'rotateOutUpLeft' },
{ label: '右逆时针旋转', value: 'rotateOutUpRight' },
{ label: '弹出', value: 'bounceOut' },
{ label: '向左弹出', value: 'bounceOutLeft' },
{ label: '向右弹出', value: 'bounceOutRight' },
{ label: '向上弹出', value: 'bounceOutUp' },
{ label: '向下弹出', value: 'bounceOutDown' },
{ label: '中心X轴旋转', value: 'flipOutX' },
{ label: '中心Y轴旋转', value: 'flipOutY' },
{ label: '左长半径旋转', value: 'rollOut' },
{ label: '由小变大退出', value: 'zoomOut' },
{ label: '左变大退出', value: 'zoomOutLeft' },
{ label: '右变大退出', value: 'zoomOutRight' },
{ label: '向上变大退出', value: 'zoomOutUp' },
{ label: '向下变大退出', value: 'zoomOutDown' },
{ label: '向左滑动收起', value: 'slideOutLeft' },
{ label: '向右滑动收起', value: 'slideOutRight' },
{ label: '向上滑动收起', value: 'slideOutUp' },
{ label: '向下滑动收起', value: 'slideOutDown' },
],
},
]

View File

@ -0,0 +1,273 @@
/* eslint-disable no-lonely-if */
import { calculateRotatedPointCoordinate, getCenterPoint } from './translate'
const funcs = {
lt: calculateLeftTop,
t: calculateTop,
rt: calculateRightTop,
r: calculateRight,
rb: calculateRightBottom,
b: calculateBottom,
lb: calculateLeftBottom,
l: calculateLeft,
}
function calculateLeftTop(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint } = pointInfo
let newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
let newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
let newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
let newWidth = newBottomRightPoint.x - newTopLeftPoint.x
let newHeight = newBottomRightPoint.y - newTopLeftPoint.y
if (needLockProportion) {
if (newWidth / newHeight > proportion) {
newTopLeftPoint.x += Math.abs(newWidth - newHeight * proportion)
newWidth = newHeight * proportion
} else {
newTopLeftPoint.y += Math.abs(newHeight - newWidth / proportion)
newHeight = newWidth / proportion
}
// 由于现在求的未旋转前的坐标是以没按比例缩减宽高前的坐标来计算的
// 所以缩减宽高后,需要按照原来的中心点旋转回去,获得缩减宽高并旋转后对应的坐标
// 然后以这个坐标和对称点获得新的中心点,并重新计算未旋转前的坐标
const rotatedTopLeftPoint = calculateRotatedPointCoordinate(newTopLeftPoint, newCenterPoint, style.rotate)
newCenterPoint = getCenterPoint(rotatedTopLeftPoint, symmetricPoint)
newTopLeftPoint = calculateRotatedPointCoordinate(rotatedTopLeftPoint, newCenterPoint, -style.rotate)
newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
newWidth = newBottomRightPoint.x - newTopLeftPoint.x
newHeight = newBottomRightPoint.y - newTopLeftPoint.y
}
if (newWidth > 0 && newHeight > 0) {
style.width = Math.round(newWidth)
style.height = Math.round(newHeight)
style.left = Math.round(newTopLeftPoint.x)
style.top = Math.round(newTopLeftPoint.y)
}
}
function calculateRightTop(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint } = pointInfo
let newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
let newTopRightPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
let newBottomLeftPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
let newWidth = newTopRightPoint.x - newBottomLeftPoint.x
let newHeight = newBottomLeftPoint.y - newTopRightPoint.y
if (needLockProportion) {
if (newWidth / newHeight > proportion) {
newTopRightPoint.x -= Math.abs(newWidth - newHeight * proportion)
newWidth = newHeight * proportion
} else {
newTopRightPoint.y += Math.abs(newHeight - newWidth / proportion)
newHeight = newWidth / proportion
}
const rotatedTopRightPoint = calculateRotatedPointCoordinate(newTopRightPoint, newCenterPoint, style.rotate)
newCenterPoint = getCenterPoint(rotatedTopRightPoint, symmetricPoint)
newTopRightPoint = calculateRotatedPointCoordinate(rotatedTopRightPoint, newCenterPoint, -style.rotate)
newBottomLeftPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
newWidth = newTopRightPoint.x - newBottomLeftPoint.x
newHeight = newBottomLeftPoint.y - newTopRightPoint.y
}
if (newWidth > 0 && newHeight > 0) {
style.width = Math.round(newWidth)
style.height = Math.round(newHeight)
style.left = Math.round(newBottomLeftPoint.x)
style.top = Math.round(newTopRightPoint.y)
}
}
function calculateRightBottom(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint } = pointInfo
let newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
let newTopLeftPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
let newBottomRightPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
let newWidth = newBottomRightPoint.x - newTopLeftPoint.x
let newHeight = newBottomRightPoint.y - newTopLeftPoint.y
if (needLockProportion) {
if (newWidth / newHeight > proportion) {
newBottomRightPoint.x -= Math.abs(newWidth - newHeight * proportion)
newWidth = newHeight * proportion
} else {
newBottomRightPoint.y -= Math.abs(newHeight - newWidth / proportion)
newHeight = newWidth / proportion
}
const rotatedBottomRightPoint = calculateRotatedPointCoordinate(newBottomRightPoint, newCenterPoint, style.rotate)
newCenterPoint = getCenterPoint(rotatedBottomRightPoint, symmetricPoint)
newTopLeftPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
newBottomRightPoint = calculateRotatedPointCoordinate(rotatedBottomRightPoint, newCenterPoint, -style.rotate)
newWidth = newBottomRightPoint.x - newTopLeftPoint.x
newHeight = newBottomRightPoint.y - newTopLeftPoint.y
}
if (newWidth > 0 && newHeight > 0) {
style.width = Math.round(newWidth)
style.height = Math.round(newHeight)
style.left = Math.round(newTopLeftPoint.x)
style.top = Math.round(newTopLeftPoint.y)
}
}
function calculateLeftBottom(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint } = pointInfo
let newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
let newTopRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
let newBottomLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
let newWidth = newTopRightPoint.x - newBottomLeftPoint.x
let newHeight = newBottomLeftPoint.y - newTopRightPoint.y
if (needLockProportion) {
if (newWidth / newHeight > proportion) {
newBottomLeftPoint.x += Math.abs(newWidth - newHeight * proportion)
newWidth = newHeight * proportion
} else {
newBottomLeftPoint.y -= Math.abs(newHeight - newWidth / proportion)
newHeight = newWidth / proportion
}
const rotatedBottomLeftPoint = calculateRotatedPointCoordinate(newBottomLeftPoint, newCenterPoint, style.rotate)
newCenterPoint = getCenterPoint(rotatedBottomLeftPoint, symmetricPoint)
newTopRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
newBottomLeftPoint = calculateRotatedPointCoordinate(rotatedBottomLeftPoint, newCenterPoint, -style.rotate)
newWidth = newTopRightPoint.x - newBottomLeftPoint.x
newHeight = newBottomLeftPoint.y - newTopRightPoint.y
}
if (newWidth > 0 && newHeight > 0) {
style.width = Math.round(newWidth)
style.height = Math.round(newHeight)
style.left = Math.round(newBottomLeftPoint.x)
style.top = Math.round(newTopRightPoint.y)
}
}
function calculateTop(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint, curPoint } = pointInfo
let rotatedcurPositon = calculateRotatedPointCoordinate(curPositon, curPoint, -style.rotate)
let rotatedTopMiddlePoint = calculateRotatedPointCoordinate({
x: curPoint.x,
y: rotatedcurPositon.y,
}, curPoint, style.rotate)
// 勾股定理
let newHeight = Math.sqrt((rotatedTopMiddlePoint.x - symmetricPoint.x) ** 2 + (rotatedTopMiddlePoint.y - symmetricPoint.y) ** 2)
if (newHeight > 0) {
const newCenter = {
x: rotatedTopMiddlePoint.x - (rotatedTopMiddlePoint.x - symmetricPoint.x) / 2,
y: rotatedTopMiddlePoint.y + (symmetricPoint.y - rotatedTopMiddlePoint.y) / 2,
}
let width = style.width
// 因为调整的是高度 所以只需根据锁定的比例调整宽度即可
if (needLockProportion) {
width = newHeight * proportion
}
style.width = width
style.height = Math.round(newHeight)
style.top = Math.round(newCenter.y - (newHeight / 2))
style.left = Math.round(newCenter.x - (style.width / 2))
}
}
function calculateRight(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint, curPoint } = pointInfo
const rotatedcurPositon = calculateRotatedPointCoordinate(curPositon, curPoint, -style.rotate)
const rotatedRightMiddlePoint = calculateRotatedPointCoordinate({
x: rotatedcurPositon.x,
y: curPoint.y,
}, curPoint, style.rotate)
let newWidth = Math.sqrt((rotatedRightMiddlePoint.x - symmetricPoint.x) ** 2 + (rotatedRightMiddlePoint.y - symmetricPoint.y) ** 2)
if (newWidth > 0) {
const newCenter = {
x: rotatedRightMiddlePoint.x - (rotatedRightMiddlePoint.x - symmetricPoint.x) / 2,
y: rotatedRightMiddlePoint.y + (symmetricPoint.y - rotatedRightMiddlePoint.y) / 2,
}
let height = style.height
// 因为调整的是宽度 所以只需根据锁定的比例调整高度即可
if (needLockProportion) {
height = newWidth / proportion
}
style.height = height
style.width = Math.round(newWidth)
style.top = Math.round(newCenter.y - (style.height / 2))
style.left = Math.round(newCenter.x - (newWidth / 2))
}
}
function calculateBottom(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint, curPoint } = pointInfo
const rotatedcurPositon = calculateRotatedPointCoordinate(curPositon, curPoint, -style.rotate)
const rotatedBottomMiddlePoint = calculateRotatedPointCoordinate({
x: curPoint.x,
y: rotatedcurPositon.y,
}, curPoint, style.rotate)
const newHeight = Math.sqrt((rotatedBottomMiddlePoint.x - symmetricPoint.x) ** 2 + (rotatedBottomMiddlePoint.y - symmetricPoint.y) ** 2)
if (newHeight > 0) {
const newCenter = {
x: rotatedBottomMiddlePoint.x - (rotatedBottomMiddlePoint.x - symmetricPoint.x) / 2,
y: rotatedBottomMiddlePoint.y + (symmetricPoint.y - rotatedBottomMiddlePoint.y) / 2,
}
let width = style.width
// 因为调整的是高度 所以只需根据锁定的比例调整宽度即可
if (needLockProportion) {
width = newHeight * proportion
}
style.width = width
style.height = Math.round(newHeight)
style.top = Math.round(newCenter.y - (newHeight / 2))
style.left = Math.round(newCenter.x - (style.width / 2))
}
}
function calculateLeft(style, curPositon, proportion, needLockProportion, pointInfo) {
const { symmetricPoint, curPoint } = pointInfo
const rotatedcurPositon = calculateRotatedPointCoordinate(curPositon, curPoint, -style.rotate)
const rotatedLeftMiddlePoint = calculateRotatedPointCoordinate({
x: rotatedcurPositon.x,
y: curPoint.y,
}, curPoint, style.rotate)
const newWidth = Math.sqrt((rotatedLeftMiddlePoint.x - symmetricPoint.x) ** 2 + (rotatedLeftMiddlePoint.y - symmetricPoint.y) ** 2)
if (newWidth > 0) {
const newCenter = {
x: rotatedLeftMiddlePoint.x - (rotatedLeftMiddlePoint.x - symmetricPoint.x) / 2,
y: rotatedLeftMiddlePoint.y + (symmetricPoint.y - rotatedLeftMiddlePoint.y) / 2,
}
let height = style.height
if (needLockProportion) {
height = newWidth / proportion
}
style.height = height
style.width = Math.round(newWidth)
style.top = Math.round(newCenter.y - (style.height / 2))
style.left = Math.round(newCenter.x - (newWidth / 2))
}
}
export default function calculateComponentPositonAndSize(name, style, curPositon, proportion, needLockProportion, pointInfo) {
funcs[name](style, curPositon, proportion, needLockProportion, pointInfo)
}

View File

@ -0,0 +1,20 @@
import { $ } from './utils'
import { mod360 } from './translate'
// 将组合中的各个子组件拆分出来,并计算它们新的 style
export default function decomposeComponent(component, editorRect, parentStyle) {
const componentRect = $(`#component${component.id}`).getBoundingClientRect()
// 获取元素的中心点坐标
const center = {
x: componentRect.left - editorRect.left + componentRect.width / 2,
y: componentRect.top - editorRect.top + componentRect.height / 2,
}
component.style.rotate = mod360(component.style.rotate + parentStyle.rotate)
component.style.width = parseFloat(component.groupStyle.width) / 100 * parentStyle.width
component.style.height = parseFloat(component.groupStyle.height) / 100 * parentStyle.height
// 计算出元素新的 top left 坐标
component.style.left = center.x - component.style.width / 2
component.style.top = center.y - component.style.height / 2
component.groupStyle = {}
}

View File

@ -0,0 +1,3 @@
import Vue from 'vue'
// 用于监听、触发事件
export default new Vue()

View File

@ -0,0 +1,39 @@
// 编辑器自定义事件
const events = {
redirect(url) {
if (url) {
window.location.href = url
}
},
alert(msg) {
if (msg) {
alert(msg)
}
},
}
const mixins = {
methods: events,
}
const eventList = [
{
key: 'redirect',
label: '跳转事件',
event: events.redirect,
param: '',
},
{
key: 'alert',
label: 'alert 事件',
event: events.alert,
param: '',
},
]
export {
mixins,
events,
eventList,
}

View File

@ -0,0 +1,5 @@
let id = 0
// 主要用于 Vue 的 diff 算法,为每个元素创建一个独一无二的 ID
export default function generateID() {
return id++
}

View File

@ -0,0 +1,18 @@
export default async function runAnimation($el, animations = []) {
const play = (animation) => new Promise(resolve => {
$el.classList.add(animation.value, 'animated')
const removeAnimation = () => {
$el.removeEventListener('animationend', removeAnimation)
$el.removeEventListener('animationcancel', removeAnimation)
$el.classList.remove(animation.value, 'animated')
resolve()
}
$el.addEventListener('animationend', removeAnimation)
$el.addEventListener('animationcancel', removeAnimation)
})
for (let i = 0, len = animations.length; i < len; i++) {
await play(animations[i])
}
}

View File

@ -0,0 +1,143 @@
import store from '@/store'
import eventBus from '@/utils/eventBus'
const ctrlKey = 17,
vKey = 86, // 粘贴
cKey = 67, // 复制
xKey = 88, // 剪切
yKey = 89, // 重做
zKey = 90, // 撤销
gKey = 71, // 组合
bKey = 66, // 拆分
lKey = 76, // 锁定
uKey = 85, // 解锁
sKey = 83, // 保存
pKey = 80, // 预览
dKey = 68, // 删除
deleteKey = 46, // 删除
eKey = 69 // 清空画布
export const keycodes = [66, 67, 68, 69, 71, 76, 80, 83, 85, 86, 88, 89, 90]
// 与组件状态无关的操作
const basemap = {
[vKey]: paste,
[yKey]: redo,
[zKey]: undo,
[sKey]: save,
[pKey]: preview,
[eKey]: clearCanvas,
}
// 组件锁定状态下可以执行的操作
const lockMap = {
...basemap,
[uKey]: unlock,
}
// 组件未锁定状态下可以执行的操作
const unlockMap = {
...basemap,
[cKey]: copy,
[xKey]: cut,
[gKey]: compose,
[bKey]: decompose,
[dKey]: deleteComponent,
[deleteKey]: deleteComponent,
[lKey]: lock,
}
let isCtrlDown = false
// 全局监听按键操作并执行相应命令
export function listenGlobalKeyDown() {
window.onkeydown = (e) => {
const { curComponent } = store.state
if (e.keyCode == ctrlKey) {
isCtrlDown = true
} else if (e.keyCode == deleteKey && curComponent) {
store.commit('deleteComponent')
store.commit('recordSnapshot')
} else if (isCtrlDown) {
if (!curComponent || !curComponent.isLock) {
e.preventDefault()
unlockMap[e.keyCode] && unlockMap[e.keyCode]()
} else if (curComponent && curComponent.isLock) {
e.preventDefault()
lockMap[e.keyCode] && lockMap[e.keyCode]()
}
}
}
window.onkeyup = (e) => {
if (e.keyCode == ctrlKey) {
isCtrlDown = false
}
}
}
function copy() {
store.commit('copy')
}
function paste() {
store.commit('paste')
store.commit('recordSnapshot')
}
function cut() {
store.commit('cut')
}
function redo() {
store.commit('redo')
}
function undo() {
store.commit('undo')
}
function compose() {
if (store.state.areaData.components.length) {
store.commit('compose')
store.commit('recordSnapshot')
}
}
function decompose() {
const curComponent = store.state.curComponent
if (curComponent && !curComponent.isLock && curComponent.component == 'Group') {
store.commit('decompose')
store.commit('recordSnapshot')
}
}
function save() {
eventBus.$emit('save')
}
function preview() {
eventBus.$emit('preview')
}
function deleteComponent() {
if (store.state.curComponent) {
store.commit('deleteComponent')
store.commit('recordSnapshot')
}
}
function clearCanvas() {
eventBus.$emit('clearCanvas')
}
function lock() {
store.commit('lock')
}
function unlock() {
store.commit('unlock')
}

View File

@ -0,0 +1,55 @@
import { sin, cos } from '@/utils/translate'
export function getStyle(style, filter = []) {
const needUnit = [
'fontSize',
'width',
'height',
'top',
'left',
'borderWidth',
'letterSpacing',
'borderRadius',
]
const result = {}
Object.keys(style).forEach(key => {
if (!filter.includes(key)) {
if (key != 'rotate') {
result[key] = style[key]
if (needUnit.includes(key)) {
result[key] += 'px'
}
} else {
result.transform = key + '(' + style[key] + 'deg)'
}
}
})
return result
}
// 获取一个组件旋转 rotate 后的样式
export function getComponentRotatedStyle(style) {
style = { ...style }
if (style.rotate != 0) {
const newWidth = style.width * cos(style.rotate) + style.height * sin(style.rotate)
const diffX = (style.width - newWidth) / 2 // 旋转后范围变小是正值,变大是负值
style.left += diffX
style.right = style.left + newWidth
const newHeight = style.height * cos(style.rotate) + style.width * sin(style.rotate)
const diffY = (newHeight - style.height) / 2 // 始终是正
style.top -= diffY
style.bottom = style.top + newHeight
style.width = newWidth
style.height = newHeight
} else {
style.bottom = style.top + style.height
style.right = style.left + style.width
}
return style
}

View File

@ -0,0 +1,9 @@
import { Message } from 'element-ui'
export default function toast(message = '', type = 'error', duration = 1500) {
Message({
message,
type,
duration,
})
}

View File

@ -0,0 +1,127 @@
import store from '@/store'
// 角度转弧度
// Math.PI = 180 度
function angleToRadian(angle) {
return angle * Math.PI / 180
}
/**
* 计算根据圆心旋转后的点的坐标
* @param {Object} point 旋转前的点坐标
* @param {Object} center 旋转中心
* @param {Number} rotate 旋转的角度
* @return {Object} 旋转后的坐标
* https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式
*/
export function calculateRotatedPointCoordinate(point, center, rotate) {
/**
* 旋转公式
* 点a(x, y)
* 旋转中心c(x, y)
* 旋转后点n(x, y)
* 旋转角度θ tan ??
* nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
* ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
*/
return {
x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,
y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,
}
}
/**
* 获取旋转后的点坐标八个点之一
* @param {Object} style 样式
* @param {Object} center 组件中心点
* @param {String} name 点名称
* @return {Object} 旋转后的点坐标
*/
export function getRotatedPointCoordinate(style, center, name) {
let point // point 是未旋转前的坐标
switch (name) {
case 't':
point = {
x: style.left + (style.width / 2),
y: style.top,
}
break
case 'b':
point = {
x: style.left + (style.width / 2),
y: style.top + style.height,
}
break
case 'l':
point = {
x: style.left,
y: style.top + style.height / 2,
}
break
case 'r':
point = {
x: style.left + style.width,
y: style.top + style.height / 2,
}
break
case 'lt':
point = {
x: style.left,
y: style.top,
}
break
case 'rt':
point = {
x: style.left + style.width,
y: style.top,
}
break
case 'lb':
point = {
x: style.left,
y: style.top + style.height,
}
break
default: // rb
point = {
x: style.left + style.width,
y: style.top+ style.height,
}
break
}
return calculateRotatedPointCoordinate(point, center, style.rotate)
}
// 求两点之间的中点坐标
export function getCenterPoint(p1, p2) {
return {
x: p1.x + ((p2.x - p1.x) / 2),
y: p1.y + ((p2.y - p1.y) / 2),
}
}
export function sin(rotate) {
return Math.abs(Math.sin(angleToRadian(rotate)))
}
export function cos(rotate) {
return Math.abs(Math.cos(angleToRadian(rotate)))
}
export function mod360(deg) {
return (deg + 360) % 360
}
export function changeStyleWithScale(value) {
return value * parseInt(store.state.canvasStyleData.scale) / 100
}

View File

@ -0,0 +1,26 @@
export function deepCopy(target) {
if (typeof target == 'object') {
const result = Array.isArray(target)? [] : {}
for (const key in target) {
if (typeof target[key] == 'object') {
result[key] = deepCopy(target[key])
} else {
result[key] = target[key]
}
}
return result
}
return target
}
export function swap(arr, i, j) {
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
export function $(selector) {
return document.querySelector(selector)
}

View File

@ -1,5 +1,5 @@
<template>
<div class="Echarts" style="display: flex;">
<div style="display: flex;width: 100%;height: 100%;">
<div :id="chartId" style="width: 100%;height: 100%;" />
</div>
</template>
@ -11,6 +11,7 @@ import { baseLineOption, stackLineOption } from '../chart/line/line'
import { basePieOption } from '../chart/pie/pie'
import { baseFunnelOption } from '../chart/funnel/funnel'
import { baseRadarOption } from '../chart/radar/radar'
import eventBus from '@/utils/eventBus'
export default {
name: 'ChartComponent',
@ -41,9 +42,15 @@ export default {
// domecharts
this.myChart = this.$echarts.init(document.getElementById(this.chartId))
this.drawEcharts()
//
eventBus.$on('resizing', (componentId) => {
this.chartResize()
})
},
methods: {
drawEcharts() {
debugger
const chart = this.chart
let chart_option = {}
// type

View File

@ -11,10 +11,13 @@
:data="data"
:props="defaultProps"
:render-content="renderNode"
default-expand-all
:filter-node-method="filterNode"
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag"
@node-drag-start="handleDragStart"
/>
</div>
<div v-if="showdetail" class="detail-class">
<el-card class="filter-card-class">
<div slot="header" class="button-div-class">
@ -82,8 +85,8 @@ export default {
},
renderNode(h, { node, data, store }) {
return (
<div class='custom-tree-node' on-click={() => this.detail(data)} on-dblclick={() => this.addView2Drawing(data.id)} >
<span class='label-span'>{node.label}</span>
<div class='custom-tree-node' on-click={() => this.detail(data)} on-dblclick={() => this.addView2Drawing(data.id)}>
<span class='label-span' >{node.label}</span>
{data.type !== 'group' && data.type !== 'scene' ? (
<svg-icon icon-class={data.type} class='chart-icon' />
@ -102,31 +105,54 @@ export default {
this.detailItem = null
},
addView2Drawing(viewId) {
// viewInfo(viewId).then(res => {
// const info = res.data
// this.$emit('panel-view-add', info)
// })
// viewInfo(viewId).then(res => {
// const info = res.data
// this.$emit('panel-view-add', info)
// })
bus.$emit('panel-view-add', { id: viewId })
// this.$emit('panel-view-add', viewId)
// this.$emit('panel-view-add', viewId)
},
handleDragStart(node, ev) {
ev.dataTransfer.effectAllowed = 'copy'
const dataTrans = {
type: 'view',
id: node.data.id
}
ev.dataTransfer.setData('componentInfo', JSON.stringify(dataTrans))
// bus.$emit('component-on-drag')
},
//
allowDrag(draggingNode) {
if (draggingNode.data.type === 'scene') {
return false
} else {
return true
}
},
allowDrop(draggingNode, dropNode, type) {
return false
}
}
}
</script>
<style lang="scss" scoped>
.top-div-class {
.top-div-class {
max-height: calc(100vh - 335px);
width: 100%;
position: fixed;
overflow-y : auto
}
.detail-class {
}
.detail-class {
width: 100%;
position: fixed;
bottom: 0px;
}
.view-list-thumbnails {
}
.view-list-thumbnails {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,180 @@
<template>
<div class="home">
<Toolbar />
<main>
<!-- 左侧组件列表 -->
<section class="left">
<ComponentList />
</section>
<!-- 中间画布 -->
<section class="center">
<div
class="content"
@drop="handleDrop"
@dragover="handleDragOver"
@mousedown="handleMouseDown"
@mouseup="deselectCurComponent"
>
<Editor />
</div>
</section>
<!-- 右侧属性列表 -->
<section class="right">
<el-tabs v-model="activeName">
<el-tab-pane label="属性" name="attr">
<AttrList v-if="curComponent" />
<p v-else class="placeholder">请选择组件</p>
</el-tab-pane>
<el-tab-pane label="动画" name="animation">
<AnimationList v-if="curComponent" />
<p v-else class="placeholder">请选择组件</p>
</el-tab-pane>
<el-tab-pane label="事件" name="events">
<EventList v-if="curComponent" />
<p v-else class="placeholder">请选择组件</p>
</el-tab-pane>
</el-tabs>
</section>
</main>
</div>
</template>
<script>
import Editor from '@/components/Editor/index'
import ComponentList from '@/components/ComponentList' //
import AttrList from '@/components/AttrList' //
import AnimationList from '@/components/AnimationList' //
import EventList from '@/components/EventList' //
import componentList from '@/custom-component/component-list' //
import Toolbar from '@/components/Toolbar'
import { deepCopy } from '@/utils/utils'
import { mapState } from 'vuex'
import generateID from '@/utils/generateID'
import { listenGlobalKeyDown } from '@/utils/shortcutKey'
export default {
name: 'Canvas',
components: { Editor, ComponentList, AttrList, AnimationList, EventList, Toolbar },
data() {
return {
activeName: 'attr',
reSelectAnimateIndex: undefined
}
},
computed: mapState([
'componentData',
'curComponent',
'isClickComponent',
'canvasStyleData'
]),
created() {
this.restore()
//
listenGlobalKeyDown()
},
methods: {
restore() {
//
if (localStorage.getItem('canvasData')) {
this.$store.commit('setComponentData', this.resetID(JSON.parse(localStorage.getItem('canvasData'))))
}
if (localStorage.getItem('canvasStyle')) {
this.$store.commit('setCanvasStyle', JSON.parse(localStorage.getItem('canvasStyle')))
}
},
resetID(data) {
data.forEach(item => {
item.id = generateID()
})
return data
},
handleDrop(e) {
debugger
e.preventDefault()
e.stopPropagation()
const component = deepCopy(componentList[e.dataTransfer.getData('index')])
component.style.top = e.offsetY
component.style.left = e.offsetX
component.id = generateID()
this.$store.commit('addComponent', { component })
this.$store.commit('recordSnapshot')
},
handleDragOver(e) {
debugger
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleMouseDown() {
debugger
this.$store.commit('setClickComponentStatus', false)
},
deselectCurComponent(e) {
if (!this.isClickComponent) {
this.$store.commit('setCurComponent', { component: null, index: null })
}
// 0 1 2
if (e.button != 2) {
this.$store.commit('hideContextMenu')
}
}
}
}
</script>
<style lang="scss">
.home {
height: 100vh;
background: #fff;
main {
height: calc(100% - 64px);
position: relative;
.left {
position: absolute;
height: 100%;
width: 200px;
left: 0;
top: 0;
padding-top: 10px;
}
.right {
position: absolute;
height: 100%;
width: 262px;
right: 0;
top: 0;
}
.center {
margin-left: 200px;
margin-right: 262px;
background: #f5f5f5;
height: 100%;
overflow: auto;
padding: 20px;
.content {
width: 100%;
height: 100%;
overflow: auto;
}
}
}
.placeholder {
text-align: center;
color: #333;
}
}
</style>

View File

@ -73,19 +73,27 @@
</div>
<div ref="leftPanel" :class="{show:show}" class="leftPanel-container">
<div class="leftPanel-background" />
<div v-if="show" class="leftPanel">
<div />
<div v-show="show" class="leftPanel">
<div class="leftPanel-items">
<view-select v-if="show && showIndex===0" />
<filter-group v-if="show && showIndex===1" />
<view-select v-show=" showIndex===0" />
<filter-group v-show="show && showIndex===1" />
</div>
</div>
</div>
</de-aside-container>
<de-main-container class="ms-main-container">
<drawing-board />
<div
class="content"
@drop="handleDrop"
@dragover="handleDragOver"
@mousedown="handleMouseDown"
@mouseup="deselectCurComponent"
>
<Editor />
</div>
</de-main-container>
</de-container>
</el-container>
@ -98,8 +106,16 @@ import DeAsideContainer from '@/components/dataease/DeAsideContainer'
import { addClass, removeClass } from '@/utils'
import FilterGroup from '../filter'
import ViewSelect from '../ViewSelect'
import DrawingBoard from '../DrawingBoard'
import bus from '@/utils/bus'
import Editor from '@/components/Editor/index'
import { deepCopy } from '@/utils/utils'
import componentList from '@/custom-component/component-list' //
import generateID from '@/utils/generateID'
import { listenGlobalKeyDown } from '@/utils/shortcutKey'
import { mapState } from 'vuex'
import { uuid } from 'vue-uuid'
export default {
components: {
DeMainContainer,
@ -107,16 +123,24 @@ export default {
DeAsideContainer,
FilterGroup,
ViewSelect,
DrawingBoard
Editor
},
data() {
return {
show: false,
clickNotClose: false,
showIndex: -1
showIndex: -1,
activeName: 'attr',
reSelectAnimateIndex: undefined
}
},
computed: mapState([
'componentData',
'curComponent',
'isClickComponent',
'canvasStyleData'
]),
watch: {
show(value) {
if (value && !this.clickNotClose) {
@ -129,8 +153,16 @@ export default {
}
}
},
created() {
this.restore()
//
listenGlobalKeyDown()
},
mounted() {
this.insertToBody()
bus.$on('component-on-drag', () => {
this.show = false
})
},
beforeDestroy() {
const elx = this.$refs.rightPanel
@ -171,6 +203,73 @@ export default {
},
preViewShow() {
bus.$emit('panel-drawing-preview')
},
//
restore() {
//
if (localStorage.getItem('canvasData')) {
this.$store.commit('setComponentData', this.resetID(JSON.parse(localStorage.getItem('canvasData'))))
}
if (localStorage.getItem('canvasStyle')) {
this.$store.commit('setCanvasStyle', JSON.parse(localStorage.getItem('canvasStyle')))
}
},
resetID(data) {
data.forEach(item => {
item.id = uuid.v1()
})
return data
},
handleDrop(e) {
let component
console.log('handleDrop123')
const componentInfo = JSON.parse(e.dataTransfer.getData('componentInfo'))
if (componentInfo.type === 'view') {
componentList.forEach(componentTemp => {
if (componentTemp.type === 'view') {
component = deepCopy(componentTemp)
component.style.top = e.offsetY
component.style.left = e.offsetX
component.id = uuid.v1()
const propValue = {
id: component.id,
viewId: componentInfo.id
}
component.propValue = propValue
}
})
}
this.$store.commit('addComponent', { component })
this.$store.commit('recordSnapshot')
},
handleDragOver(e) {
console.log('handleDragOver123')
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleMouseDown() {
console.log('handleMouseDown123')
this.$store.commit('setClickComponentStatus', false)
},
deselectCurComponent(e) {
console.log('deselectCurComponent123')
if (!this.isClickComponent) {
this.$store.commit('setCurComponent', { component: null, index: null })
}
// 0 1 2
if (e.button != 2) {
this.$store.commit('hideContextMenu')
}
}
}
@ -215,7 +314,7 @@ export default {
.leftPanel {
width: 100%;
max-width: 260px;
max-width: 200px;
height: calc(100vh - 91px);
position: fixed;
top: 91px;

View File

@ -1,393 +0,0 @@
<template xmlns:el-col="http://www.w3.org/1999/html">
<el-row style="height: 100%;overflow-y: hidden;width: 100%;">
<span>仪表盘名称{{ panelName }}</span>
</el-row>
</template>
<script>
import { loadTable, getScene, addGroup, delGroup, addTable, delTable, groupTree } from '@/api/dataset/dataset'
export default {
name: 'PanelView',
data() {
return {
sceneMode: false,
dialogTitle: '',
search: '',
editGroup: false,
editTable: false,
tData: [],
tableData: [],
currGroup: {},
expandedArray: [],
groupForm: {
name: '',
pid: null,
level: 0,
type: '',
children: [],
sort: 'type desc,name asc'
},
tableForm: {
name: '',
mode: '',
sort: 'type asc,create_time desc,name asc'
},
groupFormRules: {
name: [
{ required: true, message: this.$t('commons.input_content'), trigger: 'blur' }
]
},
tableFormRules: {
name: [
{ required: true, message: this.$t('commons.input_content'), trigger: 'blur' }
],
mode: [
{ required: true, message: this.$t('commons.input_content'), trigger: 'blur' }
]
}
}
},
computed: {
panelName: function() {
console.log(this.$store.state.panel.panelName)
return this.$store.state.panel.panelName
}
},
watch: {
// search(val){
// this.groupForm.name = val;
// this.tree(this.groupForm);
// }
},
mounted() {
this.tree(this.groupForm)
this.refresh()
this.tableTree()
// this.$router.push('/dataset');
},
methods: {
clickAdd(param) {
// console.log(param);
this.add(param.type)
this.groupForm.pid = param.data.id
this.groupForm.level = param.data.level + 1
},
beforeClickAdd(type, data, node) {
return {
'type': type,
'data': data,
'node': node
}
},
clickMore(param) {
console.log(param)
switch (param.type) {
case 'rename':
this.add(param.data.type)
this.groupForm = JSON.parse(JSON.stringify(param.data))
break
case 'move':
break
case 'delete':
this.delete(param.data)
break
case 'editTable':
this.editTable = true
this.tableForm = JSON.parse(JSON.stringify(param.data))
this.tableForm.mode = this.tableForm.mode + ''
break
case 'deleteTable':
this.deleteTable(param.data)
break
}
},
beforeClickMore(type, data, node) {
return {
'type': type,
'data': data,
'node': node
}
},
add(type) {
switch (type) {
case 'group':
this.dialogTitle = this.$t('dataset.group')
break
case 'scene':
this.dialogTitle = this.$t('dataset.scene')
break
}
this.groupForm.type = type
this.editGroup = true
},
saveGroup(group) {
// console.log(group);
this.$refs['groupForm'].validate((valid) => {
if (valid) {
addGroup(group).then(res => {
this.close()
this.$message({
message: this.$t('commons.save_success'),
type: 'success',
showClose: true
})
this.tree(this.groupForm)
})
} else {
this.$message({
message: this.$t('commons.input_content'),
type: 'error',
showClose: true
})
return false
}
})
},
saveTable(table) {
// console.log(table)
table.mode = parseInt(table.mode)
this.$refs['tableForm'].validate((valid) => {
if (valid) {
addTable(table).then(response => {
this.closeTable()
this.$message({
message: this.$t('commons.save_success'),
type: 'success',
showClose: true
})
this.tableTree()
// this.$router.push('/dataset/home')
this.$emit('switchComponent', { name: '' })
this.$store.dispatch('dataset/setTable', null)
})
} else {
this.$message({
message: this.$t('commons.input_content'),
type: 'error',
showClose: true
})
return false
}
})
},
delete(data) {
this.$confirm(this.$t('dataset.confirm_delete'), this.$t('dataset.tips'), {
confirmButtonText: this.$t('dataset.confirm'),
cancelButtonText: this.$t('dataset.cancel'),
type: 'warning'
}).then(() => {
delGroup(data.id).then(response => {
this.$message({
type: 'success',
message: this.$t('dataset.delete_success'),
showClose: true
})
this.tree(this.groupForm)
})
}).catch(() => {
})
},
deleteTable(data) {
this.$confirm(this.$t('dataset.confirm_delete'), this.$t('dataset.tips'), {
confirmButtonText: this.$t('dataset.confirm'),
cancelButtonText: this.$t('dataset.cancel'),
type: 'warning'
}).then(() => {
delTable(data.id).then(response => {
this.$message({
type: 'success',
message: this.$t('dataset.delete_success'),
showClose: true
})
this.tableTree()
// this.$router.push('/dataset/home')
this.$emit('switchComponent', { name: '' })
this.$store.dispatch('dataset/setTable', null)
})
}).catch(() => {
})
},
close() {
this.editGroup = false
this.groupForm = {
name: '',
pid: null,
level: 0,
type: '',
children: [],
sort: 'type desc,name asc'
}
},
closeTable() {
this.editTable = false
this.tableForm = {
name: ''
}
},
tree(group) {
groupTree(group).then(res => {
this.tData = res.data
})
},
tableTree() {
this.tableData = []
if (this.currGroup.id) {
loadTable({
sort: 'type asc,create_time desc,name asc',
sceneId: this.currGroup.id
}).then(res => {
this.tableData = res.data
})
}
},
nodeClick(data, node) {
// console.log(data);
// console.log(node);
if (data.type === 'scene') {
this.sceneMode = true
this.currGroup = data
this.$store.dispatch('dataset/setSceneData', this.currGroup.id)
}
if (node.expanded) {
this.expandedArray.push(data.id)
} else {
const index = this.expandedArray.indexOf(data.id)
if (index > -1) {
this.expandedArray.splice(index, 1)
}
}
// console.log(this.expandedArray);
},
back() {
this.sceneMode = false
// const route = this.$store.state.permission.currentRoutes
// console.log(route)
// this.$router.push('/dataset/index')
this.$store.dispatch('dataset/setSceneData', null)
this.$emit('switchComponent', { name: '' })
},
clickAddData(param) {
// console.log(param);
switch (param.type) {
case 'db':
this.addDB()
break
case 'sql':
this.$message(param.type)
break
case 'excel':
this.$message(param.type)
break
case 'custom':
this.$message(param.type)
break
}
},
beforeClickAddData(type) {
return {
'type': type
}
},
addDB() {
// this.$router.push({
// name: 'add_db',
// params: {
// scene: this.currGroup
// }
// })
this.$emit('switchComponent', { name: 'AddDB', param: this.currGroup })
},
sceneClick(data, node) {
// console.log(data);
this.$store.dispatch('dataset/setTable', null)
this.$store.dispatch('dataset/setTable', data.id)
// this.$router.push({
// name: 'table',
// params: {
// table: data
// }
// })
this.$emit('switchComponent', { name: 'ViewTable' })
},
refresh() {
const path = this.$route.path
if (path === '/dataset/table') {
this.sceneMode = true
const sceneId = this.$store.state.dataset.sceneData
getScene(sceneId).then(res => {
this.currGroup = res.data
})
}
},
panelDefaultClick(data, node) {
// console.log(data);
// console.log(node);
}
}
}
</script>
<style scoped>
.header-title {
font-size: 14px;
flex: 1;
color: #606266;
font-weight: bold;
}
.el-divider--horizontal {
margin: 12px 0
}
.search-input {
padding: 12px 0;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.custom-position {
flex: 1;
display: flex;
align-items: center;
font-size: 14px;
flex-flow: row nowrap;
}
.form-item {
margin-bottom: 0;
}
.title-css {
height: 26px;
}
.title-text {
line-height: 26px;
}
</style>