├── .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 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------
/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 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
7 |
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 |
2 |
5 |
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 |
2 |
3 |
4 |
5 | {{ info.title }}
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
15 |
--------------------------------------------------------------------------------
/src/views/flow/sidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
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 |
2 |
14 |
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 |
2 |
11 |
12 |
13 |
14 |
60 |
--------------------------------------------------------------------------------
/src/icons/svg/reply.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/flow/components/grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
29 |
30 |
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 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
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 | 
57 |
58 | ## Browsers support
59 |
60 | Modern browsers and Internet Explorer 10+.
61 |
62 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
2 |
14 |
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 |
2 |
7 |
26 |
27 |
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 |
2 |
12 |
13 |
14 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
95 |
96 |
104 |
105 |
107 |
--------------------------------------------------------------------------------
/src/views/flow/components/tarbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
89 |
90 |
112 |
--------------------------------------------------------------------------------
/src/views/flow/components/info-table.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | |
6 |
7 | {{ info.key }}
8 | |
9 |
10 |
14 | {{ info.value }}
15 |
16 | |
17 |
18 |
19 |
20 |
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 |
2 |
25 |
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 |
2 |
20 |
21 |
22 |
73 |
74 |
86 |
87 |
120 |
--------------------------------------------------------------------------------
/src/views/flow/components/palette-node.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
{{ $t(data.name) }}
11 |
12 |
![]()
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
32 |
33 |
34 |
61 |
62 |
140 |
--------------------------------------------------------------------------------
/src/icons/svg/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
14 |
15 |
16 |
17 |
18 | {{ value.label }}
19 | {{ nodeCount }}
20 |
21 |
22 |
23 | {{ value.status.label }}
24 |
25 |
31 |
41 |
42 |
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 |
2 |
3 |
8 |
9 |
10 |
21 |
22 |
30 |
41 |
42 |
43 |
44 |
108 |
109 |
198 |
--------------------------------------------------------------------------------
/src/views/flow/components/tabs/tabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
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 |
2 |
3 |
4 |
10 |
11 |
OOPS!
12 |
15 |
{{ message }}
16 |
Please check that the URL you entered is correct, or click the button below to return to the homepage.
17 |
Back to home
18 |
19 |
20 |
21 |
22 |
23 |
34 |
35 |
229 |
--------------------------------------------------------------------------------
/src/views/flow/components/flow-node.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
18 |
19 | {{ value.label }}
20 |
21 |
22 | {{ value.status.label }}
23 |
24 |
30 |
40 |
46 |
58 |
59 |
65 |
77 |
78 |
79 |
80 |
81 |
186 |
187 |
231 |
--------------------------------------------------------------------------------
/src/views/flow/components/menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
186 |
187 |
279 |
--------------------------------------------------------------------------------