Ver Fonte

搭建项目框架

dusenyao há 4 anos atrás
pai
commit
d683e74fb8

+ 0 - 1
.browserslistrc

@@ -1,3 +1,2 @@
 > 1%
 last 2 versions
-not dead

+ 5 - 0
.env.development

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'development'
+
+# base api
+VUE_APP_BASE_API = '/dev-api'

+ 5 - 0
.env.production

@@ -0,0 +1,5 @@
+# just a flag
+ENV = 'production'
+
+# base api
+VUE_APP_BASE_API = '/prod-api'

+ 4 - 0
.env.staging

@@ -0,0 +1,4 @@
+NODE_ENV = production
+
+# just a flag
+ENV = 'staging'

+ 4 - 0
.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 261 - 12
.eslintrc.js

@@ -1,25 +1,274 @@
 module.exports = {
   root: true,
+
   env: {
     node: true,
+    browser: true,
+    es6: true
   },
-  extends: ["plugin:vue/essential", "eslint:recommended", "@vue/prettier"],
+
+  extends: ['plugin:vue/strongly-recommended', 'eslint:recommended', '@vue/prettier'],
+
   parserOptions: {
-    parser: "babel-eslint",
+    parser: 'babel-eslint'
   },
+
   rules: {
-    "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
-    "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'prettier/prettier': [
+      'error',
+      {
+        tabWidth: 2,
+        useTabs: false,
+        semi: true,
+        singleQuote: true,
+        trailingComma: 'none',
+        endOfLine: 'auto',
+        bracketSpacing: true,
+        arrowParens: 'avoid',
+        printWidth: 100
+      }
+    ],
+    'no-alert': 0,
+    'no-array-constructor': 2,
+    'no-bitwise': 0,
+    'no-caller': 1,
+    'no-catch-shadow': 2,
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-constant-condition': 2,
+    'no-continue': 0,
+    'no-control-regex': 2,
+    'no-delete-var': 2,
+    'no-div-regex': 1,
+    'no-dupe-keys': 2,
+    'no-dupe-args': 2,
+    'no-duplicate-case': 2,
+    'no-else-return': 2,
+    'no-empty': 2,
+    'no-empty-character-class': 2,
+    'no-empty-label': 0,
+    'no-eq-null': 2,
+    'no-eval': 1,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': 2,
+    'no-extra-semi': 2,
+    'no-fallthrough': 1,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implicit-coercion': 1,
+    'no-implied-eval': 2,
+    'no-inline-comments': 0,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-invalid-this': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': 2,
+    'no-lone-blocks': 2,
+    'no-lonely-if': 2,
+    'no-loop-func': 1,
+    'no-mixed-requires': [0, false],
+    'no-mixed-spaces-and-tabs': [2, false],
+    'linebreak-style': [0, 'windows'],
+    'no-multi-spaces': 1,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [
+      1,
+      {
+        max: 2
+      }
+    ],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-nested-ternary': 0,
+    'no-new': 1,
+    'no-new-func': 1,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-param-reassign': 2,
+    'no-path-concat': 0,
+    'no-plusplus': 0,
+    'no-process-env': 0,
+    'no-process-exit': 0,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-restricted-modules': 0,
+    'no-return-assign': 1,
+    'no-script-url': 0,
+    'no-self-compare': 2,
+    'no-sequences': 0,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-sync': 0,
+    'no-ternary': 0,
+    'no-trailing-spaces': 1,
+    'no-this-before-super': 0,
+    'no-throw-literal': 2,
+    'no-undef': 1,
+    'no-undef-init': 2,
+    'no-undefined': 2,
+    'no-unexpected-multiline': 2,
+    'no-underscore-dangle': 1,
+    'no-unneeded-ternary': 2,
+    'no-unreachable': 2,
+    'no-unused-vars': [
+      2,
+      {
+        vars: 'all',
+        args: 'after-used'
+      }
+    ],
+    'no-use-before-define': 2,
+    'no-useless-call': 2,
+    'no-void': 2,
+    'no-var': 0,
+    'no-warning-comments': [
+      1,
+      {
+        terms: ['todo', 'fixme', 'xxx'],
+        location: 'start'
+      }
+    ],
+    'no-with': 2,
+    'array-bracket-spacing': [2, 'never'],
+    'arrow-parens': 0,
+    'arrow-spacing': 0,
+    'accessor-pairs': 0,
+    'block-scoped-var': 0,
+    'brace-style': [1, '1tbs'],
+    camelcase: 2,
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': 0,
+    'comma-style': [2, 'last'],
+    complexity: [0, 11],
+    'computed-property-spacing': [0, 'never'],
+    'consistent-return': 0,
+    'consistent-this': [2, 'that'],
+    'constructor-super': 0,
+    curly: [2, 'all'],
+    'default-case': 0,
+    'dot-location': 0,
+    'dot-notation': [
+      0,
+      {
+        allowKeywords: true
+      }
+    ],
+    'eol-last': 0,
+    eqeqeq: 2,
+    'func-names': 0,
+    'func-style': [0, 'declaration'],
+    'generator-star-spacing': 0,
+    'guard-for-in': 0,
+    'handle-callback-err': 0,
+    'id-length': 0,
+    indent: [2, 2],
+    'init-declarations': 0,
+    'key-spacing': [
+      0,
+      {
+        beforeColon: false,
+        afterColon: true
+      }
+    ],
+    'lines-around-comment': 0,
+    'max-depth': [0, 4],
+    'max-len': [0, 100, 4],
+    'max-nested-callbacks': [0, 2],
+    'max-params': [0, 3],
+    'max-statements': [0, 10],
+    'jsx-quotes': [2, 'prefer-single'],
+    'keyword-spacing': [
+      2,
+      {
+        before: true,
+        after: true
+      }
+    ],
+    'new-cap': [
+      2,
+      {
+        newIsCap: true,
+        capIsNew: false
+      }
+    ],
+    'new-parens': 2,
+    'object-curly-spacing': [0, 'never'],
+    'object-shorthand': 0,
+    'one-var': 0,
+    'operator-assignment': [0, 'always'],
+    'operator-linebreak': [
+      2,
+      'after',
+      {
+        overrides: {
+          '?': 'before',
+          ':': 'before'
+        }
+      }
+    ],
+    'padded-blocks': 0,
+    'prefer-const': 0,
+    'prefer-spread': 0,
+    'prefer-reflect': 0,
+    quotes: [1, 'single'],
+    'quote-props': [0, 'always'],
+    radix: 2,
+    'id-match': 0,
+    'require-yield': 0,
+    semi: [0, 'always'],
+    'semi-spacing': [
+      0,
+      {
+        before: false,
+        after: true
+      }
+    ],
+    'sort-vars': 0,
+    'space-after-keywords': [0, 'always'],
+    'space-before-blocks': [0, 'always'],
+    'space-before-function-paren': [0, 'always'],
+    'space-in-parens': [0, 'never'],
+    'space-infix-ops': 0,
+    'space-return-throw-case': 0,
+    'space-unary-ops': [
+      0,
+      {
+        words: true,
+        nonwords: false
+      }
+    ],
+    'spaced-comment': 0,
+    strict: 2,
+    'use-isnan': 2,
+    'valid-jsdoc': 0,
+    'valid-typeof': 2,
+    'vars-on-top': 2,
+    'wrap-iife': [2, 'inside'],
+    'wrap-regex': 0,
+    yoda: [2, 'never']
   },
+
+  // jest 测试配置
   overrides: [
     {
-      files: [
-        "**/__tests__/*.{j,t}s?(x)",
-        "**/tests/unit/**/*.spec.{j,t}s?(x)",
-      ],
+      files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
       env: {
-        jest: true,
-      },
-    },
-  ],
+        jest: true
+      }
+    }
+  ]
 };

+ 0 - 3
.gitignore

@@ -12,9 +12,6 @@ yarn-debug.log*
 yarn-error.log*
 pnpm-debug.log*
 
-# 本地配置文件
-.vscode/launch.json
-
 # 本地缓存文件
 .stylelintcache
 tests/**/coverage/

+ 6 - 0
.stylelintignore

@@ -0,0 +1,6 @@
+public/**
+src/assets/**
+node_modules/*
+
+tests
+dist

+ 29 - 0
.vscode/launch.json

@@ -0,0 +1,29 @@
+{
+  // 使用 IntelliSense 了解相关属性。
+  // 悬停以查看现有属性的描述。
+  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "type": "chrome",
+      "request": "launch",
+      "name": "vuejs: chrome",
+      "url": "http://localhost:9217",
+      "webRoot": "${workspaceFolder}/src",
+      "breakOnLoad": true,
+      "sourceMapPathOverrides": {
+        // 对应浏览器sources下 webpack:/// 的 .目录 和 src目录
+        "webpack:///src/*": "${webRoot}/*",
+        "webpack:///./src/*.js": "${webRoot}/*.js"
+      }
+    },
+    {
+      "type": "firefox",
+      "request": "launch",
+      "name": "vuejs: firefox",
+      "url": "http://localhost:9217",
+      "webRoot": "${workspaceFolder}/src",
+      "pathMappings": [{ "url": "webpack:///src/", "path": "${webRoot}/" }]
+    }
+  ]
+}

+ 7 - 1
babel.config.js

@@ -1,3 +1,9 @@
 module.exports = {
-  presets: ["@vue/cli-plugin-babel/preset"],
+  presets: ['@vue/cli-plugin-babel/preset'],
+  env: {
+    development: {
+      // 解决Vue热加载编译速度慢问题
+      plugins: ['dynamic-import-node']
+    }
+  }
 };

+ 26 - 1
jest.config.js

@@ -1,3 +1,28 @@
 module.exports = {
-  preset: "@vue/cli-plugin-unit-jest",
+  preset: '@vue/cli-plugin-unit-jest',
+  // 是否收集测试时的覆盖率信息。 由于要带上覆盖率搜集语句重新访问所有执行过的文件,这可能会让测试执行速度被明显减慢。默认值 false
+  // collectCoverage: true,
+  // 测试哪里的文件
+  collectCoverageFrom: ['src/components/**/*.{js,vue}', 'src/utils/**/*.{js,vue}'],
+  // 告诉jest需要解析的文件
+  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
+  // 告诉jest针对不同类型的文件的文件如何转义
+  transform: {
+    '^.+\\.vue$': 'vue-jest',
+    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
+    '^.+\\.jsx?$': 'babel-jest'
+  },
+  // 别名,同webpack中的alias
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1'
+  },
+  // 生成快照
+  snapshotSerializers: ['jest-serializer-vue'],
+  // 告诉jest去哪里找编写的测试文件
+  testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
+  // Jest输出覆盖信息文件的目录。
+  coverageDirectory: '<rootDir>/tests/unit/coverage',
+  // 列出包含reporter名字的列表,而Jest会用他们来生成覆盖报告。
+  coverageReporters: ['lcov', 'text-summary'],
+  testURL: 'http://localhost/'
 };

+ 9 - 0
jsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 685 - 119
package-lock.json


+ 48 - 21
package.json

@@ -1,37 +1,60 @@
 {
   "name": "gcls_sys_learn_web",
   "version": "0.1.0",
-  "private": true,
+  "description": "全球中文学习系统 教学系统 Web端",
   "scripts": {
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
-    "test:unit": "vue-cli-service test:unit",
-    "lint": "vue-cli-service lint"
+    "build:stage": "vue-cli-service build --mode staging",
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
+    "test:unit": "jest --clearCache && vue-cli-service test:unit",
+    "lint": "eslint --ext .js,.vue src"
   },
   "dependencies": {
-    "core-js": "^3.6.5",
-    "vue": "^2.6.11",
-    "vue-router": "^3.2.0",
-    "vuex": "^3.4.0"
+    "axios": "^0.21.1",
+    "core-js": "^3.10.1",
+    "element-ui": "^2.15.1",
+    "js-cookie": "^2.2.1",
+    "normalize.css": "^8.0.1",
+    "nprogress": "^0.2.0",
+    "vue": "^2.6.12",
+    "vue-router": "^3.5.1",
+    "vuex": "^3.6.2"
   },
   "devDependencies": {
-    "@vue/cli-plugin-babel": "~4.5.0",
-    "@vue/cli-plugin-eslint": "~4.5.0",
-    "@vue/cli-plugin-router": "~4.5.0",
-    "@vue/cli-plugin-unit-jest": "~4.5.0",
-    "@vue/cli-plugin-vuex": "~4.5.0",
-    "@vue/cli-service": "~4.5.0",
+    "@babel/core": "^7.13.15",
+    "@babel/preset-env": "^7.13.15",
+    "@vue/cli-plugin-babel": "~4.5.12",
+    "@vue/cli-plugin-eslint": "~4.5.12",
+    "@vue/cli-plugin-router": "~4.5.12",
+    "@vue/cli-plugin-unit-jest": "^4.5.12",
+    "@vue/cli-plugin-vuex": "~4.5.12",
+    "@vue/cli-service": "~4.5.12",
     "@vue/eslint-config-prettier": "^6.0.0",
-    "@vue/test-utils": "^1.0.3",
+    "@vue/test-utils": "^1.1.4",
     "babel-eslint": "^10.1.0",
-    "eslint": "^6.7.2",
+    "babel-jest": "^26.6.3",
+    "babel-loader": "^8.2.2",
+    "babel-plugin-dynamic-import-node": "^2.3.3",
+    "eslint": "^7.24.0",
     "eslint-plugin-prettier": "^3.3.1",
-    "eslint-plugin-vue": "^6.2.2",
-    "lint-staged": "^9.5.0",
-    "node-sass": "^4.12.0",
-    "prettier": "^2.2.1",
-    "sass-loader": "^8.0.2",
-    "vue-template-compiler": "^2.6.11"
+    "eslint-plugin-vue": "^7.8.0",
+    "html-webpack-plugin": "^5.3.1",
+    "node-sass": "^5.0.0",
+    "prettier": "2.2.1",
+    "sass": "^1.32.8",
+    "sass-loader": "^10.1.1",
+    "script-ext-html-webpack-plugin": "^2.1.5",
+    "stylelint": "^13.12.0",
+    "stylelint-config-standard": "^21.0.0",
+    "stylelint-declaration-block-no-ignored-properties": "^2.3.0",
+    "stylelint-order": "^4.1.0",
+    "stylelint-scss": "^3.19.0",
+    "stylelint-webpack-plugin": "^2.1.1",
+    "svg-sprite-loader": "^6.0.5",
+    "svgo": "^2.3.0",
+    "vue-loader": "^15.9.6",
+    "vue-template-compiler": "^2.6.12"
   },
   "gitHooks": {
     "pre-commit": "lint-staged"
@@ -41,5 +64,9 @@
       "vue-cli-service lint",
       "git add"
     ]
+  },
+  "engines": {
+    "node": ">=8.9",
+    "npm": ">= 3.0.0"
   }
 }

+ 5 - 26
src/App.vue

@@ -1,32 +1,11 @@
 <template>
   <div id="app">
-    <div id="nav">
-      <router-link to="/">Home</router-link> |
-      <router-link to="/about">About</router-link>
-    </div>
     <router-view />
   </div>
 </template>
 
-<style lang="scss">
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-}
-
-#nav {
-  padding: 30px;
-
-  a {
-    font-weight: bold;
-    color: #2c3e50;
-
-    &.router-link-exact-active {
-      color: #42b983;
-    }
-  }
-}
-</style>
+<script>
+export default {
+  name: 'App'
+};
+</script>

+ 0 - 0
src/api/table.js


+ 66 - 0
src/common/SvgIcon/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <div
+    v-if="isExternal"
+    :style="styleExternalIcon"
+    class="svg-external-icon svg-icon"
+    v-on="$listeners"
+  />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
+  </svg>
+</template>
+
+<script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate';
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    isExternal() {
+      return isExternal(this.iconClass);
+    },
+    iconName() {
+      return `#icon-${this.iconClass}`;
+    },
+    svgClass() {
+      if (this.className) {
+        return 'svg-icon ' + this.className;
+      }
+      return 'svg-icon';
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      };
+    }
+  }
+};
+</script>
+
+<style scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover !important;
+  display: inline-block;
+}
+</style>

+ 0 - 138
src/components/HelloWorld.vue

@@ -1,138 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br />
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
-        >vue-cli documentation</a
-      >.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
-          target="_blank"
-          rel="noopener"
-          >babel</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
-          target="_blank"
-          rel="noopener"
-          >router</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
-          target="_blank"
-          rel="noopener"
-          >vuex</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
-          target="_blank"
-          rel="noopener"
-          >eslint</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest"
-          target="_blank"
-          rel="noopener"
-          >unit-jest</a
-        >
-      </li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li>
-        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
-      </li>
-      <li>
-        <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
-          >Forum</a
-        >
-      </li>
-      <li>
-        <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
-          >Community Chat</a
-        >
-      </li>
-      <li>
-        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
-          >Twitter</a
-        >
-      </li>
-      <li>
-        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
-      </li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li>
-        <a href="https://router.vuejs.org" target="_blank" rel="noopener"
-          >vue-router</a
-        >
-      </li>
-      <li>
-        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/vue-devtools#vue-devtools"
-          target="_blank"
-          rel="noopener"
-          >vue-devtools</a
-        >
-      </li>
-      <li>
-        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
-          >vue-loader</a
-        >
-      </li>
-      <li>
-        <a
-          href="https://github.com/vuejs/awesome-vue"
-          target="_blank"
-          rel="noopener"
-          >awesome-vue</a
-        >
-      </li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: "HelloWorld",
-  props: {
-    msg: String,
-  },
-};
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped lang="scss">
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 9 - 0
src/icons/index.js

@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import SvgIcon from '@/common/SvgIcon'; // svg component
+
+// register globally
+Vue.component('SvgIcon', SvgIcon);
+
+const req = require.context('./svg', false, /\.svg$/);
+const requireAll = requireContext => requireContext.keys().map(requireContext);
+requireAll(req);

+ 22 - 0
src/icons/svgo.yml

@@ -0,0 +1,22 @@
+# replace default config
+
+# multipass: true
+# full: true
+
+plugins:
+
+  # - name
+  #
+  # or:
+  # - name: false
+  # - name: true
+  #
+  # or:
+  # - name:
+  #     param1: 1
+  #     param2: 2
+
+- removeAttrs:
+    attrs:
+      - 'fill'
+      - 'fill-rule'

+ 20 - 0
src/layouts/components/Header.vue

@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <el-header>
+      <span></span>
+    </el-header>
+  </div>
+</template>
+
+<script>
+export default {};
+</script>
+
+<style lang="scss" scoped>
+$header-h: 49px;
+
+.el-header {
+  height: $header-h;
+  line-height: $header-h;
+}
+</style>

+ 46 - 0
src/layouts/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <div>
+    <Header />
+    <section class="app-main">
+      <transition name="fade-transfrom" mode="out-in">
+        <router-view :key="key"></router-view>
+      </transition>
+    </section>
+  </div>
+</template>
+
+<script>
+import Header from './components/Header';
+export default {
+  name: 'Layout',
+  components: {
+    Header
+  },
+  computed: {
+    key() {
+      console.log(this.$route.path);
+      return this.$route.path;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.app-main {
+  /* 50 = navbar  */
+  min-height: calc(100vh - 50px);
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+  padding-top: 50px;
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
+</style>

+ 14 - 6
src/main.js

@@ -1,12 +1,20 @@
-import Vue from "vue";
-import App from "./App.vue";
-import router from "./router";
-import store from "./store";
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+import '@/styles/index.scss'; // global css
+import 'normalize.css/normalize.css';
+
+Vue.use(ElementUI);
 
 Vue.config.productionTip = false;
 
 new Vue({
   router,
   store,
-  render: (h) => h(App),
-}).$mount("#app");
+  render: h => h(App)
+}).$mount('#app');

+ 56 - 15
src/router/index.js

@@ -1,28 +1,69 @@
-import Vue from "vue";
-import VueRouter from "vue-router";
-import Home from "../views/Home.vue";
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+import Layout from '@/layouts';
+import Login from '@/views/login';
+
+import NProgress from 'nprogress'; // progress bar
+import 'nprogress/nprogress.css';
+
+NProgress.configure({ showSpinner: false });
 
 Vue.use(VueRouter);
 
 const routes = [
   {
-    path: "/",
-    name: "Home",
-    component: Home,
+    path: '/login',
+    component: Login
   },
   {
-    path: "/about",
-    name: "About",
-    // route level code-splitting
-    // this generates a separate chunk (about.[hash].js) for this route
-    // which is lazy-loaded when the route is visited.
-    component: () =>
-      import(/* webpackChunkName: "about" */ "../views/About.vue"),
+    path: '/404',
+    component: () => import('@/views/404')
   },
+  {
+    path: '/',
+    component: Layout,
+    redirect: '/taskkanban',
+    children: [
+      {
+        path: '/taskkanban',
+        name: 'TaskKanban',
+        component: () => import('@/views/teacher/TaskKanban')
+      }
+    ]
+  },
+  {
+    path: '*',
+    redirect: '/404',
+    component: () => import('@/views/404')
+  }
 ];
 
-const router = new VueRouter({
-  routes,
+const createRouter = () =>
+  new VueRouter({
+    // mode: 'history',
+    scrollBehavior: () => ({ y: 0 }),
+    routes
+  });
+
+const router = createRouter();
+
+// 全局前置守卫
+router.beforeEach(async (to, from, next) => {
+  NProgress.start();
+  next();
+});
+
+// 全局后置钩子
+router.afterEach(() => {
+  NProgress.done();
 });
 
+// 重置路由
+export function resetRouter() {
+  const newRouter = createRouter();
+
+  router.matcher = newRouter.matcher; // reset router
+}
+
 export default router;

+ 3 - 3
src/store/index.js

@@ -1,5 +1,5 @@
-import Vue from "vue";
-import Vuex from "vuex";
+import Vue from 'vue';
+import Vuex from 'vuex';
 
 Vue.use(Vuex);
 
@@ -7,5 +7,5 @@ export default new Vuex.Store({
   state: {},
   mutations: {},
   actions: {},
-  modules: {},
+  modules: {}
 });

+ 47 - 0
src/styles/index.scss

@@ -0,0 +1,47 @@
+@import './variables.scss';
+
+body {
+  height: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: optimizeLegibility;
+  /* stylelint-disable-next-line */
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
+    Microsoft YaHei, Arial, sans-serif; /* stylelint-disable-line */
+}
+
+label {
+  font-weight: 700;
+}
+
+html {
+  height: 100%;
+  box-sizing: border-box;
+}
+
+#app {
+  height: 100%;
+}
+
+a:focus,
+a:active {
+  outline: none;
+}
+
+/* stylelint-disable-next-line no-descending-specificity */
+a,
+a:focus,
+a:hover {
+  cursor: pointer;
+  color: inherit;
+  text-decoration: none;
+}
+
+div:focus {
+  outline: none;
+}
+
+// main-container global css
+.app-container {
+  padding: 20px;
+}

+ 1 - 0
src/styles/variables.scss

@@ -0,0 +1 @@
+$bacColor: rgb(229, 233, 242);

+ 18 - 0
src/views/404.vue

@@ -0,0 +1,18 @@
+<template>
+  <div class="http-404">
+    <h1>404</h1>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Page404'
+};
+</script>
+
+<style lang="scss">
+.http-404 {
+  color: rgb(0, 174, 255);
+  text-align: center;
+}
+</style>

+ 0 - 5
src/views/About.vue

@@ -1,5 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>

+ 0 - 18
src/views/Home.vue

@@ -1,18 +0,0 @@
-<template>
-  <div class="home">
-    <img alt="Vue logo" src="../assets/logo.png" />
-    <HelloWorld msg="Welcome to Your Vue.js App" />
-  </div>
-</template>
-
-<script>
-// @ is an alias to /src
-import HelloWorld from "@/components/HelloWorld.vue";
-
-export default {
-  name: "Home",
-  components: {
-    HelloWorld,
-  },
-};
-</script>

+ 81 - 0
src/views/login/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="login-container">
+    <el-form ref="loginForm" class="login-form" auto-complete="on" label-position="left">
+      <div class="title-container">
+        <h3 class="title">系统登录</h3>
+      </div>
+
+      <el-form-item>
+        <el-input placeholder="用户名" />
+      </el-form-item>
+      <el-form-item>
+        <el-input placeholder="密码" />
+      </el-form-item>
+      <el-row>
+        <el-col :span="12">
+          <el-button
+            class="login-button"
+            type="primary"
+            @click.native.prevent="handleLogin('teacher')"
+            >教师登录</el-button
+          >
+        </el-col>
+        <el-col :span="12">
+          <el-button
+            class="login-button"
+            type="primary"
+            @click.native.prevent="handleLogin('student')"
+            >学员登录</el-button
+          >
+        </el-col>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {};
+  },
+  methods: {
+    handleLogin(loginType) {
+      console.log(loginType);
+    }
+  }
+};
+</script>
+
+<style lang="scss">
+.login-container {
+  background-color: #f5f5f5;
+  min-height: 100%;
+  width: 100%;
+  overflow: hidden;
+
+  .title-container {
+    position: relative;
+
+    .title {
+      font-size: 26px;
+      color: #999;
+      margin: 0 auto 40px auto;
+      text-align: center;
+      font-weight: bold;
+    }
+  }
+
+  .login-form {
+    position: relative;
+    width: 520px;
+    max-width: 100%;
+    padding: 160px 35px 0;
+    margin: 0 auto;
+    overflow: hidden;
+  }
+
+  .login-button {
+    margin: 0 81px;
+  }
+}
+</style>

+ 13 - 0
src/views/teacher/TaskKanban.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    <h1>Task</h1>
+  </div>
+</template>
+
+<script>
+export default {};
+</script>
+
+<style lang="scss" scoped>
+// note
+</style>

+ 44 - 0
stylelint.config.js

@@ -0,0 +1,44 @@
+module.exports = {
+  defaultSeverity: 'error',
+  extends: 'stylelint-config-standard',
+  plugins: ['stylelint-declaration-block-no-ignored-properties', 'stylelint-order'],
+  rules: {
+    'plugin/declaration-block-no-ignored-properties': true,
+    // 各分类属性顺序
+    'order/order': ['custom-properties', 'dollar-variables', 'declarations', 'rules', 'at-rules'],
+    // 指定2个空格
+    indentation: 2,
+    // 多个选择器样式之间允许有空行
+    'rule-empty-line-before': null,
+    // 样式块中不允许重复的属性
+    'declaration-block-no-duplicate-properties': true,
+    // 指定颜色函数使用传统符号隔开
+    'color-function-notation': 'legacy',
+    // 函数 url 链接不允许 shceme relative
+    'function-url-no-scheme-relative': true,
+    // 可组合成一个属性的写法,不允许拆开书写
+    'declaration-block-no-redundant-longhand-properties': true,
+    // 选择器最大深度为4
+    'selector-max-compound-selectors': 4,
+    // 限制 id选择器的数目在一个选择器中
+    'selector-max-id': 1,
+    // 最多2个类型选择器
+    'selector-max-type': 2,
+    // 不允许使用通配符选择器
+    'selector-max-universal': 0,
+    // 不允许未知的动画
+    'no-unknown-animations': true,
+    // 在字体名称必须使用引号的地方使用引号,其他地方不能使用
+    'font-family-name-quotes': 'always-where-required',
+    // url 函数内部必须有引号
+    'function-url-quotes': 'always',
+    // 指定字符串引号为单引号
+    'string-quotes': 'single',
+    // 在规则的分号之前不允许有空格
+    'at-rule-semicolon-space-before': 'never',
+    // 首行不允许空行
+    'no-empty-first-line': true,
+    // 不允许使用 unicode 作为顺序标记
+    'unicode-bom': 'never'
+  }
+};

+ 126 - 0
vue.config.js

@@ -0,0 +1,126 @@
+const path = require('path');
+const StyleLintPlugin = require('stylelint-webpack-plugin');
+
+function resolve(dir) {
+  return path.join(__dirname, './', dir);
+}
+
+const name = '全球中文学习系统 教学系统 Web端';
+
+const NODE_ENV = process.env.NODE_ENV;
+
+const port = process.env.port || process.env.npm_config_port || 9217;
+
+// 配置项说明 https://cli.vuejs.org/config/
+module.exports = {
+  publicPath: NODE_ENV === 'development' ? '/' : './',
+  outputDir: 'dist',
+  assetsDir: 'static',
+  lintOnSave: false,
+  runtimeCompiler: true,
+  productionSourceMap: false,
+  devServer: {
+    port: port,
+    open: true, //默认false   true自动打开网页
+    overlay: {
+      warnings: false,
+      errors: true
+    }
+  },
+  css: {
+    loaderOptions: {
+      scss: {
+        // 为 scss 配置共享全局变量
+        additionalData: '@import "./src/styles/variables.scss";' //注意
+      }
+    }
+  },
+  configureWebpack: {
+    name: name,
+    // 配置路径别名
+    resolve: {
+      alias: {
+        '@': resolve('src')
+      }
+    },
+    devtool: NODE_ENV === 'development' ? 'source-map' : '',
+    plugins: [
+      new StyleLintPlugin({
+        files: ['**/*.{vue,htm,html,css,sss,less,scss,sass}'],
+        fix: false, // 是否自动修复
+        cache: true, // 是否缓存
+        emitErrors: true,
+        failOnError: false
+      })
+    ]
+  },
+  chainWebpack(config) {
+    // 启用预加载,提高首屏加载速度
+    config.plugin('preload').tap(() => [
+      {
+        rel: 'preload',
+        // 忽略 runtime.js
+        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
+        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
+        include: 'initial'
+      }
+    ]);
+
+    // 当页面很多时,它将导致太多无意义的请求
+    // config.plugins.delete('prefetch');
+
+    // 设置 svg-sprite-loader
+    config.module.rule('svg').exclude.add(resolve('src/icons')).end();
+    config.module
+      .rule('icons')
+      .test(/\.svg$/)
+      .include.add(resolve('src/icons'))
+      .end()
+      .use('svg-sprite-loader')
+      .loader('svg-sprite-loader')
+      .options({
+        symbolId: 'icon-[name]'
+      })
+      .end();
+
+    config.when(process.env.NODE_ENV !== 'development', () => {
+      config
+        .plugin('ScriptExtHtmlWebpackPlugin')
+        .after('html')
+        .use('script-ext-html-webpack-plugin', [
+          {
+            // `runtime`必须与runtimeChunk名称相同。默认是“运行时”
+            inline: /runtime\..*\.js$/
+          }
+        ])
+        .end();
+
+      config.optimization.splitChunks({
+        chunks: 'all',
+        cacheGroups: {
+          libs: {
+            name: 'chunk-libs',
+            test: /[\\/]node_modules[\\/]/,
+            priority: 10,
+            chunks: 'initial' // 仅打包最初依赖的第三方
+          },
+          elementUI: {
+            name: 'chunk-elementUI', // 将elementUI拆分为一个包
+            priority: 20, // 权重必须大于libs和app,否则将被打包到libs或app中
+            test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
+          },
+          commons: {
+            name: 'chunk-commons',
+            test: resolve('src/components'), // 可以自定义规则
+            minChunks: 3, // 最小公用数
+            priority: 5,
+            reuseExistingChunk: true
+          }
+        }
+      });
+
+      // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
+      config.optimization.runtimeChunk('single');
+    });
+  }
+};

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff