├── src ├── api │ ├── menu.js │ ├── user.js │ ├── role.js │ └── taskDetailsController.js ├── styles │ ├── common-style.less │ ├── mixin.scss │ ├── variables.scss │ ├── element-ui.scss │ ├── transition.scss │ ├── index.scss │ └── sidebar.scss ├── assets │ ├── imgs │ │ └── admin.jpg │ └── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png ├── layout │ ├── components │ │ ├── index.js │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Link.vue │ │ │ ├── Item.vue │ │ │ ├── Logo.vue │ │ │ ├── index.vue │ │ │ └── SidebarItem.vue │ │ ├── AppMain.vue │ │ └── Navbar.vue │ ├── mixin │ │ └── ResizeHandler.js │ └── index.vue ├── App.vue ├── utils │ ├── global.config.js │ ├── get-page-title.js │ ├── auth.js │ ├── validate.js │ ├── myreq.js │ ├── request.js │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── settings.js │ │ ├── app.js │ │ └── user.js ├── icons │ ├── svg │ │ ├── link.svg │ │ ├── user.svg │ │ ├── example.svg │ │ ├── table.svg │ │ ├── password.svg │ │ ├── nested.svg │ │ ├── eye.svg │ │ ├── eye-open.svg │ │ ├── tree.svg │ │ ├── dashboard.svg │ │ └── form.svg │ ├── index.js │ └── svgo.yml ├── settings.js ├── views │ ├── dashboard │ │ └── index.vue │ ├── manage │ │ ├── menus │ │ │ └── index.vue │ │ ├── roles │ │ │ └── index.vue │ │ └── users │ │ │ └── index.vue │ ├── task │ │ ├── crons │ │ │ ├── year.vue │ │ │ ├── month.vue │ │ │ ├── hour.vue │ │ │ ├── secondAndMinute.vue │ │ │ ├── day.vue │ │ │ ├── week.vue │ │ │ └── index.vue │ │ ├── records │ │ │ └── index.vue │ │ ├── errors │ │ │ └── index.vue │ │ └── details │ │ │ └── index.vue │ ├── login │ │ └── index.vue │ └── 404.vue ├── components │ ├── Hamburger │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── Breadcrumb │ │ └── index.vue ├── main.js ├── permission.js └── router │ └── index.js ├── public ├── favicon.ico └── index.html ├── tests └── unit │ ├── .eslintrc.js │ ├── utils │ ├── param2Obj.spec.js │ ├── validate.spec.js │ ├── formatTime.spec.js │ └── parseTime.spec.js │ └── components │ ├── Hamburger.spec.js │ ├── SvgIcon.spec.js │ └── Breadcrumb.spec.js ├── data └── img │ ├── task-core-off.jpg │ ├── task-cron-on.jpg │ ├── task-details-list.jpg │ ├── task-manage-index.jpg │ ├── task-manage-login.jpg │ └── task-records-list.jpg ├── .env.development ├── jsconfig.json ├── .idea ├── vcs.xml ├── modules.xml ├── job-plus-vue.iml └── workspace.xml ├── postcss.config.js ├── babel.config.js ├── mock ├── utils.js ├── table.js ├── index.js ├── user.js └── mock-server.js ├── jest.config.js ├── package.json ├── vue.config.js ├── README.md └── LICENSE /src/api/menu.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/common-style.less: -------------------------------------------------------------------------------- 1 | .common-footer-pagination{ 2 | text-align: right; 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /data/img/task-core-off.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-core-off.jpg -------------------------------------------------------------------------------- /data/img/task-cron-on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-cron-on.jpg -------------------------------------------------------------------------------- /src/assets/imgs/admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/src/assets/imgs/admin.jpg -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | //.env.development 2 | //VUE_APP_BASE_API = '需要请求API' 3 | VUE_APP_BASE_API = 'http://127.0.0.1:18080' -------------------------------------------------------------------------------- /data/img/task-details-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-details-list.jpg -------------------------------------------------------------------------------- /data/img/task-manage-index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-manage-index.jpg -------------------------------------------------------------------------------- /data/img/task-manage-login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-manage-login.jpg -------------------------------------------------------------------------------- /data/img/task-records-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/data/img/task-records-list.jpg -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dianjiu/job-plus-vue/HEAD/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /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/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils/global.config.js: -------------------------------------------------------------------------------- 1 | export const taskStatus = { 2 | 0: { 3 | label: '已停用', 4 | value: 0 5 | }, 6 | 1: { 7 | label: '已启用', 8 | value: 1 9 | } 10 | } -------------------------------------------------------------------------------- /postcss.config.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/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Job Plus Vue' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: 'Job Plus', 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 = 'job_plus_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/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/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 user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dianjiu 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 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/dianjiu/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/job-plus-vue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/manage/menus/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/manage/roles/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/manage/users/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://dianjiu.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mock/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} url 3 | * @returns {Object} 4 | */ 5 | function param2Obj(url) { 6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 7 | if (!search) { 8 | return {} 9 | } 10 | const obj = {} 11 | const searchArr = search.split('&') 12 | searchArr.forEach(v => { 13 | const index = v.indexOf('=') 14 | if (index !== -1) { 15 | const name = v.substring(0, index) 16 | const val = v.substring(index + 1, v.length) 17 | obj[name] = val 18 | } 19 | }) 20 | return obj 21 | } 22 | 23 | module.exports = { 24 | param2Obj 25 | } 26 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/table.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | module.exports = [ 15 | { 16 | url: '/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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 | // eslint-disable-next-line no-prototype-builtins 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /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/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/dianjiu/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 | -------------------------------------------------------------------------------- /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/dianjiu/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/dianjiu/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/dianjiu/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/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRoutes() { 4 | return request({ 5 | url: '/vue-element-admin/routes', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function getRoles() { 11 | return request({ 12 | url: '/vue-element-admin/roles', 13 | method: 'get' 14 | }) 15 | } 16 | 17 | export function addRole(data) { 18 | return request({ 19 | url: '/vue-element-admin/role', 20 | method: 'post', 21 | data 22 | }) 23 | } 24 | 25 | export function updateRole(id, data) { 26 | return request({ 27 | url: `/vue-element-admin/role/${id}`, 28 | method: 'put', 29 | data 30 | }) 31 | } 32 | 33 | export function deleteRole(id) { 34 | return request({ 35 | url: `/vue-element-admin/role/${id}`, 36 | method: 'delete' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /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 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /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/api/taskDetailsController.js: -------------------------------------------------------------------------------- 1 | import req from '@/utils/myreq' 2 | let baseUrl = "/tTaskDetails" 3 | /** 4 | * 任务信息列表 5 | * 6 | */ 7 | export function getTaskList(data) { 8 | return req.postJson(`${baseUrl}/listByPage`,data) 9 | } 10 | 11 | /** 12 | * 任务信息详情 13 | * 14 | */ 15 | export function getTaskInfo(id) { 16 | return req.postJson(`${baseUrl}/get`,{id}) 17 | } 18 | /** 19 | * 删除任务信息详情 20 | * 21 | */ 22 | export function deleteTask(id) { 23 | return req.get(`${baseUrl}/delete`,{id}) 24 | } 25 | /** 26 | * 添加任务信息详情 27 | * 28 | */ 29 | export function insertTask(data) { 30 | return req.postJson(`${baseUrl}/insert`,data) 31 | } 32 | /** 33 | * 修改任务状态 34 | * 35 | */ 36 | export function optionTask(params) { 37 | return req.post(`${baseUrl}/optionTask`,params) 38 | } 39 | /** 40 | * 执行任务 41 | * 42 | */ 43 | export function runTask(data) { 44 | return req.post(`${baseUrl}/runtask`,data) 45 | } 46 | /** 47 | * 编辑任务 48 | * 49 | */ 50 | export function updateTask(data) { 51 | return req.postJson(`${baseUrl}/update`,data) 52 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /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/utils/myreq.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import qs from 'qs' 3 | 4 | export default { 5 | get(url, params) { 6 | return request({ 7 | method: 'get', 8 | url, 9 | params, 10 | headers: { 11 | 'X-Requested-With': 'XMLHttpRequest', 12 | 'withCredentials': true, 13 | } 14 | }) 15 | }, 16 | post(url, data) { 17 | return request({ 18 | method: 'post', 19 | url, 20 | // data: qs.stringify(data), 21 | params: data, 22 | headers: { 23 | 'X-Requested-With': 'XMLHttpRequest', 24 | 'Content-Type': 'application/form-data;charset=UTF-8', 25 | } 26 | }) 27 | }, 28 | postJson(url, data) { 29 | return request({ 30 | method: 'post', 31 | url, 32 | data, 33 | headers: { 34 | 'X-Requested-With': 'XMLHttpRequest', 35 | 'Content-Type': 'application/json;charset=UTF-8', 36 | } 37 | }) 38 | }, 39 | postFile(url, data) { 40 | return request({ 41 | method: 'post', 42 | url, 43 | data, 44 | headers: { 45 | 'X-Requested-With': 'XMLHttpRequest', 46 | 'Content-Type': 'application/application/json;charset=UTF-8', 47 | }, 48 | responseType: 'blob' //设置返回类型 49 | }) 50 | } 51 | } -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const table = require('./table') 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | module.exports = { 54 | mocks, 55 | mockXHR 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: '点九' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | module.exports = [ 27 | // user login 28 | { 29 | url: '/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "job-plus-vue", 3 | "version": "4.4.0", 4 | "description": "A vue admin template with Element UI for job-plus", 5 | "author": "DainjiU ", 6 | "scripts": { 7 | "dev": "vue-cli-service serve", 8 | "build:prod": "vue-cli-service build", 9 | "build:stage": "vue-cli-service build --mode staging", 10 | "preview": "node build/index.js --preview", 11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 13 | "test:ci": "npm run lint && npm run test:unit" 14 | }, 15 | "dependencies": { 16 | "axios": "0.18.1", 17 | "core-js": "3.6.5", 18 | "element-ui": "2.13.2", 19 | "js-cookie": "2.2.0", 20 | "lodash": "^4.17.15", 21 | "normalize.css": "7.0.0", 22 | "nprogress": "0.2.0", 23 | "path-to-regexp": "2.4.0", 24 | "vue": "2.6.10", 25 | "vue-router": "3.0.6", 26 | "vuex": "3.1.0", 27 | "yarn": "^1.22.4" 28 | }, 29 | "devDependencies": { 30 | "@vue/cli-plugin-babel": "4.4.4", 31 | "@vue/cli-plugin-unit-jest": "4.4.4", 32 | "@vue/cli-service": "4.4.4", 33 | "@vue/test-utils": "1.0.0-beta.29", 34 | "autoprefixer": "9.5.1", 35 | "babel-jest": "23.6.0", 36 | "babel-plugin-dynamic-import-node": "2.3.3", 37 | "chalk": "2.4.2", 38 | "connect": "3.6.6", 39 | "html-webpack-plugin": "3.2.0", 40 | "less": "^3.11.3", 41 | "less-loader": "^6.2.0", 42 | "mockjs": "1.0.1-beta3", 43 | "runjs": "4.3.2", 44 | "sass": "1.26.8", 45 | "sass-loader": "8.0.2", 46 | "script-ext-html-webpack-plugin": "2.1.3", 47 | "serve-static": "1.13.2", 48 | "svg-sprite-loader": "4.1.3", 49 | "svgo": "1.2.2", 50 | "vue-template-compiler": "2.6.10" 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions" 55 | ], 56 | "engines": { 57 | "node": ">=8.9", 58 | "npm": ">= 3.0.0" 59 | }, 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /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/zh-CN' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | import '@/styles/common-style.less' 11 | 12 | import App from './App' 13 | import store from './store' 14 | import router from './router' 15 | 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 mock api 22 | * you can execute: mockXHR() 23 | * 24 | * Currently MockJs will be used in the production environment, 25 | * please remove it before going online ! ! ! 26 | */ 27 | Vue.use(ElementUI) 28 | Vue.prototype.$success = (message) => Vue.prototype.$message({ type: 'success', message }) 29 | Vue.prototype.$danger = (message) => Vue.prototype.$message({ type: 'error', message }) 30 | Vue.prototype.$warning = (message) => Vue.prototype.$message({ type: 'warning', message }) 31 | Vue.prototype.$quickConfirm = (title,successCallback, failCallback=function(){}, options={}) => { 32 | Vue.prototype.$confirm( 33 | title, 34 | "提示", 35 | { 36 | confirmButtonText: '确定', 37 | cancelButtonText: '取消', 38 | type: 'warning', 39 | distinguishCancelAndClose: true, 40 | closeOnClickModal:false, 41 | ...options 42 | } 43 | ).then(function() { 44 | successCallback(); 45 | }).catch(function(action) { 46 | if(action == 'cancel') { 47 | failCallback(); 48 | } 49 | }); 50 | }; 51 | if (process.env.NODE_ENV === 'production') { 52 | const { mockXHR } = require('../mock') 53 | mockXHR() 54 | } 55 | 56 | // set ElementUI lang to EN 57 | Vue.use(ElementUI, { locale }) 58 | // 如果想要中文版 element-ui,按如下方式声明 59 | // Vue.use(ElementUI) 60 | 61 | Vue.config.productionTip = false 62 | 63 | new Vue({ 64 | el: '#app', 65 | router, 66 | store, 67 | render: h => h(App) 68 | }) 69 | -------------------------------------------------------------------------------- /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: 60000 // 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 | // if the custom code is not 20000, it is judged as an error. 48 | if (res.code != 200) { 49 | Message({ 50 | message: res.msg || 'Error', 51 | type: 'error', 52 | duration: 3 * 1000 53 | }) 54 | return Promise.reject(new Error(res.msg || 'Error')) 55 | } else { 56 | return res.data 57 | } 58 | }, 59 | error => { 60 | console.log('err' + error) // for debug 61 | Message({ 62 | message: error.message, 63 | type: 'error', 64 | duration: 3 * 1000 65 | }) 66 | return Promise.reject(error) 67 | } 68 | ) 69 | 70 | export default service 71 | -------------------------------------------------------------------------------- /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 = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | next() 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(`/login?redirect=${to.path}`) 56 | // NProgress.done() 57 | // } 58 | // } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 42 | 78 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/user' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | token: getToken(), 8 | name: '', 9 | avatar: '' 10 | } 11 | } 12 | 13 | const state = getDefaultState() 14 | 15 | const mutations = { 16 | RESET_STATE: (state) => { 17 | Object.assign(state, getDefaultState()) 18 | }, 19 | SET_TOKEN: (state, token) => { 20 | state.token = token 21 | }, 22 | SET_NAME: (state, name) => { 23 | state.name = name 24 | }, 25 | SET_AVATAR: (state, avatar) => { 26 | state.avatar = avatar 27 | } 28 | } 29 | 30 | const actions = { 31 | // user login 32 | login({ commit }, userInfo) { 33 | const { username, password } = userInfo 34 | return new Promise((resolve, reject) => { 35 | login({ username: username.trim(), password: password }).then(response => { 36 | const { data } = response 37 | commit('SET_TOKEN', data.token) 38 | setToken(data.token) 39 | resolve() 40 | }).catch(error => { 41 | reject(error) 42 | }) 43 | }) 44 | }, 45 | 46 | // get user info 47 | getInfo({ commit, state }) { 48 | return new Promise((resolve, reject) => { 49 | getInfo(state.token).then(response => { 50 | const { data } = response 51 | 52 | if (!data) { 53 | return reject('Verification failed, please Login again.') 54 | } 55 | 56 | const { name, avatar } = data 57 | 58 | commit('SET_NAME', name) 59 | commit('SET_AVATAR', avatar) 60 | resolve(data) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // user logout 68 | logout({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | removeToken() // must remove token first 72 | resetRouter() 73 | commit('RESET_STATE') 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // remove token 82 | resetToken({ commit }) { 83 | return new Promise(resolve => { 84 | removeToken() // must remove token first 85 | commit('RESET_STATE') 86 | resolve() 87 | }) 88 | } 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | mutations, 95 | actions 96 | } 97 | 98 | -------------------------------------------------------------------------------- /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 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map(route => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach(i => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path) 41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dianjiu 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 | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0 || !time) { 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')) { 21 | if ((/^[0-9]+$/.test(time))) { 22 | // support "1548221490638" 23 | time = parseInt(time) 24 | } else { 25 | // support safari 26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari 27 | time = time.replace(new RegExp(/-/gm), '/') 28 | } 29 | } 30 | 31 | if ((typeof time === 'number') && (time.toString().length === 10)) { 32 | time = time * 1000 33 | } 34 | date = new Date(time) 35 | } 36 | const formatObj = { 37 | y: date.getFullYear(), 38 | m: date.getMonth() + 1, 39 | d: date.getDate(), 40 | h: date.getHours(), 41 | i: date.getMinutes(), 42 | s: date.getSeconds(), 43 | a: date.getDay() 44 | } 45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 46 | const value = formatObj[key] 47 | // Note: getDay() returns 0 on Sunday 48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 49 | return value.toString().padStart(2, '0') 50 | }) 51 | return time_str 52 | } 53 | 54 | /** 55 | * @param {number} time 56 | * @param {string} option 57 | * @returns {string} 58 | */ 59 | export function formatTime(time, option) { 60 | if (('' + time).length === 10) { 61 | time = parseInt(time) * 1000 62 | } else { 63 | time = +time 64 | } 65 | const d = new Date(time) 66 | const now = Date.now() 67 | 68 | const diff = (now - d) / 1000 69 | 70 | if (diff < 30) { 71 | return '刚刚' 72 | } else if (diff < 3600) { 73 | // less 1 hour 74 | return Math.ceil(diff / 60) + '分钟前' 75 | } else if (diff < 3600 * 24) { 76 | return Math.ceil(diff / 3600) + '小时前' 77 | } else if (diff < 3600 * 24 * 2) { 78 | return '1天前' 79 | } 80 | if (option) { 81 | return parseTime(time, option) 82 | } else { 83 | return ( 84 | d.getMonth() + 85 | 1 + 86 | '月' + 87 | d.getDate() + 88 | '日' + 89 | d.getHours() + 90 | '时' + 91 | d.getMinutes() + 92 | '分' 93 | ) 94 | } 95 | } 96 | 97 | /** 98 | * @param {string} url 99 | * @returns {Object} 100 | */ 101 | export function param2Obj(url) { 102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 103 | if (!search) { 104 | return {} 105 | } 106 | const obj = {} 107 | const searchArr = search.split('&') 108 | searchArr.forEach(v => { 109 | const index = v.indexOf('=') 110 | if (index !== -1) { 111 | const name = v.substring(0, index) 112 | const val = v.substring(index + 1, v.length) 113 | obj[name] = val 114 | } 115 | }) 116 | return obj 117 | } 118 | -------------------------------------------------------------------------------- /src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 64 | 65 | 143 | -------------------------------------------------------------------------------- /src/views/task/crons/year.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 125 | 126 | 131 | -------------------------------------------------------------------------------- /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://dianjiu.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'/'el-icon-x' 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 | { 41 | path: '/404', 42 | component: () => import('@/views/404'), 43 | hidden: true 44 | }, 45 | 46 | { 47 | path: '/', 48 | component: Layout, 49 | redirect: '/task', 50 | // children: [{ 51 | // path: 'dashboard', 52 | // name: '仪表盘', 53 | // component: () => import('@/views/dashboard/index'), 54 | // meta: { title: '仪表盘', icon: 'dashboard' } 55 | // }] 56 | }, 57 | 58 | { 59 | path: '/task', 60 | component: Layout, 61 | redirect: '/task/details', 62 | name: '任务管理', 63 | meta: { title: '任务管理', icon: 'el-icon-s-help' }, 64 | children: [ 65 | { 66 | path: 'details', 67 | name: '任务列表', 68 | component: () => import('@/views/task/details/index'), 69 | meta: { title: '任务列表', icon: 'el-icon-folder-opened' } 70 | }, 71 | { 72 | path: 'records', 73 | name: '执行记录', 74 | component: () => import('@/views/task/records/index'), 75 | meta: { title: '执行记录', icon: 'el-icon-document' } 76 | }, 77 | { 78 | path: 'errors', 79 | name: '异常日志', 80 | component: () => import('@/views/task/errors/index'), 81 | meta: { title: '异常日志', icon: 'el-icon-document-delete' } 82 | }, 83 | { 84 | path: 'corns', 85 | name: '生成器', 86 | component: () => import('@/views/task/crons/index'), 87 | meta: { title: '生成器', icon: 'el-icon-view' } 88 | } 89 | ] 90 | }, 91 | 92 | { 93 | path: '/manage', 94 | component: Layout, 95 | redirect: '/manage/users', 96 | name: '权限管理', 97 | meta: { title: '权限管理', icon: 'el-icon-s-tools' }, 98 | children: [ 99 | { 100 | path: 'users', 101 | name: '用户管理', 102 | component: () => import('@/views/manage/users/index'), 103 | meta: { title: '用户管理', icon: 'el-icon-user' } 104 | }, 105 | { 106 | path: 'roles', 107 | name: '角色管理', 108 | component: () => import('@/views/manage/roles/index'), 109 | meta: { title: '角色管理', icon: 'el-icon-news' } 110 | }, 111 | { 112 | path: 'menus', 113 | name: '菜单管理', 114 | component: () => import('@/views/manage/menus/index'), 115 | meta: { title: '菜单管理', icon: 'el-icon-mouse' } 116 | } 117 | ] 118 | }, 119 | 120 | // 404 page must be placed at the end !!! 121 | { path: '*', redirect: '/404', hidden: true } 122 | ] 123 | 124 | const createRouter = () => new Router({ 125 | // mode: 'history', // require service support 126 | scrollBehavior: () => ({ y: 0 }), 127 | routes: constantRoutes 128 | }) 129 | 130 | const router = createRouter() 131 | 132 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 133 | export function resetRouter() { 134 | const newRouter = createRouter() 135 | router.matcher = newRouter.matcher // reset router 136 | } 137 | 138 | export default router 139 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 1619683370413 55 | 61 | 62 | 63 | 64 | 66 | 67 | 76 | 77 | -------------------------------------------------------------------------------- /src/views/task/crons/month.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 138 | 139 | 144 | -------------------------------------------------------------------------------- /src/views/task/crons/hour.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 137 | 138 | 143 | -------------------------------------------------------------------------------- /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 || 'Job Plus Admin' // page title 10 | 11 | // If your port is set to 80, 12 | // use administrator privileges to execute the command line. 13 | // For example, Mac: sudo npm run 14 | // You can change the port by the following methods: 15 | // port = 2022 npm run dev OR npm run dev --port = 2022 16 | const port = process.env.port || process.env.npm_config_port || 8080 // dev port 17 | 18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 19 | module.exports = { 20 | /** 21 | * You will need to set publicPath if you plan to deploy your site under a sub path, 22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 23 | * then publicPath should be set to "/bar/". 24 | * In most cases please use '/' !!! 25 | * Detail: https://cli.vuejs.org/config/#publicpath 26 | */ 27 | publicPath: './', 28 | outputDir: 'dist', 29 | assetsDir: 'static', 30 | lintOnSave: process.env.NODE_ENV === 'development', 31 | productionSourceMap: false, 32 | devServer: { 33 | port: port, 34 | open: true, 35 | overlay: { 36 | warnings: false, 37 | errors: true 38 | }, 39 | /*proxy: { 40 | '/api':{ 41 | target:'http://127.0.0.1:18080', 42 | changeOrigin:true, 43 | pathRewrite:{ 44 | '^/api':'' 45 | } 46 | }, 47 | },*/ 48 | before: require('./mock/mock-server.js') 49 | }, 50 | configureWebpack: { 51 | // provide the app's title in webpack's name field, so that 52 | // it can be accessed in index.html to inject the correct title. 53 | name: name, 54 | resolve: { 55 | alias: { 56 | '@': resolve('src') 57 | } 58 | } 59 | }, 60 | chainWebpack(config) { 61 | // it can improve the speed of the first screen, it is recommended to turn on preload 62 | config.plugin('preload').tap(() => [ 63 | { 64 | rel: 'preload', 65 | // to ignore runtime.js 66 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 67 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 68 | include: 'initial' 69 | } 70 | ]) 71 | 72 | // when there are many pages, it will cause too many meaningless requests 73 | config.plugins.delete('prefetch') 74 | 75 | // set svg-sprite-loader 76 | config.module 77 | .rule('svg') 78 | .exclude.add(resolve('src/icons')) 79 | .end() 80 | config.module 81 | .rule('icons') 82 | .test(/\.svg$/) 83 | .include.add(resolve('src/icons')) 84 | .end() 85 | .use('svg-sprite-loader') 86 | .loader('svg-sprite-loader') 87 | .options({ 88 | symbolId: 'icon-[name]' 89 | }) 90 | .end() 91 | 92 | config 93 | .when(process.env.NODE_ENV !== 'development', 94 | config => { 95 | config 96 | .plugin('ScriptExtHtmlWebpackPlugin') 97 | .after('html') 98 | .use('script-ext-html-webpack-plugin', [{ 99 | // `runtime` must same as runtimeChunk name. default is `runtime` 100 | inline: /runtime\..*\.js$/ 101 | }]) 102 | .end() 103 | config 104 | .optimization.splitChunks({ 105 | chunks: 'all', 106 | cacheGroups: { 107 | libs: { 108 | name: 'chunk-libs', 109 | test: /[\\/]node_modules[\\/]/, 110 | priority: 10, 111 | chunks: 'initial' // only package third parties that are initially dependent 112 | }, 113 | elementUI: { 114 | name: 'chunk-elementUI', // split elementUI into a single package 115 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 116 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 117 | }, 118 | commons: { 119 | name: 'chunk-commons', 120 | test: resolve('src/components'), // can customize your rules 121 | minChunks: 3, // minimum common number 122 | priority: 5, 123 | reuseExistingChunk: true 124 | } 125 | } 126 | }) 127 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 128 | config.optimization.runtimeChunk('single') 129 | } 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/views/task/crons/secondAndMinute.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 141 | 142 | 147 | -------------------------------------------------------------------------------- /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 | .sub-el-icon { 61 | margin-right: 12px; 62 | margin-left: -2px; 63 | } 64 | 65 | .el-menu { 66 | border: none; 67 | height: 100%; 68 | width: 100% !important; 69 | } 70 | 71 | // menu hover 72 | .submenu-title-noDropdown, 73 | .el-submenu__title { 74 | &:hover { 75 | background-color: $menuHover !important; 76 | } 77 | } 78 | 79 | .is-active>.el-submenu__title { 80 | color: $subMenuActiveText !important; 81 | } 82 | 83 | & .nest-menu .el-submenu>.el-submenu__title, 84 | & .el-submenu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | background-color: $subMenuBg !important; 87 | 88 | &:hover { 89 | background-color: $subMenuHover !important; 90 | } 91 | } 92 | } 93 | 94 | .hideSidebar { 95 | .sidebar-container { 96 | width: 54px !important; 97 | } 98 | 99 | .main-container { 100 | margin-left: 54px; 101 | } 102 | 103 | .submenu-title-noDropdown { 104 | padding: 0 !important; 105 | position: relative; 106 | 107 | .el-tooltip { 108 | padding: 0 !important; 109 | 110 | .svg-icon { 111 | margin-left: 20px; 112 | } 113 | 114 | .sub-el-icon { 115 | margin-left: 19px; 116 | } 117 | } 118 | } 119 | 120 | .el-submenu { 121 | overflow: hidden; 122 | 123 | &>.el-submenu__title { 124 | padding: 0 !important; 125 | 126 | .svg-icon { 127 | margin-left: 20px; 128 | } 129 | 130 | .sub-el-icon { 131 | margin-left: 19px; 132 | } 133 | 134 | .el-submenu__icon-arrow { 135 | display: none; 136 | } 137 | } 138 | } 139 | 140 | .el-menu--collapse { 141 | .el-submenu { 142 | &>.el-submenu__title { 143 | &>span { 144 | height: 0; 145 | width: 0; 146 | overflow: hidden; 147 | visibility: hidden; 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .el-menu--collapse .el-menu .el-submenu { 156 | min-width: $sideBarWidth !important; 157 | } 158 | 159 | // mobile responsive 160 | .mobile { 161 | .main-container { 162 | margin-left: 0px; 163 | } 164 | 165 | .sidebar-container { 166 | transition: transform .28s; 167 | width: $sideBarWidth !important; 168 | } 169 | 170 | &.hideSidebar { 171 | .sidebar-container { 172 | pointer-events: none; 173 | transition-duration: 0.3s; 174 | transform: translate3d(-$sideBarWidth, 0, 0); 175 | } 176 | } 177 | } 178 | 179 | .withoutAnimation { 180 | 181 | .main-container, 182 | .sidebar-container { 183 | transition: none; 184 | } 185 | } 186 | } 187 | 188 | // when menu collapsed 189 | .el-menu--vertical { 190 | &>.el-menu { 191 | .svg-icon { 192 | margin-right: 16px; 193 | } 194 | .sub-el-icon { 195 | margin-right: 12px; 196 | margin-left: -2px; 197 | } 198 | } 199 | 200 | .nest-menu .el-submenu>.el-submenu__title, 201 | .el-menu-item { 202 | &:hover { 203 | // you can use $subMenuHover 204 | background-color: $menuHover !important; 205 | } 206 | } 207 | 208 | // the scroll bar appears when the subMenu is too long 209 | >.el-menu--popup { 210 | max-height: 100vh; 211 | overflow-y: auto; 212 | 213 | &::-webkit-scrollbar-track-piece { 214 | background: #d3dce6; 215 | } 216 | 217 | &::-webkit-scrollbar { 218 | width: 6px; 219 | } 220 | 221 | &::-webkit-scrollbar-thumb { 222 | background: #99a9bf; 223 | border-radius: 20px; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/views/task/records/index.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 158 | 159 | 178 | -------------------------------------------------------------------------------- /src/views/task/crons/day.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 155 | 156 | 161 | -------------------------------------------------------------------------------- /src/views/task/crons/week.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 152 | 153 | 158 | -------------------------------------------------------------------------------- /src/views/task/crons/index.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 173 | 174 | 183 | 184 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 123 | 124 | 170 | 171 | 222 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # job-plus-vue 2 | 3 | > 基于Vue的Job Plus的后管UI 4 | 5 | ![VUE](https://img.shields.io/badge/vue-4.0-green.svg?style=flat-square) [![LICENSE](https://img.shields.io/github/license/dianjiu/job-plus-vue.svg?style=flat-square)](https://github.com/dianjiu/job-plus-vue/blob/master/LICENSE) [![star](https://img.shields.io/github/stars/dianjiu/job-plus-vue.svg?label=Stars&style=social)](https://github.com/dianjiu/job-plus-vue) [![star](https://gitee.com/dianjiu/job-plus-vue/badge/star.svg?theme=white)](https://gitee.com/dianjiu/job-plus-vue) 6 | 7 | 8 | [https://github.com/dianjiu/job-plus-vue](https://github.com/dianjiu/job-plus-vue) 9 | 10 | [https://gitee.io/dianjiu/job-plus-vue](https://gitee.io/dianjiu/job-plus-vue) 11 | 12 | ## 相关项目 job-plus 13 | 14 | > 基于SpringBoot的轻量级定时任务管理系统 15 | 16 | ![SpringBoot](https://img.shields.io/badge/springboot-2.3.1-green.svg?style=flat-square) [![LICENSE](https://img.shields.io/github/license/dianjiu/job-plus.svg?style=flat-square)](https://github.com/dianjiu/job-plus/blob/master/LICENSE) [![star](https://img.shields.io/github/stars/dianjiu/job-plus.svg?label=Stars&style=social)](https://github.com/dianjiu/job-plus) [![star](https://gitee.com/dianjiu/job-plus/badge/star.svg?theme=white)](https://gitee.com/dianjiu/job-plus) 17 | 18 | 19 | [https://github.com/dianjiu/job-plus](https://github.com/dianjiu/job-plus) 20 | 21 | [https://gitee.io/dianjiu/job-plus](https://gitee.io/dianjiu/job-plus) 22 | 23 | ## 项目功能 24 | 1. 架构潮流:系统采用SpringBoot+VUE前后端分离,前端单独部署,Nginx负载均衡 25 | 2. 接口友好:同时支持swagger2、knife4j两种可视化接口API调试,支持离线接口文档; 26 | 3. 任务管理:支持通过Web页面对任务进行CRUD操作,可视化界面,快速上手; 27 | 4. 执行记录:支持通过web页面在线查看调度结果、执行结果、下次执行时间; 28 | 5. 实时日志:支持通过web页面实时查看执行器输出的完整的执行日志; 29 | 6. 唯一搜索:支持通过web界面根据jobname或jobgroup进行全局唯一查询 30 | 7. 强自定义:支持在线配置定时任务请求类型、请求路径、请求参数、Cron表达式,即时生效; 31 | 8. 动态控制:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; 32 | 9. 执行策略:支持丰富的执行策略,包括:Get请求、PostJson请求、PostFrom表单请求; 33 | 10. 自动注册:周期性自动注册任务, 同时,也支持手动录入定时任务地址; 34 | 11. 自动执行:系统会自动发现注册的任务并触发执行,同时,也支持手动触发-立即执行; 35 | 12. 用户管理:支持在线管理系统用户、角色、菜单,默认管理员、开发者、普通用户三种角色; 36 | 13. 权限控制:支持在线权限控制,管理员拥有全量权限,开发者拥有除角色管理外的所有权限,普通用户仅支持任务管理相关权限; 37 | 14. 集群部署:支持分布式执行,系统支持集群部署,可保证任务执行的高可用; 38 | 15. 弹性调度:一旦有任务机器上线或者下线,下次调度时将会重新分配任务; 39 | 16. 路由策略:系统集群部署时提供丰富的路由策略,包括:轮询、随机、故障转移、忙碌转移等常用策略; 40 | 17. 故障转移:任务路由策略选择"故障转移"情况下,如果集群中某一台机器故障,将会自动切换到一台正常的执行器发送调度请求; 41 | 18. 阻塞策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; 42 | 19. 超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; 43 | 20. 重试机制:支持自定义任务重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; 44 | 21. 消息工厂:默认提供邮件工厂的方式推送消息,同时预留扩展接口,可方便的扩展短信、钉钉等消息方式; 45 | 22. 邮件告警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; 46 | 23. 运行报表:支持实时查看运行数据,以及调度报表,如调度日期分布图,任务组执行比例比例分布图等; 47 | 24. 事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,提供触发任务单次执行的API服务; 48 | 25. 脚本任务:支持以GLUE分布式平台开发和运行脚本任务,包括Shell、Python、NodeJS等类型脚本; 49 | 26. 多线并发:系统支持多线程触发调度运行,确保调度精确执行,不被堵塞; 50 | 27. 降级隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; 51 | 28. Gradle: 将会把最新稳定版推送到gradle中央仓库, 方便用户接入和使用; 52 | 29. Maven: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用; 53 | 30. 一致性:基于Redis分布式锁保证集群分布式调度的最终一致性, 一次任务调度只会触发一次执行; 54 | 31. 全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行; 55 | 32. 跨语言:系统提供语言无关的 RESTFUL API 服务,第三方任意语言可据此对接Task Manage; 56 | 33. 国际化:后管系统支持国际化设置,提供中文、英文两种可选语言,默认为中文; 57 | 34. 容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; 58 | 59 | ## 开发进度 60 | - [x] 架构潮流:系统采用SpringBoot+VUE前后端分离,前端单独部署,Nginx负载均衡 61 | - [x] 接口友好:同时支持swagger2、knife4j两种可视化接口API调试,支持离线接口文档; 62 | - [x] 任务管理:支持通过Web页面对任务进行CRUD操作,可视化界面,快速上手; 63 | - [x] 执行记录:支持通过web页面在线查看调度结果、执行结果、下次执行时间; 64 | - [x] 实时日志:支持通过web页面实时查看执行器输出的完整的执行日志; 65 | - [x] 唯一搜索:支持通过web界面根据jobname或jobgroup进行全局唯一查询; 66 | - [x] 强自定义:支持在线配置定时任务请求类型、请求路径、请求参数、Cron表达式,即时生效; 67 | - [x] 动态控制:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; 68 | - [x] 执行策略:支持丰富的执行策略,包括:Get请求、PostJson请求、PostFrom表单请求; 69 | - [x] 自动注册:周期性自动注册任务, 同时,也支持手动录入定时任务地址; 70 | - [x] 自动执行:系统会自动发现注册的任务并触发执行,同时,也支持手动触发-立即执行; 71 | - [ ] 用户管理:支持在线管理系统用户、角色、菜单,默认管理员、开发者、普通用户三种角色; 72 | - [ ] 权限控制:支持在线权限控制,管理员拥有全量权限,开发者拥有除角色管理外的所有权限,普通用户仅支持任务管理相关权限; 73 | - [ ] 集群部署:支持分布式执行,系统支持集群部署,可保证任务执行的高可用; 74 | - [ ] 弹性调度:一旦有任务机器上线或者下线,下次调度时将会重新分配任务; 75 | - [ ] 路由策略:系统集群部署时提供丰富的路由策略,包括:轮询、随机、故障转移、忙碌转移等常用策略; 76 | - [ ] 故障转移:任务路由策略选择"故障转移"情况下,如果集群中某一台机器故障,将会自动切换到一台正常的执行器发送调度请求; 77 | - [ ] 阻塞策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; 78 | - [ ] 超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; 79 | - [ ] 重试机制:支持自定义任务重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; 80 | - [ ] 消息工厂:默认提供邮件工厂的方式推送消息,同时预留扩展接口,可方便的扩展短信、钉钉等消息方式; 81 | - [ ] 邮件告警:任务失败时支持邮件报警,支持配置多邮件地址群发报警邮件; 82 | - [ ] 运行报表:支持实时查看运行数据,以及调度报表,如调度日期分布图,任务组执行比例比例分布图等; 83 | - [x] 事件触发:除了"Cron方式"和"任务依赖方式"触发任务执行之外,提供触发任务单次执行的API服务; 84 | - [ ] 脚本任务:支持以GLUE分布式平台开发和运行脚本任务,包括Shell、Python、NodeJS等类型脚本; 85 | - [ ] 多线并发:系统支持多线程触发调度运行,确保调度精确执行,不被堵塞; 86 | - [ ] 降级隔离:调度线程池进行隔离拆分,慢任务自动降级进入"Slow"线程池,避免耗尽调度线程,提高系统稳定性; 87 | - [ ] Gradle: 将会把最新稳定版推送到gradle中央仓库, 方便用户接入和使用; 88 | - [ ] Maven: 将会把最新稳定版推送到maven中央仓库, 方便用户接入和使用; 89 | - [ ] 一致性:基于Redis分布式锁保证集群分布式调度的最终一致性, 一次任务调度只会触发一次执行; 90 | - [ ] 全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行; 91 | - [x] 跨语言:系统提供语言无关的 RESTFUL API 服务,第三方任意语言可据此对接Task Manage; 92 | - [x] 国际化:后管系统支持国际化设置,提供中文、英文两种可选语言,默认为中文; 93 | - [ ] 容器化:提供官方docker镜像,并实时更新推送dockerhub,进一步实现产品开箱即用; 94 | 95 | ## 在线演示 96 | 97 | http://jobplus.dianjiu.org.cn (部署中。。。) 98 | 99 | 测试账号 测试密码 100 | 101 | admin admin 102 | 103 | user user 104 | 105 | 106 | ## Build Setup 107 | 108 | ```bash 109 | # 克隆项目 110 | git clone https://github.com/dianjiu/job-plus-vue.git 111 | 112 | # 进入项目目录 113 | cd job-plus-vue 114 | 115 | # 安装依赖 116 | yarn install 117 | 118 | # 启动服务 119 | yarn run dev 120 | ``` 121 | 122 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 123 | 124 | ## 发布 125 | 126 | ```bash 127 | # 构建测试环境 128 | yarn run build:stage 129 | 130 | # 构建生产环境 131 | yarn run build:prod 132 | ``` 133 | 134 | ## 其它 135 | 136 | ```bash 137 | # 预览发布环境效果 138 | yarn run preview 139 | 140 | # 预览发布环境效果 + 静态资源分析 141 | yarn run preview -- --report 142 | 143 | # 代码格式检查 144 | yarn run lint 145 | 146 | # 代码格式检查并自动修复 147 | yarn run lint -- --fix 148 | ``` 149 | 150 | ## 项目图片 151 | 152 | ### 页面演示 153 | > 登录页 154 | 155 | ![登录页](./data/img/task-manage-login.jpg) 156 | 157 | > 仪表盘 158 | 159 | ![仪表盘](./data/img/task-manage-index.jpg) 160 | 161 | > 任务管理 =》任务列表 162 | 163 | ![仪表盘](./data/img/task-details-list.jpg) 164 | 165 | > 任务管理 =》执行记录 166 | 167 | ![仪表盘](./data/img/task-records-list.jpg) 168 | 169 | > 任务管理 =》生成器 (关闭cron生成器) 170 | 171 | ![仪表盘](./data/img/task-core-off.jpg) 172 | 173 | > 任务管理 =》生成器 (打开cron生成器) 174 | 175 | ![仪表盘](./data/img/task-cron-on.jpg) 176 | 177 | ## 最后致谢 178 | 感谢以下开源项目提供的项目参考 179 | - https://github.com/ElemeFE/element 180 | - https://gitee.com/lindeyi/vue-cron 181 | - https://github.com/xuxueli/xxl-job 182 | - https://gitee.com/52itstyle/spring-boot-task 183 | - https://github.com/helloflygit/springboot-quartz 184 | - https://github.com/simonsfan/springboot-quartz-demo 185 | - https://github.com/PanJiaChen/vue-admin-template 186 | -------------------------------------------------------------------------------- /src/views/task/errors/index.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 201 | 202 | 226 | -------------------------------------------------------------------------------- /src/views/task/details/index.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 285 | 286 | 307 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 点九开源 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------