├── .eslintignore
├── art
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── babel.config.js
├── tests
└── unit
│ ├── .eslintrc.js
│ ├── components
│ ├── Hamburger.spec.js
│ ├── SvgIcon.spec.js
│ └── Breadcrumb.spec.js
│ └── utils
│ ├── validate.spec.js
│ ├── parseTime.spec.js
│ └── formatTime.spec.js
├── public
├── favicon.ico
└── index.html
├── .travis.yml
├── .env.production
├── src
├── assets
│ └── 404_images
│ │ ├── 404.png
│ │ └── 404_cloud.png
├── App.vue
├── utils
│ ├── get-page-title.js
│ ├── auth.js
│ ├── validate.js
│ ├── scroll-to.js
│ ├── request.js
│ └── index.js
├── icons
│ ├── svg
│ │ ├── link.svg
│ │ ├── fullscreen.svg
│ │ ├── user.svg
│ │ ├── example.svg
│ │ ├── table.svg
│ │ ├── password.svg
│ │ ├── nested.svg
│ │ ├── eye.svg
│ │ ├── eye-open.svg
│ │ ├── exit-fullscreen.svg
│ │ ├── tree.svg
│ │ ├── dashboard.svg
│ │ └── form.svg
│ ├── index.js
│ └── svgo.yml
├── layout
│ ├── components
│ │ ├── index.js
│ │ ├── Sidebar
│ │ │ ├── Item.vue
│ │ │ ├── Link.vue
│ │ │ ├── FixiOSBug.js
│ │ │ ├── index.vue
│ │ │ ├── Logo.vue
│ │ │ └── SidebarItem.vue
│ │ ├── AppMain.vue
│ │ ├── TagsView
│ │ │ ├── ScrollPane.vue
│ │ │ └── index.vue
│ │ └── Navbar.vue
│ ├── mixin
│ │ └── ResizeHandler.js
│ └── index.vue
├── views
│ ├── redirect
│ │ └── index.vue
│ ├── dashboard
│ │ ├── steps.js
│ │ └── index.vue
│ ├── add
│ │ ├── newAccount.vue
│ │ └── newVersion.vue
│ ├── 404.vue
│ ├── login
│ │ └── index.vue
│ └── list
│ │ ├── accounts.vue
│ │ └── versions.vue
├── settings.js
├── store
│ ├── getters.js
│ ├── modules
│ │ ├── settings.js
│ │ ├── app.js
│ │ ├── permission.js
│ │ ├── account.js
│ │ └── tagsView.js
│ └── index.js
├── styles
│ ├── mixin.scss
│ ├── variables.scss
│ ├── element-ui.scss
│ ├── transition.scss
│ ├── index.scss
│ └── sidebar.scss
├── components
│ ├── SvgIcon
│ │ └── index.vue
│ ├── Screenfull
│ │ └── index.vue
│ ├── Hamburger
│ │ └── index.vue
│ ├── Breadcrumb
│ │ └── index.vue
│ └── Pagination
│ │ └── index.vue
├── main.js
├── api
│ ├── update.js
│ └── account.js
├── permission.js
└── router
│ └── index.js
├── .env.staging
├── .postcssrc.js
├── .gitignore
├── .editorconfig
├── .env.development
├── README.md
├── jest.config.js
├── LICENSE
├── mock
├── index.js
├── mock-server.js
├── update.js
└── account.js
├── package.json
├── vue.config.js
└── .eslintrc.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | src/assets
3 | public
4 | dist
5 |
--------------------------------------------------------------------------------
/art/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/1.png
--------------------------------------------------------------------------------
/art/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/2.png
--------------------------------------------------------------------------------
/art/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/3.png
--------------------------------------------------------------------------------
/art/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/4.png
--------------------------------------------------------------------------------
/art/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/5.png
--------------------------------------------------------------------------------
/art/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/art/6.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: stable
3 | script: npm run test
4 | notifications:
5 | email: false
6 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'production'
3 |
4 | # base api
5 | VUE_APP_BASE_URL = 'http://localhost:1111'
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuexiangjys/xupdate-management/HEAD/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/.env.staging:
--------------------------------------------------------------------------------
1 | NODE_ENV = production
2 |
3 | # just a flag
4 | ENV = 'staging'
5 |
6 | # base api
7 | VUE_APP_BASE_API = 'http://localhost:8088/mock'
8 |
9 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | 'plugins': {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | 'autoprefixer': {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | package-lock.json
8 | tests/**/coverage/
9 |
10 | # Editor directories and files
11 | .idea
12 | .vscode
13 | *.suo
14 | *.ntvs*
15 | *.njsproj
16 | *.sln
17 |
--------------------------------------------------------------------------------
/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || '版本更新管理'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | default as Navbar
3 | }
4 | from './Navbar'
5 | export {
6 | default as Sidebar
7 | }
8 | from './Sidebar'
9 | export {
10 | default as AppMain
11 | }
12 | from './AppMain'
13 | export {
14 | default as TagsView
15 | }
16 | from './TagsView/index.vue'
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | insert_final_newline = false
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/src/views/redirect/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/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/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'xupdate_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/icons/svg/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: '版本更新管理',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether need tagsView
8 | */
9 | tagsView: true,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether fix the header
14 | */
15 | fixedHeader: false,
16 |
17 | /**
18 | * @type {boolean} true | false
19 | * @description Whether show the logo in sidebar
20 | */
21 | sidebarLogo: false
22 | }
23 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | isFirstRun: state => state.app.isFirstRun,
5 | visitedViews: state => state.tagsView.visitedViews,
6 | cachedViews: state => state.tagsView.cachedViews,
7 | permission_routes: state => state.permission.routes,
8 | token: state => state.account.token,
9 | avatar: state => state.account.avatar,
10 | name: state => state.account.name
11 | }
12 | export default getters
13 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validName(str) {
18 | return /^[a-zA-Z0-9_-]{4,16}$/.test(str)
19 | }
20 |
21 | export function isEmpty(obj) {
22 | if (typeof obj == "undefined" || obj == null || obj == "") {
23 | return true;
24 | } else {
25 | return false;
26 | }
27 | }
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'development'
3 |
4 | # base url
5 | VUE_APP_BASE_URL = 'http://localhost:8088/mock'
6 |
7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable,
8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled.
9 | # It only does one thing by converting all import() to require().
10 | # This configuration can significantly increase the speed of hot updates,
11 | # when you have a large number of pages.
12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
13 |
14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true
15 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
32 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= webpackConfig.name %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xupdate-management
2 |
3 | > 使用Vue.js编写的版本更新管理后台,为XUpdate提供版本更新管理
4 |
5 | ## 运行
6 |
7 | ``` bash
8 | # install dependencies
9 | npm install
10 |
11 | # serve for development with hot reload at http://localhost:8088/mock
12 | npm run dev
13 |
14 | # preview for production at http://localhost:1111
15 | npm run preview
16 |
17 | # package
18 | npm run build:prod
19 |
20 | ```
21 |
22 | ## 界面预览
23 |
24 | 
25 |
26 | 
27 |
28 | 
29 |
30 | 
31 |
32 | 
33 |
34 | 
35 |
36 | ## 关联链接
37 |
38 | * [XUpdate 一个轻量级、高可用性的Android版本更新框架](https://github.com/xuexiangjys/XUpdate)
39 |
40 | * [为XUpdate提供后台API服务](https://github.com/xuexiangjys/XUpdateService)
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const {
4 | showSettings,
5 | tagsView,
6 | fixedHeader,
7 | sidebarLogo,
8 | } = defaultSettings
9 |
10 | const state = {
11 | tagsView: tagsView,
12 | fixedHeader: fixedHeader,
13 | sidebarLogo: sidebarLogo
14 | }
15 |
16 | const mutations = {
17 | CHANGE_SETTING: (state, {
18 | key,
19 | value
20 | }) => {
21 | if (state.hasOwnProperty(key)) {
22 | state[key] = value
23 | }
24 | }
25 | }
26 |
27 | const actions = {
28 | changeSetting({
29 | commit
30 | }, data) {
31 | commit('CHANGE_SETTING', data)
32 | }
33 | }
34 |
35 | export default {
36 | namespaced: true,
37 | state,
38 | mutations,
39 | actions
40 | }
41 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
1 | import { validLoginName, isExternal } from '@/utils/validate.js'
2 |
3 | describe('Utils:validate', () => {
4 | it('validLoginName', () => {
5 | expect(validLoginName('admin')).toBe(true)
6 | expect(validLoginName('editor')).toBe(true)
7 | expect(validLoginName('xxxx')).toBe(false)
8 | })
9 | it('isExternal', () => {
10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13 | expect(isExternal('/dashboard')).toBe(false)
14 | expect(isExternal('./dashboard')).toBe(false)
15 | expect(isExternal('dashboard')).toBe(false)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 |
5 | Vue.use(Vuex)
6 |
7 | // https://webpack.js.org/guides/dependency-management/#requirecontext
8 | const modulesFiles = require.context('./modules', false, /\.js$/)
9 |
10 | // you do not need `import app from './modules/app'`
11 | // it will auto require all vuex module from modules file
12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => {
13 | // set './app.js' => 'app'
14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
15 | const value = modulesFiles(modulePath)
16 | modules[moduleName] = value.default
17 | return modules
18 | }, {})
19 |
20 | const store = new Vuex.Store({
21 | modules,
22 | getters
23 | })
24 |
25 | export default store
26 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/views/dashboard/steps.js:
--------------------------------------------------------------------------------
1 | const steps = [{
2 | element: '#hamburger-container',
3 | popover: {
4 | title: '侧滑栏',
5 | description: '打开或者关闭侧滑菜单',
6 | position: 'bottom'
7 | }
8 | },
9 | {
10 | element: '#breadcrumb-container',
11 | popover: {
12 | title: '导航栏',
13 | description: '页面导航栏,指示当前页面所处的位置',
14 | position: 'bottom'
15 | }
16 | },
17 | {
18 | element: '#screenfull',
19 | popover: {
20 | title: '全屏切换',
21 | description: '可切换页面全屏模式',
22 | position: 'left'
23 | }
24 | },
25 | {
26 | element: '#tags-view-container',
27 | popover: {
28 | title: '选项卡',
29 | description: '历史访问的页面, 点击自由切换',
30 | position: 'bottom'
31 | },
32 | padding: 0
33 | }
34 | ]
35 |
36 | export default steps
37 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left : 0;
23 | position : relative;
24 | margin : 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width : 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
--------------------------------------------------------------------------------
/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/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
34 |
35 |
44 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity : 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity : 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity : 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets
4 |
5 | import ElementUI from 'element-ui'
6 | import 'element-ui/lib/theme-chalk/index.css'
7 | // import locale from 'element-ui/lib/locale/lang/en' // lang i18n
8 |
9 | import '@/styles/index.scss' // global css
10 |
11 | import App from './App'
12 | import store from './store'
13 | import router from './router'
14 |
15 | import '@/icons' // icon
16 | import '@/permission' // permission control
17 |
18 | /**
19 | * If you don't want to use mock-server
20 | * you want to use mockjs for request interception
21 | * you can execute:
22 | *
23 | * import { mockXHR } from '../mock'
24 | * mockXHR()
25 | */
26 |
27 | // set ElementUI lang to EN
28 | // Vue.use(ElementUI, {
29 | // locale
30 | // })
31 |
32 | Vue.use(ElementUI)
33 |
34 | Vue.config.productionTip = false
35 |
36 | new Vue({
37 | el: '#app',
38 | router,
39 | store,
40 | render: h => h(App)
41 | })
42 |
--------------------------------------------------------------------------------
/src/api/update.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getVersions() {
4 | return request({
5 | url: '/update/versions',
6 | method: 'get'
7 | })
8 | }
9 |
10 | export function getPagingVersions(data) {
11 | return request({
12 | url: '/update/versionPageQuery',
13 | method: 'post',
14 | data
15 | })
16 | }
17 |
18 | export function addVersionInfo(data) {
19 | return request({
20 | url: '/update/newVersion',
21 | method: 'post',
22 | data
23 | })
24 | }
25 |
26 | export function uploadApkFile(formData) {
27 | return request({
28 | url: '/update/uploadApk',
29 | method: 'post',
30 | data: formData,
31 | headers: {
32 | 'Content-Type': 'multipart/form-data'
33 | }
34 | })
35 | }
36 |
37 | export function deleteVersion(data) {
38 | return request({
39 | url: '/update/delete',
40 | method: 'post',
41 | data
42 | })
43 | }
44 |
45 | export function updateVersion(data) {
46 | return request({
47 | url: '/update/updateInfo',
48 | method: 'post',
49 | data
50 | })
51 | }
--------------------------------------------------------------------------------
/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
1 | import { parseTime } from '@/utils/index.js'
2 |
3 | describe('Utils:parseTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | it('timestamp', () => {
6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7 | })
8 | it('ten digits timestamp', () => {
9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
10 | })
11 | it('new Date', () => {
12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
13 | })
14 | it('format', () => {
15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
18 | })
19 | it('get the day of the week', () => {
20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
21 | })
22 | it('get the day of the week', () => {
23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
24 | })
25 | it('empty argument', () => {
26 | expect(parseTime()).toBeNull()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-present PanJiaChen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/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/api/account.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function login(data) {
4 | return request({
5 | url: '/account/login',
6 | method: 'post',
7 | data
8 | })
9 | }
10 |
11 | export function getInfo(token) {
12 | return request({
13 | url: '/account/info',
14 | method: 'get',
15 | params: { token }
16 | })
17 | }
18 |
19 | export function logout() {
20 | return request({
21 | url: '/account/logout',
22 | method: 'post'
23 | })
24 | }
25 |
26 | export function getAccounts() {
27 | return request({
28 | url: '/account/accounts',
29 | method: 'get'
30 | })
31 | }
32 |
33 | export function getPagingAccounts(data) {
34 | return request({
35 | url: '/account/accountPageQuery',
36 | method: 'post',
37 | data
38 | })
39 | }
40 |
41 | export function register(data) {
42 | return request({
43 | url: '/account/register',
44 | method: 'post',
45 | data
46 | })
47 | }
48 |
49 | export function deleteAccount(data) {
50 | return request({
51 | url: '/account/delete',
52 | method: 'post',
53 | data
54 | })
55 | }
56 |
57 | export function updateAccount(data) {
58 | return request({
59 | url: '/account/updateInfo',
60 | method: 'post',
61 | data
62 | })
63 | }
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/src/components/Screenfull/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
50 |
51 |
61 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
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/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 | isFirstRun: Cookies.get('isFirstRun') ? Cookies.get('isFirstRun') : true
10 | }
11 |
12 | const mutations = {
13 | TOGGLE_SIDEBAR: state => {
14 | state.sidebar.opened = !state.sidebar.opened
15 | state.sidebar.withoutAnimation = false
16 | if (state.sidebar.opened) {
17 | Cookies.set('sidebarStatus', 1)
18 | } else {
19 | Cookies.set('sidebarStatus', 0)
20 | }
21 | },
22 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
23 | Cookies.set('sidebarStatus', 0)
24 | state.sidebar.opened = false
25 | state.sidebar.withoutAnimation = withoutAnimation
26 | },
27 | TOGGLE_DEVICE: (state, device) => {
28 | state.device = device
29 | },
30 | TOGGLE_FIRST_RUN: (state, isFirstRun) => {
31 | state.isFirstRun = isFirstRun
32 | Cookies.set('isFirstRun', isFirstRun)
33 | }
34 | }
35 |
36 | const actions = {
37 | toggleSideBar({
38 | commit
39 | }) {
40 | commit('TOGGLE_SIDEBAR')
41 | },
42 | closeSideBar({
43 | commit
44 | }, {
45 | withoutAnimation
46 | }) {
47 | commit('CLOSE_SIDEBAR', withoutAnimation)
48 | },
49 | toggleDevice({
50 | commit
51 | }, device) {
52 | commit('TOGGLE_DEVICE', device)
53 | },
54 | hasShowGuide({
55 | commit
56 | }) {
57 | commit('TOGGLE_FIRST_RUN', false)
58 | }
59 | }
60 |
61 | export default {
62 | namespaced: true,
63 | state,
64 | mutations,
65 | actions
66 | }
67 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
57 |
--------------------------------------------------------------------------------
/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | import { asyncRoutes, constantRoutes } from '@/router'
2 |
3 | /**
4 | * Use meta.role to determine if the current user has permission
5 | * @param roles
6 | * @param route
7 | */
8 | function hasPermission(roles, route) {
9 | if (route.meta && route.meta.roles) {
10 | return roles.some(role => route.meta.roles.includes(role))
11 | } else {
12 | return true
13 | }
14 | }
15 |
16 | /**
17 | * Filter asynchronous routing tables by recursion
18 | * @param routes asyncRoutes
19 | * @param roles
20 | */
21 | export function filterAsyncRoutes(routes, roles) {
22 | const res = []
23 |
24 | routes.forEach(route => {
25 | const tmp = { ...route }
26 | if (hasPermission(roles, tmp)) {
27 | if (tmp.children) {
28 | tmp.children = filterAsyncRoutes(tmp.children, roles)
29 | }
30 | res.push(tmp)
31 | }
32 | })
33 |
34 | return res
35 | }
36 |
37 | const state = {
38 | routes: [],
39 | addRoutes: []
40 | }
41 |
42 | const mutations = {
43 | SET_ROUTES: (state, routes) => {
44 | state.addRoutes = routes
45 | state.routes = constantRoutes.concat(routes)
46 | }
47 | }
48 |
49 | const actions = {
50 | generateRoutes({ commit }, roles) {
51 | return new Promise(resolve => {
52 | let accessedRoutes
53 | if (roles.includes('admin')) {
54 | accessedRoutes = asyncRoutes || []
55 | } else {
56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
57 | }
58 | commit('SET_ROUTES', accessedRoutes)
59 | resolve(accessedRoutes)
60 | })
61 | }
62 | }
63 |
64 | export default {
65 | namespaced: true,
66 | state,
67 | mutations,
68 | actions
69 | }
70 |
--------------------------------------------------------------------------------
/src/icons/svg/exit-fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
当前用户: {{ name }}
7 |
8 |
9 |
10 |
51 |
52 |
76 |
--------------------------------------------------------------------------------
/src/utils/scroll-to.js:
--------------------------------------------------------------------------------
1 | Math.easeInOutQuad = function(t, b, c, d) {
2 | t /= d / 2
3 | if (t < 1) {
4 | return c / 2 * t * t + b
5 | }
6 | t--
7 | return -c / 2 * (t * (t - 2) - 1) + b
8 | }
9 |
10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
11 | var requestAnimFrame = (function() {
12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
13 | })()
14 |
15 | /**
16 | * Because it's so fucking difficult to detect the scrolling element, just move them all
17 | * @param {number} amount
18 | */
19 | function move(amount) {
20 | document.documentElement.scrollTop = amount
21 | document.body.parentNode.scrollTop = amount
22 | document.body.scrollTop = amount
23 | }
24 |
25 | function position() {
26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
27 | }
28 |
29 | /**
30 | * @param {number} to
31 | * @param {number} duration
32 | * @param {Function} callback
33 | */
34 | export function scrollTo(to, duration, callback) {
35 | const start = position()
36 | const change = to - start
37 | const increment = 20
38 | let currentTime = 0
39 | duration = (typeof (duration) === 'undefined') ? 500 : duration
40 | var animateScroll = function() {
41 | // increment the time
42 | currentTime += increment
43 | // find the value with the quadratic in-out easing function
44 | var val = Math.easeInOutQuad(currentTime, start, change, duration)
45 | // move the document.body
46 | move(val)
47 | // do the animation unless its over
48 | if (currentTime < duration) {
49 | requestAnimFrame(animateScroll)
50 | } else {
51 | if (callback && typeof (callback) === 'function') {
52 | // the animation is done so lets callback
53 | callback()
54 | }
55 | }
56 | }
57 | animateScroll()
58 | }
59 |
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | import { param2Obj } from '../src/utils'
3 |
4 | import account from './account'
5 | import update from './update'
6 |
7 |
8 | const mocks = [
9 | ...account,
10 | ...update,
11 | ]
12 |
13 | // for front mock
14 | // please use it cautiously, it will redefine XMLHttpRequest,
15 | // which will cause many of your third-party libraries to be invalidated(like progress event).
16 | export function mockXHR() {
17 | // mock patch
18 | // https://github.com/nuysoft/Mock/issues/300
19 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
20 | Mock.XHR.prototype.send = function() {
21 | if (this.custom.xhr) {
22 | this.custom.xhr.withCredentials = this.withCredentials || false
23 |
24 | if (this.responseType) {
25 | this.custom.xhr.responseType = this.responseType
26 | }
27 | }
28 | this.proxy_send(...arguments)
29 | }
30 |
31 | function XHR2ExpressReqWrap(respond) {
32 | return function(options) {
33 | let result = null
34 | if (respond instanceof Function) {
35 | const { body, type, url } = options
36 | // https://expressjs.com/en/4x/api.html#req
37 | result = respond({
38 | method: type,
39 | body: JSON.parse(body),
40 | query: param2Obj(url)
41 | })
42 | } else {
43 | result = respond
44 | }
45 | return Mock.mock(result)
46 | }
47 | }
48 |
49 | for (const i of mocks) {
50 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
51 | }
52 | }
53 |
54 | // for mock server
55 | const responseFake = (url, type, respond) => {
56 | return {
57 | url: new RegExp(`/mock${url}`),
58 | type: type || 'get',
59 | response(req, res) {
60 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
61 | }
62 | }
63 | }
64 |
65 | export default mocks.map(route => {
66 | return responseFake(route.url, route.type, route.response)
67 | })
68 |
--------------------------------------------------------------------------------
/mock/mock-server.js:
--------------------------------------------------------------------------------
1 | const chokidar = require('chokidar')
2 | const bodyParser = require('body-parser')
3 | const chalk = require('chalk')
4 | const path = require('path')
5 |
6 | const mockDir = path.join(process.cwd(), 'mock')
7 |
8 | function registerRoutes(app) {
9 | let mockLastIndex
10 | const { default: mocks } = require('./index.js')
11 | for (const mock of mocks) {
12 | app[mock.type](mock.url, mock.response)
13 | mockLastIndex = app._router.stack.length
14 | }
15 | const mockRoutesLength = Object.keys(mocks).length
16 | return {
17 | mockRoutesLength: mockRoutesLength,
18 | mockStartIndex: mockLastIndex - mockRoutesLength
19 | }
20 | }
21 |
22 | function unregisterRoutes() {
23 | Object.keys(require.cache).forEach(i => {
24 | if (i.includes(mockDir)) {
25 | delete require.cache[require.resolve(i)]
26 | }
27 | })
28 | }
29 |
30 | module.exports = app => {
31 | // es6 polyfill
32 | require('@babel/register')
33 |
34 | // parse app.body
35 | // https://expressjs.com/en/4x/api.html#req.body
36 | app.use(bodyParser.json())
37 | app.use(bodyParser.urlencoded({
38 | extended: true
39 | }))
40 |
41 | const mockRoutes = registerRoutes(app)
42 | var mockRoutesLength = mockRoutes.mockRoutesLength
43 | var mockStartIndex = mockRoutes.mockStartIndex
44 |
45 | // watch files, hot reload mock server
46 | chokidar.watch(mockDir, {
47 | ignored: /mock-server/,
48 | ignoreInitial: true
49 | }).on('all', (event, path) => {
50 | if (event === 'change' || event === 'add') {
51 | // remove mock routes stack
52 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
53 |
54 | // clear routes cache
55 | unregisterRoutes()
56 |
57 | const mockRoutes = registerRoutes(app)
58 | mockRoutesLength = mockRoutes.mockRoutesLength
59 | mockStartIndex = mockRoutes.mockStartIndex
60 |
61 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
62 | }
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import {
4 | Message
5 | } from 'element-ui'
6 | import NProgress from 'nprogress' // progress bar
7 | import 'nprogress/nprogress.css' // progress bar style
8 | import {
9 | getToken
10 | } from '@/utils/auth' // get token from cookie
11 | import getPageTitle from '@/utils/get-page-title'
12 |
13 | NProgress.configure({
14 | showSpinner: false
15 | }) // NProgress Configuration
16 |
17 | const whiteList = ['/login'] // no redirect whitelist
18 |
19 | router.beforeEach(async (to, from, next) => {
20 | // start progress bar
21 | NProgress.start()
22 |
23 | // set page title
24 | document.title = getPageTitle(to.meta.title)
25 |
26 | // determine whether the user has logged in
27 | const hasToken = getToken()
28 |
29 | if (hasToken) {
30 | if (to.path === '/login') {
31 | // if is logged in, redirect to the home page
32 | next({
33 | path: '/'
34 | })
35 | NProgress.done()
36 | } else {
37 | const hasGetUserInfo = store.getters.name
38 | if (hasGetUserInfo) {
39 | next()
40 | } else {
41 | try {
42 | // get user info
43 | await store.dispatch('account/getInfo')
44 |
45 | next()
46 | } catch (error) {
47 | // remove token and go to login page to re-login
48 | await store.dispatch('account/resetToken')
49 | Message.error(error || 'Has Error')
50 | next(`/login?redirect=${to.path}`)
51 | NProgress.done()
52 | }
53 | }
54 | }
55 | } else {
56 | /* has no token*/
57 |
58 | if (whiteList.indexOf(to.path) !== -1) {
59 | // in the free login whitelist, go directly
60 | next()
61 | } else {
62 | // other pages that do not have permission to access are redirected to the login page.
63 | next(`/login?redirect=${to.path}`)
64 | NProgress.done()
65 | }
66 | }
67 | })
68 |
69 | router.afterEach(() => {
70 | // finish progress bar
71 | NProgress.done()
72 | })
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xupdate-management",
3 | "version": "1.0.0",
4 | "description": "为xupdate提供的管理后台",
5 | "author": "xuexiangjys ",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "vue-cli-service serve",
9 | "build:prod": "vue-cli-service build",
10 | "build:stage": "vue-cli-service build --mode staging",
11 | "preview": "node build/index.js --preview",
12 | "lint": "eslint --ext .js,.vue src",
13 | "test:unit": "jest --clearCache && vue-cli-service test:unit",
14 | "test:ci": "npm run lint && npm run test:unit",
15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
16 | },
17 | "dependencies": {
18 | "axios": "0.21.0",
19 | "core-js": "^3.8.1",
20 | "driver.js": "^0.9.8",
21 | "element-ui": "2.14.1",
22 | "js-cookie": "2.2.1",
23 | "normalize.css": "8.0.1",
24 | "nprogress": "0.2.0",
25 | "path-to-regexp": "6.2.0",
26 | "screenfull": "^5.0.2",
27 | "vue": "2.6.12",
28 | "vue-router": "3.4.9",
29 | "vuex": "3.6.0"
30 | },
31 | "devDependencies": {
32 | "@babel/core": "7.12.10",
33 | "@babel/register": "7.12.10",
34 | "@vue/cli-plugin-babel": "4.5.9",
35 | "@vue/cli-plugin-eslint": "4.5.9",
36 | "@vue/cli-plugin-unit-jest": "4.5.9",
37 | "@vue/cli-service": "4.5.9",
38 | "@vue/test-utils": "1.1.1",
39 | "babel-core": "7.0.0-bridge.0",
40 | "babel-eslint": "10.1.0",
41 | "babel-jest": "26.6.3",
42 | "chalk": "4.1.0",
43 | "connect": "3.7.0",
44 | "eslint": "7.15.0",
45 | "eslint-plugin-vue": "7.2.0",
46 | "html-webpack-plugin": "4.5.0",
47 | "mockjs": "1.1.0",
48 | "node-sass": "^5.0.0",
49 | "runjs": "^4.3.2",
50 | "sass-loader": "^10.1.0",
51 | "script-ext-html-webpack-plugin": "2.1.5",
52 | "script-loader": "0.7.2",
53 | "serve-static": "^1.14.1",
54 | "svg-sprite-loader": "5.1.1",
55 | "svgo": "1.3.2",
56 | "vue-template-compiler": "2.6.12"
57 | },
58 | "engines": {
59 | "node": ">=8.9",
60 | "npm": ">= 3.0.0"
61 | },
62 | "browserslist": [
63 | "> 1%",
64 | "last 2 versions",
65 | "not ie <= 8"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
79 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
92 |
93 |
102 |
--------------------------------------------------------------------------------
/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_URL, // url = base url + request url
9 | withCredentials: true, // send cookies when cross-domain requests
10 | timeout: 5000 // request timeout
11 | })
12 |
13 | // request interceptor
14 | service.interceptors.request.use(
15 | config => {
16 | // do something before request is sent
17 |
18 | if (store.getters.token) {
19 | // 添加请求Token
20 | config.headers['X-Token'] = getToken()
21 | }
22 | // 添加请求时间戳
23 | config.headers['X-TimeStamp'] = (new Date()).getTime();
24 | return config
25 | },
26 | error => {
27 | // do something with request error
28 | console.log(error) // for debug
29 | return Promise.reject(error)
30 | }
31 | )
32 |
33 | // response interceptor
34 | service.interceptors.response.use(
35 | /**
36 | * If you want to get http information such as headers or status
37 | * Please return response => response
38 | */
39 |
40 | /**
41 | * Determine the request status by custom code
42 | * Here is just an example
43 | * You can also judge the status by HTTP Status Code
44 | */
45 | response => {
46 | const res = response.data
47 |
48 | if (res.code !== 0) {
49 | Message({
50 | message: res.msg || 'error',
51 | type: 'error',
52 | duration: 5 * 1000
53 | })
54 |
55 | // 100: TOKEN_INVALID; 101: TOKEN_MISSING; 102: AUTH_ERROR;
56 | if (res.code === 100 || res.code === 101 || res.code === 102) {
57 | // to re-login
58 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
59 | confirmButtonText: 'Re-Login',
60 | cancelButtonText: 'Cancel',
61 | type: 'warning'
62 | }).then(() => {
63 | store.dispatch('account/resetToken').then(() => {
64 | location.reload()
65 | })
66 | })
67 | }
68 | return Promise.reject(res.msg || 'error')
69 | } else {
70 | return res
71 | }
72 | },
73 | error => {
74 | console.log('err' + error) // for debug
75 | Message({
76 | message: error.message,
77 | type: 'error',
78 | duration: 5 * 1000
79 | })
80 | return Promise.reject(error)
81 | }
82 | )
83 |
84 | export default service
85 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
63 |
64 |
108 |
--------------------------------------------------------------------------------
/src/store/modules/account.js:
--------------------------------------------------------------------------------
1 | import {
2 | login,
3 | logout,
4 | getInfo
5 | } from '@/api/account'
6 | import {
7 | getToken,
8 | setToken,
9 | removeToken
10 | } from '@/utils/auth'
11 | import {
12 | resetRouter
13 | } from '@/router'
14 |
15 | const state = {
16 | token: getToken(),
17 | name: '',
18 | avatar: ''
19 | }
20 |
21 | const mutations = {
22 | SET_TOKEN: (state, token) => {
23 | state.token = token
24 | },
25 | SET_NAME: (state, name) => {
26 | state.name = name
27 | },
28 | SET_AVATAR: (state, avatar) => {
29 | state.avatar = avatar
30 | }
31 | }
32 |
33 | const actions = {
34 | // account login
35 | login({
36 | commit
37 | }, accountInfo) {
38 | const {
39 | loginName,
40 | password
41 | } = accountInfo
42 | return new Promise((resolve, reject) => {
43 | login({
44 | loginName: loginName.trim(),
45 | password: password
46 | }).then(response => {
47 | const {
48 | data
49 | } = response
50 | commit('SET_TOKEN', data.token)
51 | setToken(data.token)
52 | resolve()
53 | }).catch(error => {
54 | reject(error)
55 | })
56 | })
57 | },
58 |
59 | // get account info
60 | getInfo({
61 | commit,
62 | state
63 | }) {
64 | return new Promise((resolve, reject) => {
65 | getInfo(state.token).then(response => {
66 | const {
67 | data
68 | } = response
69 |
70 | if (!data) {
71 | reject('Verification failed, please Login again.')
72 | }
73 |
74 | const {
75 | nick,
76 | avatar
77 | } = data
78 |
79 | commit('SET_NAME', nick)
80 | commit('SET_AVATAR', avatar)
81 | resolve(data)
82 | }).catch(error => {
83 | reject(error)
84 | })
85 | })
86 | },
87 |
88 | // account logout
89 | logout({
90 | commit
91 | }) {
92 | return new Promise((resolve, reject) => {
93 | logout().then(() => {
94 | commit('SET_NAME', '')
95 | commit('SET_AVATAR', '')
96 | commit('SET_TOKEN', '')
97 | removeToken()
98 | resetRouter()
99 | resolve()
100 | }).catch(error => {
101 | reject(error)
102 | })
103 | })
104 | },
105 |
106 | // remove token
107 | resetToken({
108 | commit
109 | }) {
110 | return new Promise(resolve => {
111 | commit('SET_TOKEN', '')
112 | removeToken()
113 | resolve()
114 | })
115 | }
116 | }
117 |
118 | export default {
119 | namespaced: true,
120 | state,
121 | mutations,
122 | actions
123 | }
124 |
--------------------------------------------------------------------------------
/src/layout/components/TagsView/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
69 |
70 |
86 |
--------------------------------------------------------------------------------
/mock/update.js:
--------------------------------------------------------------------------------
1 | const versions = [{
2 | "versionId": 10,
3 | "updateStatus": 2,
4 | "versionCode": 24,
5 | "versionName": "1.0.4",
6 | "uploadTime": "2018-07-30 09:36:39",
7 | "apkSize": 1697,
8 | "appKey": "test3",
9 | "modifyContent": "1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。",
10 | "downloadUrl": "xupdate_demo_1.0.apk",
11 | "apkMd5": "03B41AD67A4AD62896BB9A2781718203"
12 | }, {
13 | "versionId": 11,
14 | "updateStatus": 1,
15 | "versionCode": 34,
16 | "versionName": "1.23.4",
17 | "uploadTime": "2018-07-30 09:47:25",
18 | "apkSize": 1649,
19 | "appKey": "com.xuexiang.xupdatedemo",
20 | "modifyContent": "1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。",
21 | "downloadUrl": "xupdate_demo_1.0.2.apk",
22 | "apkMd5": "E4B79A36EFB9F17DF7E3BB161F9BCFD8"
23 | }, {
24 | "versionId": 12,
25 | "updateStatus": 1,
26 | "versionCode": 4,
27 | "versionName": "1.0.3",
28 | "uploadTime": "2018-07-30 10:52:53",
29 | "apkSize": 1649,
30 | "appKey": "com.xuexiang.xupdatedemo",
31 | "modifyContent": "1、优化api接口。\\r\\n2、添加使用demo演示。\\r\\n3、新增自定义更新服务API接口。\\r\\n4、优化更新提示界面。",
32 | "downloadUrl": "xupdate_demo_1.0.2.apk",
33 | "apkMd5": "E4B79A36EFB9F17DF7E3BB161F9BCFD8"
34 | }];
35 |
36 | export default [{
37 | url: '/update/versions',
38 | type: 'get',
39 | response: _ => {
40 | return {
41 | code: 0,
42 | data: versions,
43 | msg: ''
44 | }
45 | }
46 | },
47 |
48 | {
49 | url: '/update/versionPageQuery',
50 | type: 'post',
51 | response: _ => {
52 | return {
53 | code: 0,
54 | data: {
55 | total: 3,
56 | array: versions,
57 | pageSize: 10,
58 | pageNum: 1
59 | },
60 | msg: ''
61 | }
62 | }
63 | },
64 |
65 | {
66 | url: '/update/newVersion',
67 | type: 'post',
68 | response: _ => {
69 | return {
70 | code: 0,
71 | data: {
72 | "versionId": 111
73 | },
74 | msg: ''
75 | }
76 | }
77 | },
78 |
79 | {
80 | url: '/update/uploadApk',
81 | type: 'post',
82 | response: _ => {
83 | return {
84 | code: 0,
85 | data: true,
86 | msg: ''
87 | }
88 | }
89 | },
90 |
91 | {
92 | url: '/update/delete',
93 | type: 'post',
94 | response: _ => {
95 | return {
96 | code: 0,
97 | data: true,
98 | msg: ''
99 | }
100 | }
101 | },
102 |
103 | {
104 | url: '/update/updateInfo',
105 | type: 'post',
106 | response: _ => {
107 | return {
108 | code: 0,
109 | data: true,
110 | msg: ''
111 | }
112 | }
113 | },
114 | ]
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
96 |
--------------------------------------------------------------------------------
/tests/unit/components/Breadcrumb.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils'
2 | import VueRouter from 'vue-router'
3 | import ElementUI from 'element-ui'
4 | import Breadcrumb from '@/components/Breadcrumb/index.vue'
5 |
6 | const localVue = createLocalVue()
7 | localVue.use(VueRouter)
8 | localVue.use(ElementUI)
9 |
10 | const routes = [
11 | {
12 | path: '/',
13 | name: 'home',
14 | children: [{
15 | path: 'dashboard',
16 | name: 'dashboard'
17 | }]
18 | },
19 | {
20 | path: '/menu',
21 | name: 'menu',
22 | children: [{
23 | path: 'menu1',
24 | name: 'menu1',
25 | meta: { title: 'menu1' },
26 | children: [{
27 | path: 'menu1-1',
28 | name: 'menu1-1',
29 | meta: { title: 'menu1-1' }
30 | },
31 | {
32 | path: 'menu1-2',
33 | name: 'menu1-2',
34 | redirect: 'noredirect',
35 | meta: { title: 'menu1-2' },
36 | children: [{
37 | path: 'menu1-2-1',
38 | name: 'menu1-2-1',
39 | meta: { title: 'menu1-2-1' }
40 | },
41 | {
42 | path: 'menu1-2-2',
43 | name: 'menu1-2-2'
44 | }]
45 | }]
46 | }]
47 | }]
48 |
49 | const router = new VueRouter({
50 | routes
51 | })
52 |
53 | describe('Breadcrumb.vue', () => {
54 | const wrapper = mount(Breadcrumb, {
55 | localVue,
56 | router
57 | })
58 | it('dashboard', () => {
59 | router.push('/dashboard')
60 | const len = wrapper.findAll('.el-breadcrumb__inner').length
61 | expect(len).toBe(1)
62 | })
63 | it('normal route', () => {
64 | router.push('/menu/menu1')
65 | const len = wrapper.findAll('.el-breadcrumb__inner').length
66 | expect(len).toBe(2)
67 | })
68 | it('nested route', () => {
69 | router.push('/menu/menu1/menu1-2/menu1-2-1')
70 | const len = wrapper.findAll('.el-breadcrumb__inner').length
71 | expect(len).toBe(4)
72 | })
73 | it('no meta.title', () => {
74 | router.push('/menu/menu1/menu1-2/menu1-2-2')
75 | const len = wrapper.findAll('.el-breadcrumb__inner').length
76 | expect(len).toBe(3)
77 | })
78 | // it('click link', () => {
79 | // router.push('/menu/menu1/menu1-2/menu1-2-2')
80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
81 | // const second = breadcrumbArray.at(1)
82 | // console.log(breadcrumbArray)
83 | // const href = second.find('a').attributes().href
84 | // expect(href).toBe('#/menu/menu1')
85 | // })
86 | // it('noRedirect', () => {
87 | // router.push('/menu/menu1/menu1-2/menu1-2-1')
88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
89 | // const redirectBreadcrumb = breadcrumbArray.at(2)
90 | // expect(redirectBreadcrumb.contains('a')).toBe(false)
91 | // })
92 | it('last breadcrumb', () => {
93 | router.push('/menu/menu1/menu1-2/menu1-2-1')
94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
95 | const redirectBreadcrumb = breadcrumbArray.at(3)
96 | expect(redirectBreadcrumb.contains('a')).toBe(false)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
32 |
33 |
34 |
35 |
66 |
67 |
--------------------------------------------------------------------------------
/mock/account.js:
--------------------------------------------------------------------------------
1 | const tokens = {
2 | admin: {
3 | token: 'admin-token'
4 | },
5 | editor: {
6 | token: 'editor-token'
7 | },
8 | xuexiang: {
9 | token: 'xuexiang-token'
10 | }
11 | }
12 |
13 | const accounts = {
14 | 'admin-token': {
15 | roles: ['admin'],
16 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
17 | nick: 'Super Admin'
18 | },
19 | 'editor-token': {
20 | roles: ['editor'],
21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
22 | nick: 'Normal Editor'
23 | },
24 | 'xuexiang-token': {
25 | roles: ['admin'],
26 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
27 | nick: '[薛翔]'
28 | }
29 | }
30 |
31 |
32 | const accountArray = [{
33 | "accountId": 1,
34 | "loginName": "admin",
35 | "password": "123456",
36 | "nick": "admin",
37 | "authority": "admin",
38 | "avatar": "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
39 | "phone": "13513957542",
40 | "address": "南京市江宁区",
41 | "registerTime": 1525536000000
42 | }, {
43 | "accountId": 2,
44 | "loginName": "xuexiang",
45 | "password": "123456",
46 | "nick": "薛翔",
47 | "authority": "admin",
48 | "avatar": "https://raw.githubusercontent.com/xuexiangjys/Resource/master/img/avatar/avatar_github.jpg",
49 | "phone": "13913845875",
50 | "address": "南京市江宁区",
51 | "registerTime": 1544457600000
52 | }];
53 |
54 | export default [
55 | // account login
56 | {
57 | url: '/account/login',
58 | type: 'post',
59 | response: config => {
60 | const {
61 | loginName
62 | } = config.body
63 | const token = tokens[loginName]
64 |
65 | // mock error
66 | if (!token) {
67 | return {
68 | code: 60204,
69 | msg: 'Account and password are incorrect.'
70 | }
71 | }
72 |
73 | return {
74 | code: 0,
75 | data: token,
76 | msg: ''
77 | }
78 | }
79 | },
80 |
81 | // get account info
82 | {
83 | url: '/account/info',
84 | type: 'get',
85 | response: config => {
86 | const {
87 | token
88 | } = config.query
89 | const info = accounts[token]
90 |
91 | // mock error
92 | if (!info) {
93 | return {
94 | code: 50008,
95 | msg: 'Login failed, unable to get account details.'
96 | }
97 | }
98 |
99 | return {
100 | code: 0,
101 | data: info,
102 | msg: ''
103 | }
104 | }
105 | },
106 |
107 | // account logout
108 | {
109 | url: '/account/logout',
110 | type: 'post',
111 | response: _ => {
112 | return {
113 | code: 0,
114 | data: true,
115 | msg: ''
116 | }
117 | }
118 | },
119 |
120 | // account accounts
121 | {
122 | url: '/account/accounts',
123 | type: 'get',
124 | response: _ => {
125 | return {
126 | code: 0,
127 | msg: "",
128 | data: accountArray
129 | }
130 | }
131 | },
132 |
133 | {
134 | url: '/account/accountPageQuery',
135 | type: 'post',
136 | response: _ => {
137 | return {
138 | code: 0,
139 | data: {
140 | total: 2,
141 | array: accountArray,
142 | pageSize: 10,
143 | pageNum: 1
144 | },
145 | msg: ''
146 | }
147 | }
148 | },
149 |
150 | {
151 | url: '/account/register',
152 | type: 'post',
153 | response: _ => {
154 | return {
155 | code: 0,
156 | data: true,
157 | msg: ''
158 | }
159 | }
160 | },
161 |
162 | {
163 | url: '/account/delete',
164 | type: 'post',
165 | response: _ => {
166 | return {
167 | code: 0,
168 | data: true,
169 | msg: ''
170 | }
171 | }
172 | },
173 |
174 | {
175 | url: '/account/updateInfo',
176 | type: 'post',
177 | response: _ => {
178 | return {
179 | code: 0,
180 | data: true,
181 | msg: ''
182 | }
183 | }
184 | },
185 | ]
--------------------------------------------------------------------------------
/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 || '版本更新管理' // page title
10 | const port = 8088 // dev port
11 |
12 | // All configuration item explanations can be find in https://cli.vuejs.org/config/
13 | module.exports = {
14 | /**
15 | * You will need to set publicPath if you plan to deploy your site under a sub path,
16 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
17 | * then publicPath should be set to "/bar/".
18 | * In most cases please use '/' !!!
19 | * Detail: https://cli.vuejs.org/config/#publicpath
20 | */
21 | publicPath: './',
22 | outputDir: 'dist',
23 | assetsDir: 'static',
24 | lintOnSave: false,
25 | productionSourceMap: false,
26 | devServer: {
27 | port: port,
28 | open: true,
29 | overlay: {
30 | warnings: false,
31 | errors: false
32 | },
33 | after: require('./mock/mock-server.js')
34 | },
35 | configureWebpack: {
36 | // provide the app's title in webpack's name field, so that
37 | // it can be accessed in index.html to inject the correct title.
38 | name: name,
39 | resolve: {
40 | alias: {
41 | '@': resolve('src')
42 | }
43 | }
44 | },
45 | chainWebpack(config) {
46 | config.plugins.delete('preload') // TODO: need test
47 | config.plugins.delete('prefetch') // TODO: need test
48 |
49 | // set svg-sprite-loader
50 | config.module
51 | .rule('svg')
52 | .exclude.add(resolve('src/icons'))
53 | .end()
54 | config.module
55 | .rule('icons')
56 | .test(/\.svg$/)
57 | .include.add(resolve('src/icons'))
58 | .end()
59 | .use('svg-sprite-loader')
60 | .loader('svg-sprite-loader')
61 | .options({
62 | symbolId: 'icon-[name]'
63 | })
64 | .end()
65 |
66 | // set preserveWhitespace
67 | config.module
68 | .rule('vue')
69 | .use('vue-loader')
70 | .loader('vue-loader')
71 | .tap(options => {
72 | options.compilerOptions.preserveWhitespace = true
73 | return options
74 | })
75 | .end()
76 |
77 | config
78 | // https://webpack.js.org/configuration/devtool/#development
79 | .when(process.env.NODE_ENV === 'development',
80 | config => config.devtool('cheap-source-map')
81 | )
82 |
83 | config
84 | .when(process.env.NODE_ENV !== 'development',
85 | config => {
86 | config
87 | .plugin('ScriptExtHtmlWebpackPlugin')
88 | .after('html')
89 | .use('script-ext-html-webpack-plugin', [{
90 | // `runtime` must same as runtimeChunk name. default is `runtime`
91 | inline: /runtime\..*\.js$/
92 | }])
93 | .end()
94 | config
95 | .optimization.splitChunks({
96 | chunks: 'all',
97 | cacheGroups: {
98 | libs: {
99 | name: 'chunk-libs',
100 | test: /[\\/]node_modules[\\/]/,
101 | priority: 10,
102 | chunks: 'initial' // only package third parties that are initially dependent
103 | },
104 | elementUI: {
105 | name: 'chunk-elementUI', // split elementUI into a single package
106 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
107 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
108 | },
109 | commons: {
110 | name: 'chunk-commons',
111 | test: resolve('src/components'), // can customize your rules
112 | minChunks: 3, // minimum common number
113 | priority: 5,
114 | reuseExistingChunk: true
115 | }
116 | }
117 | })
118 | config.optimization.runtimeChunk('single')
119 | }
120 | )
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * Parse the time to string
7 | * @param {(Object|string|number)} time
8 | * @param {string} cFormat
9 | * @returns {string}
10 | */
11 | export function parseTime(time, cFormat) {
12 | if (arguments.length === 0) {
13 | return null
14 | }
15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16 | let date
17 | if (typeof time === 'object') {
18 | date = time
19 | } else {
20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
21 | time = parseInt(time)
22 | }
23 | if ((typeof time === 'number') && (time.toString().length === 10)) {
24 | time = time * 1000
25 | }
26 | date = new Date(time)
27 | }
28 | const formatObj = {
29 | y: date.getFullYear(),
30 | m: date.getMonth() + 1,
31 | d: date.getDate(),
32 | h: date.getHours(),
33 | i: date.getMinutes(),
34 | s: date.getSeconds(),
35 | a: date.getDay()
36 | }
37 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
38 | let value = formatObj[key]
39 | // Note: getDay() returns 0 on Sunday
40 | if (key === 'a') {
41 | return ['日', '一', '二', '三', '四', '五', '六'][value]
42 | }
43 | if (result.length > 0 && value < 10) {
44 | value = '0' + value
45 | }
46 | return value || 0
47 | })
48 | return time_str
49 | }
50 |
51 | /**
52 | * @param {number} time
53 | * @param {string} option
54 | * @returns {string}
55 | */
56 | export function formatTime(time, option) {
57 | if (('' + time).length === 10) {
58 | time = parseInt(time) * 1000
59 | } else {
60 | time = +time
61 | }
62 | const d = new Date(time)
63 | const now = Date.now()
64 |
65 | const diff = (now - d) / 1000
66 |
67 | if (diff < 30) {
68 | return '刚刚'
69 | } else if (diff < 3600) {
70 | // less 1 hour
71 | return Math.ceil(diff / 60) + '分钟前'
72 | } else if (diff < 3600 * 24) {
73 | return Math.ceil(diff / 3600) + '小时前'
74 | } else if (diff < 3600 * 24 * 2) {
75 | return '1天前'
76 | }
77 | if (option) {
78 | return parseTime(time, option)
79 | } else {
80 | return (
81 | d.getMonth() +
82 | 1 +
83 | '月' +
84 | d.getDate() +
85 | '日' +
86 | d.getHours() +
87 | '时' +
88 | d.getMinutes() +
89 | '分'
90 | )
91 | }
92 | }
93 |
94 | /**
95 | * @param {string} url
96 | * @returns {Object}
97 | */
98 | export function param2Obj(url) {
99 | const search = url.split('?')[1]
100 | if (!search) {
101 | return {}
102 | }
103 | return JSON.parse(
104 | '{"' +
105 | decodeURIComponent(search)
106 | .replace(/"/g, '\\"')
107 | .replace(/&/g, '","')
108 | .replace(/=/g, '":"')
109 | .replace(/\+/g, ' ') +
110 | '"}'
111 | )
112 | }
113 |
114 | /**
115 | * @param {date} 时间
116 | * @returns {fmt} 格式
117 | */
118 | export function formatDate(date, fmt) {
119 | if (/(y+)/.test(fmt)) {
120 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
121 | }
122 | let o = {
123 | 'M+': date.getMonth() + 1,
124 | 'd+': date.getDate(),
125 | 'H+': date.getHours(),
126 | 'm+': date.getMinutes(),
127 | 's+': date.getSeconds()
128 | }
129 | for (let k in o) {
130 | let str = o[k] + '';
131 | if (new RegExp(`(${k})`).test(fmt)) {
132 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
133 | }
134 | }
135 | return fmt;
136 | }
137 |
138 | function padLeftZero(str) {
139 | return ('00' + str).substring(str.length);
140 | }
141 |
142 | /**
143 | * 解决排序后数据错乱导致列表数据更新出错的问题
144 | * @param {tableData} 列表数据数据
145 | * @returns {selectItem} 需要替换的数据
146 | * @returns {idName} id名
147 | */
148 | export function updateTableItem(tableData, selectItem, idName = 'id') {
149 | for (const v of tableData) {
150 | if (v[idName] === selectItem[idName]) {
151 | const index = tableData.indexOf(v)
152 | tableData.splice(index, 1, selectItem)
153 | break
154 | }
155 | }
156 | }
157 |
158 | /**
159 | * 解决排序后数据错乱导致列表数据删除出错的问题
160 | * @param {tableData} 列表数据数据
161 | * @returns {selectItem} 需要删除的数据
162 | * @returns {idName} id名
163 | */
164 | export function deleteTableItem(tableData, selectItem, idName = 'id') {
165 | for (const v of tableData) {
166 | if (v[idName] === selectItem[idName]) {
167 | const index = tableData.indexOf(v)
168 | tableData.splice(index, 1)
169 | break
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/views/add/newAccount.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 添加账户
32 | 重置
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
122 |
123 |
138 |
--------------------------------------------------------------------------------
/src/store/modules/tagsView.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | visitedViews: [],
3 | cachedViews: []
4 | }
5 |
6 | const mutations = {
7 | ADD_VISITED_VIEW: (state, view) => {
8 | if (state.visitedViews.some(v => v.path === view.path)) return
9 | state.visitedViews.push(
10 | Object.assign({}, view, {
11 | title: view.meta.title || 'no-name'
12 | })
13 | )
14 | },
15 | ADD_CACHED_VIEW: (state, view) => {
16 | if (state.cachedViews.includes(view.name)) return
17 | if (!view.meta.noCache) {
18 | state.cachedViews.push(view.name)
19 | }
20 | },
21 |
22 | DEL_VISITED_VIEW: (state, view) => {
23 | for (const [i, v] of state.visitedViews.entries()) {
24 | if (v.path === view.path) {
25 | state.visitedViews.splice(i, 1)
26 | break
27 | }
28 | }
29 | },
30 | DEL_CACHED_VIEW: (state, view) => {
31 | for (const i of state.cachedViews) {
32 | if (i === view.name) {
33 | const index = state.cachedViews.indexOf(i)
34 | state.cachedViews.splice(index, 1)
35 | break
36 | }
37 | }
38 | },
39 |
40 | DEL_OTHERS_VISITED_VIEWS: (state, view) => {
41 | state.visitedViews = state.visitedViews.filter(v => {
42 | return v.meta.affix || v.path === view.path
43 | })
44 | },
45 | DEL_OTHERS_CACHED_VIEWS: (state, view) => {
46 | for (const i of state.cachedViews) {
47 | if (i === view.name) {
48 | const index = state.cachedViews.indexOf(i)
49 | state.cachedViews = state.cachedViews.slice(index, index + 1)
50 | break
51 | }
52 | }
53 | },
54 |
55 | DEL_ALL_VISITED_VIEWS: state => {
56 | // keep affix tags
57 | const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
58 | state.visitedViews = affixTags
59 | },
60 | DEL_ALL_CACHED_VIEWS: state => {
61 | state.cachedViews = []
62 | },
63 |
64 | UPDATE_VISITED_VIEW: (state, view) => {
65 | for (let v of state.visitedViews) {
66 | if (v.path === view.path) {
67 | v = Object.assign(v, view)
68 | break
69 | }
70 | }
71 | }
72 | }
73 |
74 | const actions = {
75 | addView({ dispatch }, view) {
76 | dispatch('addVisitedView', view)
77 | dispatch('addCachedView', view)
78 | },
79 | addVisitedView({ commit }, view) {
80 | commit('ADD_VISITED_VIEW', view)
81 | },
82 | addCachedView({ commit }, view) {
83 | commit('ADD_CACHED_VIEW', view)
84 | },
85 |
86 | delView({ dispatch, state }, view) {
87 | return new Promise(resolve => {
88 | dispatch('delVisitedView', view)
89 | dispatch('delCachedView', view)
90 | resolve({
91 | visitedViews: [...state.visitedViews],
92 | cachedViews: [...state.cachedViews]
93 | })
94 | })
95 | },
96 | delVisitedView({ commit, state }, view) {
97 | return new Promise(resolve => {
98 | commit('DEL_VISITED_VIEW', view)
99 | resolve([...state.visitedViews])
100 | })
101 | },
102 | delCachedView({ commit, state }, view) {
103 | return new Promise(resolve => {
104 | commit('DEL_CACHED_VIEW', view)
105 | resolve([...state.cachedViews])
106 | })
107 | },
108 |
109 | delOthersViews({ dispatch, state }, view) {
110 | return new Promise(resolve => {
111 | dispatch('delOthersVisitedViews', view)
112 | dispatch('delOthersCachedViews', view)
113 | resolve({
114 | visitedViews: [...state.visitedViews],
115 | cachedViews: [...state.cachedViews]
116 | })
117 | })
118 | },
119 | delOthersVisitedViews({ commit, state }, view) {
120 | return new Promise(resolve => {
121 | commit('DEL_OTHERS_VISITED_VIEWS', view)
122 | resolve([...state.visitedViews])
123 | })
124 | },
125 | delOthersCachedViews({ commit, state }, view) {
126 | return new Promise(resolve => {
127 | commit('DEL_OTHERS_CACHED_VIEWS', view)
128 | resolve([...state.cachedViews])
129 | })
130 | },
131 |
132 | delAllViews({ dispatch, state }, view) {
133 | return new Promise(resolve => {
134 | dispatch('delAllVisitedViews', view)
135 | dispatch('delAllCachedViews', view)
136 | resolve({
137 | visitedViews: [...state.visitedViews],
138 | cachedViews: [...state.cachedViews]
139 | })
140 | })
141 | },
142 | delAllVisitedViews({ commit, state }) {
143 | return new Promise(resolve => {
144 | commit('DEL_ALL_VISITED_VIEWS')
145 | resolve([...state.visitedViews])
146 | })
147 | },
148 | delAllCachedViews({ commit, state }) {
149 | return new Promise(resolve => {
150 | commit('DEL_ALL_CACHED_VIEWS')
151 | resolve([...state.cachedViews])
152 | })
153 | },
154 |
155 | updateVisitedView({ commit }, view) {
156 | commit('UPDATE_VISITED_VIEW', view)
157 | }
158 | }
159 |
160 | export default {
161 | namespaced: true,
162 | state,
163 | mutations,
164 | actions
165 | }
166 |
--------------------------------------------------------------------------------
/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height : 100%;
5 | transition : margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position : relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition : width 0.28s;
12 | width : $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height : 100%;
15 | position : fixed;
16 | font-size : 0px;
17 | top : 0;
18 | bottom : 0;
19 | left : 0;
20 | z-index : 1001;
21 | overflow : hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display : inline-block;
52 | width : 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .el-menu {
61 | border: none;
62 | height: 100%;
63 | width : 100% !important;
64 | }
65 |
66 | // menu hover
67 | .submenu-title-noDropdown,
68 | .el-submenu__title {
69 | &:hover {
70 | background-color: $menuHover !important;
71 | }
72 | }
73 |
74 | .is-active>.el-submenu__title {
75 | color: $subMenuActiveText !important;
76 | }
77 |
78 | & .nest-menu .el-submenu>.el-submenu__title,
79 | & .el-submenu .el-menu-item {
80 | min-width : $sideBarWidth !important;
81 | background-color: $subMenuBg !important;
82 |
83 | &:hover {
84 | background-color: $subMenuHover !important;
85 | }
86 | }
87 | }
88 |
89 | .hideSidebar {
90 | .sidebar-container {
91 | width: 54px !important;
92 | }
93 |
94 | .main-container {
95 | margin-left: 54px;
96 | }
97 |
98 | .svg-icon {
99 | margin-right: 0px;
100 | }
101 |
102 | .submenu-title-noDropdown {
103 | padding : 0 !important;
104 | position: relative;
105 |
106 | .el-tooltip {
107 | padding: 0 !important;
108 |
109 | .svg-icon {
110 | margin-left: 20px;
111 | }
112 | }
113 | }
114 |
115 | .el-submenu {
116 | overflow: hidden;
117 |
118 | &>.el-submenu__title {
119 | padding: 0 !important;
120 |
121 | .svg-icon {
122 | margin-left: 20px;
123 | }
124 |
125 | .el-submenu__icon-arrow {
126 | display: none;
127 | }
128 | }
129 | }
130 |
131 | .el-menu--collapse {
132 | .el-submenu {
133 | &>.el-submenu__title {
134 | &>span {
135 | height : 0;
136 | width : 0;
137 | overflow : hidden;
138 | visibility: hidden;
139 | display : inline-block;
140 | }
141 | }
142 | }
143 | }
144 | }
145 |
146 | .el-menu--collapse .el-menu .el-submenu {
147 | min-width: $sideBarWidth !important;
148 | }
149 |
150 | // mobile responsive
151 | .mobile {
152 | .main-container {
153 | margin-left: 0px;
154 | }
155 |
156 | .sidebar-container {
157 | transition: transform .28s;
158 | width : $sideBarWidth !important;
159 | }
160 |
161 | &.hideSidebar {
162 | .sidebar-container {
163 | pointer-events : none;
164 | transition-duration: 0.3s;
165 | transform : translate3d(-$sideBarWidth, 0, 0);
166 | }
167 | }
168 | }
169 |
170 | .withoutAnimation {
171 |
172 | .main-container,
173 | .sidebar-container {
174 | transition: none;
175 | }
176 | }
177 | }
178 |
179 | // when menu collapsed
180 | .el-menu--vertical {
181 | &>.el-menu {
182 | .svg-icon {
183 | margin-right: 16px;
184 | }
185 | }
186 |
187 | .nest-menu .el-submenu>.el-submenu__title,
188 | .el-menu-item {
189 | &:hover {
190 | // you can use $subMenuHover
191 | background-color: $menuHover !important;
192 | }
193 | }
194 |
195 | // the scroll bar appears when the subMenu is too long
196 | >.el-menu--popup {
197 | max-height: 100vh;
198 | overflow-y: auto;
199 |
200 | &::-webkit-scrollbar-track-piece {
201 | background: #d3dce6;
202 | }
203 |
204 | &::-webkit-scrollbar {
205 | width: 6px;
206 | }
207 |
208 | &::-webkit-scrollbar-thumb {
209 | background : #99a9bf;
210 | border-radius: 20px;
211 | }
212 | }
213 | }
--------------------------------------------------------------------------------
/src/views/add/newVersion.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 | 选取.apk文件
28 |
29 |
30 |
31 | 添加版本
32 | 重置
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
129 |
130 |
145 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | /* Layout */
7 | import Layout from '@/layout'
8 |
9 | /**
10 | * Note: sub-menu only appear when route children.length >= 1
11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
12 | *
13 | * hidden: true if set true, item will not show in the sidebar(default is false)
14 | * alwaysShow: true if set true, will always show the root menu
15 | * if not set alwaysShow, when item has more than one children route,
16 | * it will becomes nested mode, otherwise not show the root menu
17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
18 | * name:'router-name' the name is used by (must set!!!)
19 | * meta : {
20 | roles: ['admin','editor'] control the page roles (you can set multiple roles)
21 | title: 'title' the name show in sidebar and breadcrumb (recommend set)
22 | icon: 'svg-name' the icon show in the sidebar
23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
25 | }
26 | */
27 |
28 | /**
29 | * constantRoutes
30 | * a base page that does not have permission requirements
31 | * all roles can be accessed
32 | */
33 | export const constantRoutes = [{
34 | path: '/redirect',
35 | component: Layout,
36 | hidden: true,
37 | children: [{
38 | path: '/redirect/:path*',
39 | component: () => import('@/views/redirect/index')
40 | }]
41 | }, {
42 | path: '/login',
43 | component: () => import('@/views/login/index'),
44 | hidden: true
45 | },
46 |
47 | {
48 | path: '/404',
49 | component: () => import('@/views/404'),
50 | hidden: true
51 | },
52 |
53 | {
54 | path: '/',
55 | component: Layout,
56 | redirect: '/dashboard',
57 | children: [{
58 | path: 'dashboard',
59 | name: 'Dashboard',
60 | component: () => import('@/views/dashboard/index'),
61 | meta: {
62 | title: '控制台',
63 | icon: 'dashboard',
64 | affix: true
65 | }
66 | }]
67 | },
68 |
69 | {
70 | path: '/list',
71 | component: Layout,
72 | redirect: '/list/versions',
73 | name: 'List',
74 | meta: {
75 | title: '数据管理',
76 | icon: 'example'
77 | },
78 | children: [{
79 | path: 'versions',
80 | name: 'Versions',
81 | component: () => import('@/views/list/versions'),
82 | meta: {
83 | title: '版本列表'
84 | }
85 | },
86 | {
87 | path: 'accounts',
88 | name: 'Accounts',
89 | component: () => import('@/views/list/accounts'),
90 | meta: {
91 | title: '账户列表',
92 | }
93 | }
94 | ]
95 | },
96 |
97 | {
98 | path: '/add',
99 | component: Layout,
100 | redirect: '/add/newVersion',
101 | name: 'Add',
102 | meta: {
103 | title: '数据添加',
104 | icon: 'form'
105 | },
106 | children: [{
107 | path: 'newVersion',
108 | name: 'NewVersion',
109 | component: () => import('@/views/add/newVersion'),
110 | meta: {
111 | title: '添加新版本'
112 | }
113 | },
114 | {
115 | path: 'newAccount',
116 | name: 'NewAccount',
117 | component: () => import('@/views/add/newAccount'),
118 | meta: {
119 | title: '添加新账户'
120 | }
121 | }
122 | ]
123 | },
124 | // 404 page must be placed at the end !!!
125 | {
126 | path: '*',
127 | redirect: '/404',
128 | hidden: true
129 | }
130 | ]
131 |
132 |
133 | /**
134 | * asyncRoutes
135 | * the routes that need to be dynamically loaded based on user roles
136 | */
137 | export const asyncRoutes = [{
138 | path: '/add',
139 | component: Layout,
140 | redirect: '/add/newVersion',
141 | name: 'Add',
142 | meta: {
143 | title: '数据添加',
144 | icon: 'form',
145 | roles: ['admin', 'editor']
146 | },
147 | children: [{
148 | path: 'newVersion',
149 | name: 'NewVersion',
150 | component: () => import('@/views/add/newVersion'),
151 | meta: {
152 | title: '添加新版本',
153 | roles: ['admin', 'editor']
154 | }
155 | },
156 | {
157 | path: 'newAccount',
158 | name: 'NewAccount',
159 | component: () => import('@/views/add/newAccount'),
160 | meta: {
161 | title: '添加新账户',
162 | roles: ['admin']
163 | }
164 | }
165 | ]
166 | }, ]
167 |
168 | const createRouter = () => new Router({
169 | // mode: 'history', // require service support
170 | scrollBehavior: () => ({
171 | y: 0
172 | }),
173 | routes: constantRoutes
174 | })
175 |
176 | const router = createRouter()
177 |
178 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
179 | export function resetRouter() {
180 | const newRouter = createRouter()
181 | router.matcher = newRouter.matcher // reset router
182 | }
183 |
184 | export default router
185 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | parser: 'babel-eslint',
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true,
10 | es6: true,
11 | },
12 | extends: ['plugin:vue/recommended', 'eslint:recommended'],
13 |
14 | // add your custom rules here
15 | //it is base on https://github.com/vuejs/eslint-config-vue
16 | rules: {
17 | "vue/max-attributes-per-line": [2, {
18 | "singleline": 10,
19 | "multiline": {
20 | "max": 1,
21 | "allowFirstLine": false
22 | }
23 | }],
24 | "vue/singleline-html-element-content-newline": "off",
25 | "vue/multiline-html-element-content-newline":"off",
26 | "vue/name-property-casing": ["error", "PascalCase"],
27 | "vue/no-v-html": "off",
28 | 'accessor-pairs': 2,
29 | 'arrow-spacing': [2, {
30 | 'before': true,
31 | 'after': true
32 | }],
33 | 'block-spacing': [2, 'always'],
34 | 'brace-style': [2, '1tbs', {
35 | 'allowSingleLine': true
36 | }],
37 | 'camelcase': [0, {
38 | 'properties': 'always'
39 | }],
40 | 'comma-dangle': [2, 'never'],
41 | 'comma-spacing': [2, {
42 | 'before': false,
43 | 'after': true
44 | }],
45 | 'comma-style': [2, 'last'],
46 | 'constructor-super': 2,
47 | 'curly': [2, 'multi-line'],
48 | 'dot-location': [2, 'property'],
49 | 'eol-last': 2,
50 | 'eqeqeq': ["error", "always", {"null": "ignore"}],
51 | 'generator-star-spacing': [2, {
52 | 'before': true,
53 | 'after': true
54 | }],
55 | 'handle-callback-err': [2, '^(err|error)$'],
56 | 'indent': [2, 2, {
57 | 'SwitchCase': 1
58 | }],
59 | 'jsx-quotes': [2, 'prefer-single'],
60 | 'key-spacing': [2, {
61 | 'beforeColon': false,
62 | 'afterColon': true
63 | }],
64 | 'keyword-spacing': [2, {
65 | 'before': true,
66 | 'after': true
67 | }],
68 | 'new-cap': [2, {
69 | 'newIsCap': true,
70 | 'capIsNew': false
71 | }],
72 | 'new-parens': 2,
73 | 'no-array-constructor': 2,
74 | 'no-caller': 2,
75 | 'no-console': 'off',
76 | 'no-class-assign': 2,
77 | 'no-cond-assign': 2,
78 | 'no-const-assign': 2,
79 | 'no-control-regex': 0,
80 | 'no-delete-var': 2,
81 | 'no-dupe-args': 2,
82 | 'no-dupe-class-members': 2,
83 | 'no-dupe-keys': 2,
84 | 'no-duplicate-case': 2,
85 | 'no-empty-character-class': 2,
86 | 'no-empty-pattern': 2,
87 | 'no-eval': 2,
88 | 'no-ex-assign': 2,
89 | 'no-extend-native': 2,
90 | 'no-extra-bind': 2,
91 | 'no-extra-boolean-cast': 2,
92 | 'no-extra-parens': [2, 'functions'],
93 | 'no-fallthrough': 2,
94 | 'no-floating-decimal': 2,
95 | 'no-func-assign': 2,
96 | 'no-implied-eval': 2,
97 | 'no-inner-declarations': [2, 'functions'],
98 | 'no-invalid-regexp': 2,
99 | 'no-irregular-whitespace': 2,
100 | 'no-iterator': 2,
101 | 'no-label-var': 2,
102 | 'no-labels': [2, {
103 | 'allowLoop': false,
104 | 'allowSwitch': false
105 | }],
106 | 'no-lone-blocks': 2,
107 | 'no-mixed-spaces-and-tabs': 2,
108 | 'no-multi-spaces': 2,
109 | 'no-multi-str': 2,
110 | 'no-multiple-empty-lines': [2, {
111 | 'max': 1
112 | }],
113 | 'no-native-reassign': 2,
114 | 'no-negated-in-lhs': 2,
115 | 'no-new-object': 2,
116 | 'no-new-require': 2,
117 | 'no-new-symbol': 2,
118 | 'no-new-wrappers': 2,
119 | 'no-obj-calls': 2,
120 | 'no-octal': 2,
121 | 'no-octal-escape': 2,
122 | 'no-path-concat': 2,
123 | 'no-proto': 2,
124 | 'no-redeclare': 2,
125 | 'no-regex-spaces': 2,
126 | 'no-return-assign': [2, 'except-parens'],
127 | 'no-self-assign': 2,
128 | 'no-self-compare': 2,
129 | 'no-sequences': 2,
130 | 'no-shadow-restricted-names': 2,
131 | 'no-spaced-func': 2,
132 | 'no-sparse-arrays': 2,
133 | 'no-this-before-super': 2,
134 | 'no-throw-literal': 2,
135 | 'no-trailing-spaces': 2,
136 | 'no-undef': 2,
137 | 'no-undef-init': 2,
138 | 'no-unexpected-multiline': 2,
139 | 'no-unmodified-loop-condition': 2,
140 | 'no-unneeded-ternary': [2, {
141 | 'defaultAssignment': false
142 | }],
143 | 'no-unreachable': 2,
144 | 'no-unsafe-finally': 2,
145 | 'no-unused-vars': [2, {
146 | 'vars': 'all',
147 | 'args': 'none'
148 | }],
149 | 'no-useless-call': 2,
150 | 'no-useless-computed-key': 2,
151 | 'no-useless-constructor': 2,
152 | 'no-useless-escape': 0,
153 | 'no-whitespace-before-property': 2,
154 | 'no-with': 2,
155 | 'one-var': [2, {
156 | 'initialized': 'never'
157 | }],
158 | 'operator-linebreak': [2, 'after', {
159 | 'overrides': {
160 | '?': 'before',
161 | ':': 'before'
162 | }
163 | }],
164 | 'padded-blocks': [2, 'never'],
165 | 'quotes': [2, 'single', {
166 | 'avoidEscape': true,
167 | 'allowTemplateLiterals': true
168 | }],
169 | 'semi': [2, 'never'],
170 | 'semi-spacing': [2, {
171 | 'before': false,
172 | 'after': true
173 | }],
174 | 'space-before-blocks': [2, 'always'],
175 | 'space-before-function-paren': [2, 'never'],
176 | 'space-in-parens': [2, 'never'],
177 | 'space-infix-ops': 2,
178 | 'space-unary-ops': [2, {
179 | 'words': true,
180 | 'nonwords': false
181 | }],
182 | 'spaced-comment': [2, 'always', {
183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
184 | }],
185 | 'template-curly-spacing': [2, 'never'],
186 | 'use-isnan': 2,
187 | 'valid-typeof': 2,
188 | 'wrap-iife': [2, 'any'],
189 | 'yield-star-spacing': [2, 'both'],
190 | 'yoda': [2, 'never'],
191 | 'prefer-const': 2,
192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
193 | 'object-curly-spacing': [2, 'always', {
194 | objectsInObjects: false
195 | }],
196 | 'array-bracket-spacing': [2, 'never']
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
OOPS!
12 |
15 |
{{ message }}
16 |
Please check that the URL you entered is correct, or click the button below to return to the homepage.
17 |
Back to home
18 |
19 |
20 |
21 |
22 |
23 |
34 |
35 |
229 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
版本更新管理系统
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 | 登陆
31 |
32 |
33 |
34 |
35 |
124 |
125 |
172 |
173 |
237 |
--------------------------------------------------------------------------------
/src/views/list/accounts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{formatAuthority(scope.row.authority)}}
12 |
13 |
14 |
15 |
16 |
17 | {{changeDateFormat(scope.row.registerTime)}}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
207 |
208 |
216 |
--------------------------------------------------------------------------------
/src/views/list/versions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{(scope.row.apkSize / 1024).toFixed(2)}}
12 |
13 |
14 |
15 |
16 | {{scope.row.updateStatus == 2 ? "是":"否"}}
17 |
18 |
19 |
20 |
21 | {{changeDateFormat(scope.row.uploadTime)}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
54 |
55 |
56 |
57 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
216 |
217 |
225 |
--------------------------------------------------------------------------------
/src/layout/components/TagsView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 | {{tag.title}}
8 |
9 |
10 |
11 |
25 |
26 |
27 |
28 |
208 |
209 |
285 |
286 |
314 |
--------------------------------------------------------------------------------