├── .eslintignore
├── public
├── favicon.ico
└── index.html
├── README.md
├── tests
└── unit
│ ├── .eslintrc.js
│ ├── utils
│ ├── param2Obj.spec.js
│ ├── validate.spec.js
│ ├── formatTime.spec.js
│ └── parseTime.spec.js
│ └── components
│ ├── Hamburger.spec.js
│ ├── SvgIcon.spec.js
│ └── Breadcrumb.spec.js
├── .env.development
├── .travis.yml
├── src
├── assets
│ ├── 404_images
│ │ ├── 404.png
│ │ └── 404_cloud.png
│ └── default_profile.png
├── layout
│ ├── components
│ │ ├── index.js
│ │ ├── Sidebar
│ │ │ ├── FixiOSBug.js
│ │ │ ├── Link.vue
│ │ │ ├── Item.vue
│ │ │ ├── index.vue
│ │ │ ├── Logo.vue
│ │ │ └── SidebarItem.vue
│ │ ├── AppMain.vue
│ │ └── Navbar.vue
│ ├── mixin
│ │ └── ResizeHandler.js
│ └── index.vue
├── api
│ ├── student
│ │ ├── record.js
│ │ └── exam.js
│ ├── admin
│ │ ├── chapter.js
│ │ ├── record.js
│ │ ├── exam.js
│ │ ├── repo.js
│ │ ├── user.js
│ │ ├── paper.js
│ │ └── question.js
│ └── system
│ │ └── user.js
├── utils
│ ├── get-page-title.js
│ ├── auth.js
│ ├── validate.js
│ ├── request.js
│ └── index.js
├── icons
│ ├── svg
│ │ ├── link.svg
│ │ ├── user.svg
│ │ ├── example.svg
│ │ ├── table.svg
│ │ ├── password.svg
│ │ ├── nested.svg
│ │ ├── eye.svg
│ │ ├── eye-open.svg
│ │ ├── tree.svg
│ │ ├── dashboard.svg
│ │ └── form.svg
│ ├── index.js
│ └── svgo.yml
├── directive
│ ├── waves
│ │ ├── index.js
│ │ ├── waves.css
│ │ └── waves.js
│ └── permission
│ │ ├── index.js
│ │ └── permission.js
├── settings.js
├── store
│ ├── getters.js
│ ├── index.js
│ └── modules
│ │ ├── settings.js
│ │ ├── app.js
│ │ ├── permission.js
│ │ └── user.js
├── styles
│ ├── mixin.scss
│ ├── variables.scss
│ ├── element-ui.scss
│ ├── transition.scss
│ ├── index.scss
│ └── sidebar.scss
├── views
│ ├── dashboard
│ │ └── index.vue
│ ├── online-exam
│ │ ├── online-exam
│ │ │ ├── info.vue
│ │ │ └── list.vue
│ │ └── exam-grade
│ │ │ ├── list.vue
│ │ │ └── record.vue
│ ├── exam-manager
│ │ ├── repo
│ │ │ ├── chapter.vue
│ │ │ └── list.vue
│ │ └── paper
│ │ │ └── list.vue
│ ├── user-manager
│ │ └── person-info
│ │ │ └── index.vue
│ ├── 404.vue
│ └── login
│ │ ├── register.vue
│ │ └── index.vue
├── App.vue
├── main.js
├── components
│ ├── Hamburger
│ │ └── index.vue
│ ├── SvgIcon
│ │ └── index.vue
│ └── Breadcrumb
│ │ └── index.vue
├── permission.js
└── router
│ └── index.js
├── .env.production
├── .env.staging
├── jsconfig.json
├── postcss.config.js
├── .gitignore
├── .editorconfig
├── babel.config.js
├── mock
├── utils.js
├── table.js
├── index.js
├── user.js
└── mock-server.js
├── jest.config.js
├── package.json
├── vue.config.js
└── .eslintrc.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | src/assets
3 | public
4 | dist
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huanfenz/ExamVue/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 在线考试系统
2 |
3 | 在线考试系统Vue工程。
4 |
5 | 后端工程在这:https://github.com/huanfenz/Exam
6 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'development'
3 |
4 | # base api
5 | VUE_APP_BASE_API = '/dev-api'
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: 10
3 | script: npm run test
4 | notifications:
5 | email: false
6 |
--------------------------------------------------------------------------------
/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huanfenz/ExamVue/HEAD/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/src/assets/default_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huanfenz/ExamVue/HEAD/src/assets/default_profile.png
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | # just a flag
2 | ENV = 'production'
3 |
4 | # base api
5 | VUE_APP_BASE_API = '/prod-api'
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huanfenz/ExamVue/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 = '/stage-api'
8 |
9 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | },
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | 'plugins': {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | 'autoprefixer': {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/api/student/record.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 分页查询成绩列表
4 | export function queryRecordByPageByStudent(params) {
5 | return request({
6 | url: '/student/record/queryPage',
7 | method: 'get',
8 | params
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/.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/directive/waves/index.js:
--------------------------------------------------------------------------------
1 | import waves from './waves'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('waves', waves)
5 | }
6 |
7 | if (window.Vue) {
8 | window.waves = waves
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | waves.install = install
13 | export default waves
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | insert_final_newline = false
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: '在线考试系统',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: false,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether show the logo in sidebar
14 | */
15 | sidebarLogo: false
16 | }
17 |
--------------------------------------------------------------------------------
/src/directive/permission/index.js:
--------------------------------------------------------------------------------
1 | import permission from './permission'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('permission', permission)
5 | }
6 |
7 | if (window.Vue) {
8 | window['permission'] = permission
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | permission.install = install
13 | export default permission
14 |
--------------------------------------------------------------------------------
/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/utils/auth.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const TokenKey = 'vue_admin_template_token'
4 |
5 | export function getToken() {
6 | return Cookies.get(TokenKey)
7 | }
8 |
9 | export function setToken(token) {
10 | return Cookies.set(TokenKey, token)
11 | }
12 |
13 | export function removeToken() {
14 | return Cookies.remove(TokenKey)
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | token: state => state.user.token,
5 | id: state => state.user.id,
6 | avatar: state => state.user.avatar,
7 | name: state => state.user.name,
8 | roles: state => state.user.roles,
9 | permission_routes: state => state.permission.routes
10 | }
11 | export default getters
12 |
--------------------------------------------------------------------------------
/src/api/admin/chapter.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 查询所有章节信息
4 | export function queryChapters(params) {
5 | return request({
6 | url: '/admin/chapter/queryChapters',
7 | method: 'get',
8 | params
9 | })
10 | }
11 |
12 | // 保存章节
13 | export function saveChapters(data) {
14 | return request({
15 | url: '/admin/chapter/saveChapters',
16 | method: 'post',
17 | data
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername(str) {
18 | const valid_map = ['admin', 'editor']
19 | return valid_map.indexOf(str.trim()) >= 0
20 | }
21 |
--------------------------------------------------------------------------------
/tests/unit/utils/param2Obj.spec.js:
--------------------------------------------------------------------------------
1 | import { param2Obj } from '@/utils/index.js'
2 | describe('Utils:param2Obj', () => {
3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
4 |
5 | it('param2Obj test', () => {
6 | expect(param2Obj(url)).toEqual({
7 | name: 'bill',
8 | age: '29',
9 | sex: '1',
10 | field: window.btoa('test'),
11 | key: '测试'
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 | import app from './modules/app'
5 | import permission from './modules/permission'
6 | import settings from './modules/settings'
7 | import user from './modules/user'
8 |
9 | // import createPersistedState from 'vuex-persistedstate'
10 |
11 | Vue.use(Vuex)
12 |
13 | const store = new Vuex.Store({
14 | modules: {
15 | app,
16 | permission,
17 | settings,
18 | user
19 | },
20 | getters
21 | // plugins: [createPersistedState()]
22 | })
23 |
24 | export default store
25 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
4 | '@vue/cli-plugin-babel/preset'
5 | ],
6 | 'env': {
7 | 'development': {
8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
11 | 'plugins': ['dynamic-import-node']
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/mock/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} url
3 | * @returns {Object}
4 | */
5 | function param2Obj(url) {
6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
7 | if (!search) {
8 | return {}
9 | }
10 | const obj = {}
11 | const searchArr = search.split('&')
12 | searchArr.forEach(v => {
13 | const index = v.indexOf('=')
14 | if (index !== -1) {
15 | const name = v.substring(0, index)
16 | const val = v.substring(index + 1, v.length)
17 | obj[name] = val
18 | }
19 | })
20 | return obj
21 | }
22 |
23 | module.exports = {
24 | param2Obj
25 | }
26 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 欢迎使用 在线考试系统
5 | 系统版本:1.0.0
6 |
7 |
8 |
9 |
10 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/mock/table.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 |
3 | const data = Mock.mock({
4 | 'items|30': [{
5 | id: '@id',
6 | title: '@sentence(10, 20)',
7 | 'status|1': ['published', 'draft', 'deleted'],
8 | author: 'name',
9 | display_time: '@datetime',
10 | pageviews: '@integer(300, 5000)'
11 | }]
12 | })
13 |
14 | module.exports = [
15 | {
16 | url: '/vue-admin-template/table/list',
17 | type: 'get',
18 | response: config => {
19 | const items = data.items
20 | return {
21 | code: 20000,
22 | data: {
23 | total: items.length,
24 | items: items
25 | }
26 | }
27 | }
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= webpackConfig.name %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
38 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
4 |
5 | const state = {
6 | showSettings: showSettings,
7 | fixedHeader: fixedHeader,
8 | sidebarLogo: sidebarLogo
9 | }
10 |
11 | const mutations = {
12 | CHANGE_SETTING: (state, { key, value }) => {
13 | // eslint-disable-next-line no-prototype-builtins
14 | if (state.hasOwnProperty(key)) {
15 | state[key] = value
16 | }
17 | }
18 | }
19 |
20 | const actions = {
21 | changeSetting({ commit }, data) {
22 | commit('CHANGE_SETTING', data)
23 | }
24 | }
25 |
26 | export default {
27 | namespaced: true,
28 | state,
29 | mutations,
30 | actions
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 210px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/tests/unit/components/Hamburger.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import Hamburger from '@/components/Hamburger/index.vue'
3 | describe('Hamburger.vue', () => {
4 | it('toggle click', () => {
5 | const wrapper = shallowMount(Hamburger)
6 | const mockFn = jest.fn()
7 | wrapper.vm.$on('toggleClick', mockFn)
8 | wrapper.find('.hamburger').trigger('click')
9 | expect(mockFn).toBeCalled()
10 | })
11 | it('prop isActive', () => {
12 | const wrapper = shallowMount(Hamburger)
13 | wrapper.setProps({ isActive: true })
14 | expect(wrapper.contains('.is-active')).toBe(true)
15 | wrapper.setProps({ isActive: false })
16 | expect(wrapper.contains('.is-active')).toBe(false)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/tests/unit/components/SvgIcon.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import SvgIcon from '@/components/SvgIcon/index.vue'
3 | describe('SvgIcon.vue', () => {
4 | it('iconClass', () => {
5 | const wrapper = shallowMount(SvgIcon, {
6 | propsData: {
7 | iconClass: 'test'
8 | }
9 | })
10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11 | })
12 | it('className', () => {
13 | const wrapper = shallowMount(SvgIcon, {
14 | propsData: {
15 | iconClass: 'test'
16 | }
17 | })
18 | expect(wrapper.classes().length).toBe(1)
19 | wrapper.setProps({ className: 'test' })
20 | expect(wrapper.classes().includes('test')).toBe(true)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/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 { validUsername, isExternal } from '@/utils/validate.js'
2 |
3 | describe('Utils:validate', () => {
4 | it('validUsername', () => {
5 | expect(validUsername('admin')).toBe(true)
6 | expect(validUsername('editor')).toBe(true)
7 | expect(validUsername('xxxx')).toBe(false)
8 | })
9 | it('isExternal', () => {
10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13 | expect(isExternal('/dashboard')).toBe(false)
14 | expect(isExternal('./dashboard')).toBe(false)
15 | expect(isExternal('dashboard')).toBe(false)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
32 |
33 |
41 |
--------------------------------------------------------------------------------
/src/directive/permission/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | function checkPermission(el, binding) {
4 | const { value } = binding
5 | const roles = store.getters && store.getters.roles
6 |
7 | if (value && value instanceof Array) {
8 | if (value.length > 0) {
9 | const permissionRoles = value
10 |
11 | const hasPermission = roles.some(role => {
12 | return permissionRoles.includes(role)
13 | })
14 |
15 | if (!hasPermission) {
16 | el.parentNode && el.parentNode.removeChild(el)
17 | }
18 | }
19 | } else {
20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`)
21 | }
22 | }
23 |
24 | export default {
25 | inserted(el, binding) {
26 | checkPermission(el, binding)
27 | },
28 | update(el, binding) {
29 | checkPermission(el, binding)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
3 | transform: {
4 | '^.+\\.vue$': 'vue-jest',
5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
6 | 'jest-transform-stub',
7 | '^.+\\.jsx?$': 'babel-jest'
8 | },
9 | moduleNameMapper: {
10 | '^@/(.*)$': '/src/$1'
11 | },
12 | snapshotSerializers: ['jest-serializer-vue'],
13 | testMatch: [
14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
15 | ],
16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
17 | coverageDirectory: '/tests/unit/coverage',
18 | // 'collectCoverage': true,
19 | 'coverageReporters': [
20 | 'lcov',
21 | 'text-summary'
22 | ],
23 | testURL: 'http://localhost/'
24 | }
25 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
46 | // to fix el-date-picker css style
47 | .el-range-separator {
48 | box-sizing: content-box;
49 | }
50 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.css:
--------------------------------------------------------------------------------
1 | .waves-ripple {
2 | position: absolute;
3 | border-radius: 100%;
4 | background-color: rgba(0, 0, 0, 0.15);
5 | background-clip: padding-box;
6 | pointer-events: none;
7 | -webkit-user-select: none;
8 | -moz-user-select: none;
9 | -ms-user-select: none;
10 | user-select: none;
11 | -webkit-transform: scale(0);
12 | -ms-transform: scale(0);
13 | transform: scale(0);
14 | opacity: 1;
15 | }
16 |
17 | .waves-ripple.z-active {
18 | opacity: 0;
19 | -webkit-transform: scale(2);
20 | -ms-transform: scale(2);
21 | transform: scale(2);
22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out;
25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
26 | }
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/src/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 |
8 | import '@/styles/index.scss' // global css
9 |
10 | import App from './App'
11 | import store from './store'
12 | import router from './router'
13 |
14 | import '@/icons' // icon
15 | import '@/permission' // permission control
16 |
17 | /**
18 | * If you don't want to use mock-server
19 | * you want to use MockJs for mock api
20 | * you can execute: mockXHR()
21 | *
22 | * Currently MockJs will be used in the production environment,
23 | * please remove it before going online ! ! !
24 | */
25 | if (process.env.NODE_ENV === 'production') {
26 | const { mockXHR } = require('../mock')
27 | mockXHR()
28 | }
29 |
30 | // 如果想要中文版 element-ui,按如下方式声明
31 | Vue.use(ElementUI)
32 |
33 | Vue.config.productionTip = false
34 |
35 | new Vue({
36 | el: '#app',
37 | router,
38 | store,
39 | render: h => h(App)
40 | })
41 |
--------------------------------------------------------------------------------
/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/admin/record.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 分页查询考试记录
4 | export function queryRecordByPage(params) {
5 | return request({
6 | url: '/admin/record/queryPage',
7 | method: 'get',
8 | params
9 | })
10 | }
11 |
12 | // 学生分页查询考试记录
13 | export function queryRecordByPageByStudent(params) {
14 | return request({
15 | url: '/admin/record/queryPageByStudent',
16 | method: 'get',
17 | params
18 | })
19 | }
20 |
21 | // 删除考试信息
22 | export function deleteRecord(data) {
23 | return request({
24 | url: '/admin/record/deleteRecord',
25 | method: 'delete',
26 | data
27 | })
28 | }
29 |
30 | // 删除一些考试信息
31 | export function deleteRecords(data) {
32 | return request({
33 | url: '/admin/record/deleteRecords',
34 | method: 'delete',
35 | data
36 | })
37 | }
38 |
39 | // 修改分数
40 | export function alterScore(data) {
41 | return request({
42 | url: '/admin/record/alterScore',
43 | method: 'put',
44 | data
45 | })
46 | }
47 |
48 | // 提交批改
49 | export function commitCheck(data) {
50 | return request({
51 | url: '/admin/record/commitCheck',
52 | method: 'post',
53 | data
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 | @import './mixin.scss';
3 | @import './transition.scss';
4 | @import './element-ui.scss';
5 | @import './sidebar.scss';
6 |
7 | body {
8 | height: 100%;
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 | }
14 |
15 | label {
16 | font-weight: 700;
17 | }
18 |
19 | html {
20 | height: 100%;
21 | box-sizing: border-box;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
28 | *,
29 | *:before,
30 | *:after {
31 | box-sizing: inherit;
32 | }
33 |
34 | a:focus,
35 | a:active {
36 | outline: none;
37 | }
38 |
39 | a,
40 | a:focus,
41 | a:hover {
42 | cursor: pointer;
43 | color: inherit;
44 | text-decoration: none;
45 | }
46 |
47 | div:focus {
48 | outline: none;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | visibility: hidden;
54 | display: block;
55 | font-size: 0;
56 | content: " ";
57 | clear: both;
58 | height: 0;
59 | }
60 | }
61 |
62 | // main-container global css
63 | .app-container {
64 | padding: 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/src/api/admin/exam.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取考试数量
4 | export function getExamCount() {
5 | return request({
6 | url: '/admin/exam/getCount',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 查询所有考试信息
12 | export function queryExams() {
13 | return request({
14 | url: '/admin/exam/queryAll',
15 | method: 'get'
16 | })
17 | }
18 |
19 | // 分页查询考试信息
20 | export function queryExamsByPage(params) {
21 | return request({
22 | url: '/admin/exam/queryPage',
23 | method: 'get',
24 | params
25 | })
26 | }
27 |
28 | // 添加考试信息
29 | export function addExam(data) {
30 | return request({
31 | url: '/admin/exam/addOne',
32 | method: 'post',
33 | data
34 | })
35 | }
36 |
37 | // 删除考试信息
38 | export function deleteExam(data) {
39 | return request({
40 | url: '/admin/exam/deleteOne',
41 | method: 'delete',
42 | data
43 | })
44 | }
45 |
46 | // 删除一些考试信息
47 | export function deleteExams(data) {
48 | return request({
49 | url: '/admin/exam/deleteSome',
50 | method: 'delete',
51 | data
52 | })
53 | }
54 |
55 | // 更新考试信息
56 | export function updateExam(data) {
57 | return request({
58 | url: '/admin/exam/updateOne',
59 | method: 'put',
60 | data
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/src/api/admin/repo.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取题库数量
4 | export function getRepoCount() {
5 | return request({
6 | url: '/admin/repo/getCount',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 查询所有题库信息
12 | export function queryRepos() {
13 | return request({
14 | url: '/admin/repo/queryAll',
15 | method: 'get'
16 | })
17 | }
18 |
19 | // 分页查询题库信息
20 | export function queryReposByPage(params) {
21 | return request({
22 | url: '/admin/repo/queryPage',
23 | method: 'get',
24 | params
25 | })
26 | }
27 |
28 | // 添加题库信息
29 | export function addRepo(data) {
30 | return request({
31 | url: '/admin/repo/addOne',
32 | method: 'post',
33 | data
34 | })
35 | }
36 |
37 | // 删除题库信息
38 | export function deleteRepo(data) {
39 | return request({
40 | url: '/admin/repo/deleteOne',
41 | method: 'delete',
42 | data
43 | })
44 | }
45 |
46 | // 删除一些题库信息
47 | export function deleteRepos(data) {
48 | return request({
49 | url: '/admin/repo/deleteSome',
50 | method: 'delete',
51 | data
52 | })
53 | }
54 |
55 | // 更新题库信息
56 | export function updateRepo(data) {
57 | return request({
58 | url: '/admin/repo/updateOne',
59 | method: 'put',
60 | data
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/src/api/admin/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取用户数量
4 | export function getUserCount() {
5 | return request({
6 | url: '/admin/user/getCount',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 查询所有用户信息
12 | export function queryUsers() {
13 | return request({
14 | url: '/admin/user/queryAll',
15 | method: 'get'
16 | })
17 | }
18 |
19 | // 分页查询用户信息
20 | export function queryUsersByPage(params) {
21 | return request({
22 | url: '/admin/user/queryPage',
23 | method: 'get',
24 | params
25 | })
26 | }
27 |
28 | // 添加用户信息
29 | export function addUser(data) {
30 | return request({
31 | url: '/admin/user/addOne',
32 | method: 'post',
33 | data
34 | })
35 | }
36 |
37 | // 删除用户信息
38 | export function deleteUser(data) {
39 | return request({
40 | url: '/admin/user/deleteOne',
41 | method: 'delete',
42 | data
43 | })
44 | }
45 |
46 | // 删除一些用户信息
47 | export function deleteUsers(data) {
48 | return request({
49 | url: '/admin/user/deleteSome',
50 | method: 'delete',
51 | data
52 | })
53 | }
54 |
55 | // 更新用户信息
56 | export function updateUser(data) {
57 | return request({
58 | url: '/admin/user/updateOne',
59 | method: 'put',
60 | data
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie'
2 |
3 | const state = {
4 | sidebar: {
5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
6 | withoutAnimation: false
7 | },
8 | device: 'desktop'
9 | }
10 |
11 | const mutations = {
12 | TOGGLE_SIDEBAR: state => {
13 | state.sidebar.opened = !state.sidebar.opened
14 | state.sidebar.withoutAnimation = false
15 | if (state.sidebar.opened) {
16 | Cookies.set('sidebarStatus', 1)
17 | } else {
18 | Cookies.set('sidebarStatus', 0)
19 | }
20 | },
21 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
22 | Cookies.set('sidebarStatus', 0)
23 | state.sidebar.opened = false
24 | state.sidebar.withoutAnimation = withoutAnimation
25 | },
26 | TOGGLE_DEVICE: (state, device) => {
27 | state.device = device
28 | }
29 | }
30 |
31 | const actions = {
32 | toggleSideBar({ commit }) {
33 | commit('TOGGLE_SIDEBAR')
34 | },
35 | closeSideBar({ commit }, { withoutAnimation }) {
36 | commit('CLOSE_SIDEBAR', withoutAnimation)
37 | },
38 | toggleDevice({ commit }, device) {
39 | commit('TOGGLE_DEVICE', device)
40 | }
41 | }
42 |
43 | export default {
44 | namespaced: true,
45 | state,
46 | mutations,
47 | actions
48 | }
49 |
--------------------------------------------------------------------------------
/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
1 | import { parseTime } from '@/utils/index.js'
2 |
3 | describe('Utils:parseTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | it('timestamp', () => {
6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7 | })
8 | it('timestamp string', () => {
9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
10 | })
11 | it('ten digits timestamp', () => {
12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
13 | })
14 | it('new Date', () => {
15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
16 | })
17 | it('format', () => {
18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
21 | })
22 | it('get the day of the week', () => {
23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
24 | })
25 | it('get the day of the week', () => {
26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
27 | })
28 | it('empty argument', () => {
29 | expect(parseTime()).toBeNull()
30 | })
31 |
32 | it('null', () => {
33 | expect(parseTime(null)).toBeNull()
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/src/api/system/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取验证码
4 | export function getCode() {
5 | return request({
6 | url: '/system/user/captcha',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 登录
12 | export function login(data) {
13 | return request({
14 | url: '/system/user/login',
15 | method: 'post',
16 | data
17 | })
18 | }
19 |
20 | // 获取用户信息
21 | export function getInfo(token) {
22 | return request({
23 | url: '/system/user/info',
24 | method: 'get',
25 | params: { token }
26 | })
27 | }
28 |
29 | // 获取用户信息
30 | export function getUserById(id) {
31 | return request({
32 | url: '/system/user/getUserById',
33 | method: 'get',
34 | params: { id }
35 | })
36 | }
37 |
38 | // 登出
39 | export function logout(token) {
40 | return request({
41 | url: '/system/user/logout',
42 | method: 'post',
43 | params: { token }
44 | })
45 | }
46 |
47 | // 注册
48 | export function register(data) {
49 | return request({
50 | url: '/system/user/register',
51 | method: 'post',
52 | data
53 | })
54 | }
55 |
56 | // 修改密码
57 | export function alterPassword(params) {
58 | return request({
59 | url: '/system/user/alterPassword',
60 | method: 'post',
61 | params
62 | })
63 | }
64 |
65 | // 修改个人信息
66 | export function updateInfo(data) {
67 | return request({
68 | url: '/system/user/updateInfo',
69 | method: 'put',
70 | data
71 | })
72 | }
73 |
--------------------------------------------------------------------------------
/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/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 | const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
53 | commit('SET_ROUTES', accessedRoutes)
54 | resolve(accessedRoutes)
55 | })
56 | }
57 | }
58 |
59 | export default {
60 | namespaced: true,
61 | state,
62 | mutations,
63 | actions
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/src/api/admin/paper.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取试卷数量
4 | export function getPaperCount() {
5 | return request({
6 | url: '/admin/paper/getCount',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 查询所有试卷信息
12 | export function queryPapers() {
13 | return request({
14 | url: '/admin/paper/queryAll',
15 | method: 'get'
16 | })
17 | }
18 |
19 | // 分页查询试卷信息
20 | export function queryPapersByPage(params) {
21 | return request({
22 | url: '/admin/paper/queryPage',
23 | method: 'get',
24 | params
25 | })
26 | }
27 |
28 | // 添加试卷信息
29 | export function addPaper(data) {
30 | return request({
31 | url: '/admin/paper/addOne',
32 | method: 'post',
33 | data
34 | })
35 | }
36 |
37 | // 移除试卷信息
38 | export function removePaper(data) {
39 | return request({
40 | url: '/admin/paper/removePaper',
41 | method: 'delete',
42 | data
43 | })
44 | }
45 |
46 | // 删除一些试卷信息
47 | export function removePapers(data) {
48 | return request({
49 | url: '/admin/paper/removePapers',
50 | method: 'delete',
51 | data
52 | })
53 | }
54 |
55 | // 更新试卷信息
56 | export function updatePaper(data) {
57 | return request({
58 | url: '/admin/paper/updateOne',
59 | method: 'put',
60 | data
61 | })
62 | }
63 |
64 | // 保存试卷
65 | export function savePaper(data) {
66 | return request({
67 | url: '/admin/paper/savePaper',
68 | method: 'post',
69 | data
70 | })
71 | }
72 |
73 | // 获得试卷
74 | export function getPaper(params) {
75 | return request({
76 | url: '/admin/paper/getPaper',
77 | method: 'get',
78 | params
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | const Mock = require('mockjs')
2 | const { param2Obj } = require('./utils')
3 |
4 | const user = require('./user')
5 | const table = require('./table')
6 |
7 | const mocks = [
8 | ...user,
9 | ...table
10 | ]
11 |
12 | // for front mock
13 | // please use it cautiously, it will redefine XMLHttpRequest,
14 | // which will cause many of your third-party libraries to be invalidated(like progress event).
15 | function mockXHR() {
16 | // mock patch
17 | // https://github.com/nuysoft/Mock/issues/300
18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
19 | Mock.XHR.prototype.send = function() {
20 | if (this.custom.xhr) {
21 | this.custom.xhr.withCredentials = this.withCredentials || false
22 |
23 | if (this.responseType) {
24 | this.custom.xhr.responseType = this.responseType
25 | }
26 | }
27 | this.proxy_send(...arguments)
28 | }
29 |
30 | function XHR2ExpressReqWrap(respond) {
31 | return function(options) {
32 | let result = null
33 | if (respond instanceof Function) {
34 | const { body, type, url } = options
35 | // https://expressjs.com/en/4x/api.html#req
36 | result = respond({
37 | method: type,
38 | body: JSON.parse(body),
39 | query: param2Obj(url)
40 | })
41 | } else {
42 | result = respond
43 | }
44 | return Mock.mock(result)
45 | }
46 | }
47 |
48 | for (const i of mocks) {
49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
50 | }
51 | }
52 |
53 | module.exports = {
54 | mocks,
55 | mockXHR
56 | }
57 |
58 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
55 |
--------------------------------------------------------------------------------
/src/api/student/exam.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 分页查询考试信息
4 | export function queryExamsByPage(params) {
5 | return request({
6 | url: '/student/exam/queryPage',
7 | method: 'get',
8 | params
9 | })
10 | }
11 |
12 | // 校验密码
13 | export function checkPassword(data) {
14 | return request({
15 | url: '/student/exam/checkPassword',
16 | method: 'post',
17 | data
18 | })
19 | }
20 |
21 | // 创建学生考试
22 | export function createStudentExam(data) {
23 | return request({
24 | url: '/student/exam/createStudentExam',
25 | method: 'post',
26 | data
27 | })
28 | }
29 |
30 | // 获取学生回答
31 | export function getExamInfo(params) {
32 | return request({
33 | url: '/student/exam/getExamInfo',
34 | method: 'get',
35 | params
36 | })
37 | }
38 |
39 | // 获得问题
40 | export function getQuestion(params) {
41 | return request({
42 | url: '/student/exam/getQuestion',
43 | method: 'get',
44 | params
45 | })
46 | }
47 |
48 | // 保存答案
49 | export function saveAnswer(data) {
50 | return request({
51 | url: '/student/exam/saveAnswer',
52 | method: 'post',
53 | data
54 | })
55 | }
56 |
57 | // 交卷
58 | export function commitPaper(data) {
59 | return request({
60 | url: '/student/exam/commitPaper',
61 | method: 'post',
62 | data
63 | })
64 | }
65 |
66 | // 获得分数
67 | export function getScore(params) {
68 | return request({
69 | url: '/student/exam/getScore',
70 | method: 'get',
71 | params
72 | })
73 | }
74 |
75 | // 得到详情
76 | export function getDetail(params) {
77 | return request({
78 | url: '/student/exam/getDetail',
79 | method: 'get',
80 | params
81 | })
82 | }
83 |
--------------------------------------------------------------------------------
/src/views/online-exam/online-exam/info.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
感谢您的作答
6 |
试卷批阅结束后会公布详情!
7 |
8 |
9 |
10 |
11 | {{ score }}分
12 |
13 |
感谢您的作答
14 |
如果试卷有主观题,主观题分数将在阅卷后累计到总成绩,请知晓!
15 |
16 |
17 |
18 |
19 |
20 |
21 |
45 |
46 |
73 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/user.js:
--------------------------------------------------------------------------------
1 |
2 | const tokens = {
3 | admin: {
4 | token: 'admin-token'
5 | },
6 | editor: {
7 | token: 'editor-token'
8 | }
9 | }
10 |
11 | const users = {
12 | 'admin-token': {
13 | roles: ['admin'],
14 | introduction: 'I am a super administrator',
15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
16 | name: 'Super Admin'
17 | },
18 | 'editor-token': {
19 | roles: ['editor'],
20 | introduction: 'I am an editor',
21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
22 | name: 'Normal Editor'
23 | }
24 | }
25 |
26 | module.exports = [
27 | // user login
28 | {
29 | url: '/vue-admin-template/user/login',
30 | type: 'post',
31 | response: config => {
32 | const { username } = config.body
33 | const token = tokens[username]
34 |
35 | // mock error
36 | if (!token) {
37 | return {
38 | code: 60204,
39 | message: 'Account and password are incorrect.'
40 | }
41 | }
42 |
43 | return {
44 | code: 20000,
45 | data: token
46 | }
47 | }
48 | },
49 |
50 | // get user info
51 | {
52 | url: '/vue-admin-template/user/info\.*',
53 | type: 'get',
54 | response: config => {
55 | const { token } = config.query
56 | const info = users[token]
57 |
58 | // mock error
59 | if (!info) {
60 | return {
61 | code: 50008,
62 | message: 'Login failed, unable to get user details.'
63 | }
64 | }
65 |
66 | return {
67 | code: 20000,
68 | data: info
69 | }
70 | }
71 | },
72 |
73 | // user logout
74 | {
75 | url: '/vue-admin-template/user/logout',
76 | type: 'post',
77 | response: _ => {
78 | return {
79 | code: 20000,
80 | data: 'success'
81 | }
82 | }
83 | }
84 | ]
85 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { Message } from 'element-ui'
3 | import store from '@/store'
4 | import { getToken } from '@/utils/auth'
5 | import qs from 'qs'
6 |
7 | // create an axios instance
8 | const service = axios.create({
9 | baseURL: 'http://localhost:8093/', // url = base url + request url
10 | // withCredentials: true, // send cookies when cross-domain requests
11 | timeout: 5000 // request timeout
12 | })
13 |
14 | // request interceptor
15 | service.interceptors.request.use(
16 | config => {
17 | // do something before request is sent
18 |
19 | if (config.method === 'get') {
20 | // 如果是get请求,且params是数组类型如arr=[1,2],则转换成arr=1&arr=2
21 | config.paramsSerializer = function(params) {
22 | return qs.stringify(params, { arrayFormat: 'repeat' })
23 | }
24 | }
25 |
26 | if (store.getters.token) {
27 | // let each request carry token
28 | // ['X-Token'] is a custom headers key
29 | // please modify it according to the actual situation
30 | config.headers['X-Token'] = getToken()
31 | }
32 | return config
33 | },
34 | error => {
35 | // do something with request error
36 | console.log(error) // for debug
37 | return Promise.reject(error)
38 | }
39 | )
40 |
41 | // response interceptor
42 | service.interceptors.response.use(
43 | /**
44 | * If you want to get http information such as headers or status
45 | * Please return response => response
46 | */
47 |
48 | /**
49 | * Determine the request status by custom code
50 | * Here is just an example
51 | * You can also judge the status by HTTP Status Code
52 | */
53 | response => {
54 | const res = response.data
55 | return res
56 | },
57 | error => {
58 | console.log('err' + error) // for debug
59 | Message({
60 | message: error.message,
61 | type: 'error',
62 | duration: 5 * 1000
63 | })
64 | return Promise.reject(error)
65 | }
66 | )
67 |
68 | export default service
69 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "exam-vue",
3 | "version": "0.0.1",
4 | "description": "Huanfenz's online exam system!",
5 | "author": "Huanfenz ",
6 | "scripts": {
7 | "dev": "vue-cli-service serve",
8 | "build:prod": "vue-cli-service build",
9 | "build:stage": "vue-cli-service build --mode staging",
10 | "preview": "node build/index.js --preview",
11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
12 | "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 | },
16 | "dependencies": {
17 | "@wangeditor/editor": "^0.15.15",
18 | "@wangeditor/editor-for-vue": "^0.6.14-3",
19 | "@wangeditor/plugin-formula": "^1.0.8",
20 | "axios": "0.18.1",
21 | "core-js": "3.6.5",
22 | "element-ui": "^2.13.2",
23 | "js-cookie": "2.2.0",
24 | "katex": "^0.15.3",
25 | "less-loader": "^6.0.0",
26 | "normalize.css": "7.0.0",
27 | "nprogress": "0.2.0",
28 | "path-to-regexp": "2.4.0",
29 | "vue": "2.6.10",
30 | "vue-router": "3.0.6",
31 | "vuex": "3.1.0",
32 | "vuex-persistedstate": "^4.1.0"
33 | },
34 | "devDependencies": {
35 | "@vue/cli-plugin-babel": "4.4.4",
36 | "@vue/cli-plugin-eslint": "4.4.4",
37 | "@vue/cli-plugin-unit-jest": "4.4.4",
38 | "@vue/cli-service": "4.4.4",
39 | "@vue/test-utils": "1.0.0-beta.29",
40 | "autoprefixer": "9.5.1",
41 | "babel-eslint": "10.1.0",
42 | "babel-jest": "23.6.0",
43 | "babel-plugin-dynamic-import-node": "2.3.3",
44 | "chalk": "2.4.2",
45 | "connect": "3.6.6",
46 | "eslint": "6.7.2",
47 | "eslint-plugin-vue": "6.2.2",
48 | "html-webpack-plugin": "3.2.0",
49 | "mockjs": "1.0.1-beta3",
50 | "runjs": "4.3.2",
51 | "sass": "1.26.8",
52 | "sass-loader": "8.0.2",
53 | "script-ext-html-webpack-plugin": "2.1.3",
54 | "serve-static": "1.13.2",
55 | "svg-sprite-loader": "4.1.3",
56 | "svgo": "1.2.2",
57 | "vue-template-compiler": "2.6.10"
58 | },
59 | "browserslist": [
60 | "> 1%",
61 | "last 2 versions"
62 | ],
63 | "engines": {
64 | "node": ">=8.9",
65 | "npm": ">= 3.0.0"
66 | },
67 | "license": "MIT"
68 | }
69 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
79 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.js:
--------------------------------------------------------------------------------
1 | import './waves.css'
2 |
3 | const context = '@@wavesContext'
4 |
5 | function handleClick(el, binding) {
6 | function handle(e) {
7 | const customOpts = Object.assign({}, binding.value)
8 | const opts = Object.assign({
9 | ele: el, // 波纹作用元素
10 | type: 'hit', // hit 点击位置扩散 center中心点扩展
11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
12 | },
13 | customOpts
14 | )
15 | const target = opts.ele
16 | if (target) {
17 | target.style.position = 'relative'
18 | target.style.overflow = 'hidden'
19 | const rect = target.getBoundingClientRect()
20 | let ripple = target.querySelector('.waves-ripple')
21 | if (!ripple) {
22 | ripple = document.createElement('span')
23 | ripple.className = 'waves-ripple'
24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
25 | target.appendChild(ripple)
26 | } else {
27 | ripple.className = 'waves-ripple'
28 | }
29 | switch (opts.type) {
30 | case 'center':
31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
33 | break
34 | default:
35 | ripple.style.top =
36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
37 | document.body.scrollTop) + 'px'
38 | ripple.style.left =
39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
40 | document.body.scrollLeft) + 'px'
41 | }
42 | ripple.style.backgroundColor = opts.color
43 | ripple.className = 'waves-ripple z-active'
44 | return false
45 | }
46 | }
47 |
48 | if (!el[context]) {
49 | el[context] = {
50 | removeHandle: handle
51 | }
52 | } else {
53 | el[context].removeHandle = handle
54 | }
55 |
56 | return handle
57 | }
58 |
59 | export default {
60 | bind(el, binding) {
61 | el.addEventListener('click', handleClick(el, binding), false)
62 | },
63 | update(el, binding) {
64 | el.removeEventListener('click', el[context].removeHandle, false)
65 | el.addEventListener('click', handleClick(el, binding), false)
66 | },
67 | unbind(el) {
68 | el.removeEventListener('click', el[context].removeHandle, false)
69 | el[context] = null
70 | delete el[context]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/mock/mock-server.js:
--------------------------------------------------------------------------------
1 | const chokidar = require('chokidar')
2 | const bodyParser = require('body-parser')
3 | const chalk = require('chalk')
4 | const path = require('path')
5 | const Mock = require('mockjs')
6 |
7 | const mockDir = path.join(process.cwd(), 'mock')
8 |
9 | function registerRoutes(app) {
10 | let mockLastIndex
11 | const { mocks } = require('./index.js')
12 | const mocksForServer = mocks.map(route => {
13 | return responseFake(route.url, route.type, route.response)
14 | })
15 | for (const mock of mocksForServer) {
16 | app[mock.type](mock.url, mock.response)
17 | mockLastIndex = app._router.stack.length
18 | }
19 | const mockRoutesLength = Object.keys(mocksForServer).length
20 | return {
21 | mockRoutesLength: mockRoutesLength,
22 | mockStartIndex: mockLastIndex - mockRoutesLength
23 | }
24 | }
25 |
26 | function unregisterRoutes() {
27 | Object.keys(require.cache).forEach(i => {
28 | if (i.includes(mockDir)) {
29 | delete require.cache[require.resolve(i)]
30 | }
31 | })
32 | }
33 |
34 | // for mock server
35 | const responseFake = (url, type, respond) => {
36 | return {
37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
38 | type: type || 'get',
39 | response(req, res) {
40 | console.log('request invoke:' + req.path)
41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
42 | }
43 | }
44 | }
45 |
46 | module.exports = app => {
47 | // parse app.body
48 | // https://expressjs.com/en/4x/api.html#req.body
49 | app.use(bodyParser.json())
50 | app.use(bodyParser.urlencoded({
51 | extended: true
52 | }))
53 |
54 | const mockRoutes = registerRoutes(app)
55 | var mockRoutesLength = mockRoutes.mockRoutesLength
56 | var mockStartIndex = mockRoutes.mockStartIndex
57 |
58 | // watch files, hot reload mock server
59 | chokidar.watch(mockDir, {
60 | ignored: /mock-server/,
61 | ignoreInitial: true
62 | }).on('all', (event, path) => {
63 | if (event === 'change' || event === 'add') {
64 | try {
65 | // remove mock routes stack
66 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
67 |
68 | // clear routes cache
69 | unregisterRoutes()
70 |
71 | const mockRoutes = registerRoutes(app)
72 | mockRoutesLength = mockRoutes.mockRoutesLength
73 | mockStartIndex = mockRoutes.mockStartIndex
74 |
75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
76 | } catch (error) {
77 | console.log(chalk.redBright(error))
78 | }
79 | }
80 | })
81 | }
82 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // progress bar
5 | import 'nprogress/nprogress.css' // progress bar style
6 | import { getToken } from '@/utils/auth' // get token from cookie
7 | import getPageTitle from '@/utils/get-page-title'
8 |
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 |
11 | const whiteList = ['/login', '/register'] // 没有重定向白名单
12 |
13 | router.beforeEach(async(to, from, next) => {
14 | // start progress bar~
15 | NProgress.start()
16 |
17 | // set page title
18 | document.title = getPageTitle(to.meta.title)
19 |
20 | // determine whether the user has logged in
21 | const hasToken = getToken()
22 |
23 | if (hasToken) {
24 | if (to.path === '/login') {
25 | // if is logged in, redirect to the home page
26 | next({ path: '/' })
27 | NProgress.done()
28 | } else {
29 | // determine whether the user has obtained his permission roles through getInfo
30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0
31 | if (hasRoles) {
32 | next()
33 | } else {
34 | try {
35 | // get user info
36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
37 | const { roles } = await store.dispatch('user/getInfo')
38 |
39 | // generate accessible routes map based on roles
40 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
41 |
42 | // dynamically add accessible routes
43 | router.addRoutes(accessRoutes)
44 |
45 | // hack method to ensure that addRoutes is complete
46 | // set the replace: true, so the navigation will not leave a history record
47 | next({ ...to, replace: true })
48 | } catch (error) {
49 | // remove token and go to login page to re-login
50 | await store.dispatch('user/resetToken')
51 | // Message.error(error || 'Has Error')
52 | Message.error({
53 | message: error || '出现错误,请稍后再试'
54 | })
55 | next(`/login`)
56 | NProgress.done()
57 | }
58 | }
59 | }
60 | } else {
61 | /* has no token*/
62 |
63 | if (whiteList.indexOf(to.path) !== -1) {
64 | // in the free login whitelist, go directly
65 | next()
66 | } else {
67 | // other pages that do not have permission to access are redirected to the login page.
68 | next(`/login`)
69 | NProgress.done()
70 | }
71 | }
72 | })
73 |
74 | router.afterEach(() => {
75 | // finish progress bar
76 | NProgress.done()
77 | })
78 |
--------------------------------------------------------------------------------
/src/api/admin/question.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | // 获取题目数量
4 | export function getQuestionCount() {
5 | return request({
6 | url: '/admin/question/getCount',
7 | method: 'get'
8 | })
9 | }
10 |
11 | // 查询所有题目信息
12 | export function queryQuestions() {
13 | return request({
14 | url: '/admin/question/queryAll',
15 | method: 'get'
16 | })
17 | }
18 |
19 | // 分页查询题目信息
20 | export function queryQuestionsByPage(params) {
21 | return request({
22 | url: '/admin/question/queryPage',
23 | method: 'get',
24 | params
25 | })
26 | }
27 |
28 | // 根据id查询题目
29 | export function queryQuestionById(params) {
30 | return request({
31 | url: '/admin/question/queryOneById',
32 | method: 'get',
33 | params
34 | })
35 | }
36 |
37 | // 根据题目id查询选项
38 | export function queryOptionsByQuestionId(params) {
39 | return request({
40 | url: '/admin/question/queryOptionsByQuestionId',
41 | method: 'get',
42 | params
43 | })
44 | }
45 |
46 | // 添加题目信息
47 | export function addQuestion(data) {
48 | return request({
49 | url: '/admin/question/addOne',
50 | method: 'post',
51 | data
52 | })
53 | }
54 |
55 | // 删除题目信息
56 | export function deleteQuestion(data) {
57 | return request({
58 | url: '/admin/question/deleteOne',
59 | method: 'delete',
60 | data
61 | })
62 | }
63 |
64 | // 删除一些题目信息
65 | export function deleteQuestions(data) {
66 | return request({
67 | url: '/admin/question/deleteSome',
68 | method: 'delete',
69 | data
70 | })
71 | }
72 |
73 | // 更新题目信息
74 | export function updateQuestion(data) {
75 | return request({
76 | url: '/admin/question/updateOne',
77 | method: 'put',
78 | data
79 | })
80 | }
81 |
82 | // 保存题目
83 | export function saveQuestion(data) {
84 | return request({
85 | url: '/admin/question/saveQuestion',
86 | method: 'post',
87 | data
88 | })
89 | }
90 |
91 | // 移除题目
92 | export function removeQuestion(data) {
93 | return request({
94 | url: '/admin/question/removeQuestion',
95 | method: 'post',
96 | data
97 | })
98 | }
99 |
100 | // 移除一些题目
101 | export function removeQuestions(data) {
102 | return request({
103 | url: '/admin/question/removeQuestions',
104 | method: 'post',
105 | data
106 | })
107 | }
108 |
109 | // 获取随机题目
110 | export function getRandomQuestion(params) {
111 | return request({
112 | url: '/admin/question/getRandomQuestion',
113 | method: 'get',
114 | params
115 | })
116 | }
117 |
118 | export function exportExcel(data) {
119 | return request({
120 | url: '/admin/question/exportExcel',
121 | method: 'post',
122 | data
123 | })
124 | }
125 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
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/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, logout, getInfo } from '@/api/system/user'
2 | import { getToken, setToken, removeToken } from '@/utils/auth'
3 | import { resetRouter } from '@/router'
4 |
5 | const getDefaultState = () => {
6 | return {
7 | token: getToken(),
8 | id: 0,
9 | name: '',
10 | avatar: '',
11 | roles: []
12 | }
13 | }
14 |
15 | const state = getDefaultState()
16 |
17 | const mutations = {
18 | RESET_STATE: (state) => {
19 | Object.assign(state, getDefaultState())
20 | },
21 | SET_TOKEN: (state, token) => {
22 | state.token = token
23 | },
24 | SET_ID: (state, id) => {
25 | state.id = id
26 | },
27 | SET_NAME: (state, name) => {
28 | state.name = name
29 | },
30 | SET_AVATAR: (state, avatar) => {
31 | state.avatar = avatar
32 | },
33 | SET_ROLES: (state, roles) => {
34 | state.roles = roles
35 | }
36 | }
37 |
38 | const actions = {
39 | // 用户登录
40 | login({ commit }, userInfo) {
41 | const { username, password, code, key } = userInfo
42 | return new Promise((resolve, reject) => {
43 | login({ username: username.trim(), password: password, key: key, code: code }).then(response => {
44 | const { status, message, data } = response
45 | if (status !== 200) {
46 | reject(message)
47 | }
48 |
49 | commit('SET_TOKEN', data.token)
50 | setToken(data.token)
51 | resolve()
52 | }).catch(error => {
53 | reject(error)
54 | })
55 | })
56 | },
57 |
58 | // 获取用户信息
59 | getInfo({ commit, state }) {
60 | return new Promise((resolve, reject) => {
61 | getInfo(state.token).then(response => {
62 | const { data } = response
63 |
64 | if (!data) {
65 | reject('验证失败,请重新登录')
66 | }
67 |
68 | if (data.role === '管理员') data['roles'] = ['admin']
69 | else data['roles'] = ['student']
70 |
71 | const { id, roles, username, profile } = data
72 |
73 | // roles 必须是非空数组
74 | if (!roles || roles.length <= 0) {
75 | reject('getInfo: roles 必须是非空数组!')
76 | }
77 |
78 | commit('SET_ID', id)
79 | commit('SET_ROLES', roles)
80 | commit('SET_NAME', username)
81 | commit('SET_AVATAR', profile)
82 | resolve(data)
83 | }).catch(error => {
84 | reject(error)
85 | })
86 | })
87 | },
88 |
89 | // 用户退出
90 | logout({ commit, state }) {
91 | return new Promise((resolve, reject) => {
92 | logout(state.token).then(() => {
93 | removeToken() // 必须先移除token
94 | resetRouter()
95 | commit('RESET_STATE')
96 | resolve()
97 | }).catch(error => {
98 | reject(error)
99 | })
100 | })
101 | },
102 |
103 | // 重置token
104 | resetToken({ commit }) {
105 | return new Promise(resolve => {
106 | removeToken() // 必须先移除token
107 | commit('RESET_STATE')
108 | resolve()
109 | })
110 | }
111 | }
112 |
113 | export default {
114 | namespaced: true,
115 | state,
116 | mutations,
117 | actions
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/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 | null}
10 | */
11 | export function parseTime(time, cFormat) {
12 | if (arguments.length === 0 || !time) {
13 | return null
14 | }
15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16 | let date
17 | if (typeof time === 'object') {
18 | date = time
19 | } else {
20 | if ((typeof time === 'string')) {
21 | if ((/^[0-9]+$/.test(time))) {
22 | // support "1548221490638"
23 | time = parseInt(time)
24 | } else {
25 | // support safari
26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
27 | time = time.replace(new RegExp(/-/gm), '/')
28 | }
29 | }
30 |
31 | if ((typeof time === 'number') && (time.toString().length === 10)) {
32 | time = time * 1000
33 | }
34 | date = new Date(time)
35 | }
36 | const formatObj = {
37 | y: date.getFullYear(),
38 | m: date.getMonth() + 1,
39 | d: date.getDate(),
40 | h: date.getHours(),
41 | i: date.getMinutes(),
42 | s: date.getSeconds(),
43 | a: date.getDay()
44 | }
45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
46 | const value = formatObj[key]
47 | // Note: getDay() returns 0 on Sunday
48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
49 | return value.toString().padStart(2, '0')
50 | })
51 | return time_str
52 | }
53 |
54 | /**
55 | * @param {number} time
56 | * @param {string} option
57 | * @returns {string}
58 | */
59 | export function formatTime(time, option) {
60 | if (('' + time).length === 10) {
61 | time = parseInt(time) * 1000
62 | } else {
63 | time = +time
64 | }
65 | const d = new Date(time)
66 | const now = Date.now()
67 |
68 | const diff = (now - d) / 1000
69 |
70 | if (diff < 30) {
71 | return '刚刚'
72 | } else if (diff < 3600) {
73 | // less 1 hour
74 | return Math.ceil(diff / 60) + '分钟前'
75 | } else if (diff < 3600 * 24) {
76 | return Math.ceil(diff / 3600) + '小时前'
77 | } else if (diff < 3600 * 24 * 2) {
78 | return '1天前'
79 | }
80 | if (option) {
81 | return parseTime(time, option)
82 | } else {
83 | return (
84 | d.getMonth() +
85 | 1 +
86 | '月' +
87 | d.getDate() +
88 | '日' +
89 | d.getHours() +
90 | '时' +
91 | d.getMinutes() +
92 | '分'
93 | )
94 | }
95 | }
96 |
97 | /**
98 | * @param {string} url
99 | * @returns {Object}
100 | */
101 | export function param2Obj(url) {
102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
103 | if (!search) {
104 | return {}
105 | }
106 | const obj = {}
107 | const searchArr = search.split('&')
108 | searchArr.forEach(v => {
109 | const index = v.indexOf('=')
110 | if (index !== -1) {
111 | const name = v.substring(0, index)
112 | const val = v.substring(index + 1, v.length)
113 | obj[name] = val
114 | }
115 | })
116 | return obj
117 | }
118 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
29 |
30 |
31 |
58 |
59 |
137 |
--------------------------------------------------------------------------------
/src/views/exam-manager/repo/chapter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 返 回
7 | 添 加
14 | 保 存
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
155 |
156 |
158 |
--------------------------------------------------------------------------------
/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 |
11 | // If your port is set to 80,
12 | // use administrator privileges to execute the command line.
13 | // For example, Mac: sudo npm run
14 | // You can change the port by the following methods:
15 | // port = 9528 npm run dev OR npm run dev --port = 9528
16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port
17 |
18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/
19 | module.exports = {
20 | /**
21 | * You will need to set publicPath if you plan to deploy your site under a sub path,
22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
23 | * then publicPath should be set to "/bar/".
24 | * In most cases please use '/' !!!
25 | * Detail: https://cli.vuejs.org/config/#publicpath
26 | */
27 | publicPath: '/',
28 | outputDir: 'dist',
29 | assetsDir: 'static',
30 | lintOnSave: false,
31 | productionSourceMap: false,
32 | devServer: {
33 | port: port,
34 | open: true,
35 | overlay: {
36 | warnings: false,
37 | errors: true
38 | },
39 | before: require('./mock/mock-server.js')
40 | },
41 | configureWebpack: {
42 | // provide the app's title in webpack's name field, so that
43 | // it can be accessed in index.html to inject the correct title.
44 | name: name,
45 | resolve: {
46 | alias: {
47 | '@': resolve('src')
48 | }
49 | }
50 | },
51 | chainWebpack(config) {
52 | // it can improve the speed of the first screen, it is recommended to turn on preload
53 | config.plugin('preload').tap(() => [
54 | {
55 | rel: 'preload',
56 | // to ignore runtime.js
57 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
58 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
59 | include: 'initial'
60 | }
61 | ])
62 |
63 | // when there are many pages, it will cause too many meaningless requests
64 | config.plugins.delete('prefetch')
65 |
66 | // set svg-sprite-loader
67 | config.module
68 | .rule('svg')
69 | .exclude.add(resolve('src/icons'))
70 | .end()
71 | config.module
72 | .rule('icons')
73 | .test(/\.svg$/)
74 | .include.add(resolve('src/icons'))
75 | .end()
76 | .use('svg-sprite-loader')
77 | .loader('svg-sprite-loader')
78 | .options({
79 | symbolId: 'icon-[name]'
80 | })
81 | .end()
82 |
83 | // set preserveWhitespace
84 | config.module
85 | .rule('vue')
86 | .use('vue-loader')
87 | .loader('vue-loader')
88 | .tap(options => {
89 | options.compilerOptions.preserveWhitespace = true
90 | return options
91 | })
92 | .end()
93 |
94 | config
95 | .when(process.env.NODE_ENV !== 'development',
96 | config => {
97 | config
98 | .plugin('ScriptExtHtmlWebpackPlugin')
99 | .after('html')
100 | .use('script-ext-html-webpack-plugin', [{
101 | // `runtime` must same as runtimeChunk name. default is `runtime`
102 | inline: /runtime\..*\.js$/
103 | }])
104 | .end()
105 | config
106 | .optimization.splitChunks({
107 | chunks: 'all',
108 | cacheGroups: {
109 | libs: {
110 | name: 'chunk-libs',
111 | test: /[\\/]node_modules[\\/]/,
112 | priority: 10,
113 | chunks: 'initial' // only package third parties that are initially dependent
114 | },
115 | elementUI: {
116 | name: 'chunk-elementUI', // split elementUI into a single package
117 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
118 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
119 | },
120 | commons: {
121 | name: 'chunk-commons',
122 | test: resolve('src/components'), // can customize your rules
123 | minChunks: 3, // minimum common number
124 | priority: 5,
125 | reuseExistingChunk: true
126 | }
127 | }
128 | })
129 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
130 | config.optimization.runtimeChunk('single')
131 | }
132 | )
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/views/user-manager/person-info/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 修改信息
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
点击下方图片上传头像
26 |
27 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | 取消修改
47 | 保存修改
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
153 |
154 |
156 |
--------------------------------------------------------------------------------
/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .sub-el-icon {
61 | margin-right: 12px;
62 | margin-left: -2px;
63 | }
64 |
65 | .el-menu {
66 | border: none;
67 | height: 100%;
68 | width: 100% !important;
69 | }
70 |
71 | // menu hover
72 | .submenu-title-noDropdown,
73 | .el-submenu__title {
74 | &:hover {
75 | background-color: $menuHover !important;
76 | }
77 | }
78 |
79 | .is-active>.el-submenu__title {
80 | color: $subMenuActiveText !important;
81 | }
82 |
83 | & .nest-menu .el-submenu>.el-submenu__title,
84 | & .el-submenu .el-menu-item {
85 | min-width: $sideBarWidth !important;
86 | background-color: $subMenuBg !important;
87 |
88 | &:hover {
89 | background-color: $subMenuHover !important;
90 | }
91 | }
92 | }
93 |
94 | .hideSidebar {
95 | .sidebar-container {
96 | width: 54px !important;
97 | }
98 |
99 | .main-container {
100 | margin-left: 54px;
101 | }
102 |
103 | .submenu-title-noDropdown {
104 | padding: 0 !important;
105 | position: relative;
106 |
107 | .el-tooltip {
108 | padding: 0 !important;
109 |
110 | .svg-icon {
111 | margin-left: 20px;
112 | }
113 |
114 | .sub-el-icon {
115 | margin-left: 19px;
116 | }
117 | }
118 | }
119 |
120 | .el-submenu {
121 | overflow: hidden;
122 |
123 | &>.el-submenu__title {
124 | padding: 0 !important;
125 |
126 | .svg-icon {
127 | margin-left: 20px;
128 | }
129 |
130 | .sub-el-icon {
131 | margin-left: 19px;
132 | }
133 |
134 | .el-submenu__icon-arrow {
135 | display: none;
136 | }
137 | }
138 | }
139 |
140 | .el-menu--collapse {
141 | .el-submenu {
142 | &>.el-submenu__title {
143 | &>span {
144 | height: 0;
145 | width: 0;
146 | overflow: hidden;
147 | visibility: hidden;
148 | display: inline-block;
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
155 | .el-menu--collapse .el-menu .el-submenu {
156 | min-width: $sideBarWidth !important;
157 | }
158 |
159 | // mobile responsive
160 | .mobile {
161 | .main-container {
162 | margin-left: 0px;
163 | }
164 |
165 | .sidebar-container {
166 | transition: transform .28s;
167 | width: $sideBarWidth !important;
168 | }
169 |
170 | &.hideSidebar {
171 | .sidebar-container {
172 | pointer-events: none;
173 | transition-duration: 0.3s;
174 | transform: translate3d(-$sideBarWidth, 0, 0);
175 | }
176 | }
177 | }
178 |
179 | .withoutAnimation {
180 |
181 | .main-container,
182 | .sidebar-container {
183 | transition: none;
184 | }
185 | }
186 | }
187 |
188 | // when menu collapsed
189 | .el-menu--vertical {
190 | &>.el-menu {
191 | .svg-icon {
192 | margin-right: 16px;
193 | }
194 | .sub-el-icon {
195 | margin-right: 12px;
196 | margin-left: -2px;
197 | }
198 | }
199 |
200 | .nest-menu .el-submenu>.el-submenu__title,
201 | .el-menu-item {
202 | &:hover {
203 | // you can use $subMenuHover
204 | background-color: $menuHover !important;
205 | }
206 | }
207 |
208 | // the scroll bar appears when the subMenu is too long
209 | >.el-menu--popup {
210 | max-height: 100vh;
211 | overflow-y: auto;
212 |
213 | &::-webkit-scrollbar-track-piece {
214 | background: #d3dce6;
215 | }
216 |
217 | &::-webkit-scrollbar {
218 | width: 6px;
219 | }
220 |
221 | &::-webkit-scrollbar-thumb {
222 | background: #99a9bf;
223 | border-radius: 20px;
224 | }
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
哎哟喂!
12 |
{{ message }}
13 |
请检查您输入的网址是否正确,或按下面的按钮返回主页。
14 |
回到主页
15 |
16 |
17 |
18 |
19 |
20 |
31 |
32 |
226 |
--------------------------------------------------------------------------------
/.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/online-exam/exam-grade/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
25 | 搜索
26 |
27 |
34 | 显示全部
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
51 |
52 | {{
53 | scope.row.examName
54 | }}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | 是
67 | 否
68 |
69 |
70 |
71 |
72 |
73 | 查看试卷详细情况
78 |
79 |
80 |
81 |
82 |
83 |
94 |
95 |
96 |
97 |
218 |
219 |
221 |
--------------------------------------------------------------------------------
/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'/'el-icon-x' the icon show in the sidebar
23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
25 | }
26 | */
27 |
28 | /**
29 | * constantRoutes
30 | * a base page that does not have permission requirements
31 | * all roles can be accessed
32 | */
33 | export const constantRoutes = [
34 | {
35 | path: '/login',
36 | component: () => import('@/views/login/index'),
37 | hidden: true
38 | },
39 |
40 | {
41 | path: '/register',
42 | component: () => import('@/views/login/register'),
43 | hidden: true
44 | },
45 |
46 | {
47 | path: '/404',
48 | component: () => import('@/views/404'),
49 | hidden: true
50 | },
51 |
52 | {
53 | path: '/',
54 | component: Layout,
55 | redirect: '/dashboard',
56 | children: [{
57 | path: 'dashboard',
58 | name: 'Dashboard',
59 | component: () => import('@/views/dashboard/index'),
60 | meta: { title: '首页', icon: 'dashboard' }
61 | }]
62 | }
63 | ]
64 |
65 | /**
66 | * asyncRoutes
67 | * the routes that need to be dynamically loaded based on user roles
68 | */
69 | export const asyncRoutes = [
70 | // 用户管理
71 | {
72 | path: '/user-manager',
73 | component: Layout,
74 | redirect: '/user-manager/user',
75 | name: 'UserManager',
76 | meta: {
77 | title: '用户管理',
78 | icon: 'nested',
79 | roles: ['admin', 'student']
80 | },
81 | children: [
82 | {
83 | path: 'persion-info',
84 | name: 'persionInfo',
85 | component: () => import('@/views/user-manager/person-info/index'),
86 | meta: {
87 | title: '个人信息',
88 | icon: 'form',
89 | roles: ['admin', 'student'],
90 | noCache: true
91 | }
92 | },
93 | {
94 | path: 'user',
95 | name: 'User',
96 | component: () => import('@/views/user-manager/user/list'),
97 | meta: {
98 | title: '用户管理',
99 | icon: 'form',
100 | roles: ['admin'],
101 | noCache: true
102 | }
103 | }
104 | ]
105 | },
106 | // 考试管理
107 | {
108 | path: '/exam-manager',
109 | component: Layout,
110 | redirect: '/exam-manager/repo',
111 | name: 'ExamManager',
112 | meta: {
113 | title: '考试管理',
114 | icon: 'nested',
115 | roles: ['admin']
116 | },
117 | children: [
118 | {
119 | path: 'repo',
120 | name: 'Repo',
121 | component: () => import('@/views/exam-manager/repo/list'), // Parent router-view
122 | meta: {
123 | title: '题库管理',
124 | icon: 'form',
125 | roles: ['admin'],
126 | noCache: true
127 | }
128 | },
129 | {
130 | path: 'question',
131 | name: 'Question',
132 | component: () => import('@/views/exam-manager/question/list'), // Parent router-view
133 | meta: {
134 | title: '题目管理',
135 | icon: 'form',
136 | roles: ['admin'],
137 | noCache: true
138 | }
139 | },
140 | {
141 | path: 'paper',
142 | name: 'Paper',
143 | component: () => import('@/views/exam-manager/paper/list'), // Parent router-view
144 | meta: {
145 | title: '试卷管理',
146 | icon: 'form',
147 | roles: ['admin'],
148 | noCache: true
149 | }
150 | },
151 | {
152 | path: 'paperForm',
153 | name: 'PaperForm',
154 | component: () => import('@/views/exam-manager/paper/paperForm'), // Parent router-view
155 | meta: {
156 | title: '试卷表单',
157 | roles: ['admin'],
158 | noCache: true
159 | },
160 | hidden: true
161 | },
162 | {
163 | path: 'exam',
164 | name: 'Exam',
165 | component: () => import('@/views/exam-manager/exam/list'), // Parent router-view
166 | meta: {
167 | title: '考试管理',
168 | icon: 'form',
169 | roles: ['admin'],
170 | noCache: true
171 | }
172 | }
173 | ]
174 | },
175 | // 在线考试
176 | {
177 | path: '/online-exam',
178 | component: Layout,
179 | redirect: '/online-exam/exam',
180 | name: 'OnlineExamDir',
181 | meta: {
182 | title: '在线考试',
183 | icon: 'nested',
184 | roles: ['admin', 'student']
185 | },
186 | children: [
187 | {
188 | path: 'exam-list',
189 | name: 'ExamList',
190 | component: () => import('@/views/online-exam/online-exam/list'), // Parent router-view
191 | meta: {
192 | title: '考试列表',
193 | icon: 'form',
194 | roles: ['admin', 'student'],
195 | noCache: true
196 | }
197 | },
198 | {
199 | path: 'exam-record',
200 | name: 'ExamRecord',
201 | component: () => import('@/views/online-exam/exam-grade/record'), // Parent router-view
202 | meta: {
203 | title: '考试记录',
204 | icon: 'form',
205 | roles: ['admin', 'student'],
206 | noCache: true
207 | }
208 | },
209 | // 考试信息
210 | {
211 | path: 'info',
212 | name: 'ExamInfo',
213 | component: () => import('@/views/online-exam/online-exam/info'),
214 | meta: {
215 | title: '考试信息',
216 | icon: 'form',
217 | roles: ['admin', 'student'],
218 | noCache: true
219 | },
220 | hidden: true
221 | },
222 | // 考试详细情况
223 | {
224 | path: 'detail',
225 | name: 'ExamDetail',
226 | component: () => import('@/views/online-exam/online-exam/detail'),
227 | meta: {
228 | title: '考试详情',
229 | icon: 'form',
230 | roles: ['admin', 'student'],
231 | noCache: true
232 | },
233 | hidden: true
234 | }
235 | ]
236 | },
237 | // 正在考试
238 | {
239 | path: 'online-exam/run',
240 | name: 'RunExam',
241 | component: () => import('@/views/online-exam/online-exam/run'),
242 | meta: {
243 | title: '开始考试',
244 | roles: ['admin', 'student'],
245 | noCache: true
246 | },
247 | hidden: true
248 | },
249 | // 404 page must be placed at the end !!!
250 | { path: '*', redirect: '/404', hidden: true }
251 | ]
252 |
253 | const createRouter = () => new Router({
254 | // mode: 'history', // require service support
255 | scrollBehavior: () => ({ y: 0 }),
256 | routes: constantRoutes
257 | })
258 |
259 | const router = createRouter()
260 |
261 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
262 | export function resetRouter() {
263 | const newRouter = createRouter()
264 | router.matcher = newRouter.matcher // reset router
265 | }
266 |
267 | export default router
268 |
--------------------------------------------------------------------------------
/src/views/exam-manager/paper/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
22 |
28 |
29 |
30 |
37 | 搜索
38 |
39 |
46 | 显示全部
47 |
48 |
55 | 添加试卷
56 |
57 |
64 | 批量删除
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {{
77 | scope.row.name
78 | }}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 修改
101 | 删除
106 |
107 |
108 |
109 |
110 |
111 |
122 |
123 |
124 |
125 |
267 |
268 |
274 |
--------------------------------------------------------------------------------
/src/views/login/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
在线考试系统-注册界面
14 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | 确认
91 | 返回登录
97 |
98 |
99 |
100 |
101 |
102 |
187 |
188 |
237 |
238 |
301 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
在线考试系统-登录界面
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 登录
89 | 注册
95 |
96 |
97 |
98 |
99 |
100 | 管理员 username: admin
101 | password: admin
102 |
103 |
104 | 学生 username: wangpeng
105 | password: 123456
106 |
107 |
108 |
109 |
110 |
111 |
112 |
199 |
200 |
268 |
269 |
332 |
--------------------------------------------------------------------------------
/src/views/online-exam/online-exam/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
19 |
20 |
27 | 搜索
28 |
29 |
36 | 显示全部
37 |
38 |
39 |
40 |
41 |
42 | 考试名称:{{ curExam.name }}
43 | 考试试卷:{{ curExam.paperName }}
44 | 考试时长:{{ curExam.duration }} 分钟
45 | 开始时间:{{ curExam.startTime }}
46 | 结束时间:{{ curExam.endTime }}
47 | 注意事项:{{ curExam.notice }}
48 |
49 | 输入密码:
50 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
68 |
69 | {{
70 | scope.row.name
71 | }}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | 不限制考试时间
82 | {{ scope.row.startTime }}
83 |
84 |
85 |
86 |
87 |
88 | 不限制考试时间
89 | {{ scope.row.endTime }}
90 |
91 |
92 |
93 |
94 |
95 | 完全开放
96 | 需要密码
97 |
98 |
99 |
100 |
101 |
102 | 开始考试
107 |
108 |
109 |
110 |
111 |
112 |
123 |
124 |
125 |
126 |
352 |
353 |
355 |
--------------------------------------------------------------------------------
/src/views/exam-manager/repo/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
21 | 搜索
22 |
23 |
30 | 显示全部
31 |
32 |
39 | 添加题库
40 |
41 |
48 | 批量删除
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 否
63 | 是
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
76 |
77 |
78 |
79 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {{
98 | scope.row.name
99 | }}
100 |
101 |
102 |
103 |
104 |
105 | 否
106 | 是
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | 章节管理
121 | 修改
126 | 删除
131 |
132 |
133 |
134 |
135 |
136 |
147 |
148 |
149 |
150 |
374 |
375 |
385 |
--------------------------------------------------------------------------------
/src/views/online-exam/exam-grade/record.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
44 | 搜索
45 |
46 |
53 | 显示全部
54 |
55 |
63 | 批量删除
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 | {{
84 | scope.row.examName
85 | }}
86 |
87 |
88 |
89 |
90 |
91 | 考试中
92 | 考试完成
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | 等待批改主观题
101 | 批改完成
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | 不及格
110 | 及格
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | {{ scope.row.finishTime }}
119 | 等待交卷
120 |
121 |
122 |
123 |
124 | 批改试卷
131 | 查看试卷答题情况
137 | 删除
143 |
144 |
145 |
146 |
147 |
148 |
159 |
160 |
161 |
162 |
391 |
392 |
394 |
--------------------------------------------------------------------------------