├── .editorconfig ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── .gitignore ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── iconfont.css ├── icons │ └── your-icon.png ├── index.html ├── robots.txt ├── static │ └── img │ │ └── logo.png └── tinymce │ ├── langs │ └── zh_CN.js │ └── skins │ ├── content │ ├── default │ │ ├── content.css │ │ └── content.min.css │ ├── document │ │ ├── content.css │ │ └── content.min.css │ └── writer │ │ ├── content.css │ │ └── content.min.css │ └── ui │ ├── oxide-dark │ ├── content.css │ ├── content.inline.css │ ├── content.inline.min.css │ ├── content.min.css │ ├── skin.css │ └── skin.min.css │ └── oxide │ ├── content.css │ ├── content.inline.css │ ├── content.inline.min.css │ ├── content.min.css │ ├── content.mobile.min.css │ ├── fonts │ └── tinymce-mobile.woff │ ├── skin.css │ ├── skin.min.css │ └── skin.mobile.min.css ├── script ├── lib │ ├── exec-promise.js │ ├── install-dep.js │ ├── plugin-get-all.js │ ├── semver-validate.js │ └── util.js ├── plugin-get-config.js ├── plugin-init.js ├── plugin-new.js └── template │ ├── plugin-stage-config.js.ejs │ └── plugin │ ├── README.md.ejs │ ├── asset │ └── image │ │ └── logo.png │ ├── component │ └── component.vue.ejs │ ├── package.json.ejs │ ├── stage-config.js.ejs │ └── view │ ├── stage1.vue.ejs │ └── stage2.vue.ejs ├── src ├── app.vue ├── assets │ ├── image │ │ ├── about │ │ │ ├── avatar.png │ │ │ ├── header-bg.png │ │ │ ├── icon.png │ │ │ ├── icon1.png │ │ │ ├── icon2.png │ │ │ ├── icon3.png │ │ │ ├── icon4.png │ │ │ ├── open-source.jpg │ │ │ ├── qrcode.jpg │ │ │ ├── team-icon.png │ │ │ └── welcome.png │ │ ├── error-page │ │ │ ├── 404.png │ │ │ └── logo.png │ │ ├── login │ │ │ ├── login-ba.png │ │ │ ├── login-btn.png │ │ │ ├── nickname.png │ │ │ ├── password.png │ │ │ └── team-name.png │ │ ├── logo.png │ │ ├── mobile-logo.png │ │ └── user │ │ │ ├── corner.png │ │ │ ├── user-bg.png │ │ │ ├── user.jpg │ │ │ └── user.png │ └── style │ │ ├── elementFont.css │ │ ├── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ │ ├── index.scss │ │ ├── realize │ │ ├── animation.scss │ │ ├── element-variable.scss │ │ ├── mixin.scss │ │ ├── reset.scss │ │ ├── transition.scss │ │ └── variable.scss │ │ └── shared.scss ├── component │ ├── base │ │ ├── date-picker │ │ │ └── lin-date-picker.vue │ │ ├── dialog │ │ │ └── lin-dialog.vue │ │ ├── dropdown │ │ │ └── lin-dropdown.vue │ │ ├── icon │ │ │ └── lin-icon.vue │ │ ├── search │ │ │ └── lin-search.vue │ │ ├── source-code │ │ │ └── source-code.vue │ │ ├── sticky-top │ │ │ └── sticky-top.vue │ │ ├── tinymce │ │ │ └── index.vue │ │ └── upload-image │ │ │ ├── README.md │ │ │ ├── index.vue │ │ │ └── utils.js │ ├── layout │ │ ├── app-main.vue │ │ ├── avatar.vue │ │ ├── back-top.vue │ │ ├── breadcrumb.vue │ │ ├── clear-tab.vue │ │ ├── index.js │ │ ├── menu-tab.vue │ │ ├── nav-bar.vue │ │ ├── reuse-tab.vue │ │ ├── screen-full.vue │ │ ├── sidebar │ │ │ ├── logo.vue │ │ │ ├── menu-tree.vue │ │ │ ├── search.vue │ │ │ └── sidebar.vue │ │ └── user.vue │ └── notify │ │ ├── emitter.js │ │ ├── index.js │ │ ├── notify.vue │ │ └── observer.js ├── config │ ├── error-code.js │ ├── global.js │ ├── index.js │ └── stage │ │ ├── admin.js │ │ ├── book.js │ │ ├── center.js │ │ ├── index.js │ │ └── plugin.js ├── lin │ ├── context │ │ ├── admin.js │ │ └── index.js │ ├── directive │ │ ├── authorize.js │ │ └── index.js │ ├── filter │ │ └── index.js │ ├── model │ │ ├── admin.js │ │ ├── log.js │ │ ├── notify.js │ │ └── user.js │ ├── plugin │ │ ├── README.md │ │ ├── axios.js │ │ └── index.js │ └── util │ │ ├── auto-jump.js │ │ ├── cookie.js │ │ ├── date.js │ │ ├── emitter.js │ │ ├── index.js │ │ ├── search.js │ │ ├── sse.js │ │ ├── storage.js │ │ ├── token.js │ │ └── util.js ├── main.js ├── model │ └── book.js ├── plugin │ ├── custom │ │ ├── README.md │ │ ├── assets │ │ │ └── image │ │ │ │ └── logo.png │ │ ├── package.json │ │ ├── stage-config.js │ │ └── view │ │ │ ├── multiple-input.vue │ │ │ ├── tinymce.vue │ │ │ └── upload-image.vue │ └── lin-cms-ui │ │ ├── README.md │ │ ├── assets │ │ ├── image │ │ │ └── logo.png │ │ └── style │ │ │ └── container.scss │ │ ├── component │ │ └── component.vue │ │ ├── model │ │ └── movie.js │ │ ├── package.json │ │ ├── simulation │ │ └── movie.js │ │ ├── stage-config.js │ │ └── view │ │ ├── basic │ │ ├── button │ │ │ └── button.vue │ │ ├── icon │ │ │ └── icon.vue │ │ └── link │ │ │ └── link.vue │ │ ├── data │ │ ├── badge │ │ │ └── badge.vue │ │ ├── pagination │ │ │ └── pagination.vue │ │ ├── progress │ │ │ └── progress.vue │ │ └── tag │ │ │ └── tag.vue │ │ ├── form │ │ ├── cascader.vue │ │ ├── checkbox.vue │ │ ├── date-picker.vue │ │ ├── date-time-picker.vue │ │ ├── input.vue │ │ ├── multiple-input.vue │ │ ├── radio.vue │ │ ├── rate │ │ │ └── rate.vue │ │ ├── select.vue │ │ ├── slider.vue │ │ ├── switch │ │ │ └── switch.vue │ │ └── time-picker.vue │ │ ├── navigation │ │ ├── breadcrumb.vue │ │ ├── dropdown.vue │ │ ├── steps.vue │ │ └── tab │ │ │ └── tab.vue │ │ ├── notice │ │ ├── alert.vue │ │ ├── loading.vue │ │ ├── message.vue │ │ └── notification.vue │ │ ├── other │ │ ├── dialog.vue │ │ └── timeline.vue │ │ └── table │ │ ├── data.js │ │ ├── table-combo.vue │ │ └── table.vue ├── router │ ├── home-router.js │ ├── index.js │ └── route.js ├── store │ ├── action.js │ ├── getter.js │ ├── index.js │ ├── mutation-type.js │ ├── mutation.js │ └── state.js └── view │ ├── about │ └── about.vue │ ├── admin │ ├── group │ │ ├── group-create.vue │ │ ├── group-edit.vue │ │ ├── group-list.vue │ │ ├── group-permission.vue │ │ └── use-group.js │ └── user │ │ ├── use-user.js │ │ ├── user-create.vue │ │ ├── user-info.vue │ │ ├── user-list.vue │ │ └── user-password.vue │ ├── book │ ├── book-list.vue │ └── book.vue │ ├── center │ └── center.vue │ ├── error-page │ └── 404.vue │ ├── home │ └── home.vue │ ├── log │ └── log.vue │ └── login │ └── login.vue ├── tests └── unit │ └── .eslintrc.js ├── vue.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | ENV = 'development' 2 | 3 | VUE_APP_BASE_URL = 'http://localhost:5000/' 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | 2 | 3 | VUE_APP_BASE_URL = 'http://localhost:5000/' 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /builds/ 2 | /public/ 3 | /dist/ 4 | /script/.cache 5 | /*.js 6 | /node_modules/ 7 | /tests/unit/LIcon.test.js 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | jest: true, 8 | }, 9 | plugins: ['vue', 'vuejs-accessibility'], 10 | extends: ['plugin:vue/vue3-essential', '@vue/airbnb'], 11 | rules: { 12 | 'vue/script-setup-uses-vars': 'off', 13 | 'vue/custom-event-name-casing': 'off', 14 | 'vuejs-accessibility/rule-name': 'off', 15 | 'vue/no-deprecated-slot-attribute': 'off', 16 | 'vue/experimental-script-setup-vars': 'off', 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 19 | 'no-shadow': 0, 20 | 'no-plusplus': 0, // 禁止使用++,-- 21 | 'guard-for-in': 0, 22 | 'no-extra-semi': 0, // 和prettier冲突 23 | 'import/extensions': 0, // import不需要写文件扩展名 24 | semi: ['error', 'never'], // 无分号 25 | 'import/no-unresolved': 0, 26 | 'no-restricted-syntax': 0, 27 | 'no-underscore-dangle': 0, // 无下划线 28 | 'no-restricted-syntax': 0, 29 | 'consistent-return': 'off', 30 | 'max-len': ['error', { code: 200 }], 31 | 'no-use-before-define': 'off', 32 | 'no-prototype-builtins': 'off', 33 | 'class-methods-use-this': 'off', 34 | 'template-curly-spacing': 'off', 35 | 'arrow-parens': ['error', 'as-needed'], 36 | 'vue/multi-word-component-names': 'off', 37 | 'comma-dangle': ['error', 'only-multiline'], 38 | 'no-param-reassign': ['error', { props: false }], 39 | 'vuejs-accessibility/click-events-have-key-events': 'off', 40 | 'import/no-extraneous-dependencies': ['error', { devDependencies: ['script/**/*.js'] }], 41 | indent: [ 42 | 'warn', 43 | 2, 44 | { 45 | ignoredNodes: ['TemplateLiteral'], 46 | SwitchCase: 1, 47 | }, 48 | ], 49 | 'object-curly-newline': [ 50 | 'error', 51 | { 52 | ImportDeclaration: 'never', 53 | }, 54 | ], 55 | }, 56 | parserOptions: { 57 | parser: '@babel/eslint-parser', 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **重现步骤(可选):** 11 | 12 | **期望的结果是什么?** 13 | 14 | **实际的结果是什么?** 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **描述你希望的支持的新功能?** 11 | 12 | **你期望的 API 是怎样的?** 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /script/.cache 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号 3 | semi: false, //行位是否使用分号,默认为true 4 | trailingComma: 'all', //是否使用尾逗号,有三个可选值"" 5 | printWidth: 120, 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | addons: 5 | chrome: stable 6 | sudo: required 7 | before_script: 8 | - "sudo chown root /opt/google/chrome/chrome-sandbox" 9 | - "sudo chmod 4755 /opt/google/chrome/chrome-sandbox" 10 | script: npm run test:unit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@vue/cli-plugin-babel/preset']], 3 | plugins: [ 4 | //去除element babel 新版直接手动导入即可 5 | '@babel/plugin-proposal-optional-chaining', 6 | '@babel/plugin-proposal-nullish-coalescing-operator', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | transformIgnorePatterns: [ 14 | '/node_modules/' 15 | ], 16 | moduleNameMapper: { 17 | '^@/(.*)$': '/src/$1' 18 | }, 19 | snapshotSerializers: [ 20 | 'jest-serializer-vue' 21 | ], 22 | testMatch: [ 23 | '**/tests/unit/**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 24 | ], 25 | testURL: 'http://localhost/', 26 | watchPlugins: [ 27 | 'jest-watch-typeahead/filename', 28 | 'jest-watch-typeahead/testname' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lin-cms-vue", 3 | "version": "0.4.3", 4 | "license": "MIT", 5 | "scripts": { 6 | "serve": "node script/plugin-get-config.js && vue-cli-service serve", 7 | "build": "node script/plugin-get-config.js && vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint", 10 | "commit": "git add . && git-cz", 11 | "plugin:init": "node script/plugin-init.js", 12 | "plugin:new": "node script/plugin-new.js", 13 | "plugin:reconfig": "node script/plugin-get-config.js" 14 | }, 15 | "dependencies": { 16 | "@babel/polyfill": "^7.4.4", 17 | "@element-plus/icons-vue": "^2.1.0", 18 | "@tinymce/tinymce-vue": "^4.0.0", 19 | "axios": "^0.24.0", 20 | "core-js": "^3.23.5", 21 | "dayjs": "^1.10.4", 22 | "element-plus": "^2.3.8", 23 | "event-source-polyfill": "^1.0.7", 24 | "fastscan": "^1.0.4", 25 | "good-storage": "^1.1.0", 26 | "js-cookie": "^2.2.0", 27 | "lodash": "^4.17.14", 28 | "mitt": "^2.1.0", 29 | "moment": "^2.29.4", 30 | "photoswipe": "^4.1.2", 31 | "screenfull": "^4.2.0", 32 | "swiper": "^6.4.5", 33 | "vue": "^3.2.24", 34 | "vue-picture-cropper": "^0.1.9", 35 | "vue-router": "^4.0.10", 36 | "vuex": "^4.0.2", 37 | "vuex-persist": "^2.0.1", 38 | "yarn": "^1.22.19" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.11.4", 42 | "@babel/eslint-parser": "^7.17.0", 43 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 44 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 45 | "@vue/cli-plugin-babel": "^5.0.3", 46 | "@vue/cli-plugin-eslint": "^5.0.3", 47 | "@vue/cli-plugin-unit-jest": "^5.0.3", 48 | "@vue/cli-service": "^5.0.3", 49 | "@vue/compiler-sfc": "^3.2.0", 50 | "@vue/eslint-config-airbnb": "^6.0.0", 51 | "@vue/test-utils": "^2.0.0-beta.8", 52 | "babel-jest": "^26.3.0", 53 | "babel-plugin-component": "^1.1.1", 54 | "chalk": "^2.4.2", 55 | "child_process": "^1.0.2", 56 | "commitizen": "^4.2.3", 57 | "cz-conventional-changelog": "^2.1.0", 58 | "directory-tree": "^2.2.3", 59 | "ejs": "^2.6.2", 60 | "eslint": "^8.11.0", 61 | "eslint-plugin-vue": "^8.5.0", 62 | "eslint-plugin-vuejs-accessibility": "^1.1.1", 63 | "fs-extra": "^8.1.0", 64 | "ignore-loader": "^0.1.2", 65 | "inquirer": "^6.5.0", 66 | "js-yaml": "^3.13.1", 67 | "lint-staged": "^9.5.0", 68 | "sass": "^1.26.5", 69 | "sass-loader": "^8.0.2", 70 | "semver": "^6.2.0", 71 | "shelljs": "^0.8.5", 72 | "validate-npm-package-name": "^3.0.0", 73 | "yaml-front-matter": "^4.0.0" 74 | }, 75 | "browserslist": [ 76 | "> 1%", 77 | "last 2 versions", 78 | "not ie <= 11" 79 | ], 80 | "config": { 81 | "commitizen": { 82 | "path": "./node_modules/cz-conventional-changelog" 83 | } 84 | }, 85 | "gitHooks": { 86 | "pre-commit": "lint-staged" 87 | }, 88 | "lint-staged": { 89 | "*.{js,vue}": [ 90 | "prettier --write", 91 | "vue-cli-service lint", 92 | "git add" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/your-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/public/icons/your-icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | lin-cms 11 | 94 | 95 | 96 | 97 | 98 |
99 |
100 | 101 |
102 |
103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/public/static/img/logo.png -------------------------------------------------------------------------------- /public/tinymce/skins/content/default/content.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 9 | line-height: 1.4; 10 | margin: 1rem; 11 | } 12 | table { 13 | border-collapse: collapse; 14 | } 15 | table th, 16 | table td { 17 | border: 1px solid #ccc; 18 | padding: .4rem; 19 | } 20 | blockquote { 21 | border-left: 2px solid #ccc; 22 | margin-left: 1.5rem; 23 | padding-left: 1rem; 24 | } 25 | figure { 26 | display: table; 27 | margin: 1rem auto; 28 | } 29 | figure figcaption { 30 | color: #999; 31 | display: block; 32 | margin-top: .25rem; 33 | text-align: center; 34 | } 35 | hr { 36 | border-color: #ccc; 37 | border-style: solid; 38 | border-width: 1px 0 0 0; 39 | } 40 | code { 41 | background-color: #e8e8e8; 42 | border-radius: 3px; 43 | padding: .1rem .2rem; 44 | } -------------------------------------------------------------------------------- /public/tinymce/skins/content/default/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table td,table th{border:1px solid #ccc;padding:.4rem}blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem} 8 | /*# sourceMappingURL=content.min.css.map */ 9 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/document/content.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | @media screen { 8 | html { 9 | background: #f4f4f4; 10 | } 11 | } 12 | body { 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 14 | } 15 | @media screen { 16 | body { 17 | background-color: #fff; 18 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.15); 19 | box-sizing: border-box; 20 | margin: 1rem auto 0; 21 | max-width: 820px; 22 | min-height: calc(99vh); 23 | padding: 4rem 6rem 6rem 6rem; 24 | } 25 | } 26 | table { 27 | border-collapse: collapse; 28 | } 29 | table th, 30 | table td { 31 | border: 1px solid #ccc; 32 | padding: .4rem; 33 | } 34 | blockquote { 35 | border-left: 2px solid #ccc; 36 | margin-left: 1.5rem; 37 | padding-left: 1rem; 38 | } 39 | figure figcaption { 40 | color: #999; 41 | margin-top: .25rem; 42 | text-align: center; 43 | } 44 | hr { 45 | border-color: #ccc; 46 | border-style: solid; 47 | border-width: 1px 0 0 0; 48 | } -------------------------------------------------------------------------------- /public/tinymce/skins/content/document/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | @media screen{html{background:#f4f4f4}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(99vh);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table td,table th{border:1px solid #ccc;padding:.4rem}blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0} 8 | /*# sourceMappingURL=content.min.css.map */ 9 | -------------------------------------------------------------------------------- /public/tinymce/skins/content/writer/content.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | body { 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 9 | line-height: 1.4; 10 | margin: 1rem auto; 11 | max-width: 900px; 12 | } 13 | table { 14 | border-collapse: collapse; 15 | } 16 | table th, 17 | table td { 18 | border: 1px solid #ccc; 19 | padding: .4rem; 20 | } 21 | blockquote { 22 | border-left: 2px solid #ccc; 23 | margin-left: 1.5rem; 24 | padding-left: 1rem; 25 | } 26 | figure { 27 | display: table; 28 | margin: 1rem auto; 29 | } 30 | figure figcaption { 31 | color: #999; 32 | display: block; 33 | margin-top: .25rem; 34 | text-align: center; 35 | } 36 | hr { 37 | border-color: #ccc; 38 | border-style: solid; 39 | border-width: 1px 0 0 0; 40 | } 41 | code { 42 | background-color: #e8e8e8; 43 | border-radius: 3px; 44 | padding: .1rem .2rem; 45 | } -------------------------------------------------------------------------------- /public/tinymce/skins/content/writer/content.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Tiny Technologies, Inc. All rights reserved. 3 | * Licensed under the LGPL or a commercial license. 4 | * For LGPL see License.txt in the project root for license information. 5 | * For commercial licenses see https://www.tiny.cloud/ 6 | */ 7 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table td,table th{border:1px solid #ccc;padding:.4rem}blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem} 8 | /*# sourceMappingURL=content.min.css.map */ 9 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/content.mobile.min.css: -------------------------------------------------------------------------------- 1 | .tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{position:absolute;display:inline-block;background-color:green;opacity:.5}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%} -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /script/lib/exec-promise.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | 3 | // 执行命令 4 | function exec(cmd) { 5 | return new Promise((resolve, reject) => { 6 | child_process.exec(cmd, (error, stdout) => { 7 | if (error) { 8 | reject(error) 9 | } 10 | resolve(stdout) 11 | }) 12 | }) 13 | } 14 | 15 | module.exports = exec 16 | -------------------------------------------------------------------------------- /script/lib/install-dep.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const semver = require('semver') 3 | const exec = require('./exec-promise') 4 | 5 | // 获取当前安装包版本 6 | const getLocalVersion = async (pkg, isDev) => { 7 | let lsInfo 8 | try { 9 | lsInfo = JSON.parse(await exec(`npm ls ${pkg} --json --depth=0 ${isDev ? '--dev' : '--prod'}`)) 10 | if (lsInfo.dependencies) { 11 | lsInfo = lsInfo.dependencies[pkg].version 12 | } 13 | } catch (e) { 14 | lsInfo = false 15 | } 16 | return lsInfo 17 | } 18 | 19 | // pkg: 需要安装的包名, isDev: 是否安装到 dev 依赖, pPackage: 项目的 package 文件 20 | const installFuc = async (pkg, version, pPackage = {}, isDev = false) => { 21 | if (!pkg) { 22 | throw new Error('未传入需要安装的包名') 23 | } 24 | 25 | // 参数兼容性处理 26 | const originPkg = {} 27 | originPkg.dependencies = pPackage.dependencies || {} 28 | originPkg.devDependencies = pPackage.devDependencies || {} 29 | 30 | const key = isDev ? 'devDependencies' : 'dependencies' 31 | 32 | // 获取当前安装包版本 33 | let v = await getLocalVersion(pkg, isDev) 34 | 35 | // 如果未检测到安装包版本, 属于异常, 则本地先安装一下 36 | if (v) { 37 | // 检测当前已安装版本是否符合要求 38 | if (semver.satisfies(v, version)) { 39 | console.log(`当前已存在依赖 ${pkg}@${v}`) 40 | return true 41 | } 42 | } else { 43 | console.log(chalk.yellow(`安装依赖 ${pkg}@${version}`)) 44 | await exec(`npm install ${isDev ? '--save-dev' : ''} ${pkg}@${version}`) 45 | v = await getLocalVersion(pkg, isDev) 46 | } 47 | 48 | // 本地项目本身无该包 49 | if (!originPkg[key][pkg]) { 50 | return true 51 | } 52 | 53 | // 检测当前已安装版本是否符合要求 54 | if (semver.satisfies(v, originPkg[key][pkg])) { 55 | console.log(`当前已存在依赖 ${pkg}@${v}`) 56 | return true 57 | } 58 | 59 | // 不符合要求则按照项目要求回滚 60 | await exec(`npm install ${isDev ? '--save-dev' : ''} ${pkg}@${originPkg[key][pkg]}`) 61 | 62 | // 检测回滚后能否符合要求 63 | v = await getLocalVersion(pkg, isDev) 64 | if (semver.satisfies(v, version)) { 65 | console.log(`已更新依赖 ${pkg}@${v}`) 66 | return true 67 | } 68 | 69 | // 回滚后还是不符合, 说明两版本要求冲突 70 | throw new Error(`依赖包 ${pkg} 与本地版本有冲突, 版本要求是 ${version}, 本地项目要求是 ${originPkg[key][pkg]}`) 71 | } 72 | 73 | module.exports = installFuc 74 | -------------------------------------------------------------------------------- /script/lib/plugin-get-all.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const path = require('path') 3 | const fs = require('fs-extra') 4 | const chalk = require('chalk') 5 | const { came } = require('./util') 6 | 7 | // 验证是否是插件 8 | function isPlugin(source) { 9 | let result = true 10 | if (!fs.lstatSync(source).isDirectory()) { 11 | return false 12 | } 13 | const configPath = path.resolve(source, './stage-config.js') 14 | const packagePath = path.resolve(source, './package.json') 15 | if (result && !fs.existsSync(configPath)) { 16 | result = false 17 | } 18 | if (result && !fs.existsSync(packagePath)) { 19 | result = false 20 | } 21 | if (!result) { 22 | console.log(chalk.yellow(`${source} 不符合 Lin-CMS 插件规范`)) 23 | } 24 | 25 | return result 26 | } 27 | 28 | function getPlugins(source) { 29 | if (!fs.existsSync(source)) { 30 | console.log(chalk.yellow(`目录不存在: ${source}`)) 31 | return [] 32 | } 33 | const folders = fs.readdirSync(source) 34 | const pluginsList = [] 35 | 36 | folders.forEach(item => { 37 | const itemPath = path.join(source, item) 38 | if (!isPlugin(itemPath)) { 39 | return 40 | } 41 | const config = {} 42 | config.name = item 43 | config.camelCaseName = came(item) 44 | config.path = path.resolve(__dirname, `../src/plugins/${item}/`) 45 | config.packageCtx = JSON.parse(fs.readFileSync(path.resolve(itemPath, './package.json'), 'utf8')) 46 | pluginsList.push(config) 47 | }) 48 | 49 | return pluginsList 50 | } 51 | 52 | module.exports = getPlugins 53 | -------------------------------------------------------------------------------- /script/lib/semver-validate.js: -------------------------------------------------------------------------------- 1 | // 预计算一下版本是否有冲突 2 | const semver = require('semver') 3 | 4 | const validateSemver = (range1, range2) => { 5 | if (!range1 || !range2) { 6 | return false 7 | } 8 | // 都是指定版本 9 | if (semver.valid(range1) && semver.valid(range2)) { 10 | return (semver.coerce(range1) === semver.coerce(range2)) 11 | } 12 | 13 | // 都是范围 14 | if (semver.validRange(range1) && semver.validRange(range2)) { 15 | return semver.intersects(range1, range2) 16 | } 17 | 18 | // 一个版本一个范围 19 | if (semver.valid(range1) && semver.validRange(range2)) { 20 | return semver.satisfies(range1, range2) 21 | } 22 | 23 | if (semver.valid(range2) && semver.validRange(range1)) { 24 | return semver.satisfies(range2, range1) 25 | } 26 | 27 | return false 28 | } 29 | 30 | module.exports = validateSemver 31 | -------------------------------------------------------------------------------- /script/lib/util.js: -------------------------------------------------------------------------------- 1 | const came = str => `${str}`.replace(/-\D/g, match => match.charAt(1).toUpperCase()) 2 | 3 | const hyphenate = str => `${str}`.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) 4 | 5 | module.exports = { 6 | came, 7 | hyphenate, 8 | } 9 | -------------------------------------------------------------------------------- /script/plugin-get-config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | const ejs = require('ejs') 6 | const getAllPlugin = require('./lib/plugin-get-all') 7 | 8 | const targetDir = path.resolve(__dirname, '../src/config/stage/plugin.js') 9 | const pluginsPath = path.resolve(__dirname, '../src/plugin') 10 | const templatePath = path.resolve(__dirname, './template/plugin-stage-config.js.ejs') 11 | 12 | // eslint-disable-next-line 13 | console.log(chalk.green('配置插件...')) 14 | 15 | const template = fs.readFileSync(templatePath, 'utf8') 16 | const puginList = getAllPlugin(pluginsPath) 17 | const result = ejs.render(template, { plugins: puginList }) 18 | 19 | fs.writeFile(targetDir, result) 20 | 21 | // eslint-disable-next-line 22 | console.log(chalk.green(`插件配置完成: ${targetDir}\n`)) 23 | -------------------------------------------------------------------------------- /script/plugin-init.js: -------------------------------------------------------------------------------- 1 | // 手动添加完插件后执行此脚本进行初始化动作 2 | const fs = require('fs-extra') 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | const path = require('path') 5 | const chalk = require('chalk') 6 | const shell = require('shelljs') 7 | const inquirer = require('inquirer') 8 | const getAllPlugin = require('./lib/plugin-get-all') 9 | const semverValidate = require('./lib/semver-validate') 10 | const installDep = require('./lib/install-dep') 11 | 12 | const projectPackage = require('../package.json') 13 | 14 | const pluginsPath = path.resolve(__dirname, '../src/plugin') 15 | // 检测是否有插件文件夹 16 | if (!fs.existsSync(pluginsPath)) { 17 | console.log(chalk.red('未找到插件文件夹目录, 请确认 src 文件夹中是否有 plugins 目录')) 18 | process.exit(1) 19 | } 20 | 21 | const pluginList = getAllPlugin(pluginsPath) 22 | 23 | // 将数组 forEach 异步化 24 | async function asyncForEach(array, callback) { 25 | for (let index = 0; index < array.length; index++) { 26 | // eslint-disable-next-line 27 | await callback(array[index], index, array) 28 | } 29 | } 30 | 31 | // 监测 npm 是否已安装 32 | if (!shell.which('npm')) { 33 | console.log(chalk.red('检测到未安装 npm, 请先安装 npm 再重新执行, 查看: https://www.npmjs.com/get-npm')) 34 | process.exit(1) 35 | } 36 | 37 | async function handler() { 38 | const questions = [ 39 | { 40 | type: 'checkbox', 41 | name: 'plugin', 42 | choices: pluginList.map(item => ({ name: item.name, value: item })), 43 | message: '请选择需要初始化的插件\n', 44 | }, 45 | ] 46 | 47 | const { plugins } = await inquirer.prompt(questions) 48 | 49 | if (plugins.length === 0) { 50 | console.log('未选择需要初始化的插件') 51 | return 52 | } 53 | 54 | console.log(chalk.green(`开始初始化插件 ${plugins.map(item => item.name).join(', ')}`)) 55 | 56 | await asyncForEach(plugins, async ({ name, packageCtx }) => { 57 | console.log(`* 插件 ${name}`) 58 | const keys = ['dependencies', 'devDependencies'] 59 | let hasError = false 60 | 61 | await asyncForEach(keys, async key => { 62 | await asyncForEach(Object.keys(packageCtx[key]), async pkg => { 63 | const v1 = packageCtx[key][pkg] 64 | const v2 = projectPackage[key][pkg] 65 | if (v1 && v2) { 66 | if (!semverValidate(v1, v2)) { 67 | return 68 | } 69 | } 70 | try { 71 | await installDep(pkg, v1, projectPackage, key === 'devDependencies') 72 | } catch (e) { 73 | hasError = true 74 | console.log(chalk.red(e.message)) 75 | } 76 | // const result = await installDep(pkg, v1, projectPackage, (key === 'devDependencies')) 77 | // if (result instanceof Error) { 78 | // hasError = true 79 | // console.log(chalk.red(result.message)) 80 | // } 81 | }) 82 | }) 83 | 84 | if (hasError) { 85 | console.log(chalk.yellow('插件初始化结束, 但存在问题, 请手动解决')) 86 | } 87 | }) 88 | } 89 | 90 | handler().then(() => { 91 | // eslint-disable-next-line 92 | require('./plugin-get-config') 93 | }) 94 | -------------------------------------------------------------------------------- /script/template/plugin-stage-config.js.ejs: -------------------------------------------------------------------------------- 1 | // 本文件是自动生成, 请勿修改 2 | <% plugins.forEach(function(plugin){ %>import <%= plugin.camelCaseName %> from '@/plugin/<%= plugin.name %>/stage-config' 3 | <% }); %> 4 | const pluginsConfig = [ 5 | <% plugins.forEach(function(plugin){ %> <%= plugin.camelCaseName %>, 6 | <% }); %>] 7 | 8 | export default pluginsConfig 9 | -------------------------------------------------------------------------------- /script/template/plugin/README.md.ejs: -------------------------------------------------------------------------------- 1 | # 插件名: <%= title %>(<%= name %>) 2 | 3 | 插件描述, <%= title %> 用于处于xxx业务场景, 提供了xxx功能 4 | 5 | ## 舞台视口列表 6 | 7 | ### TestView 8 | 9 | 地址: `/<%= name %>/test.vue` 10 | 显示xxx内容, 可进行xxx操作 11 | 12 | ## 自由视口列表 13 | 14 | ### Test 15 | 16 | 属性: 17 | 18 | | Require | Name | Type | Default | Desc | 19 | |:-------:|:----:|:------:|:-------:|:----:| 20 | | true | name | String | val | 描述 | 21 | 22 | 事件: 23 | 24 | | Name | Argument | Desc | 25 | |:----:|:------------:|:----:| 26 | | name | 事件参数描述 | 描述 | 27 | 28 | 方法: 29 | 30 | | Name | Argument | Result | Desc | 31 | |:----:|:------------:|:----------:|:----:| 32 | | name | 方法参数描述 | 返回值描述 | 描述 | -------------------------------------------------------------------------------- /script/template/plugin/asset/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/script/template/plugin/asset/image/logo.png -------------------------------------------------------------------------------- /script/template/plugin/component/component.vue.ejs: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /script/template/plugin/package.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-plugin-<%= name %>", 3 | "title": "<%= title %>", 4 | "version": "<%= version %>", 5 | "_linVersion": "0.3.5", 6 | "description": "<%= description %>", 7 | "author": "<%= author %>", 8 | "dependencies": {}, 9 | "devDependencies": {} 10 | } -------------------------------------------------------------------------------- /script/template/plugin/stage-config.js.ejs: -------------------------------------------------------------------------------- 1 | const <%= camelCaseName %>Router = { 2 | route: null, 3 | name: null, 4 | title: '<%= title %>', 5 | type: 'folder', 6 | icon: 'iconfont icon-demo', 7 | filePath: 'views/<%= name %>/', 8 | order: null, 9 | inNav: true, 10 | children: [ 11 | { 12 | title: '舞台页面', 13 | type: 'view', 14 | name: '<%= name %>Stage1', 15 | route: '/<%= name %>/stage1', 16 | filePath: 'plugin/<%= name %>/view/stage1.vue', 17 | inNav: true, 18 | icon: 'iconfont icon-demo', 19 | right: null, 20 | }, 21 | { 22 | title: '舞台页面', 23 | type: 'view', 24 | name: '<%= name %>Stage2', 25 | route: '/<%= name %>/stage2', 26 | filePath: 'plugin/<%= name %>/view/stage2.vue', 27 | inNav: true, 28 | icon: 'iconfont icon-demo', 29 | right: null, 30 | }, 31 | ], 32 | } 33 | 34 | export default <%= camelCaseName %>Router 35 | -------------------------------------------------------------------------------- /script/template/plugin/view/stage1.vue.ejs: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | 51 | -------------------------------------------------------------------------------- /script/template/plugin/view/stage2.vue.ejs: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /src/assets/image/about/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/avatar.png -------------------------------------------------------------------------------- /src/assets/image/about/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/header-bg.png -------------------------------------------------------------------------------- /src/assets/image/about/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/icon.png -------------------------------------------------------------------------------- /src/assets/image/about/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/icon1.png -------------------------------------------------------------------------------- /src/assets/image/about/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/icon2.png -------------------------------------------------------------------------------- /src/assets/image/about/icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/icon3.png -------------------------------------------------------------------------------- /src/assets/image/about/icon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/icon4.png -------------------------------------------------------------------------------- /src/assets/image/about/open-source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/open-source.jpg -------------------------------------------------------------------------------- /src/assets/image/about/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/qrcode.jpg -------------------------------------------------------------------------------- /src/assets/image/about/team-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/team-icon.png -------------------------------------------------------------------------------- /src/assets/image/about/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/about/welcome.png -------------------------------------------------------------------------------- /src/assets/image/error-page/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/error-page/404.png -------------------------------------------------------------------------------- /src/assets/image/error-page/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/error-page/logo.png -------------------------------------------------------------------------------- /src/assets/image/login/login-ba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/login/login-ba.png -------------------------------------------------------------------------------- /src/assets/image/login/login-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/login/login-btn.png -------------------------------------------------------------------------------- /src/assets/image/login/nickname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/login/nickname.png -------------------------------------------------------------------------------- /src/assets/image/login/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/login/password.png -------------------------------------------------------------------------------- /src/assets/image/login/team-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/login/team-name.png -------------------------------------------------------------------------------- /src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/logo.png -------------------------------------------------------------------------------- /src/assets/image/mobile-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/mobile-logo.png -------------------------------------------------------------------------------- /src/assets/image/user/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/user/corner.png -------------------------------------------------------------------------------- /src/assets/image/user/user-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/user/user-bg.png -------------------------------------------------------------------------------- /src/assets/image/user/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/user/user.jpg -------------------------------------------------------------------------------- /src/assets/image/user/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/image/user/user.png -------------------------------------------------------------------------------- /src/assets/style/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/style/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/style/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/assets/style/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/assets/style/index.scss: -------------------------------------------------------------------------------- 1 | @import './realize/reset'; 2 | @import './realize/animation'; 3 | @import './realize/transition'; 4 | 5 | .lin-container { 6 | .el-divider--horizontal { 7 | margin:0px; 8 | } 9 | .lin-title { 10 | height: 59px; 11 | line-height: 59px; 12 | color: $parent-title-color; 13 | font-size: 16px; 14 | font-weight: 500; 15 | text-indent: 40px; 16 | border-bottom: 1px solid #dae1ed; 17 | } 18 | 19 | .lin-wrap { 20 | padding: 20px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/style/realize/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade { 2 | from { 3 | opacity: 0.1; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | @keyframes img-add-view { 11 | 0% { 12 | opacity: 0; 13 | transform: scale(0.8); 14 | } 15 | 100% { 16 | opacity: 1; 17 | transform: scale(1); 18 | } 19 | } 20 | 21 | // 添加单品 22 | @keyframes arr-item-add { 23 | 0% { 24 | opacity: 0; 25 | transform: translateY(-100%); 26 | } 27 | 100% { 28 | opacity: 1; 29 | transform: translateY(0%); 30 | } 31 | } 32 | 33 | // 删除单品 34 | @keyframes arr-item-reduce { 35 | 0% { 36 | opacity: 1; 37 | transform: translateY(0%); 38 | } 39 | 100% { 40 | opacity: 0; 41 | transform: translateY(100%); 42 | } 43 | } 44 | 45 | // 页面切换动画 46 | .fade-transform-leave-active, 47 | .fade-transform-enter-active { 48 | transition: all 0.3s; 49 | } 50 | .fade-transform-enter { 51 | opacity: 0; 52 | transform: translateX(-30px); 53 | } 54 | .fade-transform-leave-to { 55 | opacity: 0; 56 | transform: translateX(30px); 57 | } 58 | 59 | .fadeChild-transform-leave-active, 60 | .fadeChild-transform-enter-active { 61 | transition: all 0.3s; 62 | } 63 | .fadeChild-transform-enter { 64 | opacity: 0; 65 | transform: translateX(-30px); 66 | } 67 | .fadeChild-transform-leave-to { 68 | opacity: 0; 69 | transform: translateX(30px); 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/style/realize/mixin.scss: -------------------------------------------------------------------------------- 1 | 2 | // 根据不同的屏幕加载背景图片 3 | @mixin bi($url, $type: 'png') { 4 | background-image: url($url + "@2x." + $type); 5 | @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) { 6 | background-image: url($url + "@3x." + $type); 7 | background-size: cover; 8 | } 9 | } 10 | 11 | // 文字超出后以...显示 12 | @mixin no-wrap { 13 | text-overflow: ellipsis; 14 | overflow: hidden; 15 | white-space: nowrap; 16 | } 17 | 18 | // 扩展小图标按钮的点击区域 19 | @mixin extend-click() { 20 | position: relative; 21 | &:before { 22 | content: ''; 23 | position: absolute; 24 | top: -10px; 25 | left: -10px; 26 | right: -10px; 27 | bottom: -10px; 28 | } 29 | } 30 | 31 | @mixin btn-scale-hover(){ 32 | transition: .4s all; 33 | &:hover{ 34 | transform: scale(1.1); 35 | } 36 | } 37 | 38 | @mixin bi-saturate-hover($background-color, $value: 20){ // 背景颜色增加饱和度 39 | transition: .6s all; 40 | &:hover{ 41 | background-color: saturate($background-color, $value); 42 | } 43 | } 44 | 45 | @mixin color-saturate-hover($color, $value: 30){ // 颜色增加饱和度 46 | transition: .6s all; 47 | &:hover{ 48 | color: saturate($color, $value); 49 | } 50 | } 51 | 52 | @mixin bi-lighten-hover($background-color, $value: 10){ // 背景颜色变浅。 53 | transition: .6s all; 54 | &:hover{ 55 | background-color: lighten($background-color, $value); 56 | } 57 | } 58 | 59 | @mixin bi-opacity-hover($opacity: .8){ // 透明度 60 | transition: .4s all; 61 | &:hover{ 62 | opacity: $opacity; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/assets/style/realize/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | menu, 72 | nav, 73 | output, 74 | ruby, 75 | section, 76 | summary, 77 | time, 78 | mark, 79 | audio, 80 | video, 81 | input { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 87 | Microsoft YaHei, sans-serif; 88 | font-weight: normal; 89 | vertical-align: baseline; 90 | } 91 | 92 | article, 93 | aside, 94 | details, 95 | figcaption, 96 | figure, 97 | footer, 98 | header, 99 | menu, 100 | nav, 101 | section { 102 | display: block; 103 | } 104 | 105 | html, 106 | body, 107 | #app { 108 | width: 100%; 109 | height: 100%; 110 | } 111 | 112 | blockquote:before, 113 | blockquote:after, 114 | q:before, 115 | q:after { 116 | content: none; 117 | } 118 | 119 | table { 120 | border-collapse: collapse; 121 | border-spacing: 0; 122 | } 123 | 124 | input, 125 | button { 126 | padding: 0; 127 | &:focus { 128 | outline: -webkit-focus-ring-color auto 0px; 129 | } 130 | } 131 | 132 | /* custom */ 133 | 134 | a { 135 | color: #7e8c8d; 136 | -webkit-backface-visibility: hidden; 137 | text-decoration: none; 138 | cursor: pointer; 139 | } 140 | 141 | li { 142 | list-style: none; 143 | } 144 | 145 | img { 146 | display: block; 147 | width: 100%; 148 | height: auto; 149 | } 150 | 151 | body { 152 | line-height: 1; 153 | -moz-osx-font-smoothing: grayscale; 154 | -webkit-font-smoothing: antialiased; 155 | text-rendering: optimizeLegibility; 156 | } 157 | 158 | //*, 159 | //*:before, 160 | //*:after { 161 | // box-sizing: inherit; 162 | //} 163 | 164 | a:focus, 165 | a:active { 166 | outline: none; 167 | } 168 | 169 | a, 170 | a:focus, 171 | a:hover { 172 | cursor: pointer; 173 | color: inherit; 174 | text-decoration: none; 175 | } 176 | 177 | .clearfix { 178 | &:after { 179 | visibility: hidden; 180 | display: block; 181 | font-size: 0; 182 | content: ' '; 183 | clear: both; 184 | height: 0; 185 | } 186 | } 187 | 188 | // 饿了么主题 189 | 190 | /* 改变主题色变量 */ 191 | $--color-primary: #4c76af; 192 | /* 改变 icon 字体路径变量,必需 */ 193 | $--font-path: '~element-plus/lib/theme-chalk/fonts'; 194 | // @import '../../../../node_modules/element-plus/packages/theme-chalk/src/index'; 195 | -------------------------------------------------------------------------------- /src/assets/style/realize/transition.scss: -------------------------------------------------------------------------------- 1 | 2 | .fade-enter-active, .fade-leave-active { 3 | transition: opacity .5s; 4 | } 5 | .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { 6 | opacity: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/style/realize/variable.scss: -------------------------------------------------------------------------------- 1 | $theme: #3963bc; 2 | 3 | /* 布局 */ 4 | $sidebar-width: 210px; 5 | $sidebar-background: #192a5e; 6 | 7 | $navbar-height: 30px; 8 | $navbar-padding: 20px; 9 | $header-height: 86px; 10 | $reuse-tab-height: 40px; 11 | 12 | $navbar-background: #BECCD8; 13 | $app-main-background: #F9FAFB; 14 | $header-background: #EEF4F9; 15 | $right-side-font-color: #666666; 16 | $reuse-tab-item-background: #FFFFFF; 17 | 18 | $title-color: #45526b; 19 | $parent-title-color: #3963bc; 20 | $table-border-color: #dee2e6; 21 | 22 | /* 菜单 */ 23 | $menu-item-hover: #0a1949; 24 | $menu-item-bg: #122150; 25 | $sub-menu-title: #c4c9d2; 26 | $menu-item-height: 50px; 27 | -------------------------------------------------------------------------------- /src/assets/style/shared.scss: -------------------------------------------------------------------------------- 1 | @import './realize/mixin.scss'; 2 | @import './realize/variable.scss'; -------------------------------------------------------------------------------- /src/component/base/date-picker/lin-date-picker.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 69 | -------------------------------------------------------------------------------- /src/component/base/dialog/lin-dialog.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/component/base/dropdown/lin-dropdown.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | 34 | 40 | -------------------------------------------------------------------------------- /src/component/base/icon/lin-icon.vue: -------------------------------------------------------------------------------- 1 | 6 | 29 | 37 | -------------------------------------------------------------------------------- /src/component/base/search/lin-search.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 45 | 60 | -------------------------------------------------------------------------------- /src/component/base/source-code/source-code.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 49 | -------------------------------------------------------------------------------- /src/component/base/sticky-top/sticky-top.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/component/base/tinymce/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 102 | 103 | 108 | -------------------------------------------------------------------------------- /src/component/layout/app-main.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /src/component/layout/avatar.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 102 | -------------------------------------------------------------------------------- /src/component/layout/back-top.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 62 | 63 | 81 | -------------------------------------------------------------------------------- /src/component/layout/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 62 | -------------------------------------------------------------------------------- /src/component/layout/clear-tab.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/component/layout/index.js: -------------------------------------------------------------------------------- 1 | import NavBar from './nav-bar' 2 | import Sidebar from './sidebar/sidebar.vue' 3 | import AppMain from './app-main' 4 | import ReuseTab from './reuse-tab' 5 | import MenuTab from './menu-tab.vue' 6 | import BackTop from './back-top.vue' 7 | import User from './user.vue' 8 | 9 | export { NavBar, Sidebar, AppMain, ReuseTab, MenuTab, BackTop, User } 10 | -------------------------------------------------------------------------------- /src/component/layout/menu-tab.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | 91 | -------------------------------------------------------------------------------- /src/component/layout/nav-bar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 91 | 92 | 118 | -------------------------------------------------------------------------------- /src/component/layout/screen-full.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 49 | 50 | 75 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/logo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 64 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/menu-tree.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 51 | 52 | 75 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/search.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 93 | 94 | 115 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/sidebar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 58 | 59 | 74 | -------------------------------------------------------------------------------- /src/component/notify/emitter.js: -------------------------------------------------------------------------------- 1 | class Emitter { 2 | constructor() { 3 | this.listeners = new Map() 4 | } 5 | 6 | addListener(label, callback, vm) { 7 | if (typeof callback === 'function') { 8 | // eslint-disable-next-line no-unused-expressions 9 | this.listeners.has(label) || this.listeners.set(label, []) 10 | this.listeners.get(label).push({ callback, vm }) 11 | return true 12 | } 13 | return false 14 | } 15 | 16 | removeListener(label, callback, vm) { 17 | const listeners = this.listeners.get(label) 18 | let index 19 | 20 | if (listeners?.length) { 21 | index = listeners.reduce((i, listener, index) => { 22 | if (typeof listener.callback === 'function' && listener.callback === callback && listener.vm === vm) { 23 | // eslint-disable-next-line no-param-reassign 24 | i = index 25 | } 26 | return i 27 | }, -1) 28 | 29 | if (index > -1) { 30 | listeners.splice(index, 1) 31 | this.listeners.set(label, listeners) 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | emit(label, ...args) { 39 | const listeners = this.listeners.get(label) 40 | 41 | if (listeners?.length) { 42 | listeners.forEach(listener => { 43 | listener.callback.call(listener.vm, ...args) 44 | }) 45 | return true 46 | } 47 | return false 48 | } 49 | } 50 | 51 | export default new Emitter() 52 | -------------------------------------------------------------------------------- /src/component/notify/index.js: -------------------------------------------------------------------------------- 1 | /* Author: https://github.com/nathantsoi/vue-native-websocket */ 2 | import Notify from './notify.vue' 3 | import Observer from './observer' 4 | import Emitter from './emitter' 5 | 6 | export default { 7 | install(app, connection, opts = {}) { 8 | if (typeof connection === 'object') { 9 | // eslint-disable-next-line no-param-reassign 10 | opts = connection 11 | // eslint-disable-next-line no-param-reassign 12 | connection = '' 13 | } 14 | let observer = null 15 | 16 | opts.$setInstance = wsInstance => { 17 | app.config.globalProperties.$socket = wsInstance 18 | } 19 | app.config.globalProperties.$connect = (connectionUrl = connection, connectionOpts = opts) => { 20 | connectionOpts.$setInstance = opts.$setInstance 21 | observer = new Observer(connectionUrl, connectionOpts) 22 | app.config.globalProperties.$socket = observer.WebSocket 23 | } 24 | 25 | app.config.globalProperties.$disconnect = () => { 26 | if (observer?.reconnection) { 27 | observer.reconnection = false 28 | } 29 | if (app.config.globalProperties.$socket) { 30 | app.config.globalProperties.$socket.close() 31 | delete app.config.globalProperties.$socket 32 | } 33 | } 34 | const hasProxy = typeof Proxy !== 'undefined' && typeof Proxy === 'function' && /native code/.test(Proxy.toString()) 35 | app.component('LinNotify', Notify) 36 | app.mixin({ 37 | created() { 38 | const vm = this 39 | const { sockets } = this.$options 40 | 41 | if (hasProxy) { 42 | this.$options.sockets = new Proxy( 43 | {}, 44 | { 45 | set(target, key, value) { 46 | Emitter.addListener(key, value, vm) 47 | target[key] = value 48 | return true 49 | }, 50 | deleteProperty(target, key) { 51 | Emitter.removeListener(key, vm.$options.sockets[key], vm) 52 | delete target.key 53 | return true 54 | }, 55 | }, 56 | ) 57 | if (sockets) { 58 | Object.keys(sockets).forEach(key => { 59 | this.$options.sockets[key] = sockets[key] 60 | }) 61 | } 62 | } else { 63 | Object.seal(this.$options.sockets) 64 | 65 | // if !hasProxy need addListener 66 | if (sockets) { 67 | Object.keys(sockets).forEach(key => { 68 | Emitter.addListener(key, sockets[key], vm) 69 | }) 70 | } 71 | } 72 | }, 73 | beforeUnmount() { 74 | if (hasProxy) { 75 | const { sockets } = this.$options 76 | 77 | if (sockets) { 78 | Object.keys(sockets).forEach(key => { 79 | delete this.$options.sockets[key] 80 | }) 81 | } 82 | } 83 | }, 84 | }) 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/component/notify/observer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import Emitter from './emitter' 3 | 4 | export default class { 5 | constructor(connectionUrl, opts = {}) { 6 | this.format = opts.format && opts.format.toLowerCase() 7 | 8 | if (connectionUrl.startsWith('//')) { 9 | const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws' 10 | connectionUrl = `${scheme}:${connectionUrl}` 11 | } 12 | 13 | this.connectionUrl = connectionUrl 14 | this.opts = opts 15 | 16 | this.reconnection = this.opts.reconnection || false 17 | this.reconnectionAttempts = this.opts.reconnectionAttempts || Infinity 18 | this.reconnectionDelay = this.opts.reconnectionDelay || 1000 19 | this.reconnectTimeoutId = 0 20 | this.reconnectionCount = 0 21 | 22 | this.passToStoreHandler = this.opts.passToStoreHandler || false 23 | 24 | this.connect(connectionUrl, opts) 25 | 26 | if (opts.store) { 27 | this.store = opts.store 28 | } 29 | if (opts.mutations) { 30 | this.mutations = opts.mutations 31 | } 32 | this.onEvent() 33 | } 34 | 35 | connect(connectionUrl, opts = {}) { 36 | const protocol = opts.protocol || '' 37 | this.WebSocket = 38 | opts.WebSocket || (protocol === '' ? new WebSocket(connectionUrl) : new WebSocket(connectionUrl, protocol)) 39 | if (this.format === 'json') { 40 | if (!('sendObj' in this.WebSocket)) { 41 | this.WebSocket.sendObj = obj => this.WebSocket.send(JSON.stringify(obj)) 42 | } 43 | } 44 | 45 | return this.WebSocket 46 | } 47 | 48 | reconnect() { 49 | if (this.reconnectionCount <= this.reconnectionAttempts) { 50 | this.reconnectionCount++ 51 | clearTimeout(this.reconnectTimeoutId) 52 | 53 | this.reconnectTimeoutId = setTimeout(() => { 54 | if (this.store) { 55 | this.passToStore('SOCKET_RECONNECT', this.reconnectionCount) 56 | } 57 | 58 | this.connect(this.connectionUrl, this.opts) 59 | this.onEvent() 60 | }, this.reconnectionDelay) 61 | } else if (this.store) { 62 | this.passToStore('SOCKET_RECONNECT_ERROR', true) 63 | } 64 | } 65 | 66 | onEvent() { 67 | ;['onmessage', 'onclose', 'onerror', 'onopen'].forEach(eventType => { 68 | this.WebSocket[eventType] = event => { 69 | Emitter.emit(eventType, event) 70 | 71 | if (this.store) { 72 | this.passToStore(`SOCKET_${eventType}`, event) 73 | } 74 | 75 | if (this.reconnection && eventType === 'onopen') { 76 | this.opts.$setInstance(event.currentTarget) 77 | this.reconnectionCount = 0 78 | } 79 | 80 | if (this.reconnection && eventType === 'onclose') { 81 | this.reconnect() 82 | } 83 | } 84 | }) 85 | } 86 | 87 | passToStore(eventName, event) { 88 | if (this.passToStoreHandler) { 89 | this.passToStoreHandler(eventName, event, this.defaultPassToStore.bind(this)) 90 | } else { 91 | this.defaultPassToStore(eventName, event) 92 | } 93 | } 94 | 95 | defaultPassToStore(eventName, event) { 96 | if (!eventName.startsWith('SOCKET_')) { 97 | return 98 | } 99 | let method = 'commit' 100 | let target = eventName.toUpperCase() 101 | let msg = event 102 | if (this.format === 'json' && event.data) { 103 | msg = JSON.parse(event.data) 104 | if (msg.mutation) { 105 | target = [msg.namespace || '', msg.mutation].filter(e => !!e).join('/') 106 | } else if (msg.action) { 107 | method = 'dispatch' 108 | target = [msg.namespace || '', msg.action].filter(e => !!e).join('/') 109 | } 110 | } 111 | if (this.mutations) { 112 | target = this.mutations[target] || target 113 | } 114 | this.store[method](target, msg) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/config/error-code.js: -------------------------------------------------------------------------------- 1 | const errorCode = { 2 | 777: '前端错误码未定义', 3 | 999: '服务器未知错误', 4 | 10000: '未携带令牌', 5 | 10020: '资源不存在', 6 | 10030: '参数错误', 7 | 10041: 'assessToken损坏', 8 | 10042: 'refreshToken损坏', 9 | 10051: 'assessToken过期', 10 | 10052: 'refreshToken过期', 11 | 10060: '字段重复', 12 | 10070: '不可操作', 13 | } 14 | 15 | export default errorCode 16 | -------------------------------------------------------------------------------- /src/config/global.js: -------------------------------------------------------------------------------- 1 | window.MAX_SUCCESS_CODE = 9998 2 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const Config = { 2 | sidebarLevel: 3, // 侧边栏层级(无限级)限制, 默认为 3 级,可根据需求自行扩充 3 | openAutoJumpOut: true, // 是否开启无操作跳出 4 | defaultRoute: '/about', // 默认打开的路由 5 | websocketEnable: false, // 默认关闭 websocket 6 | showSidebarSearch: true, // 默认打开侧边栏搜索 7 | notLoginRoute: ['login'], // 无需登录即可访问的路由 name, 8 | useFrontEndErrorMsg: false, // 默认采用后端返回异常 9 | stagnateTime: 60 * 60 * 1000, // 无操作停滞时间,默认1小时 10 | baseURL: process.env.VUE_APP_BASE_URL, // API接口baseURL,在根目录.env文件查找对应环境变量配置 11 | } 12 | 13 | export default Config 14 | -------------------------------------------------------------------------------- /src/config/stage/admin.js: -------------------------------------------------------------------------------- 1 | const adminRouter = { 2 | route: null, 3 | name: null, 4 | title: '权限管理', 5 | type: 'folder', 6 | icon: 'iconfont icon-huiyuanguanli', 7 | isElementIcon: false, 8 | filePath: 'view/admin/', 9 | order: null, 10 | inNav: true, 11 | permission: ['超级管理员独有权限'], 12 | children: [ 13 | { 14 | route: '/admin/user', 15 | name: null, 16 | title: '用户管理', 17 | type: 'folder', // 取 route 为默认加载页 18 | icon: 'iconfont icon-huiyuanguanli', 19 | isElementIcon: false, 20 | filePath: 'view/admin/user/', 21 | inNav: true, 22 | children: [ 23 | { 24 | title: '用户列表', 25 | type: 'view', 26 | name: 'userList', 27 | route: '/admin/user/list', 28 | filePath: 'view/admin/user/user-list.vue', 29 | inNav: true, 30 | icon: 'iconfont icon-huiyuanguanli', 31 | isElementIcon: false, 32 | permission: ['超级管理员独有权限'], 33 | }, 34 | { 35 | title: '添加用户', 36 | type: 'view', 37 | inNav: true, 38 | route: '/admin/user/add', 39 | icon: 'iconfont icon-add', 40 | isElementIcon: false, 41 | name: 'UserCreate', 42 | filePath: 'view/admin/user/user-create.vue', 43 | permission: ['超级管理员独有权限'], 44 | }, 45 | ], 46 | }, 47 | { 48 | route: '/admin/group/list', 49 | name: null, 50 | title: '分组管理', 51 | type: 'tab', // 取 route 为默认加载页 52 | icon: 'iconfont icon-yunyingguanli_fuwufenzuguanli', 53 | filePath: 'view/admin/group', 54 | inNav: true, 55 | children: [ 56 | { 57 | route: '/admin/group/list', 58 | type: 'view', 59 | name: 'groupList', 60 | inNav: true, 61 | filePath: 'view/admin/group/group-list.vue', 62 | title: '分组列表', 63 | icon: 'iconfont icon-huiyuanguanli', 64 | permission: ['超级管理员独有权限'], 65 | }, 66 | { 67 | route: '/admin/group/add', 68 | type: 'view', 69 | name: 'GroupCreate', 70 | filePath: 'view/admin/group/group-create.vue', 71 | inNav: true, 72 | title: '添加分组', 73 | icon: 'iconfont icon-add', 74 | permission: ['超级管理员独有权限'], 75 | }, 76 | { 77 | route: '/admin/group/edit', 78 | type: 'view', 79 | name: 'GroupEdit', 80 | filePath: 'view/admin/group/group-edit.vue', 81 | inNav: false, 82 | title: '修改分组', 83 | icon: 'iconfont icon-add', 84 | permission: ['超级管理员独有权限'], 85 | }, 86 | ], 87 | }, 88 | ], 89 | } 90 | 91 | export default adminRouter 92 | -------------------------------------------------------------------------------- /src/config/stage/book.js: -------------------------------------------------------------------------------- 1 | const bookRouter = { 2 | route: null, 3 | name: null, 4 | title: '图书管理', 5 | type: 'folder', // 类型: folder, tab, view 6 | icon: 'iconfont icon-tushuguanli', 7 | isElementIcon: false, 8 | filePath: 'view/book/', // 文件路径 9 | order: null, 10 | inNav: true, 11 | children: [ 12 | { 13 | title: '图书列表', 14 | type: 'view', 15 | name: 'BookCreate', 16 | route: '/book/list', 17 | filePath: 'view/book/book-list.vue', 18 | inNav: true, 19 | icon: 'iconfont icon-tushuguanli', 20 | isElementIcon: false, 21 | }, 22 | { 23 | title: '添加图书', 24 | type: 'view', 25 | name: 'BookCreate', 26 | route: '/book/add', 27 | filePath: 'view/book/book.vue', 28 | inNav: true, 29 | icon: 'iconfont icon-add', 30 | isElementIcon: false, 31 | }, 32 | ], 33 | } 34 | 35 | export default bookRouter 36 | -------------------------------------------------------------------------------- /src/config/stage/center.js: -------------------------------------------------------------------------------- 1 | const centerRouter = { 2 | route: null, 3 | name: null, 4 | title: '个人', 5 | type: 'view', // 类型: folder, tab, view 6 | icon: 'iconfont icon-tushuguanli', 7 | isElementIcon: false, 8 | filePath: 'view/center/', // 文件路径 9 | order: null, 10 | inNav: false, 11 | } 12 | 13 | export default centerRouter 14 | -------------------------------------------------------------------------------- /src/config/stage/index.js: -------------------------------------------------------------------------------- 1 | import Utils from '@/lin/util/util' 2 | import adminConfig from './admin' 3 | import bookConfig from './book' // 引入图书管理路由文件 4 | import pluginsConfig from './plugin' 5 | 6 | // eslint-disable-next-line import/no-mutable-exports 7 | let homeRouter = [ 8 | { 9 | title: '林间有风', 10 | type: 'view', 11 | name: Symbol('about'), 12 | route: '/about', 13 | filePath: 'view/about/about.vue', 14 | inNav: true, 15 | icon: 'iconfont icon-iconset0103', 16 | isElementIcon: false, 17 | order: 1, 18 | }, 19 | { 20 | title: '日志管理', 21 | type: 'view', 22 | name: Symbol('log'), 23 | route: '/log', 24 | filePath: 'view/log/log.vue', 25 | inNav: true, 26 | icon: 'iconfont icon-rizhiguanli', 27 | isElementIcon: false, 28 | order: 2, 29 | permission: ['查询所有日志'], 30 | }, 31 | { 32 | title: '个人中心', 33 | type: 'view', 34 | name: Symbol('center'), 35 | route: '/center', 36 | filePath: 'view/center/center.vue', 37 | inNav: false, 38 | icon: 'iconfont icon-rizhiguanli', 39 | isElementIcon: false, 40 | }, 41 | { 42 | title: '404', 43 | type: 'view', 44 | name: Symbol('404'), 45 | route: '/404', 46 | filePath: 'view/error-page/404.vue', 47 | inNav: false, 48 | icon: 'iconfont icon-rizhiguanli', 49 | isElementIcon: false, 50 | }, 51 | bookConfig, 52 | adminConfig, 53 | ] 54 | 55 | // 接入插件 56 | const plugins = [...pluginsConfig] 57 | filterPlugin(homeRouter) 58 | homeRouter = homeRouter.concat(plugins) 59 | 60 | // 处理顺序 61 | homeRouter = Utils.sortByOrder(homeRouter) 62 | deepReduceName(homeRouter) 63 | 64 | export default homeRouter 65 | 66 | /** 67 | * 筛除已经被添加的插件 68 | */ 69 | function filterPlugin(data) { 70 | if (plugins.length === 0) { 71 | return 72 | } 73 | if (Array.isArray(data)) { 74 | data.forEach(item => { 75 | filterPlugin(item) 76 | }) 77 | } else { 78 | const findResult = plugins.findIndex(item => data === item) 79 | if (findResult >= 0) { 80 | plugins.splice(findResult, 1) 81 | } 82 | if (data.children) { 83 | filterPlugin(data.children) 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * 使用 Symbol 处理 name 字段, 保证唯一性 90 | */ 91 | function deepReduceName(target) { 92 | if (Array.isArray(target)) { 93 | target.forEach(item => { 94 | if (typeof item !== 'object') { 95 | return 96 | } 97 | deepReduceName(item) 98 | }) 99 | return 100 | } 101 | if (typeof target === 'object') { 102 | if (typeof target.name !== 'symbol') { 103 | target.name = target.name || Utils.getRandomStr() 104 | target.name = Symbol(target.name) 105 | } 106 | 107 | if (Array.isArray(target.children)) { 108 | target.children.forEach(item => { 109 | if (typeof item !== 'object') { 110 | return 111 | } 112 | deepReduceName(item) 113 | }) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/config/stage/plugin.js: -------------------------------------------------------------------------------- 1 | // 本文件是自动生成, 请勿修改 2 | import custom from '@/plugin/custom/stage-config' 3 | import linCmsUi from '@/plugin/lin-cms-ui/stage-config' 4 | 5 | const pluginsConfig = [custom, linCmsUi] 6 | 7 | export default pluginsConfig 8 | -------------------------------------------------------------------------------- /src/lin/context/admin.js: -------------------------------------------------------------------------------- 1 | import { provide, ref, inject } from 'vue' 2 | 3 | // eslint-disable-next-line symbol-description 4 | const adminSymbol = Symbol() 5 | 6 | export const useAdminProvide = () => { 7 | const flag = ref(false) 8 | provide(adminSymbol, flag) 9 | } 10 | 11 | export const useAdminInject = () => { 12 | const adminContext = inject(adminSymbol) 13 | 14 | return adminContext 15 | } 16 | -------------------------------------------------------------------------------- /src/lin/context/index.js: -------------------------------------------------------------------------------- 1 | import { useAdminProvide, useAdminInject } from './admin' 2 | 3 | export { useAdminInject } 4 | 5 | export const useProvide = () => { 6 | useAdminProvide() 7 | } 8 | -------------------------------------------------------------------------------- /src/lin/directive/authorize.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * 判断是否允许访问该DOM 5 | * @param {*} permission 权限 6 | * @param {*} user 当前用户实例 7 | * @param {*} permissions 当前管理员所在分组权限集 8 | */ 9 | function isAllowed(permission, user, permissions) { 10 | if (user.admin) return true 11 | 12 | if (typeof permission === 'string') { 13 | return permissions.includes(permission) 14 | } 15 | if (permission instanceof Array) { 16 | return permission.some(auth => permissions.indexOf(auth) >= 0) 17 | } 18 | return false 19 | } 20 | 21 | export default { 22 | beforeMount(el, binding) { 23 | let type 24 | let permission 25 | const element = el 26 | 27 | if (Object.prototype.toString.call(binding.value) === '[object Object]') { 28 | ;({ permission } = binding.value); 29 | ({ type } = binding.value) 30 | } else { 31 | permission = binding.value 32 | } 33 | const isAllow = isAllowed(permission, store.state.user || {}, store.state.permissions) 34 | if (!isAllow && permission) { 35 | if (type) { 36 | element.disabled = true 37 | element.style.opacity = 0.4 38 | element.style.cursor = 'not-allowed' 39 | } else { 40 | element.style.display = 'none' 41 | } 42 | } 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /src/lin/directive/index.js: -------------------------------------------------------------------------------- 1 | import './authorize' 2 | -------------------------------------------------------------------------------- /src/lin/filter/index.js: -------------------------------------------------------------------------------- 1 | import Utils from '../util/util' 2 | /* 3 | * 全局的过滤函数 4 | * */ 5 | export function checkAddZone(num) { 6 | return num < 10 ? `0${num.toString()}` : num 7 | } 8 | 9 | export const filters = { 10 | filterAddress(value) { 11 | // 过滤地址 12 | if (!value) return value 13 | const obj = value 14 | return `${obj.provinceName}${obj.cityName}${obj.countyName} ${obj.detailInfo}` 15 | }, 16 | 17 | filterTime(value) { 18 | // 过滤时间戳,返回值yyyy-mm-dd 19 | if (!value) { 20 | return value 21 | } 22 | const date = new Date(value * 1000) 23 | const y = 1900 + date.getYear() 24 | const m = `0${date.getMonth() + 1}` 25 | const d = `0${date.getDate()}` 26 | const val = `${y}-${m.substring(m.length - 2, m.length)}-${d.substring(d.length - 2, d.length)}` 27 | return val 28 | }, 29 | 30 | filterTimeYmdHms(value) { 31 | // 过滤时间戳,返回值yyyy-mm-dd ss 32 | if (!value) { 33 | return value 34 | } 35 | const date = new Date(value * 1000) 36 | const y = 1900 + date.getYear() 37 | const m = `0${date.getMonth() + 1}` 38 | const d = `0${date.getDate()}` 39 | const hh = date.getHours() 40 | const mm = `${date.getMinutes()}` 41 | const ss = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds() 42 | const val = `${y}-${m.substring(m.length - 2, m.length)}-${d.substring(d.length - 2, d.length)} ${hh}:${mm}:${ss}` 43 | return val 44 | }, 45 | 46 | filterTimeYear(value) { 47 | // 过滤时间戳, 返回值 今年:mm-dd 往年:yyyy-mm-dd 48 | const jy = 1900 + new Date().getYear() 49 | const date = new Date(value * 1000) 50 | const y = 1900 + date.getYear() 51 | const m = `0${date.getMonth() + 1}` 52 | const d = `0${date.getDate()}` 53 | const val = `${y}-${m.substring(m.length - 2, m.length)}-${d.substring(d.length - 2, d.length)}` 54 | const thisYear = `${m.substring(m.length - 2, m.length)}-${d.substring(d.length - 2, d.length)}` 55 | if (jy === y) { 56 | return thisYear 57 | } 58 | return val 59 | }, 60 | 61 | dateFormatter(nows) { 62 | if (!nows) return '' 63 | const now = new Date(nows) 64 | const year = now.getFullYear() 65 | 66 | let month = now.getMonth() + 1 67 | month = checkAddZone(month) 68 | 69 | let date = now.getDate() 70 | date = checkAddZone(date) 71 | return `${year}-${month}-${date}` 72 | }, 73 | 74 | dateTimeFormatter(t) { 75 | if (!t) return '' 76 | t = new Date(t).getTime() // eslint-disable-line 77 | t = new Date(t) // eslint-disable-line 78 | const year = t.getFullYear() 79 | let month = t.getMonth() + 1 80 | month = checkAddZone(month) 81 | 82 | let date = t.getDate() 83 | date = checkAddZone(date) 84 | 85 | let hour = t.getHours() 86 | hour = checkAddZone(hour) 87 | 88 | let min = t.getMinutes() 89 | min = checkAddZone(min) 90 | 91 | let se = t.getSeconds() 92 | se = checkAddZone(se) 93 | 94 | return `${year}-${month}-${date} ${hour}:${min}:${se}` 95 | }, 96 | 97 | filterTitle(value, len = 9) { 98 | return Utils.cutString(value, len) 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /src/lin/model/admin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { post, get, put, _delete } from '@/lin/plugin/axios' 3 | 4 | export default class Admin { 5 | constructor(uPage = 0, uCount = 10, gPage = 0, gCount = 5) { 6 | this.uPage = uPage 7 | this.uCount = uCount 8 | this.lPage = gPage 9 | this.gCount = gCount 10 | } 11 | 12 | async increaseUPage() { 13 | this.uPage += 1 14 | } 15 | 16 | async increaseGPage() { 17 | this.lPage += 1 18 | } 19 | 20 | async decreaseUPage() { 21 | this.uPage -= 1 22 | if (this.uPage < 0) { 23 | this.uPage = 0 24 | } 25 | } 26 | 27 | async decreaseGPage() { 28 | this.lPage -= 1 29 | if (this.lPage < 0) { 30 | this.lPage = 0 31 | } 32 | } 33 | 34 | static getAllPermissions() { 35 | return get('cms/admin/permission') 36 | } 37 | 38 | static async getAdminUsers({ groupId, count = this.uCount, page = this.uPage }) { 39 | let res 40 | if (groupId) { 41 | res = await get('cms/admin/users', { 42 | count, 43 | page, 44 | group_id: groupId, 45 | }) 46 | } else { 47 | res = await get('cms/admin/users', { 48 | count, 49 | page, 50 | }) 51 | } 52 | return res 53 | } 54 | 55 | async nextUsersPage() { 56 | await this.increaseUPage() 57 | return this.getAdminUsers({}) 58 | } 59 | 60 | async preUsersPage() { 61 | await this.decreaseUPage() 62 | return this.getAdminUsers({}) 63 | } 64 | 65 | async getGroupsWithPermissions({ count = this.uCount, page = this.uPage }) { 66 | const res = await get('cms/admin/groups', { 67 | count, 68 | page, 69 | }) 70 | return res 71 | } 72 | 73 | async nextGroupsPage() { 74 | await this.increaseGPage() 75 | return this.getGroupsWithPermissions({}) 76 | } 77 | 78 | async preGroupsPage() { 79 | await this.decreaseGPage() 80 | return this.getGroupsWithPermissions({}) 81 | } 82 | 83 | static async getAllGroups() { 84 | const groups = await get('cms/admin/group/all') 85 | return groups 86 | } 87 | 88 | static async getOneGroup(id) { 89 | const group = await get(`cms/admin/group/${id}`) 90 | return group 91 | } 92 | 93 | // eslint-disable-next-line camelcase 94 | static async createOneGroup(name, info, permission_ids) { 95 | const res = await post('cms/admin/group', { 96 | name, 97 | info, 98 | permission_ids, 99 | }) 100 | return res 101 | } 102 | 103 | static async updateOneGroup(name, info, id) { 104 | const res = await put(`cms/admin/group/${id}`, { 105 | name, 106 | info, 107 | }) 108 | return res 109 | } 110 | 111 | static async deleteOneGroup(id) { 112 | const res = await _delete(`cms/admin/group/${id}`) 113 | return res 114 | } 115 | 116 | static async deleteOneUser(id) { 117 | const res = await _delete(`cms/admin/user/${id}`) 118 | return res 119 | } 120 | 121 | // eslint-disable-next-line camelcase 122 | static async updateOneUser(email, group_ids, id) { 123 | const res = await put(`cms/admin/user/${id}`, { 124 | email, 125 | group_ids, 126 | }) 127 | return res 128 | } 129 | 130 | // eslint-disable-next-line camelcase 131 | static async dispatchPermissions(group_id, permission_ids) { 132 | const res = await post('cms/admin/permission/dispatch/batch', { 133 | group_id, 134 | permission_ids, 135 | }) 136 | return res 137 | } 138 | 139 | // eslint-disable-next-line camelcase 140 | static async changePassword(new_password, confirm_password, id) { 141 | const res = await put(`cms/admin/user/${id}/password`, { 142 | new_password, 143 | confirm_password, 144 | }) 145 | return res 146 | } 147 | 148 | // eslint-disable-next-line camelcase 149 | static async removePermissions(group_id, permission_ids) { 150 | const res = await post('cms/admin/permission/remove', { 151 | group_id, 152 | permission_ids, 153 | }) 154 | return res 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/lin/model/log.js: -------------------------------------------------------------------------------- 1 | import _axios, { get } from '@/lin/plugin/axios' 2 | 3 | class Log { 4 | name = null 5 | 6 | start = null 7 | 8 | end = null 9 | 10 | keyword = null 11 | 12 | constructor({ 13 | uPage = 0, 14 | uCount = 5, 15 | 16 | lPage = 0, 17 | lCount = 10, 18 | 19 | sPage = 0, 20 | sCount = 10, 21 | }) { 22 | if (uPage === 0) { 23 | this.uPage = uPage 24 | } 25 | if (uCount) { 26 | this.uCount = uCount 27 | } 28 | if (lPage === 0) { 29 | this.lPage = lPage 30 | } 31 | if (lCount) { 32 | this.lCount = lCount 33 | } 34 | if (sPage === 0) { 35 | this.sPage = sPage 36 | } 37 | if (sCount) { 38 | this.sCount = sCount 39 | } 40 | } 41 | 42 | async increaseUpage() { 43 | this.uPage += 1 44 | } 45 | 46 | async increaseLpage() { 47 | this.lPage += 1 48 | } 49 | 50 | increaseSpage() { 51 | this.sPage += 1 52 | } 53 | 54 | init() { 55 | this.lPage = 0 56 | this.uPage = 0 57 | this.sPage = 0 58 | } 59 | 60 | setBaseInfo(name, start, end) { 61 | this.name = name 62 | this.start = start 63 | this.end = end 64 | } 65 | 66 | setKeyword(keyword) { 67 | this.keyword = keyword 68 | } 69 | 70 | async addTestLog() { 71 | const log = await get('cms/test/info') 72 | return log 73 | } 74 | 75 | /** 76 | * 查询已经被记录过日志的用户(分页) 77 | * @param {number} count 每页个数 78 | * @param {number} page 第几页 79 | */ 80 | async getLoggedUsers({ count, page }) { 81 | const users = await get('cms/log/users', { 82 | count: count || this.uCount, 83 | page: page || this.uPage, 84 | }) 85 | return users 86 | } 87 | 88 | /** 89 | * 查询日志信息(分页) 90 | * @param {number} count 每页个数 91 | * @param {number} page 第几页 92 | * @param {number} name 用户昵称 93 | * @param {number} start 起始时间 # 2018-11-01 09:39:35 94 | * @param {number} end 结束时间 95 | */ 96 | async getLogs({ count, page, name, start, end, next = false }) { 97 | if (!next) { 98 | this.setBaseInfo(name, start, end) 99 | } 100 | if (page === 0) { 101 | this.lPage = 0 102 | } 103 | const res = await _axios({ 104 | url: 'cms/log', 105 | params: { 106 | count: count || this.lCount, 107 | page: page || this.lPage, 108 | name: name || this.name, 109 | start: start || this.start, 110 | end: end || this.end, 111 | }, 112 | handleError: true, 113 | }) 114 | return res 115 | } 116 | 117 | /** 118 | * 所搜日志信息(分页) 119 | * @param {number} count 每页个数 120 | * @param {number} page 第几页 121 | * @param {number} keyword 搜索关键词 122 | * @param {number} name 用户昵称 123 | * @param {number} start 起始时间 # 2018-11-01 09:39:35 124 | * @param {number} end 结束时间 125 | */ 126 | async searchLogs({ count, page, keyword, name, start, end, next = false }) { 127 | if (!next) { 128 | this.setBaseInfo(name, start, end) 129 | this.setKeyword(keyword) 130 | } 131 | if (page === 0) { 132 | this.sPage = 0 133 | } 134 | try { 135 | const res = await get('cms/log/search', { 136 | count: count || this.sCount, 137 | page: page || this.sPage, 138 | keyword: keyword || this.keyword, 139 | name: name || this.name, 140 | start: start || this.start, 141 | end: end || this.end, 142 | }) 143 | return res 144 | } catch (error) { 145 | console.log(error) 146 | } 147 | } 148 | 149 | async moreUserPage() { 150 | await this.increaseUpage() 151 | return this.getLoggedUsers({}) 152 | } 153 | 154 | async moreLogPage() { 155 | await this.increaseLpage() 156 | return this.getLogs({ next: true }) 157 | } 158 | 159 | async moreSearchPage() { 160 | this.increaseSpage() 161 | return this.searchLogs({ next: true }) 162 | } 163 | } 164 | 165 | export default new Log({}) 166 | -------------------------------------------------------------------------------- /src/lin/model/notify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import { post, get, put } from '@/lin/plugin/axios' 3 | import Config from '../../config' 4 | import Sse from '../util/sse' 5 | 6 | export default class Notify { 7 | url = null 8 | 9 | events = null 10 | 11 | sse = null 12 | 13 | constructor(url) { 14 | this.url = url 15 | } 16 | 17 | async getEvents() { 18 | const res = await get('cms/notify/events') 19 | this.events = res.events 20 | } 21 | 22 | async initSse() { 23 | await this.getEvents() 24 | this.sse = new Sse(Config.baseUrl + this.url, this.events) 25 | } 26 | 27 | /** 28 | * 创建events 29 | * @param {number} group_id 30 | * @param {Array} events 31 | */ 32 | // eslint-disable-next-line camelcase 33 | async createEvents(group_id, events) { 34 | const res = await post('cms/notify/events', { group_id, events }) 35 | return res 36 | } 37 | 38 | /** 39 | * 更新events 40 | * @param {number} group_id 41 | * @param {Array} events 42 | */ 43 | // eslint-disable-next-line camelcase 44 | async updateEvents(group_id, events) { 45 | const res = await put('cms/notify/events', { group_id, events }) 46 | return res 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lin/model/user.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import _axios, { get, put } from '@/lin/plugin/axios' 3 | import { saveTokens } from '../util/token' 4 | 5 | export default class User { 6 | /** 7 | * 分配用户 8 | * @param {object} user 注册信息 9 | */ 10 | static register(user) { 11 | return _axios({ 12 | method: 'post', 13 | url: 'cms/user/register', 14 | data: { 15 | email: user.email, 16 | username: user.username, 17 | password: user.password, 18 | group_ids: user.groupIds, 19 | confirm_password: user.confirmPassword, 20 | }, 21 | handleError: true, 22 | }) 23 | } 24 | 25 | /** 26 | * 登陆获取tokens 27 | * @param { String } username 用户名 28 | * @param { String } password 密码 29 | * @param { String } captcha 验证码 30 | * @param { String } tag 验证码签名 31 | */ 32 | static async getToken(username, password, captcha, tag) { 33 | const tokens = await _axios({ 34 | url: 'cms/user/login', 35 | method: 'POST', 36 | data: { 37 | captcha, 38 | username, 39 | password, 40 | }, 41 | headers: { 42 | tag, 43 | }, 44 | }) 45 | saveTokens(tokens.access_token, tokens.refresh_token) 46 | return tokens 47 | } 48 | 49 | /** 50 | * 获取当前用户信息,并返回User实例 51 | */ 52 | static async getInformation() { 53 | const info = await get('cms/user/information') 54 | const storeUser = store.getters.user === null ? {} : store.getters.user 55 | return Object.assign({ ...storeUser }, info) 56 | } 57 | 58 | /** 59 | * 获取当前用户信息和所拥有的权限 60 | */ 61 | static async getPermissions() { 62 | const info = await get('cms/user/permissions') 63 | const storeUser = store.getters.user === null ? {} : store.getters.user 64 | return Object.assign({ ...storeUser }, info) 65 | } 66 | 67 | /** 68 | * 用户修改密码 69 | * @param {string} newPassword 新密码 70 | * @param {string} confirmPassword 确认新密码 71 | * @param {string} oldPassword 旧密码 72 | */ 73 | // eslint-disable-next-line camelcase 74 | static updatePassword({ old_password, new_password, confirm_password }) { 75 | return put('cms/user/change_password', { 76 | new_password, 77 | confirm_password, 78 | old_password, 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lin/plugin/README.md: -------------------------------------------------------------------------------- 1 | # Lin-plugins 2 | 3 | 文件夹内容描述 4 | 5 | 更多内容请查看 [文档](#) 6 | -------------------------------------------------------------------------------- /src/lin/plugin/index.js: -------------------------------------------------------------------------------- 1 | import './axios' 2 | -------------------------------------------------------------------------------- /src/lin/util/auto-jump.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 定时自动登出功能, 启用后一段时间无用户操作, 则自动登出. 需在项目 config 中配置 3 | */ 4 | import store from '@/store' 5 | import Config from '@/config' 6 | 7 | 8 | let timer 9 | 10 | export default router => { 11 | if (timer) clearTimeout(timer) 12 | if (!Config.openAutoJumpOut) return 13 | if (router?.currentRoute.value.path === '/' || router?.currentRoute.value.path === '/login') { 14 | return 15 | } 16 | 17 | timer = setTimeout(() => { 18 | store.dispatch('loginOut') 19 | const { origin } = window.location 20 | window.location.href = origin 21 | }, Config.stagnateTime) 22 | } 23 | -------------------------------------------------------------------------------- /src/lin/util/cookie.js: -------------------------------------------------------------------------------- 1 | import cookies from 'js-cookie' 2 | /** 3 | * 存储tokens 4 | * @param {string} accessToken 5 | * @param {string} refreshToken 6 | */ 7 | export function saveTokens(accessToken, refreshToken) { 8 | // 存储tokens tokens只进入cookies,不进入vuex全局管理 9 | cookies.set('access_token', `Bearer ${accessToken}`) 10 | cookies.set('refresh_token', `Bearer ${refreshToken}`) 11 | } 12 | 13 | /** 14 | * 存储access_token 15 | * @param {string} accessToken 16 | */ 17 | export function saveAccessToken(accessToken) { 18 | cookies.set('access_token', `Bearer ${accessToken}`) 19 | } 20 | 21 | /** 22 | * 获得某个token 23 | * @param {string} tokenKey 24 | */ 25 | export function getToken(tokenKey) { 26 | return cookies.get(tokenKey) 27 | } 28 | 29 | /** 30 | * 移除token 31 | */ 32 | export function removeToken() { 33 | cookies.remove('access_token') 34 | cookies.remove('refresh_token') 35 | sessionStorage.removeItem('flag') 36 | sessionStorage.clear() 37 | localStorage.clear() 38 | } 39 | -------------------------------------------------------------------------------- /src/lin/util/date.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | // 设置语言为中文 4 | moment.locale('zh-cn') 5 | 6 | /** 7 | * @param {number} hours 8 | */ 9 | export function getDateAfterHours(hours) { 10 | const now = new Date() 11 | return new Date(now.setHours(now.getHours() + hours)) 12 | } 13 | /** 14 | * @param {number} days 15 | */ 16 | export function getDateAfterDays(days) { 17 | const now = new Date() 18 | return new Date(now.setHours(now.getHours() + days * 24)) 19 | } 20 | -------------------------------------------------------------------------------- /src/lin/util/emitter.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | export default mitt() 4 | -------------------------------------------------------------------------------- /src/lin/util/index.js: -------------------------------------------------------------------------------- 1 | export * from './date' 2 | -------------------------------------------------------------------------------- /src/lin/util/search.js: -------------------------------------------------------------------------------- 1 | import FastScanner from 'fastscan' 2 | 3 | // const words = ['今日头条', 4 | // '微信', '支付宝', 5 | // ] 6 | // const scanner = new FastScanner(words) 7 | // const content = '今日头条小程序终于来了,这是继微信、支付宝、百度后,第四个推出小程序功能的App。猫眼电影率先试水,出现在今日头条。' 8 | // const offWords = scanner.search(content) 9 | // console.log(offWords) 10 | // const hits = scanner.hits(content) 11 | // console.log(hits) 12 | 13 | /** 14 | * 15 | * @param {string} word 16 | * @param {string} content 17 | */ 18 | export async function searchForWord(word, content) { 19 | const scanner = new FastScanner([word]) 20 | const offWords = scanner.search(content) 21 | return offWords 22 | } 23 | 24 | /** 25 | * 26 | * @param {Array} words 27 | * @param {string} content 28 | */ 29 | export async function searchForWords(words, content) { 30 | const scanner = new FastScanner(words) 31 | const offWords = scanner.search(content) 32 | return offWords 33 | } 34 | /** 35 | * 36 | * @param {string} keyword 37 | * @param {Array} logs 38 | */ 39 | export function searchLogKeyword(keyword, logs, className = 'strong') { 40 | console.log('keyword', keyword) 41 | console.log('logs', logs) 42 | const _logs = logs.map(log => { 43 | let msg = log.message 44 | msg = msg.replace(RegExp(`${keyword}`, 'g'), `${keyword}`) 45 | // eslint-disable-next-line 46 | log.message = msg 47 | return log 48 | }) 49 | return _logs 50 | } 51 | -------------------------------------------------------------------------------- /src/lin/util/sse.js: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | 3 | // import EventSourcePolyfill from 'event-source-polyfill' 4 | import 'event-source-polyfill/src/eventsource' 5 | import { getToken } from './cookie' 6 | import store from '../../store' 7 | 8 | export default class Sse { 9 | source = null 10 | 11 | /** 12 | * 需在vuex中确认有user对象后才能初始化,否则不连接服务器 13 | * 注意: sse单独走自己的请求路线,不与axios重合,所以axios里面的配置在此处失效 14 | * @param {string} url sse全路径 15 | * @param {Array} events 当前用户可监听的路径 16 | */ 17 | constructor(url, events) { 18 | /* eslint-disable no-undef */ 19 | console.log(url, events) 20 | this.source = new EventSourcePolyfill(url, { 21 | headers: { 22 | Authorization: getToken('access_token'), 23 | }, 24 | }) 25 | this.open() 26 | 27 | events.forEach(event => { 28 | this.addEventListener(event) 29 | }) 30 | } 31 | 32 | open() { 33 | this.source.onopen = event => { 34 | console.log('sse opened', event) 35 | } 36 | } 37 | 38 | error() { 39 | this.source.onerror = event => { 40 | console.log('error', event) 41 | } 42 | } 43 | 44 | addEventListener(eventName) { 45 | this.source.addEventListener(eventName, event => { 46 | // console.log('receive one message: ', event.data) 47 | // console.log('receive one message: ', event.lastEventId) 48 | store.commit('MARK_UNREAD_MESSAGE', { data: event.data, id: event.lastEventId }) 49 | ElMessage.warning(JSON.parse(event.data).message) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lin/util/storage.js: -------------------------------------------------------------------------------- 1 | import storage from 'good-storage' 2 | 3 | const LOGIN_KEY = '__login__' 4 | 5 | export function setLoggedIn(flag) { 6 | storage.session.set(LOGIN_KEY, flag) 7 | return flag 8 | } 9 | 10 | export function loadLoggedIn() { 11 | return storage.session.get(LOGIN_KEY, '') 12 | } 13 | 14 | export function cleanLoggedIn() { 15 | storage.session.remove(LOGIN_KEY) 16 | } 17 | -------------------------------------------------------------------------------- /src/lin/util/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储tokens 3 | * @param {string} accessToken 4 | * @param {string} refreshToken 5 | */ 6 | export function saveTokens(accessToken, refreshToken) { 7 | localStorage.setItem('access_token', `Bearer ${accessToken}`) 8 | localStorage.setItem('refresh_token', `Bearer ${refreshToken}`) 9 | } 10 | 11 | /** 12 | * 存储access_token 13 | * @param {string} accessToken 14 | */ 15 | export function saveAccessToken(accessToken) { 16 | localStorage.setItem('access_token', `Bearer ${accessToken}`) 17 | } 18 | 19 | /** 20 | * 获得某个token 21 | * @param {string} tokenKey 22 | */ 23 | export function getToken(tokenKey) { 24 | return localStorage.getItem(tokenKey) 25 | } 26 | 27 | /** 28 | * 移除token 29 | */ 30 | export function removeToken() { 31 | localStorage.removeItem('access_token') 32 | localStorage.removeItem('refresh_token') 33 | } 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'dayjs/locale/zh-cn' 2 | import { createApp } from 'vue' 3 | import ElementPlus from 'element-plus' 4 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 5 | import zhCn from 'element-plus/dist/locale/zh-cn.mjs' 6 | 7 | import '@/config/global' 8 | import 'lin/plugin' 9 | import { filters } from 'lin/filter' 10 | import permissionDirective from 'lin/directive/authorize' 11 | 12 | import App from '@/app.vue' 13 | import store from '@/store' 14 | import router from '@/router' 15 | 16 | import LinNotify from '@/component/notify' 17 | import LIcon from '@/component/base/icon/lin-icon' 18 | import StickyTop from '@/component/base/sticky-top/sticky-top' 19 | import SourceCode from '@/component/base/source-code/source-code' 20 | 21 | import '@/assets/style/index.scss' 22 | import 'element-plus/dist/index.css' 23 | import '@/assets/style/realize/element-variable.scss' 24 | 25 | const app = createApp(App) 26 | 27 | // 注册element plus icons 28 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 29 | app.component(key, component) 30 | } 31 | 32 | app.use(store) 33 | app.use(router) 34 | app.use(ElementPlus, { 35 | locale: zhCn, 36 | }) 37 | app.use(LinNotify, { 38 | reconnection: true, 39 | reconnectionAttempts: 5, 40 | reconnectionDelay: 3000, 41 | }) 42 | 43 | // base 组件注册 44 | app.component('l-icon', LIcon) 45 | app.component('sticky-top', StickyTop) 46 | app.component('source-code', SourceCode) 47 | 48 | app.config.globalProperties.$filters = filters 49 | 50 | app.directive('permission', permissionDirective) 51 | 52 | app.mount('#app') 53 | 54 | // 设置 App 实例 55 | window.App = app 56 | -------------------------------------------------------------------------------- /src/model/book.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | import _axios, { get, put, _delete } from '@/lin/plugin/axios' 3 | 4 | // 我们通过 class 这样的语法糖使模型这个概念更加具象化,其优点:耦合性低、可维护性。 5 | class Book { 6 | // constructor() {} 7 | 8 | // 类中的方法可以代表一个用户行为 9 | async createBook(data) { 10 | return _axios({ 11 | method: 'post', 12 | url: 'v1/book', 13 | data, 14 | }) 15 | } 16 | 17 | // 在这里通过 async await 语法糖让代码同步执行 18 | // 1. await 一定要搭配 async 来使用 19 | // 2. await 后面跟的是一个 Promise 对象 20 | async getBook(id) { 21 | const res = await get(`v1/book/${id}`) 22 | return res 23 | } 24 | 25 | async editBook(id, info) { 26 | const res = await put(`v1/book/${id}`, info) 27 | return res 28 | } 29 | 30 | async deleteBook(id) { 31 | const res = await _delete(`v1/book/${id}`) 32 | return res 33 | } 34 | 35 | async getBooks() { 36 | return _axios({ 37 | method: 'get', 38 | url: 'v1/book', 39 | handleError: true, 40 | }) 41 | } 42 | } 43 | 44 | export default new Book() 45 | -------------------------------------------------------------------------------- /src/plugin/custom/README.md: -------------------------------------------------------------------------------- 1 | # 插件名: 自定义组件展示 2 | 3 | 展示 Lin-CMS 自行封装的组件,开发者可根据需求选择是否使用。 -------------------------------------------------------------------------------- /src/plugin/custom/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/plugin/custom/assets/image/logo.png -------------------------------------------------------------------------------- /src/plugin/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-plugin-custom", 3 | "title": "自定义组件展示", 4 | "version": "1.0.0", 5 | "_linVersion": "0.4.0", 6 | "description": "自定义组件展示", 7 | "author": "", 8 | "dependencies": { 9 | "element-plus": "^1.0.2-beta.36" 10 | }, 11 | "devDependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /src/plugin/custom/stage-config.js: -------------------------------------------------------------------------------- 1 | const CustomRouter = { 2 | route: null, 3 | name: null, 4 | title: '自定义组件', 5 | type: 'folder', 6 | icon: 'iconfont icon-zidingyi', 7 | filePath: 'view/custom/', 8 | order: null, 9 | inNav: true, 10 | children: [ 11 | { 12 | title: 'upload 图像上传', 13 | type: 'view', 14 | name: 'ImgsUploadDemo', 15 | route: '/custom/upload-image', 16 | filePath: 'plugin/custom/view/upload-image.vue', 17 | inNav: true, 18 | icon: 'iconfont icon-upload', 19 | permission: null, 20 | }, 21 | { 22 | title: 'editor 富文本', 23 | type: 'view', 24 | name: 'Tinymce', 25 | route: '/custom/tinymce', 26 | filePath: 'plugin/custom/view/tinymce.vue', 27 | inNav: true, 28 | icon: 'iconfont icon-fuwenbenbianjiqi_gongshi', 29 | permission: null, 30 | }, 31 | { 32 | title: 'multiple 多重输入', 33 | type: 'view', 34 | name: 'Multiple', 35 | route: '/custom/multiple', 36 | filePath: 'plugin/custom/view/multiple-input.vue', 37 | inNav: true, 38 | icon: 'iconfont icon-multiple_inputs', 39 | permission: null, 40 | }, 41 | ], 42 | } 43 | 44 | export default CustomRouter 45 | -------------------------------------------------------------------------------- /src/plugin/custom/view/multiple-input.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 77 | 78 | 155 | -------------------------------------------------------------------------------- /src/plugin/custom/view/tinymce.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/README.md: -------------------------------------------------------------------------------- 1 | # 插件名: ui(lin-cms-ui) 2 | 3 | 插件描述, ui 用于处于xxx业务场景, 提供了xxx功能 4 | 5 | ## 舞台视口列表 6 | 7 | ### TestView 8 | 9 | 地址: `/lin-cms-ui/test.vue` 10 | 显示xxx内容, 可进行xxx操作 11 | 12 | ## 自由视口列表 13 | 14 | ### Test 15 | 16 | 属性: 17 | 18 | | Require | Name | Type | Default | Desc | 19 | |:-------:|:----:|:------:|:-------:|:----:| 20 | | true | name | String | val | 描述 | 21 | 22 | 事件: 23 | 24 | | Name | Argument | Desc | 25 | |:----:|:------------:|:----:| 26 | | name | 事件参数描述 | 描述 | 27 | 28 | 方法: 29 | 30 | | Name | Argument | Result | Desc | 31 | |:----:|:------------:|:----------:|:----:| 32 | | name | 方法参数描述 | 返回值描述 | 描述 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaleLin/lin-cms-vue/51277f2c3ef457e08fda588ff450726aefc03ea4/src/plugin/lin-cms-ui/assets/image/logo.png -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/assets/style/container.scss: -------------------------------------------------------------------------------- 1 | .lin-wrap-ui :v-deep(.el-card__body) { 2 | padding-top: 30px; 3 | padding-bottom: 0px; 4 | } 5 | .lin-wrap-ui :v-deep(.el-collapse) { 6 | border-top: none; 7 | border-bottom: none; 8 | cursor: pointer; 9 | .el-collapse-item__header { 10 | border-bottom: none; 11 | color: #2f4e8c; 12 | padding-left: calc(100% - 77px); 13 | } 14 | 15 | .el-collapse-item__content { 16 | background: #e9f0f8; 17 | color: #2f4e8c; 18 | border-radius: 4px; 19 | padding: 0px 20px 20px 20px; 20 | margin-bottom: 20px; 21 | } 22 | } 23 | .lin-wrap-ui { 24 | padding: 30px 40px; 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/component/component.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/model/movie.js: -------------------------------------------------------------------------------- 1 | import { movieList } from '../simulation/movie' 2 | 3 | class Movie { 4 | getTop250(start = 0, count = 20) { 5 | const arr = [] 6 | const tempList = movieList.slice() 7 | const currentList = tempList.splice(start, count) 8 | currentList.forEach((element, index) => { 9 | const tempCasts = [] 10 | const tempDirectors = [] 11 | element.casts.forEach(el => { 12 | tempCasts.push(el.name) 13 | }) 14 | element.directors.forEach(el => { 15 | tempDirectors.push(el.name) 16 | }) 17 | 18 | arr.push({ 19 | title: element.title, 20 | originalTitle: element.original_title, 21 | year: element.year, 22 | rating: element.rating.average, 23 | casts: tempCasts.join('/'), 24 | directors: tempDirectors.join('/'), 25 | genres: element.genres.join('/'), 26 | rank: index + 1 + start, 27 | sorting: 50, 28 | recommend: 0, 29 | remark: '这是一部不错的电影', 30 | editFlag: false, 31 | thumb: element.thumb ? element.thumb : '', 32 | }) 33 | }) 34 | 35 | return arr 36 | } 37 | 38 | getDataByQuery(query = '') { 39 | const arr = [] 40 | for (let index = 0; index < movieList.length; index++) { 41 | const element = movieList[index] 42 | 43 | if (element.title.match(query)) { 44 | const tempCasts = [] 45 | const tempDirectors = [] 46 | element.casts.forEach(el => { 47 | tempCasts.push(el.name) 48 | }) 49 | element.directors.forEach(el => { 50 | tempDirectors.push(el.name) 51 | }) 52 | 53 | arr.push({ 54 | title: element.title, 55 | originalTitle: element.original_title, 56 | year: element.year, 57 | rating: element.rating.average, 58 | casts: tempCasts.join('/'), 59 | directors: tempDirectors.join('/'), 60 | genres: element.genres.join('/'), 61 | rank: index + 1, 62 | sorting: 50, 63 | recommend: 0, 64 | remark: '这是一部不错的电影', 65 | editFlag: false, 66 | thumb: element.thumb ? element.thumb : '', 67 | }) 68 | } 69 | } 70 | 71 | return arr 72 | } 73 | } 74 | 75 | export default new Movie() 76 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-plugin-lin-cms-ui", 3 | "title": "UI", 4 | "version": "1.0.0", 5 | "_linVersion": "0.0.1-alpha.3", 6 | "description": "", 7 | "author": "", 8 | "dependencies": {}, 9 | "devDependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/basic/icon/icon.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 162 | 163 | 166 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/basic/link/link.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 116 | 117 | 123 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/data/progress/progress.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 106 | 107 | 114 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/form/multiple-input.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 78 | 79 | 132 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/form/rate/rate.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 84 | 85 | 108 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/form/switch/switch.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 120 | 121 | 124 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/navigation/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 100 | 101 | 123 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/table/data.js: -------------------------------------------------------------------------------- 1 | export const tableColumn = [ 2 | // { prop: 'sorting', label: '排序', noRepeat: true }, 3 | { prop: 'rank', label: '排名' }, 4 | { prop: 'title', label: '电影名', width: 150 }, 5 | // { prop: 'originalTitle', label: '原名', width: 150 }, 6 | { 7 | prop: 'rating', 8 | label: '评分', 9 | noRepeat: true, 10 | width: 100, 11 | }, 12 | // { prop: 'genres', label: '类型', width: 150 }, 13 | { prop: 'directors', label: '导演', width: 150 }, 14 | { prop: 'casts', label: '主演', width: 150 }, 15 | { prop: 'year', label: '年份' }, 16 | { prop: 'recommend', label: '推荐', noRepeat: true }, 17 | { 18 | prop: 'remark', 19 | label: '备注', 20 | noRepeat: true, 21 | width: 200, 22 | }, 23 | ] 24 | 25 | export const a = 1 26 | -------------------------------------------------------------------------------- /src/router/home-router.js: -------------------------------------------------------------------------------- 1 | import stageConfig from '@/config/stage' // 引入舞台配置 2 | 3 | /** 4 | * 深度遍历配置树, 摘取叶子节点作为路由部分 5 | * @param {*} config 配置项 6 | * @param {*} fuc 回调函数 7 | */ 8 | function deepTravel(config, fuc) { 9 | if (Array.isArray(config)) { 10 | config.forEach(subConfig => { 11 | deepTravel(subConfig, fuc) 12 | }) 13 | } else if (config.children?.length) { 14 | config.children.forEach(subConfig => { 15 | deepTravel(subConfig, fuc) 16 | }) 17 | } else { 18 | fuc(config) 19 | } 20 | } 21 | 22 | const homeRouter = [] 23 | 24 | /** 25 | * 构造舞台view路由 26 | */ 27 | deepTravel(stageConfig, viewConfig => { 28 | const viewRouter = {} 29 | viewRouter.path = viewConfig.route 30 | viewRouter.name = viewConfig.name 31 | viewRouter.component = () => import(`@/${viewConfig.filePath}`) 32 | viewRouter.meta = { 33 | title: viewConfig.title, 34 | icon: viewConfig.icon, 35 | permission: viewConfig.permission, 36 | type: viewConfig.type, 37 | blueBaseColor: viewConfig.blueBaseColor ? 'viewConfig.blueBaseColor' : '', 38 | } 39 | homeRouter.push(viewRouter) 40 | }) 41 | 42 | export default homeRouter 43 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import { ElMessage } from 'element-plus' 3 | 4 | import appConfig from '@/config/index' 5 | import Util from '@/lin/util/util' 6 | import autoJump from '@/lin/util/auto-jump' 7 | import store from '../store' 8 | import routes from './route' 9 | 10 | // 判断是否需要登录访问, 配置位于 config 文件夹 11 | let isLoginRequired = routeName => { 12 | // 首次执行时缓存配置 13 | let { notLoginRoute } = appConfig 14 | const notLoginMark = {} 15 | 16 | // 构建标记对象 17 | if (Array.isArray(notLoginRoute)) { 18 | for (let i = 0; i < notLoginRoute.length; i += 1) { 19 | notLoginMark[notLoginRoute[i].toString()] = true 20 | } 21 | } 22 | 23 | notLoginRoute = null // 释放内存 24 | 25 | // 重写初始化函数 26 | isLoginRequired = name => { 27 | if (!name) { 28 | return true 29 | } 30 | // 处理 Symbol 类型 31 | const target = typeof name === 'symbol' ? name.description : name 32 | return !notLoginMark[target] 33 | } 34 | 35 | return isLoginRequired(routeName) 36 | } 37 | 38 | const router = createRouter({ 39 | scrollBehavior: () => ({ y: 0 }), 40 | base: process.env.BASE_URL, 41 | history: createWebHashHistory(), 42 | routes, 43 | }) 44 | 45 | router.beforeEach((to, from, next) => { 46 | // 登录验证 47 | if (isLoginRequired(to.name) && !store.state.loggedIn) { 48 | next({ path: '/login' }) 49 | return 50 | } 51 | 52 | // TODO: tab 模式重复点击验证 53 | 54 | // 权限验证 55 | if (store?.state && store?.getters) { 56 | const { permissions, user } = store.getters 57 | if (to.path !== '/about' && !Util.hasPermission(permissions, to.meta, user)) { 58 | ElMessage.error('您无此页面的权限哟') 59 | next({ path: '/about' }) 60 | return 61 | } 62 | } 63 | 64 | // 路由发生变化重新计时 65 | autoJump(router) 66 | 67 | // 路由发生变化修改页面title 68 | if (to.meta.title) { 69 | document.title = to.meta.title 70 | } 71 | 72 | next() 73 | }) 74 | 75 | export default router 76 | -------------------------------------------------------------------------------- /src/router/route.js: -------------------------------------------------------------------------------- 1 | import homeRouter from './home-router' 2 | 3 | const routes = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | redirect: '/about', 8 | component: () => import('@/view/home/home'), 9 | children: [...homeRouter], 10 | }, 11 | { 12 | path: '/login', 13 | name: 'login', 14 | component: () => import('@/view/login/login'), 15 | }, 16 | { 17 | redirect: '/404', 18 | path: '/:pathMatch(.*)', 19 | }, 20 | ] 21 | 22 | export default routes 23 | -------------------------------------------------------------------------------- /src/store/action.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-type' 2 | 3 | export default { 4 | setUserAndState({ commit }, user) { 5 | commit(types.SET_USER, user) 6 | commit(types.SET_LOGGED_IN, true) 7 | }, 8 | 9 | loginOut({ commit }) { 10 | localStorage.clear() 11 | commit(types.REMOVE_LOGGED_IN, false) 12 | }, 13 | 14 | readMessage({ commit }, message) { 15 | commit(types.REMOVE_UNREAD_MESSAGE, message.id) 16 | commit(types.MARK_READ_MESSAGE, message) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, createLogger } from 'vuex' 2 | import VuexPersistence from 'vuex-persist' 3 | 4 | import mutations from './mutation' 5 | import state from './state' 6 | import * as getters from './getter' 7 | import actions from './action' 8 | 9 | const vuexLocal = new VuexPersistence({ 10 | storage: window.localStorage, 11 | reducer: stateData => ({ 12 | user: stateData.user, 13 | loggedIn: stateData.loggedIn, 14 | permissions: stateData.permissions, 15 | }), 16 | }) 17 | 18 | const debug = process.env.NODE_ENV !== 'production' 19 | 20 | export default createStore({ 21 | state, 22 | getters, 23 | actions, 24 | mutations, 25 | strict: debug, 26 | plugins: debug ? [vuexLocal.plugin, createLogger()] : [vuexLocal.plugin], 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/mutation-type.js: -------------------------------------------------------------------------------- 1 | export const SET_LOGGED_IN = 'SET_LOGGED_IN' 2 | 3 | export const REMOVE_LOGGED_IN = 'REMOVE_LOGGED_IN' 4 | 5 | export const SET_USER = 'SET_USER' 6 | 7 | export const MARK_READ_MESSAGE = 'MARK_READ_MESSAGE' 8 | 9 | export const REMOVE_UNREAD_MESSAGE = 'REMOVE_UNREAD_MESSAGE' 10 | 11 | export const MARK_UNREAD_MESSAGE = 'MARK_UNREAD_MESSAGE' 12 | 13 | export const SET_USER_PERMISSIONS = 'SET_USER_PERMISSIONS' 14 | 15 | export const SET_REFRESH_OPTION = 'SET_REFRESH_OPTION' 16 | -------------------------------------------------------------------------------- /src/store/mutation.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation-type' 2 | 3 | export default { 4 | [types.SET_LOGGED_IN](state) { 5 | state.loggedIn = true 6 | }, 7 | 8 | [types.REMOVE_LOGGED_IN](state) { 9 | state.loggedIn = false 10 | state.user = null 11 | }, 12 | 13 | [types.SET_USER](state, payload) { 14 | state.user = payload 15 | }, 16 | 17 | [types.MARK_READ_MESSAGE](state, payload) { 18 | state.alreadyReadMessages.push(payload) 19 | }, 20 | 21 | [types.MARK_UNREAD_MESSAGE](state, payload) { 22 | // console.log('===: ', payload) 23 | state.unreadMessages.push(payload) 24 | }, 25 | 26 | [types.REMOVE_UNREAD_MESSAGE](state, payload) { 27 | // payload => message.id 28 | const { unreadMessages } = state 29 | const index = unreadMessages.findIndex(el => el.id === payload) 30 | unreadMessages.splice(index, 1) 31 | }, 32 | 33 | [types.SET_USER_PERMISSIONS](state, permissions) { 34 | state.permissions = permissions 35 | .map(permission => Object.values(permission)) 36 | .flat(2) 37 | .map(p => p.permission) 38 | }, 39 | 40 | [types.SET_REFRESH_OPTION](state, option) { 41 | state.refreshOptions = option 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | import appConfig from '@/config/index' // 引入项目配置 2 | import stageConfig from '@/config/stage' // 引入舞台配置 3 | 4 | export default { 5 | user: {}, // 当前用户 6 | loggedIn: false, // 是否登录 7 | permissions: [], // 每个用户的所有权限 8 | 9 | // 推送消息 10 | unreadMessages: [], 11 | alreadyReadMessages: [], 12 | 13 | // 舞台配置 14 | stageConfig, 15 | 16 | // 当前页信息 17 | currentRoute: { 18 | config: null, 19 | treePath: [], 20 | }, 21 | 22 | sidebarLevel: appConfig.sidebarLevel || 3, 23 | defaultRoute: appConfig.defaultRoute || '/about', 24 | } 25 | -------------------------------------------------------------------------------- /src/view/admin/group/group-create.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 134 | 135 | 158 | -------------------------------------------------------------------------------- /src/view/admin/group/group-edit.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 116 | 117 | 140 | -------------------------------------------------------------------------------- /src/view/admin/group/group-list.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 102 | 103 | 116 | -------------------------------------------------------------------------------- /src/view/admin/group/use-group.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, reactive } from 'vue' 2 | import { ElMessageBox, ElMessage } from 'element-plus' 3 | import AdminModel from '@/lin/model/admin' 4 | 5 | export const useGroupList = () => { 6 | const tableData = ref([]) // 表格数据 7 | const loading = ref(false) 8 | 9 | /** 10 | * 获取所有分组并传给table渲染 11 | */ 12 | const getAllGroups = async () => { 13 | try { 14 | loading.value = true 15 | tableData.value = await AdminModel.getAllGroups() 16 | loading.value = false 17 | } catch (e) { 18 | loading.value = false 19 | console.error(e) 20 | } 21 | } 22 | 23 | /** 24 | * 删除某项数据 25 | * @param {object} id 选中的一行数据 ID 26 | */ 27 | const handleDelete = id => { 28 | let res = {} 29 | ElMessageBox.confirm('此操作将永久删除该分组, 是否继续?', '提示', { 30 | confirmButtonText: '确定', 31 | cancelButtonText: '取消', 32 | type: 'warning', 33 | }).then(async () => { 34 | try { 35 | loading.value = true 36 | res = await AdminModel.deleteOneGroup(id) 37 | } catch (e) { 38 | loading.value = false 39 | console.log(e) 40 | } 41 | if (res.code < window.MAX_SUCCESS_CODE) { 42 | await getAllGroups() 43 | ElMessage.message({ type: 'success', message: `${res.message}` }) 44 | } else { 45 | loading.value = false 46 | ElMessage.message({ type: 'error', message: `${res.message}` }) 47 | } 48 | }) 49 | } 50 | 51 | onMounted(async () => { 52 | await getAllGroups() 53 | }) 54 | 55 | return { 56 | loading, 57 | tableData, 58 | handleDelete, 59 | getAllGroups, 60 | } 61 | } 62 | 63 | export const useEditGroup = (ctx, getAllGroups) => { 64 | let cacheGroup = {} 65 | const id = ref(0) // 分组id 66 | const form = ref(null) 67 | const group = reactive({ name: '', info: '' }) 68 | const dialogFormVisible = ref(false) // 是否弹窗 69 | const rules = { 70 | // 表单验证规则 71 | info: [], 72 | name: [{ required: true, message: '请输入分组名称', trigger: 'blur' }], 73 | } 74 | 75 | /** 76 | * 获取所拥有的权限并渲染 由子组件提供 77 | * @param {*} val 选中的某行数据 78 | */ 79 | const handleEdit = row => { 80 | id.value = row.id 81 | group.name = row.name 82 | group.info = row.info 83 | cacheGroup = { ...group } 84 | dialogFormVisible.value = true 85 | } 86 | 87 | /** 88 | * 修改分组信息 89 | */ 90 | const confirmEdit = async () => { 91 | if (group.name === '') { 92 | ElMessage.warning('请将信息填写完整') 93 | return 94 | } 95 | if (cacheGroup.name !== group.name || cacheGroup.info !== group.info) { 96 | const res = await AdminModel.updateOneGroup(group.name, group.info, id.value) 97 | if (res.code < window.MAX_SUCCESS_CODE) { 98 | ElMessage.success(`${res.message}`) 99 | getAllGroups() 100 | } 101 | } 102 | dialogFormVisible.value = false 103 | } 104 | 105 | const handleClose = done => { 106 | done() 107 | } 108 | const rowDoubleClick = row => { 109 | handleEdit(row) 110 | } 111 | const resetForm = () => { 112 | form.value.resetFields() 113 | } 114 | 115 | return { 116 | id, 117 | form, 118 | rules, 119 | group, 120 | resetForm, 121 | handleEdit, 122 | confirmEdit, 123 | handleClose, 124 | rowDoubleClick, 125 | dialogFormVisible, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/view/admin/user/use-user.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue' 2 | import AdminModel from '@/lin/model/admin' 3 | 4 | export const useUserList = () => { 5 | const allGroups = ref([]) 6 | const pageCount = ref(10) // 每页10条数据 7 | const tableData = ref([]) 8 | const groupId = ref(null) 9 | const loading = ref(false) 10 | const totalNum = ref(0) // 分组内的用户总数 11 | const currentPage = ref(1) // 默认获取第一页的数据 12 | 13 | /** 14 | * 获取管理员列表数据 15 | */ 16 | const getAdminUsers = async () => { 17 | let res = {} 18 | try { 19 | loading.value = true 20 | res = await AdminModel.getAdminUsers({ 21 | groupId: groupId.value, 22 | count: pageCount.value, 23 | page: currentPage.value - 1, 24 | }) 25 | loading.value = false 26 | tableData.value = shuffleList(res.items) 27 | totalNum.value = res.total 28 | } catch (e) { 29 | loading.value = false 30 | console.error(e) 31 | } 32 | } 33 | 34 | /** 35 | * 获取所有分组数据 36 | */ 37 | const getAllGroups = async () => { 38 | try { 39 | loading.value = true 40 | allGroups.value = await AdminModel.getAllGroups() 41 | loading.value = false 42 | } catch (e) { 43 | loading.value = false 44 | console.error(e) 45 | } 46 | } 47 | 48 | /** 49 | * 多分组用 ',' 分割展示 50 | */ 51 | const shuffleList = users => { 52 | const list = [] 53 | users.forEach(element => { 54 | element.groupNames = element.groups.map(item => item.name).join(',') 55 | list.push(element) 56 | }) 57 | return list 58 | } 59 | 60 | onMounted(async () => { 61 | await getAdminUsers() 62 | getAllGroups() 63 | }) 64 | 65 | return { 66 | groupId, 67 | loading, 68 | totalNum, 69 | allGroups, 70 | pageCount, 71 | tableData, 72 | currentPage, 73 | getAdminUsers, 74 | } 75 | } 76 | 77 | export const useFormData = (ctx, dialogFormVisible, getAdminUsers, currentPage, loading, info, password) => { 78 | const id = ref(null) 79 | const activeTab = ref('修改信息') 80 | 81 | /** 82 | * 监听子组件更新管理员信息是否成功 83 | * 如果更新了管理员信息,重新请求管理员列表 84 | */ 85 | const handleInfoResult = flag => { 86 | dialogFormVisible.value = false 87 | if (flag === true) { 88 | getAdminUsers() 89 | } 90 | } 91 | 92 | /** 93 | * 根据分组查询管理员 94 | */ 95 | const handleChange = async () => { 96 | currentPage.value = 1 97 | loading.value = true 98 | await getAdminUsers() 99 | loading.value = false 100 | } 101 | 102 | /** 103 | * 监听是否完成密码更新 104 | * @param {boolean} result 是否完成密码更新 105 | */ 106 | const handlePasswordResult = result => { 107 | if (result === true) { 108 | dialogFormVisible.value = false 109 | } 110 | } 111 | 112 | /** 113 | * 翻页 114 | */ 115 | const handleCurrentChange = async val => { 116 | currentPage.value = val 117 | await getAdminUsers() 118 | } 119 | 120 | /** 121 | * 提交表单信息,更新管理员信息 122 | */ 123 | const confirmEdit = async () => { 124 | if (activeTab.value === '修改信息') { 125 | await info.value.submitForm() 126 | } else { 127 | await password.value.submitForm() 128 | } 129 | } 130 | 131 | /** 132 | * 关闭编辑弹窗 133 | */ 134 | const handleClose = done => { 135 | dialogFormVisible.value = false 136 | password.value.resetForm() 137 | activeTab.value = '修改信息' 138 | done() 139 | } 140 | 141 | const handleClick = tab => { 142 | activeTab.value = tab.props.name 143 | } 144 | 145 | /** 146 | * 重置表单 147 | */ 148 | const resetForm = () => { 149 | if (activeTab.value === '修改信息') { 150 | info.value.resetForm() 151 | } else { 152 | password.value.resetForm() 153 | } 154 | } 155 | 156 | return { 157 | id, 158 | activeTab, 159 | resetForm, 160 | confirmEdit, 161 | handleClose, 162 | handleClick, 163 | handleChange, 164 | handleInfoResult, 165 | handleCurrentChange, 166 | handlePasswordResult, 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/view/admin/user/user-create.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 57 | -------------------------------------------------------------------------------- /src/view/admin/user/user-password.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 128 | 129 | 134 | -------------------------------------------------------------------------------- /src/view/book/book-list.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 106 | 107 | 132 | -------------------------------------------------------------------------------- /src/view/book/book.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 144 | 145 | 172 | -------------------------------------------------------------------------------- /src/view/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, dir) 5 | } 6 | 7 | module.exports = { 8 | lintOnSave: false, 9 | productionSourceMap: false, 10 | // assetsDir: 'static', 11 | chainWebpack: config => { 12 | config.resolve.alias.set('@', resolve('src')).set('lin', resolve('src/lin')).set('assets', resolve('src/assets')) 13 | config.module.rule('ignore').test(/\.md$/).use('ignore-loader').loader('ignore-loader').end() 14 | }, 15 | configureWebpack: { 16 | devtool: 'source-map', 17 | resolve: { 18 | extensions: ['.js', '.json', '.vue', '.scss', '.html'], 19 | }, 20 | }, 21 | css: { 22 | loaderOptions: { 23 | sass: { 24 | prependData: `@import "@/assets/style/shared.scss";`, 25 | }, 26 | }, 27 | }, 28 | devServer: {}, 29 | // node_modules依赖项es6语法未转换问题 30 | transpileDependencies: ['vuex-persist'], 31 | } 32 | --------------------------------------------------------------------------------