Przeglądaj źródła

项目基础框架

dusenyao 1 rok temu
rodzic
commit
b6cb7064a0
50 zmienionych plików z 2672 dodań i 657 usunięć
  1. 8 0
      .env
  2. 4 0
      .env.development
  3. 4 0
      .env.production
  4. 4 0
      .eslintignore
  5. 305 0
      .eslintrc.js
  6. 1 1
      .gitignore
  7. 1 0
      .npmrc
  8. 8 0
      .prettierrc.js
  9. 6 0
      .stylelintignore
  10. 10 5
      README.md
  11. 8 4
      babel.config.js
  12. 47 0
      forge.config.js
  13. 7 10
      jsconfig.json
  14. 50 0
      main.js
  15. 526 526
      package-lock.json
  16. 56 26
      package.json
  17. 2 19
      src/App.vue
  18. 112 0
      src/api/app.js
  19. 16 0
      src/api/user.js
  20. BIN
      src/assets/header/avatar-default.png
  21. BIN
      src/assets/header/exit.png
  22. 96 0
      src/common/SvgIcon/index.vue
  23. 0 58
      src/components/HelloWorld.vue
  24. 8 0
      src/icons/index.js
  25. 79 0
      src/layouts/default/breadcrumb/index.vue
  26. 172 0
      src/layouts/default/header/index.vue
  27. 38 0
      src/layouts/default/index.vue
  28. 25 5
      src/main.js
  29. 39 0
      src/router/guard/index.js
  30. 23 0
      src/router/index.js
  31. 26 0
      src/router/modules/basic.js
  32. 3 0
      src/router/modules/index.js
  33. 5 0
      src/store/getters.js
  34. 20 0
      src/store/index.js
  35. 53 0
      src/store/modules/app.js
  36. 83 0
      src/store/modules/user.js
  37. 13 0
      src/store/mutation-types.js
  38. 99 0
      src/styles/index.scss
  39. 22 0
      src/styles/variables.scss
  40. 51 0
      src/utils/auth.js
  41. 15 0
      src/utils/common.js
  42. 12 0
      src/utils/filter.js
  43. 134 0
      src/utils/http.js
  44. 48 0
      src/utils/validate.js
  45. 15 0
      src/views/home/index.vue
  46. 141 0
      src/views/login/index.vue
  47. 3 0
      src/views/not_found/404.vue
  48. 32 0
      src/views/userInfo.vue
  49. 86 0
      stylelint.config.js
  50. 156 3
      vue.config.js

+ 8 - 0
.env

@@ -0,0 +1,8 @@
+# FileServer
+VUE_APP_FileServer = '/GCLSFileServer/ServiceInterface'
+
+# LearnWebSI
+VUE_APP_LearnWebSI = '/GCLSLearnWebSI/ServiceInterface'
+
+# BookWebSI
+VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'

+ 4 - 0
.env.development

@@ -0,0 +1,4 @@
+# base api
+VUE_APP_BASE_API = '/api'
+
+VUE_APP_FILE = '/file'

+ 4 - 0
.env.production

@@ -0,0 +1,4 @@
+# base api
+VUE_APP_BASE_API = 'https://gcls.helxsoft.cn/'
+
+VUE_APP_FILE = 'https://file-kf.helxsoft.cn/'

+ 4 - 0
.eslintignore

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

+ 305 - 0
.eslintrc.js

@@ -0,0 +1,305 @@
+module.exports = {
+  root: true,
+
+  // 为什么是这样的parser配置?https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@babel/eslint-parser',
+    sourceType: 'module',
+    ecmaVersion: 7,
+    ecmaFeatures: {
+      impliedStrict: true,
+    },
+  },
+
+  globals: {},
+
+  env: {
+    node: true,
+    browser: true,
+  },
+
+  extends: [
+    'eslint:recommended',
+    'plugin:vue/recommended',
+    'plugin:prettier/recommended',
+    '@vue/eslint-config-prettier',
+  ],
+
+  plugins: [
+    // 注意这里不能配置 html 选项,为什么?https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
+    'vue',
+    'prettier',
+  ],
+
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'vue/multi-word-component-names': 0,
+    'vue/max-attributes-per-line': [
+      2,
+      {
+        singleline: 8,
+        multiline: {
+          max: 1,
+        },
+      },
+    ],
+    'vue/html-self-closing': [
+      1,
+      {
+        html: {
+          void: 'always',
+          normal: 'never',
+          component: 'always',
+        },
+        svg: 'always',
+        math: 'always',
+      },
+    ],
+    'accessor-pairs': 2,
+    'arrow-spacing': [
+      2,
+      {
+        before: true,
+        after: true,
+      },
+    ],
+    'block-spacing': [2, 'always'],
+    'brace-style': [
+      2,
+      '1tbs',
+      {
+        allowSingleLine: true,
+      },
+    ],
+    camelcase: [
+      0,
+      {
+        properties: 'always',
+      },
+    ],
+    'comma-dangle': [2, 'always-multiline'],
+    'comma-spacing': [
+      2,
+      {
+        before: false,
+        after: true,
+      },
+    ],
+    'comma-style': [2, 'last'],
+    curly: [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    eqeqeq: [2, 'always'],
+    'generator-star-spacing': [
+      2,
+      {
+        before: false,
+        after: true,
+      },
+    ],
+    indent: [
+      2,
+      2,
+      {
+        SwitchCase: 1,
+      },
+    ],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [
+      2,
+      {
+        beforeColon: false,
+        afterColon: true,
+      },
+    ],
+    'keyword-spacing': [
+      2,
+      {
+        before: true,
+        after: true,
+      },
+    ],
+    'new-cap': [
+      2,
+      {
+        newIsCap: true,
+        capIsNew: false,
+      },
+    ],
+    'no-caller': 2,
+    'no-eval': 2,
+    'no-labels': [
+      2,
+      {
+        allowLoop: false,
+        allowSwitch: false,
+      },
+    ],
+    'no-multi-spaces': 2,
+    'no-multiple-empty-lines': [
+      2,
+      {
+        max: 1,
+      },
+    ],
+    'no-return-assign': [2, 'except-parens'],
+    'no-sequences': 2,
+    'no-trailing-spaces': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [
+      2,
+      {
+        defaultAssignment: false,
+      },
+    ],
+    'no-unused-vars': [1],
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-whitespace-before-property': 2,
+    'one-var': [
+      2,
+      {
+        initialized: 'never',
+      },
+    ],
+    'operator-linebreak': [
+      2,
+      'after',
+      {
+        overrides: {
+          '?': 'before',
+          ':': 'before',
+        },
+      },
+    ],
+    'padded-blocks': [2, 'never'],
+    quotes: [
+      2,
+      'single',
+      {
+        avoidEscape: true,
+        allowTemplateLiterals: true,
+      },
+    ],
+    semi: [2, 'always'],
+    'semi-spacing': [
+      2,
+      {
+        before: false,
+        after: true,
+      },
+    ],
+    'space-before-blocks': [2, 'always'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [
+      2,
+      {
+        words: true,
+        nonwords: false,
+      },
+    ],
+    'spaced-comment': [
+      2,
+      'always',
+      {
+        markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','],
+      },
+    ],
+    'template-curly-spacing': [2, 'never'],
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    'object-curly-spacing': [2, 'always'],
+    'no-alert': process.env.NODE_ENV === 'production' ? 1 : 0,
+    'no-array-constructor': 2,
+    'no-bitwise': 1,
+    'no-div-regex': 1,
+    'no-else-return': 2,
+    'no-empty': 1,
+    'no-eq-null': 2,
+    'no-extend-native': 2,
+    'no-multi-assign': 1,
+    'no-negated-condition': 2,
+    'no-duplicate-imports': 2,
+    'no-extra-bind': 2,
+    'no-tabs': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-floating-decimal': 2,
+    'no-implicit-coercion': 1,
+    'no-implied-eval': 2,
+    'no-inline-comments': 0,
+    'no-invalid-this': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-lone-blocks': 2,
+    'no-lonely-if': 2,
+    'linebreak-style': [0, 'windows'],
+    'no-multi-str': 2,
+    'no-nested-ternary': 0,
+    'no-new': 1,
+    'no-new-func': 1,
+    'no-new-object': 2,
+    'no-new-wrappers': 2,
+    'no-octal-escape': 2,
+    'no-param-reassign': 2,
+    'no-plusplus': [1, { allowForLoopAfterthoughts: true }],
+    'no-proto': 2,
+    'no-self-compare': 2,
+    'func-call-spacing': 2,
+    'no-ternary': 0,
+    'no-throw-literal': 2,
+    'no-undef-init': 2,
+    'no-use-before-define': 2,
+    'no-useless-call': 2,
+    'no-void': 2,
+    'no-var': 2,
+    'prefer-rest-params': 2,
+    'prefer-template': 2,
+    'no-warning-comments': [
+      1,
+      {
+        terms: ['todo', 'fix', '!', '?'],
+        location: 'start',
+      },
+    ],
+    'array-bracket-spacing': [2, 'never'],
+    'computed-property-spacing': [1, 'never'],
+    'consistent-return': 0,
+    'default-case': 1,
+    'func-names': 1,
+    'func-style': 0,
+    'guard-for-in': 0,
+    'id-length': 0,
+    'init-declarations': 1,
+    'lines-around-comment': 0,
+    'max-depth': [1, 4],
+    'max-len': [1, { code: 120, ignoreUrls: true, ignoreTemplateLiterals: true, ignoreRegExpLiterals: true }],
+    'max-nested-callbacks': 1,
+    'max-params': [1, 6],
+    'max-statements': [1, 40],
+    'new-parens': 2,
+    'object-shorthand': 1,
+    'operator-assignment': 1,
+    'prefer-spread': 1,
+    'quote-props': [1, 'as-needed'],
+    radix: [1, 'as-needed'],
+    'id-match': 0,
+    'sort-vars': [1, { ignoreCase: true }],
+    strict: 2,
+    'vars-on-top': 2,
+    'wrap-regex': 0,
+    yoda: [2, 'never'],
+  },
+
+  // jest 测试配置
+  overrides: [
+    {
+      files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
+      env: {
+        jest: true,
+      },
+    },
+  ],
+};

+ 1 - 1
.gitignore

@@ -1,7 +1,7 @@
 .DS_Store
 node_modules
 /dist
-
+/out
 
 # local env files
 .env.local

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+engine-strict = false

+ 8 - 0
.prettierrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  tabWidth: 2,
+  printWidth: 120,
+  semi: true,
+  singleQuote: true,
+  endOfLine: 'auto',
+  arrowParens: 'always',
+};

+ 6 - 0
.stylelintignore

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

+ 10 - 5
README.md

@@ -1,24 +1,29 @@
-# electron-demo
+# gcls_page_textbook
 
 ## Project setup
-```
+
+```plain
 npm install
 ```
 
 ### Compiles and hot-reloads for development
-```
+
+```plain
 npm run serve
 ```
 
 ### Compiles and minifies for production
-```
+
+```plain
 npm run build
 ```
 
 ### Lints and fixes files
-```
+
+```plain
 npm run lint
 ```
 
 ### Customize configuration
+
 See [Configuration Reference](https://cli.vuejs.org/config/).

+ 8 - 4
babel.config.js

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

+ 47 - 0
forge.config.js

@@ -0,0 +1,47 @@
+const { FusesPlugin } = require('@electron-forge/plugin-fuses');
+const { FuseV1Options, FuseVersion } = require('@electron/fuses');
+
+module.exports = {
+  packagerConfig: {
+    asar: true,
+  },
+  rebuildConfig: {},
+  makers: [
+    {
+      name: '@electron-forge/maker-squirrel',
+      platforms: ['win32'],
+      config: {
+        name: 'electron-vue',
+      },
+    },
+    {
+      name: '@electron-forge/maker-zip',
+      platforms: ['darwin'],
+    },
+    // {
+    //   name: '@electron-forge/maker-deb',
+    //   config: {},
+    // },
+    // {
+    //   name: '@electron-forge/maker-rpm',
+    //   config: {},
+    // },
+  ],
+  plugins: [
+    {
+      name: '@electron-forge/plugin-auto-unpack-natives',
+      config: {},
+    },
+    // Fuses 用于启用/禁用各种Electron功能
+    // 在打包时,在对应用程序进行代码签名之前
+    new FusesPlugin({
+      version: FuseVersion.V1,
+      [FuseV1Options.RunAsNode]: false,
+      [FuseV1Options.EnableCookieEncryption]: true,
+      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
+      [FuseV1Options.EnableNodeCliInspectArguments]: false,
+      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
+      [FuseV1Options.OnlyLoadAppFromAsar]: true,
+    }),
+  ],
+};

+ 7 - 10
jsconfig.json

@@ -1,19 +1,16 @@
 {
   "compilerOptions": {
-    "target": "es5",
+    "target": "ES2017",
     "module": "esnext",
     "baseUrl": "./",
     "moduleResolution": "node",
     "paths": {
-      "@/*": [
-        "src/*"
-      ]
+      "@/*": ["src/*"]
     },
-    "lib": [
-      "esnext",
-      "dom",
-      "dom.iterable",
-      "scripthost"
-    ]
+    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
+  },
+  "exclude": ["node_modules", "dist"],
+  "vueCompilerOptions": {
+    "target": 2
   }
 }

+ 50 - 0
main.js

@@ -0,0 +1,50 @@
+const { app, BrowserWindow } = require('electron');
+const path = require('path');
+const url = require('url');
+
+// 创建窗口
+const createWindow = () => {
+  const win = new BrowserWindow({
+    width: 1200,
+    height: 800,
+    autoHideMenuBar: true, // 隐藏菜单栏
+    webPreferences: {
+      nodeIntegration: true, // 是否集成 Node.js
+      enableRemoteModule: true, // 是否启用 remote 模块
+      webSecurity: false, // 是否禁用同源策略
+    },
+  });
+
+  win.loadURL(
+    url.format({
+      pathname: path.join(__dirname, './dist', 'index.html'),
+      protocol: 'file:',
+      slashes: true, // true: file://, false: file:
+      hash: '/login',
+    }),
+  );
+};
+
+// 当 Electron 完成初始化并准备创建浏览器窗口时调用此方法
+app.whenReady().then(() => {
+  createWindow();
+
+  app.on('activate', () => {
+    if (BrowserWindow.getAllWindows().length === 0) {
+      createWindow();
+    }
+  });
+});
+
+// 当所有窗口都已关闭时退出
+app.on('window-all-closed', () => {
+  if (process.platform !== 'darwin') {
+    app.quit();
+  }
+});
+
+app.on('activate', () => {
+  if (BrowserWindow.getAllWindows().length === 0) {
+    createWindow();
+  }
+});

Plik diff jest za duży
+ 526 - 526
package-lock.json


+ 56 - 26
package.json

@@ -1,40 +1,70 @@
 {
-  "name": "electron-demo",
+  "name": "gcls_page_textbook",
   "version": "0.1.0",
   "private": true,
+  "main": "main.js",
+  "description": "An Electron & Vue.js quick start boilerplate",
+  "author": "dsy",
   "scripts": {
-    "serve": "vue-cli-service serve",
+    "electron": "nodemon --watch main.js --exec \"electron .\"",
+    "dev": "vue-cli-service serve",
     "build": "vue-cli-service build",
-    "lint": "vue-cli-service lint"
+    "lint": "vue-cli-service lint",
+    "start": "electron-forge start",
+    "package": "electron-forge package",
+    "make": "electron-forge make"
   },
   "dependencies": {
-    "core-js": "^3.8.3",
-    "vue": "^2.6.14"
+    "@electron-forge/plugin-fuses": "^7.3.0",
+    "axios": "^1.6.7",
+    "core-js": "^3.36.0",
+    "electron-squirrel-startup": "^1.0.0",
+    "element-ui": "^2.15.14",
+    "js-cookie": "^3.0.5",
+    "md5": "^2.3.0",
+    "nprogress": "^0.2.0",
+    "vue": "^2.6.14",
+    "vue-router": "^3.6.5",
+    "vuex": "^3.6.2"
   },
   "devDependencies": {
-    "@babel/core": "^7.12.16",
-    "@babel/eslint-parser": "^7.12.16",
-    "@vue/cli-plugin-babel": "~5.0.0",
-    "@vue/cli-plugin-eslint": "~5.0.0",
-    "@vue/cli-service": "~5.0.0",
-    "eslint": "^7.32.0",
-    "eslint-plugin-vue": "^8.0.3",
+    "@babel/core": "^7.24.0",
+    "@babel/eslint-parser": "^7.23.10",
+    "@types/md5": "^2.3.5",
+    "@rushstack/eslint-patch": "^1.7.2",
+    "@vue/eslint-config-prettier": "^9.0.0",
+    "@vue/preload-webpack-plugin": "^2.0.0",
+    "@electron-forge/cli": "^7.3.0",
+    "@electron-forge/maker-deb": "^7.3.0",
+    "@electron-forge/maker-rpm": "^7.3.0",
+    "@electron-forge/maker-squirrel": "^7.3.0",
+    "@electron-forge/maker-zip": "^7.3.0",
+    "@electron-forge/plugin-auto-unpack-natives": "^7.3.0",
+    "@vue/cli-plugin-babel": "~5.0.8",
+    "@vue/cli-plugin-eslint": "~5.0.8",
+    "@vue/cli-service": "~5.0.8",
+    "compression-webpack-plugin": "^6.1.2",
+    "electron": "^29.1.0",
+    "eslint": "^8.56.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-vue": "^9.22.0",
+    "prettier": "^3.2.5",
+    "nodemon": "^3.1.0",
+    "sass": "^1.71.1",
+    "sass-loader": "^14.1.1",
+    "patch-package": "^8.0.0",
+    "postcss-html": "^1.6.0",
+    "stylelint": "^15.11.0",
+    "stylelint-config-recess-order": "^4.6.0",
+    "stylelint-config-recommended-scss": "^14.0.0",
+    "stylelint-config-recommended-vue": "^1.5.0",
+    "stylelint-config-standard-scss": "^12.0.0",
+    "stylelint-declaration-block-no-ignored-properties": "^2.8.0",
+    "stylelint-webpack-plugin": "^4.1.1",
+    "svg-sprite-loader": "^6.0.11",
+    "svgo": "^3.2.0",
     "vue-template-compiler": "^2.6.14"
   },
-  "eslintConfig": {
-    "root": true,
-    "env": {
-      "node": true
-    },
-    "extends": [
-      "plugin:vue/essential",
-      "eslint:recommended"
-    ],
-    "parserOptions": {
-      "parser": "@babel/eslint-parser"
-    },
-    "rules": {}
-  },
   "browserslist": [
     "> 1%",
     "last 2 versions",

+ 2 - 19
src/App.vue

@@ -1,28 +1,11 @@
 <template>
   <div id="app">
-    <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <RouterView />
   </div>
 </template>
 
 <script>
-import HelloWorld from './components/HelloWorld.vue'
-
 export default {
   name: 'App',
-  components: {
-    HelloWorld
-  }
-}
+};
 </script>
-
-<style>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
-}
-</style>

+ 112 - 0
src/api/app.js

@@ -0,0 +1,112 @@
+import { http } from '@/utils/http';
+// import store from '@/store';
+// import { app } from '@/store/mutation-types';
+
+/**
+ * @description 得到验证码图像
+ */
+export function GetVerificationCodeImage() {
+  return http.get(`${process.env.VUE_APP_FileServer}?MethodName=login_control-GetVerificationCodeImage`);
+}
+
+/**
+ * @description 得到系统标志
+ * @param {object} data 请求数据
+ */
+export function GetLogo(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=sys_config_manager-GetLogo`, data);
+}
+
+/**
+ * 得到文件 ID 与 URL 的映射
+ */
+export function GetFileURLMap(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=file_store_manager-GetFileURLMap`, data);
+}
+
+/**
+ * 得到文件存储信息
+ */
+export function GetFileStoreInfo(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=file_store_manager-GetFileStoreInfo`, data);
+}
+
+/**
+ * 得到用户能进入的子系统列表(电脑端)
+ */
+export function GetChildSysList_CanEnter_PC(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-GetChildSysList_CanEnter_PC`, data);
+}
+
+/**
+ * 解开访问令牌
+ * @param {object} data 请求数据
+ * @param {string} data.access_token 访问令牌
+ */
+export function ParseAccessToken(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-ParseAccessToken`, data);
+}
+
+/**
+ * 保存文件字节流的 Base64 编码文本信息
+ */
+export function SaveFileByteBase64Text(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=file_store_manager-SaveFileByteBase64Text`, data);
+}
+
+/**
+ * 上传文件
+ * @param {String} SecurityLevel 保密级别
+ * @param {object} file 文件对象
+ * @param {object} option 上传选项
+ * @param {function} option.handleUploadProgress 上传进度回调
+ * @param {boolean} option.isGlobalprogress 是否使用全局进度条
+ */
+// export function fileUpload(
+//   SecurityLevel,
+//   file,
+//   { handleUploadProgress, isGlobalprogress = false } = { isGlobalprogress: false },
+// ) {
+//   let formData = null;
+//   if (file instanceof FormData) {
+//     formData = file;
+//   } else {
+//     formData = new FormData();
+//     formData.append(file.filename, file.file, file.file.name);
+//   }
+
+//   let onUploadProgress = handleUploadProgress || null;
+//   if (isGlobalprogress) {
+//     store.commit(`app/${app.SHOW_PROGRESS}`, true);
+//     onUploadProgress = ({ loaded, progress, total }) => {
+//       // 因为上传进度为 1 后,上传事件还会继续一段时间,所以这里将进度设置为 0.99
+//       let precent = progress >= 1 ? 0.99 : progress;
+//       store.commit(`app/${app.SET_UPLOAD_INFO}`, { loaded, progress: precent, total });
+//     };
+//   }
+
+//   const controller = new AbortController();
+//   store.commit(`app/${app.SET_UPLOAD_CONTROLLER}`, controller);
+
+//   return http
+//     .postForm('/GCLSFileServer/WebFileUpload', formData, {
+//       params: {
+//         SecurityLevel,
+//       },
+//       signal: controller.signal,
+//       transformRequest: [data => data],
+//       onUploadProgress,
+//       timeout: 0,
+//     })
+//     .then(res => {
+//       store.commit(`app/${app.SET_UPLOAD_INFO}`, { loaded: 0, progress: 1, total: 0 });
+//       return res;
+//     })
+//     .finally(() => {
+//       store.commit(`app/${app.SET_UPLOAD_CONTROLLER}`, null);
+//     });
+// }
+
+export function GetStaticResources(MethodName, data) {
+  return http.post(`/GCLSFileServer/ServiceInterface?MethodName=${MethodName}`, data);
+}

+ 16 - 0
src/api/user.js

@@ -0,0 +1,16 @@
+import { http } from '@/utils/http';
+
+/**
+ * @description 用户登录
+ * @param {object} data
+ */
+export function login(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-Login`, data);
+}
+
+/**
+ * 获取用户信息
+ */
+export function GetMyUserInfo(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=user_manager-GetMyUserInfo`, data);
+}

BIN
src/assets/header/avatar-default.png


BIN
src/assets/header/exit.png


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

@@ -0,0 +1,96 @@
+<template>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners"></div>
+  <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, isNumber } from '@/utils/validate';
+
+export default {
+  name: 'SvgIcon',
+  props: {
+    size: {
+      type: [String, Number],
+      default: '',
+    },
+    width: {
+      type: [String, Number],
+      default: '1rem',
+    },
+    height: {
+      type: [String, Number],
+      default: '1rem',
+    },
+    iconClass: {
+      type: String,
+      required: true,
+    },
+    className: {
+      type: String,
+      default: '',
+    },
+  },
+  computed: {
+    _width() {
+      return this._getSize(this.width, this.size);
+    },
+    _height() {
+      return this._getSize(this.height, this.size);
+    },
+    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%`,
+      };
+    },
+  },
+  methods: {
+    /**
+     * 获取尺寸
+     * @param {String|Number} value
+     * @param {String|Number} size
+     */
+    _getSize(value, size) {
+      if (size) {
+        if (isNumber(size)) return `${size}px`;
+        return size;
+      }
+      if (isNumber(value)) {
+        return `${value}px`;
+      }
+      return value;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.svg-icon {
+  width: v-bind(_width);
+  height: v-bind(_height);
+  overflow: hidden;
+  vertical-align: -0.15em;
+  fill: currentColor;
+}
+
+.svg-external-icon {
+  display: inline-block;
+  background-color: currentColor;
+  mask-size: cover !important;
+}
+</style>

+ 0 - 58
src/components/HelloWorld.vue

@@ -1,58 +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-eslint" target="_blank" rel="noopener">eslint</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>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 8 - 0
src/icons/index.js

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

+ 79 - 0
src/layouts/default/breadcrumb/index.vue

@@ -0,0 +1,79 @@
+<template>
+  <div v-if="isShowBreadcrumb" class="breadcrumb">
+    <SvgIcon :icon-class="breadcrumb[0].meta.icon" />
+    <ul>
+      <li v-for="({ meta: { title }, path, redirect }, i) in breadcrumb" :key="title">
+        <span class="separator">/</span>
+        <span :key="i" class="breadcrumb-name" @click="$router.push(path.length > 0 ? path : redirect)">
+          {{ title }}
+        </span>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LayoutBreadcrumb',
+  data() {
+    return {
+      breadcrumb: [],
+    };
+  },
+  computed: {
+    isShowBreadcrumb() {
+      return this.breadcrumb.length > 0;
+    },
+  },
+  watch: {
+    $route() {
+      this.getBreadcrumb();
+    },
+  },
+  created() {
+    this.getBreadcrumb();
+  },
+  methods: {
+    getBreadcrumb() {
+      this.breadcrumb = this.$route.matched.filter((item) => item?.meta && item.meta.title);
+      this.$emit('update:isShowBreadcrumb', this.isShowBreadcrumb);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.breadcrumb {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  padding: 16px 24px;
+
+  > ul {
+    display: flex;
+
+    li {
+      font-weight: 400;
+      color: $font-light-color;
+
+      .separator {
+        margin: 0 8px;
+        color: #c9cdd4;
+      }
+
+      .breadcrumb-name {
+        font-size: 14px;
+      }
+
+      &:not(:last-child) {
+        cursor: pointer;
+      }
+
+      &:nth-last-child(1) {
+        font-weight: bold;
+        color: $font-color;
+      }
+    }
+  }
+}
+</style>

+ 172 - 0
src/layouts/default/header/index.vue

@@ -0,0 +1,172 @@
+<template>
+  <header class="header">
+    <el-image class="logo" :src="$store.state.app.config.logo_image_url" />
+
+    <!-- <ul class="header-list">
+      <li
+        v-for="({ name }, i) in projectList"
+        :key="i"
+        :class="i === LoginNavIndex ? 'active' : ''"
+        @click="handleCommand(i)"
+      >
+        {{ name }}
+      </li>
+    </ul> -->
+
+    <div v-if="!token" class="selectLoginOrRegistration">
+      <span>登录</span>
+    </div>
+    <!-- 用户头像和用户名 -->
+    <el-dropdown v-else trigger="click" class="user">
+      <span class="el-dropdown-link">
+        <img
+          class="avatar"
+          :src="token.image_url ? token.image_url : require('@/assets/header/avatar-default.png')"
+          alt="head portrait"
+        />
+        <span class="real_name">{{ token.user_real_name }}</span>
+      </span>
+      <el-dropdown-menu slot="dropdown" class="user-menu">
+        <el-dropdown-item @click.native="logout">
+          <img :src="require('@/assets/header/exit.png')" /><span>退出登录</span>
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </el-dropdown>
+  </header>
+</template>
+
+<script>
+import { GetChildSysList_CanEnter_PC } from '@/api/app';
+import { getToken } from '@/utils/auth';
+
+export default {
+  name: 'LayoutHeader',
+  data() {
+    const token = getToken();
+
+    return {
+      token: this.$store.state.user || token,
+      loginNav: 'GCLS-Exercise',
+      LoginNavIndex: 0,
+      projectList: [],
+    };
+  },
+  created() {
+    GetChildSysList_CanEnter_PC().then(({ child_sys_list }) => {
+      if (child_sys_list && child_sys_list.length > 0) {
+        this.projectList = child_sys_list;
+        this.projectList.forEach((item, index) => {
+          if (item.key === this.loginNav) {
+            this.LoginNavIndex = index;
+          }
+        });
+      }
+    });
+  },
+  methods: {
+    // 切换项目
+    handleCommand(command) {
+      this.LoginNavIndex = command;
+      if (!this.token) {
+        this.$message.warning('请先登录');
+        window.location.href = '/';
+        return;
+      }
+      const relative_path = this.projectList[command].relative_path;
+      location.href = relative_path;
+    },
+    logout() {
+      this.$store.dispatch('user/signOut');
+      // window.location.href = '/';
+      this.$router.push('/login');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.header {
+  position: sticky;
+  top: 0;
+  left: 0;
+  display: flex;
+  column-gap: 24px;
+  align-items: center;
+  justify-content: space-between;
+  height: $header-h;
+  padding: 0 24px;
+  overflow: hidden;
+  background-color: #fff;
+  border-bottom: 1px solid #ebebeb;
+
+  .logo {
+    height: 48px;
+    margin-right: 36px;
+  }
+
+  &-list {
+    display: flex;
+    flex: 1;
+
+    li {
+      padding: 0 12px;
+      font-weight: 500;
+      line-height: $header-h;
+      color: rgba(0, 0, 0, 45%);
+      white-space: nowrap;
+      cursor: pointer;
+
+      &:hover {
+        background: #f6f6f6;
+      }
+
+      &.active {
+        color: #f90;
+        background: #ffefd6;
+      }
+    }
+  }
+
+  .user {
+    cursor: pointer;
+
+    .el-dropdown-link {
+      display: flex;
+      align-items: center;
+
+      .avatar {
+        width: 32px;
+        height: 32px;
+        border-radius: 50%;
+      }
+
+      .real_name {
+        display: inline-block;
+        padding-left: 10px;
+        font-size: 16px;
+        color: #000;
+        vertical-align: super;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.user-menu {
+  min-width: 156px;
+
+  .el-dropdown-menu__item {
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+    color: #000;
+
+    img {
+      width: 24px;
+      height: 24px;
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 38 - 0
src/layouts/default/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="app">
+    <LayoutHeader />
+    <LayoutBreadcrumb :is-show-breadcrumb.sync="isShowBreadcrumb" />
+    <div class="app-container" :style="{ height: `calc(100vh - 64px ${isShowBreadcrumb ? '- 56px' : ''})` }">
+      <router-view />
+    </div>
+  </div>
+</template>
+
+<script>
+import LayoutHeader from './header/index.vue';
+import LayoutBreadcrumb from './breadcrumb/index.vue';
+
+export default {
+  name: 'LayoutDefault',
+  components: {
+    LayoutHeader,
+    LayoutBreadcrumb,
+  },
+  data() {
+    return {
+      isShowBreadcrumb: false,
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.app {
+  display: flex;
+  flex-direction: column;
+
+  &-container {
+    overflow: auto;
+  }
+}
+</style>

+ 25 - 5
src/main.js

@@ -1,8 +1,28 @@
-import Vue from 'vue'
-import App from './App.vue'
+import Vue from 'vue';
+import App from './App.vue';
 
-Vue.config.productionTip = false
+import router from './router';
+import store from './store';
+
+import '@/icons';
+import '@/styles/index.scss';
+import '@/utils/filter';
+
+import ElementUI from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+import { setupRouterGuard } from '@/router/guard';
+
+Vue.use(ElementUI, {
+  size: 'small',
+});
+
+setupRouterGuard(router);
+
+Vue.config.productionTip = false;
 
 new Vue({
-  render: h => h(App),
-}).$mount('#app')
+  router,
+  store,
+  render: (h) => h(App),
+}).$mount('#app');

+ 39 - 0
src/router/guard/index.js

@@ -0,0 +1,39 @@
+import { getSessionID, getConfig } from '@/utils/auth';
+
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+NProgress.configure({ showSpinner: false });
+
+const whiteList = ['/login', '/EnterSys', '/open/share/exercise']; // 重定向白名单
+
+export function setupRouterGuard(router) {
+  // 全局前置守卫
+  router.beforeEach(async (to, from, next) => {
+    NProgress.start();
+
+    if (getSessionID() && getConfig()) {
+      if (to.path === '/login') {
+        next({ path: '/' });
+        NProgress.done();
+      } else {
+        next();
+      }
+    } else if (whiteList.includes(to.path)) {
+      // 在登录白名单中,直接进入
+      next();
+    } else {
+      // 其他无权访问的页面将重定向到登录页面
+      if (process.env.NODE_ENV === 'production') {
+        window.location.href = '/';
+      } else {
+        next('/login');
+      }
+      NProgress.done();
+    }
+  });
+
+  // 全局后置钩子
+  router.afterEach(() => {
+    NProgress.done();
+  });
+}

+ 23 - 0
src/router/index.js

@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+import { routes } from './modules/index';
+
+Vue.use(VueRouter);
+
+const createRouter = () =>
+  new VueRouter({
+    mode: 'hash',
+    scrollBehavior: () => ({ y: 0 }),
+    routes,
+  });
+
+const router = createRouter();
+
+// 重置路由
+export function resetRouter() {
+  const newRouter = createRouter();
+  router.matcher = newRouter.matcher;
+}
+
+export default router;

+ 26 - 0
src/router/modules/basic.js

@@ -0,0 +1,26 @@
+import DEFAULT from '@/layouts/default';
+
+export const loginPage = {
+  path: '/login',
+  name: 'Login',
+  component: () => import('@/views/login'),
+};
+
+export const homePage = {
+  path: '/',
+  component: DEFAULT,
+  redirect: '/home',
+  children: [
+    {
+      path: '/home',
+      name: 'Home',
+      component: () => import('@/views/userInfo.vue'),
+    },
+  ],
+};
+
+export const NotFoundPage = {
+  path: '*',
+  name: '404',
+  component: () => import('@/views/not_found/404.vue'),
+};

+ 3 - 0
src/router/modules/index.js

@@ -0,0 +1,3 @@
+import { homePage, loginPage, NotFoundPage } from './basic';
+
+export const routes = [homePage, loginPage, NotFoundPage];

+ 5 - 0
src/store/getters.js

@@ -0,0 +1,5 @@
+const getters = {
+  isTeacher: (state) => state.user.user_type === 'TEACHER',
+};
+
+export default getters;

+ 20 - 0
src/store/index.js

@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import getters from './getters';
+
+import app from './modules/app';
+import user from './modules/user';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: {},
+  mutations: {},
+  actions: {},
+  modules: {
+    app,
+    user,
+  },
+  getters,
+  strict: process.env.NODE_ENV === 'development',
+});

+ 53 - 0
src/store/modules/app.js

@@ -0,0 +1,53 @@
+import { app } from '@/store/mutation-types';
+import { getConfig } from '@/utils/auth';
+import { conversionSize } from '@/utils/common';
+
+const getDefaultSate = () => {
+  const config = getConfig();
+  return {
+    showProgress: false,
+    uploadController: null,
+    uploadInfo: {
+      loaded: 0,
+      progress: 0,
+      total: 0,
+    },
+    config: {
+      logo_image_url: config?.logo_image_url ?? '',
+    },
+  };
+};
+
+const state = getDefaultSate();
+
+const mutations = {
+  [app.SHOW_PROGRESS]: (state, isShow) => {
+    state.showProgress = isShow;
+  },
+  [app.RESET_UPLOAD_INFO]: (state) => {
+    state.uploadInfo = {
+      loaded: 0,
+      progress: 0,
+      total: 0,
+    };
+  },
+  [app.SET_UPLOAD_CONTROLLER]: (state, controller) => {
+    state.uploadController = controller;
+  },
+  [app.SET_UPLOAD_INFO]: (state, { loaded, progress, total }) => {
+    state.uploadInfo = {
+      loaded: conversionSize(loaded),
+      progress: parseInt(String(progress * 100).replace(/(\d+)\.\d+/, '$1')),
+      total: conversionSize(total),
+    };
+  },
+};
+
+const actions = {};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 83 - 0
src/store/modules/user.js

@@ -0,0 +1,83 @@
+import { user } from '../mutation-types';
+import { login } from '@/api/user';
+import { setToken, getToken, removeToken } from '@/utils/auth';
+import { resetRouter } from '@/router';
+
+const getDefaultSate = () => {
+  const token = getToken();
+
+  return {
+    access_token: token?.access_token ?? '',
+    session_id: token?.session_id ?? '',
+    user_code: token?.user_code ?? '',
+    user_real_name: token?.user_real_name ?? '',
+    user_type: token?.user_type ?? '',
+    user_name: token?.user_name ?? '',
+    image_url: token?.image_url ?? '',
+    language_type: token?.language_type ?? 'ZH',
+    popedom_code_list: token?.popedom_code_list ?? [],
+  };
+};
+
+const state = getDefaultSate();
+
+const mutations = {
+  [user.RESET_STATE]: state => {
+    Object.assign(state, getDefaultSate());
+  },
+  [user.SET_USER_INFO]: (
+    state,
+    { user_code, user_real_name, user_type, language_type, session_id, image_url, popedom_code_list, access_token },
+  ) => {
+    state.user_code = user_code;
+    state.user_real_name = user_real_name;
+    state.user_type = user_type;
+    state.language_type = language_type;
+    state.session_id = session_id;
+    state.image_url = image_url;
+    state.language_type = language_type || 'ZH';
+    state.popedom_code_list = popedom_code_list;
+    state.access_token = access_token;
+  },
+};
+
+const actions = {
+  // 登录
+  login({ commit }, loginForm) {
+    return new Promise((reslove, reject) => {
+      login(loginForm)
+        .then(response => {
+          setToken(response);
+          commit(user.SET_USER_INFO, response);
+          reslove();
+        })
+        .catch(error => {
+          reject(error);
+        });
+    });
+  },
+
+  // 用户退出
+  signOut({ commit }) {
+    return new Promise(resolve => {
+      removeToken();
+      resetRouter();
+      commit(user.RESET_STATE);
+      resolve();
+    });
+  },
+
+  enterSys({ commit }) {
+    return new Promise(reslove => {
+      commit(user.RESET_STATE);
+      reslove();
+    });
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions,
+};

+ 13 - 0
src/store/mutation-types.js

@@ -0,0 +1,13 @@
+const app = {
+  SET_UPLOAD_INFO: 'SET_UPLOAD_INFO', // 设置上传信息
+  SET_UPLOAD_CONTROLLER: 'SET_UPLOAD_CONTROLLER', // 设置上传控制器
+  RESET_UPLOAD_INFO: 'RESET_UPLOAD_INFO', // 重置上传信息
+  SHOW_PROGRESS: 'SHOW_PROGRESS', // 显示进度条
+};
+
+const user = {
+  RESET_STATE: 'RESET_STATE', // 重置用户状态
+  SET_USER_INFO: 'SET_USER_INFO', // 设置用户信息
+};
+
+export { app, user };

+ 99 - 0
src/styles/index.scss

@@ -0,0 +1,99 @@
+:root {
+  box-sizing: border-box;
+  font-family: 'Inter', system-ui, 'Avenir', 'Helvetica', 'Arial', sans-serif;
+  font-size: 16px;
+  font-weight: 400;
+  line-height: 1.5;
+  color: $font-color;
+  color-scheme: light dark;
+  background-color: $main-background-color;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-size-adjust: 100%;
+}
+
+body {
+  width: 100%;
+  min-width: 320px;
+  height: 100vh;
+  min-height: 100vh;
+  padding: 0;
+  margin: 0;
+}
+
+#app {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+
+  &:hover {
+    color: #535bf2;
+  }
+}
+
+button {
+  padding: 0.6em 1.2em;
+  font-family: inherit;
+  font-size: 1em;
+  font-weight: 500;
+  cursor: pointer;
+  background-color: #1a1a1a;
+  border: 1px solid transparent;
+  border-radius: 8px;
+  transition: border-color 0.25s;
+
+  &:hover {
+    border-color: #646cff;
+  }
+
+  &:focus,
+  &:focus-visible {
+    outline: 4px auto -webkit-focus-ring-color;
+  }
+}
+
+ul {
+  padding-inline-start: 0;
+  margin: 0;
+
+  li {
+    list-style: none;
+  }
+}
+
+.card {
+  padding: 2em;
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: inherit;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: $font-color;
+    background-color: $main-background-color;
+  }
+
+  a:hover {
+    color: #747bff;
+  }
+
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 22 - 0
src/styles/variables.scss

@@ -0,0 +1,22 @@
+// color
+$main-color: #165dff;
+$light-main-color: #306eff;
+$main-background-color: #f7f8fa;
+$main-hover-color: #3371ff;
+$main-active-color: #e7eeff;
+$danger-color: #f53f3f;
+$danger-hover-color: #f56060;
+$font-color: #1d2129;
+$font-light-color: #4e5969;
+$fill-color: #f2f3f5;
+$text-color: #86909c;
+$content-color: #f9f8f9;
+$border-color: #e5e6eb;
+$border: 1px solid $border-color;
+$hanzi-writer-color: #de4444;
+$error-color: #f2555a;
+$right-color: #30a47d;
+$right-bc-color: #e8f7f2;
+
+// px
+$header-h: 64px;

+ 51 - 0
src/utils/auth.js

@@ -0,0 +1,51 @@
+import Cookies from 'js-cookie';
+
+const TokenKey = 'GCLS_Token';
+
+export function getSessionID() {
+  const token = Cookies.get(TokenKey);
+  const _token = token ? JSON.parse(token) : null;
+  return _token ? _token.session_id ?? '' : '';
+}
+
+/**
+ * 获取 token
+ * @returns {object | null}
+ */
+export function getToken() {
+  const token = Cookies.get(TokenKey);
+  return token ? JSON.parse(token) : null;
+}
+
+/**
+ * 设置 token
+ * @param {object} token
+ */
+export function setToken(token) {
+  const _token = typeof token === 'object' ? JSON.stringify(token) : '';
+  Cookies.set(TokenKey, _token);
+}
+
+/**
+ * 删除 token
+ */
+export function removeToken() {
+  Cookies.remove(TokenKey);
+}
+
+// 系统信息
+const ConfigKey = 'GCLS_Config';
+
+export function getConfig() {
+  const config = Cookies.get(ConfigKey);
+  return config ? JSON.parse(config) : null;
+}
+
+export function setConfig(value) {
+  let _val = typeof value === 'object' ? JSON.stringify(value) : '';
+  Cookies.set(ConfigKey, _val);
+}
+
+export function removeConfig() {
+  Cookies.remove(ConfigKey);
+}

+ 15 - 0
src/utils/common.js

@@ -0,0 +1,15 @@
+/**
+ * 换算数据大小
+ * @param {Number} size
+ * @returns String
+ */
+export function conversionSize(size) {
+  let _size = size;
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let factor = 0;
+  while (_size > 1024 && factor < units.length - 1) {
+    _size /= 1024;
+    factor += 1;
+  }
+  return `${_size.toFixed(2)}${units[factor]}`;
+}

+ 12 - 0
src/utils/filter.js

@@ -0,0 +1,12 @@
+import Vue from 'vue';
+
+// 创建一个全局过滤器,用于限制字符串只能输入数字,第二个参数是能输入的小数点位数,去除多余的字母和小数点、数字
+Vue.filter('number', (value, num = 0) => {
+  return value
+    .replace(/[^\d.]/g, '')
+    .replace(/\.{2,}/g, '.')
+    .replace('.', `$#$`)
+    .replace(/\./g, '')
+    .replace('$#$', '.')
+    .replace(new RegExp(`^(\\-)*(\\d+)\\.(\\d{${num}}).*$`), '$1$2.$3');
+});

+ 134 - 0
src/utils/http.js

@@ -0,0 +1,134 @@
+import axios from 'axios';
+// import store from '@/store';
+
+import { Message } from 'element-ui';
+import { getToken } from '@/utils/auth';
+import store from '@/store';
+
+const service = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API,
+  timeout: 30000,
+});
+
+// 请求拦截器
+service.interceptors.request.use(
+  config => {
+    config.headers['Content-Type'] = 'application/json';
+    return config;
+  },
+  error => {
+    return Promise.reject(error);
+  },
+);
+
+// 响应拦截器
+service.interceptors.response.use(
+  response => {
+    const res = response.data;
+    const { code, status, message, error } = res;
+    if (code === 404) {
+      Message({
+        message: '请求的资源不存在',
+        type: 'error',
+        duration: 3 * 1000,
+      });
+
+      return Promise.reject(new Error(message || 'Error'));
+    }
+
+    // 返回数据失败
+    if (status === 0) {
+      Message({
+        message: `${error}`,
+        type: 'error',
+        duration: 3 * 1000,
+      });
+
+      return Promise.reject(new Error(`${error}` || 'Error'));
+    }
+
+    // 无效的操作用户
+    if (status === -1) {
+      Message({
+        message: error,
+        type: 'error',
+        duration: 3 * 1000,
+      });
+      // store.dispatch('user/signOut');
+      const match = /#\/(.*?)\?/.exec(window.location.hash); // 使用 exec 方法进行匹配
+
+      // 答题跳转链接
+      let extractedString = '';
+      if (match && match.length > 1) {
+        extractedString = match[1];
+      }
+      // 答题链接特殊处理
+      if (extractedString === 'open/share/exercise') {
+        return Promise.reject(new Error(`${error}` || 'Error'));
+      }
+      // window.location.href = '/';
+    }
+
+    return res;
+  },
+  error => {
+    if (error.code === 'ERR_CANCELED') {
+      return Message.success('取消上传成功');
+    }
+
+    Message({
+      message: error.message,
+      type: 'error',
+      duration: 3 * 1000,
+    });
+
+    return Promise.reject(error);
+  },
+);
+
+/**
+ * 得到必需的请求参数
+ * @returns {object} 返回必需的请求参数
+ * */
+function getRequestParams() {
+  const token = store.state.user;
+
+  return {
+    AccessToken: token?.access_token ?? '',
+    UserCode: token?.user_code ?? '',
+    UserType: token?.user_type ?? '',
+    SessionID: token?.session_id ?? '',
+  };
+}
+
+/**
+ * @description http 请求封装
+ */
+export const http = {
+  /**
+   * @param {String} url 请求地址
+   * @param {object} config 请求配置
+   */
+  get: (url, config) => service.get(url, config),
+  /**
+   * @param {string} url 请求地址
+   * @param {object} data 请求数据
+   * @param {object} config 请求配置
+   */
+  post: (url, data = {}, config = {}) => {
+    config.params = {
+      ...config.params,
+      ...getRequestParams(),
+    };
+    return service.post(url, data, config);
+  },
+  postForm: (url, data = {}, config = {}) => {
+    config.params = {
+      ...config.params,
+      ...getRequestParams(),
+    };
+    return service.postForm(url, data, config);
+  },
+  put: (url, data, config) => service.put(url, data, config),
+  delete: (url, data, config) => service.delete(url, data, config),
+};

+ 48 - 0
src/utils/validate.js

@@ -0,0 +1,48 @@
+/**
+ * @description 是否外链
+ * @param {String} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path);
+}
+
+/**
+ * @description 只允许输入两位小数
+ * @param {String} value
+ * @returns { Number }
+ */
+export function twoDecimal(value) {
+  if (!value) {
+    return '';
+  }
+  let val = value;
+  val = val.replace(/[^\d.]/g, ''); // 清除"数字"和"."以外的字符
+  val = val.replace(/\.{2,}/g, '.'); // 只保留第一个 "." 清除多余的
+  val = val.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
+  val = val.replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3'); // 只能输入两位小数
+  return val;
+}
+
+/**
+ * @description 是否是数字
+ * @param {String|Number} value
+ * @returns {Boolean}
+ */
+export function isNumber(value) {
+  let _val = Number(value);
+  if (typeof _val === 'number') {
+    // 检查是否是有限数并且不是NaN
+    return isFinite(_val) && !isNaN(_val);
+  }
+  return false;
+}
+
+/**
+ * 判断 Node 元素类型
+ * @param {Node} node
+ * @param {String} type
+ */
+export function isNodeType(node, type) {
+  return node.nodeName.toLowerCase() === type.toLowerCase();
+}

+ 15 - 0
src/views/home/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {
+  name: 'HomePage',
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,141 @@
+<template>
+  <div class="login">
+    <main class="login-container">
+      <h1 class="title">用户登录</h1>
+      <el-form ref="loginForm" :model="form" :rules="rules" label-position="right">
+        <el-form-item prop="user_name">
+          <el-input v-model="form.user_name" />
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input v-model="form.password" type="password" show-password />
+        </el-form-item>
+        <el-form-item prop="verification_code_image_text" class="verification-code">
+          <el-input v-model="form.verification_code_image_text" placeholder="验证码" @keyup.enter.native="signIn" />
+          <el-image
+            v-if="image_content_base64.length > 0"
+            :src="`data:image/jpg;base64,${image_content_base64}`"
+            @click="updateVerificationCode"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button class="submit" type="primary" @click="signIn">登录</el-button>
+        </el-form-item>
+      </el-form>
+    </main>
+  </div>
+</template>
+
+<script>
+import md5 from 'md5';
+import { GetVerificationCodeImage, GetLogo } from '@/api/app';
+import { setConfig } from '@/utils/auth';
+
+export default {
+  name: 'LoginPage',
+  data() {
+    return {
+      form: {
+        user_type: 'TEACHER',
+        user_name: '',
+        password: '',
+        is_password_md5: 'true',
+        verification_code_image_id: '',
+        verification_code_image_text: '',
+      },
+      rules: {
+        user_name: [
+          {
+            required: true,
+            message: '请输入用户名',
+            trigger: 'blur',
+          },
+        ],
+        password: [
+          {
+            required: true,
+            message: '请输入密码',
+            trigger: 'blur',
+          },
+        ],
+        verification_code_image_text: [{ required: true, trigger: 'blur', message: '验证码不能为空' }],
+      },
+      image_content_base64: '',
+    };
+  },
+  created() {
+    this.updateVerificationCode();
+    this.getLogo();
+  },
+  methods: {
+    updateVerificationCode() {
+      GetVerificationCodeImage().then(({ image_id, image_content_base64: image }) => {
+        this.form.verification_code_image_id = image_id;
+        this.image_content_base64 = image;
+      });
+    },
+
+    getLogo() {
+      GetLogo().then((res) => {
+        setConfig(res);
+      });
+    },
+
+    signIn() {
+      this.$refs.loginForm.validate((valid) => {
+        if (!valid) return false;
+
+        let _form = { ...this.form, password: md5(this.form.password).toUpperCase() };
+        this.$store
+          .dispatch('user/login', _form)
+          .then(() => {
+            this.$router.push({ path: '/' });
+          })
+          .catch(() => {
+            this.updateVerificationCode();
+          });
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.login {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  background-color: #2d3a4b;
+
+  &-container {
+    padding: 200px;
+
+    .title {
+      margin-bottom: 20px;
+      color: #abc;
+      text-align: center;
+    }
+
+    :deep .el-form {
+      max-width: 400px;
+      margin: 0 auto;
+
+      .verification-code {
+        .el-input {
+          width: calc(100% - 90px);
+          margin-right: 10px;
+        }
+
+        .el-image {
+          height: 30px;
+          vertical-align: bottom;
+          cursor: pointer;
+        }
+      }
+    }
+
+    .submit {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 3 - 0
src/views/not_found/404.vue

@@ -0,0 +1,3 @@
+<template>
+  <h1>404</h1>
+</template>

+ 32 - 0
src/views/userInfo.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="user-info"></div>
+</template>
+
+<script>
+import { GetMyUserInfo } from '@/api/user';
+
+export default {
+  name: 'UserInfo',
+  data() {
+    return {
+      userInfo: {},
+    };
+  },
+  created() {
+    this.getMyUserInfo();
+  },
+  methods: {
+    getMyUserInfo() {
+      GetMyUserInfo().then((res) => {
+        this.userInfo = res;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.user-info {
+  padding: 24px;
+}
+</style>

+ 86 - 0
stylelint.config.js

@@ -0,0 +1,86 @@
+module.exports = {
+  root: true,
+  defaultSeverity: 'warning',
+  extends: ['stylelint-config-recommended-scss', 'stylelint-config-recess-order', 'stylelint-config-standard-scss'],
+  plugins: ['stylelint-declaration-block-no-ignored-properties'],
+  rules: {
+    'scss/dollar-variable-pattern': null,
+    'selector-class-pattern': null,
+    'order/properties-alphabetical-order': null,
+    // 嵌套过多,建议关闭此规则
+    'no-descending-specificity': null,
+    'selector-max-id': 1,
+    'selector-pseudo-class-no-unknown': [
+      true,
+      {
+        ignorePseudoClasses: ['deep', 'global'],
+      },
+    ],
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: [
+          'tailwind',
+          'apply',
+          'variants',
+          'responsive',
+          'screen',
+          'function',
+          'if',
+          'each',
+          'extend',
+          'include',
+          'mixin',
+          'at-root',
+          'use',
+        ],
+      },
+    ],
+    'rule-empty-line-before': [
+      'always',
+      {
+        ignore: ['after-comment', 'first-nested'],
+      },
+    ],
+    'color-function-notation': 'legacy',
+    // 函数 url 链接不允许 shceme relative
+    'function-url-no-scheme-relative': true,
+    // 可组合成一个属性的写法,不允许拆开书写
+    'declaration-block-no-redundant-longhand-properties': true,
+    // 选择器最大深度
+    'selector-max-compound-selectors': 10,
+    // 最多2个类型选择器
+    'selector-max-type': 2,
+    // 不允许未知的动画
+    'no-unknown-animations': true,
+    // 在字体名称必须使用引号的地方使用引号,其他地方不能使用
+    'font-family-name-quotes': 'always-unless-keyword',
+    // url 函数内部必须有引号
+    'function-url-quotes': 'always',
+    'value-keyword-case': ['lower', { ignoreKeywords: ['optimizeLegibility', 'currentColor'] }],
+    'max-nesting-depth': [10, { ignore: ['blockless-at-rules', 'pseudo-classes'] }],
+    'selector-no-qualifying-type': [true, { ignore: ['attribute', 'class', 'id'] }],
+    'font-family-no-missing-generic-family-keyword': [
+      true,
+      {
+        ignoreFontFamilies: ['League'],
+      },
+    ],
+  },
+  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
+  overrides: [
+    {
+      files: ['*.vue', '**/*.vue'],
+      extends: [
+        'stylelint-config-recess-order',
+        'stylelint-config-standard-scss',
+        'stylelint-config-recommended-vue/scss',
+      ],
+      rules: {
+        'keyframes-name-pattern': null,
+        'declaration-property-value-no-unknown': [true, { ignoreProperties: { '/.+/': '/(v-bind(.*))|($.*)/' } }],
+        'selector-pseudo-element-no-unknown': true,
+      },
+    },
+  ],
+};

+ 156 - 3
vue.config.js

@@ -1,4 +1,157 @@
-const { defineConfig } = require('@vue/cli-service')
+const path = require('path');
+const { defineConfig } = require('@vue/cli-service');
+const StyleLintPlugin = require('stylelint-webpack-plugin');
+const CompressionPlugin = require('compression-webpack-plugin');
+const PreloadPlugin = require('@vue/preload-webpack-plugin');
+
+function resolve(dir) {
+  return path.join(__dirname, './', dir);
+}
+
+const NODE_ENV = process.env.NODE_ENV;
+
+const port = process.env.port || 9564;
+
 module.exports = defineConfig({
-  transpileDependencies: true
-})
+  publicPath: NODE_ENV === 'development' ? '/' : './',
+  outputDir: 'dist',
+  assetsDir: 'static',
+  lintOnSave: NODE_ENV === 'development',
+  runtimeCompiler: true,
+  productionSourceMap: false,
+  devServer: {
+    port,
+    open: {
+      target: [`http://localhost:${port}`],
+    },
+    client: {
+      overlay: {
+        warnings: false,
+        errors: true,
+      },
+    },
+    proxy: {
+      [process.env.VUE_APP_BASE_API]: {
+        // target: 'https://gcls.utschool.cn/',
+        target: 'https://gcls.helxsoft.cn/',
+        // target: 'https://youthchinesedu.blcup.com/',
+        changeOrigin: true,
+        pathRewrite: {
+          [`^${process.env.VUE_APP_BASE_API}`]: '',
+        },
+      },
+      [process.env.VUE_APP_FILE]: {
+        // target: 'https://file-cs.helxsoft.cn',
+        target: 'https://file-kf.helxsoft.cn/',
+        changeOrigin: true,
+        pathRewrite: {
+          [`^${process.env.VUE_APP_FILE}`]: '',
+        },
+      },
+    },
+  },
+  css: {
+    loaderOptions: {
+      sass: {
+        additionalData: '@use "@/styles/variables.scss" as *;',
+      },
+    },
+  },
+  configureWebpack: {
+    name: '教材编辑器',
+    // 配置路径别名
+    resolve: {
+      alias: {
+        '@': resolve('src'),
+      },
+    },
+    devtool: NODE_ENV === 'development' ? 'source-map' : false,
+    plugins: [
+      // stylelint 配置
+      new StyleLintPlugin({
+        files: ['src/**/*.{vue,html,css,scss}'],
+        fix: true, // 是否自动修复
+        failOnError: false,
+      }),
+      new CompressionPlugin({
+        algorithm: 'gzip', // 使用gzip压缩
+        test: /\.js$|\.html$|\.css$/, // 匹配文件名
+        minRatio: 0.8, // 压缩率小于0.8才会压缩
+        threshold: 10240, // 对超过10k的数据压缩
+        deleteOriginalAssets: false, // 是否删除未压缩的源文件,谨慎设置,如果希望提供非gzip的资源,可不设置或者设置为false(比如删除打包后的gz后还可以加载到原始资源文件)
+      }),
+    ],
+  },
+  chainWebpack(config) {
+    // 启用预加载,提高首屏加载速度
+    config
+      .plugin('preload')
+      .use(PreloadPlugin, [
+        {
+          rel: 'preload',
+          include: 'initial',
+          fileBlacklist: [/\.map$/, /hot-update\.js$/],
+          as(entry) {
+            if (/\.css$/.test(entry)) return 'style';
+
+            return 'script';
+          },
+        },
+      ])
+      .after('html');
+
+    // 当页面很多时,prefetch 将导致太多无意义的请求,开启这个
+    config
+      .plugin('prefetch')
+      .use(PreloadPlugin, [
+        {
+          rel: 'prefetch',
+          include: 'asyncChunks',
+        },
+      ])
+      .after('html');
+
+    // 设置 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(NODE_ENV !== 'development', () => {
+      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');
+    });
+  },
+});

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików