├── .eslintignore ├── .env ├── babel.config.js ├── src ├── views │ ├── flow │ │ ├── styles │ │ │ ├── index.scss │ │ │ ├── variables.scss │ │ │ ├── colors.scss │ │ │ └── mixin.scss │ │ ├── constants │ │ │ ├── index.js │ │ │ ├── view.constants.js │ │ │ └── state.constants.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── info-content.vue │ │ │ ├── tabs │ │ │ │ ├── tab-pane.vue │ │ │ │ └── tabs.vue │ │ │ ├── grid.vue │ │ │ ├── node-settings.vue │ │ │ ├── tarbar.vue │ │ │ ├── info-table.vue │ │ │ ├── palette-node.vue │ │ │ ├── flow-group.vue │ │ │ ├── layout.vue │ │ │ ├── flow-node.vue │ │ │ └── menu.vue │ │ ├── index.vue │ │ ├── sidebar.vue │ │ ├── palette.vue │ │ └── utils.js │ └── 404.vue ├── assets │ ├── images │ │ └── grip.png │ └── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png ├── store │ ├── getters.js │ ├── modules │ │ ├── flow │ │ │ ├── settings.js │ │ │ ├── index.js │ │ │ ├── type.js │ │ │ ├── info.js │ │ │ └── chat.js │ │ ├── settings.js │ │ └── app.js │ └── index.js ├── layout │ ├── components │ │ ├── index.js │ │ ├── Sidebar │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── FixiOSBug.js │ │ │ ├── index.vue │ │ │ ├── Logo.vue │ │ │ └── SidebarItem.vue │ │ ├── AppMain.vue │ │ └── Navbar.vue │ ├── mixin │ │ └── ResizeHandler.js │ └── index.vue ├── App.vue ├── icons │ ├── svg │ │ ├── link.svg │ │ ├── enterGroup.svg │ │ ├── user.svg │ │ ├── info.svg │ │ ├── example.svg │ │ ├── stop.svg │ │ ├── warning.svg │ │ ├── list.svg │ │ ├── copy.svg │ │ ├── table.svg │ │ ├── password.svg │ │ ├── check.svg │ │ ├── disabled.svg │ │ ├── play.svg │ │ ├── paste.svg │ │ ├── nested.svg │ │ ├── eye.svg │ │ ├── arrow-circle-up.svg │ │ ├── exclamation-circle.svg │ │ ├── delete.svg │ │ ├── asterisk.svg │ │ ├── box.svg │ │ ├── eye-open.svg │ │ ├── question.svg │ │ ├── reply.svg │ │ ├── refresh.svg │ │ ├── ungroup.svg │ │ ├── tree.svg │ │ ├── addGroup.svg │ │ ├── dashboard.svg │ │ ├── form.svg │ │ └── settings.svg │ ├── index.js │ └── svgo.yml ├── utils │ ├── get-page-title.js │ ├── auth.js │ ├── validate.js │ ├── request.js │ └── index.js ├── settings.js ├── styles │ ├── mixin.scss │ ├── variables.scss │ ├── element-ui.scss │ ├── transition.scss │ ├── index.scss │ └── sidebar.scss ├── lang │ ├── zh.js │ ├── en.js │ └── index.js ├── components │ ├── SvgIcon │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ └── Breadcrumb │ │ └── index.vue ├── main.js ├── permission.js ├── router │ └── index.js └── api │ ├── flow.js │ └── flow.mock.js ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Hamburger.spec.js │ ├── SvgIcon.spec.js │ └── Breadcrumb.spec.js │ └── utils │ ├── validate.spec.js │ ├── parseTime.spec.js │ └── formatTime.spec.js ├── public ├── favicon.ico ├── icons │ └── node-red │ │ ├── db.png │ │ ├── sort.png │ │ ├── folder.png │ │ ├── inject.png │ │ ├── redis.png │ │ ├── split.png │ │ ├── parser-csv.png │ │ ├── parser-json.png │ │ ├── parser-xml.png │ │ └── parser-yaml.png └── index.html ├── .travis.yml ├── .env.production ├── .env.staging ├── .postcssrc.js ├── .gitignore ├── .editorconfig ├── .env.development ├── jest.config.js ├── LICENSE ├── mock ├── index.js └── mock-server.js ├── README.md ├── package.json ├── vue.config.js └── .eslintrc.js /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=zh 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/views/flow/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/grip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/src/assets/images/grip.png -------------------------------------------------------------------------------- /public/icons/node-red/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/db.png -------------------------------------------------------------------------------- /public/icons/node-red/sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/sort.png -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/src/assets/404_images/404.png -------------------------------------------------------------------------------- /public/icons/node-red/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/folder.png -------------------------------------------------------------------------------- /public/icons/node-red/inject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/inject.png -------------------------------------------------------------------------------- /public/icons/node-red/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/redis.png -------------------------------------------------------------------------------- /public/icons/node-red/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/split.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /public/icons/node-red/parser-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/parser-csv.png -------------------------------------------------------------------------------- /public/icons/node-red/parser-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/parser-json.png -------------------------------------------------------------------------------- /public/icons/node-red/parser-xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/parser-xml.png -------------------------------------------------------------------------------- /public/icons/node-red/parser-yaml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jigang-duan/vue-flow-editor/HEAD/public/icons/node-red/parser-yaml.png -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device 4 | } 5 | export default getters 6 | -------------------------------------------------------------------------------- /src/views/flow/constants/index.js: -------------------------------------------------------------------------------- 1 | import VIEW from './view.constants' 2 | import state from './state.constants' 3 | 4 | export default { 5 | VIEW, 6 | state 7 | } 8 | -------------------------------------------------------------------------------- /src/views/flow/constants/view.constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PORT_TYPE_INPUT: Symbol('port type input'), 3 | PORT_TYPE_OUTPUT: Symbol('port type output') 4 | } 5 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /src/views/flow/styles/variables.scss: -------------------------------------------------------------------------------- 1 | @import './colors.scss'; 2 | 3 | 4 | $palette-width: 220px; 5 | 6 | $menu-item-size: 64px; 7 | $menu-item-button-size: $menu-item-size * 0.75; 8 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/store/modules/flow/settings.js: -------------------------------------------------------------------------------- 1 | 2 | const state = { 3 | } 4 | 5 | const mutations = { 6 | } 7 | 8 | const actions = { 9 | } 10 | 11 | export default { 12 | namespaced: true, 13 | state, 14 | mutations, 15 | actions 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/flow/styles/colors.scss: -------------------------------------------------------------------------------- 1 | 2 | $primary-color: #1890ff; 3 | 4 | $link-color: #888; 5 | 6 | $background-color: #fafafa; 7 | 8 | $primary-border-color: #bbbbbb; 9 | 10 | $node-selected-color: #ff7f0e; 11 | $port-selected-color: #ff7f0e; 12 | 13 | $palette-header-background: #fafafa; 14 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Flow Editor' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/store/modules/flow/index.js: -------------------------------------------------------------------------------- 1 | import type from './type' 2 | import chat from './chat' 3 | import info from './info' 4 | import settings from './settings' 5 | 6 | export default { 7 | namespaced: true, 8 | modules: { 9 | type, 10 | chat, 11 | info, 12 | settings 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'Vue Flow Editor', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/icons/svg/enterGroup.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | import flow from './modules/flow/' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | flow 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /src/views/flow/constants/state.constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | DEFAULT: Symbol('DEFAULT'), 3 | MOVING: Symbol('MOVING'), 4 | JOINING: Symbol('JOINING'), 5 | MOVING_ACTIVE: Symbol('MOVING_ACTIVE'), 6 | ADDING: Symbol('ADDING'), 7 | EDITING: Symbol('EDITING'), 8 | EXPORT: Symbol('EXPORT'), 9 | IMPORT: Symbol('IMPORT'), 10 | IMPORT_DRAGGING: Symbol('IMPORT_DRAGGING'), 11 | QUICK_JOINING: Symbol('QUICK_JOINING'), 12 | PANNING: Symbol('PANNING') 13 | } 14 | -------------------------------------------------------------------------------- /src/icons/svg/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/flow/type.js: -------------------------------------------------------------------------------- 1 | import { fetchProcessorTypes } from '@/api/flow' 2 | 3 | const state = { 4 | types: [] 5 | } 6 | 7 | const mutations = { 8 | SET_TYPES: (state, types) => { 9 | state.types = types 10 | } 11 | } 12 | 13 | const actions = { 14 | async getTypes({ dispatch, commit }) { 15 | try { 16 | commit('SET_TYPES', await fetchProcessorTypes()) 17 | } catch (error) { 18 | console.warn(error) 19 | } 20 | } 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions 28 | } 29 | -------------------------------------------------------------------------------- /src/icons/svg/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/dev-api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/icons/svg/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/flow/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Layout } from './layout' 2 | export { default as PaletteNode } from './palette-node' 3 | export { default as FlowGrid } from './grid' 4 | export { default as FlowNode } from './flow-node' 5 | export { default as FlowGroup } from './flow-group' 6 | export { default as Tabs } from './tabs/tabs' 7 | export { default as TabPane } from './tabs/tab-pane' 8 | export { default as Tarbar } from './tarbar' 9 | export { default as InfoContent } from './info-content' 10 | export { default as InfoTable } from './info-table' 11 | export { default as NodeSettings } from './node-settings' 12 | export { default as Menu } from './menu' 13 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | if (state.hasOwnProperty(key)) { 14 | state[key] = value 15 | } 16 | } 17 | } 18 | 19 | const actions = { 20 | changeSetting({ commit }, data) { 21 | commit('CHANGE_SETTING', data) 22 | } 23 | } 24 | 25 | export default { 26 | namespaced: true, 27 | state, 28 | mutations, 29 | actions 30 | } 31 | 32 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/icons/svg/play.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/views/flow/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin component-border { 2 | border: 1px solid $primary-border-color; 3 | box-sizing: border-box; 4 | } 5 | 6 | @mixin sidbar { 7 | position: absolute; 8 | top: calc(50% - 26px); 9 | padding: 15px 8px; 10 | border: 1px solid #ccc; 11 | background: #f9f9f9; 12 | color: #999; 13 | text-align: center; 14 | cursor: pointer; 15 | } 16 | 17 | @mixin component-footer { 18 | border-top: 1px solid $primary-border-color; 19 | background: $background-color; 20 | text-align: right; 21 | position: absolute; 22 | bottom: 0; 23 | left: 0; 24 | right: 0; 25 | height: 25px; 26 | line-height: 23px; 27 | padding: 0 10px; 28 | user-select: none; 29 | 30 | .button-group:not(:last-child) { 31 | margin-right: 5px; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/icons/svg/paste.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /src/lang/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 处理器 3 | ExecuteScript: '执行脚本', 4 | QueryDatabaseTableRecord: '查询数据库表记录', 5 | PutDatabaseRecord: '放置数据库记录', 6 | UpdateRecord: '更新记录', 7 | SplitJson: '分割Json', 8 | PublishKafka_2_0: '发布Kafka_2_0', 9 | GenerateFlowFile: '生成FlowFile', 10 | PutDistributedMapCache: '放置分布式Map缓存', 11 | InvokeHTTP: '调用HTTP', 12 | // 服务: 13 | AvroSchemaRegistry: 'AvroSchemaRegistry', 14 | CSVRecordSetWriter: 'CSVRecordSetWriter', 15 | DBCPConnectionPool: 'DBCPConnectionPool', 16 | DistributedMapCacheClientService: 'DistributedMapCacheClientService', 17 | DistributedMapCacheServer: 'DistributedMapCacheServer', 18 | RedisConnectionPoolService: 'RedisConnectionPoolService', 19 | RedisDistributedMapCacheClientService: 'RedisDistributedMapCacheClientService', 20 | CSVReader: 'CSVReader', 21 | JsonRecordSetWriter: 'JsonRecordSetWriter' 22 | } 23 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 44 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 处理器 3 | ExecuteScript: 'ExecuteScript', 4 | QueryDatabaseTableRecord: 'QueryDatabaseTableRecord', 5 | PutDatabaseRecord: 'PutDatabaseRecord', 6 | UpdateRecord: 'UpdateRecord', 7 | SplitJson: 'SplitJson', 8 | PublishKafka_2_0: 'PublishKafka_2_0', 9 | GenerateFlowFile: 'GenerateFlowFile', 10 | PutDistributedMapCache: 'PutDistributedMapCache', 11 | InvokeHTTP: 'InvokeHTTP', 12 | // 服务: 13 | AvroSchemaRegistry: 'AvroSchemaRegistry', 14 | CSVRecordSetWriter: 'CSVRecordSetWriter', 15 | DBCPConnectionPool: 'DBCPConnectionPool', 16 | DistributedMapCacheClientService: 'DistributedMapCacheClientService', 17 | DistributedMapCacheServer: 'DistributedMapCacheServer', 18 | RedisConnectionPoolService: 'RedisConnectionPoolService', 19 | RedisDistributedMapCacheClientService: 'RedisDistributedMapCacheClientService', 20 | CSVReader: 'CSVReader', 21 | JsonRecordSetWriter: 'JsonRecordSetWriter' 22 | } 23 | -------------------------------------------------------------------------------- /src/views/flow/components/info-content.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | 41 | -------------------------------------------------------------------------------- /src/icons/svg/arrow-circle-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exclamation-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/flow/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 39 | 40 | 53 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('ten digits timestamp', () => { 9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('new Date', () => { 12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('format', () => { 15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 18 | }) 19 | it('get the day of the week', () => { 20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 24 | }) 25 | it('empty argument', () => { 26 | expect(parseTime()).toBeNull() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/icons/svg/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/icons/svg/asterisk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/icons/svg/box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Box 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/flow/sidebar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | 44 | 53 | -------------------------------------------------------------------------------- /src/lang/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import Cookies from 'js-cookie' 4 | import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang 5 | import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang 6 | import enLocale from './en' 7 | import zhLocale from './zh' 8 | 9 | Vue.use(VueI18n) 10 | 11 | const messages = { 12 | en: { 13 | ...enLocale, 14 | ...elementEnLocale 15 | }, 16 | zh: { 17 | ...zhLocale, 18 | ...elementZhLocale 19 | } 20 | } 21 | 22 | export function getLanguage() { 23 | const chooseLanguage = Cookies.get('language') 24 | if (chooseLanguage) return chooseLanguage 25 | 26 | // if has not choose language 27 | const language = (navigator.language || navigator.browserLanguage).toLowerCase() 28 | const locales = Object.keys(messages) 29 | for (const locale of locales) { 30 | if (language.indexOf(locale) > -1) { 31 | return locale 32 | } 33 | } 34 | return 'en' 35 | } 36 | 37 | const i18n = new VueI18n({ 38 | // set locale 39 | // options: en | zh | es 40 | locale: getLanguage(), 41 | // set locale messages 42 | messages 43 | }) 44 | 45 | export default i18n 46 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/icons/svg/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | // import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import i18n from './lang' // internationalization 16 | import '@/icons' // icon 17 | import '@/permission' // permission control 18 | 19 | /** 20 | * If you don't want to use mock-server 21 | * you want to use mockjs for request interception 22 | * you can execute: 23 | * 24 | * import { mockXHR } from '../mock' 25 | * mockXHR() 26 | */ 27 | 28 | // form-create 29 | // ElementUI 30 | import formCreate from 'form-create/element' 31 | // 获取生成器 32 | // import { maker } from 'form-create/element' 33 | // 三级联动数据,不用可以不引入 34 | import 'form-create/district/province_city_area.js' 35 | 36 | // Flow 37 | import flow from '@flow/constants' 38 | 39 | Vue.prototype.FLOW = flow 40 | 41 | // set ElementUI lang to EN 42 | Vue.use(ElementUI, { 43 | i18n: (key, value) => i18n.t(key, value) 44 | }) 45 | Vue.use(formCreate) 46 | 47 | Vue.config.productionTip = false 48 | 49 | new Vue({ 50 | el: '#app', 51 | router, 52 | store, 53 | i18n, 54 | render: h => h(App) 55 | }) 56 | -------------------------------------------------------------------------------- /src/views/flow/components/tabs/tab-pane.vue: -------------------------------------------------------------------------------- 1 | 14 | 60 | -------------------------------------------------------------------------------- /src/icons/svg/reply.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/flow/components/grid.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 64 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/icons/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /src/icons/svg/ungroup.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import { param2Obj } from '../src/utils' 3 | 4 | const mocks = [ 5 | ] 6 | 7 | // for front mock 8 | // please use it cautiously, it will redefine XMLHttpRequest, 9 | // which will cause many of your third-party libraries to be invalidated(like progress event). 10 | export function mockXHR() { 11 | // mock patch 12 | // https://github.com/nuysoft/Mock/issues/300 13 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 14 | Mock.XHR.prototype.send = function() { 15 | if (this.custom.xhr) { 16 | this.custom.xhr.withCredentials = this.withCredentials || false 17 | 18 | if (this.responseType) { 19 | this.custom.xhr.responseType = this.responseType 20 | } 21 | } 22 | this.proxy_send(...arguments) 23 | } 24 | 25 | function XHR2ExpressReqWrap(respond) { 26 | return function(options) { 27 | let result = null 28 | if (respond instanceof Function) { 29 | const { body, type, url } = options 30 | // https://expressjs.com/en/4x/api.html#req 31 | result = respond({ 32 | method: type, 33 | body: JSON.parse(body), 34 | query: param2Obj(url) 35 | }) 36 | } else { 37 | result = respond 38 | } 39 | return Mock.mock(result) 40 | } 41 | } 42 | 43 | for (const i of mocks) { 44 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 45 | } 46 | } 47 | 48 | // for mock server 49 | const responseFake = (url, type, respond) => { 50 | return { 51 | url: new RegExp(`/mock${url}`), 52 | type: type || 'get', 53 | response(req, res) { 54 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 55 | } 56 | } 57 | } 58 | 59 | export default mocks.map(route => { 60 | return responseFake(route.url, route.type, route.response) 61 | }) 62 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/flow'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | // await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | // await store.dispatch('user/resetToken') 41 | Message.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/flow?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-flow-editor 2 | 3 | > Vue + Svg 实现的flow可视化编辑器。 4 | 5 | > 说明: 6 | UI 参考 node-red 7 | [mock server](https://github.com/jigang-duan/flow-editor-mock.git) 8 | 静态版本在static分支(不依赖server,只是为了展示) 9 | 10 | ## Build Setup 11 | 12 | ```bash 13 | # 克隆项目 14 | git clone https://github.com/jigang-duan/vue-flow-editor.git 15 | 16 | # 进入项目目录 17 | cd vue-flow-editor 18 | 19 | # 安装依赖 20 | npm install 21 | 22 | # 启动服务 23 | npm run dev 24 | ``` 25 | 26 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 27 | 28 | ## 发布 29 | 30 | ```bash 31 | # 构建测试环境 32 | npm run build:stage 33 | 34 | # 构建生产环境 35 | npm run build:prod 36 | ``` 37 | 38 | ## 其它 39 | 40 | ```bash 41 | # 预览发布环境效果 42 | npm run preview 43 | 44 | # 预览发布环境效果 + 静态资源分析 45 | npm run preview -- --report 46 | 47 | # 代码格式检查 48 | npm run lint 49 | 50 | # 代码格式检查并自动修复 51 | npm run lint -- --fix 52 | ``` 53 | 54 | ## 浏览 55 | 56 | ![demo](https://gitee.com/SCD-Wear/img-bed/raw/master/flow/editor.png) 57 | 58 | ## Browsers support 59 | 60 | Modern browsers and Internet Explorer 10+. 61 | 62 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 63 | | --------- | --------- | --------- | --------- | 64 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 65 | 66 | ## License 67 | 68 | [MIT](http://opensource.org/licenses/MIT) 69 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | 6 | const mockDir = path.join(process.cwd(), 'mock') 7 | 8 | function registerRoutes(app) { 9 | let mockLastIndex 10 | const { default: mocks } = require('./index.js') 11 | for (const mock of mocks) { 12 | app[mock.type](mock.url, mock.response) 13 | mockLastIndex = app._router.stack.length 14 | } 15 | const mockRoutesLength = Object.keys(mocks).length 16 | return { 17 | mockRoutesLength: mockRoutesLength, 18 | mockStartIndex: mockLastIndex - mockRoutesLength 19 | } 20 | } 21 | 22 | function unregisterRoutes() { 23 | Object.keys(require.cache).forEach(i => { 24 | if (i.includes(mockDir)) { 25 | delete require.cache[require.resolve(i)] 26 | } 27 | }) 28 | } 29 | 30 | module.exports = app => { 31 | // es6 polyfill 32 | require('@babel/register') 33 | 34 | // parse app.body 35 | // https://expressjs.com/en/4x/api.html#req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ 38 | extended: true 39 | })) 40 | 41 | const mockRoutes = registerRoutes(app) 42 | var mockRoutesLength = mockRoutes.mockRoutesLength 43 | var mockStartIndex = mockRoutes.mockStartIndex 44 | 45 | // watch files, hot reload mock server 46 | chokidar.watch(mockDir, { 47 | ignored: /mock-server/, 48 | ignoreInitial: true 49 | }).on('all', (event, path) => { 50 | if (event === 'change' || event === 'add') { 51 | try { 52 | // remove mock routes stack 53 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 54 | 55 | // clear routes cache 56 | unregisterRoutes() 57 | 58 | const mockRoutes = registerRoutes(app) 59 | mockRoutesLength = mockRoutes.mockRoutesLength 60 | mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 63 | } catch (error) { 64 | console.log(chalk.redBright(error)) 65 | } 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 65 | -------------------------------------------------------------------------------- /src/icons/svg/addGroup.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/store/modules/flow/info.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | content: [ 3 | { 4 | key: '1', 5 | title: '信息', 6 | component: 'InfoTable', 7 | value: { 8 | infos: [ 9 | { 10 | key: '名称', 11 | value: 'Flow' 12 | }, 13 | { 14 | key: 'uncaught', 15 | value: false 16 | } 17 | ] 18 | } 19 | }, 20 | { 21 | key: '2', 22 | title: '全局状态', 23 | component: 'InfoTable', 24 | value: { 25 | infos: [ 26 | { 27 | key: '线程数', 28 | value: 0 29 | }, 30 | { 31 | icon: 'list', 32 | value: '14,655 / 23.98 MB' 33 | }, 34 | { 35 | key: '控制器传输个数', 36 | value: 0 37 | }, 38 | { 39 | icon: 'play', 40 | value: 15 41 | }, 42 | { 43 | icon: 'stop', 44 | value: 223 45 | }, 46 | { 47 | icon: 'warning', 48 | value: 13 49 | }, 50 | { 51 | icon: 'disabled', 52 | value: 7 53 | }, 54 | { 55 | icon: 'check', 56 | value: 0 57 | }, 58 | { 59 | icon: 'asterisk', 60 | value: 0 61 | }, 62 | { 63 | icon: 'arrow-circle-up', 64 | value: 0 65 | }, 66 | { 67 | icon: 'exclamation-circle', 68 | value: 0 69 | }, 70 | { 71 | icon: 'question', 72 | value: 0 73 | }, 74 | { 75 | icon: 'refresh', 76 | value: '15:56:27 CST' 77 | } 78 | ] 79 | } 80 | }, 81 | { 82 | key: '3', 83 | title: '描述', 84 | component: 'Viewer', 85 | value: '# This is Viewer.\n Hello World.' 86 | } 87 | ] 88 | } 89 | 90 | const mutations = { 91 | } 92 | 93 | const actions = { 94 | } 95 | 96 | export default { 97 | namespaced: true, 98 | state, 99 | mutations, 100 | actions 101 | } 102 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-flow-editor", 3 | "version": "1.0.0", 4 | "description": "A vue flow editor", 5 | "author": "Jigang Duan ", 6 | "scripts": { 7 | "lint": "eslint --ext .js,.vue src", 8 | "build:prod": "vue-cli-service build", 9 | "build:stage": "vue-cli-service build --mode staging", 10 | "dev": "vue-cli-service serve", 11 | "ghpages": "node build/ghpages.js", 12 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'", 13 | "preview": "node build/index.js --preview", 14 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 15 | "test:ci": "npm run lint && npm run test:unit", 16 | "test:unit": "jest --clearCache && vue-cli-service test:unit" 17 | }, 18 | "dependencies": { 19 | "@toast-ui/vue-editor": "^1.1.1", 20 | "axios": "^0.19.0", 21 | "element-ui": "2.7.2", 22 | "form-create": "^1.6.5", 23 | "gsap": "^2.1.2", 24 | "js-cookie": "2.2.0", 25 | "lodash": "^4.17.11", 26 | "normalize.css": "7.0.0", 27 | "nprogress": "0.2.0", 28 | "path-to-regexp": "2.4.0", 29 | "vue": "2.6.10", 30 | "vue-i18n": "^8.0.0", 31 | "vue-router": "3.0.6", 32 | "vuex": "3.1.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "7.0.0", 36 | "@babel/register": "7.0.0", 37 | "@hapi/hoek": "^6.2.1", 38 | "@vue/cli-plugin-babel": "3.6.0", 39 | "@vue/cli-plugin-eslint": "3.6.0", 40 | "@vue/cli-plugin-unit-jest": "^3.7.0", 41 | "@vue/cli-service": "3.6.0", 42 | "@vue/test-utils": "1.0.0-beta.29", 43 | "babel-core": "7.0.0-bridge.0", 44 | "babel-eslint": "10.0.1", 45 | "babel-jest": "^24.8.0", 46 | "chalk": "2.4.2", 47 | "connect": "3.6.6", 48 | "eslint": "5.15.3", 49 | "eslint-plugin-vue": "5.2.2", 50 | "gh-pages": "^2.0.1", 51 | "html-webpack-plugin": "3.2.0", 52 | "mockjs": "1.0.1-beta3", 53 | "node-sass": "^4.9.0", 54 | "runjs": "^4.3.2", 55 | "sass-loader": "^7.1.0", 56 | "script-ext-html-webpack-plugin": "2.1.3", 57 | "script-loader": "0.7.2", 58 | "serve-static": "^1.13.2", 59 | "svg-sprite-loader": "4.1.3", 60 | "svgo": "1.2.2", 61 | "vue-cli-plugin-i18n": "^0.6.0", 62 | "vue-template-compiler": "2.6.10", 63 | "webpack": "^4.30.0" 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not ie <= 8" 69 | ], 70 | "engines": { 71 | "node": ">=8.9", 72 | "npm": ">= 3.0.0" 73 | }, 74 | "license": "MIT" 75 | } 76 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | /* Layout */ 7 | // import Layout from '@/layout' 8 | 9 | /** 10 | * Note: sub-menu only appear when route children.length >= 1 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | * 13 | * hidden: true if set true, item will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu 15 | * if not set alwaysShow, when item has more than one children route, 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 22 | icon: 'svg-name' the icon show in the sidebar 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 25 | } 26 | */ 27 | 28 | /** 29 | * constantRoutes 30 | * a base page that does not have permission requirements 31 | * all roles can be accessed 32 | */ 33 | export const constantRoutes = [ 34 | // { 35 | // path: '/login', 36 | // component: () => import('@/views/login/index'), 37 | // hidden: true 38 | // }, 39 | { 40 | path: '/flow', 41 | component: () => import('@/views/flow/index'), 42 | hidden: true 43 | }, 44 | 45 | { 46 | path: '/404', 47 | component: () => import('@/views/404'), 48 | hidden: true 49 | }, 50 | 51 | { path: '/', redirect: '/flow', hidden: true }, 52 | 53 | // 404 page must be placed at the end !!! 54 | { path: '*', redirect: '/404', hidden: true } 55 | ] 56 | 57 | const createRouter = () => new Router({ 58 | // mode: 'history', // require service support 59 | scrollBehavior: () => ({ y: 0 }), 60 | routes: constantRoutes 61 | }) 62 | 63 | const router = createRouter() 64 | 65 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 66 | export function resetRouter() { 67 | const newRouter = createRouter() 68 | router.matcher = newRouter.matcher // reset router 69 | } 70 | 71 | export default router 72 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (!res.code) { 50 | return res 51 | } 52 | if (res.code !== 20000) { 53 | Message({ 54 | message: res.message || 'error', 55 | type: 'error', 56 | duration: 5 * 1000 57 | }) 58 | 59 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 60 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 61 | // to re-login 62 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 63 | confirmButtonText: 'Re-Login', 64 | cancelButtonText: 'Cancel', 65 | type: 'warning' 66 | }).then(() => { 67 | store.dispatch('user/resetToken').then(() => { 68 | location.reload() 69 | }) 70 | }) 71 | } 72 | return Promise.reject(res.message || 'error') 73 | } else { 74 | return res 75 | } 76 | }, 77 | error => { 78 | console.log('err' + error) // for debug 79 | Message({ 80 | message: error.message, 81 | type: 'error', 82 | duration: 5 * 1000 83 | }) 84 | return Promise.reject(error) 85 | } 86 | ) 87 | 88 | export default service 89 | -------------------------------------------------------------------------------- /src/views/flow/components/node-settings.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 95 | 96 | 104 | 105 | 107 | -------------------------------------------------------------------------------- /src/views/flow/components/tarbar.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 89 | 90 | 112 | -------------------------------------------------------------------------------- /src/views/flow/components/info-table.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 60 | 61 | 113 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 21 | time = parseInt(time) 22 | } 23 | if ((typeof time === 'number') && (time.toString().length === 10)) { 24 | time = time * 1000 25 | } 26 | date = new Date(time) 27 | } 28 | const formatObj = { 29 | y: date.getFullYear(), 30 | m: date.getMonth() + 1, 31 | d: date.getDate(), 32 | h: date.getHours(), 33 | i: date.getMinutes(), 34 | s: date.getSeconds(), 35 | a: date.getDay() 36 | } 37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 38 | let value = formatObj[key] 39 | // Note: getDay() returns 0 on Sunday 40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 41 | if (result.length > 0 && value < 10) { 42 | value = '0' + value 43 | } 44 | return value || 0 45 | }) 46 | return time_str 47 | } 48 | 49 | /** 50 | * @param {number} time 51 | * @param {string} option 52 | * @returns {string} 53 | */ 54 | export function formatTime(time, option) { 55 | if (('' + time).length === 10) { 56 | time = parseInt(time) * 1000 57 | } else { 58 | time = +time 59 | } 60 | const d = new Date(time) 61 | const now = Date.now() 62 | 63 | const diff = (now - d) / 1000 64 | 65 | if (diff < 30) { 66 | return '刚刚' 67 | } else if (diff < 3600) { 68 | // less 1 hour 69 | return Math.ceil(diff / 60) + '分钟前' 70 | } else if (diff < 3600 * 24) { 71 | return Math.ceil(diff / 3600) + '小时前' 72 | } else if (diff < 3600 * 24 * 2) { 73 | return '1天前' 74 | } 75 | if (option) { 76 | return parseTime(time, option) 77 | } else { 78 | return ( 79 | d.getMonth() + 80 | 1 + 81 | '月' + 82 | d.getDate() + 83 | '日' + 84 | d.getHours() + 85 | '时' + 86 | d.getMinutes() + 87 | '分' 88 | ) 89 | } 90 | } 91 | 92 | /** 93 | * @param {string} url 94 | * @returns {Object} 95 | */ 96 | export function param2Obj(url) { 97 | const search = url.split('?')[1] 98 | if (!search) { 99 | return {} 100 | } 101 | return JSON.parse( 102 | '{"' + 103 | decodeURIComponent(search) 104 | .replace(/"/g, '\\"') 105 | .replace(/&/g, '","') 106 | .replace(/=/g, '":"') 107 | .replace(/\+/g, ' ') + 108 | '"}' 109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/views/flow/palette.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | 74 | 86 | 87 | 120 | -------------------------------------------------------------------------------- /src/views/flow/components/palette-node.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 54 | 55 | 135 | -------------------------------------------------------------------------------- /src/store/modules/flow/chat.js: -------------------------------------------------------------------------------- 1 | import * as fApi from '@/api/flow' 2 | 3 | const state = { 4 | processors: [], 5 | links: [], 6 | groups: [], 7 | flowGroupIDs: ['root'] 8 | } 9 | 10 | const getters = { 11 | flowGroupId: state => { 12 | const { flowGroupIDs } = state 13 | return flowGroupIDs[flowGroupIDs.length - 1] 14 | } 15 | } 16 | 17 | const mutations = { 18 | SET_PROCESS_GROUP: (state, { processors, links, groups }) => { 19 | state.processors = processors 20 | state.links = links 21 | state.groups = groups 22 | }, 23 | SET_CUR_GROUP: (state, ids) => { 24 | state.flowGroupIDs = ids 25 | } 26 | } 27 | 28 | const actions = { 29 | async getProcessGroup({ commit, getters }) { 30 | const { flowGroupId } = getters 31 | const updated = await fApi.fetchProcessGroup(flowGroupId) 32 | commit('SET_PROCESS_GROUP', updated) 33 | if (flowGroupId === 'root') { 34 | commit('SET_CUR_GROUP', [updated.id]) 35 | } 36 | }, 37 | async newProcessor({ commit, getters }, { typeId, x, y, maxX, maxY }) { 38 | const { flowGroupId } = getters 39 | const updated = await fApi.createProcessor(typeId, { x, y, maxX, maxY }, flowGroupId) 40 | commit('SET_PROCESS_GROUP', updated) 41 | }, 42 | async UpdateSnippet({ getters, commit, state }, payload) { 43 | const { flowGroupId } = getters 44 | const processors = state.processors.filter(p => payload.processors.includes(p.id)) 45 | const groups = state.groups.filter(p => payload.groups.includes(p.id)) 46 | const updated = await fApi.UpdateSnippet({ processors, groups }, flowGroupId) 47 | commit('SET_PROCESS_GROUP', updated) 48 | }, 49 | async newConnection({ commit, getters }, link) { 50 | const { flowGroupId } = getters 51 | const updated = await fApi.createConnection(link, flowGroupId) 52 | commit('SET_PROCESS_GROUP', updated) 53 | }, 54 | async cloneSnippet({ state, commit, getters }, payload) { 55 | const { flowGroupId } = getters 56 | const oldids = state.processors.map(p => p.id) 57 | const processors = state.processors.filter(p => payload.processors.includes(p.id)) 58 | const links = state.links.filter(l => payload.links.includes(l.id)) 59 | const updated = await fApi.cloneSnippet({ processors, links }, flowGroupId) 60 | commit('SET_PROCESS_GROUP', updated) 61 | return updated.processors.filter(p => !oldids.includes(p.id)).map(p => p.id) 62 | }, 63 | async addGroup({ commit, state, getters }, payload) { 64 | const { flowGroupId } = getters 65 | const processors = state.processors.filter(p => payload.processors.includes(p.id)) 66 | const links = state.links.filter(link => payload.links.includes(link.id)) 67 | const updated = await fApi.addGroup({ processors, links }, flowGroupId) 68 | commit('SET_PROCESS_GROUP', updated) 69 | }, 70 | async remvoeSnippet({ commit, getters }, { processors, links, groups }) { 71 | const { flowGroupId } = getters 72 | const updated = await fApi.deleteSnippet({ processors, links, groups }, flowGroupId) 73 | commit('SET_PROCESS_GROUP', updated) 74 | }, 75 | async ungroup({ dispatch, commit, getters }, groupID) { 76 | const { flowGroupId } = getters 77 | const updated = await fApi.ungroup(groupID, flowGroupId) 78 | commit('SET_PROCESS_GROUP', updated) 79 | }, 80 | async enterGroup({ dispatch, commit, state }, groupID) { 81 | const { flowGroupIDs } = state 82 | const index = flowGroupIDs.findIndex(id => id === groupID) 83 | if (index < 0) { 84 | commit('SET_CUR_GROUP', [...flowGroupIDs, groupID]) 85 | } else { 86 | commit('SET_CUR_GROUP', flowGroupIDs.slice(0, index + 1)) 87 | } 88 | dispatch('getProcessGroup') 89 | } 90 | } 91 | 92 | export default { 93 | namespaced: true, 94 | state, 95 | getters, 96 | mutations, 97 | actions 98 | } 99 | -------------------------------------------------------------------------------- /src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 61 | 62 | 140 | -------------------------------------------------------------------------------- /src/icons/svg/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/api/flow.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // const BaseURL = 'http://192.168.10.235:3000/mock/32/flow' 4 | const BaseURL = 'http://localhost:8080/flow' 5 | 6 | const mapTypeGoups = (group) => { 7 | return { 8 | ...group, 9 | items: group.items.map(it => ({ 10 | ...it, 11 | style: { 12 | height: '28px', 13 | 'background-color': it.color 14 | } 15 | })) 16 | } 17 | } 18 | 19 | const mapProcessor = (p) => { 20 | return { 21 | ...p, 22 | port: { 23 | outputActive: false, 24 | inputActive: false 25 | } 26 | } 27 | } 28 | 29 | const mapProcessGroup = processGroup => { 30 | const { id, processors, connections, processGroups } = processGroup || {} 31 | const ps = processors && processors.map(mapProcessor) || [] 32 | const links = connections || [] 33 | const groups = processGroups && processGroups.map(g => ({ ...g, count: g.processors && g.processors.length || g.count || 0 })) || [] 34 | return { 35 | processors: ps, 36 | links, 37 | groups, 38 | id 39 | } 40 | } 41 | 42 | export const fetchProcessorTypes = async() => { 43 | const typeGoups = await request({ 44 | url: `${BaseURL}/dataflow/processor-types`, 45 | method: 'get' 46 | }) 47 | return typeGoups.map(mapTypeGoups) 48 | } 49 | 50 | export const fetchProcessGroup = async(id = 'root') => { 51 | const processGroup = await request({ 52 | url: `${BaseURL}/dataflow/process-groups/${id}`, 53 | method: 'get' 54 | }) 55 | return mapProcessGroup(processGroup) 56 | } 57 | 58 | export const createProcessor = async(typeId, { x, y, maxX, maxY }, groupId = 'root') => { 59 | const processGroup = await request({ 60 | url: `${BaseURL}/process-groups/${groupId}/processors`, 61 | method: 'post', 62 | data: { 63 | typeId, 64 | rect: { 65 | x, y, 66 | maxX, maxY 67 | } 68 | } 69 | }) 70 | return mapProcessGroup(processGroup) 71 | } 72 | 73 | export const UpdateSnippet = async({ processors, groups }, groupId = 'root') => { 74 | const processGroup = await request({ 75 | url: `${BaseURL}/process-groups/${groupId}/snippet`, 76 | method: 'put', 77 | data: { 78 | processors, 79 | processGroups: groups 80 | } 81 | }) 82 | return mapProcessGroup(processGroup) 83 | } 84 | 85 | export const createConnection = async({ source, sourcePort, target }, groupId = 'root') => { 86 | const processGroup = await request({ 87 | url: `${BaseURL}/process-groups/${groupId}/connections`, 88 | method: 'post', 89 | data: { 90 | source: source.id, 91 | sourcePort, 92 | target: target.id 93 | } 94 | }) 95 | return mapProcessGroup(processGroup) 96 | } 97 | 98 | export const cloneSnippet = async({ processors, links }, groupId = 'root') => { 99 | const processGroup = await request({ 100 | url: `${BaseURL}/process-groups/${groupId}/snippet`, 101 | method: 'post', 102 | data: { 103 | processors, 104 | connections: links 105 | } 106 | }) 107 | return mapProcessGroup(processGroup) 108 | } 109 | 110 | export const addGroup = async({ processors, links }, groupId = 'root') => { 111 | const processGroup = await request({ 112 | url: `${BaseURL}/process-groups/${groupId}/process-groups`, 113 | method: 'post', 114 | data: { 115 | processors, 116 | connections: links 117 | } 118 | }) 119 | return mapProcessGroup(processGroup) 120 | } 121 | 122 | export const deleteSnippet = async({ processors, links, groups }, groupId = 'root') => { 123 | const processGroup = await request({ 124 | url: `${BaseURL}/process-groups/${groupId}/snippet`, 125 | method: 'delete', 126 | data: { 127 | processors, 128 | connections: links, 129 | processGroups: groups 130 | } 131 | }) 132 | return mapProcessGroup(processGroup) 133 | } 134 | 135 | export async function ungroup(groupId, parentID = 'root') { 136 | const processGroup = await request({ 137 | url: `${BaseURL}/process-groups/${groupId}/ungroup`, 138 | method: 'put', 139 | params: { 140 | parentID 141 | } 142 | }) 143 | return mapProcessGroup(processGroup) 144 | } 145 | -------------------------------------------------------------------------------- /src/views/flow/components/flow-group.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 116 | 117 | 160 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .el-menu { 61 | border: none; 62 | height: 100%; 63 | width: 100% !important; 64 | } 65 | 66 | // menu hover 67 | .submenu-title-noDropdown, 68 | .el-submenu__title { 69 | &:hover { 70 | background-color: $menuHover !important; 71 | } 72 | } 73 | 74 | .is-active>.el-submenu__title { 75 | color: $subMenuActiveText !important; 76 | } 77 | 78 | & .nest-menu .el-submenu>.el-submenu__title, 79 | & .el-submenu .el-menu-item { 80 | min-width: $sideBarWidth !important; 81 | background-color: $subMenuBg !important; 82 | 83 | &:hover { 84 | background-color: $subMenuHover !important; 85 | } 86 | } 87 | } 88 | 89 | .hideSidebar { 90 | .sidebar-container { 91 | width: 54px !important; 92 | } 93 | 94 | .main-container { 95 | margin-left: 54px; 96 | } 97 | 98 | .svg-icon { 99 | margin-right: 0px; 100 | } 101 | 102 | .submenu-title-noDropdown { 103 | padding: 0 !important; 104 | position: relative; 105 | 106 | .el-tooltip { 107 | padding: 0 !important; 108 | 109 | .svg-icon { 110 | margin-left: 20px; 111 | } 112 | } 113 | } 114 | 115 | .el-submenu { 116 | overflow: hidden; 117 | 118 | &>.el-submenu__title { 119 | padding: 0 !important; 120 | 121 | .svg-icon { 122 | margin-left: 20px; 123 | } 124 | 125 | .el-submenu__icon-arrow { 126 | display: none; 127 | } 128 | } 129 | } 130 | 131 | .el-menu--collapse { 132 | .el-submenu { 133 | &>.el-submenu__title { 134 | &>span { 135 | height: 0; 136 | width: 0; 137 | overflow: hidden; 138 | visibility: hidden; 139 | display: inline-block; 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | .el-menu--collapse .el-menu .el-submenu { 147 | min-width: $sideBarWidth !important; 148 | } 149 | 150 | // mobile responsive 151 | .mobile { 152 | .main-container { 153 | margin-left: 0px; 154 | } 155 | 156 | .sidebar-container { 157 | transition: transform .28s; 158 | width: $sideBarWidth !important; 159 | } 160 | 161 | &.hideSidebar { 162 | .sidebar-container { 163 | pointer-events: none; 164 | transition-duration: 0.3s; 165 | transform: translate3d(-$sideBarWidth, 0, 0); 166 | } 167 | } 168 | } 169 | 170 | .withoutAnimation { 171 | 172 | .main-container, 173 | .sidebar-container { 174 | transition: none; 175 | } 176 | } 177 | } 178 | 179 | // when menu collapsed 180 | .el-menu--vertical { 181 | &>.el-menu { 182 | .svg-icon { 183 | margin-right: 16px; 184 | } 185 | } 186 | 187 | .nest-menu .el-submenu>.el-submenu__title, 188 | .el-menu-item { 189 | &:hover { 190 | // you can use $subMenuHover 191 | background-color: $menuHover !important; 192 | } 193 | } 194 | 195 | // the scroll bar appears when the subMenu is too long 196 | >.el-menu--popup { 197 | max-height: 100vh; 198 | overflow-y: auto; 199 | 200 | &::-webkit-scrollbar-track-piece { 201 | background: #d3dce6; 202 | } 203 | 204 | &::-webkit-scrollbar { 205 | width: 6px; 206 | } 207 | 208 | &::-webkit-scrollbar-thumb { 209 | background: #99a9bf; 210 | border-radius: 20px; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/settings.js') 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | const name = defaultSettings.title || 'Vue Flow Editor' // page title 10 | // If your port is set to 80, 11 | // use administrator privileges to execute the command line. 12 | // For example, Mac: sudo npm run 13 | const port = 9528 // dev port 14 | 15 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 16 | module.exports = { 17 | /** 18 | * You will need to set publicPath if you plan to deploy your site under a sub path, 19 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 20 | * then publicPath should be set to "/bar/". 21 | * In most cases please use '/' !!! 22 | * Detail: https://cli.vuejs.org/config/#publicpath 23 | */ 24 | publicPath: '/', 25 | 26 | outputDir: 'dist', 27 | assetsDir: 'static', 28 | lintOnSave: process.env.NODE_ENV === 'development', 29 | productionSourceMap: false, 30 | 31 | devServer: { 32 | port: port, 33 | open: true, 34 | overlay: { 35 | warnings: false, 36 | errors: true 37 | }, 38 | proxy: { 39 | // change xxx-api/login => mock/login 40 | // detail: https://cli.vuejs.org/config/#devserver-proxy 41 | [process.env.VUE_APP_BASE_API]: { 42 | target: `http://localhost:${port}/mock`, 43 | changeOrigin: true, 44 | pathRewrite: { 45 | ['^' + process.env.VUE_APP_BASE_API]: '' 46 | } 47 | } 48 | }, 49 | after: require('./mock/mock-server.js') 50 | }, 51 | 52 | configureWebpack: { 53 | // provide the app's title in webpack's name field, so that 54 | // it can be accessed in index.html to inject the correct title. 55 | name: name, 56 | resolve: { 57 | alias: { 58 | '@': resolve('src'), 59 | '@flow': resolve('src/views/flow') 60 | } 61 | } 62 | }, 63 | 64 | chainWebpack(config) { 65 | config.plugins.delete('preload') // TODO: need test 66 | config.plugins.delete('prefetch') // TODO: need test 67 | 68 | // set svg-sprite-loader 69 | config.module 70 | .rule('svg') 71 | .exclude.add(resolve('src/icons')) 72 | .end() 73 | config.module 74 | .rule('icons') 75 | .test(/\.svg$/) 76 | .include.add(resolve('src/icons')) 77 | .end() 78 | .use('svg-sprite-loader') 79 | .loader('svg-sprite-loader') 80 | .options({ 81 | symbolId: 'icon-[name]' 82 | }) 83 | .end() 84 | 85 | // set preserveWhitespace 86 | config.module 87 | .rule('vue') 88 | .use('vue-loader') 89 | .loader('vue-loader') 90 | .tap(options => { 91 | options.compilerOptions.preserveWhitespace = true 92 | return options 93 | }) 94 | .end() 95 | 96 | config 97 | // https://webpack.js.org/configuration/devtool/#development 98 | .when(process.env.NODE_ENV === 'development', 99 | config => config.devtool('cheap-source-map') 100 | ) 101 | 102 | config 103 | .when(process.env.NODE_ENV !== 'development', 104 | config => { 105 | config 106 | .plugin('ScriptExtHtmlWebpackPlugin') 107 | .after('html') 108 | .use('script-ext-html-webpack-plugin', [{ 109 | // `runtime` must same as runtimeChunk name. default is `runtime` 110 | inline: /runtime\..*\.js$/ 111 | }]) 112 | .end() 113 | config 114 | .optimization.splitChunks({ 115 | chunks: 'all', 116 | cacheGroups: { 117 | libs: { 118 | name: 'chunk-libs', 119 | test: /[\\/]node_modules[\\/]/, 120 | priority: 10, 121 | chunks: 'initial' // only package third parties that are initially dependent 122 | }, 123 | elementUI: { 124 | name: 'chunk-elementUI', // split elementUI into a single package 125 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 126 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 127 | }, 128 | commons: { 129 | name: 'chunk-commons', 130 | test: resolve('src/components'), // can customize your rules 131 | minChunks: 3, // minimum common number 132 | priority: 5, 133 | reuseExistingChunk: true 134 | } 135 | } 136 | }) 137 | config.optimization.runtimeChunk('single') 138 | } 139 | ) 140 | }, 141 | 142 | pluginOptions: { 143 | i18n: { 144 | locale: 'zh', 145 | fallbackLocale: 'en', 146 | localeDir: 'locales', 147 | enableInSFC: false 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/views/flow/utils.js: -------------------------------------------------------------------------------- 1 | 2 | const onlyPositive = (num) => num > 0 ? num : 0 3 | 4 | export const nodeInfoByEvent = (event) => { 5 | const id = event.dataTransfer.getData('node_id') 6 | const touchX = event.dataTransfer.getData('node_touch_x') 7 | const touchY = event.dataTransfer.getData('node_touch_y') 8 | const x = onlyPositive(event.offsetX - touchX) 9 | const y = onlyPositive(event.offsetY - touchY) 10 | return { 11 | typeId: id, x, y 12 | } 13 | } 14 | 15 | export const calcMousePosition = (event, svgElement, scale = 1.0) => { 16 | const x = (event.pageX - svgElement.getBoundingClientRect().x) / scale 17 | const y = (event.pageY - svgElement.getBoundingClientRect().y) / scale 18 | return { 19 | x, y 20 | } 21 | } 22 | 23 | export const mouseAtNode = (event, scale = 1.0) => calcMousePosition(event, event.target.parentElement, scale) 24 | 25 | const node_width = 100 26 | const node_height = 30 27 | const lineCurveScale = 0.75 28 | 29 | export const generateLinkPath = (origX, origY, destX, destY, sc) => { 30 | const dy = destY - origY 31 | const dx = destX - origX 32 | const delta = Math.sqrt(dy * dy + dx * dx) 33 | let scale = lineCurveScale 34 | const scaleY = 0 35 | if (dx * sc > 0) { 36 | if (delta < node_width) { 37 | scale = 0.75 - 0.75 * ((node_width - delta) / node_width) 38 | // scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width)); 39 | // if (Math.abs(dy) < 3*node_height) { 40 | // scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ; 41 | // } 42 | } 43 | } else { 44 | scale = 0.4 - 0.2 * (Math.max(0, (node_width - Math.min(Math.abs(dx), Math.abs(dy))) / node_width)) 45 | } 46 | if (dx * sc > 0) { 47 | return 'M ' + origX + ' ' + origY + 48 | ' C ' + (origX + sc * (node_width * scale)) + ' ' + (origY + scaleY * node_height) + ' ' + 49 | (destX - sc * (scale) * node_width) + ' ' + (destY - scaleY * node_height) + ' ' + 50 | destX + ' ' + destY 51 | } else { 52 | const midX = Math.floor(destX - dx / 2) 53 | let midY = Math.floor(destY - dy / 2) 54 | // 55 | if (dy === 0) { 56 | midY = destY + node_height 57 | } 58 | const cp_height = node_height / 2 59 | const y1 = (destY + midY) / 2 60 | const topX = origX + sc * node_width * scale 61 | const topY = dy > 0 ? Math.min(y1 - dy / 2, origY + cp_height) : Math.max(y1 - dy / 2, origY - cp_height) 62 | const bottomX = destX - sc * node_width * scale 63 | const bottomY = dy > 0 ? Math.max(y1, destY - cp_height) : Math.min(y1, destY + cp_height) 64 | const x1 = (origX + topX) / 2 65 | const scy = dy > 0 ? 1 : -1 66 | const cp = [ 67 | // Orig -> Top 68 | [x1, origY], 69 | [topX, dy > 0 ? Math.max(origY, topY - cp_height) : Math.min(origY, topY + cp_height)], 70 | // Top -> Mid 71 | // [Mirror previous cp] 72 | [x1, dy > 0 ? Math.min(midY, topY + cp_height) : Math.max(midY, topY - cp_height)], 73 | // Mid -> Bottom 74 | // [Mirror previous cp] 75 | [bottomX, dy > 0 ? Math.max(midY, bottomY - cp_height) : Math.min(midY, bottomY + cp_height)], 76 | // Bottom -> Dest 77 | // [Mirror previous cp] 78 | [(destX + bottomX) / 2, destY] 79 | ] 80 | if (cp[2][1] === topY + scy * cp_height) { 81 | if (Math.abs(dy) < cp_height * 10) { 82 | cp[1][1] = topY - scy * cp_height / 2 83 | cp[3][1] = bottomY - scy * cp_height / 2 84 | } 85 | cp[2][0] = topX 86 | } 87 | return 'M ' + origX + ' ' + origY + 88 | ' C ' + 89 | cp[0][0] + ' ' + cp[0][1] + ' ' + 90 | cp[1][0] + ' ' + cp[1][1] + ' ' + 91 | topX + ' ' + topY + 92 | ' S ' + 93 | cp[2][0] + ' ' + cp[2][1] + ' ' + 94 | midX + ' ' + midY + 95 | ' S ' + 96 | cp[3][0] + ' ' + cp[3][1] + ' ' + 97 | bottomX + ' ' + bottomY + 98 | ' S ' + 99 | cp[4][0] + ' ' + cp[4][1] + ' ' + 100 | destX + ' ' + destY 101 | } 102 | } 103 | 104 | export const getAllFlowNodes = (node, links, nodes) => { 105 | const visited = {} 106 | visited[node.id] = true 107 | const nns = [node] 108 | const stack = [node] 109 | while (stack.length !== 0) { 110 | const n = stack.shift() 111 | const childLinks = links.filter(link => (link.source === n.id) || (link.target === n.id)) 112 | childLinks.forEach(childLink => { 113 | let id = (childLink.source === n.id) ? childLink.target : childLink.source 114 | const child = nodes.find(n => n.id === id) 115 | if (!id) { 116 | id = `${child.source}:${child.sourcePort}:${child.target}` 117 | } 118 | if (!visited[id]) { 119 | visited[id] = true 120 | nns.push(child) 121 | stack.push(child) 122 | } 123 | }) 124 | } 125 | return nns 126 | } 127 | 128 | export const computeLine = (links, nodes) => { 129 | return links.map(link => { 130 | return { 131 | id: link.id, 132 | sourcePort: link.sourcePort, 133 | source: nodes.find(node => node.id === link.source), 134 | target: nodes.find(node => node.id === link.target) 135 | } 136 | }).filter(link => link.source && link.target && link.source.id && link.target.id) 137 | } 138 | -------------------------------------------------------------------------------- /src/views/flow/components/layout.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 108 | 109 | 198 | -------------------------------------------------------------------------------- /src/views/flow/components/tabs/tabs.vue: -------------------------------------------------------------------------------- 1 | 26 | 89 | 90 | 193 | -------------------------------------------------------------------------------- /src/api/flow.mock.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep' 2 | 3 | export const Groups = [ 4 | { 5 | 'id': 'fdCD6BD6-C5Bb-ECdb-6647-b8FDfe8eBa64', 6 | 'title': '连接器', 7 | 'name': 'Connector', 8 | 'items': [ 9 | { 10 | 'id': '76DA30b6-61BF-75AF-AfFc-d2FdB9AdC874', 11 | 'name': 'MySQL', 12 | 'icon': 'icons/node-red/db.png', 13 | 'iconOnRight': false, 14 | 'style': { 15 | 'height': '28px', 16 | 'background-color': '#79f299' 17 | }, 18 | 'hasInput': false, 19 | 'hasOutput': true 20 | }, 21 | { 22 | 'id': 'EAb1dda2-2B17-79CF-820B-a7D28587De7d', 23 | 'name': 'CSV', 24 | 'icon': 'icons/node-red/parser-csv.png', 25 | 'iconOnRight': false, 26 | 'style': { 27 | 'height': '28px', 28 | 'background-color': '#f2797c' 29 | }, 30 | 'hasInput': false, 31 | 'hasOutput': true 32 | }, 33 | { 34 | 'id': '6f7F4b9b-8ef4-Ee72-9F8C-faC65DeBd78b', 35 | 'name': 'Redis', 36 | 'icon': 'icons/node-red/redis.png', 37 | 'iconOnRight': true, 38 | 'style': { 39 | 'height': '28px', 40 | 'background-color': '#799ff2' 41 | }, 42 | 'hasInput': true, 43 | 'hasOutput': false 44 | } 45 | ] 46 | }, 47 | { 48 | 'id': '18Ed9dB5-F3B1-6c83-4de6-d7BcBbA2c84c', 49 | 'title': '转换器', 50 | 'name': 'Converter', 51 | 'items': [ 52 | { 53 | 'id': '871155A1-e8EE-D3Fc-CFcC-E2c83BB98B31', 54 | 'name': 'sort', 55 | 'icon': 'icons/node-red/sort.png', 56 | 'iconOnRight': false, 57 | 'style': { 58 | 'height': '28px', 59 | 'background-color': '#f2b779' 60 | }, 61 | 'hasInput': true, 62 | 'hasOutput': true 63 | }, 64 | { 65 | 'id': 'fA1Be6A1-1335-9a0e-5a7D-7b5cdBbbcfE3', 66 | 'name': 'split', 67 | 'icon': 'icons/node-red/split.png', 68 | 'iconOnRight': false, 69 | 'style': { 70 | 'height': '28px', 71 | 'background-color': '#9479f2' 72 | }, 73 | 'hasInput': true, 74 | 'hasOutput': true 75 | } 76 | ] 77 | }, 78 | { 79 | 'id': 'F163FeC2-8189-DC1c-bFB5-c7b5eabCF604', 80 | 'title': '格式转换器', 81 | 'name': 'Converter', 82 | 'items': [ 83 | { 84 | 'id': 'ccBE4edc-06ab-6Ab1-A54a-22Ff1A9a5aD0', 85 | 'name': 'Yaml', 86 | 'icon': 'icons/node-red/parser-yaml.png', 87 | 'iconOnRight': false, 88 | 'style': { 89 | 'height': '28px', 90 | 'background-color': '#c3f279' 91 | }, 92 | 'hasInput': true, 93 | 'hasOutput': true 94 | }, 95 | { 96 | 'id': '8b19F27B-DbAF-c59d-22F4-f9f141b73Ee6', 97 | 'name': 'Json', 98 | 'icon': 'icons/node-red/parser-json.png', 99 | 'iconOnRight': false, 100 | 'style': { 101 | 'height': '28px', 102 | 'background-color': '#f279e6' 103 | }, 104 | 'hasInput': true, 105 | 'hasOutput': true 106 | }, 107 | { 108 | 'id': '0DCfA336-C02A-fA38-3beC-2F3db5ef3cF8', 109 | 'name': 'Xml', 110 | 'icon': 'icons/node-red/parser-xml.png', 111 | 'iconOnRight': false, 112 | 'style': { 113 | 'height': '28px', 114 | 'background-color': '#d4f2da' 115 | }, 116 | 'hasInput': true, 117 | 'hasOutput': true 118 | } 119 | ] 120 | } 121 | ] 122 | 123 | const guid = () => { 124 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 125 | const r = Math.random() * 16 | 0 126 | const v = c === 'x' ? r : (r & 0x3 | 0x8) 127 | return v.toString(16) 128 | }) 129 | } 130 | 131 | export const cloneNodes = (nodes) => { 132 | const ns = cloneDeep(nodes) 133 | return ns.map(n => ({ ...n, id: guid(), cloneId: n.id, rect: { ...n.rect, x: n.rect.x + 10, y: n.rect.y + 10 }})) 134 | } 135 | 136 | export const generateNodeByType = (typeId, x, y, maxX, maxY) => { 137 | const width = 140 138 | const height = 30 139 | const minx = width / 2 + 5 140 | const miny = height / 2 + 5 141 | const maxx = maxX - width / 2 - 5 142 | const maxy = maxY - height / 2 - 5 143 | let cx = x 144 | let cy = y 145 | if (x < minx) cx = minx 146 | else if (x > maxx) cx = maxx 147 | if (y < miny) cy = miny 148 | else if (y > maxy) cy = maxy 149 | let type 150 | Groups.forEach(group => { 151 | group.nodes.forEach(n => { 152 | if (n.id === typeId) { 153 | type = n 154 | } 155 | }) 156 | }) 157 | return { 158 | id: guid(), 159 | 'label': type.name, 160 | 'hasInput': type.hasInput, 161 | 'hasOutput': type.hasOutput, 162 | iconOnRight: type.iconOnRight, 163 | 'error': false, 164 | 'changed': false, 165 | 'status': { 166 | 'show': false, 167 | 'label': 'ad reprehenderit dolore' 168 | }, 169 | 'rect': { 170 | 'x': cx, 171 | 'y': cy, 172 | 'h': height, 173 | 'w': width 174 | }, 175 | 'icon': type.icon, 176 | 'style': { 177 | 'color': type.style['background-color'] 178 | }, 179 | 'port': { 180 | 'outputActive': false, 181 | 'inputActive': false 182 | } 183 | } 184 | } 185 | 186 | export const generateGroup = ({ nodes, links }) => { 187 | const minX = Math.min(...nodes.map(n => n.rect.x)) 188 | const minY = Math.min(...nodes.map(n => n.rect.y)) 189 | return { 190 | id: guid(), 191 | label: '新建分组', 192 | content: { 193 | nodes, 194 | links 195 | }, 196 | error: false, 197 | changed: false, 198 | status: { 199 | show: false, 200 | label: '' 201 | }, 202 | rect: { 203 | x: minX, 204 | y: minY, 205 | w: 140, 206 | h: 70 207 | }, 208 | icon: 'icons/node-red/folder.png', 209 | style: { 210 | color: 'RGBA(242, 244, 245, 1.00)' 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 229 | -------------------------------------------------------------------------------- /src/views/flow/components/flow-node.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 186 | 187 | 231 | -------------------------------------------------------------------------------- /src/views/flow/components/menu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 186 | 187 | 279 | --------------------------------------------------------------------------------