forked from github/dataease
feat:更换画布
This commit is contained in:
parent
470f8cddae
commit
5db39b5855
@ -18,4 +18,10 @@ public interface ViewApi {
|
||||
@ApiOperation("视图树")
|
||||
@PostMapping("/tree")
|
||||
List<PanelViewDto> tree(BaseGridRequest request);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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",
|
||||
|
539
frontend/src/assets/iconfont/demo.css
Normal file
539
frontend/src/assets/iconfont/demo.css
Normal 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;
|
||||
}
|
307
frontend/src/assets/iconfont/demo_index.html
Normal file
307
frontend/src/assets/iconfont/demo_index.html
Normal 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"></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"></span>
|
||||
<div class="name">向右旋转</div>
|
||||
<div class="code-name">&#xe66a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">图片</div>
|
||||
<div class="code-name">&#xe616;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">锁</div>
|
||||
<div class="code-name">&#xe672;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">矩形</div>
|
||||
<div class="code-name">&#xe790;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">文本</div>
|
||||
<div class="code-name">&#xe652;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">按钮</div>
|
||||
<div class="code-name">&#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"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</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"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</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"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</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>
|
40
frontend/src/assets/iconfont/iconfont.css
Normal file
40
frontend/src/assets/iconfont/iconfont.css
Normal 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";
|
||||
}
|
||||
|
BIN
frontend/src/assets/iconfont/iconfont.eot
Normal file
BIN
frontend/src/assets/iconfont/iconfont.eot
Normal file
Binary file not shown.
1
frontend/src/assets/iconfont/iconfont.js
Normal file
1
frontend/src/assets/iconfont/iconfont.js
Normal file
File diff suppressed because one or more lines are too long
51
frontend/src/assets/iconfont/iconfont.json
Normal file
51
frontend/src/assets/iconfont/iconfont.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
44
frontend/src/assets/iconfont/iconfont.svg
Normal file
44
frontend/src/assets/iconfont/iconfont.svg
Normal 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="" 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="" 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="" 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="" 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="" 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="" 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 |
BIN
frontend/src/assets/iconfont/iconfont.ttf
Normal file
BIN
frontend/src/assets/iconfont/iconfont.ttf
Normal file
Binary file not shown.
BIN
frontend/src/assets/iconfont/iconfont.woff
Normal file
BIN
frontend/src/assets/iconfont/iconfont.woff
Normal file
Binary file not shown.
BIN
frontend/src/assets/iconfont/iconfont.woff2
Normal file
BIN
frontend/src/assets/iconfont/iconfont.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/title.jpg
Normal file
BIN
frontend/src/assets/title.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
118
frontend/src/components/AnimationList.vue
Normal file
118
frontend/src/components/AnimationList.vue
Normal 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>
|
81
frontend/src/components/AttrList.vue
Normal file
81
frontend/src/components/AttrList.vue
Normal 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>
|
67
frontend/src/components/ComponentList.vue
Normal file
67
frontend/src/components/ComponentList.vue
Normal 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>
|
31
frontend/src/components/Editor/Area.vue
Normal file
31
frontend/src/components/Editor/Area.vue
Normal 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>
|
46
frontend/src/components/Editor/ComponentWrapper.vue
Normal file
46
frontend/src/components/Editor/ComponentWrapper.vue
Normal 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>
|
126
frontend/src/components/Editor/ContextMenu.vue
Normal file
126
frontend/src/components/Editor/ContextMenu.vue
Normal 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>
|
32
frontend/src/components/Editor/Grid.vue
Normal file
32
frontend/src/components/Editor/Grid.vue
Normal 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>
|
249
frontend/src/components/Editor/MarkLine.vue
Normal file
249
frontend/src/components/Editor/MarkLine.vue
Normal 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>
|
88
frontend/src/components/Editor/Preview.vue
Normal file
88
frontend/src/components/Editor/Preview.vue
Normal 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>
|
388
frontend/src/components/Editor/Shape.vue
Normal file
388
frontend/src/components/Editor/Shape.vue
Normal 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>
|
307
frontend/src/components/Editor/index.vue
Normal file
307
frontend/src/components/Editor/index.vue
Normal 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>
|
80
frontend/src/components/EventList.vue
Normal file
80
frontend/src/components/EventList.vue
Normal 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>
|
49
frontend/src/components/Modal.vue
Normal file
49
frontend/src/components/Modal.vue
Normal 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>
|
254
frontend/src/components/Toolbar.vue
Normal file
254
frontend/src/components/Toolbar.vue
Normal 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>
|
67
frontend/src/custom-component/Group.vue
Normal file
67
frontend/src/custom-component/Group.vue
Normal 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>
|
23
frontend/src/custom-component/Picture.vue
Normal file
23
frontend/src/custom-component/Picture.vue
Normal 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>
|
23
frontend/src/custom-component/RectShape.vue
Normal file
23
frontend/src/custom-component/RectShape.vue
Normal 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>
|
57
frontend/src/custom-component/UserView.vue
Normal file
57
frontend/src/custom-component/UserView.vue
Normal 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>
|
47
frontend/src/custom-component/VButton.vue
Normal file
47
frontend/src/custom-component/VButton.vue
Normal 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>
|
118
frontend/src/custom-component/VText.vue
Normal file
118
frontend/src/custom-component/VText.vue
Normal 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 || ' '
|
||||
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>
|
104
frontend/src/custom-component/component-list.js
Normal file
104
frontend/src/custom-component/component-list.js
Normal 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: ' ',
|
||||
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
|
16
frontend/src/custom-component/index.js
Normal file
16
frontend/src/custom-component/index.js
Normal 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)
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
11
frontend/src/store/animation.js
Normal file
11
frontend/src/store/animation.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
mutations: {
|
||||
addAnimation({ curComponent }, animation) {
|
||||
curComponent.animations.push(animation)
|
||||
},
|
||||
|
||||
removeAnimation({ curComponent }, index) {
|
||||
curComponent.animations.splice(index, 1)
|
||||
},
|
||||
},
|
||||
}
|
100
frontend/src/store/compose.js
Normal file
100
frontend/src/store/compose.js
Normal 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 })
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
18
frontend/src/store/contextmenu.js
Normal file
18
frontend/src/store/contextmenu.js
Normal 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
|
||||
},
|
||||
},
|
||||
}
|
67
frontend/src/store/copy.js
Normal file
67
frontend/src/store/copy.js
Normal 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
|
||||
},
|
||||
},
|
||||
}
|
11
frontend/src/store/event.js
Normal file
11
frontend/src/store/event.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
mutations: {
|
||||
addEvent({ curComponent }, { event, param }) {
|
||||
curComponent.events[event] = param
|
||||
},
|
||||
|
||||
removeEvent({ curComponent }, event) {
|
||||
delete curComponent.events[event]
|
||||
},
|
||||
},
|
||||
}
|
@ -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)
|
||||
|
42
frontend/src/store/layer.js
Normal file
42
frontend/src/store/layer.js
Normal 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('已经到底了')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
11
frontend/src/store/lock.js
Normal file
11
frontend/src/store/lock.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default {
|
||||
mutations: {
|
||||
lock({ curComponent }) {
|
||||
curComponent.isLock = true
|
||||
},
|
||||
|
||||
unlock({ curComponent }) {
|
||||
curComponent.isLock = false
|
||||
},
|
||||
},
|
||||
}
|
33
frontend/src/store/snapshot.js
Normal file
33
frontend/src/store/snapshot.js
Normal 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
3614
frontend/src/styles/animate.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
46
frontend/src/styles/reset.css
Normal file
46
frontend/src/styles/reset.css
Normal 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;
|
||||
}
|
94
frontend/src/utils/animationClassData.js
Normal file
94
frontend/src/utils/animationClassData.js
Normal 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' },
|
||||
],
|
||||
},
|
||||
]
|
273
frontend/src/utils/calculateComponentPositonAndSize.js
Normal file
273
frontend/src/utils/calculateComponentPositonAndSize.js
Normal 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)
|
||||
}
|
20
frontend/src/utils/decomposeComponent.js
Normal file
20
frontend/src/utils/decomposeComponent.js
Normal 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 = {}
|
||||
}
|
3
frontend/src/utils/eventBus.js
Normal file
3
frontend/src/utils/eventBus.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Vue from 'vue'
|
||||
// 用于监听、触发事件
|
||||
export default new Vue()
|
39
frontend/src/utils/events.js
Normal file
39
frontend/src/utils/events.js
Normal 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,
|
||||
}
|
5
frontend/src/utils/generateID.js
Normal file
5
frontend/src/utils/generateID.js
Normal file
@ -0,0 +1,5 @@
|
||||
let id = 0
|
||||
// 主要用于 Vue 的 diff 算法,为每个元素创建一个独一无二的 ID
|
||||
export default function generateID() {
|
||||
return id++
|
||||
}
|
18
frontend/src/utils/runAnimation.js
Normal file
18
frontend/src/utils/runAnimation.js
Normal 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])
|
||||
}
|
||||
}
|
143
frontend/src/utils/shortcutKey.js
Normal file
143
frontend/src/utils/shortcutKey.js
Normal 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')
|
||||
}
|
55
frontend/src/utils/style.js
Normal file
55
frontend/src/utils/style.js
Normal 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
|
||||
}
|
9
frontend/src/utils/toast.js
Normal file
9
frontend/src/utils/toast.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
export default function toast(message = '', type = 'error', duration = 1500) {
|
||||
Message({
|
||||
message,
|
||||
type,
|
||||
duration,
|
||||
})
|
||||
}
|
127
frontend/src/utils/translate.js
Normal file
127
frontend/src/utils/translate.js
Normal 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
|
||||
}
|
26
frontend/src/utils/utils.js
Normal file
26
frontend/src/utils/utils.js
Normal 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)
|
||||
}
|
@ -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 {
|
||||
// 基于准备好的dom,初始化echarts实例
|
||||
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
|
||||
|
@ -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>
|
||||
|
180
frontend/src/views/panel/canvas/index.vue
Normal file
180
frontend/src/views/panel/canvas/index.vue
Normal 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>
|
@ -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;
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user