├── src ├── lin │ ├── plugin │ │ ├── index.js │ │ └── README.md │ ├── util │ │ ├── index.js │ │ ├── emitter.js │ │ ├── storage.js │ │ ├── date.js │ │ ├── auto-jump.js │ │ ├── token.js │ │ ├── cookie.js │ │ ├── search.js │ │ └── sse.js │ ├── directive │ │ ├── index.js │ │ └── authorize.js │ ├── context │ │ ├── index.js │ │ └── admin.js │ ├── model │ │ ├── setting.js │ │ ├── notify.js │ │ ├── user.js │ │ ├── admin.js │ │ └── log.js │ └── filter │ │ └── index.js ├── config │ ├── global.js │ ├── stage │ │ ├── center.js │ │ ├── plugin.js │ │ ├── index.js │ │ └── admin.js │ ├── error-code.js │ └── index.js ├── plugin │ ├── custom │ │ ├── README.md │ │ ├── assets │ │ │ └── image │ │ │ │ └── logo.png │ │ ├── package.json │ │ ├── view │ │ │ └── tinymce.vue │ │ └── stage-config.js │ ├── lin-cms-ui │ │ ├── assets │ │ │ ├── image │ │ │ │ └── logo.png │ │ │ └── style │ │ │ │ └── container.scss │ │ ├── package.json │ │ ├── README.md │ │ ├── view │ │ │ ├── table │ │ │ │ └── data.js │ │ │ └── form │ │ │ │ ├── rate │ │ │ │ └── rate.vue │ │ │ │ └── multiple-input.vue │ │ ├── component │ │ │ └── component.vue │ │ └── model │ │ │ └── movie.js │ ├── blog │ │ ├── package.json │ │ ├── model │ │ │ ├── classify.js │ │ │ ├── comment.js │ │ │ ├── channel.js │ │ │ ├── tag.js │ │ │ └── article.js │ │ ├── stage-config.js │ │ └── view │ │ │ └── comment │ │ │ └── comment-dialog.vue │ └── base │ │ ├── package.json │ │ ├── stage-config.js │ │ ├── model │ │ └── base.js │ │ └── view │ │ ├── type │ │ ├── type-dialog.vue │ │ └── type-list.vue │ │ └── item │ │ └── item-dialog.vue ├── assets │ ├── style │ │ ├── shared.scss │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ ├── realize │ │ │ ├── transition.scss │ │ │ ├── variable.scss │ │ │ ├── animation.scss │ │ │ ├── mixin.scss │ │ │ └── reset.scss │ │ ├── index.scss │ │ ├── form.scss │ │ ├── list.scss │ │ └── vditor-preview.scss │ └── image │ │ ├── logo.png │ │ ├── about │ │ ├── icon.png │ │ ├── icon1.png │ │ ├── icon2.png │ │ ├── icon3.png │ │ ├── icon4.png │ │ ├── avatar.png │ │ ├── qrcode.jpg │ │ ├── welcome.png │ │ ├── header-bg.png │ │ ├── team-icon.png │ │ └── open-source.jpg │ │ ├── mobile-logo.png │ │ ├── user │ │ ├── corner.png │ │ ├── user.jpg │ │ ├── user.png │ │ └── user-bg.png │ │ ├── error-page │ │ ├── 404.png │ │ └── logo.png │ │ └── login │ │ ├── login-ba.png │ │ ├── login-btn.png │ │ ├── nickname.png │ │ ├── password.png │ │ └── team-name.png ├── component │ ├── base │ │ ├── sticky-top │ │ │ └── sticky-top.vue │ │ ├── dialog │ │ │ └── lin-dialog.vue │ │ ├── icon │ │ │ └── lin-icon.vue │ │ ├── dropdown │ │ │ └── lin-dropdown.vue │ │ ├── source-code │ │ │ └── source-code.vue │ │ ├── search │ │ │ └── lin-search.vue │ │ ├── date-picker │ │ │ └── lin-date-picker.vue │ │ └── tinymce │ │ │ └── index.vue │ ├── layout │ │ ├── index.js │ │ ├── clear-tab.vue │ │ ├── app-main.vue │ │ ├── breadcrumb.vue │ │ ├── sidebar │ │ │ ├── logo.vue │ │ │ ├── menu-tree.vue │ │ │ ├── sidebar.vue │ │ │ └── search.vue │ │ ├── screen-full.vue │ │ ├── back-top.vue │ │ ├── menu-tab.vue │ │ ├── avatar.vue │ │ └── nav-bar.vue │ └── notify │ │ ├── emitter.js │ │ ├── index.js │ │ └── observer.js ├── router │ ├── route.js │ ├── home-router.js │ └── index.js ├── store │ ├── action.js │ ├── mutation-type.js │ ├── state.js │ ├── index.js │ └── mutation.js ├── view │ ├── error-page │ │ └── 404.vue │ └── admin │ │ ├── group │ │ ├── group-permission.vue │ │ ├── group-form-dialog.vue │ │ ├── group-edit.vue │ │ └── group-create.vue │ │ ├── permission │ │ ├── permission-edit-dialog.vue │ │ └── permission-list.vue │ │ ├── user │ │ ├── user-password.vue │ │ └── user-form-dialog.vue │ │ ├── setting │ │ └── setting-form-dialog.vue │ │ └── log │ │ └── serilog-dialog.vue ├── app.vue └── main.js ├── public ├── robots.txt ├── favicon.ico ├── icons │ └── your-icon.png ├── static │ └── img │ │ └── logo.png ├── tinymce │ └── skins │ │ ├── ui │ │ └── oxide │ │ │ ├── fonts │ │ │ └── tinymce-mobile.woff │ │ │ └── content.mobile.min.css │ │ └── content │ │ ├── default │ │ ├── content.min.css │ │ └── content.css │ │ ├── writer │ │ ├── content.min.css │ │ └── content.css │ │ └── document │ │ ├── content.min.css │ │ └── content.css └── index.html ├── tests └── unit │ └── .eslintrc.js ├── postcss.config.js ├── .eslintignore ├── script ├── template │ ├── plugin │ │ ├── asset │ │ │ └── image │ │ │ │ └── logo.png │ │ ├── package.json.ejs │ │ ├── README.md.ejs │ │ ├── view │ │ │ ├── stage2.vue.ejs │ │ │ └── stage1.vue.ejs │ │ ├── stage-config.js.ejs │ │ └── component │ │ │ └── component.vue.ejs │ └── plugin-stage-config.js.ejs ├── lib │ ├── util.js │ ├── exec-promise.js │ ├── semver-validate.js │ ├── plugin-get-all.js │ └── install-dep.js ├── plugin-get-config.js └── plugin-init.js ├── .env.production ├── .env.development ├── .prettierrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── workflows │ ├── gitee-mirror.yml │ └── deploy-to-server.yml ├── babel.config.js ├── .editorconfig ├── .travis.yml ├── .gitignore ├── jest.config.js ├── vue.config.js ├── LICENSE ├── .eslintrc.js └── package.json /src/lin/plugin/index.js: -------------------------------------------------------------------------------- 1 | import './axios' 2 | -------------------------------------------------------------------------------- /src/lin/util/index.js: -------------------------------------------------------------------------------- 1 | export * from './date' 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/lin/directive/index.js: -------------------------------------------------------------------------------- 1 | import './authorize' 2 | -------------------------------------------------------------------------------- /src/config/global.js: -------------------------------------------------------------------------------- 1 | window.MAX_SUCCESS_CODE = 9998 2 | -------------------------------------------------------------------------------- /src/lin/util/emitter.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | export default mitt() 4 | -------------------------------------------------------------------------------- /src/lin/plugin/README.md: -------------------------------------------------------------------------------- 1 | # Lin-plugins 2 | 3 | 文件夹内容描述 4 | 5 | 更多内容请查看 [文档](#) 6 | -------------------------------------------------------------------------------- /src/plugin/custom/README.md: -------------------------------------------------------------------------------- 1 | # 插件名: 自定义组件展示 2 | 3 | 展示 Lin-CMS 自行封装的组件,开发者可根据需求选择是否使用。 -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/style/shared.scss: -------------------------------------------------------------------------------- 1 | @import './realize/mixin.scss'; 2 | @import './realize/variable.scss'; 3 | -------------------------------------------------------------------------------- /public/icons/your-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/public/icons/your-icon.png -------------------------------------------------------------------------------- /public/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/public/static/img/logo.png -------------------------------------------------------------------------------- /src/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/logo.png -------------------------------------------------------------------------------- /src/assets/image/about/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/icon.png -------------------------------------------------------------------------------- /src/assets/image/about/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/icon1.png -------------------------------------------------------------------------------- /src/assets/image/about/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/icon2.png -------------------------------------------------------------------------------- /src/assets/image/about/icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/icon3.png -------------------------------------------------------------------------------- /src/assets/image/about/icon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/icon4.png -------------------------------------------------------------------------------- /src/assets/image/mobile-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/mobile-logo.png -------------------------------------------------------------------------------- /src/assets/image/user/corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/user/corner.png -------------------------------------------------------------------------------- /src/assets/image/user/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/user/user.jpg -------------------------------------------------------------------------------- /src/assets/image/user/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/user/user.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /builds/ 2 | /public/ 3 | /dist/ 4 | /script/.cache 5 | /*.js 6 | /node_modules/ 7 | /tests/unit/LIcon.test.js 8 | -------------------------------------------------------------------------------- /src/assets/image/about/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/avatar.png -------------------------------------------------------------------------------- /src/assets/image/about/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/qrcode.jpg -------------------------------------------------------------------------------- /src/assets/image/about/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/welcome.png -------------------------------------------------------------------------------- /src/assets/image/user/user-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/user/user-bg.png -------------------------------------------------------------------------------- /src/assets/image/about/header-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/header-bg.png -------------------------------------------------------------------------------- /src/assets/image/about/team-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/team-icon.png -------------------------------------------------------------------------------- /src/assets/image/error-page/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/error-page/404.png -------------------------------------------------------------------------------- /src/assets/image/error-page/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/error-page/logo.png -------------------------------------------------------------------------------- /src/assets/image/login/login-ba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/login/login-ba.png -------------------------------------------------------------------------------- /src/assets/image/login/login-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/login/login-btn.png -------------------------------------------------------------------------------- /src/assets/image/login/nickname.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/login/nickname.png -------------------------------------------------------------------------------- /src/assets/image/login/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/login/password.png -------------------------------------------------------------------------------- /src/assets/image/login/team-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/login/team-name.png -------------------------------------------------------------------------------- /src/assets/image/about/open-source.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/image/about/open-source.jpg -------------------------------------------------------------------------------- /src/plugin/custom/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/plugin/custom/assets/image/logo.png -------------------------------------------------------------------------------- /src/assets/style/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/style/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/style/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/assets/style/fonts/element-icons.woff -------------------------------------------------------------------------------- /script/template/plugin/asset/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/script/template/plugin/asset/image/logo.png -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/src/plugin/lin-cms-ui/assets/image/logo.png -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'production' 2 | VUE_APP_BASE_URL = 'https://igeekfan.cn/lincms_api/' 3 | VUE_APP_VVLOG_URL = 'http://igeekfan.cn/cms' 4 | -------------------------------------------------------------------------------- /public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoyunchong/lin-cms-vue/HEAD/public/tinymce/skins/ui/oxide/fonts/tinymce-mobile.woff -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'development' 2 | # api 请求地址 3 | VUE_APP_BASE_URL = 'https://localhost:5001/' 4 | # lin-cms-vvlog运行的地址,用于直接跳转至原文查看 5 | VUE_APP_VVLOG_URL = 'http://localhost:8080/' 6 | -------------------------------------------------------------------------------- /src/lin/context/index.js: -------------------------------------------------------------------------------- 1 | import { useAdminProvide, useAdminInject } from './admin' 2 | 3 | export { useAdminInject } 4 | 5 | export const useProvide = () => { 6 | useAdminProvide() 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号 3 | semi: false, //行位是否使用分号,默认为true 4 | trailingComma: 'all', //是否使用尾逗号,有三个可选值"" 5 | printWidth: 120, 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/assets/style/realize/transition.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition: opacity .5s; 4 | } 5 | 6 | .fade-enter, 7 | .fade-leave-to 8 | 9 | /* .fade-leave-active below version 2.1.8 */ 10 | { 11 | opacity: 0; 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/plugin/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-plugin-lin-cms-ui", 3 | "title": "blog", 4 | "version": "1.0.0", 5 | "_linVersion": "0.0.1-alpha.3", 6 | "description": "", 7 | "author": "", 8 | "dependencies": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | "devDependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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%} -------------------------------------------------------------------------------- /src/component/base/sticky-top/sticky-top.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/plugin/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc-plugin-base", 3 | "title": "字典管理", 4 | "version": "1.0.0", 5 | "_linVersion": "0.0.1-alpha.3", 6 | "description": "实现字典管理-字典类别管理", 7 | "author": "IGeekFan", 8 | "dependencies": {}, 9 | "devDependencies": {} 10 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | filePath: 'view/center/', // 文件路径 8 | order: null, 9 | inNav: false, 10 | } 11 | 12 | export default centerRouter 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/config/stage/plugin.js: -------------------------------------------------------------------------------- 1 | // 本文件是自动生成, 请勿修改 2 | import base from '@/plugin/base/stage-config' 3 | import blog from '@/plugin/blog/stage-config' 4 | import custom from '@/plugin/custom/stage-config' 5 | import linCmsUi from '@/plugin/lin-cms-ui/stage-config' 6 | 7 | const pluginsConfig = [ 8 | base, 9 | blog, 10 | custom, 11 | linCmsUi, 12 | ] 13 | 14 | export default pluginsConfig 15 | -------------------------------------------------------------------------------- /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/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/component/base/dialog/lin-dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/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/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/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 | 10 | .lin-title { 11 | height: 59px; 12 | line-height: 59px; 13 | color: $parent-title-color; 14 | font-size: 16px; 15 | font-weight: 500; 16 | text-indent: 40px; 17 | border-bottom: 1px solid #dae1ed; 18 | } 19 | 20 | .lin-wrap { 21 | padding: 20px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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/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/assets/style/form.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-bottom: 20px; 3 | 4 | .title { 5 | height: 59px; 6 | line-height: 59px; 7 | color: $parent-title-color; 8 | font-size: 16px; 9 | font-weight: 500; 10 | text-indent: 40px; 11 | border-bottom: 1px solid #dae1ec; 12 | 13 | .back { 14 | float: right; 15 | margin-right: 40px; 16 | cursor: pointer; 17 | } 18 | } 19 | 20 | .wrap { 21 | padding: 20px; 22 | } 23 | 24 | .submit { 25 | float: left; 26 | } 27 | } 28 | 29 | .el-divider--horizontal { 30 | margin: 0; 31 | } 32 | -------------------------------------------------------------------------------- /src/component/layout/clear-tab.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/gitee-mirror.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Gitee Mirror💕 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - vue-next 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Sync to Gitee 💕 15 | uses: wearerequired/git-mirror-action@master 16 | env: 17 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 18 | with: 19 | source-repo: "git@github.com:luoyunchong/lin-cms-vue.git" 20 | destination-repo: "git@gitee.com:igeekfan/lin-cms-vue.git" 21 | -------------------------------------------------------------------------------- /src/plugin/custom/view/tinymce.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/assets/style/container.scss: -------------------------------------------------------------------------------- 1 | .lin-wrap-ui :deep(.el-card__body) { 2 | padding-top: 30px; 3 | padding-bottom: 0px; 4 | } 5 | .lin-wrap-ui :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/component/layout/app-main.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /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/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: 76px; 10 | $reuse-tab-height: 30px; 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 | -------------------------------------------------------------------------------- /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 | 方法参数描述 | 返回值描述 | 描述 | -------------------------------------------------------------------------------- /src/component/base/icon/lin-icon.vue: -------------------------------------------------------------------------------- 1 | 6 | 29 | 37 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/plugin/blog/model/classify.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | class Classify { 4 | async addClassify(params) { 5 | const res = await post("api/blog/classifies", params); 6 | return res; 7 | } 8 | 9 | async getClassify(id) { 10 | const res = await get(`api/blog/classifies/${id}`); 11 | return res; 12 | } 13 | 14 | async editClassify(id, data) { 15 | const res = await put(`api/blog/classifies/${id}`, data); 16 | return res; 17 | } 18 | 19 | async deleteClassify(id) { 20 | const res = await _delete(`api/blog/classifies/${id}`); 21 | return res; 22 | } 23 | 24 | async getClassifies(params) { 25 | const res = await get("api/blog/classifies", params); 26 | return res; 27 | } 28 | } 29 | 30 | export default new Classify(); 31 | -------------------------------------------------------------------------------- /src/plugin/blog/model/comment.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | class Comment { 4 | 5 | async addComment(info) { 6 | const res = await post("api/blog/comments/", info); 7 | return res; 8 | } 9 | 10 | async getComment(id) { 11 | const res = await get(`api/blog/comments/${id}`); 12 | return res; 13 | } 14 | 15 | async editComment(id, info) { 16 | const res = await put(`api/blog/comments/${id}?is_audit=${info}`); 17 | return res; 18 | } 19 | 20 | async delectComment(id) { 21 | const res = await _delete(`api/blog/comments/cms/${id}`); 22 | return res; 23 | } 24 | 25 | async getComments(pagesParmas) { 26 | const res = await get("api/blog/comments/", pagesParmas); 27 | return res; 28 | } 29 | } 30 | 31 | export default new Comment(); 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/lin/model/setting.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | class Setting { 4 | async addSetting(params) { 5 | const res = await post("cms/settings", params); 6 | return res; 7 | } 8 | 9 | async editSetting(id, params) { 10 | const res = await put(`cms/settings/${id}`, params); 11 | return res; 12 | } 13 | 14 | async getSetting(id) { 15 | const res = await get(`cms/settings/${id}`); 16 | return res; 17 | } 18 | 19 | async deleteSetting(id) { 20 | const res = await _delete(`cms/settings/${id}`); 21 | return res; 22 | } 23 | 24 | async getSettings(params) { 25 | const res = await get("cms/settings", params); 26 | return res; 27 | } 28 | } 29 | 30 | export default new Setting(); 31 | -------------------------------------------------------------------------------- /src/plugin/base/stage-config.js: -------------------------------------------------------------------------------- 1 | const baseRouter = { 2 | route: null, 3 | name: null, 4 | title: "基础资料", 5 | type: "folder", // 类型: folder, tab, view 6 | icon: "iconfont icon-tushuguanli", 7 | order: null, 8 | inNav: true, 9 | children: [ 10 | { 11 | name: null, 12 | title: "字典管理", 13 | type: "view", 14 | name: "ItemList", 15 | route: "/base/item", 16 | icon: 'iconfont icon-tuichu', 17 | filePath: "plugin/base/view/item/item-list.vue", 18 | inNav: true 19 | }, 20 | { 21 | name: null, 22 | title: "字典类别管理", 23 | type: "view", 24 | name: "TypeList", 25 | icon: 'iconfont icon-top', 26 | route: "/base/type", 27 | filePath: "plugin/base/view/type/type-list.vue", 28 | inNav: true 29 | } 30 | ] 31 | }; 32 | 33 | export default baseRouter; 34 | -------------------------------------------------------------------------------- /src/plugin/blog/model/channel.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | class Channel { 4 | async addChannel(params) { 5 | const res = await post("api/blog/channels", params); 6 | return res; 7 | } 8 | 9 | async getChannel(id) { 10 | const res = await get(`api/blog/channels/${id}`); 11 | return res; 12 | } 13 | 14 | async editChannel(id, data) { 15 | const res = await put(`api/blog/channels/${id}`, data); 16 | return res; 17 | } 18 | 19 | async deleteChannel(id) { 20 | const res = await _delete(`api/blog/channels/${id}`); 21 | return res; 22 | } 23 | 24 | async getChannels(params) { 25 | const res = await get("api/blog/channels", params); 26 | return res; 27 | } 28 | } 29 | 30 | export default new Channel(); 31 | -------------------------------------------------------------------------------- /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/plugin/blog/model/tag.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | class Tag { 4 | async addTag(params) { 5 | const res = await post("api/blog/tags", params); 6 | return res; 7 | } 8 | 9 | async getTag(id) { 10 | const res = await get(`api/blog/tags/${id}`); 11 | return res; 12 | } 13 | 14 | async editTag(id, data) { 15 | const res = await put(`api/blog/tags/${id}`, data); 16 | return res; 17 | } 18 | 19 | async correctTagCount(id) { 20 | const res = await put(`api/blog/tags/correct/${id}`); 21 | return res; 22 | } 23 | 24 | 25 | async deleteTag(id) { 26 | const res = await _delete(`api/blog/tags/${id}`); 27 | return res; 28 | } 29 | 30 | async getTags(params) { 31 | const res = await get("api/blog/tags", params); 32 | return res; 33 | } 34 | } 35 | 36 | export default new Tag(); 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/template/plugin/view/stage2.vue.ejs: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/component/component.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/component/base/dropdown/lin-dropdown.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 33 | 34 | 40 | -------------------------------------------------------------------------------- /script/template/plugin/component/component.vue.ejs: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /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 | publicPath: '/cms/', 9 | lintOnSave: false, 10 | productionSourceMap: false, 11 | // assetsDir: 'static', 12 | chainWebpack: config => { 13 | config.resolve.alias.set('@', resolve('src')).set('lin', resolve('src/lin')).set('assets', resolve('src/assets')) 14 | config.module.rule('ignore').test(/\.md$/).use('ignore-loader').loader('ignore-loader').end() 15 | }, 16 | configureWebpack: { 17 | devtool: 'source-map', 18 | resolve: { 19 | extensions: ['.js', '.json', '.vue', '.scss', '.html'], 20 | }, 21 | }, 22 | css: { 23 | loaderOptions: { 24 | sass: { 25 | prependData: `@import "@/assets/style/shared.scss";`, 26 | }, 27 | }, 28 | }, 29 | devServer: { 30 | client: { 31 | overlay: false 32 | } 33 | }, 34 | // node_modules依赖项es6语法未转换问题 35 | transpileDependencies: ['vuex-persist'], 36 | } 37 | -------------------------------------------------------------------------------- /src/lin/model/notify.js: -------------------------------------------------------------------------------- 1 | import { post, get, put } from '@/lin/plugin/axios' 2 | import Config from '../../config' 3 | import Sse from '../util/sse' 4 | 5 | export default class Notify { 6 | url = null 7 | 8 | events = null 9 | 10 | sse = null 11 | 12 | constructor(url) { 13 | this.url = url 14 | } 15 | 16 | async getEvents() { 17 | const res = await get('cms/notify/events') 18 | this.events = res.events 19 | } 20 | 21 | async initSse() { 22 | await this.getEvents() 23 | this.sse = new Sse(Config.baseUrl + this.url, this.events) 24 | } 25 | 26 | /** 27 | * 创建events 28 | * @param {number} group_id 29 | * @param {Array} events 30 | */ 31 | async createEvents(group_id, events) { 32 | const res = await post('cms/notify/events', { group_id, events }) 33 | return res 34 | } 35 | 36 | /** 37 | * 更新events 38 | * @param {number} group_id 39 | * @param {Array} events 40 | */ 41 | async updateEvents(group_id, events) { 42 | const res = await put('cms/notify/events', { group_id, events }) 43 | return res 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/view/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 45 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /src/assets/style/list.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 8px 8px; 3 | margin-bottom: 20px; 4 | 5 | .header { 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | 10 | .header-left { 11 | float: left; 12 | 13 | .title { 14 | height: 59px; 15 | line-height: 59px; 16 | color: $parent-title-color; 17 | font-size: 16px; 18 | font-weight: 500; 19 | 20 | .back { 21 | float: right; 22 | margin-right: 40px; 23 | cursor: pointer; 24 | } 25 | } 26 | } 27 | 28 | .header-right { 29 | float: right; 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | } 34 | } 35 | 36 | .wrap { 37 | padding: 20px; 38 | } 39 | 40 | .pagination { 41 | display: flex; 42 | justify-content: flex-end; 43 | margin: 20px; 44 | } 45 | 46 | .submit { 47 | float: right; 48 | } 49 | 50 | .thumb { 51 | background-size: contain; 52 | width: 100%; 53 | height: 32px; 54 | background-repeat: no-repeat; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/component/base/source-code/source-code.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 49 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.props = viewConfig.props 32 | viewRouter.component = () => import(`@/${viewConfig.filePath}`) 33 | viewRouter.meta = { 34 | title: viewConfig.title, 35 | icon: viewConfig.icon, 36 | permission: viewConfig.permission, 37 | type: viewConfig.type, 38 | blueBaseColor: viewConfig.blueBaseColor ? 'viewConfig.blueBaseColor' : '', 39 | } 40 | homeRouter.push(viewRouter) 41 | }) 42 | 43 | export default homeRouter 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /script/template/plugin/view/stage1.vue.ejs: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | 51 | -------------------------------------------------------------------------------- /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/blog/model/article.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from "@/lin/plugin/axios"; 2 | 3 | 4 | class Article { 5 | 6 | 7 | 8 | async addArticle(info) { 9 | const res = await post("api/blog/articles/", info); 10 | return res; 11 | } 12 | 13 | async getArticle(id) { 14 | const res = await get(`api/blog/articles/${id}`); 15 | return res; 16 | } 17 | 18 | async editArticle(id, info) { 19 | const res = await put(`api/blog/articles/${id}`, info); 20 | return res; 21 | } 22 | 23 | async auditArticle(id, is_audit) { 24 | const res = await put(`api/blog/articles/audit/${id}?is_audit=${is_audit}`); 25 | return res; 26 | } 27 | 28 | 29 | async deleteArticle(id) { 30 | const res = await _delete(`api/blog/articles/${id}`); 31 | return res; 32 | } 33 | 34 | async deleteCmsArticle(id) { 35 | const res = await _delete(`api/blog/articles/cms/${id}`); 36 | return res; 37 | } 38 | 39 | async getArticles(pagesParmas) { 40 | const res = await get("api/blog/articles/", pagesParmas); 41 | return res; 42 | } 43 | 44 | async getAllArticles(pagesParmas) { 45 | const res = await get("api/blog/articles/all", pagesParmas); 46 | return res; 47 | } 48 | } 49 | 50 | export default new Article(); 51 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/component/layout/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 62 | -------------------------------------------------------------------------------- /src/component/base/search/lin-search.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 45 | 60 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/logo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 64 | -------------------------------------------------------------------------------- /src/plugin/base/model/base.js: -------------------------------------------------------------------------------- 1 | import { 2 | post, 3 | get, 4 | put, 5 | _delete, 6 | } from '@/lin/plugin/axios' 7 | 8 | class Base { 9 | 10 | async addItem(params) { 11 | const res = await post('api/base/item', params) 12 | return res 13 | } 14 | 15 | async getItem(id) { 16 | const res = await get(`api/base/item/${id}`) 17 | return res 18 | } 19 | 20 | async editItem(id, data) { 21 | const res = await put(`api/base/item/${id}`, data) 22 | return res 23 | } 24 | 25 | async deleteItem(id) { 26 | const res = await _delete(`api/base/item/${id}`) 27 | return res 28 | } 29 | 30 | async getItems(params) { 31 | const res = await get('api/base/item', params) 32 | return res 33 | } 34 | 35 | async addType(params) { 36 | const res = await post('api/base/type', params) 37 | return res 38 | } 39 | 40 | async getType(id) { 41 | const res = await get(`api/base/type/${id}`) 42 | return res 43 | } 44 | 45 | async editType(id, data) { 46 | const res = await put(`api/base/type/${id}`, data) 47 | return res 48 | } 49 | 50 | async deleteType(id) { 51 | const res = await _delete(`api/base/type/${id}`) 52 | return res 53 | } 54 | 55 | async getTypes(params) { 56 | const res = await get('api/base/type', params) 57 | return res 58 | } 59 | } 60 | 61 | export default new Base() 62 | -------------------------------------------------------------------------------- /src/assets/style/realize/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade { 2 | from { 3 | opacity: 0.1; 4 | } 5 | 6 | to { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | @keyframes img-add-view { 12 | 0% { 13 | opacity: 0; 14 | transform: scale(0.8); 15 | } 16 | 17 | 100% { 18 | opacity: 1; 19 | transform: scale(1); 20 | } 21 | } 22 | 23 | // 添加单品 24 | @keyframes arr-item-add { 25 | 0% { 26 | opacity: 0; 27 | transform: translateY(-100%); 28 | } 29 | 30 | 100% { 31 | opacity: 1; 32 | transform: translateY(0%); 33 | } 34 | } 35 | 36 | // 删除单品 37 | @keyframes arr-item-reduce { 38 | 0% { 39 | opacity: 1; 40 | transform: translateY(0%); 41 | } 42 | 43 | 100% { 44 | opacity: 0; 45 | transform: translateY(100%); 46 | } 47 | } 48 | 49 | // 页面切换动画 50 | .fade-transform-leave-active, 51 | .fade-transform-enter-active { 52 | transition: all 0.3s; 53 | } 54 | 55 | .fade-transform-enter { 56 | opacity: 0; 57 | transform: translateX(-30px); 58 | } 59 | 60 | .fade-transform-leave-to { 61 | opacity: 0; 62 | transform: translateX(30px); 63 | } 64 | 65 | .fadeChild-transform-leave-active, 66 | .fadeChild-transform-enter-active { 67 | transition: all 0.3s; 68 | } 69 | 70 | .fadeChild-transform-enter { 71 | opacity: 0; 72 | transform: translateX(-30px); 73 | } 74 | 75 | .fadeChild-transform-leave-to { 76 | opacity: 0; 77 | transform: translateX(30px); 78 | } 79 | -------------------------------------------------------------------------------- /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/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/view/admin/group/group-permission.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/assets/style/realize/mixin.scss: -------------------------------------------------------------------------------- 1 | // 根据不同的屏幕加载背景图片 2 | @mixin bi($url, $type: 'png') { 3 | background-image: url($url + "@2x." + $type); 4 | 5 | @media (-webkit-min-device-pixel-ratio: 3), 6 | (min-device-pixel-ratio: 3) { 7 | background-image: url($url + "@3x." + $type); 8 | background-size: cover; 9 | } 10 | } 11 | 12 | // 文字超出后以...显示 13 | @mixin no-wrap { 14 | text-overflow: ellipsis; 15 | overflow: hidden; 16 | white-space: nowrap; 17 | } 18 | 19 | // 扩展小图标按钮的点击区域 20 | @mixin extend-click() { 21 | position: relative; 22 | 23 | &:before { 24 | content: ''; 25 | position: absolute; 26 | top: -10px; 27 | left: -10px; 28 | right: -10px; 29 | bottom: -10px; 30 | } 31 | } 32 | 33 | @mixin btn-scale-hover() { 34 | transition: .4s all; 35 | 36 | &:hover { 37 | transform: scale(1.1); 38 | } 39 | } 40 | 41 | @mixin bi-saturate-hover($background-color, $value: 20) { 42 | // 背景颜色增加饱和度 43 | transition: .6s all; 44 | 45 | &:hover { 46 | background-color: saturate($background-color, $value); 47 | } 48 | } 49 | 50 | @mixin color-saturate-hover($color, $value: 30) { 51 | // 颜色增加饱和度 52 | transition: .6s all; 53 | 54 | &:hover { 55 | color: saturate($color, $value); 56 | } 57 | } 58 | 59 | @mixin bi-lighten-hover($background-color, $value: 10) { 60 | // 背景颜色变浅。 61 | transition: .6s all; 62 | 63 | &:hover { 64 | background-color: lighten($background-color, $value); 65 | } 66 | } 67 | 68 | @mixin bi-opacity-hover($opacity: .8) { 69 | // 透明度 70 | transition: .4s all; 71 | 72 | &:hover { 73 | opacity: $opacity; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'dayjs/locale/zh-cn' 2 | import { createApp } from 'vue' 3 | import ElementPlus from 'element-plus' 4 | import zh from 'element-plus/es/locale/lang/zh-cn' 5 | 6 | import '@/config/global' 7 | import 'lin/plugin' 8 | import { filters } from 'lin/filter' 9 | import permissionDirective from 'lin/directive/authorize' 10 | 11 | import App from '@/app.vue' 12 | import store from '@/store' 13 | import router from '@/router' 14 | 15 | import LinNotify from '@/component/notify' 16 | import LIcon from '@/component/base/icon/lin-icon' 17 | import StickyTop from '@/component/base/sticky-top/sticky-top' 18 | import SourceCode from '@/component/base/source-code/source-code' 19 | 20 | import '@/assets/style/index.scss' 21 | import 'element-plus/dist/index.css' 22 | import '@/assets/style/realize/element-variable.scss' 23 | import '@/assets/style/main.scss'; 24 | 25 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 26 | 27 | const app = createApp(App) 28 | 29 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 30 | app.component(key, component) 31 | } 32 | app.use(store) 33 | app.use(router) 34 | app.use(ElementPlus, { zh }) 35 | app.use(LinNotify, { 36 | reconnection: true, 37 | reconnectionAttempts: 5, 38 | reconnectionDelay: 3000, 39 | }) 40 | 41 | // base 组件注册 42 | app.component('l-icon', LIcon) 43 | app.component('sticky-top', StickyTop) 44 | app.component('source-code', SourceCode) 45 | 46 | app.config.globalProperties.$filters = filters 47 | 48 | app.directive('permission', permissionDirective) 49 | 50 | app.mount('#app') 51 | 52 | // 设置 App 实例 53 | window.App = app 54 | -------------------------------------------------------------------------------- /src/config/stage/index.js: -------------------------------------------------------------------------------- 1 | import adminConfig from './admin' 2 | import pluginsConfig from './plugin' 3 | import Utils from '@/lin/util/util' 4 | 5 | let homeRouter = [ 6 | { 7 | title: '林间有风', 8 | type: 'view', 9 | name: 'about', 10 | route: '/about', 11 | filePath: 'view/about/about.vue', 12 | inNav: true, 13 | icon: 'iconfont icon-iconset0103', 14 | order: 1, 15 | }, 16 | 17 | { 18 | title: '个人中心', 19 | type: 'view', 20 | name: Symbol('center'), 21 | route: '/center', 22 | filePath: 'view/center/center.vue', 23 | inNav: false, 24 | icon: 'iconfont icon-rizhiguanli', 25 | }, 26 | { 27 | title: '404', 28 | type: 'view', 29 | name: Symbol('404'), 30 | route: '/404', 31 | filePath: 'view/error-page/404.vue', 32 | inNav: false, 33 | icon: 'iconfont icon-rizhiguanli', 34 | }, 35 | adminConfig, 36 | ] 37 | 38 | // 接入插件 39 | const plugins = [...pluginsConfig] 40 | filterPlugin(homeRouter) 41 | homeRouter = homeRouter.concat(plugins) 42 | 43 | // 处理顺序 44 | homeRouter = Utils.sortByOrder(homeRouter) 45 | 46 | export default homeRouter 47 | 48 | /** 49 | * 筛除已经被添加的插件 50 | */ 51 | function filterPlugin(data) { 52 | if (plugins.length === 0) { 53 | return 54 | } 55 | if (Array.isArray(data)) { 56 | data.forEach(item => { 57 | filterPlugin(item) 58 | }) 59 | } else { 60 | const findResult = plugins.findIndex(item => data === item) 61 | if (findResult >= 0) { 62 | plugins.splice(findResult, 1) 63 | } 64 | if (data.children) { 65 | filterPlugin(data.children) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-server.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: Deploy To Server 💕 3 | 4 | # Controls when the action will run. Triggers the workflow on push or pull request 5 | # events but only for the main branch 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - vue-next 11 | pull_request: 12 | branches: [main] 13 | 14 | jobs: 15 | deploy-gh-pages: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | # if your docs needs submodules, uncomment the following line 23 | # submodules: true 24 | 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v2 27 | with: 28 | version: 7 29 | run_install: true 30 | 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 16 35 | cache: pnpm 36 | 37 | - name: Install Deps 38 | run: pnpm install 39 | 40 | - name: Build Docs 41 | env: 42 | NODE_OPTIONS: --max_old_space_size=4096 43 | run: pnpm run build 44 | 45 | # https://github.com/marketplace/actions/ssh-deploy 46 | - name: Deploy to Server 47 | uses: easingthemes/ssh-deploy@v2.1.5 48 | env: 49 | SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} 50 | ARGS: '--delete' 51 | SOURCE: 'dist/' 52 | REMOTE_HOST: ${{ secrets.HOST }} 53 | REMOTE_USER: ${{ secrets.USER }} 54 | TARGET: ${{ secrets.REMOTE_TARGET }} 55 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/menu-tree.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | 64 | -------------------------------------------------------------------------------- /src/component/layout/screen-full.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 49 | 50 | 75 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/sidebar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 58 | 59 | 74 | -------------------------------------------------------------------------------- /src/component/base/date-picker/lin-date-picker.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 67 | -------------------------------------------------------------------------------- /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/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(data) { 11 | return _axios({ 12 | method: 'post', 13 | url: 'cms/user/register', 14 | data, 15 | handleError: true, 16 | }) 17 | } 18 | 19 | /** 20 | * 登陆获取tokens 21 | * @param { String } username 用户名 22 | * @param { String } password 密码 23 | * @param { String } captcha 验证码 24 | * @param { String } tag 验证码签名 25 | */ 26 | static async getToken(username, password, captcha, tag) { 27 | const tokens = await _axios({ 28 | url: 'cms/user/login', 29 | method: 'POST', 30 | data: { 31 | captcha, 32 | username, 33 | password, 34 | }, 35 | headers: { 36 | tag, 37 | }, 38 | }) 39 | saveTokens(tokens.access_token, tokens.refresh_token) 40 | return tokens 41 | } 42 | 43 | /** 44 | * 获取当前用户信息,并返回User实例 45 | */ 46 | static async getInformation() { 47 | const info = await get('cms/user/information') 48 | const storeUser = store.getters.user === null ? {} : store.getters.user 49 | return Object.assign({ ...storeUser }, info) 50 | } 51 | 52 | /** 53 | * 获取当前用户信息和所拥有的权限 54 | */ 55 | static async getPermissions() { 56 | const info = await get('cms/user/permissions') 57 | const storeUser = store.getters.user === null ? {} : store.getters.user 58 | return Object.assign({ ...storeUser }, info) 59 | } 60 | 61 | /** 62 | * 用户修改密码 63 | * @param {string} newPassword 新密码 64 | * @param {string} confirmPassword 确认新密码 65 | * @param {string} oldPassword 旧密码 66 | */ 67 | static updatePassword({ old_password, new_password, confirm_password }) { 68 | return put('cms/user/change_password', { 69 | new_password, 70 | confirm_password, 71 | old_password, 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/component/layout/back-top.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 62 | 63 | 81 | -------------------------------------------------------------------------------- /src/assets/style/vditor-preview.scss: -------------------------------------------------------------------------------- 1 | #preview :deep { 2 | color: #24292e; 3 | padding: 0px 12px 20px 12px !important; 4 | 5 | ul li, 6 | a, 7 | p { 8 | line-height: 1.6; 9 | font-size: 15px !important; 10 | } 11 | 12 | img { 13 | max-width: 100%; 14 | width: auto !important; 15 | height: auto !important; 16 | 17 | &:hover { 18 | cursor: zoom-in; 19 | } 20 | } 21 | 22 | .code-pre { 23 | color: #333; 24 | border-radius: 2px; 25 | 26 | .hljs-keyword { 27 | color: #00f; 28 | } 29 | 30 | .hljs-title, 31 | .hljs-params, 32 | .hljs-function { 33 | color: #333; 34 | } 35 | 36 | .hljs-number { 37 | color: #361da3; 38 | } 39 | 40 | .hljs-meta { 41 | color: #2b91af; 42 | } 43 | 44 | .hljs-string { 45 | color: #a31515; 46 | } 47 | } 48 | 49 | h1, 50 | h2, 51 | h3, 52 | h4, 53 | h5, 54 | h6 { 55 | color: #333; 56 | line-height: 1.25; 57 | margin-top: 24px; 58 | margin-bottom: 16px; 59 | padding-bottom: 5px; 60 | } 61 | 62 | strong { 63 | font-weight: 600; 64 | } 65 | 66 | p, 67 | blockquote, 68 | ul, 69 | ol, 70 | dl, 71 | table, 72 | pre { 73 | margin-top: 0; 74 | margin-bottom: 16px; 75 | } 76 | 77 | h1 { 78 | font-size: 30px; 79 | margin-bottom: 5px; 80 | } 81 | 82 | h2 { 83 | margin-top: 20px; 84 | border-bottom: 1px solid #eaecef; 85 | } 86 | 87 | h3 { 88 | margin-top: 10px; 89 | } 90 | 91 | h4, 92 | h5, 93 | h6 { 94 | margin-top: 5px; 95 | } 96 | 97 | ul li:after { 98 | width: 4px; 99 | height: 4px; 100 | } 101 | 102 | ul li input[type='checkbox']:before { 103 | z-index: 199 !important; 104 | } 105 | 106 | blockquote p { 107 | margin-bottom: 0px; 108 | } 109 | 110 | a { 111 | color: #4285f4; 112 | 113 | &:hover { 114 | text-decoration: underline; 115 | } 116 | } 117 | 118 | .preview-img .close { 119 | right: 22px; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.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 | // @off 要求太严格了,有时需要在循环中写 await 13 | "no-return-await": "off", 14 | "no-await-in-loop": "off", 15 | 'vue/script-setup-uses-vars': 'off', 16 | 'vue/custom-event-name-casing': 'off', 17 | 'vuejs-accessibility/rule-name': 'off', 18 | 'vue/no-deprecated-slot-attribute': 'off', 19 | 'vue/experimental-script-setup-vars': 'off', 20 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 21 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 22 | 'no-shadow': 0, 23 | 'no-plusplus': 0, // 禁止使用++,-- 24 | 'guard-for-in': 0, 25 | 'no-extra-semi': 0, // 和prettier冲突 26 | 'import/extensions': 0, // import不需要写文件扩展名 27 | semi: ['error', 'never'], // 无分号 28 | 'import/no-unresolved': 0, 29 | 'no-restricted-syntax': 0, 30 | 'no-underscore-dangle': 0, // 无下划线 31 | 'no-restricted-syntax': 0, 32 | 'consistent-return': 'off', 33 | 'max-len': ['error', { code: 200 }], 34 | 'no-use-before-define': 'off', 35 | 'no-prototype-builtins': 'off', 36 | 'class-methods-use-this': 'off', 37 | 'template-curly-spacing': 'off', 38 | 'arrow-parens': ['error', 'as-needed'], 39 | 'vue/multi-word-component-names': 'off', 40 | 'comma-dangle': ['error', 'only-multiline'], 41 | 'no-param-reassign': ['error', { props: false }], 42 | 'vuejs-accessibility/click-events-have-key-events': 'off', 43 | 'import/no-extraneous-dependencies': ['error', { devDependencies: ['script/**/*.js'] }], 44 | indent: [ 45 | 'warn', 46 | 2, 47 | { 48 | ignoredNodes: ['TemplateLiteral'], 49 | SwitchCase: 1, 50 | }, 51 | ], 52 | 'object-curly-newline': [ 53 | 'error', 54 | { 55 | ImportDeclaration: 'never', 56 | }, 57 | ], 58 | }, 59 | parserOptions: { 60 | parser: '@babel/eslint-parser', 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/component/layout/menu-tab.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | 91 | -------------------------------------------------------------------------------- /src/plugin/blog/stage-config.js: -------------------------------------------------------------------------------- 1 | const blogRouter = { 2 | route: null, 3 | name: null, 4 | title: "工作台", 5 | type: "folder", // 类型: folder, tab, view 6 | icon: "iconfont icon-tushuguanli", 7 | filePath: "view/blog/", 8 | order: null, 9 | inNav: true, 10 | children: [ 11 | { 12 | name: null, 13 | title: "专栏管理", 14 | type: "view", 15 | name: "ClassifyList", 16 | route: "/cms/classify/list", 17 | filePath: "plugin/blog/view/classify/classify-list.vue", 18 | icon: 'iconfont icon-shangpinguanli', 19 | inNav: true, 20 | permission: ["分类专栏列表"] 21 | }, 22 | { 23 | name: null, 24 | title: "技术频道", 25 | type: "view", 26 | name: "ChannelList", 27 | route: "/cms/channel/list", 28 | icon: 'iconfont icon-tianjia', 29 | filePath: "plugin/blog/view/channel/channel-list.vue", 30 | inNav: true, 31 | permission: ["技术频道列表"] 32 | }, 33 | { 34 | name: null, 35 | title: "标签管理", 36 | type: "view", 37 | name: "TagList", 38 | route: "/cms/tag/list", 39 | icon: 'iconfont icon-shangpinguanli', 40 | filePath: "plugin/blog/view/tag/tag-list.vue", 41 | inNav: true, 42 | permission: ["所有标签"] 43 | }, 44 | { 45 | name: null, 46 | title: "随笔管理", 47 | type: "view", 48 | name: "ArticleList", 49 | route: "/cms/article/list", 50 | icon: 'iconfont icon-iconset0103', 51 | filePath: "plugin/blog/view/article/article-list.vue", 52 | inNav: true, 53 | permission: ['所有随笔'] 54 | }, 55 | { 56 | name: null, 57 | title: "随笔详情", 58 | type: "view", 59 | name: "ArticleForm", 60 | route: "/cms/article/form", 61 | icon: 'iconfont icon-iconset0103', 62 | filePath: "plugin/blog/view/article/article-form.vue", 63 | inNav: false 64 | }, 65 | { 66 | name: "CommentList", 67 | title: "评论管理", 68 | type: "view", 69 | route: "/comment/list", 70 | icon: 'iconfont icon-tuichu', 71 | filePath: "plugin/blog/view/comment/comment-list.vue", 72 | inNav: true, 73 | permission: ["评论列表"] 74 | }, 75 | { 76 | title: "博客详情页", 77 | type: "view", 78 | name: "ArticleDetail", 79 | route: "/post/:id", 80 | filePath: "plugin/blog/view/home/article-detail.vue", 81 | inNav: false 82 | } 83 | ] 84 | }; 85 | 86 | export default blogRouter; 87 | -------------------------------------------------------------------------------- /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 32 | ? element.thumb 33 | : 'https://consumerminiaclprd01.blob.core.chinacloudapi.cn/miniappbackground/sfgmember/lin/270-400.png', 34 | }) 35 | }) 36 | 37 | return arr 38 | } 39 | 40 | getDataByQuery(query = '') { 41 | const arr = [] 42 | for (let index = 0; index < movieList.length; index++) { 43 | const element = movieList[index] 44 | 45 | if (element.title.match(query)) { 46 | const tempCasts = [] 47 | const tempDirectors = [] 48 | element.casts.forEach(el => { 49 | tempCasts.push(el.name) 50 | }) 51 | element.directors.forEach(el => { 52 | tempDirectors.push(el.name) 53 | }) 54 | 55 | arr.push({ 56 | title: element.title, 57 | originalTitle: element.original_title, 58 | year: element.year, 59 | rating: element.rating.average, 60 | casts: tempCasts.join('/'), 61 | directors: tempDirectors.join('/'), 62 | genres: element.genres.join('/'), 63 | rank: index + 1, 64 | sorting: 50, 65 | recommend: 0, 66 | remark: '这是一部不错的电影', 67 | editFlag: false, 68 | thumb: element.thumb 69 | ? element.thumb 70 | : 'https://consumerminiaclprd01.blob.core.chinacloudapi.cn/miniappbackground/sfgmember/lin/270-400.png', 71 | }) 72 | } 73 | } 74 | 75 | return arr 76 | } 77 | } 78 | 79 | export default new Movie() 80 | -------------------------------------------------------------------------------- /src/view/admin/group/group-form-dialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 85 | -------------------------------------------------------------------------------- /src/view/admin/permission/permission-edit-dialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 85 | -------------------------------------------------------------------------------- /src/plugin/blog/view/comment/comment-dialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/component/layout/avatar.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 99 | -------------------------------------------------------------------------------- /src/view/admin/user/user-password.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 91 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 128 | &:focus { 129 | outline: -webkit-focus-ring-color auto 0px; 130 | } 131 | } 132 | 133 | /* custom */ 134 | 135 | a { 136 | color: #7e8c8d; 137 | -webkit-backface-visibility: hidden; 138 | text-decoration: none; 139 | cursor: pointer; 140 | } 141 | 142 | li { 143 | list-style: none; 144 | } 145 | 146 | img { 147 | display: block; 148 | width: 100%; 149 | height: auto; 150 | } 151 | 152 | body { 153 | line-height: 1; 154 | -moz-osx-font-smoothing: grayscale; 155 | -webkit-font-smoothing: antialiased; 156 | text-rendering: optimizeLegibility; 157 | } 158 | 159 | //*, 160 | //*:before, 161 | //*:after { 162 | // box-sizing: inherit; 163 | //} 164 | 165 | a:focus, 166 | a:active { 167 | outline: none; 168 | } 169 | 170 | a, 171 | a:focus, 172 | a:hover { 173 | cursor: pointer; 174 | color: inherit; 175 | text-decoration: none; 176 | } 177 | 178 | .clearfix { 179 | &:after { 180 | visibility: hidden; 181 | display: block; 182 | font-size: 0; 183 | content: ' '; 184 | clear: both; 185 | height: 0; 186 | } 187 | } 188 | 189 | // 饿了么主题 190 | 191 | /* 改变主题色变量 */ 192 | $--color-primary: #4c76af; 193 | /* 改变 icon 字体路径变量,必需 */ 194 | $--font-path: '~element-plus/lib/theme-chalk/fonts'; 195 | // @import '../../../../node_modules/element-plus/packages/theme-chalk/src/index'; 196 | -------------------------------------------------------------------------------- /src/view/admin/permission/permission-list.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 90 | 91 | 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lin-cms-vue", 3 | "version": "0.4.3", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "node script/plugin-get-config.js && vue-cli-service serve", 7 | "serve": "node script/plugin-get-config.js && vue-cli-service serve", 8 | "build": "node script/plugin-get-config.js && vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "lint": "vue-cli-service lint", 11 | "commit": "git add . && git-cz", 12 | "plugin:init": "node script/plugin-init.js", 13 | "plugin:new": "node script/plugin-new.js", 14 | "plugin:reconfig": "node script/plugin-get-config.js" 15 | }, 16 | "dependencies": { 17 | "@babel/polyfill": "^7.4.4", 18 | "@element-plus/icons-vue": "^2.0.10", 19 | "@tinymce/tinymce-vue": "^4.0.0", 20 | "axios": "^0.24.0", 21 | "core-js": "^3.34.0", 22 | "dayjs": "^1.11.10", 23 | "element-plus": "^2.7.3", 24 | "event-source-polyfill": "^1.0.7", 25 | "fastscan": "^1.0.4", 26 | "good-storage": "^1.1.0", 27 | "js-cookie": "^2.2.0", 28 | "lodash": "^4.17.21", 29 | "mitt": "^2.1.0", 30 | "moment": "^2.29.4", 31 | "photoswipe": "^4.1.2", 32 | "screenfull": "^4.2.0", 33 | "swiper": "^6.4.5", 34 | "vditor": "^3.10.4", 35 | "vue": "^3.4.27", 36 | "vue-picture-cropper": "^0.1.9", 37 | "vue-router": "^4.0.10", 38 | "vuex": "^4.1.0", 39 | "vuex-persist": "^2.0.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.11.4", 43 | "@babel/eslint-parser": "^7.17.0", 44 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 45 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 46 | "@vue/cli-plugin-babel": "^5.0.3", 47 | "@vue/cli-plugin-eslint": "^5.0.3", 48 | "@vue/cli-plugin-unit-jest": "^5.0.3", 49 | "@vue/cli-service": "^5.0.3", 50 | "@vue/compiler-sfc": "^3.2.0", 51 | "@vue/eslint-config-airbnb": "^6.0.0", 52 | "@vue/test-utils": "^2.0.0-beta.8", 53 | "babel-jest": "^26.3.0", 54 | "babel-plugin-component": "^1.1.1", 55 | "chalk": "^2.4.2", 56 | "child_process": "^1.0.2", 57 | "commitizen": "^4.2.3", 58 | "cz-conventional-changelog": "^2.1.0", 59 | "directory-tree": "^2.2.3", 60 | "ejs": "^2.6.2", 61 | "eslint": "^8.11.0", 62 | "eslint-plugin-vue": "^8.5.0", 63 | "eslint-plugin-vuejs-accessibility": "^1.1.1", 64 | "fs-extra": "^8.1.0", 65 | "ignore-loader": "^0.1.2", 66 | "inquirer": "^6.5.0", 67 | "js-yaml": "^3.13.1", 68 | "lint-staged": "^9.5.0", 69 | "sass": "^1.26.5", 70 | "sass-loader": "^8.0.2", 71 | "semver": "^6.2.0", 72 | "shelljs": "^0.8.5", 73 | "validate-npm-package-name": "^3.0.0", 74 | "yaml-front-matter": "^4.0.0" 75 | }, 76 | "browserslist": [ 77 | "> 1%", 78 | "last 2 versions", 79 | "not ie <= 11" 80 | ], 81 | "config": { 82 | "commitizen": { 83 | "path": "./node_modules/cz-conventional-changelog" 84 | } 85 | }, 86 | "gitHooks": { 87 | "pre-commit": "" 88 | }, 89 | "lint-staged": { 90 | "*.{js,vue}": [ 91 | "prettier --write", 92 | "vue-cli-service lint", 93 | "git add" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/component/base/tinymce/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 102 | 103 | 108 | -------------------------------------------------------------------------------- /src/component/layout/nav-bar.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 91 | 92 | 118 | -------------------------------------------------------------------------------- /src/component/layout/sidebar/search.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 93 | 94 | 115 | -------------------------------------------------------------------------------- /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) 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/view/admin/group/group-edit.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 81 | 105 | -------------------------------------------------------------------------------- /src/plugin/base/view/type/type-dialog.vue: -------------------------------------------------------------------------------- 1 | 38 | 104 | 105 | -------------------------------------------------------------------------------- /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 | filePath: 'view/admin/', 8 | order: null, 9 | inNav: true, 10 | permission: ['超级管理员独有权限'], 11 | children: [ 12 | { 13 | title: '用户管理', 14 | type: 'view', 15 | name: 'userList', 16 | route: '/admin/user/list', 17 | filePath: 'view/admin/user/user-list.vue', 18 | inNav: true, 19 | icon: 'iconfont icon-huiyuanguanli', 20 | permission: ['查询所有用户'], 21 | }, 22 | { 23 | route: '/admin/group/list', 24 | name: null, 25 | title: '分组管理', 26 | type: 'tab', 27 | icon: 'iconfont icon-yunyingguanli_fuwufenzuguanli', 28 | filePath: 'view/admin/group', 29 | inNav: true, 30 | children: [ 31 | { 32 | route: '/admin/group/list', 33 | type: 'view', 34 | name: 'groupList', 35 | inNav: true, 36 | filePath: 'view/admin/group/group-list.vue', 37 | title: '分组列表', 38 | icon: 'iconfont icon-huiyuanguanli', 39 | permission: ['超级管理员独有权限'], 40 | }, 41 | { 42 | route: '/admin/group/add', 43 | type: 'view', 44 | name: 'GroupCreate', 45 | filePath: 'view/admin/group/group-create.vue', 46 | inNav: true, 47 | title: '添加分组', 48 | icon: 'iconfont icon-add', 49 | permission: ['超级管理员独有权限'], 50 | }, 51 | { 52 | route: '/admin/group/edit', 53 | type: 'view', 54 | name: 'GroupEdit', 55 | filePath: 'view/admin/group/group-edit.vue', 56 | inNav: false, 57 | title: '修改分组', 58 | icon: 'iconfont icon-add', 59 | permission: ['超级管理员独有权限'], 60 | }, 61 | ], 62 | }, 63 | { 64 | route: '/admin/permission/list', 65 | type: 'view', 66 | name: 'permission', 67 | inNav: true, 68 | icon: 'iconfont icon-jiemiansheji', 69 | filePath: 'view/admin/permission/permission-list.vue', 70 | title: '权限列表', 71 | }, 72 | { 73 | route: '/admin/setting/list', 74 | type: 'view', 75 | name: 'setting', 76 | inNav: true, 77 | filePath: 'view/admin/setting/setting-list.vue', 78 | title: '设置管理', 79 | icon: 'iconfont icon-shangpinguanli', 80 | permission: ['得到所有设置'], 81 | }, 82 | { 83 | route: '/admin/log/serilog', 84 | type: 'view', 85 | name: 'serilog', 86 | inNav: true, 87 | filePath: 'view/admin/log/serilog.vue', 88 | title: 'Serilog日志', 89 | icon: 'iconfont icon-naozhongxiaoxitixing', 90 | permission: ['Serilog日志'], 91 | }, 92 | { 93 | title: '日志管理', 94 | type: 'view', 95 | name: Symbol('log'), 96 | route: '/log', 97 | filePath: 'view/log/log.vue', 98 | inNav: true, 99 | icon: 'iconfont icon-rizhiguanli', 100 | order: 2, 101 | permission: ['查询所有日志'], 102 | }, 103 | { 104 | title: '日志面板', 105 | type: 'view', 106 | name: Symbol('logdashboard'), 107 | route: '/log-dashboard', 108 | filePath: 'view/home/log-dashboard.vue', 109 | inNav: true, 110 | icon: 'iconfont icon-rizhiguanli', 111 | order: 2 112 | }, 113 | ], 114 | } 115 | 116 | export default adminRouter 117 | -------------------------------------------------------------------------------- /src/view/admin/setting/setting-form-dialog.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 110 | -------------------------------------------------------------------------------- /src/lin/model/admin.js: -------------------------------------------------------------------------------- 1 | import { post, get, put, _delete } from '@/lin/plugin/axios' 2 | 3 | export default class Admin { 4 | 5 | static GetPermissionNodes() { 6 | return get('cms/admin/permission/tree') 7 | } 8 | 9 | static async getUsers({ group_id, 10 | count = this.uCount, 11 | page = this.uPage, 12 | nickname, 13 | username, 14 | email 15 | }) { 16 | let res = await get('cms/admin/users', { 17 | count, 18 | page, 19 | group_id, 20 | nickname, 21 | username, 22 | email 23 | }) 24 | return res 25 | } 26 | 27 | async nextGroupsPage() { 28 | await this.increaseGPage() 29 | return this.getGroupsWithPermissions({}) 30 | } 31 | 32 | async preGroupsPage() { 33 | await this.decreaseGPage() 34 | return this.getGroupsWithPermissions({}) 35 | } 36 | 37 | static async getGroups(query) { 38 | const groups = await get('cms/admin/group', query) 39 | return groups 40 | } 41 | 42 | static async getGroup(id) { 43 | const group = await get(`cms/admin/group/${id}`) 44 | return group 45 | } 46 | 47 | static async createGroup(name, info, permission_ids) { 48 | const res = await post('cms/admin/group', { 49 | name, 50 | info, 51 | permission_ids, 52 | }) 53 | return res 54 | } 55 | 56 | static async updateGroup(data, id) { 57 | const res = await put(`cms/admin/group/${id}`, data) 58 | return res 59 | } 60 | 61 | static async deleteGroup(id) { 62 | const res = await _delete(`cms/admin/group/${id}`) 63 | return res 64 | } 65 | 66 | static async getUser(id) { 67 | const res = await get(`cms/admin/user/${id}`) 68 | return res 69 | } 70 | 71 | static async deleteUser(id) { 72 | const res = await _delete(`cms/admin/user/${id}`) 73 | return res 74 | } 75 | static async createUser(id, user) { 76 | const res = await put(`cms/admin/user/${id}`, user) 77 | return res 78 | } 79 | static async updateUser(id, user) { 80 | const res = await put(`cms/admin/user/${id}`, user) 81 | return res 82 | } 83 | 84 | static async dispatchPermissions(group_id, permission_ids) { 85 | const res = await post('cms/admin/permission/dispatch/batch', { 86 | group_id, 87 | permission_ids, 88 | }) 89 | return res 90 | } 91 | 92 | static async changePassword(new_password, confirm_password, id) { 93 | const res = await put(`cms/admin/user/${id}/password`, { 94 | new_password, 95 | confirm_password, 96 | }) 97 | return res 98 | } 99 | 100 | static async removePermissions(group_id, permission_ids) { 101 | const res = await post('cms/admin/permission/remove', { 102 | group_id, 103 | permission_ids, 104 | }) 105 | return res 106 | } 107 | 108 | static async deletePermissionNode(permissionId) { 109 | return await _delete(`cms/admin/permission/${permissionId}`) 110 | } 111 | 112 | static async createPermissionNode(data) { 113 | return await post('cms/admin/permission', data) 114 | } 115 | 116 | static async updatePermissionNode(permissionId, data) { 117 | return await put(`cms/admin/permission/${permissionId}`, data) 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/plugin/lin-cms-ui/view/form/rate/rate.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 84 | 85 | 108 | -------------------------------------------------------------------------------- /src/plugin/base/view/type/type-list.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 122 | 123 | 126 | -------------------------------------------------------------------------------- /src/view/admin/user/user-form-dialog.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 124 | 125 | 130 | -------------------------------------------------------------------------------- /src/view/admin/log/serilog-dialog.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 119 | 120 | 129 | -------------------------------------------------------------------------------- /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/plugin/lin-cms-ui/view/form/multiple-input.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 78 | 79 | 132 | -------------------------------------------------------------------------------- /src/view/admin/group/group-create.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 108 | 109 | 132 | -------------------------------------------------------------------------------- /src/plugin/base/view/item/item-dialog.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /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 = 15, 18 | 19 | sPage = 0, 20 | sCount = 15, 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 | async getUserAndVisits() { 164 | return await get('cms/log/visitis') 165 | } 166 | 167 | async getSerilogListAsync(params) { 168 | return await get('cms/log/serilog', params) 169 | } 170 | async getLogDashboard() { 171 | return await get('cms/log/dashboard') 172 | } 173 | } 174 | 175 | export default new Log({}) 176 | --------------------------------------------------------------------------------