├── .dockerignore ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── dev.yml │ └── prod.yml ├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── service-frontend.iml └── vcs.xml ├── .travis.yml ├── Dockerfile_dev ├── Dockerfile_prod ├── LICENSE ├── README.es.md ├── README.ja.md ├── README.md ├── README.zh-CN.md ├── babel.config.js ├── build └── index.js ├── jest.config.js ├── jsconfig.json ├── nginx_dev.conf ├── nginx_prod.conf ├── package-lock.json ├── package.json ├── plopfile.js ├── postcss.config.js ├── public ├── favicon.ico ├── index.html ├── logo.jpg ├── logo.svg ├── logo02.svg ├── logo03.svg ├── logo04.svg ├── logo05.svg ├── logo06.svg ├── logo07.svg ├── logo08.svg ├── logo2.svg └── logo3.png ├── src ├── App.vue ├── api │ ├── admin.js │ ├── client.ts │ ├── draft.js │ ├── forum.js │ ├── hot_list.js │ ├── issue.js │ ├── issue_connect.js │ ├── notify.js │ ├── send_email.js │ ├── statistics.js │ ├── subject.js │ ├── tag.js │ ├── upload.js │ ├── user.js │ └── year.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── custom-theme │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ └── index.css │ └── images │ │ ├── anonymous.jpg │ │ ├── login_background.jpg │ │ └── login_cover_image.png ├── components │ ├── BackToTop │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── Charts │ │ ├── Keyboard.vue │ │ ├── LineMarker.vue │ │ ├── MixChart.vue │ │ └── mixins │ │ │ └── resize.js │ ├── Comment │ │ └── TextBox.vue │ ├── DndList │ │ └── index.vue │ ├── DragSelect │ │ └── index.vue │ ├── Dropzone │ │ └── index.vue │ ├── ErrorLog │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── ImageCropper │ │ ├── index.vue │ │ └── utils │ │ │ ├── data2blob.js │ │ │ ├── effectRipple.js │ │ │ ├── language.js │ │ │ └── mimes.js │ ├── JsonEditor │ │ └── index.vue │ ├── Kanban │ │ └── index.vue │ ├── MDeditor │ │ ├── MarkdownDisplay.vue │ │ └── MdEditor.vue │ ├── MDinput │ │ └── index.vue │ ├── MarkdownEditor │ │ ├── default-options.js │ │ └── index.vue │ ├── Notify │ │ ├── NotifyDialog.vue │ │ └── TextBox.vue │ ├── Pagination │ │ └── index.vue │ ├── PanThumb │ │ └── index.vue │ ├── RelateIssue │ │ ├── AddRelateDialog.vue │ │ └── RelateDialog.vue │ ├── RightPanel │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── Share │ │ └── DropdownMenu.vue │ ├── SizeSelect │ │ └── index.vue │ ├── Sticky │ │ └── index.vue │ ├── Suggest │ │ └── TextBox.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TextHoverEffect │ │ └── Mallki.vue │ ├── ThemePicker │ │ └── index.vue │ ├── Tinymce │ │ ├── components │ │ │ └── EditorImage.vue │ │ ├── dynamicLoadScript.js │ │ ├── index.vue │ │ ├── plugins.js │ │ └── toolbar.js │ ├── Upload │ │ ├── SingleImage.vue │ │ ├── SingleImage2.vue │ │ └── SingleImage3.vue │ └── UploadExcel │ │ └── index.vue ├── directive │ ├── clipboard │ │ ├── clipboard.js │ │ └── index.js │ ├── el-drag-dialog │ │ ├── drag.js │ │ └── index.js │ ├── el-table │ │ ├── adaptive.js │ │ └── index.js │ ├── permission │ │ ├── index.js │ │ └── permission.js │ ├── sticky.js │ └── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.js ├── filters │ └── index.js ├── icons │ ├── index.js │ ├── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── chart.svg │ │ ├── clipboard.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── international.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── qq.svg │ │ ├── search.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── star.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── theme.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── user.svg │ │ ├── wechat.svg │ │ └── zip.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Settings │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── ASidebarItem.vue │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── plugins │ └── vuetify.js ├── router │ ├── index.js │ └── modules │ │ ├── charts.js │ │ ├── components.js │ │ ├── nested.js │ │ └── table.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── errorLog.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── btn.scss │ ├── element-ui.scss │ ├── element-variables.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── bus 2.js │ ├── bus.js │ ├── clipboard.js │ ├── error-log.js │ ├── file.js │ ├── get-page-title.js │ ├── index.js │ ├── open-window.js │ ├── permission.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js ├── vendor │ ├── Export2Excel.js │ └── Export2Zip.js └── views │ ├── error-log │ ├── components │ │ ├── ErrorTestA.vue │ │ └── ErrorTestB.vue │ └── index.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── icons │ ├── element-icons.js │ ├── index.vue │ └── svg-icons.js │ ├── issueInfo │ ├── components │ │ ├── Confirm.vue │ │ └── MyRichText.vue │ └── issueInfoDetail.vue │ ├── login │ ├── auth-redirect.vue │ ├── components │ │ ├── SocialSignin.vue │ │ ├── captcha.vue │ │ └── registerPanel.vue │ └── index.vue │ ├── postIssue │ ├── components │ │ └── postIssue.vue │ └── index.vue │ ├── redirect │ └── index.vue │ ├── searchIssue │ ├── components │ │ ├── issueItem.vue │ │ ├── topIssue.vue │ │ └── topUser.vue │ └── index.vue │ ├── userInfo │ ├── userInfo.vue │ └── userIssue.vue │ └── userManage │ ├── bonusManage.vue │ ├── labelManage.vue │ ├── labelView.vue │ ├── postMessage.vue │ ├── tutorManage.vue │ ├── userCreate.vue │ ├── userList.vue │ ├── waldon.json │ └── workStat.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js ├── vue-element-admin.iml └── vue.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/dev-api' 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | src 6 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to Tencent Cloud 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | - name: Set up Docker Buildx 13 | uses: docker/setup-buildx-action@v1 14 | - name: Login to DockerHub 15 | uses: docker/login-action@v1 16 | with: 17 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 18 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 19 | - name: Build and push Docker image 20 | uses: docker/build-push-action@v2 21 | with: 22 | file: Dockerfile_dev 23 | context: . 24 | push: true 25 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_NAME }}:latest 26 | # tags: your-dockerhub-username/your-repo-name:latest 27 | 28 | - name: Deploy to Tencent Cloud server 29 | # 这是最后一个步骤使用的GitHub Action,这里使用appleboy/ssh-action@master来通过SSH连接到腾讯云服务器并执行命令 30 | uses: appleboy/ssh-action@master 31 | # 这是一些步骤需要的参数,这里需要提供腾讯云服务器的IP地址,SSH密钥,以及要执行的命令 32 | with: 33 | host: ${{ secrets.CLOUD_HOST}} 34 | username: ${{ secrets.CLOUD_USERNAME }} 35 | password: ${{ secrets.CLOUD_PASSWORD }} 36 | port: ${{ secrets.CLOUD_PORT }} 37 | script: | 38 | sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_NAME }}:latest # 这是一个命令,用于从DockerHub拉取构建好的镜像 39 | sudo docker stop ${{ secrets.DOCKER_HUB_REPO_NAME }} || true # 这是一个命令,用于停止已经运行的容器,如果没有则忽略错误 40 | sudo docker rm ${{ secrets.DOCKER_HUB_REPO_NAME }} || true # 这是一个命令,用于删除已经停止的容器,如果没有则忽略错误 41 | sudo docker run -d --name ${{ secrets.DOCKER_HUB_REPO_NAME }} -p 9527:80 -p 9528:443 -v ${{ secrets.CLOUD_SSL}}:/usr/share/nginx/ssl/ ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_NAME }}:latest # 这是一个命令,用于运行新的容器,并将容器的80端口映射到服务器的9527端口,将容器的443端口映射到服务器的9528端口 -------------------------------------------------------------------------------- /.github/workflows/prod.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to Tencent Cloud Production 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | - name: Set up Docker Buildx 13 | uses: docker/setup-buildx-action@v1 14 | - name: Login to DockerHub 15 | uses: docker/login-action@v1 16 | with: 17 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 18 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 19 | - name: Build and push Docker image 20 | uses: docker/build-push-action@v2 21 | with: 22 | file: Dockerfile_prod 23 | context: . 24 | push: true 25 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_PROD_NAME }}:latest 26 | # tags: your-dockerhub-username/your-repo-name:latest 27 | 28 | - name: Deploy to Tencent Cloud server 29 | # 这是最后一个步骤使用的GitHub Action,这里使用appleboy/ssh-action@master来通过SSH连接到腾讯云服务器并执行命令 30 | uses: appleboy/ssh-action@master 31 | # 这是一些步骤需要的参数,这里需要提供腾讯云服务器的IP地址,SSH密钥,以及要执行的命令 32 | with: 33 | host: ${{ secrets.CLOUD_HOST}} 34 | username: ${{ secrets.CLOUD_USERNAME }} 35 | password: ${{ secrets.CLOUD_PASSWORD }} 36 | port: ${{ secrets.CLOUD_PORT }} 37 | script: | 38 | sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_PROD_NAME }}:latest # 这是一个命令,用于从DockerHub拉取构建好的镜像 39 | sudo docker stop ${{ secrets.DOCKER_HUB_REPO_PROD_NAME }} || true # 这是一个命令,用于停止已经运行的容器,如果没有则忽略错误 40 | sudo docker rm ${{ secrets.DOCKER_HUB_REPO_PROD_NAME }} || true # 这是一个命令,用于删除已经停止的容器,如果没有则忽略错误 41 | sudo docker run -d --name ${{ secrets.DOCKER_HUB_REPO_PROD_NAME }} -p 80:80 -p 443:443 -v ${{ secrets.CLOUD_PIC }}:/usr/share/nginx/html/pic/ -v ${{ secrets.CLOUD_SSL}}:/usr/share/nginx/ssl/ ${{ secrets.DOCKER_HUB_USERNAME }}/${{ secrets.DOCKER_HUB_REPO_PROD_NAME }}:latest # 这是一个命令,用于运行新的容器,并将容器的80端口映射到服务器的80端口,同时挂载静态资源访问 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | tests/**/coverage/ 10 | tests/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.local 21 | 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/service-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /Dockerfile_dev: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | WORKDIR /app 3 | COPY . . 4 | 5 | RUN npm install -g npm@9.6.4 \ 6 | && npm install \ 7 | && npm run build:prod \ 8 | && cd /usr/share/nginx/html ; mkdir pic 9 | 10 | FROM nginx 11 | RUN mkdir /app 12 | COPY --from=0 /app/dist /app 13 | COPY nginx_dev.conf /etc/nginx/nginx.conf 14 | -------------------------------------------------------------------------------- /Dockerfile_prod: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | WORKDIR /app 3 | COPY . . 4 | 5 | RUN npm install -g npm@9.6.4 \ 6 | && npm install \ 7 | && npm run build:prod \ 8 | && cd /usr/share/nginx/html ; mkdir pic 9 | 10 | FROM nginx 11 | RUN mkdir /app 12 | COPY --from=0 /app/dist /app 13 | COPY nginx_prod.conf /etc/nginx/nginx.conf 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 初版文件框架 2 | 3 | - node_modules 已经引入了tiptap富文本编辑器插件以及上传图床插件,之后可以直接用 4 | 5 | - public 静态资源目录 6 | 7 | - src 8 | - api api接口(主要工作目录) 9 | - assets 全局资源目录 10 | - components 原框架的一些组件,之后不复用的话可以删掉(主要工作目录) 11 | - directive 原模版用到了一些,删了会引发一些报错,不删不影响 12 | - filters 原模版用到了一些,删了会引发一些报错,不删不影响 13 | - icons 14 | - layout 布局,侧边栏和导航栏的组件都在这里(主要工作目录) 15 | - plugins vuetify插件 16 | - router 路由(主要工作目录),原模版利用permission对权限和路由做了映射。在这里添加路由可以直接显示到侧边栏,不同的权限看到的侧边栏路由不一样。 17 | - store 状态管理(主要工作目录) 18 | - user.js 与用户权限设置有关 19 | - 其他的暂时不是特别重要,是原模版的内容,之后不用的话可以删掉 20 | - styles 样式 21 | - utills 一些js工具函数,原模版提供,之后不用可以删掉,内部有一些用户权限相关函数不要删,模版的权限管理用到了这些函数。 22 | - vendor 模版提供的导出excel,zip函数,之后学习资料下载或者导出志愿时长或许可以用到,没有删 23 | - views(主要工作目录) 24 | - management 后台管理页面,内部有关于图床的操作,后续可以参考,没有删 25 | - login 登陆注册页面,没有删除,相关权限管理可以参考 26 | - 其余是模版自带的跳转页面,error404页面 27 | - permission.js 权限管理操作,可以参考也可以沿用 28 | - test 原模版的单元测试文件目录,之后可以参考 29 | - 最后是模版一些介绍md文档,可以参考一下 30 | 31 | PS:一些logo和配色暂时还是之前项目的,之后可以更换 32 | 33 | 34 | 35 | 运行前:npm install 36 | 37 | 运行:npm run dev -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': [ 12 | 'dynamic-import-node' 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /nginx_dev.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | keepalive_timeout 65; 17 | client_max_body_size 5m; 18 | server { 19 | # listen 80; 20 | listen 443 ssl; 21 | server_name localhost; 22 | # 证书文件的绝对路径 23 | ssl_certificate /usr/share/nginx/ssl/shieask.com_bundle.crt; 24 | # 私钥文件的绝对路径 25 | ssl_certificate_key /usr/share/nginx/ssl/shieask.com.key; 26 | ssl_session_timeout 5m; 27 | #请按照以下协议配置 28 | ssl_protocols TLSv1.2 TLSv1.3; 29 | #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 30 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; 31 | ssl_prefer_server_ciphers on; 32 | 33 | location / { 34 | root /app; 35 | index index.html; 36 | try_files $uri $uri/ /index.html; 37 | } 38 | error_page 500 502 503 504 /50x.html; 39 | location = /50x.html { 40 | root /usr/share/nginx/html; 41 | } 42 | 43 | location /prod-api { 44 | 45 | rewrite ^/prod-api/(.*)$ /$1 break; 46 | 47 | proxy_pass http://shieask.com:8080; #后台接口地址 48 | 49 | proxy_set_header X-Real-IP $remote_addr; 50 | 51 | proxy_set_header X-Forwarded-For $remote_addr; 52 | 53 | } 54 | 55 | location /pic/ { 56 | alias /usr/share/nginx/html/pic/ ; 57 | } 58 | } 59 | 60 | server { 61 | listen 80; 62 | server_name localhost; 63 | return 301 https://$host$request_uri; 64 | } 65 | } -------------------------------------------------------------------------------- /nginx_prod.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | keepalive_timeout 65; 17 | client_max_body_size 5m; 18 | server { 19 | # listen 80; 20 | listen 443 ssl; 21 | # server_name localhost; 22 | server_name shieask.com www.shieask.com; 23 | # 证书文件的绝对路径 24 | ssl_certificate /usr/share/nginx/ssl/shieask.com_bundle.crt; 25 | # 私钥文件的绝对路径 26 | ssl_certificate_key /usr/share/nginx/ssl/shieask.com.key; 27 | ssl_session_timeout 5m; 28 | #请按照以下协议配置 29 | ssl_protocols TLSv1.2 TLSv1.3; 30 | #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 31 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; 32 | ssl_prefer_server_ciphers on; 33 | 34 | location / { 35 | root /app; 36 | index index.html; 37 | try_files $uri $uri/ /index.html; 38 | } 39 | error_page 500 502 503 504 /50x.html; 40 | location = /50x.html { 41 | root /usr/share/nginx/html; 42 | } 43 | 44 | location /prod-api { 45 | 46 | rewrite ^/prod-api/(.*)$ /$1 break; 47 | 48 | proxy_pass http://101.43.219.110:9001; #后端生产环境接口地址 49 | 50 | proxy_set_header X-Real-IP $remote_addr; 51 | 52 | proxy_set_header X-Forwarded-For $remote_addr; 53 | 54 | } 55 | 56 | location /pic/ { 57 | alias /usr/share/nginx/html/pic/ ; 58 | } 59 | } 60 | server { 61 | listen 80; 62 | server_name shieask.com www.shieask.com; 63 | return 301 https://$host$request_uri; 64 | } 65 | } -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('./plop-templates/view/prompt') 2 | const componentGenerator = require('./plop-templates/component/prompt') 3 | const storeGenerator = require('./plop-templates/store/prompt.js') 4 | 5 | module.exports = function(plop) { 6 | plop.setGenerator('view', viewGenerator) 7 | plop.setGenerator('component', componentGenerator) 8 | plop.setGenerator('store', storeGenerator) 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <%= webpackConfig.name %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/public/logo.jpg -------------------------------------------------------------------------------- /public/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/public/logo3.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/admin.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | export function batch_register( 5 | jwt, //: string, 6 | name_list, //: Array, 7 | student_id_list, //: Array, 8 | password_list, //: Array 9 | role_list 10 | ) { 11 | return request({ 12 | url: '/admins/create_user_batch', 13 | method: 'post', 14 | data: { 15 | jwt: jwt, 16 | name_list: name_list, 17 | student_id_list: student_id_list, 18 | password_list: password_list, 19 | role_list: role_list 20 | } 21 | }) 22 | } 23 | 24 | export function update_privilege( 25 | jwt, //: string, 26 | user_id, //: number, 27 | user_role, //: number 28 | ) { 29 | return request({ 30 | url: '/admins/update_privilege', 31 | method: 'post', 32 | data: { 33 | jwt: jwt, 34 | user_id: user_id, 35 | user_role: user_role 36 | } 37 | }) 38 | } 39 | 40 | export function get_users( 41 | jwt //: string 42 | ) { 43 | return request({ 44 | url: '/admins/users', 45 | method: 'post', 46 | data: { 47 | jwt: jwt 48 | } 49 | }) 50 | } 51 | 52 | export function freeze_user( 53 | jwt, //: string, 54 | user_id, //: number, 55 | frozen, //: number 56 | ) { 57 | return request({ 58 | url: '/admins/freeze_user', 59 | method: 'post', 60 | data: { 61 | jwt: jwt, 62 | user_id: user_id, 63 | frozen: frozen 64 | } 65 | }) 66 | } 67 | 68 | export function delete_issue( 69 | jwt, //: string, 70 | issue_id, //: number 71 | ) { 72 | return request({ 73 | url: '/admins/issue/delete', 74 | method: 'post', 75 | data: { 76 | jwt: jwt, 77 | issue_id: issue_id 78 | } 79 | }) 80 | } 81 | 82 | export function single_register( 83 | jwt, //: string, 84 | name, //: string, 85 | student_id, //: string, 86 | password, //: string 87 | role 88 | ) { 89 | return request({ 90 | url: '/admins/create_user', 91 | method: 'post', 92 | data: { 93 | jwt: jwt, 94 | name: name, 95 | student_id: student_id, 96 | password: password, 97 | role: role 98 | } 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /src/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | // @ts-ignore 3 | import qs from 'qs' 4 | 5 | 6 | function DELETE(url: string, options?: any) { 7 | if (options?.data && 8 | options.headers?.['Content-Type'] !== 'multipart/form-data') 9 | options.data = qs.stringify(options.data, { arrayFormat: 'brackets' }) 10 | return axios({ method: 'DELETE', url, ...options }) 11 | } 12 | 13 | function GET(url: string, options?: any) { 14 | if (options?.data && 15 | options.headers?.['Content-Type'] !== 'multipart/form-data') 16 | options.data = qs.stringify(options.data, { arrayFormat: 'brackets' }) 17 | return axios({ method: 'GET', url, ...options }) 18 | } 19 | 20 | function POST(url: string, options?: any) { 21 | if (options?.data && 22 | options.headers?.['Content-Type'] !== 'multipart/form-data') 23 | options.data = qs.stringify(options.data, { arrayFormat: 'brackets' }) 24 | return axios({ method: 'POST', url, ...options }) 25 | } 26 | 27 | function PUT(url: string, options?: any) { 28 | if (options?.data) 29 | options.data = qs.stringify(options.data, { arrayFormat: 'brackets' }) 30 | return axios({ method: 'PUT', url, ...options }) 31 | } 32 | 33 | const API = axios.create({ 34 | baseURL:'http://localhost:3000', //请求后端数据的基本地址,自定义 35 | timeout: 2000 //请求超时设置,单位ms 36 | }) 37 | 38 | export default { DELETE, GET, POST, PUT, API } 39 | -------------------------------------------------------------------------------- /src/api/draft.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function save_draft( 4 | jwt, //: string 5 | chapter_id, //: number, 6 | title, //: string, 7 | content, //: string, 8 | anonymous, //: number 9 | ) { 10 | return request({ 11 | url: '/issue/save_draft', 12 | method: 'post', 13 | data: { 14 | jwt: jwt, 15 | chapter_id: chapter_id, 16 | title: title, 17 | content: content, 18 | anonymous: anonymous 19 | } 20 | }) 21 | } 22 | 23 | export function load_draft( 24 | jwt, //: string 25 | ) { 26 | return request({ 27 | url: '/issue/load_draft', 28 | method: 'post', 29 | data: { 30 | jwt: jwt, 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/api/forum.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | export function get_issue_all_comments( 5 | jwt, //: string, 6 | issue_id //: number 7 | ) { 8 | return request({ 9 | url: '/issue/comments', 10 | method: 'post', 11 | data: { 12 | jwt: jwt, 13 | issue_id: issue_id 14 | } 15 | }) 16 | } 17 | 18 | export function delete_comment( 19 | jwt, //: string, 20 | comment_id //: number 21 | ) { 22 | return request({ 23 | url: '/issue/comment', 24 | method: 'delete', 25 | data: { 26 | jwt: jwt, 27 | comment_id: comment_id 28 | } 29 | }) 30 | } 31 | 32 | export function update_comment( 33 | jwt, //: string, 34 | comment_id, //: number, 35 | content //: string 36 | ) { 37 | return request({ 38 | url: '/issue/comment/update', 39 | method: 'post', 40 | data: { 41 | jwt: jwt, 42 | comment_id: comment_id, 43 | content: content 44 | } 45 | }) 46 | } 47 | 48 | export function create_comment( 49 | jwt, //: string, 50 | issue_id, //: number, 51 | content //: string 52 | ) { 53 | return request({ 54 | url: '/issue/comment/create', 55 | method: 'post', 56 | data: { 57 | jwt: jwt, 58 | issue_id: issue_id, 59 | content: content 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/api/hot_list.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 已对接 4 | export function get_active_users( 5 | jwt, //: string 6 | top_k, //: number 7 | ) { 8 | return request({ 9 | url: '/user/active_users', 10 | method: 'post', 11 | data: { 12 | jwt: jwt, 13 | top_k: top_k, 14 | } 15 | }) 16 | } 17 | 18 | export function get_popular_issues( 19 | jwt, //: string 20 | top_k, //: number 21 | ) { 22 | return request({ 23 | url: '/user/get_popular_issue', 24 | method: 'post', 25 | data: { 26 | jwt: jwt, 27 | top_k: top_k 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/api/issue_connect.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function add_association( 4 | jwt, //: string 5 | issue_ids, // : array 6 | issue_associate_id, // : number 7 | ) { 8 | return request({ 9 | url: '/issue/associate', 10 | method: 'post', 11 | data: { 12 | jwt: jwt, 13 | issue_id: issue_ids, 14 | issue_associate_id: issue_associate_id 15 | } 16 | }) 17 | } 18 | 19 | export function delete_association( 20 | jwt, //: string 21 | issue_id, // : number 22 | issue_associate_id, // : number 23 | ) { 24 | return request({ 25 | url: '/issue/associate/delete', 26 | method: 'post', 27 | data: { 28 | jwt: jwt, 29 | issue_id: issue_id, 30 | issue_associate_id: issue_associate_id 31 | } 32 | }) 33 | } 34 | 35 | export function get_association( 36 | jwt, //: string 37 | issue_id, // : number 38 | ) { 39 | return request({ 40 | url: '/issue/associate/get', 41 | method: 'post', 42 | data: { 43 | jwt: jwt, 44 | issue_id: issue_id, 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/api/notify.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function clear_all_notification( 4 | jwt, //: string 5 | ) { 6 | return request({ 7 | url: '/notification/clear_all', 8 | method: 'post', 9 | data: { 10 | jwt: jwt 11 | } 12 | }) 13 | } 14 | 15 | export function read_one_notification( 16 | jwt, //: string 17 | id, 18 | ) { 19 | return request({ 20 | url: '/notification/get', 21 | method: 'post', 22 | data: { 23 | jwt: jwt, 24 | notification_id: id 25 | } 26 | }) 27 | } 28 | 29 | export function get_all_notification( 30 | jwt, //: string 31 | ) { 32 | return request({ 33 | url: '/notification/user_receive', 34 | method: 'post', 35 | data: { 36 | jwt: jwt, 37 | } 38 | }) 39 | } 40 | 41 | export function admin_broadcast( 42 | jwt, //: string 43 | title, //: string 44 | content, //: string 45 | category, //: number 46 | ) { 47 | return request({ 48 | url: '/notification/broadcast', 49 | method: 'post', 50 | data: { 51 | jwt: jwt, 52 | title: title, 53 | content: content, 54 | category: category 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/api/send_email.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function send_mail( 4 | mail, // : string 5 | ) { 6 | return request({ 7 | url: '/mail/send', 8 | method: 'post', 9 | data: { 10 | mail: mail, 11 | } 12 | }) 13 | } 14 | 15 | export function confirm_mail( 16 | mail, // : string 17 | student_id, // : string 18 | password, // : string 19 | v_code, // : string 20 | ) { 21 | return request({ 22 | url: '/mail/confirm', 23 | method: 'post', 24 | data: { 25 | mail: mail, 26 | student_id: student_id, 27 | password: password, 28 | v_code: v_code 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/api/statistics.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function get_statistics( 4 | jwt, //: string 5 | type, // : number 6 | indicator, // : number 7 | begin_date, // : string 8 | end_date, // : string 9 | ) { 10 | return request({ 11 | url: '/admins/statistics', 12 | method: 'post', 13 | data: { 14 | jwt: jwt, 15 | type: type, 16 | indicator: indicator, 17 | begin_date: begin_date, 18 | end_date: end_date 19 | } 20 | }) 21 | } 22 | 23 | export function get_markdown( 24 | jwt, //: string 25 | begin_date, // : string 26 | end_date, // : string 27 | ) { 28 | return request({ 29 | url: '/markdown', 30 | method: 'post', 31 | data: { 32 | jwt: jwt, 33 | begin_date: begin_date, 34 | end_date: end_date 35 | } 36 | }) 37 | } 38 | 39 | export function get_tutor_bonus( 40 | jwt, //: string 41 | bonus_per_counsel, //: float 42 | bonus_per_review, //: float 43 | begin_date, //: string 44 | end_date, //: string 45 | min_bonus, //: float 46 | max_bonus, //: float 47 | ) { 48 | return request({ 49 | url: '/admins/tutor_bonus', 50 | method: 'post', 51 | data: { 52 | jwt: jwt, 53 | bonus_per_counsel: bonus_per_counsel, 54 | bonus_per_review: bonus_per_review, 55 | begin_date: begin_date, 56 | end_date: end_date, 57 | min_bonus: min_bonus, 58 | max_bonus: max_bonus 59 | } 60 | }) 61 | } 62 | 63 | export function get_student_bonus( 64 | jwt, //: string 65 | bonus_per_issue, //: float 66 | begin_date, //: string 67 | end_date, //: string 68 | min_bonus, //: float 69 | max_bonus, //: float 70 | ) { 71 | return request({ 72 | url: '/admins/student_bonus', 73 | method: 'post', 74 | data: { 75 | jwt: jwt, 76 | bonus_per_issue: bonus_per_issue, 77 | begin_date: begin_date, 78 | end_date: end_date, 79 | min_bonus: min_bonus, 80 | max_bonus: max_bonus 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/api/tag.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function update_tag( 4 | jwt, //: string, 5 | tag_id, //: number, 6 | content //: string 7 | ) { 8 | return request({ 9 | url: '/tag/update', 10 | method: 'post', 11 | data: { 12 | jwt: jwt, 13 | tag_id: tag_id, 14 | content: content 15 | } 16 | }) 17 | } 18 | 19 | export function delete_tag( 20 | jwt, //: string, 21 | tag_id //: number 22 | ) { 23 | return request({ 24 | url: '/tag/delete', 25 | method: 'delete', 26 | data: { 27 | jwt: jwt, 28 | tag_id: tag_id, 29 | } 30 | }) 31 | } 32 | 33 | export function create_tag( 34 | jwt, //: string, 35 | content //: string 36 | ) { 37 | return request({ 38 | url: '/tag/create', 39 | method: 'post', 40 | data: { 41 | jwt: jwt, 42 | content: content 43 | } 44 | }) 45 | } 46 | 47 | export function get_all_tags( 48 | jwt, //: string, 49 | ) { 50 | return request({ 51 | url: '/tag/', 52 | method: 'get', 53 | data: { 54 | jwt: jwt 55 | } 56 | }) 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/api/upload.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | 4 | export function upload_public( 5 | formData 6 | ) { 7 | return request({ 8 | url: '/images/upload/', 9 | method: 'post', 10 | data: formData, 11 | }) 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/api/year.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function get_all_years( 4 | jwt, //: string 5 | ) { 6 | return request({ 7 | url: '/year/', 8 | method: 'get', 9 | data: { 10 | jwt: jwt, 11 | } 12 | }) 13 | } 14 | 15 | export function update_year_info( 16 | jwt, //: string 17 | year_id, //: number 18 | content, //: string 19 | ) { 20 | return request({ 21 | url: '/year/update', 22 | method: 'post', 23 | data: { 24 | jwt: jwt, 25 | year_id: year_id, 26 | content: content 27 | } 28 | }) 29 | } 30 | 31 | export function create_year( 32 | jwt, //: string 33 | content, //: string 34 | ) { 35 | return request({ 36 | url: '/year/create', 37 | method: 'post', 38 | data: { 39 | jwt: jwt, 40 | content: content 41 | } 42 | }) 43 | } 44 | 45 | export function update_current_year( 46 | jwt, //: string 47 | year_id, //: number 48 | ) { 49 | return request({ 50 | url: '/year/update_current', 51 | method: 'post', 52 | data: { 53 | jwt: jwt, 54 | year_id: year_id, 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/assets/images/anonymous.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/images/anonymous.jpg -------------------------------------------------------------------------------- /src/assets/images/login_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/images/login_background.jpg -------------------------------------------------------------------------------- /src/assets/images/login_cover_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/assets/images/login_cover_image.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ item.meta.title }} 6 | {{ item.meta.title }} 7 | 8 | 9 | 10 | 11 | 12 | 69 | 70 | 83 | -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.initListener() 12 | }, 13 | activated() { 14 | if (!this.$_resizeHandler) { 15 | // avoid duplication init 16 | this.initListener() 17 | } 18 | 19 | // when keep-alive chart activated, auto resize 20 | this.resize() 21 | }, 22 | beforeDestroy() { 23 | this.destroyListener() 24 | }, 25 | deactivated() { 26 | this.destroyListener() 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_sidebarResizeHandler(e) { 32 | if (e.propertyName === 'width') { 33 | this.$_resizeHandler() 34 | } 35 | }, 36 | initListener() { 37 | this.$_resizeHandler = debounce(() => { 38 | this.resize() 39 | }, 100) 40 | window.addEventListener('resize', this.$_resizeHandler) 41 | 42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 44 | }, 45 | destroyListener() { 46 | window.removeEventListener('resize', this.$_resizeHandler) 47 | this.$_resizeHandler = null 48 | 49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | resize() { 52 | const { chart } = this 53 | chart && chart.resize() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Comment/TextBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 评分 9 | 10 | 11 | 16 | 17 | 18 | 19 | 22 | 23 | mdi-comment-check 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 108 | -------------------------------------------------------------------------------- /src/components/DragSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 66 | -------------------------------------------------------------------------------- /src/components/ErrorLog/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Error Log 12 | Clear All 13 | 14 | 15 | 16 | 17 | 18 | Msg: 19 | 20 | {{ row.err.message }} 21 | 22 | 23 | 24 | 25 | Info: 26 | 27 | {{ row.vm.$vnode.tag }} error in {{ row.info }} 28 | 29 | 30 | 31 | 32 | Url: 33 | 34 | {{ row.url }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{ scope.row.err.stack }} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 70 | 71 | 79 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 22 | 23 | 24 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/data2blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * database64文件格式转换为2进制 3 | * 4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了 5 | * @param {[String]} mime [description] 6 | * @return {[blob]} [description] 7 | */ 8 | export default function(data, mime) { 9 | data = data.split(',')[1] 10 | data = window.atob(data) 11 | var ia = new Uint8Array(data.length) 12 | for (var i = 0; i < data.length; i++) { 13 | ia[i] = data.charCodeAt(i) 14 | } 15 | // canvas.toDataURL 返回的默认格式就是 image/png 16 | return new Blob([ia], { 17 | type: mime 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/effectRipple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 点击波纹效果 3 | * 4 | * @param {[event]} e [description] 5 | * @param {[Object]} arg_opts [description] 6 | * @return {[bollean]} [description] 7 | */ 8 | export default function(e, arg_opts) { 9 | var opts = Object.assign({ 10 | ele: e.target, // 波纹作用元素 11 | type: 'hit', // hit点击位置扩散center中心点扩展 12 | bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 13 | }, arg_opts) 14 | var target = opts.ele 15 | if (target) { 16 | var rect = target.getBoundingClientRect() 17 | var ripple = target.querySelector('.e-ripple') 18 | if (!ripple) { 19 | ripple = document.createElement('span') 20 | ripple.className = 'e-ripple' 21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 22 | target.appendChild(ripple) 23 | } else { 24 | ripple.className = 'e-ripple' 25 | } 26 | switch (opts.type) { 27 | case 'center': 28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px' 29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px' 30 | break 31 | default: 32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px' 33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px' 34 | } 35 | ripple.style.backgroundColor = opts.bgc 36 | ripple.className = 'e-ripple z-active' 37 | return false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/mimes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'jpg': 'image/jpeg', 3 | 'png': 'image/png', 4 | 'gif': 'image/gif', 5 | 'svg': 'image/svg+xml', 6 | 'psd': 'image/photoshop' 7 | } 8 | -------------------------------------------------------------------------------- /src/components/JsonEditor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 56 | 57 | 78 | -------------------------------------------------------------------------------- /src/components/Kanban/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ headerText }} 5 | 6 | 12 | 13 | {{ element.name }} {{ element.id }} 14 | 15 | 16 | 17 | 18 | 19 | 54 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/MDeditor/MarkdownDisplay.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 49 | 50 | 56 | -------------------------------------------------------------------------------- /src/components/MDeditor/MdEditor.vue: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 99 | 100 | 106 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/default-options.js: -------------------------------------------------------------------------------- 1 | export default { 2 | minHeight: '700px', 3 | previewStyle: 'vertical', 4 | useCommandShortcut: true, 5 | useDefaultHTMLSanitizer: true, 6 | usageStatistics: false, 7 | hideModeSwitch: false, 8 | toolbarItems: [ 9 | 'heading', 10 | 'bold', 11 | 'italic', 12 | 'strike', 13 | 'divider', 14 | 'hr', 15 | 'quote', 16 | 'divider', 17 | 'ul', 18 | 'ol', 19 | 'task', 20 | 'indent', 21 | 'outdent', 22 | 'divider', 23 | 'table', 24 | 'image', 25 | 'link', 26 | 'divider', 27 | 'code', 28 | 'codeblock' 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Notify/TextBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | mdi-check-decagram-outline 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 94 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /src/components/RelateIssue/AddRelateDialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | mdi-close 12 | 13 | 添加关联关系 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | submit 29 | 30 | 31 | clear 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 91 | 92 | 95 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/components/Share/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | {{ item.title }} 7 | {{ item.title }} 8 | 9 | 10 | 11 | 12 | 13 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ 9 | item.label }} 10 | 11 | 12 | 13 | 14 | 15 | 58 | -------------------------------------------------------------------------------- /src/components/Sticky/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | sticky 9 | 10 | 11 | 12 | 13 | 14 | 92 | -------------------------------------------------------------------------------- /src/components/Suggest/TextBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | mdi-check-decagram-outline 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 94 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /src/components/Tinymce/dynamicLoadScript.js: -------------------------------------------------------------------------------- 1 | let callbacks = [] 2 | 3 | function loadedTinymce() { 4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 5 | // check is successfully downloaded script 6 | return window.tinymce 7 | } 8 | 9 | const dynamicLoadScript = (src, callback) => { 10 | const existingScript = document.getElementById(src) 11 | const cb = callback || function() {} 12 | 13 | if (!existingScript) { 14 | const script = document.createElement('script') 15 | script.src = src // src url for the third-party library being loaded. 16 | script.id = src 17 | document.body.appendChild(script) 18 | callbacks.push(cb) 19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd 20 | onEnd(script) 21 | } 22 | 23 | if (existingScript && cb) { 24 | if (loadedTinymce()) { 25 | cb(null, existingScript) 26 | } else { 27 | callbacks.push(cb) 28 | } 29 | } 30 | 31 | function stdOnEnd(script) { 32 | script.onload = function() { 33 | // this.onload = null here is necessary 34 | // because even IE9 works not like others 35 | this.onerror = this.onload = null 36 | for (const cb of callbacks) { 37 | cb(null, script) 38 | } 39 | callbacks = null 40 | } 41 | script.onerror = function() { 42 | this.onerror = this.onload = null 43 | cb(new Error('Failed to load ' + src), script) 44 | } 45 | } 46 | 47 | function ieOnEnd(script) { 48 | script.onreadystatechange = function() { 49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return 50 | this.onreadystatechange = null 51 | for (const cb of callbacks) { 52 | cb(null, script) // there is no way to catch loading errors in IE8 53 | } 54 | callbacks = null 55 | } 56 | } 57 | } 58 | 59 | export default dynamicLoadScript 60 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins.js: -------------------------------------------------------------------------------- 1 | // Any plugins you want to use has to be imported 2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 | // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 | 5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] 6 | 7 | export default plugins 8 | -------------------------------------------------------------------------------- /src/components/Tinymce/toolbar.js: -------------------------------------------------------------------------------- 1 | // Here is a list of the toolbar 2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 | 4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] 5 | 6 | export default toolbar 7 | -------------------------------------------------------------------------------- /src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 4 | const dragDom = el.querySelector('.el-dialog') 5 | dialogHeaderEl.style.cssText += ';cursor:move;' 6 | dragDom.style.cssText += ';top:0px;' 7 | 8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 9 | const getStyle = (function() { 10 | if (window.document.currentStyle) { 11 | return (dom, attr) => dom.currentStyle[attr] 12 | } else { 13 | return (dom, attr) => getComputedStyle(dom, false)[attr] 14 | } 15 | })() 16 | 17 | dialogHeaderEl.onmousedown = (e) => { 18 | // 鼠标按下,计算当前元素距离可视区的距离 19 | const disX = e.clientX - dialogHeaderEl.offsetLeft 20 | const disY = e.clientY - dialogHeaderEl.offsetTop 21 | 22 | const dragDomWidth = dragDom.offsetWidth 23 | const dragDomHeight = dragDom.offsetHeight 24 | 25 | const screenWidth = document.body.clientWidth 26 | const screenHeight = document.body.clientHeight 27 | 28 | const minDragDomLeft = dragDom.offsetLeft 29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 30 | 31 | const minDragDomTop = dragDom.offsetTop 32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 33 | 34 | // 获取到的值带px 正则匹配替换 35 | let styL = getStyle(dragDom, 'left') 36 | let styT = getStyle(dragDom, 'top') 37 | 38 | if (styL.includes('%')) { 39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 41 | } else { 42 | styL = +styL.replace(/\px/g, '') 43 | styT = +styT.replace(/\px/g, '') 44 | } 45 | 46 | document.onmousemove = function(e) { 47 | // 通过事件委托,计算移动的距离 48 | let left = e.clientX - disX 49 | let top = e.clientY - disY 50 | 51 | // 边界处理 52 | if (-(left) > minDragDomLeft) { 53 | left = -minDragDomLeft 54 | } else if (left > maxDragDomLeft) { 55 | left = maxDragDomLeft 56 | } 57 | 58 | if (-(top) > minDragDomTop) { 59 | top = -minDragDomTop 60 | } else if (top > maxDragDomTop) { 61 | top = maxDragDomTop 62 | } 63 | 64 | // 移动当前元素 65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 66 | 67 | // emit onDrag event 68 | vnode.child.$emit('dragDialog') 69 | } 70 | 71 | document.onmouseup = function(e) { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * ... 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 14 | -------------------------------------------------------------------------------- /src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | function checkPermission(el, binding) { 4 | const { value } = binding 5 | const roles = store.getters && store.getters.roles 6 | 7 | if (value && value instanceof Array) { 8 | if (value.length > 0) { 9 | const permissionRoles = value 10 | 11 | const hasPermission = roles.some(role => { 12 | return permissionRoles.includes(role) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } 19 | } else { 20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 21 | } 22 | } 23 | 24 | export default { 25 | inserted(el, binding) { 26 | checkPermission(el, binding) 27 | }, 28 | update(el, binding) { 29 | checkPermission(el, binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /src/directive/waves/waves.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MOSS2023ASE/service-frontend/b9822931ce6f3c38fe25e873e96446bf4d65f28b/src/directive/waves/waves.css -------------------------------------------------------------------------------- /src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | // import parseTime, formatTime and set to filter 2 | export { parseTime, formatTime } from '@/utils' 3 | 4 | /** 5 | * Show plural label if time is plural number 6 | * @param {number} time 7 | * @param {string} label 8 | * @return {string} 9 | */ 10 | function pluralize(time, label) { 11 | if (time === 1) { 12 | return time + label 13 | } 14 | return time + label + 's' 15 | } 16 | 17 | /** 18 | * @param {number} time 19 | */ 20 | export function timeAgo(time) { 21 | const between = Date.now() / 1000 - Number(time) 22 | if (between < 3600) { 23 | return pluralize(~~(between / 60), ' minute') 24 | } else if (between < 86400) { 25 | return pluralize(~~(between / 3600), ' hour') 26 | } else { 27 | return pluralize(~~(between / 86400), ' day') 28 | } 29 | } 30 | 31 | /** 32 | * Number formatting 33 | * like 10000 => 10k 34 | * @param {number} num 35 | * @param {number} digits 36 | */ 37 | export function numberFormatter(num, digits) { 38 | const si = [ 39 | { value: 1E18, symbol: 'E' }, 40 | { value: 1E15, symbol: 'P' }, 41 | { value: 1E12, symbol: 'T' }, 42 | { value: 1E9, symbol: 'G' }, 43 | { value: 1E6, symbol: 'M' }, 44 | { value: 1E3, symbol: 'k' } 45 | ] 46 | for (let i = 0; i < si.length; i++) { 47 | if (num >= si[i].value) { 48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol 49 | } 50 | } 51 | return num.toString() 52 | } 53 | 54 | /** 55 | * 10000 => "10,000" 56 | * @param {number} num 57 | */ 58 | export function toThousandFilter(num) { 59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) 60 | } 61 | 62 | /** 63 | * Upper case first char 64 | * @param {String} string 65 | */ 66 | export function uppercaseFirst(string) { 67 | return string.charAt(0).toUpperCase() + string.slice(1) 68 | } 69 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/ASidebarItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | import '@mdi/font/css/materialdesignicons.min.css' 7 | import Element from 'element-ui' 8 | // import { ElementTiptapPlugin } from 'element-tiptap' 9 | import './styles/element-variables.scss' 10 | // import 'element-tiptap/lib/index.css' 11 | import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖 12 | 13 | import '@/styles/index.scss' // global css 14 | 15 | import App from './App' 16 | import store from './store' 17 | import router from './router' 18 | 19 | import './icons' // icon 20 | import './permission' // permission control 21 | import './utils/error-log' // error log 22 | import vuetify from '@/plugins/vuetify' // path to vuetify export 23 | import * as filters from './filters' // global filters 24 | import Vuetify from 'vuetify' 25 | import 'vuetify/dist/vuetify.min.css' 26 | import dayjs from 'dayjs' 27 | Vue.prototype.dayjs = dayjs; 28 | import Antd from 'ant-design-vue'; 29 | import 'ant-design-vue/dist/antd.css'; 30 | import Vuelidate from 'vuelidate' 31 | 32 | import echarts from 'echarts'; 33 | Vue.prototype.$echarts = echarts; 34 | 35 | import mavonEditor from 'mavon-editor' 36 | import 'mavon-editor/dist/css/index.css' 37 | 38 | /** 39 | * If you don't want to use mock-server 40 | * you want to use MockJs for mock api 41 | * you can execute: mockXHR() 42 | * 43 | * Currently MockJs will be used in the production environment, 44 | * please remove it before going online ! ! ! 45 | */ 46 | /* 47 | if (process.env.NODE_ENV === 'production') { 48 | const { mockXHR } = require('../mock') 49 | mockXHR() 50 | } 51 | */ 52 | 53 | Vue.use(Element, { 54 | size: Cookies.get('size') || 'medium', // set element-ui default size 55 | locale: enLang // 如果使用中文,无需设置,请删除 56 | }) 57 | Vue.use(mavonEditor) 58 | // Vue.use(ElementTiptapPlugin, { 59 | // lang: 'zh' 60 | // }) 61 | 62 | // register global utility filters 63 | Object.keys(filters).forEach(key => { 64 | Vue.filter(key, filters[key]) 65 | }) 66 | 67 | Vue.config.productionTip = false 68 | 69 | Vue.use(Vuetify) 70 | 71 | Vue.use(Antd) 72 | 73 | new Vue({ 74 | vuetify, 75 | el: '#app', 76 | router, 77 | store, 78 | render: h => h(App) 79 | }) 80 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | // src/plugins/vuetify.js 2 | 3 | import Vue from 'vue' 4 | import Vuetify from 'vuetify' 5 | import 'vuetify/dist/vuetify.min.css' 6 | 7 | Vue.use(Vuetify) 8 | 9 | const opts = {} 10 | 11 | export default new Vuetify(opts) 12 | -------------------------------------------------------------------------------- /src/router/modules/charts.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | 3 | import Layout from '@/layout' 4 | 5 | const chartsRouter = { 6 | path: '/charts', 7 | component: Layout, 8 | redirect: 'noRedirect', 9 | name: 'Charts', 10 | meta: { 11 | title: 'Charts', 12 | icon: 'chart' 13 | }, 14 | children: [ 15 | { 16 | path: 'keyboard', 17 | component: () => import('@/views/charts/keyboard'), 18 | name: 'KeyboardChart', 19 | meta: { title: 'Keyboard Chart', noCache: true } 20 | }, 21 | { 22 | path: 'line', 23 | component: () => import('@/views/charts/line'), 24 | name: 'LineChart', 25 | meta: { title: 'Line Chart', noCache: true } 26 | }, 27 | { 28 | path: 'mix-chart', 29 | component: () => import('@/views/charts/mix-chart'), 30 | name: 'MixChart', 31 | meta: { title: 'Mix Chart', noCache: true } 32 | } 33 | ] 34 | } 35 | 36 | export default chartsRouter 37 | -------------------------------------------------------------------------------- /src/router/modules/nested.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout' 4 | 5 | const nestedRouter = { 6 | path: '/nested', 7 | component: Layout, 8 | redirect: '/nested/menu1/menu1-1', 9 | name: 'Nested', 10 | meta: { 11 | title: 'Nested Routes', 12 | icon: 'nested' 13 | }, 14 | children: [ 15 | { 16 | path: 'menu1', 17 | component: () => import('@/views/nested/menu1/index'), // Parent router-view 18 | name: 'Menu1', 19 | meta: { title: 'Menu 1' }, 20 | redirect: '/nested/menu1/menu1-1', 21 | children: [ 22 | { 23 | path: 'menu1-1', 24 | component: () => import('@/views/nested/menu1/menu1-1'), 25 | name: 'Menu1-1', 26 | meta: { title: 'Menu 1-1' } 27 | }, 28 | { 29 | path: 'menu1-2', 30 | component: () => import('@/views/nested/menu1/menu1-2'), 31 | name: 'Menu1-2', 32 | redirect: '/nested/menu1/menu1-2/menu1-2-1', 33 | meta: { title: 'Menu 1-2' }, 34 | children: [ 35 | { 36 | path: 'menu1-2-1', 37 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), 38 | name: 'Menu1-2-1', 39 | meta: { title: 'Menu 1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), 44 | name: 'Menu1-2-2', 45 | meta: { title: 'Menu 1-2-2' } 46 | } 47 | ] 48 | }, 49 | { 50 | path: 'menu1-3', 51 | component: () => import('@/views/nested/menu1/menu1-3'), 52 | name: 'Menu1-3', 53 | meta: { title: 'Menu 1-3' } 54 | } 55 | ] 56 | }, 57 | { 58 | path: 'menu2', 59 | name: 'Menu2', 60 | component: () => import('@/views/nested/menu2/index'), 61 | meta: { title: 'Menu 2' } 62 | } 63 | ] 64 | } 65 | 66 | export default nestedRouter 67 | -------------------------------------------------------------------------------- /src/router/modules/table.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout' 4 | 5 | const tableRouter = { 6 | path: '/table', 7 | component: Layout, 8 | redirect: '/table/complex-table', 9 | name: 'Table', 10 | meta: { 11 | title: 'Table', 12 | icon: 'table' 13 | }, 14 | children: [ 15 | { 16 | path: 'dynamic-table', 17 | component: () => import('@/views/table/dynamic-table/index'), 18 | name: 'DynamicTable', 19 | meta: { title: 'Dynamic Table' } 20 | }, 21 | { 22 | path: 'drag-table', 23 | component: () => import('@/views/table/drag-table'), 24 | name: 'DragTable', 25 | meta: { title: 'Drag Table' } 26 | }, 27 | { 28 | path: 'inline-edit-table', 29 | component: () => import('@/views/table/inline-edit-table'), 30 | name: 'InlineEditTable', 31 | meta: { title: 'Inline Edit' } 32 | }, 33 | { 34 | path: 'complex-table', 35 | component: () => import('@/views/table/complex-table'), 36 | name: 'ComplexTable', 37 | meta: { title: 'Complex Table' } 38 | } 39 | ] 40 | } 41 | export default tableRouter 42 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: '士问士答 WebSite', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: false, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: false, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether show the logo in sidebar 25 | */ 26 | sidebarLogo: false, 27 | 28 | /** 29 | * @type {string | array} 'production' | ['production', 'development'] 30 | * @description Need show err logs component. 31 | * The default is only used in the production env 32 | * If you want to also use it in dev, you can pass ['production', 'development'] 33 | */ 34 | errorLog: 'production' 35 | } 36 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | size: state => state.app.size, 4 | device: state => state.app.device, 5 | visitedViews: state => state.tagsView.visitedViews, 6 | cachedViews: state => state.tagsView.cachedViews, 7 | token: state => state.user.token, 8 | avatar: state => state.user.avatar, 9 | name: state => state.user.name, 10 | introduction: state => state.user.introduction, 11 | roles: state => state.user.roles, 12 | permission_routes: state => state.permission.routes, 13 | errorLogs: state => state.errorLog.logs, 14 | user_id: state => state.user.user_id 15 | } 16 | export default getters 17 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : false, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium' 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | logs: [] 3 | } 4 | 5 | const mutations = { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | }, 9 | CLEAR_ERROR_LOG: (state) => { 10 | state.logs.splice(0) 11 | } 12 | } 13 | 14 | const actions = { 15 | addErrorLog({ commit }, log) { 16 | commit('ADD_ERROR_LOG', log) 17 | }, 18 | clearErrorLog({ commit }) { 19 | commit('CLEAR_ERROR_LOG') 20 | } 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '@/router' 2 | 3 | /** 4 | * Use meta.role to determine if the current user has permission 5 | * @param roles 6 | * @param route 7 | */ 8 | function hasPermission(roles, route) { 9 | if (route.meta && route.meta.roles) { 10 | return roles.some(role => route.meta.roles.includes(role)) 11 | } else { 12 | return true 13 | } 14 | } 15 | 16 | /** 17 | * Filter asynchronous routing tables by recursion 18 | * @param routes asyncRoutes 19 | * @param roles 20 | */ 21 | export function filterAsyncRoutes(routes, roles) { 22 | const res = [] 23 | 24 | routes.forEach(route => { 25 | const tmp = { ...route } 26 | if (hasPermission(roles, tmp)) { 27 | if (tmp.children) { 28 | tmp.children = filterAsyncRoutes(tmp.children, roles) 29 | } 30 | res.push(tmp) 31 | } 32 | }) 33 | 34 | return res 35 | } 36 | 37 | const state = { 38 | routes: constantRoutes, 39 | addRoutes: [] 40 | } 41 | 42 | const mutations = { 43 | SET_ROUTES: (state, routes) => { 44 | state.addRoutes = routes 45 | state.routes = constantRoutes.concat(routes) 46 | } 47 | } 48 | 49 | const actions = { 50 | generateRoutes({ commit }, roles) { 51 | return new Promise(resolve => { 52 | let accessedRoutes 53 | if (roles.includes('admin')) { 54 | accessedRoutes = asyncRoutes || [] 55 | } else { 56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 57 | } 58 | commit('SET_ROUTES', accessedRoutes) 59 | resolve(accessedRoutes) 60 | }) 61 | } 62 | } 63 | 64 | export default { 65 | namespaced: true, 66 | state, 67 | mutations, 68 | actions 69 | } 70 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 5 | 6 | const state = { 7 | theme: variables.theme, 8 | showSettings: showSettings, 9 | tagsView: tagsView, 10 | fixedHeader: fixedHeader, 11 | sidebarLogo: sidebarLogo 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | // eslint-disable-next-line no-prototype-builtins 17 | if (state.hasOwnProperty(key)) { 18 | state[key] = value 19 | } 20 | } 21 | } 22 | 23 | const actions = { 24 | changeSetting({ commit }, data) { 25 | commit('CHANGE_SETTING', data) 26 | } 27 | } 28 | 29 | export default { 30 | namespaced: true, 31 | state, 32 | mutations, 33 | actions 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#FFE9C6; 13 | $menuActiveText:#FFD700; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | //$menuBg:#304156; 17 | $menuBg: #1687A7; 18 | $menuHover: #D3E0EA;; 19 | 20 | $subMenuBg:#1f2d3d; 21 | $subMenuHover:#FFB642; 22 | 23 | $sideBarWidth: 210px; 24 | 25 | // the :export directive is the magic sauce for webpack 26 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 27 | :export { 28 | menuText: $menuText; 29 | menuActiveText: $menuActiveText; 30 | subMenuActiveText: $subMenuActiveText; 31 | menuBg: $menuBg; 32 | menuHover: $menuHover; 33 | subMenuBg: $subMenuBg; 34 | subMenuHover: $subMenuHover; 35 | sideBarWidth: $sideBarWidth; 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Token' 4 | 5 | const UserIdKey = 'UserId' 6 | 7 | const RoleKey = 'Visitor' 8 | 9 | export function getToken() { 10 | return Cookies.get(TokenKey) 11 | } 12 | 13 | export function setToken(token) { 14 | return Cookies.set(TokenKey, token) 15 | } 16 | 17 | export function removeToken() { 18 | return Cookies.remove(TokenKey) 19 | } 20 | 21 | export function getUserId() { 22 | return Cookies.get(UserIdKey) 23 | } 24 | 25 | export function setUserId(user_id) { 26 | return Cookies.set(UserIdKey, user_id) 27 | } 28 | 29 | export function removeUserId() { 30 | return Cookies.remove(UserIdKey) 31 | } 32 | 33 | export function getRole() { 34 | return parseInt(Cookies.get(RoleKey)) 35 | } 36 | 37 | export function removeRole() { 38 | return Cookies.remove(RoleKey) 39 | } 40 | 41 | export function setRole(roles) { 42 | return Cookies.set(RoleKey, roles) 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/bus 2.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const bus = new Vue(); 4 | 5 | export default bus; 6 | -------------------------------------------------------------------------------- /src/utils/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const bus = new Vue(); 4 | 5 | export default bus; 6 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: 'Copy successfully', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: 'Copy failed', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/error-log.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import { isString, isArray } from '@/utils/validate' 4 | import settings from '@/settings' 5 | 6 | // you can set in settings.js 7 | // errorLog:'production' | ['production', 'development'] 8 | const { errorLog: needErrorLog } = settings 9 | 10 | function checkNeed() { 11 | const env = process.env.NODE_ENV 12 | if (isString(needErrorLog)) { 13 | return env === needErrorLog 14 | } 15 | if (isArray(needErrorLog)) { 16 | return needErrorLog.includes(env) 17 | } 18 | return false 19 | } 20 | 21 | if (checkNeed()) { 22 | Vue.config.errorHandler = function(err, vm, info, a) { 23 | // Don't ask me why I use Vue.nextTick, it just a hack. 24 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500 25 | Vue.nextTick(() => { 26 | store.dispatch('errorLog/addErrorLog', { 27 | err, 28 | vm, 29 | info, 30 | url: window.location.href 31 | }) 32 | 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Created by PanJiaChen on 16/11/29. 3 | * @param {Sting} url 4 | * @param {Sting} title 5 | * @param {Number} w 6 | * @param {Number} h 7 | */ 8 | export default function openWindow(url, title, w, h) { 9 | // Fixes dual-screen position Most browsers Firefox 10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 12 | 13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 15 | 16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 17 | const top = ((height / 2) - (h / 2)) + dualScreenTop 18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 19 | 20 | // Puts focus on the newWindow 21 | if (window.focus) { 22 | newWindow.focus() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | const hasPermission = roles.some(role => { 13 | return permissionRoles.includes(role) 14 | }) 15 | return hasPermission 16 | } else { 17 | 18 | return false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | //baseURL: 'shieask.com:8080', // url = base url + request url 10 | withCredentials: true, // send cookies when cross-domain requests 11 | timeout: 50000 // request timeout 12 | }) 13 | 14 | // request interceptor 15 | service.interceptors.request.use( 16 | config => { 17 | // do something before request is sent 18 | 19 | if (store.getters.token) { 20 | // let each request carry token 21 | // ['X-Token'] is a custom headers key 22 | // please modify it according to the actual situation 23 | config.headers['token'] = getToken() 24 | } 25 | return config 26 | }, 27 | error => { 28 | // do something with request error 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000 && res.code !== 200 && res.code !== 0) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | Message({ 76 | message: error.message, 77 | type: 'error', 78 | duration: 5 * 10000 79 | }) 80 | return Promise.reject(error) 81 | } 82 | ) 83 | 84 | export default service 85 | -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | return true 19 | } 20 | 21 | /** 22 | * @param {string} url 23 | * @returns {Boolean} 24 | */ 25 | export function validURL(url) { 26 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 27 | return reg.test(url) 28 | } 29 | 30 | /** 31 | * @param {string} str 32 | * @returns {Boolean} 33 | */ 34 | export function validLowerCase(str) { 35 | const reg = /^[a-z]+$/ 36 | return reg.test(str) 37 | } 38 | 39 | /** 40 | * @param {string} str 41 | * @returns {Boolean} 42 | */ 43 | export function validUpperCase(str) { 44 | const reg = /^[A-Z]+$/ 45 | return reg.test(str) 46 | } 47 | 48 | /** 49 | * @param {string} str 50 | * @returns {Boolean} 51 | */ 52 | export function validAlphabets(str) { 53 | const reg = /^[A-Za-z]+$/ 54 | return reg.test(str) 55 | } 56 | 57 | /** 58 | * @param {string} email 59 | * @returns {Boolean} 60 | */ 61 | export function validEmail(email) { 62 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 63 | return reg.test(email) 64 | } 65 | 66 | /** 67 | * @param {string} str 68 | * @returns {Boolean} 69 | */ 70 | export function isString(str) { 71 | if (typeof str === 'string' || str instanceof String) { 72 | return true 73 | } 74 | return false 75 | } 76 | 77 | /** 78 | * @param {Array} arg 79 | * @returns {Boolean} 80 | */ 81 | export function isArray(arg) { 82 | if (typeof Array.isArray === 'undefined') { 83 | return Object.prototype.toString.call(arg) === '[object Array]' 84 | } 85 | return Array.isArray(arg) 86 | } 87 | -------------------------------------------------------------------------------- /src/vendor/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/error-log/components/ErrorTestA.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ a.a }} 5 | 6 | 7 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/error-log/components/ErrorTestB.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/views/error-log/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Please click the bug icon in the upper right corner 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /src/views/icons/svg-icons.js: -------------------------------------------------------------------------------- 1 | const req = require.context('../../icons/svg', false, /\.svg$/) 2 | const requireAll = requireContext => requireContext.keys() 3 | 4 | const re = /\.\/(.*)\.svg/ 5 | 6 | const svgIcons = requireAll(req).map(i => { 7 | return i.match(re)[1] 8 | }) 9 | 10 | export default svgIcons 11 | -------------------------------------------------------------------------------- /src/views/issueInfo/components/Confirm.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 确认操作 12 | 13 | {{ this.text }} 14 | 15 | 16 | 21 | 确认 22 | 23 | 28 | 取消 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 62 | 63 | 66 | -------------------------------------------------------------------------------- /src/views/issueInfo/components/MyRichText.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 27 | 28 | 45 | -------------------------------------------------------------------------------- /src/views/login/auth-redirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/views/login/components/SocialSignin.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WeChat 6 | 7 | 8 | 9 | QQ 10 | 11 | 12 | 13 | 14 | 39 | 40 | 73 | -------------------------------------------------------------------------------- /src/views/login/components/registerPanel.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 71 | 78 | 79 | -------------------------------------------------------------------------------- /src/views/postIssue/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 36 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/views/userManage/labelView.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 标签管理 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ label.content }} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 添加标签 22 | 23 | 24 | 25 | 26 | 27 | 新建标签 28 | 29 | 30 | 31 | 创建 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/views/userManage/postMessage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 发送 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | describe('Utils:formatTime', () => { 3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 4 | const retrofit = 5 * 1000 5 | 6 | it('ten digits timestamp', () => { 7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 8 | }) 9 | it('test now', () => { 10 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 11 | }) 12 | it('less two minute', () => { 13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 14 | }) 15 | it('less two hour', () => { 16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 17 | }) 18 | it('less one day', () => { 19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 20 | }) 21 | it('more than one day', () => { 22 | expect(formatTime(d)).toBe('7月13日17时54分') 23 | }) 24 | it('format', () => { 25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | 9 | it('timestamp string', () => { 10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 11 | }) 12 | 13 | it('ten digits timestamp', () => { 14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 15 | }) 16 | it('new Date', () => { 17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 18 | }) 19 | it('format', () => { 20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 23 | }) 24 | it('get the day of the week', () => { 25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 26 | }) 27 | it('get the day of the week', () => { 28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 29 | }) 30 | it('empty argument', () => { 31 | expect(parseTime()).toBeNull() 32 | }) 33 | 34 | it('null', () => { 35 | expect(parseTime(null)).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js' 2 | describe('Utils:validate', () => { 3 | it('validUsername', () => { 4 | expect(validUsername('admin')).toBe(true) 5 | expect(validUsername('editor')).toBe(true) 6 | expect(validUsername('xxxx')).toBe(false) 7 | }) 8 | it('validURL', () => { 9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false) 12 | }) 13 | it('validLowerCase', () => { 14 | expect(validLowerCase('abc')).toBe(true) 15 | expect(validLowerCase('Abc')).toBe(false) 16 | expect(validLowerCase('123abc')).toBe(false) 17 | }) 18 | it('validUpperCase', () => { 19 | expect(validUpperCase('ABC')).toBe(true) 20 | expect(validUpperCase('Abc')).toBe(false) 21 | expect(validUpperCase('123ABC')).toBe(false) 22 | }) 23 | it('validAlphabets', () => { 24 | expect(validAlphabets('ABC')).toBe(true) 25 | expect(validAlphabets('Abc')).toBe(true) 26 | expect(validAlphabets('123aBC')).toBe(false) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /vue-element-admin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------