diff --git a/hchyun-ui/package.json b/hchyun-ui/package.json index 356f7da..09861f1 100644 --- a/hchyun-ui/package.json +++ b/hchyun-ui/package.json @@ -36,6 +36,7 @@ "management-system" ], "dependencies": { + "@antv/g6": "^4.2.7", "@riophae/vue-treeselect": "0.4.0", "axios": "0.18.1", "clipboard": "2.0.4", diff --git a/hchyun-ui/src/assets/styles/hcy.scss b/hchyun-ui/src/assets/styles/hcy.scss index ef22569..9b75fd1 100644 --- a/hchyun-ui/src/assets/styles/hcy.scss +++ b/hchyun-ui/src/assets/styles/hcy.scss @@ -1,6 +1,6 @@ /** * 通用css样式布局处理 - * Copyright (c) 2019 hchyun + * Copyright (c) 2021 hchyun */ /** 基础通用 **/ diff --git a/hchyun-ui/src/directive/permission/hasPermi.js b/hchyun-ui/src/directive/permission/hasPermi.js index 0e75612..e7a7a56 100644 --- a/hchyun-ui/src/directive/permission/hasPermi.js +++ b/hchyun-ui/src/directive/permission/hasPermi.js @@ -1,6 +1,6 @@ /** * 操作权限处理 - * Copyright (c) 2019 hchyun + * Copyright (c) 2021 hchyun */ import store from '@/store' diff --git a/hchyun-ui/src/directive/permission/hasRole.js b/hchyun-ui/src/directive/permission/hasRole.js index 00f730c..62f5eb8 100644 --- a/hchyun-ui/src/directive/permission/hasRole.js +++ b/hchyun-ui/src/directive/permission/hasRole.js @@ -1,6 +1,6 @@ /** * 角色权限处理 - * Copyright (c) 2019 hchyun + * Copyright (c) 2021 hchyun */ import store from '@/store' diff --git a/hchyun-ui/src/router/index.js b/hchyun-ui/src/router/index.js index 86fac08..4211155 100644 --- a/hchyun-ui/src/router/index.js +++ b/hchyun-ui/src/router/index.js @@ -92,6 +92,19 @@ export const constantRoutes = [ } ] }, + { + path: '/top', + component: Layout, + hidden: true, + children: [ + { + path: '/', + component: (resolve) => require(['@/views/system/top/demo-topology'], resolve), + name:'Top', + meta: {title: 'top视图'} + } + ] + }, { path: '/apiclass', component: Layout, diff --git a/hchyun-ui/src/utils/hcy.js b/hchyun-ui/src/utils/hcy.js index e587de4..b830040 100644 --- a/hchyun-ui/src/utils/hcy.js +++ b/hchyun-ui/src/utils/hcy.js @@ -1,6 +1,6 @@ /** * 通用js方法封装处理 - * Copyright (c) 2019 hchyun + * Copyright (c) 2021 hchyun */ const baseURL = process.env.VUE_APP_BASE_API diff --git a/hchyun-ui/src/views/system/top/demo-topology.vue b/hchyun-ui/src/views/system/top/demo-topology.vue new file mode 100644 index 0000000..6aacb23 --- /dev/null +++ b/hchyun-ui/src/views/system/top/demo-topology.vue @@ -0,0 +1,563 @@ + + + + + + + diff --git a/hchyun-ui/src/views/system/top/index.js b/hchyun-ui/src/views/system/top/index.js new file mode 100644 index 0000000..be5e346 --- /dev/null +++ b/hchyun-ui/src/views/system/top/index.js @@ -0,0 +1,47 @@ +/** + * Created by clay on 2021/10/14 + * Description: common utils + */ + +/** + * This is just a simple version of deep copy + * Has a lot of edge cases bug + * If you want to use a perfect deep copy, use lodash's _.cloneDeep + * @param {Object} source + * @returns {Object} targetObj + */ +export function deepClone(source) { + if (!source && typeof source !== 'object') { + throw new Error('error arguments: deepClone') + } + let targetObj = source.constructor === Array ? [] : {} + Object.keys(source).forEach(key => { + if (source[key] && typeof source[key] === 'object') { + targetObj[key] = deepClone(source[key]) + } else { + targetObj[key] = source[key] + } + }) + return targetObj +} + +/** + * Randomly extract one or more elements from an array + * If you want to use a perfect solution, use lodash's _.sample or _.sampleSize + * @param {Array} arr + * @param {number} count + * @returns {Array} arr + */ +export function getRandomArrayElements(arr, count = 1) { + if (count > arr.length) { + throw new Error('error arguments: count is greater than length of array') + } + let shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index + while (i-- > min) { + index = Math.floor((i + 1) * Math.random()) + temp = shuffled[index] + shuffled[index] = shuffled[i] + shuffled[i] = temp + } + return shuffled.slice(min) +} diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.css b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.css new file mode 100644 index 0000000..142743d --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.css @@ -0,0 +1,109 @@ +@font-face {font-family: "iconfont"; + src: url('iconfont.eot?t=1568972645985'); /* IE9 */ + src: url('iconfont.eot?t=1568972645985#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAA1MAAsAAAAAGkgAAAz/AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCGNAqiNJtOATYCJANgCzIABCAFhG0Hgggb6BVRlHJWSbKvBuyGeKPMp/UYzGar17rEgTXbq2lrenLOOiJE8OITJy7o6fe+23NLTyvVJoAwKGwrQsbFosAJPEZ3+Ytm/BseN+3fS0glCVCTCasY3VadGPyVrtxu0utIModSm3RSUfZHsjncObIyr8P0tHWYOJ0iZ73RM+2Zsj9df+4zw/JNJ9NJdwqhRklfCGc8j+3e58ZmgnVRSEKWoklKoPcrmZk0P0/vAQIBvH8/VamVDeUAJF3vd7MA0gtNCSshGl4/et+X5y2na8nOGlLHnKxoLUA8VuZibvEGsDHrBry0BQQUMFyDQ83xaxAyVtWrb5F0j+khUKzKBLeXr99Kh8NzCehMY1nptPycxS7JGwzDFukbmNR8MRW+5qXXb00Cw7fwgs+PZ125e8B32V4da2Ma5KA/HXgZDiw4BnAwk3GbBmxJj3OtmP7ceP4B7BPhSU/MEskVU6nwQu9EH7xRxVXS/eHM0Zd9YXi589MDTvXcO37+eLIvQ1Rn/RceIJoTYFwonswTIykqqxkxasyMKBHChAPqQO8G0MJa5oAekKsR0YOqCaYACYBEIOMAOZAJQDEQB1AByYAKIB7wAhABTmA1DPpgNRL6UUgBxAGpgBIgFnAPSAM4gYwAxoCMAu5jNcYwjGwkh2ggs4AyINOAC0AmAZexF1MAMJ8Ocw7woOJvQKg4+840uwPrToBSd7dEvK4t7vqaNfFFG7BPmh1xcj7mKetK4OMad2POjc62lYVVQ4stLy8tV6JEg9uc643L1cHTEs84tW63xsev5q0SacLNnHdans+TeT+TrS6PO/hylgoKhZfDejUj72RllMIEPrGHm0p736c9zytc+yj3BEMZ9dHRZi5qp4djvHrKzSpcOprmAxJqQnihShgQFtgigBsqGECffqJ/xD7UPdYg7DAu0I3GYcftqU4L4glRlYKRUFU0s8SmS1tESc5xGkAAAm7ElR8XPFOMjRcQBNI2kszvscejltEUsnOY4iPkLjIwa8BJnQliXA9BFAKCriRUMfJCIKnrBDzWSPBb6Gte6PHQLpfC7aa83tWd1s9xOzW718uYXK4NbC+OAyXobBDiAneOIKCgtwJQ79PgPByT4zv5KF/Y92Q6ZrrO4NDiScE7XTEARi9rJUIn+WZtahwSYBfad216u2fZdZfo5JVk5C3HlLPuwlvexFNXN+sHLrDn+5O6Rzn7JqTSj7DDGr0xEjH0TsV6Lprpps/7Sb+SM2alSWeIQIxWgq9fzM7u+jwf/31KUweVzqfWmaJwzqTW9QJQdH1JTujN0YTZDCFna8A9pMQNBgAerfpzEBLd7FVtijSZcDfnUJ0zbeiwo6d6u6fOdj/NmM1ROVV74VQPMRoJSgFaGsyo6jHvpNCToRTqUjXzLgmK97XbNS8D8TofjPO1BlfB3s4aI5/kEfqaTZWlCjXlMX0LoaAJyHmWut1K4mB9uFwXBglAnG2NW33VZWW7AQ5wKKiiTrIZ8CHN148AsMnayEdByNnX6oaGtt3FN8dkMQiBcgQfEDx2Hd8CIQ42V07YBTvlUUD9dA0sMPRGIKw1Wag/F4+abNVguU3AEAUJY7Q5GkKCyjOZInQRc8IwGiP1XUoSydvSOnFdiskC5xq7oueAvDDkpSAltu7iQO54T09kzwClyzCd4k5Hn8vXpxvb2BMRhk5a5yL0XgbgNJF9mmEdQ6EkpVgLgY8qaLWA8Glmw0AkwlCat1xTInCMPOWZs+JtZ9Lc8OZGEzz+SXf+JtB1GWzsvshd2ky9DcULNNDzOY07IyTWyxh4ywXONieV2OFZjrfqbKdOn+t72wqOnW/QXdxUB2bPbz/9ZC4fWmwpuO2+zpm25ewZw2uzJhPKLuvul4w7V0Egx3qTyG7bIYI7F22zMv236iMpowbruQS3/s66/hiBZRCf313YC8m8zvNzubN+KF7YNkDM687cxc16I2vYlnzmZqU0vJuLtgYBJtwEWrtNKRu8vBJLZADg0cjwAblWZ3U1I8xjKEjIpzqH2RG9nRtV+D+aAzI9Vqpz8jLdOfPOcc68UStaWSVTUUUdEkeGPQwLbTW3IAU/FxQ59ziXgoUqwcSHG2vx2KrttdXihK+uFY9rW0U4v5b5eNwaZ5Udjbc8+ZPe8APhNzA/eJiygtlM/GF/VljBPMQBWKD1qymweYiDZe2SAEm/Vmp/GkSVwAzmPro+Ep8bPyTN+BCee2ZkiX2EJ6LG70gld/45dGjePGz2ZTpzMLDt4KHBxFyQ+SSu8MiRwjHZ96p2rNDmP2Kr95dZSMphcVBkHlk6xcKswO6nSX+7LRciJ4WCiV+GHG8PPiPRsA2tz/zlkDocMwRC8qClYBJY9DL/x5/2ZbT+D+1419uu7KjXy7d73x1HAWgRB/z0470ZbTPSr+9FensQYt/b2xtFenprc09PRkpYUxhNoynpcLY8FXY46UgYxR9Jak5LhzWD7Fp5tHylnJXjXiG3qHGrY5bDiquR9p19ny+eWZA46ZykSWYu/rzYKXZGVUY9b1YDZ5xICHKGjgg2oGhhYVYWuwfK2/Zfc7CSFeGIKHkGFD1sWtQ09SemsL9wsGRfMj4WXMkP5Vfi4xGVe/cuWfHm2q2Ag2P/X7At+ULQB31oM7hbKVtbWVEJ0qZWRoyYDmiVDiGYBoRnzmSL4MC4gkB8vfJT4eBBebGYIxzJkjSoYLhfYLG+XB7Z1mbowXyY1nZlF8vlGCZZcT4s8GfKrXgFWVshU+bJB4LLGtm8L1s3H5uQST6TabGgO1htbPNa8nYPKbGJghwySZvy3L3cgAnZweY1xJ/vSVYU/iR5tnqFMwebWHOwpYT89vPn31RhtPLwihSFeA8oIlXgPYkyxC3BX+GIV8/HXskLtlzwe1h1bEv7LtxZvP3pF2vXYT/KMknZqrwDRY2FmeI/35u/oq4krNTskLd7ger5N8RXUPHl4j3lK1M36YHELuVrwUrP6teP+NwmkbBSNkGtBB67sgKpXwKmJkyVSmd+9nbDOMH89rFWbeM3062+VqJ0fmmI8M1vJwxpkoPCkO3zK4Ua38Hvf+nuOTHLr8UZ0eTcozrVzWuTqcLIEEY0GtKAREqYFdTBWP+879ekSFVDFkRentU7c4H47/ZYAS8KzbmaS1BfHJn7oiCK1oqmKTueELpFq38KZHOroqOq1B3qn5v7Po8mD+xq/SlHsfNcmuDigdR953ZVpOwcfSn1wOg3/4admg5ak6fBVve00aQiLTt9dNbS3dXA/nSor0wtFjHKN+2otm+orCxTpGTsb2pRItdHRPHA3JAzD4ty/gIDvti/5Lm+3auWNRYtojdt35iTCwbGo/+W509eea6YJOUoV8DTSHbyeDIbZIXc9pVfAMyfPGbl3eUL7vGsf73++zVDazv6rhdtt13KK5odWLiqqZ5e5O7O3lgAsz4WhV11FB8L4IvDAidvfhQ7me0XE50EEqHw+8mQl1Fb1lnyVfmr5NksG3qm1e2h5IcsWxqi+79/aKM+I6dqCnmofkefLCcdDjJifP/+cTAfrNhS1BEjaPLFLKjfUr8gxtckiOko0jHz3ratio2zykRVKPiHp8jbPS/z0Gxvs3Pp247AI4GXpTN/1SWHi6XFc8sMdwVP1z3HH1zxNS0RoWmbJ9ng2YNXecA5XeWz2XoDwS+ZgX/7mxlGNINJqR/suoLv1N1gRyW5+IYr6BzxliF0qfg5CxonajlQqWjjQWmR9QXKft6jadDZAIADSAsxLItK/XxbfGTQpNJtHRvw0cwaG7fzGT5U5f+b6QjKsmZNQaBsLIUyR0wneoFtRRY/bMkE/v9em6kULvo1iLgrs/OnzSGACqr6KRvGSAzeqB4pD1KwDCy2P9zfLh6YL58nUNJxBs/U9Fi6v0OPYFKjLZnXKc4woIhL4CHKpkkf9YnNODseKbGZmhjRCrGSbc5VHhOvuiZBckeKI0UjV1Nq1alk4NBBQmTcJWKM+kGscWMSJFSOE2/WdyQYjyXFi8zHWe3mc++RQDJoEe1EqtCZJMS9ou83xMFKarpy4R/IpzvFyeHxYNsrOCAfA/h1fMpshCEsxAuZGViLoiTMQPFhwlzeHh0Z04CHCotKzzMESJyuqAlqu7pbKcgxd2Y9bfG/gVhgSWQ54lfdf4B46csL2WbJcuBeNed0xL5Y66zFTtVT3RByf4IKwjKqAyviSNhtTpcBCju0ZQyjdGs2PVOlq3YYnxXbhMbad1Re/PkYWTl5BUUlZRVVNY1oVGMa14QmNaVpzWhWc5rXAia948ZZlLqzRywOMHAzLWQMndU2NdAp96TiIK1P99BQWO7aLglS11QWJHUNCmwObOqgQaCxEZyy1E3KHcaDpas7bhdJI6nyHmF64MH+TrCReXQ9E6z1igBcKyLMYWLmQGtoltJzuldMGMp2cPmWkSSKsFUTlj31VSo=') format('woff2'), + url('iconfont.woff?t=1568972645985') format('woff'), + url('iconfont.ttf?t=1568972645985') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ + url('iconfont.svg?t=1568972645985#iconfont') format('svg'); /* iOS 4.1- */ +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-download:before { + content: "\e68d"; +} + +.icon-zoom-out:before { + content: "\e69c"; +} + +.icon-image:before { + content: "\e752"; +} + +.icon-iconedit:before { + content: "\e650"; +} + +.icon-actualsize:before { + content: "\e665"; +} + +.icon-copy:before { + content: "\e61c"; +} + +.icon-zoom-in:before { + content: "\e600"; +} + +.icon-clear:before { + content: "\e700"; +} + +.icon-flow-line:before { + content: "\e660"; +} + +.icon-redo:before { + content: "\e716"; +} + +.icon-undo:before { + content: "\e71a"; +} + +.icon-fit:before { + content: "\e7cb"; +} + +.icon-to-front:before { + content: "\e7cc"; +} + +.icon-to-back:before { + content: "\e7cd"; +} + +.icon-roi-select:before { + content: "\e7ce"; +} + +.icon-json:before { + content: "\e623"; +} + +.icon-fullscreen:before { + content: "\e648"; +} + +.icon-broken:before { + content: "\e9ad"; +} + +.icon-curve:before { + content: "\e9b0"; +} + +.icon-paste:before { + content: "\e963"; +} + +.icon-group:before { + content: "\e915"; +} + +.icon-ungroup:before { + content: "\e917"; +} + +.icon-arrow-dropdown:before { + content: "\e601"; +} + diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.eot b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.eot new file mode 100644 index 0000000..3eda34f Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.eot differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.js b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.js new file mode 100644 index 0000000..9467797 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.js @@ -0,0 +1 @@ +!function(i){var c,o='',h=(c=document.getElementsByTagName("script"))[c.length-1].getAttribute("data-injectcss");if(h&&!i.__iconfont__svg__cssinject__){i.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var h=function(){document.removeEventListener("DOMContentLoaded",h,!1),c()};document.addEventListener("DOMContentLoaded",h,!1)}else document.attachEvent&&(t=c,l=i.document,e=!1,(v=function(){try{l.documentElement.doScroll("left")}catch(c){return void setTimeout(v,50)}o()})(),l.onreadystatechange=function(){"complete"==l.readyState&&(l.onreadystatechange=null,o())});function o(){e||(e=!0,t())}var t,l,e,v}(function(){var c,h;(c=document.createElement("div")).innerHTML=o,o=null,(h=c.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",function(c,h){h.firstChild?function(c,h){h.parentNode.insertBefore(c,h)}(c,h.firstChild):h.appendChild(c)}(h,document.body))})}(window); \ No newline at end of file diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.svg b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.svg new file mode 100644 index 0000000..f4aa4a7 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.svg @@ -0,0 +1,95 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.ttf b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.ttf new file mode 100644 index 0000000..d5ab0e1 Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.ttf differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff new file mode 100644 index 0000000..6bfe8bd Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff2 b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff2 new file mode 100644 index 0000000..482e53e Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/iconfont/iconfont.woff2 differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/images/client.png b/hchyun-ui/src/views/system/top/packages/assets/images/client.png new file mode 100644 index 0000000..f2fe60e Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/images/client.png differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/images/database.png b/hchyun-ui/src/views/system/top/packages/assets/images/database.png new file mode 100644 index 0000000..809869e Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/images/database.png differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/images/firewall.png b/hchyun-ui/src/views/system/top/packages/assets/images/firewall.png new file mode 100644 index 0000000..c364c42 Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/images/firewall.png differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/images/server.png b/hchyun-ui/src/views/system/top/packages/assets/images/server.png new file mode 100644 index 0000000..0e49219 Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/images/server.png differ diff --git a/hchyun-ui/src/views/system/top/packages/assets/logo.png b/hchyun-ui/src/views/system/top/packages/assets/logo.png new file mode 100644 index 0000000..fb83843 Binary files /dev/null and b/hchyun-ui/src/views/system/top/packages/assets/logo.png differ diff --git a/hchyun-ui/src/views/system/top/packages/elements/button.vue b/hchyun-ui/src/views/system/top/packages/elements/button.vue new file mode 100644 index 0000000..52d48c6 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/elements/button.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/elements/checkbox.vue b/hchyun-ui/src/views/system/top/packages/elements/checkbox.vue new file mode 100644 index 0000000..2609e14 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/elements/checkbox.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/elements/dropdown.vue b/hchyun-ui/src/views/system/top/packages/elements/dropdown.vue new file mode 100644 index 0000000..5659449 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/elements/dropdown.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/elements/index.js b/hchyun-ui/src/views/system/top/packages/elements/index.js new file mode 100644 index 0000000..03d3e71 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/elements/index.js @@ -0,0 +1,17 @@ +/** + * @author: clay + * @data: 2021/11/14 + * @description: cceditor内部的通用组件 + */ + +import Checkbox from './checkbox' +import Button from './button' +import Dropdown from './dropdown' +import Loading from './loading' + +export { + Checkbox, + Button, + Dropdown, + Loading +} diff --git a/hchyun-ui/src/views/system/top/packages/elements/loading.vue b/hchyun-ui/src/views/system/top/packages/elements/loading.vue new file mode 100644 index 0000000..a746470 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/elements/loading.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/top.js b/hchyun-ui/src/views/system/top/packages/top.js new file mode 100644 index 0000000..a4baaf4 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/top.js @@ -0,0 +1,39 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 整合所有的组件,对外导出,即一个完整的组件库 + */ + +import Topology from './topology' + +// 存储组件列表 +const components = [ + Topology +] + +// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册 +const install = function(Vue) { + // 判断是否安装 + if (install.installed) return + // 遍历注册全局组件 + console.info('install----CCEditor: All----') + components.map(component => Vue.component(component.name, component)) +} + +// 判断是否是直接引入文件 +if (typeof window !== 'undefined' && window.Vue) { + install(window.Vue) +} + +export default { + // 导出的对象必须具有 install,才能被 Vue.use() 方法安装 + install, + // 以下是具体的组件列表 + Topology +} + +export { + install, + // 以下是具体的组件列表 + Topology +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/index.js b/hchyun-ui/src/views/system/top/packages/topology/index.js new file mode 100644 index 0000000..caeacf1 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/index.js @@ -0,0 +1,16 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 导入组件,组件必须声明 name + */ + +import Topology from './src/topology' + +// 为组件提供 install 安装方法,供按需引入 +Topology.install = function(Vue) { + console.info('install----CCEditor: Topology----') + Vue.component(Topology.name, Topology) +} + +// 默认导出组件 +export default Topology diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-add-edge.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-add-edge.js new file mode 100644 index 0000000..d4c2c36 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-add-edge.js @@ -0,0 +1,95 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 通过先后点击两个节点来添加连线(容易和节点点击动作交叉,已弃用) + */ + +import G6 from '@antv/g6' +import theme from '../theme' + +export default { + name: 'click-add-edge', + options: { + getEvents() { + return { + 'node:click': 'onNodeClick', + 'canvas:mousemove': 'onMousemove', + 'edge:click': 'onEdgeClick' // 点击空白处,取消边 + } + }, + onNodeClick(event) { + let graph = this.graph + let node = event.item + let point = { x: event.x, y: event.y } + let model = node.getModel() + let edgeShape = self.currentEdgeShape.guid || 'line' + if (this.addingEdge && this.edge) { + // 点击第二个节点 + graph.updateItem(this.edge, { + target: model.id + }) + this.edge = null + this.addingEdge = false + // 记录【连线】前后的数据状态 + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,连线后没有【重做】功能 + // 重置undoCount,连线后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (self.undoCount > 0) { + self.historyIndex = self.historyIndex - self.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= self.undoCount; i++) { + let key = `graph_history_${self.historyIndex + i}` + self.removeHistoryData(key) + } + self.undoCount = 0 + } else { + // 正常顺序执行的情况,记录【连线】前的数据状态 + let key = `graph_history_${self.historyIndex}` + self.addHistoryData(key, this.historyData) + } + // 记录【连线】后的数据状态 + self.historyIndex += 1 + let key = `graph_history_${self.historyIndex}` + let currentData = JSON.stringify(graph.save()) + self.addHistoryData(key, currentData) + } + } else { + // 点击第一个节点 + this.historyData = JSON.stringify(graph.save()) + if (edgeShape === 'stepline') { + this.edge = graph.addItem('edge', { + source: model.id, + target: point, + type: edgeShape, + controlPoints: [{ x: 100, y: 70 }] + }) + } else { + this.edge = graph.addItem('edge', { + source: model.id, + target: point, + type: edgeShape + }) + } + this.addingEdge = true + } + }, + onMousemove(event) { + const point = { x: event.x, y: event.y } + if (this.addingEdge && this.edge) { + this.graph.updateItem(this.edge, { + target: point + }) + } + }, + onEdgeClick(ev) { + let graph = this.graph + const currentEdge = ev.item + // 拖拽过程中,点击会点击到新增的边上 + if (this.addingEdge && this.edge === currentEdge) { + graph.removeItem(this.edge) + this.edge = null + this.addingEdge = false + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-event-edit.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-event-edit.js new file mode 100644 index 0000000..8ed3f44 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/click-event-edit.js @@ -0,0 +1,124 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 鼠标点击交互 + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'click-event-edit', + options: { + getEvents() { + return { + 'node:click': 'onNodeClick', + 'node:contextmenu': 'onNodeRightClick', + 'edge:click': 'onEdgeClick', + 'edge:contextmenu': 'onEdgeRightClick', + 'canvas:click': 'onCanvasClick' + } + }, + onNodeClick(event) { + // todo..."selected"是g6自带的状态,在"drag-add-edge"中的"node:mouseup"事件也会触发,故此处不需要设置"selected"状态 + // let clickNode = event.item; + // clickNode.setState('selected', !clickNode.hasState('selected')); + vm.currentFocus = 'node' + vm.rightMenuShow = false + this.updateVmData(event) + }, + onNodeRightClick(event) { + let graph = vm.graph + let clickNode = event.item + let clickNodeModel = clickNode.getModel() + let selectedNodes = graph.findAllByState('node', 'selected') + let selectedNodeIds = selectedNodes.map(item => {return item.getModel().id}) + vm.selectedNode = clickNode + // 如果当前点击节点是之前选中的某个节点,就进行下面的处理 + if (selectedNodes.length > 1 && selectedNodeIds.indexOf(clickNodeModel.id) > -1) { + vm.rightMenuShow = true + let rightMenu = vm.$refs.rightMenu + rightMenu.style.left = event.clientX + 'px' + rightMenu.style.top = event.clientY + 'px' + } else { + // 隐藏右键菜单 + vm.rightMenuShow = false + // 先取消所有节点的选中状态 + selectedNodes.forEach(node => { + node.setState('selected', false) + }) + // 再添加该节点的选中状态 + clickNode.setState('selected', true) + vm.currentFocus = 'node' + this.updateVmData(event) + } + graph.paint() + }, + onEdgeClick(event) { + let clickEdge = event.item + clickEdge.setState('selected', !clickEdge.hasState('selected')) + vm.currentFocus = 'edge' + this.updateVmData(event) + }, + onEdgeRightClick(event) { + let graph = vm.graph + let clickEdge = event.item + let clickEdgeModel = clickEdge.getModel() + let selectedEdges = graph.findAllByState('edge', 'selected') + // 如果当前点击节点不是之前选中的单个节点,才进行下面的处理 + if (!(selectedEdges.length === 1 && clickEdgeModel.id === selectedEdges[0].getModel().id)) { + // 先取消所有节点的选中状态 + graph.findAllByState('edge', 'selected').forEach(edge => { + edge.setState('selected', false) + }) + // 再添加该节点的选中状态 + clickEdge.setState('selected', true) + vm.currentFocus = 'edge' + this.updateVmData(event) + } + let point = { x: event.x, y: event.y } + }, + onCanvasClick() { + vm.currentFocus = 'canvas' + vm.rightMenuShow = false + }, + updateVmData(event) { + if (event.item._cfg.type === 'node') { + // 更新vm的data: selectedNode 和 selectedNodeParams + let clickNode = event.item + if (clickNode.hasState('selected')) { + let clickNodeModel = clickNode.getModel() + vm.selectedNode = clickNode + let nodeAppConfig = { ...vm.nodeAppConfig } + Object.keys(nodeAppConfig).forEach(function(key) { + nodeAppConfig[key] = '' + }) + vm.selectedNodeParams = { + label: clickNodeModel.label || '', + appConfig: { ...nodeAppConfig, ...clickNodeModel.appConfig } + } + } + } else if (event.item._cfg.type === 'edge') { + // 更新vm的data: selectedEdge 和 selectedEdgeParams + let clickEdge = event.item + if (clickEdge.hasState('selected')) { + let clickEdgeModel = clickEdge.getModel() + vm.selectedEdge = clickEdge + let edgeAppConfig = { ...vm.edgeAppConfig } + Object.keys(edgeAppConfig).forEach(function(key) { + edgeAppConfig[key] = '' + }) + vm.selectedEdgeParams = { + label: clickEdgeModel.label || '', + appConfig: { ...edgeAppConfig, ...clickEdgeModel.appConfig } + } + } + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-add-edge.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-add-edge.js new file mode 100644 index 0000000..b6cb140 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-add-edge.js @@ -0,0 +1,173 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 通过拖拽节点上的锚点添加连线 + */ +import utils from '../utils' + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +import G6 from '@antv/g6' +import theme from '../theme' + +export default { + sendThis, // 暴露函数 + name: 'drag-add-edge', + options: { + getEvents() { + return { + 'node:mousedown': 'onNodeMousedown', + 'node:mouseup': 'onNodeMouseup', + 'edge:mouseup': 'onEdgeMouseup', + 'mousemove': 'onMousemove' + } + }, + onNodeMousedown(event) { + let self = this + // 交互过程中的信息 + self.evtInfo = { + action: null, + node: event.item, + target: event.target + } + if (self.evtInfo.target && self.evtInfo.target.attrs.name) { + // todo...未来可能针对锚点增加其它功能(例如拖拽调整大小) + switch (self.evtInfo.target.attrs.name) { + case 'anchor': + self.evtInfo.action = 'drawEdge' + break + } + } + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].start.call(self, event) + } + }, + onNodeMouseup(event) { + let self = this + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].stop.call(self, event) + } + }, + onEdgeMouseup(event) { + let self = this + if (self.evtInfo && self.evtInfo.action === 'drawEdge') { + self[self.evtInfo.action].stop.call(self, event) + } + }, + onMousemove(event) { + let self = this + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].move.call(self, event) + } + }, + drawEdge: { + isMoving: false, + currentLine: null, + start: function(event) { + let self = this + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + + // ************** 暂存【连线】前的数据状态 start ************** + let graph = vm.graph + self.historyData = JSON.stringify(graph.save()) + // ************** 暂存【连线】前的数据状态 end ************** + + let sourceAnchor + let sourceNodeModel = self.evtInfo.node.getModel() + // 锚点数据 + let anchorPoints = self.evtInfo.node.getAnchorPoints() + // 处理线条目标点 + if (anchorPoints && anchorPoints.length) { + // 获取距离指定坐标最近的一个锚点 + sourceAnchor = self.evtInfo.node.getLinkPoint({ + x: event.x, + y: event.y + }) + } + self.drawEdge.currentLine = self.graph.addItem('edge', { + // id: G6.Util.uniqueId(), // 这种生成id的方式有bug,会重复 + id: utils.generateUUID(), + // 起始节点 + source: sourceNodeModel.id, + sourceAnchor: sourceAnchor ? sourceAnchor.anchorIndex : '', + // 终止节点/位置 + target: { + x: event.x, + y: event.y + }, + type: self.graph.$C.edge.type || 'cc-line', + style: G6.Util.mix({}, themeStyle.edgeStyle.default, self.graph.$C.edge.style) + }) + self.drawEdge.isMoving = true + }, + move(event) { + let self = this + if (self.drawEdge.isMoving && self.drawEdge.currentLine) { + self.graph.updateItem(self.drawEdge.currentLine, { + target: { + x: event.x, + y: event.y + } + }) + } + }, + stop(event) { + let self = this + if (self.drawEdge.isMoving) { + if (self.drawEdge.currentLine === event.item) { + // 画线过程中点击则移除当前画线 + self.graph.removeItem(event.item) + } else { + let targetNode = event.item + let targetNodeModel = targetNode.getModel() + let targetAnchor = null + // 锚点数据 + let anchorPoints = targetNode.getAnchorPoints() + // 处理线条目标点 + if (anchorPoints && anchorPoints.length) { + // 获取距离指定坐标最近的一个锚点 + targetAnchor = targetNode.getLinkPoint({ x: event.x, y: event.y }) + } + self.graph.updateItem(self.drawEdge.currentLine, { + target: targetNodeModel.id, + targetAnchor: targetAnchor ? targetAnchor.anchorIndex : '' + }) + + // ************** 记录historyData的逻辑 start ************** + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,拖拽节点后没有【重做】功能 + // 重置undoCount,拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } else { + // 正常顺序执行的情况,记录拖拽前的数据状态 + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, this.historyData) + } + // 记录拖拽后的数据状态 + vm.historyIndex += 1 + let key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + } + // ************** 记录historyData的逻辑 end ************** + } + } + self.drawEdge.currentLine = null + self.drawEdge.isMoving = false + self.evtInfo = null + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-event-edit.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-event-edit.js new file mode 100644 index 0000000..cd18baa --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/drag-event-edit.js @@ -0,0 +1,53 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 鼠标拖动节点的交互(记录拖拽前后的数据,用于【撤销】和【重做】) + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'drag-event-edit', + options: { + getEvents() { + return { + 'node:dragstart': 'onNodeDragstart', + 'node:dragend': 'onNodeDragend' + } + }, + onNodeDragstart() { + let graph = vm.graph + this.historyData = JSON.stringify(graph.save()) + }, + onNodeDragend() { + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,拖拽节点后没有【重做】功能 + // 重置undoCount,拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } else { + // 正常顺序执行的情况,记录拖拽前的数据状态 + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, this.historyData) + } + // 记录拖拽后的数据状态 + vm.historyIndex += 1 + let key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/hover-event-edit.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/hover-event-edit.js new file mode 100644 index 0000000..bf2b7e1 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/hover-event-edit.js @@ -0,0 +1,25 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 悬浮交互 + */ + +export default { + name: 'hover-event-edit', + options: { + getEvents() { + return { + 'node:mouseover': 'onNodeHover', + 'node:mouseout': 'onNodeOut' + } + }, + onNodeHover(event) { + let hoverNode = event.item + hoverNode.setState('hover', true) + }, + onNodeOut(event) { + let hoverNode = event.item + hoverNode.setState('hover', false) + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/index.js new file mode 100644 index 0000000..6d2f96a --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/index.js @@ -0,0 +1,28 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: register behaviors + */ + +import dragAddEdge from './drag-add-edge' +import hoverEventEdit from './hover-event-edit' +import clickEventEdit from './click-event-edit' +import dragEventEdit from './drag-event-edit' +import keyupEventEdit from './keyup-event-edit' + +const obj = { + dragAddEdge, + hoverEventEdit, + clickEventEdit, + dragEventEdit, + keyupEventEdit +} + +export default { + obj, + register(G6) { + Object.values(obj).map(item => { + G6.registerBehavior(item.name, item.options) + }) + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/behavior/keyup-event-edit.js b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/keyup-event-edit.js new file mode 100644 index 0000000..cbd8830 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/behavior/keyup-event-edit.js @@ -0,0 +1,63 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edit mode: 键盘事件的交互,主要是删除节点和连线(记录删除前后的数据,用于【撤销】和【重做】) + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'keyup-event-edit', + options: { + getEvents() { + return { + 'keyup': 'onKeyup' + } + }, + onKeyup(event) { + let graph = this.graph + let selectedNodes = graph.findAllByState('node', 'selected') + let selectedEdges = graph.findAllByState('edge', 'selected') + if (event.keyCode === 46 && (selectedNodes.length > 0 || selectedEdges.length > 0)) { + + // ************** 记录【删除】前的数据状态 start ************** + let historyData = JSON.stringify(graph.save()) + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, historyData) + // ************** 记录【删除】前的数据状态 end ************** + + // 开始删除 + for (let i = 0; i < selectedNodes.length; i++) { + graph.removeItem(selectedNodes[i]) + } + for (let i = 0; i < selectedEdges.length; i++) { + graph.removeItem(selectedEdges[i]) + } + + // ************** 记录【删除】后的数据状态 start ************** + // 如果当前点过【撤销】了,拖拽节点后将取消【重做】功能 + // 重置undoCount,【删除】后的数据状态给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } + // 记录【删除】后的数据状态 + vm.historyIndex += 1 + key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + // ************** 记录【删除】后的数据状态 end ************** + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/config/edge.js b/hchyun-ui/src/views/system/top/packages/topology/src/config/edge.js new file mode 100644 index 0000000..2604961 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/config/edge.js @@ -0,0 +1,13 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 线条的后期设置 + */ + +export default { + type: 'cc-line', + style: { + startArrow: false, + endArrow: false + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/config/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/config/index.js new file mode 100644 index 0000000..a6c12bb --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/config/index.js @@ -0,0 +1,11 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 配置 + */ + +import edge from './edge' + +export default { + edge +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/base.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/base.js new file mode 100644 index 0000000..691a2fa --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/base.js @@ -0,0 +1,29 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 线公共方法 + */ + +import utils from '../utils' + +export default { + draw(cfg, group) { + const { startPoint, endPoint } = cfg + const keyShape = group.addShape('path', { + className: 'edge-shape', + attrs: { + ...cfg.style, + path: [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + name: 'edge-shape' + }) + return keyShape + }, + setState(name, value, item) { + // 设置边状态 + utils.edge.setState(name, value, item) + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-brokenline.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-brokenline.js new file mode 100644 index 0000000..45a294c --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-brokenline.js @@ -0,0 +1,52 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 折线 + */ + +import base from './base' +import theme from '../theme' + +/** + * fix: 继承 polyline 在 G6 3.x 里面有bug + * 现实现方法参考 https://g6.antv.vision/zh/examples/shape/customEdge#customPolyline + */ +export default { + name: 'cc-brokenline', + extendName: 'line', + options: { + ...base, + getPath(points) { + const startPoint = points[0] + const endPoint = points[1] + return [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, endPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + getShapeStyle(cfg) { + const { startPoint, endPoint } = cfg + const controlPoints = this.getControlPoints(cfg) + let points = [startPoint] // 添加起始点 + // 添加控制点 + if (controlPoints) { + points = points.concat(controlPoints) + } + // 添加结束点 + points.push(endPoint) + const path = this.getPath(points) + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + stroke: '#BBB', + lineWidth: 1, + path, + startArrow: false, + endArrow: false, + ...themeStyle.edgeStyle.default + } + return style + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-cubic.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-cubic.js new file mode 100644 index 0000000..dc01adb --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-cubic.js @@ -0,0 +1,15 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 曲线 + */ + +import base from './base' + +export default { + name: 'cc-cubic', + extendName: 'cubic', + options: { + ...base + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-line.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-line.js new file mode 100644 index 0000000..898b532 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-line.js @@ -0,0 +1,15 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 直线 + */ + +import base from './base' + +export default { + name: 'cc-line', + extendName: 'line', + options: { + ...base + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-polyline.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-polyline.js new file mode 100644 index 0000000..7435908 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/cc-polyline.js @@ -0,0 +1,77 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 多段线 + */ + +import base from './base' +import { polylineFinding } from './polyline-finding' + +export default { + name: 'cc-polyline', + extendName: 'single-edge', + options: { + ...base, + draw(cfg, group) { + const { startPoint, endPoint } = cfg + const controlPoints = this.getControlPoints(cfg) + let points = [startPoint] + if (controlPoints) { + points.push(controlPoints) + } + points.push(endPoint) + let path = this.getPath(points) + const keyShape = group.addShape('path', { + className: 'edge-shape', + attrs: { + ...cfg, + path: path + }, + draggable: true, + name: 'edge-shape' + }) + return keyShape + }, + getPath(points) { + const path = [] + for (let i = 0; i < points.length; i++) { + const point = points[i] + if (i === 0) { + path.push(['M', point.x, point.y]) + } else if (i === points.length - 1) { + path.push(['L', point.x, point.y]) + } else { + const prevPoint = points[i - 1] + let nextPoint = points[i + 1] + let cornerLen = 5 + if (Math.abs(point.y - prevPoint.y) > cornerLen || Math.abs(point.x - prevPoint.x) > cornerLen) { + if (prevPoint.x === point.x) { + path.push(['L', point.x, point.y > prevPoint.y ? point.y - cornerLen : point.y + cornerLen]) + } else if (prevPoint.y === point.y) { + path.push(['L', point.x > prevPoint.x ? point.x - cornerLen : point.x + cornerLen, point.y]) + } + } + const yLen = Math.abs(point.y - nextPoint.y) + const xLen = Math.abs(point.x - nextPoint.x) + if (yLen > 0 && yLen < cornerLen) { + cornerLen = yLen + } else if (xLen > 0 && xLen < cornerLen) { + cornerLen = xLen + } + if (prevPoint.x !== nextPoint.x && nextPoint.x === point.x) { + path.push(['Q', point.x, point.y, point.x, point.y > nextPoint.y ? point.y - cornerLen : point.y + cornerLen]) + } else if (prevPoint.y !== nextPoint.y && nextPoint.y === point.y) { + path.push(['Q', point.x, point.y, point.x > nextPoint.x ? point.x - cornerLen : point.x + cornerLen, point.y]) + } + } + } + return path + }, + getControlPoints(cfg) { + if (!cfg.sourceNode) { + return cfg.controlPoints + } + return polylineFinding(cfg.sourceNode, cfg.targetNode, cfg.startPoint, cfg.endPoint, 40) + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/index.js new file mode 100644 index 0000000..76924a7 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/index.js @@ -0,0 +1,23 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: register edges + */ + +import ccLine from './cc-line' +import ccBrokenline from './cc-brokenline' +import ccPolyline from './cc-polyline' +import ccCubic from './cc-cubic' + +const obj = { + ccLine, + ccBrokenline, + ccPolyline, + ccCubic +} + +export default function(G6) { + Object.values(obj).map(item => { + G6.registerEdge(item.name, item.options, item.extendName) + }) +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/edge/polyline-finding.js b/hchyun-ui/src/views/system/top/packages/topology/src/edge/polyline-finding.js new file mode 100644 index 0000000..39531fc --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/edge/polyline-finding.js @@ -0,0 +1,275 @@ +/** + * 带圆角折线连线的策略 + * 文档:https://www.yuque.com/antv/blog/eyi70n + * 参考:https://github.com/guozhaolong/wfd/blob/master/src/item/edge.js + * 引用:https://github.com/OXOYO/X-Flowchart-Vue/blob/master/src/global/lib/g6/edge/polylineFinding.js + */ + +// 折线寻径 +export const polylineFinding = function(sNode, tNode, sPort, tPort, offset = 10) { + const sourceBBox = sNode && sNode.getBBox ? sNode.getBBox() : getPointBBox(sPort) + const targetBBox = tNode && tNode.getBBox ? tNode.getBBox() : getPointBBox(tPort) + // 获取节点带 offset 的区域(扩展区域) + const sBBox = getExpandedBBox(sourceBBox, offset) + const tBBox = getExpandedBBox(targetBBox, offset) + // 获取扩展区域上的起始和终止连接点 + const sPoint = getExpandedPort(sBBox, sPort) + const tPoint = getExpandedPort(tBBox, tPort) + // 获取合法折点集 + let points = getConnectablePoints(sBBox, tBBox, sPoint, tPoint) + // 过滤合法点集,预处理、剪枝等 + filterConnectablePoints(points, sBBox) + // 过滤合法点集,预处理、剪枝等 + filterConnectablePoints(points, tBBox) + // 用 A-Star 算法寻径 + let polylinePoints = AStar(points, sPoint, tPoint, sBBox, tBBox) + return polylinePoints +} + +const getPointBBox = function(t) { + return { + centerX: t.x, + centerY: t.y, + minX: t.x, + minY: t.y, + maxX: t.x, + maxY: t.y, + height: 0, + width: 0 + } +} + +// 获取扩展区域 +const getExpandedBBox = function(bbox, offset) { + if (bbox.width === 0 && bbox.height === 0) { + return bbox + } + return { + centerX: bbox.centerX, + centerY: bbox.centerY, + minX: bbox.minX - offset, + minY: bbox.minY - offset, + maxX: bbox.maxX + offset, + maxY: bbox.maxY + offset, + height: bbox.height + 2 * offset, + width: bbox.width + 2 * offset + } +} + +// 获取扩展区域上的连接点 +const getExpandedPort = function(bbox, point) { + // 判断连接点在上下左右哪个区域,相应地给x或y加上或者减去offset + if (Math.abs(point.x - bbox.centerX) / bbox.width > Math.abs(point.y - bbox.centerY) / bbox.height) { + return { + x: point.x > bbox.centerX ? bbox.maxX : bbox.minX, + y: point.y + } + } + return { + x: point.x, + y: point.y > bbox.centerY ? bbox.maxY : bbox.minY + } +} + +// 获取合法折点集合 +const getConnectablePoints = function(sBBox, tBBox, sPoint, tPoint) { + let lineBBox = getBBoxFromVertexes(sPoint, tPoint) + let outerBBox = combineBBoxes(sBBox, tBBox) + let sLineBBox = combineBBoxes(sBBox, lineBBox) + let tLineBBox = combineBBoxes(tBBox, lineBBox) + let points = [ + ...vertexOfBBox(sLineBBox), + ...vertexOfBBox(tLineBBox), + ...vertexOfBBox(outerBBox) + ] + const centerPoint = { x: outerBBox.centerX, y: outerBBox.centerY } + let bboxes = [outerBBox, sLineBBox, tLineBBox, lineBBox] + bboxes.forEach(bbox => { + // 包含 bbox 延长线和线段的相交线 + points = [ + ...points, + ...crossPointsByLineAndBBox(bbox, centerPoint) + ] + }) + points.push({ x: sPoint.x, y: tPoint.y }) + points.push({ x: tPoint.x, y: sPoint.y }) + return points +} + +const getBBoxFromVertexes = function(sPoint, tPoint) { + const minX = Math.min(sPoint.x, tPoint.x) + const maxX = Math.max(sPoint.x, tPoint.x) + const minY = Math.min(sPoint.y, tPoint.y) + const maxY = Math.max(sPoint.y, tPoint.y) + + return { + centerX: (minX + maxX) / 2, + centerY: (minY + maxY) / 2, + maxX: maxX, + maxY: maxY, + minX: minX, + minY: minY, + height: maxY - minY, + width: maxX - minX + } +} + +const combineBBoxes = function(sBBox, tBBox) { + const minX = Math.min(sBBox.minX, tBBox.minX) + const minY = Math.min(sBBox.minY, tBBox.minY) + const maxX = Math.max(sBBox.maxX, tBBox.maxX) + const maxY = Math.max(sBBox.maxY, tBBox.maxY) + + return { + centerX: (minX + maxX) / 2, + centerY: (minY + maxY) / 2, + minX: minX, + minY: minY, + maxX: maxX, + maxY: maxY, + height: maxY - minY, + width: maxX - minX + } +} + +const vertexOfBBox = function(bbox) { + return [ + { x: bbox.minX, y: bbox.minY }, + { x: bbox.maxX, y: bbox.minY }, + { x: bbox.maxX, y: bbox.maxY }, + { x: bbox.minX, y: bbox.maxY } + ] +} + +const crossPointsByLineAndBBox = function(bbox, centerPoint) { + let crossPoints = [] + if (!(centerPoint.x < bbox.minX || centerPoint.x > bbox.maxX)) { + crossPoints = [ + ...crossPoints, + { x: centerPoint.x, y: bbox.minY }, + { x: centerPoint.x, y: bbox.maxY } + ] + } + if (!(centerPoint.y < bbox.minY || centerPoint.y > bbox.maxY)) { + crossPoints = [ + ...crossPoints, + { x: bbox.minX, y: centerPoint.y }, + { x: bbox.maxX, y: centerPoint.y } + ] + } + + return crossPoints +} + +// 过滤连接点 +const filterConnectablePoints = function(points, bbox) { + return points.filter(point => { + return point.x <= bbox.minX || point.x >= bbox.maxX || point.y <= bbox.minY || point.y >= bbox.maxY + }) +} + +const crossBBox = function(bboxes, p1, p2) { + for (let i = 0; i < bboxes.length; i++) { + const bbox = bboxes[i] + if (p1.x === p2.x && bbox.minX < p1.x && bbox.maxX > p1.x) { + if ((p1.y < bbox.maxY && p2.y >= bbox.maxY) || (p2.y < bbox.maxY && p1.y >= bbox.maxY)) { + return true + } + } else if (p1.y === p2.y && bbox.minY < p1.y && bbox.maxY > p1.y) { + if ((p1.x < bbox.maxX && p2.x >= bbox.maxX) || (p2.x < bbox.maxX && p1.x >= bbox.maxX)) { + return true + } + } + } + return false +} + +const getCost = function(p1, p2) { + return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) +} + +// aStar 寻径 +const AStar = function(points, sPoint, tPoint, sBBox, tBBox) { + const openList = [sPoint] + const closeList = [] + points.forEach(item => { + item.id = item.x + '-' + item.y + }) + let tmpArr = [] + points.forEach(item => { + if (!tmpArr.includes(target => target.id === item.id)) { + tmpArr.push(item) + } + }) + points = [ + ...tmpArr, + tPoint + ] + let endPoint + while (openList.length > 0) { + let minCostPoint + openList.forEach((p, i) => { + if (!p.parent) { + p.f = 0 + } + if (!minCostPoint) { + minCostPoint = p + } + if (p.f < minCostPoint.f) { + minCostPoint = p + } + }) + if (minCostPoint.x === tPoint.x && minCostPoint.y === tPoint.y) { + endPoint = minCostPoint + break + } + openList.splice(openList.findIndex(o => o.x === minCostPoint.x && o.y === minCostPoint.y), 1) + closeList.push(minCostPoint) + const neighbor = points.filter(p => { + return (p.x === minCostPoint.x || p.y === minCostPoint.y) && + !(p.x === minCostPoint.x && p.y === minCostPoint.y) && + !crossBBox([sBBox, tBBox], minCostPoint, p) + } + ) + neighbor.forEach(p => { + const inOpen = openList.find(o => o.x === p.x && o.y === p.y) + const currentG = getCost(p, minCostPoint) + if (!closeList.find(o => o.x === p.x && o.y === p.y)) { + if (inOpen) { + if (p.g > currentG) { + p.parent = minCostPoint + p.g = currentG + p.f = p.g + p.h + } + } else { + p.parent = minCostPoint + p.g = currentG + let h = getCost(p, tPoint) + if (crossBBox([tBBox], p, tPoint)) { + // 如果穿过bbox则增加该点的预估代价为bbox周长的一半 + h += (tBBox.width / 2 + tBBox.height / 2) + } + p.h = h + p.f = p.g + p.h + openList.push(p) + } + } + }) + } + if (endPoint) { + const result = [] + result.push({ + x: endPoint.x, + y: endPoint.y + }) + while (endPoint.parent) { + endPoint = endPoint.parent + result.push({ + x: endPoint.x, + y: endPoint.y + }) + } + return result.reverse() + } + return [] +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/graph/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/graph/index.js new file mode 100644 index 0000000..0e9c346 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/graph/index.js @@ -0,0 +1,159 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 图的布局方式/图的初始化 + */ + +import d3 from '../plugins/d3-installer' +import theme from '../theme' + +/** + * 图的布局方式/图的初始化 + * @type {{commonGraph: (function(*, *): G6.Graph)}} + */ +const initGraph = { + /** + * 一般布局 + * @param G6 + * @param options + * @returns {G6.Graph} + */ + commonGraph: function(G6, options) { + let graphData = options.graphData + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + // 生成G6实例 + let graph = new G6.Graph({ + plugins: options.plugins, + container: options.container, + width: options.width, + height: options.height, + // layout: { + // type: 'random', + // width: options.width, + // height: options.height + // }, + defaultNode: { + type: 'top-rect', + labelCfg: { + position: 'bottom' + } + }, + defaultEdge: { + type: 'cc-line', + labelCfg: { + position: 'center', + autoRotate: false + } + }, + nodeStateStyles: themeStyle.nodeStyle, + // nodeStyle: { + // selected: { + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + // } + // }, + edgeStateStyles: themeStyle.edgeStyle, + // edgeStyle: { + // default: { + // stroke: '#e2e2e2', + // lineWidth: 3, + // lineAppendWidth: 10 + // }, + // selected: { + // shadowColor: '#626262', + // shadowBlur: 3 + // } + // }, + modes: options.modes + }) + // 将 read 方法分解成 data() 和 render 方法,便于整个生命周期的管理 + graph.read(graphData) + graph.render() + // 返回G6实例 + return graph + }, + /** + * 力导布局 + * @param G6 + * @param options + * @returns {*} + */ + forceLayoutGraph: function(resolve, G6, options) { + let graphData = options.graphData + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + // 生成G6实例 + let graph = new G6.Graph({ + container: options.container, + width: options.width, + height: options.height, + nodeStateStyles: themeStyle.nodeStyle, + edgeStateStyles: themeStyle.edgeStyle + }) + // 初始化力导布局 + let simulation = d3 + .forceSimulation() + .force( + 'link', + d3 + .forceLink() + .id(function(d) { + return d.id + }) + .distance(linkDistance) + .strength(0.5) + ) + .force('charge', d3.forceManyBody().strength(-500).distanceMax(500).distanceMin(100)) + .force('center', d3.forceCenter(options.width / 2, options.height / 2)) + // 定义节点数据 + simulation.nodes(graphData.nodes).on('tick', ticked) + // 定义连线数据 + let edges = [] + for (let i = 0; i < graphData.edges.length; i++) { + edges.push({ + id: graphData.edges[i].id, + source: graphData.edges[i].source, + target: graphData.edges[i].target + }) + } + simulation.force('link').links(edges) + graph.data(graphData) + graph.render() + + function linkDistance(d) { + return 150 + } + + function ticked() { + // protect: planA: 移除事件监听器 planB: 手动停止力模拟 + if (graph.destroyed) { + // simulation.nodes(graphData.nodes).on('tick', null) + simulation.stop() + return + } + if (!graph.get('data')) { + // 若是第一次渲染,定义数据,渲染 + graph.data(graphData) + graph.render() + } else { + // 后续渲染,直接刷新所有点和边的位置 + graph.refreshPositions() + } + } + + // 控制时间: 只布局10秒 + let t = setTimeout(function() { + simulation.stop() + resolve(graph) + }, 10000) + + // 判断force-layout结束 + simulation.on('end', () => { + clearTimeout(t) + resolve(graph) + }) + } +} + +export default initGraph diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/node/base.js b/hchyun-ui/src/views/system/top/packages/topology/src/node/base.js new file mode 100644 index 0000000..eb7feb1 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/node/base.js @@ -0,0 +1,21 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 节点基础方法 + */ + +import utils from '../utils' + +export default { + setState(name, value, item) { + // 设置节点状态 + utils.node.setState(name, value, item) + // 设置锚点状态 + utils.anchor.setState(name, value, item) + }, + // 绘制后附加锚点 + afterDraw(cfg, group) { + // 绘制锚点 + utils.anchor.draw(cfg, group) + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/node/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/node/index.js new file mode 100644 index 0000000..85fcbaf --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/node/index.js @@ -0,0 +1,22 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: register nodes + */ + +import ccRect from './top-rect' +import ccImage from './top-image' + +const obj = { + ccRect, + ccImage +} + +export default { + obj, + register(G6) { + Object.values(obj).map(item => { + G6.registerNode(item.name, item.options, item.extendName) + }) + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/node/top-image.js b/hchyun-ui/src/views/system/top/packages/topology/src/node/top-image.js new file mode 100644 index 0000000..1ffb211 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/node/top-image.js @@ -0,0 +1,126 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 图片节点 + */ + +import utils from '../utils' + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, + name: 'top-image', + extendName: 'image', + options: { + setState(name, value, item) { + // 设置节点状态 + utils.node.setState(name, value, item) + // 设置锚点状态 + if (vm.graphMode === 'edit') { + utils.anchor.setState(name, value, item) + } + }, + // 绘制后附加锚点 + afterDraw(cfg, group) { + // 绘制锚点 + if (vm.graphMode === 'edit') { + utils.anchor.draw(cfg, group) + } + }, + // 设置告警状态 + afterUpdate(cfg, node) { + const group = node.getContainer() + // 获取children + const halos = group.findAll(function(item) { + return item.attrs.name === 'halo' + }) + // 告警 + if (cfg.appState && cfg.appState.alert) { + if (halos.length > 0) { + return + } + let size = this.getSize(cfg) || [48, 48] + let r = size[0] / 2 + let { id } = cfg + let halo1 = group.addShape('circle', { + id: id + '_halo_' + 1, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', + opacity: 0.6 + }, + name: 'halo', + zIndex: -3 + }) + let halo2 = group.addShape('circle', { + id: id + '_halo_' + 2, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', // 为了显示清晰,随意设置了颜色 + opacity: 0.6 + }, + name: 'halo', + zIndex: -2 + }) + let halo3 = group.addShape('circle', { + id: id + '_halo_' + 3, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', + opacity: 0.6 + }, + name: 'halo', + zIndex: -1 + }) + group.sort() // 排序,根据zIndex 排序 + halo1.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1, + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 0 // 无延迟 + }) + halo2.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1 + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 1000 // 1 秒延迟 + }) + halo3.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1 + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 2000 // 2 秒延迟 + }) + } else { + halos.forEach(halo => { + // FIXME: G6 3.x在底层库遗留了bug,导致removeChild()方法报错,等待解决 + group.removeChild(halo) + }) + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/node/top-rect.js b/hchyun-ui/src/views/system/top/packages/topology/src/node/top-rect.js new file mode 100644 index 0000000..4945056 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/node/top-rect.js @@ -0,0 +1,30 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 矩形节点 + */ + +import base from './base' +import theme from '../theme' + +export default { + name: 'top-rect', + extendName: 'rect', + options: { + ...base, + getShapeStyle(cfg) { + const size = this.getSize(cfg) || [48, 48] + const width = size[0] + const height = size[1] + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + x: 0 - width / 2, + y: 0 - height / 2, + width: width, + height: height, + ...themeStyle.nodeStyle.default + } + return style + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/plugins/d3-installer.js b/hchyun-ui/src/views/system/top/packages/topology/src/plugins/d3-installer.js new file mode 100644 index 0000000..1018837 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/plugins/d3-installer.js @@ -0,0 +1,9 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: install 3rd plugins + */ + +import * as d3 from 'd3-force/dist/d3-force' + +export default d3 diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/theme/dark-style.js b/hchyun-ui/src/views/system/top/packages/topology/src/theme/dark-style.js new file mode 100644 index 0000000..ab8409b --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/theme/dark-style.js @@ -0,0 +1,177 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: dark style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: 'transparent', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#FFF' + } + }, + // 连线样式 + edgeStyle: { + default: { + stroke: '#53da3a', + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10 + // endArrow: true + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '' + } + }, + // 锚点样式 + anchorStyle: { + default: { + radius: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + radius: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/theme/default-style.js b/hchyun-ui/src/views/system/top/packages/topology/src/theme/default-style.js new file mode 100644 index 0000000..7459cb6 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/theme/default-style.js @@ -0,0 +1,177 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: default style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: 'transparent', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#000' + } + }, + // 连线样式 + edgeStyle: { + default: { + stroke: '#A3B1BF', + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10 + // endArrow: true + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '' + } + }, + // 锚点样式 + anchorStyle: { + default: { + r: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + r: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/theme/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/theme/index.js new file mode 100644 index 0000000..dd2290f --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/theme/index.js @@ -0,0 +1,15 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: 编辑器主题样式 - 节点、连线的预设样式 + */ + +import defaultStyle from './default-style' +import darkStyle from './dark-style' +import officeStyle from './office-style' + +export default { + defaultStyle, + darkStyle, + officeStyle +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/theme/office-style.js b/hchyun-ui/src/views/system/top/packages/topology/src/theme/office-style.js new file mode 100644 index 0000000..48f425c --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/theme/office-style.js @@ -0,0 +1,177 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: office style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: '#FFFFFF', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#000' + } + }, + // 连线样式 + edgeStyle: { + default: { + stroke: '#41c23a', + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10 + // endArrow: true + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '' + } + }, + // 锚点样式 + anchorStyle: { + default: { + radius: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + radius: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-edit.vue b/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-edit.vue new file mode 100644 index 0000000..70eeb55 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-edit.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-preview.vue b/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-preview.vue new file mode 100644 index 0000000..be9a3eb --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/toolbar-preview.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/topology.vue b/hchyun-ui/src/views/system/top/packages/topology/src/topology.vue new file mode 100644 index 0000000..ac989be --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/topology.vue @@ -0,0 +1,1299 @@ + + + + + + diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/draw.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/draw.js new file mode 100644 index 0000000..a92d1d0 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/draw.js @@ -0,0 +1,59 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: draw anchor + */ + +import theme from '../../theme' + +export default function(cfg, group) { + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + let { anchorPoints, width, height, id } = cfg + if (anchorPoints && anchorPoints.length) { + for (let i = 0, len = anchorPoints.length; i < len; i++) { + let [x, y] = anchorPoints[i] + // 计算Marker中心点坐标 + let originX = -width / 2 + let originY = -height / 2 + let anchorX = x * width + originX + let anchorY = y * height + originY + // 添加锚点背景 + let anchorBgShape = group.addShape('marker', { + id: id + '_anchor_bg_' + i, + attrs: { + name: 'anchorBg', + x: anchorX, + y: anchorY, + // 锚点默认样式 + ...themeStyle.anchorBgStyle.default + }, + draggable: false, + name: 'markerBg-shape' + }) + // 添加锚点Marker形状 + let anchorShape = group.addShape('marker', { + id: id + '_anchor_' + i, + attrs: { + name: 'anchor', + x: anchorX, + y: anchorY, + // 锚点默认样式 + ...themeStyle.anchorStyle.default + }, + draggable: false, + name: 'marker-shape' + }) + + anchorShape.on('mouseenter', function() { + anchorBgShape.attr({ + ...themeStyle.anchorBgStyle.active + }) + }) + anchorShape.on('mouseleave', function() { + anchorBgShape.attr({ + ...themeStyle.anchorBgStyle.inactive + }) + }) + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/index.js new file mode 100644 index 0000000..58cdc6d --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/index.js @@ -0,0 +1,15 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: anchor + */ + +import draw from './draw' +import setState from './set-state' +import update from './update' + +export default { + draw, + setState, + update +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/set-state.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/set-state.js new file mode 100644 index 0000000..f51ca10 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/set-state.js @@ -0,0 +1,26 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: set anchor state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'hover') { + let group = item.getContainer() + let children = group.get('children') + for (let i = 0, len = children.length; i < len; i++) { + let child = children[i] + // 处理锚点状态 + if (child.attrs.name === 'anchor') { + if (value) { + child.attr(themeStyle.anchorStyle.hover) + } else { + child.attr(themeStyle.anchorStyle.unhover) + } + } + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/update.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/update.js new file mode 100644 index 0000000..e5ac825 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/anchor/update.js @@ -0,0 +1,31 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: update anchor + */ + +export default function(cfg, group) { + let { anchorPoints, width, height, id } = cfg + if (anchorPoints && anchorPoints.length) { + for (let i = 0, len = anchorPoints.length; i < len; i++) { + let [x, y] = anchorPoints[i] + // 计算Marker中心点坐标 + let originX = -width / 2 + let originY = -height / 2 + let anchorX = x * width + originX + let anchorY = y * height + originY + // 锚点背景 + let anchorBgShape = group.findById(id + '_anchor_bg_' + i) + // 锚点 + let anchorShape = group.findById(id + '_anchor_' + i) + anchorBgShape.attr({ + x: anchorX, + y: anchorY + }) + anchorShape.attr({ + x: anchorX, + y: anchorY + }) + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/index.js new file mode 100644 index 0000000..da9ab39 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/index.js @@ -0,0 +1,11 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: edge + */ + +import setState from './set-state' + +export default { + setState +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/set-state.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/set-state.js new file mode 100644 index 0000000..5703455 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/edge/set-state.js @@ -0,0 +1,26 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: set edge state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const group = item.getContainer() + const shape = group.get('children')[0] // 顺序根据 draw 时确定 + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'active') { + if (value) { + shape.attr(themeStyle.edgeStyle.active) + } else { + shape.attr(themeStyle.edgeStyle.inactive) + } + } else if (name === 'selected') { + if (value) { + shape.attr(themeStyle.edgeStyle.selected) + } else { + shape.attr(themeStyle.edgeStyle.unselected) + } + } +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/index.js new file mode 100644 index 0000000..5c1f3d9 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/index.js @@ -0,0 +1,64 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: graph utils + */ + +import node from './node' +import anchor from './anchor' +import edge from './edge' + +/** + * 比较两个对象的内容是否相同(两个对象的键值都相同) + * @param obj1 + * @param obj2 + * @returns {*} + */ +const isObjectValueEqual = function(obj1, obj2) { + let o1 = obj1 instanceof Object + let o2 = obj2 instanceof Object + // 不是对象的情况 + if (!o1 || !o2) { + return obj1 === obj2 + } + // 对象的属性(key值)个数不相等 + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false + } + // 判断每个属性(如果属性值也是对象则需要递归) + for (let attr in obj1) { + let t1 = obj1[attr] instanceof Object + let t2 = obj2[attr] instanceof Object + if (t1 && t2) { + return isObjectValueEqual(obj1[attr], obj2[attr]) + } else if (obj1[attr] !== obj2[attr]) { + return false + } + } + return true +} + + +/** + * 生成uuid算法,碰撞率低于1/2^^122 + * @returns {string} + */ +const generateUUID = function() { + let d = new Date().getTime() + // x 是 0-9 或 a-f 范围内的一个32位十六进制数 + let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + let r = (d + Math.random() * 16) % 16 | 0 + d = Math.floor(d / 16) + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) + }) + return uuid +} + +export default { + node, + anchor, + edge, + // 通用工具类函数 + isObjectValueEqual, + generateUUID +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/index.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/index.js new file mode 100644 index 0000000..f33b64d --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/index.js @@ -0,0 +1,11 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: node + */ + +import setState from './set-state' + +export default { + setState +} diff --git a/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/set-state.js b/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/set-state.js new file mode 100644 index 0000000..fffada0 --- /dev/null +++ b/hchyun-ui/src/views/system/top/packages/topology/src/utils/node/set-state.js @@ -0,0 +1,26 @@ +/** + * @author: clay + * @data: 2021/5/10 + * @description: set node state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const group = item.getContainer() + const shape = group.get('children')[0] // 顺序根据 draw 时确定 + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'active') { + if (value) { + shape.attr(themeStyle.nodeStyle.active) + } else { + shape.attr(themeStyle.nodeStyle.inactive) + } + } else if (name === 'selected') { + if (value) { + shape.attr(themeStyle.nodeStyle.selected) + } else { + shape.attr(themeStyle.nodeStyle.default) + } + } +} diff --git a/hchyun-ui/vue.config.js b/hchyun-ui/vue.config.js index ceac998..7d6abea 100644 --- a/hchyun-ui/vue.config.js +++ b/hchyun-ui/vue.config.js @@ -45,6 +45,9 @@ module.exports = { // }, // disableHostCheck: true // }, + // 强制内联CSS + // 默认true: 使用CSS分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用 ")}catch(c){console&&console.log(c)}}!function(c){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(c,0);else{var h=function(){document.removeEventListener("DOMContentLoaded",h,!1),c()};document.addEventListener("DOMContentLoaded",h,!1)}else document.attachEvent&&(t=c,l=i.document,e=!1,(v=function(){try{l.documentElement.doScroll("left")}catch(c){return void setTimeout(v,50)}o()})(),l.onreadystatechange=function(){"complete"==l.readyState&&(l.onreadystatechange=null,o())});function o(){e||(e=!0,t())}var t,l,e,v}(function(){var c,h;(c=document.createElement("div")).innerHTML=o,o=null,(h=c.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",function(c,h){h.firstChild?function(c,h){h.parentNode.insertBefore(c,h)}(c,h.firstChild):h.appendChild(c)}(h,document.body))})}(window); \ No newline at end of file diff --git a/packages/assets/iconfont/iconfont.svg b/packages/assets/iconfont/iconfont.svg new file mode 100644 index 0000000..f4aa4a7 --- /dev/null +++ b/packages/assets/iconfont/iconfont.svg @@ -0,0 +1,95 @@ + + + + + +Created by iconfont + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/assets/iconfont/iconfont.ttf b/packages/assets/iconfont/iconfont.ttf new file mode 100644 index 0000000..d5ab0e1 Binary files /dev/null and b/packages/assets/iconfont/iconfont.ttf differ diff --git a/packages/assets/iconfont/iconfont.woff b/packages/assets/iconfont/iconfont.woff new file mode 100644 index 0000000..6bfe8bd Binary files /dev/null and b/packages/assets/iconfont/iconfont.woff differ diff --git a/packages/assets/iconfont/iconfont.woff2 b/packages/assets/iconfont/iconfont.woff2 new file mode 100644 index 0000000..482e53e Binary files /dev/null and b/packages/assets/iconfont/iconfont.woff2 differ diff --git a/packages/assets/images/client.png b/packages/assets/images/client.png new file mode 100644 index 0000000..f2fe60e Binary files /dev/null and b/packages/assets/images/client.png differ diff --git a/packages/assets/images/database.png b/packages/assets/images/database.png new file mode 100644 index 0000000..809869e Binary files /dev/null and b/packages/assets/images/database.png differ diff --git a/packages/assets/images/firewall.png b/packages/assets/images/firewall.png new file mode 100644 index 0000000..c364c42 Binary files /dev/null and b/packages/assets/images/firewall.png differ diff --git a/packages/assets/images/icon/pc_icon_bc.png b/packages/assets/images/icon/pc_icon_bc.png new file mode 100644 index 0000000..06fb1cd Binary files /dev/null and b/packages/assets/images/icon/pc_icon_bc.png differ diff --git a/packages/assets/images/icon/pc_icon_cxx.png b/packages/assets/images/icon/pc_icon_cxx.png new file mode 100644 index 0000000..92ab002 Binary files /dev/null and b/packages/assets/images/icon/pc_icon_cxx.png differ diff --git a/packages/assets/images/icon/pc_icon_fh.png b/packages/assets/images/icon/pc_icon_fh.png new file mode 100644 index 0000000..50f8b3c Binary files /dev/null and b/packages/assets/images/icon/pc_icon_fh.png differ diff --git a/packages/assets/images/icon/pc_icon_sx.png b/packages/assets/images/icon/pc_icon_sx.png new file mode 100644 index 0000000..3800bb8 Binary files /dev/null and b/packages/assets/images/icon/pc_icon_sx.png differ diff --git a/packages/assets/images/icon/pc_icon_xx.png b/packages/assets/images/icon/pc_icon_xx.png new file mode 100644 index 0000000..3109436 Binary files /dev/null and b/packages/assets/images/icon/pc_icon_xx.png differ diff --git a/packages/assets/images/icon/pc_icon_zdbj.png b/packages/assets/images/icon/pc_icon_zdbj.png new file mode 100644 index 0000000..ae42882 Binary files /dev/null and b/packages/assets/images/icon/pc_icon_zdbj.png differ diff --git a/packages/assets/images/server.png b/packages/assets/images/server.png new file mode 100644 index 0000000..0e49219 Binary files /dev/null and b/packages/assets/images/server.png differ diff --git a/packages/assets/logo.png b/packages/assets/logo.png new file mode 100644 index 0000000..fb83843 Binary files /dev/null and b/packages/assets/logo.png differ diff --git a/packages/cc-elements/button.vue b/packages/cc-elements/button.vue new file mode 100644 index 0000000..52d48c6 --- /dev/null +++ b/packages/cc-elements/button.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/packages/cc-elements/checkbox.vue b/packages/cc-elements/checkbox.vue new file mode 100644 index 0000000..aa36f62 --- /dev/null +++ b/packages/cc-elements/checkbox.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/packages/cc-elements/dropdown.vue b/packages/cc-elements/dropdown.vue new file mode 100644 index 0000000..497489e --- /dev/null +++ b/packages/cc-elements/dropdown.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/packages/cc-elements/index.js b/packages/cc-elements/index.js new file mode 100644 index 0000000..ae59602 --- /dev/null +++ b/packages/cc-elements/index.js @@ -0,0 +1,18 @@ +/** + * @author: winyuan + * @data: 2019/11/14 + * @repository: https://github.com/winyuan + * @description: cceditor内部的通用组件 + */ + +import Checkbox from './checkbox' +import Button from './button' +import Dropdown from './dropdown' +import Loading from './loading' + +export { + Checkbox, + Button, + Dropdown, + Loading +} diff --git a/packages/cc-elements/loading.vue b/packages/cc-elements/loading.vue new file mode 100644 index 0000000..f410a92 --- /dev/null +++ b/packages/cc-elements/loading.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/cc-topology/index.js b/packages/cc-topology/index.js new file mode 100644 index 0000000..70e0b68 --- /dev/null +++ b/packages/cc-topology/index.js @@ -0,0 +1,17 @@ +/** + * @author: winyuan + * @data: 2019/08/20 + * @repository: https://github.com/winyuan + * @description: 导入组件,组件必须声明 name + */ + +import CCTopology from './src/cc-topology' + +// 为组件提供 install 安装方法,供按需引入 +CCTopology.install = function(Vue) { + console.info('install----CCEditor: CCTopology----') + Vue.component(CCTopology.name, CCTopology) +} + +// 默认导出组件 +export default CCTopology diff --git a/packages/cc-topology/src/behavior/click-add-edge.js b/packages/cc-topology/src/behavior/click-add-edge.js new file mode 100644 index 0000000..267f299 --- /dev/null +++ b/packages/cc-topology/src/behavior/click-add-edge.js @@ -0,0 +1,96 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: edit mode: 通过先后点击两个节点来添加连线(容易和节点点击动作交叉,已弃用) + */ + +// import G6 from '@antv/g6' +// import theme from '../theme' + +export default { + name: 'click-add-edge', + options: { + getEvents() { + return { + 'node:click': 'onNodeClick', + 'canvas:mousemove': 'onMousemove', + 'edge:click': 'onEdgeClick', // 点击空白处,取消边 + } + }, + onNodeClick(event) { + let graph = this.graph + let node = event.item + let point = { x: event.x, y: event.y } + let model = node.getModel() + let edgeShape = self.currentEdgeShape.guid || 'line' + if (this.addingEdge && this.edge) { + // 点击第二个节点 + graph.updateItem(this.edge, { + target: model.id + }) + this.edge = null + this.addingEdge = false + // 记录【连线】前后的数据状态 + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,连线后没有【重做】功能 + // 重置undoCount,连线后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (self.undoCount > 0) { + self.historyIndex = self.historyIndex - self.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= self.undoCount; i++) { + let key = `graph_history_${self.historyIndex + i}` + self.removeHistoryData(key) + } + self.undoCount = 0 + } else { + // 正常顺序执行的情况,记录【连线】前的数据状态 + let key = `graph_history_${self.historyIndex}` + self.addHistoryData(key, this.historyData) + } + // 记录【连线】后的数据状态 + self.historyIndex += 1 + let key = `graph_history_${self.historyIndex}` + let currentData = JSON.stringify(graph.save()) + self.addHistoryData(key, currentData) + } + } else { + // 点击第一个节点 + this.historyData = JSON.stringify(graph.save()) + if (edgeShape === 'stepline') { + this.edge = graph.addItem('edge', { + source: model.id, + target: point, + type: edgeShape, + controlPoints: [{ x: 100, y: 70 }] + }) + } else { + this.edge = graph.addItem('edge', { + source: model.id, + target: point, + type: edgeShape + }) + } + this.addingEdge = true + } + }, + onMousemove(event) { + const point = { x: event.x, y: event.y } + if (this.addingEdge && this.edge) { + this.graph.updateItem(this.edge, { + target: point + }) + } + }, + onEdgeClick(ev) { + let graph = this.graph + const currentEdge = ev.item + // 拖拽过程中,点击会点击到新增的边上 + if (this.addingEdge && this.edge === currentEdge) { + graph.removeItem(this.edge) + this.edge = null + this.addingEdge = false + } + } + } +} diff --git a/packages/cc-topology/src/behavior/click-event-edit.js b/packages/cc-topology/src/behavior/click-event-edit.js new file mode 100644 index 0000000..3af735e --- /dev/null +++ b/packages/cc-topology/src/behavior/click-event-edit.js @@ -0,0 +1,161 @@ +/** + * @author: winyuan + * @data: 2019/07/16 + * @repository: https://github.com/winyuan + * @description: edit mode: 鼠标点击交互 + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'click-event-edit', + options: { + getEvents() { + return { + 'node:click': 'onNodeClick', + 'node:contextmenu': 'onNodeRightClick', + 'edge:click': 'onEdgeClick', + 'edge:contextmenu': 'onEdgeRightClick', + 'canvas:click': 'onCanvasClick' + } + }, + onNodeClick(event) { + // todo..."selected"是g6自带的状态 ,在"drag-add-edge"中的"node:mouseup"事件也会触发,故此处不需要设置"selected"状态 + //点击事件 + // let clickNode = event.item; + // clickNode.setState('selected', !clickNode.hasState('selected')); + vm.currentFocus = 'node' + vm.rightMenuShow = false + this.updateVmData(event) + }, + onNodeRightClick(event) { + console.log(event,'88888888888888888888888888888') + let graph = vm.graph + let clickNode = event.item + let clickNodeModel = clickNode.getModel() + let selectedNodes = graph.findAllByState('node', 'selected') + let selectedNodeIds = selectedNodes.map(item => {return item.getModel().id}) + vm.selectedNode = clickNode + // 如果当前点击节点是之前选中的某个节点,就进行下面的处理 + if (selectedNodes.length > 1 && selectedNodeIds.indexOf(clickNodeModel.id) > -1) { + vm.rightMenuShow = true + let rightMenu = vm.$refs.rightMenu + rightMenu.style.left = event.clientX + 'px' + rightMenu.style.top = event.clientY + 'px' + } else { + // 隐藏右键菜单 + vm.rightMenuShow = false + // 先取消所有节点的选中状态 + selectedNodes.forEach(node => { + node.setState('selected', false) + }) + // 再添加该节点的选中状态 + clickNode.setState('selected', true) + vm.currentFocus = 'node' + this.updateVmData(event) + } + graph.paint() + }, + onEdgeClick(event) { + // todo + let clickEdge = event.item + // // todo 入口 + clickEdge.setState('selected', !clickEdge.hasState('selected')) + + + this.onEdgeRightClick(event) + + + vm.currentFocus = 'edge' + vm.rightMenuShow = false + this.updateVmData(event) + }, + onEdgeRightClick(event) { + let graph = vm.graph + let clickEdge = event.item + let clickEdgeModel = clickEdge.getModel() + let selectedEdges = graph.findAllByState('edge', 'selected') + // 如果当前点击节点不是之前选中的单个节点,才进行下面的处理 + if (!(selectedEdges.length === 1 && clickEdgeModel.id === selectedEdges[0].getModel().id)) { + // 先取消所有节点的选中状态 + selectedEdges.forEach(edge => { + edge.setState('selected', false) + }) + // 再添加该节点的选中状态 + clickEdge.setState('selected', true) + vm.currentFocus = 'edge' + this.updateVmData(event) + } + // eslint-disable-next-line no-unused-vars + let point = { x: event.x, y: event.y } + }, + onCanvasClick() { + vm.currentFocus = 'canvas' + vm.rightMenuShow = false + }, + updateVmData(event) { + if (event.item._cfg.type === 'node') { + // 更新vm的data: selectedNode 和 selectedNodeParams + //TODO 修改右侧输入信息 + let clickNode = event.item + if (clickNode.hasState('selected')) { + console.log(clickNode.getModel()) + + let clickNodeModel = clickNode.getModel() + vm.selectedNode = clickNode + //todo 修改右侧输入信息 + let nodeAppConfig = { ...vm.nodeAppConfig } + + + // console.log(nodeAppConfig,'nodeAppConfig') + + + + Object.keys(nodeAppConfig).forEach(function(key) { + nodeAppConfig[key] = '' + }) + let uuids =[] + for (let i = 0;i { + vm = _this +} + +import G6 from '@antv/g6' +import theme from '../theme' + +export default { + sendThis, // 暴露函数 + name: 'drag-add-edge', + options: { + getEvents() { + return { + 'node:mousedown': 'onNodeMousedown', + 'node:mouseup': 'onNodeMouseup', + 'edge:mouseup': 'onEdgeMouseup', + 'mousemove': 'onMousemove' + } + }, + onNodeMousedown(event) { + let self = this + // 交互过程中的信息 + self.evtInfo = { + action: null, + node: event.item, + target: event.target + } + if (self.evtInfo.target && self.evtInfo.target.attrs.name) { + // todo...未来可能针对锚点增加其它功能(例如拖拽调整大小) + switch (self.evtInfo.target.attrs.name) { + case 'anchor': + self.evtInfo.action = 'drawEdge' + break + } + } + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].start.call(self, event) + } + }, + onNodeMouseup(event) { + let self = this + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].stop.call(self, event) + } + }, + onEdgeMouseup(event) { + let self = this + if (self.evtInfo && self.evtInfo.action === 'drawEdge') { + self[self.evtInfo.action].stop.call(self, event) + } + }, + onMousemove(event) { + let self = this + if (self.evtInfo && self.evtInfo.action) { + self[self.evtInfo.action].move.call(self, event) + } + }, + drawEdge: { + isMoving: false, + currentLine: null, + start: function(event) { + let self = this + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + + // ************** 暂存【连线】前的数据状态 start ************** + let graph = vm.graph + self.historyData = JSON.stringify(graph.save()) + // ************** 暂存【连线】前的数据状态 end ************** + + let sourceAnchor + let sourceNodeModel = self.evtInfo.node.getModel() + // 锚点数据 + let anchorPoints = self.evtInfo.node.getAnchorPoints() + // 处理线条目标点 + if (anchorPoints && anchorPoints.length) { + // 获取距离指定坐标最近的一个锚点 + sourceAnchor = self.evtInfo.node.getLinkPoint({ + x: event.x, + y: event.y + }) + } + if (self.graph.$C.edge.type=='dottedline'){ + console.log(1) + self.drawEdge.currentLine = self.graph.addItem('edge', { + // id: G6.Util.uniqueId(), // 这种生成id的方式有bug,会重复 + id: utils.generateUUID(), + // 起始节点 + source: sourceNodeModel.id, + sourceAnchor: sourceAnchor ? sourceAnchor.anchorIndex : '', + // 终止节点/位置 + target: { + x: event.x, + y: event.y + }, + type: self.graph.$C.edge.type || 'solidline', + //todo 修改类型 + // style:null + style: G6.Util.mix({}, themeStyle.edgeStyle.dottedline, self.graph.$C.edge.style) + }) + }else if (self.graph.$C.edge.type=='solidline'){ + console.log(2) + self.drawEdge.currentLine = self.graph.addItem('edge', { + // id: G6.Util.uniqueId(), // 这种生成id的方式有bug,会重复 + id: utils.generateUUID(), + // 起始节点 + source: sourceNodeModel.id, + sourceAnchor: sourceAnchor ? sourceAnchor.anchorIndex : '', + // 终止节点/位置 + target: { + x: event.x, + y: event.y + }, + type: self.graph.$C.edge.type || 'solidline', + //todo 修改类型 + // style:null + style: G6.Util.mix({}, themeStyle.edgeStyle.solidline, self.graph.$C.edge.style) + }) + }else if (self.graph.$C.edge.type=='crudedottedline'){ + console.log(3) + + self.drawEdge.currentLine = self.graph.addItem('edge', { + // id: G6.Util.uniqueId(), // 这种生成id的方式有bug,会重复 + id: utils.generateUUID(), + // 起始节点 + source: sourceNodeModel.id, + sourceAnchor: sourceAnchor ? sourceAnchor.anchorIndex : '', + // 终止节点/位置 + target: { + x: event.x, + y: event.y + }, + type: self.graph.$C.edge.type || 'solidline', + //todo 修改类型 + // style:null + style: G6.Util.mix({}, themeStyle.edgeStyle.crudedottedline, self.graph.$C.edge.style) + }) + } + + // console.log(self.drawEdge) + console.log(self.graph.$C.edge.type) + // self.drawEdge.currentLine.style = G6.Util.mix({}, themeStyle.edgeStyle.dottedline, self.graph.$C.edge.style) + self.drawEdge.isMoving = true + }, + move(event) { + let self = this + if (self.drawEdge.isMoving && self.drawEdge.currentLine) { + self.graph.updateItem(self.drawEdge.currentLine, { + target: { + x: event.x, + y: event.y + } + }) + } + }, + stop(event) { + let self = this + if (self.drawEdge.isMoving) { + if (self.drawEdge.currentLine === event.item) { + // 画线过程中点击则移除当前画线 + self.graph.removeItem(event.item) + } else { + let targetNode = event.item + let targetNodeModel = targetNode.getModel() + let targetAnchor = null + // 锚点数据 + let anchorPoints = targetNode.getAnchorPoints() + // 处理线条目标点 + if (anchorPoints && anchorPoints.length) { + // 获取距离指定坐标最近的一个锚点 + targetAnchor = targetNode.getLinkPoint({ x: event.x, y: event.y }) + } + self.graph.updateItem(self.drawEdge.currentLine, { + target: targetNodeModel.id, + targetAnchor: targetAnchor ? targetAnchor.anchorIndex : '' + }) + + // ************** 记录historyData的逻辑 start ************** + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,拖拽节点后没有【重做】功能 + // 重置undoCount,拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } else { + // 正常顺序执行的情况,记录拖拽前的数据状态 + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, this.historyData) + } + // 记录拖拽后的数据状态 + vm.historyIndex += 1 + let key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + } + // ************** 记录historyData的逻辑 end ************** + } + } + self.drawEdge.currentLine = null + self.drawEdge.isMoving = false + self.evtInfo = null + } + } + } +} diff --git a/packages/cc-topology/src/behavior/drag-event-edit.js b/packages/cc-topology/src/behavior/drag-event-edit.js new file mode 100644 index 0000000..eb77d22 --- /dev/null +++ b/packages/cc-topology/src/behavior/drag-event-edit.js @@ -0,0 +1,54 @@ +/** + * @author: winyuan + * @data: 2019/07/16 + * @repository: https://github.com/winyuan + * @description: edit mode: 鼠标拖动节点的交互(记录拖拽前后的数据,用于【撤销】和【重做】) + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'drag-event-edit', + options: { + getEvents() { + return { + 'node:dragstart': 'onNodeDragstart', + 'node:dragend': 'onNodeDragend' + } + }, + onNodeDragstart() { + let graph = vm.graph + this.historyData = JSON.stringify(graph.save()) + }, + onNodeDragend() { + if (this.historyData) { + let graph = this.graph + // 如果当前点过【撤销】了,拖拽节点后没有【重做】功能 + // 重置undoCount,拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } else { + // 正常顺序执行的情况,记录拖拽前的数据状态 + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, this.historyData) + } + // 记录拖拽后的数据状态 + vm.historyIndex += 1 + let key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + } + } + } +} diff --git a/packages/cc-topology/src/behavior/hover-event-edit.js b/packages/cc-topology/src/behavior/hover-event-edit.js new file mode 100644 index 0000000..9167b72 --- /dev/null +++ b/packages/cc-topology/src/behavior/hover-event-edit.js @@ -0,0 +1,26 @@ +/** + * @author: winyuan + * @data: 2019/07/16 + * @repository: https://github.com/winyuan + * @description: edit mode: 悬浮交互 + */ + +export default { + name: 'hover-event-edit', + options: { + getEvents() { + return { + 'node:mouseover': 'onNodeHover', + 'node:mouseout': 'onNodeOut' + } + }, + onNodeHover(event) { + let hoverNode = event.item + hoverNode.setState('hover', true) + }, + onNodeOut(event) { + let hoverNode = event.item + hoverNode.setState('hover', false) + } + } +} diff --git a/packages/cc-topology/src/behavior/index.js b/packages/cc-topology/src/behavior/index.js new file mode 100644 index 0000000..2ed8c8c --- /dev/null +++ b/packages/cc-topology/src/behavior/index.js @@ -0,0 +1,29 @@ +/** + * @author: winyuan + * @data: 2019/07/16 + * @repository: https://github.com/winyuan + * @description: register behaviors + */ + +import dragAddEdge from './drag-add-edge' +import hoverEventEdit from './hover-event-edit' +import clickEventEdit from './click-event-edit' +import dragEventEdit from './drag-event-edit' +import keyupEventEdit from './keyup-event-edit' + +const obj = { + dragAddEdge, + hoverEventEdit, + clickEventEdit, + dragEventEdit, + keyupEventEdit, +} + +export default { + obj, + register(G6) { + Object.values(obj).map(item => { + G6.registerBehavior(item.name, item.options) + }) + } +} diff --git a/packages/cc-topology/src/behavior/keyup-event-edit.js b/packages/cc-topology/src/behavior/keyup-event-edit.js new file mode 100644 index 0000000..7867acc --- /dev/null +++ b/packages/cc-topology/src/behavior/keyup-event-edit.js @@ -0,0 +1,64 @@ +/** + * @author: winyuan + * @data: 2019/07/16 + * @repository: https://github.com/winyuan + * @description: edit mode: 键盘事件的交互,主要是删除节点和连线(记录删除前后的数据,用于【撤销】和【重做】) + */ + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, // 暴露函数 + name: 'keyup-event-edit', + options: { + getEvents() { + return { + 'keyup': 'onKeyup' + } + }, + onKeyup(event) { + let graph = this.graph + let selectedNodes = graph.findAllByState('node', 'selected') + let selectedEdges = graph.findAllByState('edge', 'selected') + if (event.keyCode === 46 && (selectedNodes.length > 0 || selectedEdges.length > 0)) { + + // ************** 记录【删除】前的数据状态 start ************** + let historyData = JSON.stringify(graph.save()) + let key = `graph_history_${vm.historyIndex}` + vm.addHistoryData(key, historyData) + // ************** 记录【删除】前的数据状态 end ************** + + // 开始删除 + for (let i = 0; i < selectedNodes.length; i++) { + graph.removeItem(selectedNodes[i]) + } + for (let i = 0; i < selectedEdges.length; i++) { + graph.removeItem(selectedEdges[i]) + } + + // ************** 记录【删除】后的数据状态 start ************** + // 如果当前点过【撤销】了,拖拽节点后将取消【重做】功能 + // 重置undoCount,【删除】后的数据状态给(当前所在historyIndex + 1),且清空这个时间点之后的记录 + if (vm.undoCount > 0) { + vm.historyIndex = vm.historyIndex - vm.undoCount // 此时的historyIndex应当更新为【撤销】后所在的索引位置 + for (let i = 1; i <= vm.undoCount; i++) { + let key = `graph_history_${vm.historyIndex + i}` + vm.removeHistoryData(key) + } + vm.undoCount = 0 + } + // 记录【删除】后的数据状态 + vm.historyIndex += 1 + key = `graph_history_${vm.historyIndex}` + let currentData = JSON.stringify(graph.save()) + vm.addHistoryData(key, currentData) + // ************** 记录【删除】后的数据状态 end ************** + } + } + } +} diff --git a/packages/cc-topology/src/cc-topology.vue b/packages/cc-topology/src/cc-topology.vue new file mode 100644 index 0000000..4324c65 --- /dev/null +++ b/packages/cc-topology/src/cc-topology.vue @@ -0,0 +1,1455 @@ + + + + + + diff --git a/packages/cc-topology/src/config/edge.js b/packages/cc-topology/src/config/edge.js new file mode 100644 index 0000000..658b4ce --- /dev/null +++ b/packages/cc-topology/src/config/edge.js @@ -0,0 +1,18 @@ +/** + * @author: winyuan + * @data: 2019/08/16 + * @repository: https://github.com/winyuan + * @description: 线条的后期设置 + */ +//配置是否有箭头 +import G6 from '@antv/g6' +export default { + type: 'solidline', + style: { + startArrow: false, + endArrow: { + path: G6.Arrow.vee(10, 20, 15), + d: 25 + } + } +} diff --git a/packages/cc-topology/src/config/index.js b/packages/cc-topology/src/config/index.js new file mode 100644 index 0000000..6af1d9a --- /dev/null +++ b/packages/cc-topology/src/config/index.js @@ -0,0 +1,12 @@ +/** + * @author: winyuan + * @data: 2019/08/16 + * @repository: https://github.com/winyuan + * @description: 配置 + */ + +import edge from './edge' + +export default { + edge +} diff --git a/packages/cc-topology/src/edge/base.js b/packages/cc-topology/src/edge/base.js new file mode 100644 index 0000000..4a10aea --- /dev/null +++ b/packages/cc-topology/src/edge/base.js @@ -0,0 +1,30 @@ +/** + * @author: winyuan + * @data: 2019/07/18 + * @repository: https://github.com/winyuan + * @description: 线公共方法 + */ + +import utils from '../utils' + +export default { + draw(cfg, group) { + const { startPoint, endPoint } = cfg + const keyShape = group.addShape('path', { + className: 'edge-shape', + attrs: { + ...cfg.style, + path: [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + name: 'edge-shape' + }) + return keyShape + }, + setState(name, value, item) { + // 设置边状态 + utils.edge.setState(name, value, item) + } +} diff --git a/packages/cc-topology/src/edge/cc-cubic.js b/packages/cc-topology/src/edge/cc-cubic.js new file mode 100644 index 0000000..306b6d1 --- /dev/null +++ b/packages/cc-topology/src/edge/cc-cubic.js @@ -0,0 +1,16 @@ +/** + * @author: winyuan + * @data: 2019/07/18 + * @repository: https://github.com/winyuan + * @description: 曲线 + */ + +import base from './base' + +export default { + name: 'cc-cubic', + extendName: 'cubic', + options: { + ...base + } +} diff --git a/packages/cc-topology/src/edge/crudedottedline.js b/packages/cc-topology/src/edge/crudedottedline.js new file mode 100644 index 0000000..4250df0 --- /dev/null +++ b/packages/cc-topology/src/edge/crudedottedline.js @@ -0,0 +1,53 @@ +/** + * @author: winyuan + * @data: 2019/10/22 + * @repository: https://github.com/winyuan + * @description: 折线 + */ + +import base from './base' +import theme from '../theme' + +/** + * fix: 继承 polyline 在 G6 3.x 里面有bug + * 现实现方法参考 https://g6.antv.vision/zh/examples/shape/customEdge#customPolyline + */ +export default { + name: 'crudedottedline', + extendName: 'line', + options: { + ...base, + getPath(points) { + const startPoint = points[0] + const endPoint = points[1] + return [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, endPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + getShapeStyle(cfg) { + const { startPoint, endPoint } = cfg + const controlPoints = this.getControlPoints(cfg) + let points = [startPoint] // 添加起始点 + // 添加控制点 + if (controlPoints) { + points = points.concat(controlPoints) + } + // 添加结束点 + points.push(endPoint) + const path = this.getPath(points) + const themeStyle = theme.darkStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + stroke: '#BBB', + lineWidth: 1, + path, + startArrow: false, + endArrow: true, + ...themeStyle.edgeStyle.default + } + return style + } + } +} diff --git a/packages/cc-topology/src/edge/dottedline.js b/packages/cc-topology/src/edge/dottedline.js new file mode 100644 index 0000000..2576283 --- /dev/null +++ b/packages/cc-topology/src/edge/dottedline.js @@ -0,0 +1,53 @@ +/** + * @author: winyuan + * @data: 2019/10/22 + * @repository: https://github.com/winyuan + * @description: 折线 + */ + +import base from './base' +import theme from '../theme' + +/** + * fix: 继承 polyline 在 G6 3.x 里面有bug + * 现实现方法参考 https://g6.antv.vision/zh/examples/shape/customEdge#customPolyline + */ +export default { + name: 'dottedline', + extendName: 'line', + options: { + ...base, + getPath(points) { + const startPoint = points[0] + const endPoint = points[1] + return [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, endPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + getShapeStyle(cfg) { + const { startPoint, endPoint } = cfg + const controlPoints = this.getControlPoints(cfg) + let points = [startPoint] // 添加起始点 + // 添加控制点 + if (controlPoints) { + points = points.concat(controlPoints) + } + // 添加结束点 + points.push(endPoint) + const path = this.getPath(points) + const themeStyle = theme.darkStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + stroke: '#BBB', + lineWidth: 1, + path, + startArrow: false, + endArrow: true, + ...themeStyle.edgeStyle.default + } + return style + } + } +} diff --git a/packages/cc-topology/src/edge/index.js b/packages/cc-topology/src/edge/index.js new file mode 100644 index 0000000..0ea5ba3 --- /dev/null +++ b/packages/cc-topology/src/edge/index.js @@ -0,0 +1,24 @@ +/** + * @author: winyuan + * @data: 2019/07/18 + * @repository: https://github.com/winyuan + * @description: register edges + */ + +import ccCubic from './cc-cubic' +import dottedline from './dottedline' +import solidline from './solidline' +import crudedottedline from './crudedottedline' + +const obj = { + ccCubic, + dottedline, + solidline, + crudedottedline +} + +export default function(G6) { + Object.values(obj).map(item => { + G6.registerEdge(item.name, item.options, item.extendName) + }) +} diff --git a/packages/cc-topology/src/edge/polyline-finding.js b/packages/cc-topology/src/edge/polyline-finding.js new file mode 100644 index 0000000..39531fc --- /dev/null +++ b/packages/cc-topology/src/edge/polyline-finding.js @@ -0,0 +1,275 @@ +/** + * 带圆角折线连线的策略 + * 文档:https://www.yuque.com/antv/blog/eyi70n + * 参考:https://github.com/guozhaolong/wfd/blob/master/src/item/edge.js + * 引用:https://github.com/OXOYO/X-Flowchart-Vue/blob/master/src/global/lib/g6/edge/polylineFinding.js + */ + +// 折线寻径 +export const polylineFinding = function(sNode, tNode, sPort, tPort, offset = 10) { + const sourceBBox = sNode && sNode.getBBox ? sNode.getBBox() : getPointBBox(sPort) + const targetBBox = tNode && tNode.getBBox ? tNode.getBBox() : getPointBBox(tPort) + // 获取节点带 offset 的区域(扩展区域) + const sBBox = getExpandedBBox(sourceBBox, offset) + const tBBox = getExpandedBBox(targetBBox, offset) + // 获取扩展区域上的起始和终止连接点 + const sPoint = getExpandedPort(sBBox, sPort) + const tPoint = getExpandedPort(tBBox, tPort) + // 获取合法折点集 + let points = getConnectablePoints(sBBox, tBBox, sPoint, tPoint) + // 过滤合法点集,预处理、剪枝等 + filterConnectablePoints(points, sBBox) + // 过滤合法点集,预处理、剪枝等 + filterConnectablePoints(points, tBBox) + // 用 A-Star 算法寻径 + let polylinePoints = AStar(points, sPoint, tPoint, sBBox, tBBox) + return polylinePoints +} + +const getPointBBox = function(t) { + return { + centerX: t.x, + centerY: t.y, + minX: t.x, + minY: t.y, + maxX: t.x, + maxY: t.y, + height: 0, + width: 0 + } +} + +// 获取扩展区域 +const getExpandedBBox = function(bbox, offset) { + if (bbox.width === 0 && bbox.height === 0) { + return bbox + } + return { + centerX: bbox.centerX, + centerY: bbox.centerY, + minX: bbox.minX - offset, + minY: bbox.minY - offset, + maxX: bbox.maxX + offset, + maxY: bbox.maxY + offset, + height: bbox.height + 2 * offset, + width: bbox.width + 2 * offset + } +} + +// 获取扩展区域上的连接点 +const getExpandedPort = function(bbox, point) { + // 判断连接点在上下左右哪个区域,相应地给x或y加上或者减去offset + if (Math.abs(point.x - bbox.centerX) / bbox.width > Math.abs(point.y - bbox.centerY) / bbox.height) { + return { + x: point.x > bbox.centerX ? bbox.maxX : bbox.minX, + y: point.y + } + } + return { + x: point.x, + y: point.y > bbox.centerY ? bbox.maxY : bbox.minY + } +} + +// 获取合法折点集合 +const getConnectablePoints = function(sBBox, tBBox, sPoint, tPoint) { + let lineBBox = getBBoxFromVertexes(sPoint, tPoint) + let outerBBox = combineBBoxes(sBBox, tBBox) + let sLineBBox = combineBBoxes(sBBox, lineBBox) + let tLineBBox = combineBBoxes(tBBox, lineBBox) + let points = [ + ...vertexOfBBox(sLineBBox), + ...vertexOfBBox(tLineBBox), + ...vertexOfBBox(outerBBox) + ] + const centerPoint = { x: outerBBox.centerX, y: outerBBox.centerY } + let bboxes = [outerBBox, sLineBBox, tLineBBox, lineBBox] + bboxes.forEach(bbox => { + // 包含 bbox 延长线和线段的相交线 + points = [ + ...points, + ...crossPointsByLineAndBBox(bbox, centerPoint) + ] + }) + points.push({ x: sPoint.x, y: tPoint.y }) + points.push({ x: tPoint.x, y: sPoint.y }) + return points +} + +const getBBoxFromVertexes = function(sPoint, tPoint) { + const minX = Math.min(sPoint.x, tPoint.x) + const maxX = Math.max(sPoint.x, tPoint.x) + const minY = Math.min(sPoint.y, tPoint.y) + const maxY = Math.max(sPoint.y, tPoint.y) + + return { + centerX: (minX + maxX) / 2, + centerY: (minY + maxY) / 2, + maxX: maxX, + maxY: maxY, + minX: minX, + minY: minY, + height: maxY - minY, + width: maxX - minX + } +} + +const combineBBoxes = function(sBBox, tBBox) { + const minX = Math.min(sBBox.minX, tBBox.minX) + const minY = Math.min(sBBox.minY, tBBox.minY) + const maxX = Math.max(sBBox.maxX, tBBox.maxX) + const maxY = Math.max(sBBox.maxY, tBBox.maxY) + + return { + centerX: (minX + maxX) / 2, + centerY: (minY + maxY) / 2, + minX: minX, + minY: minY, + maxX: maxX, + maxY: maxY, + height: maxY - minY, + width: maxX - minX + } +} + +const vertexOfBBox = function(bbox) { + return [ + { x: bbox.minX, y: bbox.minY }, + { x: bbox.maxX, y: bbox.minY }, + { x: bbox.maxX, y: bbox.maxY }, + { x: bbox.minX, y: bbox.maxY } + ] +} + +const crossPointsByLineAndBBox = function(bbox, centerPoint) { + let crossPoints = [] + if (!(centerPoint.x < bbox.minX || centerPoint.x > bbox.maxX)) { + crossPoints = [ + ...crossPoints, + { x: centerPoint.x, y: bbox.minY }, + { x: centerPoint.x, y: bbox.maxY } + ] + } + if (!(centerPoint.y < bbox.minY || centerPoint.y > bbox.maxY)) { + crossPoints = [ + ...crossPoints, + { x: bbox.minX, y: centerPoint.y }, + { x: bbox.maxX, y: centerPoint.y } + ] + } + + return crossPoints +} + +// 过滤连接点 +const filterConnectablePoints = function(points, bbox) { + return points.filter(point => { + return point.x <= bbox.minX || point.x >= bbox.maxX || point.y <= bbox.minY || point.y >= bbox.maxY + }) +} + +const crossBBox = function(bboxes, p1, p2) { + for (let i = 0; i < bboxes.length; i++) { + const bbox = bboxes[i] + if (p1.x === p2.x && bbox.minX < p1.x && bbox.maxX > p1.x) { + if ((p1.y < bbox.maxY && p2.y >= bbox.maxY) || (p2.y < bbox.maxY && p1.y >= bbox.maxY)) { + return true + } + } else if (p1.y === p2.y && bbox.minY < p1.y && bbox.maxY > p1.y) { + if ((p1.x < bbox.maxX && p2.x >= bbox.maxX) || (p2.x < bbox.maxX && p1.x >= bbox.maxX)) { + return true + } + } + } + return false +} + +const getCost = function(p1, p2) { + return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) +} + +// aStar 寻径 +const AStar = function(points, sPoint, tPoint, sBBox, tBBox) { + const openList = [sPoint] + const closeList = [] + points.forEach(item => { + item.id = item.x + '-' + item.y + }) + let tmpArr = [] + points.forEach(item => { + if (!tmpArr.includes(target => target.id === item.id)) { + tmpArr.push(item) + } + }) + points = [ + ...tmpArr, + tPoint + ] + let endPoint + while (openList.length > 0) { + let minCostPoint + openList.forEach((p, i) => { + if (!p.parent) { + p.f = 0 + } + if (!minCostPoint) { + minCostPoint = p + } + if (p.f < minCostPoint.f) { + minCostPoint = p + } + }) + if (minCostPoint.x === tPoint.x && minCostPoint.y === tPoint.y) { + endPoint = minCostPoint + break + } + openList.splice(openList.findIndex(o => o.x === minCostPoint.x && o.y === minCostPoint.y), 1) + closeList.push(minCostPoint) + const neighbor = points.filter(p => { + return (p.x === minCostPoint.x || p.y === minCostPoint.y) && + !(p.x === minCostPoint.x && p.y === minCostPoint.y) && + !crossBBox([sBBox, tBBox], minCostPoint, p) + } + ) + neighbor.forEach(p => { + const inOpen = openList.find(o => o.x === p.x && o.y === p.y) + const currentG = getCost(p, minCostPoint) + if (!closeList.find(o => o.x === p.x && o.y === p.y)) { + if (inOpen) { + if (p.g > currentG) { + p.parent = minCostPoint + p.g = currentG + p.f = p.g + p.h + } + } else { + p.parent = minCostPoint + p.g = currentG + let h = getCost(p, tPoint) + if (crossBBox([tBBox], p, tPoint)) { + // 如果穿过bbox则增加该点的预估代价为bbox周长的一半 + h += (tBBox.width / 2 + tBBox.height / 2) + } + p.h = h + p.f = p.g + p.h + openList.push(p) + } + } + }) + } + if (endPoint) { + const result = [] + result.push({ + x: endPoint.x, + y: endPoint.y + }) + while (endPoint.parent) { + endPoint = endPoint.parent + result.push({ + x: endPoint.x, + y: endPoint.y + }) + } + return result.reverse() + } + return [] +} diff --git a/packages/cc-topology/src/edge/solidline.js b/packages/cc-topology/src/edge/solidline.js new file mode 100644 index 0000000..ed4bd1a --- /dev/null +++ b/packages/cc-topology/src/edge/solidline.js @@ -0,0 +1,53 @@ +/** + * @author: winyuan + * @data: 2019/10/22 + * @repository: https://github.com/winyuan + * @description: 折线 + */ + +import base from './base' +import theme from '../theme' + +/** + * fix: 继承 polyline 在 G6 3.x 里面有bug + * 现实现方法参考 https://g6.antv.vision/zh/examples/shape/customEdge#customPolyline + */ +export default { + name: 'solidline', + extendName: 'line', + options: { + ...base, + getPath(points) { + const startPoint = points[0] + const endPoint = points[1] + return [ + ['M', startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, startPoint.y], + ['L', endPoint.x / 3 + 2 / 3 * startPoint.x, endPoint.y], + ['L', endPoint.x, endPoint.y] + ] + }, + getShapeStyle(cfg) { + const { startPoint, endPoint } = cfg + const controlPoints = this.getControlPoints(cfg) + let points = [startPoint] // 添加起始点 + // 添加控制点 + if (controlPoints) { + points = points.concat(controlPoints) + } + // 添加结束点 + points.push(endPoint) + const path = this.getPath(points) + const themeStyle = theme.darkStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + stroke: '#BBB', + lineWidth: 1, + path, + startArrow: false, + endArrow: true, + ...themeStyle.edgeStyle.default + } + return style + } + } +} diff --git a/packages/cc-topology/src/graph/index.js b/packages/cc-topology/src/graph/index.js new file mode 100644 index 0000000..78c29e8 --- /dev/null +++ b/packages/cc-topology/src/graph/index.js @@ -0,0 +1,160 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: 图的布局方式/图的初始化 + */ + +import d3 from '../plugins/d3-installer' +import theme from '../theme' + +/** + * 图的布局方式/图的初始化 + * @type {{commonGraph: (function(*, *): G6.Graph)}} + */ +const initGraph = { + /** + * 一般布局 + * @param G6 + * @param options + * @returns {G6.Graph} + */ + commonGraph: function(G6, options) { + let graphData = options.graphData + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + // 生成G6实例 + let graph = new G6.Graph({ + plugins: options.plugins, + container: options.container, + width: options.width, + height: options.height, + // layout: { + // type: 'random', + // width: options.width, + // height: options.height + // }, + defaultNode: { + type: 'cc-rect', + labelCfg: { + position: 'bottom' + } + }, + defaultEdge: { + type: 'cc-line', + labelCfg: { + position: 'center', + autoRotate: false + } + }, + nodeStateStyles: themeStyle.nodeStyle, + // nodeStyle: { + // selected: { + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + // } + // }, + edgeStateStyles: themeStyle.edgeStyle, + // edgeStyle: { + // default: { + // stroke: '#e2e2e2', + // lineWidth: 3, + // lineAppendWidth: 10 + // }, + // selected: { + // shadowColor: '#626262', + // shadowBlur: 3 + // } + // }, + modes: options.modes + }) + // 将 read 方法分解成 data() 和 render 方法,便于整个生命周期的管理 + graph.read(graphData) + graph.render() + // 返回G6实例 + return graph + }, + /** + * 力导布局 + * @param G6 + * @param options + * @returns {*} + */ + forceLayoutGraph: function(resolve, G6, options) { + let graphData = options.graphData + let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + // 生成G6实例 + let graph = new G6.Graph({ + container: options.container, + width: options.width, + height: options.height, + nodeStateStyles: themeStyle.nodeStyle, + edgeStateStyles: themeStyle.edgeStyle + }) + // 初始化力导布局 + let simulation = d3 + .forceSimulation() + .force( + 'link', + d3 + .forceLink() + .id(function(d) { + return d.id + }) + .distance(linkDistance) + .strength(0.5) + ) + .force('charge', d3.forceManyBody().strength(-500).distanceMax(500).distanceMin(100)) + .force('center', d3.forceCenter(options.width / 2, options.height / 2)) + // 定义节点数据 + simulation.nodes(graphData.nodes).on('tick', ticked) + // 定义连线数据 + let edges = [] + for (let i = 0; i < graphData.edges.length; i++) { + edges.push({ + id: graphData.edges[i].id, + source: graphData.edges[i].source, + target: graphData.edges[i].target + }) + } + simulation.force('link').links(edges) + graph.data(graphData) + graph.render() + + function linkDistance(d) { + return 150 + } + + function ticked() { + // protect: planA: 移除事件监听器 planB: 手动停止力模拟 + if (graph.destroyed) { + // simulation.nodes(graphData.nodes).on('tick', null) + simulation.stop() + return + } + if (!graph.get('data')) { + // 若是第一次渲染,定义数据,渲染 + graph.data(graphData) + graph.render() + } else { + // 后续渲染,直接刷新所有点和边的位置 + graph.refreshPositions() + } + } + + // 控制时间: 只布局10秒 + let t = setTimeout(function() { + simulation.stop() + resolve(graph) + }, 10000) + + // 判断force-layout结束 + simulation.on('end', () => { + clearTimeout(t) + resolve(graph) + }) + } +} + +export default initGraph diff --git a/packages/cc-topology/src/node/base.js b/packages/cc-topology/src/node/base.js new file mode 100644 index 0000000..92212bb --- /dev/null +++ b/packages/cc-topology/src/node/base.js @@ -0,0 +1,22 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: 节点基础方法 + */ + +import utils from '../utils' + +export default { + setState(name, value, item) { + // 设置节点状态 + utils.node.setState(name, value, item) + // 设置锚点状态 + utils.anchor.setState(name, value, item) + }, + // 绘制后附加锚点 + afterDraw(cfg, group) { + // 绘制锚点 + utils.anchor.draw(cfg, group) + } +} diff --git a/packages/cc-topology/src/node/cc-image.js b/packages/cc-topology/src/node/cc-image.js new file mode 100644 index 0000000..371b9e8 --- /dev/null +++ b/packages/cc-topology/src/node/cc-image.js @@ -0,0 +1,127 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: 图片节点 + */ + +import utils from '../utils' + +// 用来获取调用此js的vue组件实例(this) +let vm = null + +const sendThis = (_this) => { + vm = _this +} + +export default { + sendThis, + name: 'cc-image', + extendName: 'image', + options: { + setState(name, value, item) { + // 设置节点状态 + utils.node.setState(name, value, item) + // 设置锚点状态 + if (vm.graphMode === 'edit') { + utils.anchor.setState(name, value, item) + } + }, + // 绘制后附加锚点 + afterDraw(cfg, group) { + // 绘制锚点 + if (vm.graphMode === 'edit') { + utils.anchor.draw(cfg, group) + } + }, + // 设置告警状态 + afterUpdate(cfg, node) { + const group = node.getContainer() + // 获取children + const halos = group.findAll(function(item) { + return item.attrs.name === 'halo' + }) + // 告警 + if (cfg.appState && cfg.appState.alert) { + if (halos.length > 0) { + return + } + let size = this.getSize(cfg) || [48, 48] + let r = size[0] / 2 + let { id } = cfg + let halo1 = group.addShape('circle', { + id: id + '_halo_' + 1, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', + opacity: 0.6 + }, + name: 'halo', + zIndex: -3 + }) + let halo2 = group.addShape('circle', { + id: id + '_halo_' + 2, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', // 为了显示清晰,随意设置了颜色 + opacity: 0.6 + }, + name: 'halo', + zIndex: -2 + }) + let halo3 = group.addShape('circle', { + id: id + '_halo_' + 3, + attrs: { + name: 'halo', + x: 0, + y: 0, + r: r, + fill: cfg.color || '#F56C6C', + opacity: 0.6 + }, + name: 'halo', + zIndex: -1 + }) + group.sort() // 排序,根据zIndex 排序 + halo1.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1, + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 0 // 无延迟 + }) + halo2.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1 + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 1000 // 1 秒延迟 + }) + halo3.animate({ // 逐渐放大,并消失 + r: r + 10, + opacity: 0.1 + }, { + repeat: true, // 循环 + duration: 3000, + easing: 'easeCubic', + delay: 2000 // 2 秒延迟 + }) + } else { + halos.forEach(halo => { + // FIXME: G6 3.x在底层库遗留了bug,导致removeChild()方法报错,等待解决 + group.removeChild(halo) + }) + } + } + } +} diff --git a/packages/cc-topology/src/node/cc-rect.js b/packages/cc-topology/src/node/cc-rect.js new file mode 100644 index 0000000..270ce18 --- /dev/null +++ b/packages/cc-topology/src/node/cc-rect.js @@ -0,0 +1,31 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: 矩形节点 + */ + +import base from './base' +import theme from '../theme' + +export default { + name: 'cc-rect', + extendName: 'rect', + options: { + ...base, + getShapeStyle(cfg) { + const size = this.getSize(cfg) || [48, 48] + const width = size[0] + const height = size[1] + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + const style = { + x: 0 - width / 2, + y: 0 - height / 2, + width: width, + height: height, + ...themeStyle.nodeStyle.default + } + return style + } + } +} diff --git a/packages/cc-topology/src/node/index.js b/packages/cc-topology/src/node/index.js new file mode 100644 index 0000000..da8366f --- /dev/null +++ b/packages/cc-topology/src/node/index.js @@ -0,0 +1,23 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: register nodes + */ + +import ccRect from './cc-rect' +import ccImage from './cc-image' + +const obj = { + ccRect, + ccImage +} + +export default { + obj, + register(G6) { + Object.values(obj).map(item => { + G6.registerNode(item.name, item.options, item.extendName) + }) + } +} diff --git a/packages/cc-topology/src/plugins/d3-installer.js b/packages/cc-topology/src/plugins/d3-installer.js new file mode 100644 index 0000000..a805344 --- /dev/null +++ b/packages/cc-topology/src/plugins/d3-installer.js @@ -0,0 +1,10 @@ +/** + * @author: winyuan + * @data: 2019/07/05 + * @repository: https://github.com/winyuan + * @description: install 3rd plugins + */ + +import * as d3 from 'd3-force/dist/d3-force' + +export default d3 diff --git a/packages/cc-topology/src/theme/dark-style.js b/packages/cc-topology/src/theme/dark-style.js new file mode 100644 index 0000000..aad3328 --- /dev/null +++ b/packages/cc-topology/src/theme/dark-style.js @@ -0,0 +1,178 @@ +/** + * @author: winyuan + * @data: 2019/11/20 + * @repository: https://github.com/winyuan + * @description: dark style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: 'transparent', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#FFF' + } + }, + // 连线样式 + edgeStyle: { + default: { + stroke: '#53da3a', + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '' + } + }, + // 锚点样式 + anchorStyle: { + default: { + radius: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + radius: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/packages/cc-topology/src/theme/default-style.js b/packages/cc-topology/src/theme/default-style.js new file mode 100644 index 0000000..efbb257 --- /dev/null +++ b/packages/cc-topology/src/theme/default-style.js @@ -0,0 +1,220 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: default style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: 'transparent', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#000' + } + }, + // 连线样式 + edgeStyle: { + default: { + //修改连线颜色 + stroke: 'red', + //todo 边的宽度 + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true, + startArrow: false, + //todo 添加虚线 + // lineDash: [2, 2] + }, + solidline: { + //实线 + stroke: 'blue', + //todo 边的宽度 + lineWidth: 1.5, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true, + startArrow: false, + }, + dottedline: { + //虚线 + stroke: 'blue', + //todo 边的宽度 + lineWidth: 1.5, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true, + startArrow: false, + //todo 添加虚线 + lineDash: [2, 2] + }, + crudedottedline: { + //粗虚线 + stroke: 'blue', + //todo 边的宽度 + lineWidth: 3, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true, + startArrow: false, + //todo 添加虚线 + lineDash: [2, 2] + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '', + shadowBlur: 0, + shadowOffsetX: 0, + shadowOffsetY: 0 + } + }, + // 锚点样式 + anchorStyle: { + default: { + r: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + r: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/packages/cc-topology/src/theme/index.js b/packages/cc-topology/src/theme/index.js new file mode 100644 index 0000000..352020a --- /dev/null +++ b/packages/cc-topology/src/theme/index.js @@ -0,0 +1,16 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: 编辑器主题样式 - 节点、连线的预设样式 + */ + +import defaultStyle from './default-style' +import darkStyle from './dark-style' +import officeStyle from './office-style' + +export default { + defaultStyle, + darkStyle, + officeStyle +} diff --git a/packages/cc-topology/src/theme/office-style.js b/packages/cc-topology/src/theme/office-style.js new file mode 100644 index 0000000..9c08efd --- /dev/null +++ b/packages/cc-topology/src/theme/office-style.js @@ -0,0 +1,178 @@ +/** + * @author: winyuan + * @data: 2019/11/21 + * @repository: https://github.com/winyuan + * @description: office style + */ + +export default { + // 节点样式 + nodeStyle: { + default: { + stroke: '#CED4D9', + fill: '#FFFFFF', + shadowOffsetX: 0, + shadowOffsetY: 4, + shadowBlur: 10, + shadowColor: 'rgba(13, 26, 38, 0.08)', + lineWidth: 1, + radius: 4, + strokeOpacity: 0.7 + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + // shadowColor: '#626262', + // shadowBlur: 8, + // shadowOffsetX: -1, + // shadowOffsetY: 3 + }, + unselected: { + shadowColor: '' + } + }, + // 节点标签样式 + nodeLabelCfg: { + positions: 'center', + style: { + fill: '#000' + } + }, + // 连线样式 + edgeStyle: { + default: { + stroke: '#41c23a', + lineWidth: 2, + strokeOpacity: 0.92, + lineAppendWidth: 10, + endArrow: true + }, + active: { + shadowColor: 'red', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + inactive: { + shadowColor: '' + }, + selected: { + shadowColor: '#ff240b', + shadowBlur: 4, + shadowOffsetX: 0, + shadowOffsetY: 0 + }, + unselected: { + shadowColor: '' + } + }, + // 锚点样式 + anchorStyle: { + default: { + radius: 3, + symbol: 'circle', + fill: '#FFFFFF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 1, + strokeOpacity: 1 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + // 锚点背景样式 + anchorBgStyle: { + default: { + radius: 10, + symbol: 'circle', + fill: '#1890FF', + fillOpacity: 0, + stroke: '#1890FF', + strokeOpacity: 0, + cursor: 'crosshair' + }, + hover: { + fillOpacity: 1, + strokeOpacity: 1 + }, + unhover: { + fillOpacity: 0, + strokeOpacity: 0 + }, + active: { + fillOpacity: 0.3, + strokeOpacity: 0.5 + }, + inactive: { + fillOpacity: 0, + strokeOpacity: 0 + } + }, + + + nodeActivedOutterStyle: { lineWidth: 0 }, + groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 }, + edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 }, + nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' }, + groupActivedStyle: { stroke: '#1890FF' }, + edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' }, + nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 }, + groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 }, + + groupBackgroundPadding: [40, 10, 10, 10], + groupLabelOffsetX: 10, + groupLabelOffsetY: 10, + edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + edgeLabelRectPadding: 4, + edgeLabelRectStyle: { fill: 'white' }, + nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' }, + groupStyle: { stroke: '#CED4D9', radius: 4 }, + groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' }, + multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 }, + dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 }, + dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 }, + anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 }, + anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 }, + anchorHotsoptActivedStyle: { radius: 14 }, + anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' }, + nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' }, + edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 }, + nodeSelectedBoxStyle: { stroke: '#C2C2C2' }, + cursor: { + panningCanvas: '-webkit-grabbing', + beforePanCanvas: '-webkit-grab', + hoverNode: 'move', + hoverEffectiveAnchor: 'crosshair', + hoverEdge: 'default', + hoverGroup: 'move', + hoverUnEffectiveAnchor: 'default', + hoverEdgeControllPoint: 'crosshair', + multiSelect: 'crosshair' + }, + nodeDelegationStyle: { + stroke: '#1890FF', + fill: '#1890FF', + fillOpacity: .08, + lineDash: [4, 4], + radius: 4, + lineWidth: 1 + }, + edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 } +} diff --git a/packages/cc-topology/src/toolbar-edit.vue b/packages/cc-topology/src/toolbar-edit.vue new file mode 100644 index 0000000..1abcbe2 --- /dev/null +++ b/packages/cc-topology/src/toolbar-edit.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/packages/cc-topology/src/utils/anchor/draw.js b/packages/cc-topology/src/utils/anchor/draw.js new file mode 100644 index 0000000..45d167d --- /dev/null +++ b/packages/cc-topology/src/utils/anchor/draw.js @@ -0,0 +1,60 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: draw anchor + */ + +import theme from '../../theme' + +export default function(cfg, group) { + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + let { anchorPoints, width, height, id } = cfg + if (anchorPoints && anchorPoints.length) { + for (let i = 0, len = anchorPoints.length; i < len; i++) { + let [x, y] = anchorPoints[i] + // 计算Marker中心点坐标 + let originX = -width / 2 + let originY = -height / 2 + let anchorX = x * width + originX + let anchorY = y * height + originY + // 添加锚点背景 + let anchorBgShape = group.addShape('marker', { + id: id + '_anchor_bg_' + i, + attrs: { + name: 'anchorBg', + x: anchorX, + y: anchorY, + // 锚点默认样式 + ...themeStyle.anchorBgStyle.default + }, + draggable: false, + name: 'markerBg-shape' + }) + // 添加锚点Marker形状 + let anchorShape = group.addShape('marker', { + id: id + '_anchor_' + i, + attrs: { + name: 'anchor', + x: anchorX, + y: anchorY, + // 锚点默认样式 + ...themeStyle.anchorStyle.default + }, + draggable: false, + name: 'marker-shape' + }) + + anchorShape.on('mouseenter', function() { + anchorBgShape.attr({ + ...themeStyle.anchorBgStyle.active + }) + }) + anchorShape.on('mouseleave', function() { + anchorBgShape.attr({ + ...themeStyle.anchorBgStyle.inactive + }) + }) + } + } +} diff --git a/packages/cc-topology/src/utils/anchor/index.js b/packages/cc-topology/src/utils/anchor/index.js new file mode 100644 index 0000000..6f8f2a2 --- /dev/null +++ b/packages/cc-topology/src/utils/anchor/index.js @@ -0,0 +1,16 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: anchor + */ + +import draw from './draw' +import setState from './set-state' +import update from './update' + +export default { + draw, + setState, + update +} diff --git a/packages/cc-topology/src/utils/anchor/set-state.js b/packages/cc-topology/src/utils/anchor/set-state.js new file mode 100644 index 0000000..6e481d7 --- /dev/null +++ b/packages/cc-topology/src/utils/anchor/set-state.js @@ -0,0 +1,27 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: set anchor state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'hover') { + let group = item.getContainer() + let children = group.get('children') + for (let i = 0, len = children.length; i < len; i++) { + let child = children[i] + // 处理锚点状态 + if (child.attrs.name === 'anchor') { + if (value) { + child.attr(themeStyle.anchorStyle.hover) + } else { + child.attr(themeStyle.anchorStyle.unhover) + } + } + } + } +} diff --git a/packages/cc-topology/src/utils/anchor/update.js b/packages/cc-topology/src/utils/anchor/update.js new file mode 100644 index 0000000..0f43473 --- /dev/null +++ b/packages/cc-topology/src/utils/anchor/update.js @@ -0,0 +1,32 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: update anchor + */ + +export default function(cfg, group) { + let { anchorPoints, width, height, id } = cfg + if (anchorPoints && anchorPoints.length) { + for (let i = 0, len = anchorPoints.length; i < len; i++) { + let [x, y] = anchorPoints[i] + // 计算Marker中心点坐标 + let originX = -width / 2 + let originY = -height / 2 + let anchorX = x * width + originX + let anchorY = y * height + originY + // 锚点背景 + let anchorBgShape = group.findById(id + '_anchor_bg_' + i) + // 锚点 + let anchorShape = group.findById(id + '_anchor_' + i) + anchorBgShape.attr({ + x: anchorX, + y: anchorY + }) + anchorShape.attr({ + x: anchorX, + y: anchorY + }) + } + } +} diff --git a/packages/cc-topology/src/utils/edge/index.js b/packages/cc-topology/src/utils/edge/index.js new file mode 100644 index 0000000..690f906 --- /dev/null +++ b/packages/cc-topology/src/utils/edge/index.js @@ -0,0 +1,12 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: edge + */ + +import setState from './set-state' + +export default { + setState +} diff --git a/packages/cc-topology/src/utils/edge/set-state.js b/packages/cc-topology/src/utils/edge/set-state.js new file mode 100644 index 0000000..3bb60b7 --- /dev/null +++ b/packages/cc-topology/src/utils/edge/set-state.js @@ -0,0 +1,27 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: set edge state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const group = item.getContainer() + const shape = group.get('children')[0] // 顺序根据 draw 时确定 + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'active') { + if (value) { + shape.attr(themeStyle.edgeStyle.active) + } else { + shape.attr(themeStyle.edgeStyle.inactive) + } + } else if (name === 'selected') { + if (value) { + shape.attr(themeStyle.edgeStyle.selected) + } else { + shape.attr(themeStyle.edgeStyle.unselected) + } + } +} diff --git a/packages/cc-topology/src/utils/index.js b/packages/cc-topology/src/utils/index.js new file mode 100644 index 0000000..36fb8f1 --- /dev/null +++ b/packages/cc-topology/src/utils/index.js @@ -0,0 +1,65 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: graph utils + */ + +import node from './node' +import anchor from './anchor' +import edge from './edge' + +/** + * 比较两个对象的内容是否相同(两个对象的键值都相同) + * @param obj1 + * @param obj2 + * @returns {*} + */ +const isObjectValueEqual = function(obj1, obj2) { + let o1 = obj1 instanceof Object + let o2 = obj2 instanceof Object + // 不是对象的情况 + if (!o1 || !o2) { + return obj1 === obj2 + } + // 对象的属性(key值)个数不相等 + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false + } + // 判断每个属性(如果属性值也是对象则需要递归) + for (let attr in obj1) { + let t1 = obj1[attr] instanceof Object + let t2 = obj2[attr] instanceof Object + if (t1 && t2) { + return isObjectValueEqual(obj1[attr], obj2[attr]) + } else if (obj1[attr] !== obj2[attr]) { + return false + } + } + return true +} + + +/** + * 生成uuid算法,碰撞率低于1/2^^122 + * @returns {string} + */ +const generateUUID = function() { + let d = new Date().getTime() + // x 是 0-9 或 a-f 范围内的一个32位十六进制数 + let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + let r = (d + Math.random() * 16) % 16 | 0 + d = Math.floor(d / 16) + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) + }) + return uuid +} + +export default { + node, + anchor, + edge, + // 通用工具类函数 + isObjectValueEqual, + generateUUID +} diff --git a/packages/cc-topology/src/utils/node/index.js b/packages/cc-topology/src/utils/node/index.js new file mode 100644 index 0000000..95fa208 --- /dev/null +++ b/packages/cc-topology/src/utils/node/index.js @@ -0,0 +1,12 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: node + */ + +import setState from './set-state' + +export default { + setState +} diff --git a/packages/cc-topology/src/utils/node/set-state.js b/packages/cc-topology/src/utils/node/set-state.js new file mode 100644 index 0000000..4503f47 --- /dev/null +++ b/packages/cc-topology/src/utils/node/set-state.js @@ -0,0 +1,27 @@ +/** + * @author: winyuan + * @data: 2019/08/15 + * @repository: https://github.com/winyuan + * @description: set node state + */ + +import theme from '../../theme' + +export default function(name, value, item) { + const group = item.getContainer() + const shape = group.get('children')[0] // 顺序根据 draw 时确定 + const themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体 + if (name === 'active') { + if (value) { + shape.attr(themeStyle.nodeStyle.active) + } else { + shape.attr(themeStyle.nodeStyle.inactive) + } + } else if (name === 'selected') { + if (value) { + shape.attr(themeStyle.nodeStyle.selected) + } else { + shape.attr(themeStyle.nodeStyle.default) + } + } +} diff --git a/packages/index.js b/packages/index.js new file mode 100644 index 0000000..c0c4ce4 --- /dev/null +++ b/packages/index.js @@ -0,0 +1,40 @@ +/** + * @author: winyuan + * @data: 2019/08/20 + * @repository: https://github.com/winyuan + * @description: 整合所有的组件,对外导出,即一个完整的组件库 + */ + +import CCTopology from './cc-topology' + +// 存储组件列表 +const components = [ + CCTopology +] + +// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册 +const install = function(Vue) { + // 判断是否安装 + if (install.installed) return + // 遍历注册全局组件 + console.info('install----CCEditor: All----') + components.map(component => Vue.component(component.name, component)) +} + +// 判断是否是直接引入文件 +if (typeof window !== 'undefined' && window.Vue) { + install(window.Vue) +} + +export default { + // 导出的对象必须具有 install,才能被 Vue.use() 方法安装 + install, + // 以下是具体的组件列表 + CCTopology +} + +export { + install, + // 以下是具体的组件列表 + CCTopology +} diff --git a/packages/top/Review.vue b/packages/top/Review.vue new file mode 100644 index 0000000..e4412dd --- /dev/null +++ b/packages/top/Review.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/top/ViewTop.vue b/packages/top/ViewTop.vue new file mode 100644 index 0000000..5378759 --- /dev/null +++ b/packages/top/ViewTop.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/top/index.vue b/packages/top/index.vue new file mode 100644 index 0000000..4b4e2cc --- /dev/null +++ b/packages/top/index.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/top/topology.vue b/packages/top/topology.vue new file mode 100644 index 0000000..0f66c1b --- /dev/null +++ b/packages/top/topology.vue @@ -0,0 +1,176 @@ + + + + + + +