├── .browserslistrc ├── .env.test ├── .env.production ├── demo ├── dash_board.png ├── ts-vue-demo-01.png └── ts-vue-demo-02.png ├── public ├── favicon.ico ├── images │ └── ts-vue-login-banner01.jpeg └── index.html ├── src ├── assets │ ├── logo.png │ ├── avatar.jpeg │ └── 404-images │ │ ├── 404.png │ │ └── 404-cloud.png ├── api │ ├── types.d.ts │ ├── users.ts │ └── articles.ts ├── styles │ ├── _mixins.scss │ ├── element-variables.scss.d.ts │ ├── _variables.scss.d.ts │ ├── _svgicon.scss │ ├── element-variables.scss │ ├── _variables.scss │ ├── _transition.scss │ └── index.scss ├── icons │ ├── svg │ │ ├── chart.svg │ │ ├── guide.svg │ │ ├── link.svg │ │ ├── lock.svg │ │ ├── fullscreen.svg │ │ ├── user.svg │ │ ├── example.svg │ │ ├── table.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── hamburger.svg │ │ ├── edit.svg │ │ ├── list.svg │ │ ├── eye-off.svg │ │ ├── eye-on.svg │ │ ├── exit-fullscreen.svg │ │ ├── setting.svg │ │ ├── tree.svg │ │ ├── dashboard.svg │ │ └── form.svg │ ├── components │ │ ├── chart.ts │ │ ├── guide.ts │ │ ├── link.ts │ │ ├── lock.ts │ │ ├── index.ts │ │ ├── example.ts │ │ ├── user.ts │ │ ├── fullscreen.ts │ │ ├── table.ts │ │ ├── hamburger.ts │ │ ├── nested.ts │ │ ├── password.ts │ │ ├── edit.ts │ │ ├── list.ts │ │ ├── eye-off.ts │ │ ├── setting.ts │ │ ├── eye-on.ts │ │ ├── exit-fullscreen.ts │ │ ├── tree.ts │ │ ├── dashboard.ts │ │ └── form.ts │ └── README.md ├── utils │ ├── validate.ts │ ├── cookies.ts │ ├── index.ts │ ├── scroll-to.ts │ ├── request.ts │ └── utils.ts ├── layout │ ├── components │ │ ├── index.ts │ │ ├── AppMain.vue │ │ ├── Sidebar │ │ │ ├── SidebarItemLink.vue │ │ │ ├── index.vue │ │ │ └── SidebarItem.vue │ │ ├── Settings │ │ │ └── index.vue │ │ └── Navbar │ │ │ └── index.vue │ ├── mixin │ │ └── resize.ts │ └── index.vue ├── views │ ├── charts │ │ └── index.vue │ ├── account │ │ └── index.vue │ ├── setting │ │ └── index.vue │ ├── table │ │ ├── edit.vue │ │ ├── create.vue │ │ ├── index.vue │ │ └── components │ │ │ └── ArticleDetail.vue │ ├── guide │ │ ├── index.vue │ │ └── steps.ts │ ├── dashboard │ │ ├── index.vue │ │ ├── editor │ │ │ └── index.vue │ │ └── admin │ │ │ ├── components │ │ │ ├── BarChart.vue │ │ │ └── LineChart.vue │ │ │ └── index.vue │ ├── tree │ │ └── index.vue │ ├── 404.vue │ ├── error-page │ │ └── 404.vue │ └── login │ │ └── index.vue ├── App.vue ├── shims-tsx.d.ts ├── filters │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ ├── settings.ts │ │ ├── app.ts │ │ ├── permission.ts │ │ └── user.ts ├── components │ ├── Hamburger │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── Charts │ │ └── mixins │ │ │ └── resize.ts │ ├── Pagination │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── RightPanel │ │ └── index.vue │ ├── InterstingDemo │ │ └── tableRowEdit.vue │ └── ThemePicker │ │ └── index.vue ├── shims-vue.d.ts ├── main.ts ├── settings.ts ├── peimission.ts ├── README.md └── router │ └── index.ts ├── postcss.config.js ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.ts ├── commitlint.config.js ├── .husky ├── pre-commit ├── commit-msg └── _ │ └── husky.sh ├── .env.development ├── babel.config.js ├── mock └── mock-server.ts ├── .gitignore ├── .eslintrc.js ├── tsconfig.json ├── package.json ├── vue.config.js └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | VUE_APP_API= 3 | VUE_APP_DEV=false 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VUE_APP_API= 3 | VUE_APP_DEV=false -------------------------------------------------------------------------------- /demo/dash_board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/demo/dash_board.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/src/assets/avatar.jpeg -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"] 3 | }; 4 | -------------------------------------------------------------------------------- /demo/ts-vue-demo-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/demo/ts-vue-demo-01.png -------------------------------------------------------------------------------- /demo/ts-vue-demo-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/demo/ts-vue-demo-02.png -------------------------------------------------------------------------------- /src/assets/404-images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/src/assets/404-images/404.png -------------------------------------------------------------------------------- /src/assets/404-images/404-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/src/assets/404-images/404-cloud.png -------------------------------------------------------------------------------- /public/images/ts-vue-login-banner01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easy-wheel/ts-vue/HEAD/public/images/ts-vue-login-banner01.jpeg -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | echo "========= 执行pre-commit操作(如执行测试用例、eslint校验等,可自行添加) =======" 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | echo "========= 执行commit-msg校验 =======" 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /src/api/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IArticleData { 2 | id: number; 3 | status: string; 4 | timestamp: string | number; 5 | author: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* Mixins */ 2 | @mixin clearfix { 3 | &:after { 4 | content: ""; 5 | display: table; 6 | clear: both; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | # Base api 3 | VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server.armour.now.sh/mock-api/v1/' 4 | VUE_APP_DEV=true -------------------------------------------------------------------------------- /src/styles/element-variables.scss.d.ts: -------------------------------------------------------------------------------- 1 | export interface IScssVariables { 2 | theme: string; 3 | } 4 | 5 | export const variables: IScssVariables; 6 | 7 | export default variables; 8 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/styles/_variables.scss.d.ts: -------------------------------------------------------------------------------- 1 | export interface IScssVariables { 2 | menuBg: string; 3 | menuText: string; 4 | menuActiveText: string; 5 | } 6 | 7 | export const variables: IScssVariables; 8 | 9 | export default variables; 10 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | export const isValidUsername = (str: string) => 2 | ["admin", "editor"].indexOf(str.trim()) >= 0; 3 | 4 | // 判断是外链,直接跳转。否则使用router-link进行路由跳转 5 | export const isExternal = (path: string) => 6 | /^(https?:|mailto:|tel:)/.test(path); 7 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layout/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from "./AppMain.vue"; 2 | export { default as Navbar } from "./Navbar/index.vue"; 3 | export { default as Sidebar } from "./Sidebar/index.vue"; 4 | export { default as Settings } from "./Settings/index.vue"; 5 | -------------------------------------------------------------------------------- /src/views/charts/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/account/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/views/setting/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | // plugins: [ 4 | // [ 5 | // "component", 6 | // { 7 | // libraryName: "element-ui", 8 | // styleLibraryName: "theme-chalk" 9 | // } 10 | // ] 11 | // ] 12 | }; 13 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /mock/mock-server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import bodyParser from "body-parser"; 3 | import compression from "compression"; 4 | import morgan from "morgan"; 5 | import cors from "cors"; 6 | import http from "http"; 7 | import path from "path"; 8 | import yaml from "yamljs"; 9 | 10 | // TODO: 后期mock接入 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/components/chart.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'chart': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/api/users.ts: -------------------------------------------------------------------------------- 1 | import { getData, postData } from "@/utils/request"; 2 | 3 | export const getUsers = (params: any) => getData("/users", params); 4 | 5 | export const getUserInfo = (params: any) => postData("/users/info", params); 6 | 7 | export const login = (params: any) => postData("/users/login", params); 8 | 9 | export const logout = () => postData("/users/logout"); 10 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | // Set utils function parseTime to filter 2 | export { parseTime } from "@/utils"; 3 | 4 | // Filter for article status 5 | export const articleStatusFilter = (status: string) => { 6 | const statusMap: { [key: string]: string } = { 7 | published: "success", 8 | draft: "info", 9 | deleted: "danger" 10 | }; 11 | return statusMap[status]; 12 | }; 13 | -------------------------------------------------------------------------------- /src/views/table/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/views/table/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /src/icons/components/guide.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'guide': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/api/articles.ts: -------------------------------------------------------------------------------- 1 | import { getData, postData } from "@/utils/request"; 2 | import { IArticleData } from "./types"; 3 | 4 | export const defaultArticleData: IArticleData = { 5 | id: 0, 6 | status: "", 7 | timestamp: "", 8 | author: "" 9 | }; 10 | 11 | export const getArticles = (params: any) => getData("/articles", params); 12 | 13 | export const getArticle = (id: number, params: any) => 14 | getData(`/articles/${id}`, params); 15 | -------------------------------------------------------------------------------- /src/icons/components/link.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'link': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { shallowMount } from "@vue/test-utils"; 3 | import HelloWorld from "@/components/HelloWorld.vue"; 4 | 5 | describe("HelloWorld.vue", () => { 6 | it("renders props.msg when passed", () => { 7 | const msg = "new message"; 8 | const wrapper = shallowMount(HelloWorld, { 9 | propsData: { msg } 10 | }); 11 | expect(wrapper.text()).to.include(msg); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /src/icons/components/lock.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'lock': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/components/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | import "./chart"; 3 | import "./dashboard"; 4 | import "./edit"; 5 | import "./example"; 6 | import "./exit-fullscreen"; 7 | import "./eye-off"; 8 | import "./eye-on"; 9 | import "./form"; 10 | import "./fullscreen"; 11 | import "./guide"; 12 | import "./hamburger"; 13 | import "./link"; 14 | import "./list"; 15 | import "./lock"; 16 | import "./nested"; 17 | import "./password"; 18 | import "./setting"; 19 | import "./table"; 20 | import "./tree"; 21 | import "./user"; 22 | -------------------------------------------------------------------------------- /src/icons/components/example.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'example': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/user.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'user': { 7 | width: 130, 8 | height: 130, 9 | viewBox: '0 0 130 130', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/fullscreen.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'fullscreen': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/styles/_svgicon.scss: -------------------------------------------------------------------------------- 1 | /* Recommended css code for vue-svgicon */ 2 | .svg-icon { 3 | display: inline-block; 4 | width: 16px; 5 | height: 16px; 6 | color: inherit; 7 | fill: none; 8 | stroke: currentColor; 9 | vertical-align: -0.15em; 10 | } 11 | 12 | .svg-fill { 13 | fill: currentColor; 14 | stroke: none; 15 | } 16 | 17 | .svg-up { 18 | transform: rotate(0deg); 19 | } 20 | 21 | .svg-right { 22 | transform: rotate(90deg); 23 | } 24 | 25 | .svg-down { 26 | transform: rotate(180deg); 27 | } 28 | 29 | .svg-left { 30 | transform: rotate(-90deg); 31 | } 32 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import { IAppState } from "./modules/app"; 4 | import { IUserState } from "./modules/user"; 5 | import { IPermissionState } from "./modules/permission"; 6 | import { ISettingsState } from "./modules/settings"; 7 | 8 | Vue.use(Vuex); 9 | 10 | export interface IRootState { 11 | app: IAppState; 12 | user: IUserState; 13 | permission: IPermissionState; 14 | settings: ISettingsState; 15 | } 16 | 17 | // Declare empty store first, dynamically register all modules later. 18 | export default new Vuex.Store({}); 19 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItemLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ts-vue 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/components/table.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'table': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/eye-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/cookies.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | 3 | // App 4 | const sidebarStatusKey = "sidebar_status"; 5 | export const getSidebarStatus = () => Cookies.get(sidebarStatusKey); // 获取siderBar状态(开启/关闭) 6 | export const setSidebarStatus = ( 7 | sidebarStatus: string // 设置siderBar状态 8 | ) => Cookies.set(sidebarStatusKey, sidebarStatus); 9 | 10 | // User 11 | const tokenKey = "vue_typescript_admin_access_token"; 12 | export const getToken = () => Cookies.get(tokenKey); // 获取token 13 | export const setToken = (token: string) => Cookies.set(tokenKey, token); // 设置token 14 | export const removeToken = () => Cookies.remove(tokenKey); // 移除token 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true 7 | }, 8 | extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"], 9 | rules: { 10 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 11 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 12 | }, 13 | parserOptions: { 14 | parser: "@typescript-eslint/parser", 15 | sourceType: "module" 16 | }, 17 | overrides: [ 18 | { 19 | files: ["**/__tests__/*.{j,t}s?(x)"], 20 | env: { 21 | mocha: true 22 | } 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /src/icons/README.md: -------------------------------------------------------------------------------- 1 | # vue-svgicon 2 | 3 | ## English 4 | 5 | * All svg components were generated by `vue-svgicon` using svg files 6 | * After you adding new svg files into `icons/svg` folder, run `yarn svg` to regerenrate all svg components (before this, you should have `vue-svgicon` installed globally or use `npx`) 7 | * See details at: [https://github.com/MMF-FE/vue-svgicon](https://github.com/MMF-FE/vue-svgicon) 8 | 9 | ## 中文 10 | 11 | * 所有的 svg 组件都是由 `vue-svgicon` 生成的 12 | * 每当在 `icons/svg` 文件夹内添加 icon 之后,可以通过执行 `yarn svg` 来重新生成所有组件 (在此之前需要全局安装 `vue-svgicon` 或使用 `npx`) 13 | * 详细文档请见:[https://github.com/MMF-FE/vue-svgicon](https://github.com/MMF-FE/vue-svgicon) 14 | -------------------------------------------------------------------------------- /src/icons/components/hamburger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'hamburger': { 7 | width: 64, 8 | height: 64, 9 | viewBox: '0 0 1024 1024', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/nested.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'nested': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/password.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'password': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /.husky/_/husky.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$husky_skip_init" ]; then 3 | debug () { 4 | [ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1" 5 | } 6 | 7 | readonly hook_name="$(basename "$0")" 8 | debug "starting $hook_name..." 9 | 10 | if [ "$HUSKY" = "0" ]; then 11 | debug "HUSKY env variable is set to 0, skipping hook" 12 | exit 0 13 | fi 14 | 15 | if [ -f ~/.huskyrc ]; then 16 | debug "sourcing ~/.huskyrc" 17 | . ~/.huskyrc 18 | fi 19 | 20 | export readonly husky_skip_init=1 21 | sh -e "$0" "$@" 22 | exitCode="$?" 23 | 24 | if [ $exitCode != 0 ]; then 25 | echo "husky - $hook_name hook exited with code $exitCode (error)" 26 | fi 27 | 28 | exit $exitCode 29 | fi 30 | -------------------------------------------------------------------------------- /src/icons/components/edit.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'edit': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/list.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'list': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/components/eye-off.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'eye-off': { 7 | width: 128, 8 | height: 64, 9 | viewBox: '0 0 128 64', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | 6 | // 声明文件(*.d.ts) 7 | // 参考https://juejin.im/post/5c7f3ee8f265da2de04adff6 8 | 9 | // 自定义描述文件: 在项目的【根目录】下定义【模块相同的名称】的描述文件A.d.ts,在描述文件内编写模块声明描述 10 | // A.d.ts 11 | // declare module '*'; 12 | // declare module 'A'; 13 | 14 | // 详细描述文件 15 | // A.d.ts 16 | // declare module "A" { 17 | // // 添加具体的描述内容 18 | // }; 19 | 20 | // 常规的类型描述 21 | // 1、module 22 | // 模块描述 声明这个是一个模块文件,模块里面使用export抛出相应的内容 23 | 24 | // 2、namespace 25 | // 定义命名空间,通常用的很少 26 | 27 | // 3、interface 28 | // 定义接口类型,这点一般配合class使用 29 | 30 | // 4、class 31 | // 定义class类型 32 | 33 | // 5、type 34 | // 定义基础类型变量 35 | 36 | // 6、global 37 | // 这个属性用来定义全局变量的申明的变量可以直接调用,这点用来某些直接绑定在window下的变量 38 | // declare global { 39 | // $:any 40 | // } 41 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* Element Variables */ 2 | 3 | // Override Element UI variables 4 | $--color-primary: #11a983; 5 | $--color-success: #13ce66; 6 | $--color-warning: #ffba00; 7 | $--color-danger: #ff4949; 8 | $--color-info: #5d5d5d; 9 | $--button-font-weight: 400; 10 | $--color-text-regular: #1f2d3d; 11 | $--border-color-light: #dfe4ed; 12 | $--border-color-lighter: #e6ebf5; 13 | $--table-border: 1px solid#dfe6ec; 14 | 15 | // Icon font path, required 16 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 17 | 18 | // Apply overrided variables in Element UI 19 | @import "~element-ui/packages/theme-chalk/src/index"; 20 | 21 | // The :export directive is the magic sauce for webpack 22 | // https://mattferderer.com/use-sass-variables-in-typescript-and-javascript 23 | :export { 24 | theme: $--color-primary; 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | 3 | // Base color 4 | $blue: #324157; 5 | $light-blue: #3a71a8; 6 | $red: #c03639; 7 | $pink: #e65d6e; 8 | $green: #30b08f; 9 | $tiffany: #4ab7bd; 10 | $yellow: #fec171; 11 | $panGreen: #30b08f; 12 | 13 | // Sidebar 14 | $sideBarWidth: 210px; 15 | $subMenuBg: #1f2d3d; 16 | $subMenuHover: #001528; 17 | $subMenuActiveText: #f4f4f5; 18 | $menuBg: #304156; 19 | $menuText: #bfcbd9; 20 | $menuActiveText: #409eff; // Also see settings.sidebarTextTheme 21 | 22 | // Login page 23 | $lightGray: #eee; 24 | $darkGray: #889aa4; 25 | $loginBg: #2d3a4b; 26 | $loginCursorColor: #fff; 27 | 28 | // The :export directive is the magic sauce for webpack 29 | // https://mattferderer.com/use-sass-variables-in-typescript-and-javascript 30 | :export { 31 | menuBg: $menuBg; 32 | menuText: $menuText; 33 | menuActiveText: $menuActiveText; 34 | } 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import Component from "vue-class-component"; 4 | import router from "@/router"; 5 | import store from "@/store"; 6 | 7 | import "normalize.css"; 8 | import ElementUI from "element-ui"; 9 | import SvgIcon from "vue-svgicon"; 10 | import "@/icons/components"; 11 | import "@/peimission"; 12 | import "@/styles/element-variables.scss"; 13 | import "@/styles/index.scss"; 14 | 15 | import * as filters from "@/filters"; 16 | 17 | Vue.use(ElementUI); 18 | Vue.use(SvgIcon, { 19 | tagName: "svg-icon", 20 | defaultWidth: "1em", 21 | defaultHeight: "1em" 22 | }); 23 | 24 | // Register global filter functions 25 | Object.keys(filters).forEach(key => { 26 | Vue.filter(key, (filters as { [key: string]: Function })[key]); 27 | }); 28 | Vue.config.productionTip = false; 29 | 30 | new Vue({ 31 | router, 32 | store, 33 | render: h => h(App) 34 | }).$mount("#app"); 35 | -------------------------------------------------------------------------------- /src/icons/components/setting.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'setting': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 1024 1024', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/eye-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/styles/_transition.scss: -------------------------------------------------------------------------------- 1 | /* Global transition */ 2 | // See https://vuejs.org/v2/guide/transitions.html for detail 3 | 4 | // fade 5 | .fade-enter-active, 6 | .fade-leave-active { 7 | transition: opacity 0.28s; 8 | } 9 | 10 | .fade-enter, 11 | .fade-leave-active { 12 | opacity: 0; 13 | } 14 | 15 | // fade-transform 16 | .fade-transform-leave-active, 17 | .fade-transform-enter-active { 18 | transition: all 0.5s; 19 | } 20 | 21 | .fade-transform-enter { 22 | opacity: 0; 23 | transform: translateX(-30px); 24 | } 25 | 26 | .fade-transform-leave-to { 27 | opacity: 0; 28 | transform: translateX(30px); 29 | } 30 | 31 | // breadcrumb 32 | .breadcrumb-enter-active, 33 | .breadcrumb-leave-active { 34 | transition: all 0.5s; 35 | } 36 | 37 | .breadcrumb-enter, 38 | .breadcrumb-leave-active { 39 | opacity: 0; 40 | transform: translateX(20px); 41 | } 42 | 43 | .breadcrumb-move { 44 | transition: all 0.5s; 45 | } 46 | 47 | .breadcrumb-leave-active { 48 | position: absolute; 49 | } 50 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // @import './variables.scss'; // Already imported in style-resources-loader 2 | // @import './mixins.scss'; // Already imported in style-resources-loader 3 | @import "./transition.scss"; 4 | @import "./svgicon.scss"; 5 | 6 | /* Global scss */ 7 | 8 | body { 9 | height: 100%; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica Neue, 13 | Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 14 | } 15 | 16 | html { 17 | height: 100%; 18 | } 19 | 20 | #app { 21 | height: 100%; 22 | } 23 | 24 | *, 25 | *:before, 26 | *:after { 27 | box-sizing: border-box; 28 | } 29 | 30 | a, 31 | a:focus, 32 | a:hover { 33 | color: inherit; 34 | outline: none; 35 | text-decoration: none; 36 | } 37 | 38 | div:focus { 39 | outline: none; 40 | } 41 | 42 | .clearfix { 43 | @include clearfix; 44 | } 45 | 46 | .app-container { 47 | padding: 20px; 48 | } 49 | -------------------------------------------------------------------------------- /src/views/guide/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | interface ISettings { 2 | title: string; // Overrides the default title 3 | showSettings: boolean; // Controls settings panel display 4 | // showTagsView: boolean // Controls tagsview display 5 | // showSidebarLogo: boolean // Controls siderbar logo display 6 | // fixedHeader: boolean // If true, will fix the header component 7 | // errorLog: string[] // The env to enable the errorlog component, default 'production' only 8 | sidebarTextTheme: boolean; // If true, will change active text color for sidebar based on theme 9 | // devServerPort: number // Port number for webpack-dev-server 10 | // mockServerPort: number // Port number for mock server 11 | } 12 | 13 | // You can customize below settings :) 14 | const settings: ISettings = { 15 | title: "ts-vue", 16 | showSettings: true, 17 | // showTagsView: true, 18 | // fixedHeader: false, 19 | // showSidebarLogo: false, 20 | // errorLog: ['production'], 21 | sidebarTextTheme: true 22 | // devServerPort: 9527, 23 | // mockServerPort: 9528 24 | }; 25 | 26 | export default settings; 27 | -------------------------------------------------------------------------------- /src/icons/svg/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/components/eye-on.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'eye-on': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 1024 1024', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VuexModule, 3 | Mutation, 4 | Action, 5 | getModule, 6 | Module 7 | } from "vuex-module-decorators"; 8 | import store from "@/store"; 9 | import elementVariables from "@/styles/element-variables.scss"; 10 | import defaultSettings from "@/settings"; 11 | 12 | export interface ISettingsState { 13 | theme: string; 14 | showSettings: boolean; 15 | sidebarTextTheme: boolean; 16 | } 17 | 18 | @Module({ dynamic: true, store, name: "settings" }) 19 | class Settings extends VuexModule implements ISettingsState { 20 | public theme = elementVariables.theme; 21 | public showSettings = defaultSettings.showSettings; 22 | public sidebarTextTheme = defaultSettings.sidebarTextTheme; 23 | 24 | @Mutation 25 | private CHANGE_SETTING(payload: { key: string; value: any }) { 26 | const { key, value } = payload; 27 | if (Object.prototype.hasOwnProperty.call(this, key)) { 28 | (this as any)[key] = value; 29 | } 30 | } 31 | 32 | @Action 33 | public ChangeSetting(payload: { key: string; value: any }) { 34 | this.CHANGE_SETTING(payload); 35 | } 36 | } 37 | 38 | export const SettingsModule = getModule(Settings); 39 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 52 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/components/exit-fullscreen.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'exit-fullscreen': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | 39 | 50 | -------------------------------------------------------------------------------- /src/icons/components/tree.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'tree': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // 编译选项 4 | "target": "esnext", // 编译输出目标ES版本 5 | "module": "esnext", // 采用的模块系统 6 | "strict": true, // 以严格模式解析 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "moduleResolution": "node", // 如何处理模块 10 | "experimentalDecorators": true, // 启用装饰器 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入 13 | "strictPropertyInitialization": false, // 定义一个变量就必须给它一个初始值 14 | "allowJs": true, // 允许编辑javascript文件 15 | "sourceMap": true, // 是否包含可以用于 debug 的 sourceMap 16 | "noImplicitThis": false, // 忽略 this 的类型检查, Raise error on this expressions with an implied any type. 17 | "baseUrl": ".", // 解析非相对模块名的基准目录 18 | "pretty": true, // 给错误和消息设置样式,使用颜色和上下文 19 | "types": [ 20 | // 设置引入的定义文件 21 | "webpack-env", 22 | "mocha", 23 | "chai", 24 | "node" 25 | ], 26 | "paths": { 27 | "@/*": ["src/*"] 28 | }, 29 | "lib": [ 30 | // 编译过程中需要引入的库文件的列表 31 | "esnext", 32 | "dom", 33 | "dom.iterable", 34 | "scripthost" 35 | ] 36 | }, 37 | "include": [ 38 | // ts 管理的文件 39 | "src/**/*.ts", 40 | "src/**/*.tsx", 41 | "src/**/*.vue", 42 | "tests/**/*.ts", 43 | "tests/**/*.tsx" 44 | ], 45 | "exclude": ["node_modules"] // ts 排除的文件 46 | } 47 | -------------------------------------------------------------------------------- /src/icons/components/dashboard.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'dashboard': { 7 | width: 128, 8 | height: 100, 9 | viewBox: '0 0 128 100', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/views/guide/steps.ts: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: "#hamburger-container", 4 | popover: { 5 | title: "Hamburger", 6 | description: "Open && Close sidebar", 7 | position: "bottom" 8 | } 9 | }, 10 | { 11 | element: "#breadcrumb-container", 12 | popover: { 13 | title: "Breadcrumb", 14 | description: "Indicate the current page location", 15 | position: "bottom" 16 | } 17 | }, 18 | // { 19 | // element: "#header-search", 20 | // popover: { 21 | // title: "Page Search", 22 | // description: "Page search, quick navigation", 23 | // position: "left" 24 | // } 25 | // }, 26 | // { 27 | // element: "#screenfull", 28 | // popover: { 29 | // title: "Screenfull", 30 | // description: "Set the page into fullscreen", 31 | // position: "left" 32 | // } 33 | // }, 34 | // { 35 | // element: "#size-select", 36 | // popover: { 37 | // title: "Switch Size", 38 | // description: "Switch the system size", 39 | // position: "left" 40 | // } 41 | // }, 42 | { 43 | element: "#tags-view-container", 44 | popover: { 45 | title: "Tags view", 46 | description: "The history of the page you visited", 47 | position: "bottom" 48 | }, 49 | padding: 0 50 | } 51 | ]; 52 | 53 | export default steps; 54 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layout/mixin/resize.ts: -------------------------------------------------------------------------------- 1 | import { Component, Vue, Watch } from "vue-property-decorator"; 2 | import { AppModule, DeviceType } from "@/store/modules/app"; 3 | 4 | const WIDTH = 992; // refer to Bootstrap's responsive design 5 | 6 | @Component({ 7 | name: "ResizeMixin" 8 | }) 9 | export default class extends Vue { 10 | get device() { 11 | return AppModule.device; 12 | } 13 | 14 | get sidebar() { 15 | return AppModule.sidebar; 16 | } 17 | 18 | @Watch("$route") 19 | private onRouteChange() { 20 | if (this.device === DeviceType.Mobile && this.sidebar.opened) { 21 | AppModule.CloseSideBar(false); 22 | } 23 | } 24 | 25 | beforeMount() { 26 | window.addEventListener("resize", this.resizeHandler); 27 | } 28 | 29 | mounted() { 30 | const isMobile = this.isMobile(); 31 | if (isMobile) { 32 | AppModule.ToggleDevice(DeviceType.Mobile); 33 | AppModule.CloseSideBar(true); 34 | } 35 | } 36 | 37 | beforeDestroy() { 38 | window.removeEventListener("resize", this.resizeHandler); 39 | } 40 | 41 | private isMobile() { 42 | const rect = document.body.getBoundingClientRect(); 43 | return rect.width - 1 < WIDTH; 44 | } 45 | 46 | private resizeHandler() { 47 | if (!document.hidden) { 48 | const isMobile = this.isMobile(); 49 | AppModule.ToggleDevice(isMobile ? DeviceType.Mobile : DeviceType.Desktop); 50 | if (isMobile) { 51 | AppModule.CloseSideBar(true); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/icons/components/form.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | // @ts-ignore 4 | import icon from 'vue-svgicon' 5 | icon.register({ 6 | 'form': { 7 | width: 128, 8 | height: 128, 9 | viewBox: '0 0 128 128', 10 | data: '' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 70 | -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.ts: -------------------------------------------------------------------------------- 1 | import { ECharts } from "echarts"; 2 | import { Component, Vue } from "vue-property-decorator"; 3 | 4 | @Component({ 5 | name: "ResizeMixin" 6 | }) 7 | export default class extends Vue { 8 | protected chart!: ECharts | null; 9 | private sidebarElm?: Element; 10 | 11 | mounted() { 12 | this.initResizeEvent(); 13 | this.initSidebarResizeEvent(); 14 | } 15 | 16 | beforeDestroy() { 17 | this.destroyResizeEvent(); 18 | this.destroySidebarResizeEvent(); 19 | } 20 | 21 | activated() { 22 | this.initResizeEvent(); 23 | this.initSidebarResizeEvent(); 24 | } 25 | 26 | deactivated() { 27 | this.destroyResizeEvent(); 28 | this.destroySidebarResizeEvent(); 29 | } 30 | 31 | private chartResizeHandler() { 32 | if (this.chart) { 33 | this.chart.resize(); 34 | } 35 | } 36 | 37 | private sidebarResizeHandler(e: TransitionEvent) { 38 | if (e.propertyName === "width") { 39 | this.chartResizeHandler(); 40 | } 41 | } 42 | 43 | private initResizeEvent() { 44 | if (this.chartResizeHandler) { 45 | window.addEventListener("resize", this.chartResizeHandler); 46 | } 47 | } 48 | 49 | private destroyResizeEvent() { 50 | if (this.chartResizeHandler) { 51 | window.removeEventListener("resize", this.chartResizeHandler); 52 | } 53 | } 54 | 55 | private initSidebarResizeEvent() { 56 | this.sidebarElm = document.getElementsByClassName("sidebar-container")[0]; 57 | if (this.sidebarElm) { 58 | this.sidebarElm.addEventListener("transitionend", this 59 | .sidebarResizeHandler as EventListener); 60 | } 61 | } 62 | 63 | private destroySidebarResizeEvent() { 64 | if (this.sidebarElm) { 65 | this.sidebarElm.removeEventListener("transitionend", this 66 | .sidebarResizeHandler as EventListener); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VuexModule, 3 | Module, 4 | Mutation, 5 | Action, 6 | getModule 7 | } from "vuex-module-decorators"; 8 | import { getSidebarStatus, setSidebarStatus } from "@/utils/cookies"; 9 | import store from "@/store"; 10 | 11 | export enum DeviceType { // 定义设备枚举类型 12 | Mobile, 13 | Desktop 14 | } 15 | 16 | export interface IAppState { 17 | // 定义state接口类型 18 | device: DeviceType; 19 | sidebar: { 20 | opened: boolean; 21 | withoutAnimation: boolean; 22 | }; 23 | } 24 | 25 | @Module({ dynamic: true, store, name: "app" }) 26 | class App extends VuexModule implements IAppState { 27 | public sidebar = { 28 | opened: getSidebarStatus() !== "closed", 29 | withoutAnimation: false 30 | }; 31 | public device = DeviceType.Desktop; 32 | 33 | @Mutation 34 | private TOGGLE_SIDEBAR(withoutAnimation: boolean) { 35 | // 切换siderBar状态 36 | this.sidebar.opened = !this.sidebar.opened; 37 | this.sidebar.withoutAnimation = withoutAnimation; 38 | if (this.sidebar.opened) { 39 | setSidebarStatus("opened"); 40 | } else { 41 | setSidebarStatus("closed"); 42 | } 43 | } 44 | 45 | @Mutation 46 | private CLOSE_SIDEBAR(withoutAnimation: boolean) { 47 | this.sidebar.opened = false; 48 | this.sidebar.withoutAnimation = withoutAnimation; 49 | setSidebarStatus("closed"); 50 | } 51 | 52 | @Mutation 53 | private TOGGLE_DEVICE(device: DeviceType) { 54 | this.device = device; 55 | } 56 | 57 | @Action 58 | public ToggleSideBar(withoutAnimation: boolean) { 59 | this.TOGGLE_SIDEBAR(withoutAnimation); 60 | } 61 | 62 | @Action 63 | public CloseSideBar(withoutAnimation: boolean) { 64 | this.CLOSE_SIDEBAR(withoutAnimation); 65 | } 66 | 67 | @Action 68 | public ToggleDevice(device: DeviceType) { 69 | this.TOGGLE_DEVICE(device); 70 | } 71 | } 72 | 73 | export const AppModule = getModule(App); 74 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const parseTime = ( 2 | time?: object | string | number, 3 | cFormat?: string 4 | ): string | null => { 5 | if (time === undefined) { 6 | return null; 7 | } 8 | const format = cFormat || "{y}-{m}-{d} {h}:{i}:{s}"; 9 | let date: Date; 10 | if (typeof time === "object") { 11 | date = time as Date; 12 | } else { 13 | if (typeof time === "string" && /^[0-9]+$/.test(time)) { 14 | time = parseInt(time); 15 | } 16 | if (typeof time === "number" && time.toString().length === 10) { 17 | time = time * 1000; 18 | } 19 | date = new Date(time); 20 | } 21 | const formatObj: { [key: string]: number } = { 22 | y: date.getFullYear(), 23 | m: date.getMonth() + 1, 24 | d: date.getDate(), 25 | h: date.getHours(), 26 | i: date.getMinutes(), 27 | s: date.getSeconds(), 28 | a: date.getDay() 29 | }; 30 | const timeStr = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 31 | let value = formatObj[key]; 32 | // Note: getDay() returns 0 on Sunday 33 | if (key === "a") { 34 | return ["日", "一", "二", "三", "四", "五", "六"][value]; 35 | } 36 | if (result.length > 0 && value < 10) { 37 | return "0" + value; 38 | } 39 | return String(value) || "0"; 40 | }); 41 | return timeStr; 42 | }; 43 | 44 | // Check if an element has a class 45 | export const hasClass = (ele: HTMLElement, className: string) => { 46 | return !!ele.className.match(new RegExp("(\\s|^)" + className + "(\\s|$)")); 47 | }; 48 | 49 | // Add class to element 50 | export const addClass = (ele: HTMLElement, className: string) => { 51 | if (!hasClass(ele, className)) ele.className += " " + className; 52 | }; 53 | 54 | // Remove class from element 55 | export const removeClass = (ele: HTMLElement, className: string) => { 56 | if (hasClass(ele, className)) { 57 | const reg = new RegExp("(\\s|^)" + className + "(\\s|$)"); 58 | ele.className = ele.className.replace(reg, " "); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/utils/scroll-to.ts: -------------------------------------------------------------------------------- 1 | const easeInOutQuad = (t: number, b: number, c: number, d: number) => { 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 | const requestAnimFrame = (function() { 12 | return ( 13 | window.requestAnimationFrame || 14 | window.webkitRequestAnimationFrame || 15 | (window as any).mozRequestAnimationFrame || 16 | function(callback) { 17 | window.setTimeout(callback, 1000 / 60); 18 | } 19 | ); 20 | })(); 21 | 22 | // Because it's so fucking difficult to detect the scrolling element, just move them all 23 | const move = (amount: number) => { 24 | document.documentElement.scrollTop = amount; 25 | (document.body.parentNode as HTMLElement).scrollTop = amount; 26 | document.body.scrollTop = amount; 27 | }; 28 | 29 | const position = () => { 30 | return ( 31 | document.documentElement.scrollTop || 32 | (document.body.parentNode as HTMLElement).scrollTop || 33 | document.body.scrollTop 34 | ); 35 | }; 36 | 37 | export const scrollTo = (to: number, duration: number, callback?: Function) => { 38 | const start = position(); 39 | const change = to - start; 40 | const increment = 20; 41 | let currentTime = 0; 42 | duration = typeof duration === "undefined" ? 500 : duration; 43 | const animateScroll = function() { 44 | // increment the time 45 | currentTime += increment; 46 | // find the value with the quadratic in-out easing function 47 | const val = easeInOutQuad(currentTime, start, change, duration); 48 | // move the document.body 49 | move(val); 50 | // do the animation unless its over 51 | if (currentTime < duration) { 52 | requestAnimFrame(animateScroll); 53 | } else { 54 | if (callback && typeof callback === "function") { 55 | // the animation is done so lets callback 56 | callback(); 57 | } 58 | } 59 | }; 60 | animateScroll(); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 65 | 66 | 76 | -------------------------------------------------------------------------------- /src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 97 | -------------------------------------------------------------------------------- /src/views/dashboard/editor/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 59 | 60 | 95 | -------------------------------------------------------------------------------- /src/peimission.ts: -------------------------------------------------------------------------------- 1 | import router from "./router"; 2 | import NProgress from "nprogress"; 3 | import "nprogress/nprogress.css"; 4 | import { Message } from "element-ui"; 5 | import { Route, RouteConfig } from "vue-router"; 6 | import { UserModule } from "@/store/modules/user"; 7 | import { PermissionModule } from "@/store/modules/permission"; 8 | 9 | NProgress.configure({ showSpinner: false }); 10 | 11 | const whiteList = ["/login"]; 12 | 13 | router.beforeEach(async (to: Route, _: Route, next: any) => { 14 | // Start progress bar 15 | NProgress.start(); 16 | 17 | // Determine whether the user has logged in 18 | if (UserModule.token) { 19 | if (to.path === "/login") { 20 | // If is logged in, redirect to the home page 21 | next({ path: "/" }); 22 | NProgress.done(); 23 | } else { 24 | // Note: roles must be a object array! such as: ['admin'] or ['developer', 'editor'] 25 | if (UserModule.roles.length === 0) { 26 | try { 27 | // Get user info, including roles 28 | await UserModule.GetUserInfo(); 29 | const roles = UserModule.roles; 30 | // Generate accessible routes map based on role 31 | console.log("角色", roles); 32 | 33 | const accessedRoutes: any = await PermissionModule.GenerateRoutes( 34 | roles 35 | ); 36 | // Dynamically add accessible routes 37 | router.addRoutes(accessedRoutes); 38 | // Set the replace: true, so the navigation will not leave a history record 39 | 40 | next({ ...to, replace: true }); 41 | } catch (err) { 42 | // Remove token and redirect to login page 43 | UserModule.ResetToken(); 44 | Message.error(err || "Has Error"); 45 | next(`/login?redirect=${to.path}`); 46 | NProgress.done(); 47 | } 48 | } else { 49 | next(); 50 | } 51 | } 52 | } else { 53 | // Has no token 54 | if (whiteList.indexOf(to.path) !== -1) { 55 | // In the free login whitelist, go directly 56 | next(); 57 | } else { 58 | // Other pages that do not have permission to access are redirected to the login page. 59 | next(`/login?redirect=${to.path}`); 60 | NProgress.done(); 61 | } 62 | } 63 | }); 64 | 65 | router.afterEach((to: Route) => { 66 | // Finish progress bar 67 | NProgress.done(); 68 | 69 | // set page title 70 | document.title = to.meta.title; 71 | }); 72 | -------------------------------------------------------------------------------- /src/store/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VuexModule, 3 | Module, 4 | Mutation, 5 | Action, 6 | getModule 7 | } from "vuex-module-decorators"; 8 | import { RouteConfig } from "vue-router"; 9 | import { asyncRoutes, constantRoutes } from "@/router"; 10 | import store from "@/store"; 11 | 12 | const hasPermission = (roles: string[], route: RouteConfig) => { 13 | if (route.meta && route.meta.roles) { 14 | return roles.some(role => route.meta.roles.includes(role)); 15 | } else { 16 | return true; 17 | } 18 | }; 19 | 20 | const deepCopy = (source: any): any => { 21 | if (!source) { 22 | return source; 23 | } 24 | let sourceCopy: any = source instanceof Array ? [] : {}; 25 | for (let item in source) { 26 | sourceCopy[item] = 27 | typeof source[item] === "object" ? deepCopy(source[item]) : source[item]; 28 | } 29 | return sourceCopy; 30 | }; 31 | 32 | export const filterAsyncRoutes = (routes: RouteConfig[], roles: string[]) => { 33 | const res: RouteConfig[] = []; 34 | routes.forEach(route => { 35 | const tmp = { ...route }; 36 | if (hasPermission(roles, tmp)) { 37 | if (tmp.children) { 38 | tmp.children = filterAsyncRoutes(tmp.children, roles); 39 | } 40 | res.push(tmp); 41 | } 42 | }); 43 | return res; 44 | }; 45 | 46 | export interface IPermissionState { 47 | routes: RouteConfig[]; 48 | dynamicRoutes: RouteConfig[]; 49 | } 50 | 51 | @Module({ dynamic: true, store, name: "permission" }) 52 | class Permission extends VuexModule implements IPermissionState { 53 | public routes: RouteConfig[] = []; 54 | public dynamicRoutes: RouteConfig[] = []; 55 | 56 | @Mutation 57 | private SET_ROUTES(routes: RouteConfig[]) { 58 | this.routes = constantRoutes.concat(routes); 59 | console.log("完整路由", this.routes); 60 | this.dynamicRoutes = routes; 61 | } 62 | 63 | @Action 64 | public GenerateRoutes(roles: string[]) { 65 | // let accessedRoutes; 66 | // if (roles.includes("admin")) { 67 | // accessedRoutes = asyncRoutes; 68 | // } else { 69 | // accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); 70 | // } 71 | // this.SET_ROUTES(accessedRoutes); 72 | 73 | return new Promise(resolve => { 74 | let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles); 75 | this.SET_ROUTES(accessedRoutes); 76 | resolve(accessedRoutes); 77 | }); 78 | } 79 | } 80 | 81 | export const PermissionModule = getModule(Permission); 82 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 98 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 71 | 72 | 98 | -------------------------------------------------------------------------------- /src/views/table/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 87 | 88 | 99 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 76 | 77 | 93 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 76 | 77 | 100 | 101 | 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "Cosen95", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint --fix", 10 | "svg": "vsvg -s ./src/icons/svg -t ./src/icons/components --ext ts --es6", 11 | "test:unit": "vue-cli-service test:unit", 12 | "prepare": "husky install" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.19.0", 16 | "body-parser": "^1.19.0", 17 | "compression": "^1.7.4", 18 | "core-js": "^2.6.5", 19 | "cors": "^2.8.5", 20 | "driver.js": "^0.9.7", 21 | "echarts": "^4.3.0", 22 | "element-ui": "^2.12.0", 23 | "express": "^4.17.1", 24 | "js-cookie": "^2.2.1", 25 | "morgan": "^1.9.1", 26 | "normalize.css": "^8.0.1", 27 | "nprogress": "^0.2.0", 28 | "path-to-regexp": "^3.0.0", 29 | "screenfull": "^5.0.0", 30 | "vue": "^2.6.10", 31 | "vue-class-component": "^7.0.2", 32 | "vue-property-decorator": "^8.1.0", 33 | "vue-router": "^3.0.3", 34 | "vue-svgicon": "^3.2.6", 35 | "vuex": "^3.0.1", 36 | "vuex-class": "^0.3.2", 37 | "vuex-module-decorators": "^0.10.1", 38 | "yamljs": "^0.3.0" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "^13.1.0", 42 | "@commitlint/config-conventional": "^13.1.0", 43 | "@types/chai": "^4.1.0", 44 | "@types/compression": "^1.0.1", 45 | "@types/cors": "^2.8.6", 46 | "@types/echarts": "^4.1.14", 47 | "@types/express": "^4.17.1", 48 | "@types/js-cookie": "^2.2.2", 49 | "@types/mocha": "^5.2.4", 50 | "@types/morgan": "^1.7.37", 51 | "@types/nprogress": "^0.2.0", 52 | "@types/webpack-env": "^1.14.0", 53 | "@types/yamljs": "^0.2.30", 54 | "@vue/cli-plugin-babel": "^3.11.0", 55 | "@vue/cli-plugin-eslint": "^3.11.0", 56 | "@vue/cli-plugin-typescript": "^3.11.0", 57 | "@vue/cli-plugin-unit-mocha": "^3.11.0", 58 | "@vue/cli-service": "^3.11.0", 59 | "@vue/eslint-config-prettier": "^5.0.0", 60 | "@vue/eslint-config-typescript": "^4.0.0", 61 | "@vue/test-utils": "1.0.0-beta.29", 62 | "babel-eslint": "^10.0.1", 63 | "babel-plugin-component": "^1.1.1", 64 | "chai": "^4.1.2", 65 | "commitizen": "^4.2.4", 66 | "cz-conventional-changelog": "^3.2.0", 67 | "eslint": "^5.16.0", 68 | "eslint-plugin-prettier": "^3.1.0", 69 | "eslint-plugin-vue": "^5.0.0", 70 | "husky": "^7.0.1", 71 | "less": "^3.0.4", 72 | "less-loader": "^5.0.0", 73 | "lint-staged": "^11.1.2", 74 | "prettier": "^1.18.2", 75 | "sass": "^1.22.12", 76 | "sass-loader": "^8.0.0", 77 | "style-resources-loader": "^1.2.1", 78 | "typescript": "^3.4.3", 79 | "vue-cli-plugin-element": "^1.0.1", 80 | "vue-cli-plugin-style-resources-loader": "^0.1.3", 81 | "vue-template-compiler": "^2.6.10", 82 | "webpack": "^4.41.0" 83 | }, 84 | "config": { 85 | "commitizen": { 86 | "path": "./node_modules/cz-conventional-changelog" 87 | } 88 | }, 89 | "lint-staged": { 90 | "src/**/*.{ts,vue}": [ 91 | "vue-cli-service lint", 92 | "git add" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 56 | 57 | 127 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const name = "TS-VUE"; 3 | const sourceMap = process.env.NODE_ENV === "development"; 4 | 5 | const devServerPort = 9527; 6 | const mockServerPort = 9528; 7 | 8 | module.exports = { 9 | publicPath: "/", // 基本路径 10 | outputDir: "dist", // 输出文件目录 11 | lintOnSave: process.env.NODE_ENV === "development", // eslint-loader 是否在保存的时候检查 12 | chainWebpack: config => { 13 | // Provide the app's title in webpack's name field, so that 14 | // it can be accessed in index.html to inject the correct title. 15 | config.set("name", name); 16 | }, 17 | // configureWebpack: config => { 18 | // if (process.env.NODE_ENV === "production") { 19 | // // 为生产环境修改配置... 20 | // config.mode = "production"; 21 | // } else { 22 | // // 为开发环境修改配置... 23 | // config.mode = "development"; 24 | // } 25 | 26 | // Object.assign(config, { 27 | // // 开发生产共同配置 28 | // resolve: { 29 | // extensions: [".js", ".vue", ".json", ".ts", ".tsx"], 30 | // alias: { 31 | // vue$: "vue/dist/vue.js", 32 | // "@": path.resolve(__dirname, "./src"), 33 | // "@c": path.resolve(__dirname, "./src/components"), 34 | // utils: path.resolve(__dirname, "./src/utils"), 35 | // less: path.resolve(__dirname, "./src/less"), 36 | // views: path.resolve(__dirname, "./src/views"), 37 | // assets: path.resolve(__dirname, "./src/assets"), 38 | // com: path.resolve(__dirname, "./src/components"), 39 | // store: path.resolve(__dirname, "./src/store"), 40 | // mixins: path.resolve(__dirname, "./src/mixins") 41 | // } 42 | // } 43 | // }); 44 | // }, 45 | productionSourceMap: sourceMap, // 生产环境是否生成 sourceMap 文件 46 | css: { 47 | // css相关配置 48 | // 是否使用css分离插件 ExtractTextPlugin 49 | extract: true, 50 | // 开启 CSS source maps? 51 | sourceMap: false, 52 | // css预设器配置项 53 | loaderOptions: {}, 54 | // 启用 CSS modules for all css / pre-processor files. 55 | modules: false 56 | }, 57 | // use thread-loader for babel & TS in production build 58 | // enabled by default if the machine has more than 1 cores 59 | parallel: require("os").cpus().length > 1, 60 | // PWA 插件相关配置 61 | // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa 62 | pwa: {}, 63 | devServer: { 64 | open: true, 65 | compress: true, 66 | host: "localhost", 67 | port: devServerPort, 68 | hot: true, 69 | proxy: 70 | "https://vue-typescript-admin-mock-server.armour.now.sh/mock-api/v1/", 71 | // proxy: { 72 | // // 设置代理 73 | // // proxy all requests starting with /api to jsonplaceholder 74 | // [process.env.VUE_APP_BASE_API]: { 75 | // target: `http://localhost:${mockServerPort}/mock-api/v1`, 76 | // changeOrigin: true, 77 | // pathRewite: { 78 | // ["^" + process.env.VUE_APP_BASE_API]: "" 79 | // } 80 | // } 81 | // }, 82 | before: app => {} // 用于在服务器内部所有中间件执行前定义自定义处理程序,即此选项可在本地模拟服务器数据返回。参考https://github.com/lbwa/set/issues/8 83 | }, 84 | // 第三方插件配置 85 | pluginOptions: { 86 | // style-resources-loader(https://www.npmjs.com/package/vue-cli-plugin-style-resources-loader) 87 | // 导入一些公共的样式文件,比如:variables / mixins / functions,避免在每个样式文件中手动的@import导入 88 | "style-resources-loader": { 89 | preProcessor: "scss", 90 | patterns: [ 91 | path.resolve(__dirname, "src/styles/_variables.scss"), 92 | path.resolve(__dirname, "src/styles/_mixins.scss") 93 | ] 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/RightPanel/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 79 | 80 | 87 | 88 | 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-vue 2 | 3 |

4 | 5 | vue 6 | 7 | 8 | element-ui 9 | 10 | 11 | typescript 12 | 13 |

14 | 15 | ## 总览 16 | 17 | `ts-vue`是一个中后台前端解决方案,它基于 [vue](https://github.com/vuejs/vue), [typescript](https://www.typescriptlang.org/) 和 [element-ui](https://github.com/ElemeFE/element)实现。 18 | 19 | ## 截图 20 | 21 | ### 登陆页 22 | 23 | ![登陆页](./demo/ts-vue-demo-01.png) 24 | 25 | ### 主页 26 | 27 | ![主页](./demo/ts-vue-demo-02.png) 28 | 29 | ## 功能 30 | 31 | ```txt 32 | - 登录 / 注销 33 | 34 | - 权限验证 35 | - 页面权限 36 | - 权限配置 37 | 38 | - 多环境发布 39 | - Dev / Stage / Prod 40 | 41 | - 全局功能 42 | - 动态换肤 43 | - 动态侧边栏(支持多级路由嵌套) 44 | - Svg 图标 45 | - 全屏 46 | - 设置 47 | - Mock 数据 / Mock 服务器 48 | 49 | - 组件 50 | - ECharts 图表 51 | 52 | - 表格 53 | - 复杂表格 54 | 55 | - 控制台 56 | - 引导页 57 | - 错误页面 58 | - 404 59 | ``` 60 | 61 | ## 前序准备 62 | 63 | 你需要在本地安装 [node](http://nodejs.org/) 和 [git](https://git-scm.com/)。本项目技术栈基于 [typescript](https://www.typescriptlang.org/)、[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[vue-cli](https://github.com/vuejs/vue-cli) 、[axios](https://github.com/axios/axios) 和 [element-ui](https://github.com/ElemeFE/element),所有的请求数据都使用[faker.js](https://github.com/Marak/Faker.js)进行模拟,提前了解和学习这些知识会对使用本项目有很大的帮助。 64 | 65 | ## 目录结构 66 | 67 | ``` 68 | |-- ts-vue 69 | |-- .browserslistrc # browserslistrc 配置文件 (用于支持 Autoprefixer) 70 | |-- .env.development # development环境变量配置 71 | |-- .env.production # production环境变量配置 72 | |-- .env.test # test环境变量配置 73 | |-- .eslintrc.js # eslint 配置 74 | |-- .gitignore 75 | |-- babel.config.js # babel-loader 配置 76 | |-- package-lock.json 77 | |-- package.json # package.json 依赖 78 | |-- postcss.config.js # postcss 配置 79 | |-- README.md 80 | |-- tsconfig.json # typescript 配置 81 | |-- vue.config.js # vue-cli 配置 82 | |-- mock # mock 服务器 与 模拟数据 83 | | |-- mock-server.ts 84 | |-- public # 静态资源 (会被直接复制) 85 | | |-- favicon.ico # favicon图标 86 | | |-- index.html # html模板 87 | |-- src 88 | | |-- App.vue # 入口页面 89 | | |-- main.ts # 入口文件 加载组件 初始化等 90 | | |-- peimission.ts # 权限管理 91 | | |-- settings.ts # 设置文件 92 | | |-- shims-tsx.d.ts 93 | | |-- shims-vue.d.ts 94 | | |-- api # 所有请求 95 | | |-- assets # 主题 字体等静态资源 (由 webpack 处理加载) 96 | | |-- components # 全局组件 97 | | |-- filters # 全局过滤函数 98 | | |-- icons # svg 图标 99 | | |-- layout # 全局布局 100 | | |-- router # 路由 101 | | |-- store # 全局 vuex store 102 | | |-- styles # 全局样式 103 | | |-- utils # 全局方法 104 | | |-- views # 所有页面 105 | |-- tests # 测试 106 | ``` 107 | 108 | ## 如何设置以及启动项目 109 | 110 | ### 安装依赖 111 | 112 | ``` 113 | npm install 114 | ``` 115 | 116 | ### 启动本地开发环境(自带热启动) 117 | 118 | ``` 119 | npm run serve 120 | ``` 121 | 122 | ### 构建生产环境(自带压缩) 123 | 124 | ``` 125 | npm run build 126 | ``` 127 | 128 | ### 执行测试 129 | 130 | ``` 131 | npm run test 132 | ``` 133 | 134 | ### lint 检测 135 | 136 | ``` 137 | npm run lint 138 | ``` 139 | 140 | ### 单元测试 141 | 142 | ``` 143 | npm run test:unit 144 | ``` 145 | 146 | ### 自动生成 svg 组件 147 | 148 | ```bash 149 | npm run svg 150 | ``` 151 | 152 | ### 自定义 Vue 配置 153 | 154 | 请看 [Configuration Reference](https://cli.vuejs.org/config/). 155 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VuexModule, 3 | Module, 4 | Action, 5 | Mutation, 6 | getModule 7 | } from "vuex-module-decorators"; 8 | import { login, logout, getUserInfo } from "@/api/users"; 9 | import { getToken, setToken, removeToken } from "@/utils/cookies"; 10 | import store from "@/store"; 11 | 12 | export interface IUserState { 13 | token: string; 14 | name: string; 15 | avatar: string; 16 | introduction: string; 17 | roles: string[]; 18 | } 19 | 20 | // @Module 标记当前为module 21 | // module本身有几种可以配置的属性 22 | // 1、namespaced:boolean 启/停用 分模块 23 | // 2、stateFactory:boolean 状态工厂 24 | // 3、dynamic:boolean 在store创建之后,再添加到store中。 开启dynamic之后必须提供下面的属性 25 | // 4、name:string 指定模块名称 26 | // 5、store:Vuex.Store实体 提供初始的store 27 | @Module({ dynamic: true, store, name: "user" }) 28 | class User extends VuexModule implements IUserState { 29 | // 在需要引用的地方单独引用该store文件即可注入。 30 | // 好处:灵活使用,仅仅在需要引入的地方才注入到store中去 31 | // 缺点:需要单独引入文件 32 | 33 | /* 这里代表的就是state里面的状态 */ 34 | public token = getToken() || ""; 35 | public name = ""; 36 | public avatar = ""; 37 | public introduction = ""; 38 | public roles: string[] = []; 39 | 40 | // @Mutation 标注为mutation 41 | @Mutation 42 | private SET_TOKEN(token: string) { 43 | // 设置token 44 | this.token = token; 45 | } 46 | 47 | @Mutation 48 | private SET_NAME(name: string) { 49 | // 设置用户名 50 | this.name = name; 51 | } 52 | 53 | @Mutation 54 | private SET_AVATAR(avatar: string) { 55 | // 设置头像 56 | this.avatar = avatar; 57 | } 58 | 59 | @Mutation 60 | private SET_INTRODUCTION(introduction: string) { 61 | // 设置介绍 62 | this.introduction = introduction; 63 | } 64 | 65 | @Mutation 66 | private SET_ROLES(roles: string[]) { 67 | // 设置角色 68 | this.roles = roles; 69 | } 70 | 71 | // @Action 标注为action 72 | @Action 73 | public async Login(userInfo: { username: string; password: string }) { 74 | // 登录接口,拿到token 75 | let { username, password } = userInfo; 76 | username = username.trim(); 77 | // fix: 移除接口请求 78 | // 业务调用方可自行添加业务逻辑 79 | // let { data } = await login({ username, password }); 80 | let data = { 81 | accessToken: "abc" 82 | }; 83 | setToken(data.accessToken); 84 | this.SET_TOKEN(data.accessToken); 85 | } 86 | 87 | @Action 88 | public ResetToken() { 89 | // 重置token(清空操作),需重新登录 90 | removeToken(); 91 | this.SET_TOKEN(""); 92 | this.SET_ROLES([]); 93 | } 94 | 95 | @Action 96 | public async GetUserInfo() { 97 | // 获取用户信息 98 | if (this.token === "") { 99 | throw Error("GetUserInfo: token is undefined!"); 100 | } 101 | // fix: 移除接口请求 102 | // let { data } = await getUserInfo({ 103 | // /* Your params here */ 104 | // }); 105 | let data = { 106 | user: { 107 | roles: ["admin", "editor"], 108 | name: "cosen", 109 | avatar: "", 110 | introduction: "" 111 | } 112 | }; 113 | console.log("用户信息", data); 114 | if (!data) { 115 | throw Error("Verification failed, please Login again."); 116 | } 117 | const { roles, name, avatar, introduction } = data.user; 118 | console.log("avatar", avatar); 119 | // roles must be a non-empty array 120 | if (!roles || roles.length <= 0) { 121 | throw Error("GetUserInfo: roles must be a non-null array!"); 122 | } 123 | this.SET_ROLES(roles); 124 | this.SET_NAME(name); 125 | this.SET_AVATAR(avatar); 126 | this.SET_INTRODUCTION(introduction); 127 | } 128 | 129 | @Action 130 | public async LogOut() { 131 | // 注销 132 | if (this.token === "") { 133 | throw Error("LogOut: token is undefined!"); 134 | } 135 | // fix: 移除接口请求 136 | // await logout(); 137 | removeToken(); 138 | this.SET_TOKEN(""); 139 | this.SET_ROLES([]); 140 | } 141 | } 142 | 143 | // getModule 得到一个类型安全的store,module必须提供name属性 144 | export const UserModule = getModule(User); 145 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Message, MessageBox } from "element-ui"; 3 | import { UserModule } from "@/store/modules/user"; 4 | 5 | axios.defaults.headers = { 6 | "Content-Type": "application/json;charset=utf8" 7 | // token: Cookies.get("system_token") || "" 8 | }; 9 | 10 | axios.defaults.baseURL = process.env.VUE_APP_BASE_API 11 | ? process.env.VUE_APP_BASE_API 12 | : ""; 13 | // console.log("process.env.VUE_APP_API", process.env.VUE_APP_API); 14 | 15 | // 请求拦截器 16 | axios.interceptors.request.use( 17 | config => { 18 | // Add X-Access-Token header to every request, you can add other custom headers here 19 | if (UserModule.token) { 20 | config.headers["X-Access-Token"] = UserModule.token; 21 | } 22 | return config; 23 | }, 24 | error => { 25 | Promise.reject(error); 26 | } 27 | ); 28 | 29 | axios.interceptors.response.use( 30 | response => { 31 | // Some example codes here: 32 | // code == 20000: success 33 | // code == 50001: invalid access token 34 | // code == 50002: already login in other place 35 | // code == 50003: access token expired 36 | // code == 50004: invalid user (user not exist) 37 | // code == 50005: username or password is incorrect 38 | // You can change this part for your own usage. 39 | const res = response.data; 40 | if (res.code !== 20000) { 41 | Message({ 42 | message: res.message || "Error", 43 | type: "error", 44 | duration: 5 * 1000 45 | }); 46 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 47 | MessageBox.confirm( 48 | "你已被登出,可以取消继续留在该页面,或者重新登录", 49 | "确定登出", 50 | { 51 | confirmButtonText: "重新登录", 52 | cancelButtonText: "取消", 53 | type: "warning" 54 | } 55 | ).then(() => { 56 | UserModule.ResetToken(); 57 | location.reload(); // To prevent bugs from vue-router 58 | }); 59 | } 60 | return Promise.reject(new Error(res.message || "Error")); 61 | } else { 62 | return response; 63 | } 64 | }, 65 | error => { 66 | Message({ 67 | message: error.message, 68 | type: "error", 69 | duration: 5 * 1000 70 | }); 71 | return Promise.reject(error); 72 | } 73 | ); 74 | 75 | export function formateURLOnlyGet(link: string, json: object) { 76 | let url = link; 77 | var data = Object.entries(json); 78 | 79 | if (data.length) { 80 | url += url.indexOf("?") == -1 ? "?" : ""; 81 | url += Object.entries(data) 82 | .map(item => { 83 | return item[1].join("="); 84 | }) 85 | .join("&"); 86 | } 87 | return url; 88 | } 89 | 90 | /** 91 | * GET请求方法 92 | * @param {String} url 请求地址 93 | * @param {json} json 请求参数 94 | */ 95 | export function getData(url: string, json: object) { 96 | return axios 97 | .get(formateURLOnlyGet(url, json)) 98 | .then(res => res.data) 99 | .catch(error => error.response); 100 | } 101 | 102 | export function postData(url: string, json?: object) { 103 | return axios 104 | .post(url, json) 105 | .then(res => res.data) 106 | .catch(error => error.response); 107 | } 108 | export function deleteData(url: string, json: object) { 109 | return axios({ 110 | url: formateURLOnlyGet(url, json), 111 | method: "delete" 112 | // data: json 113 | }) 114 | .then(res => res.data) 115 | .catch(error => error.response); 116 | } 117 | export function deleteJSON(url: string, json: object) { 118 | return axios({ 119 | url: url, 120 | method: "delete", 121 | data: json 122 | }) 123 | .then(res => res.data) 124 | .catch(error => error.response); 125 | } 126 | export function putData(url: string, json: object) { 127 | return axios({ 128 | url, 129 | method: "put", 130 | data: json 131 | }) 132 | .then(res => res.data) 133 | .catch(error => error.response); 134 | } 135 | 136 | export function downloadFile(url: string, json: object) { 137 | return axios({ 138 | url, 139 | method: "post", 140 | data: json, 141 | withCredentials: true, 142 | responseType: "blob" 143 | }) 144 | .then(res => res) 145 | .catch(error => error.response); 146 | } 147 | -------------------------------------------------------------------------------- /src/layout/components/Navbar/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 81 | 82 | 149 | -------------------------------------------------------------------------------- /src/views/table/components/ArticleDetail.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 144 | 145 | 150 | -------------------------------------------------------------------------------- /src/components/InterstingDemo/tableRowEdit.vue: -------------------------------------------------------------------------------- 1 | 75 | 145 | 146 | 151 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // fn是我们需要包装的事件回调, delay是时间间隔的阈值 2 | export function throttle(fn: Function, delay: number) { 3 | // last为上一次触发回调的时间, timer是定时器 4 | let last = 0, 5 | timer: any = null; 6 | // 将throttle处理结果当作函数返回 7 | return function() { 8 | // 保留调用时的this上下文 9 | let context = this; 10 | // 保留调用时传入的参数 11 | let args = arguments; 12 | // 记录本次触发回调的时间 13 | let now = +new Date(); 14 | 15 | // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值 16 | if (now - last < delay) { 17 | // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器 18 | clearTimeout(timer); 19 | timer = setTimeout(function() { 20 | last = now; 21 | fn.apply(context, args); 22 | }, delay); 23 | } else { 24 | // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应 25 | last = now; 26 | fn.apply(context, args); 27 | } 28 | }; 29 | } 30 | 31 | export function setCookie(cName: string, value: any, expiredays: any) { 32 | if (expiredays > 0 && expiredays !== "100") { 33 | let exdate = new Date(); 34 | exdate.setDate(exdate.getDate() + expiredays); 35 | document.cookie = 36 | cName + 37 | "=" + 38 | escape(value) + 39 | // (expiredays == null ? '' : ';expires=' + exdate.toGMTString()); 40 | (expiredays == null ? "" : ";expires=" + exdate.toUTCString()); 41 | } 42 | if (expiredays === "100") { 43 | let exdate = new Date("2118-01-01 00:00:00"); 44 | document.cookie = 45 | cName + 46 | "=" + 47 | escape(value) + 48 | // (expiredays == null ? '' : ';expires=' + exdate.toGMTString()); 49 | (expiredays == null ? "" : ";expires=" + exdate.toUTCString()); 50 | } 51 | } 52 | export function getCookie(cName: string) { 53 | if (document.cookie.length > 0) { 54 | let cStart = document.cookie.indexOf(cName + "="); 55 | if (cStart !== -1) { 56 | cStart = cStart + cName.length + 1; 57 | let cEnd = document.cookie.indexOf(";", cStart); 58 | if (cEnd === -1) cEnd = document.cookie.length; 59 | return unescape(document.cookie.substring(cStart, cEnd)); 60 | } 61 | } 62 | return ""; 63 | } 64 | 65 | export function delCookie(name: string) { 66 | let exp = new Date(); 67 | exp.setTime(exp.getTime() - 1); 68 | let cval = getCookie(name); 69 | if (cval != null) 70 | // document.cookie = name + '=' + cval + ';expires=' + exp.toGMTString(); 71 | document.cookie = name + "=" + cval + ";expires=" + exp.toUTCString(); 72 | } 73 | 74 | //清除cookie 75 | export function clearCookie(name: string) { 76 | setCookie(name, "", -1); 77 | } 78 | //获取QueryString的数组 79 | export function getQueryString() { 80 | let result = window.location.search.match( 81 | new RegExp("[?&][^?&]+=[^?&]+", "g") 82 | ); 83 | if (result == null) { 84 | return ""; 85 | } 86 | for (let i = 0; i < result.length; i++) { 87 | result[i] = result[i].substring(1); 88 | } 89 | return result; 90 | } 91 | //根据 QueryString 参数名称获取值 92 | export function getQueryStringByName(name: string) { 93 | let result = window.location.search.match( 94 | new RegExp("[?&]" + name + "=([^&]+)", "i") 95 | ); 96 | if (result == null || result.length < 1) { 97 | return ""; 98 | } 99 | return result[1]; 100 | } 101 | //获取页面顶部被卷起来的高度 102 | export function getScrollTop() { 103 | return Math.max( 104 | //chrome 105 | document.body.scrollTop, 106 | //firefox/IE 107 | document.documentElement.scrollTop 108 | ); 109 | } 110 | //获取页面文档的总高度 111 | export function getDocumentHeight() { 112 | //现代浏览器(IE9+和其他浏览器)和IE8的document.body.scrollHeight和document.documentElement.scrollHeight都可以 113 | return Math.max( 114 | document.body.scrollHeight, 115 | document.documentElement.scrollHeight 116 | ); 117 | } 118 | //页面浏览器视口的高度 119 | export function getWindowHeight() { 120 | return document.compatMode === "CSS1Compat" 121 | ? document.documentElement.clientHeight 122 | : document.body.clientHeight; 123 | } 124 | //// 时间 格式化成 2018-12-12 12:12:00 125 | export function timestampToTime(timestamp: any, dayMinSecFlag: boolean) { 126 | const date = new Date(timestamp); 127 | const Y = date.getFullYear() + "-"; 128 | const M = 129 | (date.getMonth() + 1 < 10 130 | ? "0" + (date.getMonth() + 1) 131 | : date.getMonth() + 1) + "-"; 132 | const D = 133 | date.getDate() < 10 ? "0" + date.getDate() + " " : date.getDate() + " "; 134 | const h = 135 | date.getHours() < 10 ? "0" + date.getHours() + ":" : date.getHours() + ":"; 136 | const m = 137 | date.getMinutes() < 10 138 | ? "0" + date.getMinutes() + ":" 139 | : date.getMinutes() + ":"; 140 | const s = 141 | date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); 142 | if (!dayMinSecFlag) { 143 | return Y + M + D; 144 | } 145 | return Y + M + D + h + m + s; 146 | } 147 | 148 | //判断是移动端还是 pc 端 ,true 表示是移动端,false 表示是 pc 端 149 | export function isMobileOrPc() { 150 | if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)) { 151 | return true; 152 | } else { 153 | return false; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 138 | 184 | 185 | 196 | -------------------------------------------------------------------------------- /src/components/ThemePicker/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 171 | 172 | 188 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 57 | 58 | 252 | -------------------------------------------------------------------------------- /src/views/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 57 | 58 | 252 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## 在 vue 中使用 typescript 2 | 3 | ### vue-property-decorator 4 | 5 | ``` 6 | import { Vue, Component, Inject, Provide, Prop, Model, Watch, Emit, Mixins } from 'vue-property-decorator' 7 | ``` 8 | 9 | - 组件声明 10 | 11 | 创建组件的方式变成如下 12 | 13 | ``` 14 | import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; 15 | 16 | @Component 17 | export default class Test extends Vue { 18 | 19 | } 20 | 21 | ``` 22 | 23 | - data 对象 24 | 25 | ``` 26 | import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; 27 | 28 | @Component 29 | export default class Test extends Vue { 30 | private name: string; 31 | } 32 | ``` 33 | 34 | - Prop 声明 35 | 36 | ``` 37 | @Prop({ default: false }) private isCollapse!: boolean; 38 | @Prop({ default: true }) private isFirstLevel!: boolean; 39 | @Prop({ default: "" }) private basePath!: string; 40 | ``` 41 | 42 | - !: 表示一定存在,?: 表示可能不存在。这两种在语法上叫赋值断言 43 | - @Prop(options: (PropOptions | Constructor[] | Constructor) = {}) 44 | - PropOptions,可以使用以下选项:type,default,required,validator 45 | - Constructor[],指定 prop 的可选类型 46 | - Constructor,例如 String,Number,Boolean 等,指定 prop 的类型 47 | 48 | - method 49 | 50 | js 下是需要在 method 对象中声明方法,现变成如下 51 | 52 | ``` 53 | public clickFunc(): void { 54 | console.log(this.name) 55 | console.log(this.msg) 56 | } 57 | ``` 58 | 59 | - Watch 监听属性 60 | 61 | ``` 62 | @Watch("$route", { immediate: true }) 63 | private onRouteChange(route: Route) { 64 | const query = route.query as Dictionary; 65 | if (query) { 66 | this.redirect = query.redirect; 67 | this.otherQuery = this.getOtherQuery(query); 68 | } 69 | } 70 | ``` 71 | 72 | - @Watch(path: string, options: WatchOptions = {}) 73 | - options 包含两个属性 immediate?:boolean 侦听开始之后是否立即调用该回调函数 / deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数 74 | - @Watch('arr', { immediate: true, deep: true }) 75 | - onArrChanged(newValue: number[], oldValue: number[]) {} 76 | 77 | * computed 计算属性 78 | 79 | ``` 80 | public get allname() { 81 | return 'computed ' + this.name; 82 | } 83 | ``` 84 | 85 | allname 是计算后的值,name 是被监听的值 86 | 87 | - 生命周期函数 88 | 89 | ``` 90 | public created(): void { 91 | console.log('created'); 92 | } 93 | 94 | public mounted():void{ 95 | console.log('mounted') 96 | } 97 | 98 | ``` 99 | 100 | - emit 事件 101 | 102 | ``` 103 | import { Vue, Component, Emit } from 'vue-property-decorator' 104 | @Component 105 | export default class MyComponent extends Vue { 106 | count = 0 107 | @Emit() 108 | addToCount(n: number) { 109 | this.count += n 110 | } 111 | @Emit('reset') 112 | resetCount() { 113 | this.count = 0 114 | } 115 | @Emit() 116 | returnValue() { 117 | return 10 118 | } 119 | @Emit() 120 | onInputChange(e) { 121 | return e.target.value 122 | } 123 | @Emit() 124 | promise() { 125 | return new Promise(resolve => { 126 | setTimeout(() => { 127 | resolve(20) 128 | }, 0) 129 | }) 130 | } 131 | } 132 | 133 | ``` 134 | 135 | 使用 js 写法 136 | 137 | ``` 138 | export default { 139 | data() { 140 | return { 141 | count: 0 142 | } 143 | }, 144 | methods: { 145 | addToCount(n) { 146 | this.count += n 147 | this.$emit('add-to-count', n) 148 | }, 149 | resetCount() { 150 | this.count = 0 151 | this.$emit('reset') 152 | }, 153 | returnValue() { 154 | this.$emit('return-value', 10) 155 | }, 156 | onInputChange(e) { 157 | this.$emit('on-input-change', e.target.value, e) 158 | }, 159 | promise() { 160 | const promise = new Promise(resolve => { 161 | setTimeout(() => { 162 | resolve(20) 163 | }, 0) 164 | }) 165 | promise.then(value => { 166 | this.$emit('promise', value) 167 | }) 168 | } 169 | } 170 | } 171 | 172 | ``` 173 | 174 | - @Emit(event?: string) 175 | - @Emit 装饰器接收一个可选参数,该参数是\$Emit 的第一个参数,充当事件名。如果没有提供这个参数,\$Emit 会将回调函数名的 camelCase 转为 kebab-case,并将其作为事件名 176 | - @Emit 会将回调函数的返回值作为第二个参数,如果返回值是一个 Promise 对象,\$emit 会在 Promise 对象被标记为 resolved 之后触发 177 | - @Emit 的回调函数的参数,会放在其返回值之后,一起被\$emit 当做参数使用 178 | 179 | ### vuex-module-decorators 180 | 181 | 在使用 store 装饰器之前,先过一下传统的 store 用法吧 182 | 183 | ``` 184 | export default { 185 | namespaced:true, 186 | state:{ 187 | foo:"" 188 | }, 189 | getters:{ 190 | getFoo(state){ return state.foo} 191 | }, 192 | mutations:{ 193 | setFooSync(state,payload){ 194 | state.foo = payload 195 | } 196 | }, 197 | actions:{ 198 | setFoo({commit},payload){ 199 | commot("getFoo",payload) 200 | } 201 | } 202 | } 203 | 204 | ``` 205 | 206 | 然后开始使用vuex-module-decorators 207 | 208 | ``` 209 | import { 210 | VuexModule, 211 | Mutation, 212 | Action, 213 | getModule, 214 | Module 215 | } from "vuex-module-decorators"; 216 | ``` 217 | 218 | - VuexModule 用于基本属性 219 | 220 | ``` 221 | export default class TestModule extends VuexModule { } 222 | ``` 223 | 224 | VuexModule 提供了一些基本属性,包括 namespaced,state,getters,modules,mutations,actions,context 225 | 226 | - @Module 标记当前为 module 227 | 228 | ``` 229 | @Module({ dynamic: true, store, name: "settings" }) 230 | class Settings extends VuexModule implements ISettingsState { 231 | 232 | } 233 | ``` 234 | 235 | module 本身有几种可以配置的属性: 236 | 237 | - namespaced:boolean 启/停用 分模块 238 | - stateFactory:boolean 状态工厂 239 | - dynamic:boolean 在 store 创建之后,再添加到 store 中。 开启 dynamic 之后必须提供下面的属性 240 | - name:string 指定模块名称 \* store:Vuex.Store 实体 提供初始的 store 241 | 242 | * @Mutation 标注为 mutation 243 | 244 | ``` 245 | @Mutation 246 | private SET_NAME(name: string) { 247 | // 设置用户名 248 | this.name = name; 249 | } 250 | ``` 251 | 252 | * @Action 标注为 action 253 | 254 | ``` 255 | @Action 256 | public async Login(userInfo: { username: string; password: string }) { 257 | // 登录接口,拿到token 258 | let { username, password } = userInfo; 259 | username = username.trim(); 260 | const { data } = await login({ username, password }); 261 | setToken(data.accessToken); 262 | this.SET_TOKEN(data.accessToken); 263 | } 264 | ``` 265 | 266 | * getModule 得到一个类型安全的 store,module 必须提供 name 属性 267 | 268 | ``` 269 | export const UserModule = getModule(User); 270 | ``` 271 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router, { RouteConfig } from "vue-router"; 3 | 4 | /* Layout */ 5 | import Layout from "@/layout/index.vue"; 6 | 7 | Vue.use(Router); 8 | 9 | /* 10 | Note: sub-menu only appear when children.length>=1 11 | Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | */ 13 | 14 | /* 15 | name:'router-name' the name field is required when using , it should also match its component's name property 16 | detail see : https://vuejs.org/v2/guide/components-dynamic-async.html#keep-alive-with-Dynamic-Components 17 | redirect: if set to 'noredirect', no redirect action will be trigger when clicking the breadcrumb 18 | meta: { 19 | roles: ['admin', 'editor'] will control the page roles (allow setting multiple roles) 20 | title: 'title' the name showed in subMenu and breadcrumb (recommend set) 21 | icon: 'svg-name' the icon showed in the sidebar 22 | hidden: true if true, this route will not show in the sidebar (default is false) 23 | alwaysShow: true if true, will always show the root menu (default is false) 24 | if false, hide the root menu when has less or equal than one children route 25 | breadcrumb: false if false, the item will be hidden in breadcrumb (default is true) 26 | noCache: true if true, the page will not be cached (default is false) 27 | affix: true if true, the tag will affix in the tags-view 28 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 29 | } 30 | */ 31 | 32 | /** 33 | ConstantRoutes 34 | a base page that does not have permission requirements 35 | all roles can be accessed 36 | */ 37 | export const constantRoutes: RouteConfig[] = [ 38 | { 39 | path: "/login", 40 | component: () => 41 | import(/* webpackChunkName: "login" */ "@/views/login/index.vue"), 42 | meta: { hidden: true } 43 | }, 44 | { 45 | path: "/404", 46 | component: () => 47 | import(/* webpackChunkName: "404" */ "@/views/error-page/404.vue"), 48 | meta: { hidden: true } 49 | }, 50 | { 51 | path: "/", 52 | component: Layout, 53 | redirect: "/dashboard", 54 | children: [ 55 | { 56 | path: "dashboard", 57 | component: () => 58 | import( 59 | /* webpackChunkName: "dashboard" */ "@/views/dashboard/index.vue" 60 | ), 61 | name: "Dashboard", 62 | meta: { 63 | title: "dashboard", 64 | icon: "dashboard", 65 | affix: true 66 | } 67 | } 68 | ] 69 | }, 70 | { 71 | path: "/guide", 72 | component: Layout, 73 | redirect: "/guide/index", 74 | children: [ 75 | { 76 | path: "index", 77 | component: () => 78 | import(/* webpackChunkName: "guide" */ "@/views/guide/index.vue"), 79 | name: "Guide", 80 | meta: { 81 | title: "guide", 82 | icon: "guide", 83 | noCache: true 84 | } 85 | } 86 | ] 87 | }, 88 | { 89 | path: "/example", 90 | component: Layout, 91 | redirect: "/example/tree", 92 | meta: { 93 | title: "Example", 94 | icon: "example", 95 | alwaysShow: true 96 | }, 97 | children: [ 98 | { 99 | path: "tree", 100 | component: () => 101 | import(/* webpackChunkName: "tree" */ "@/views/tree/index.vue"), 102 | meta: { 103 | title: "Tree", 104 | icon: "tree" 105 | } 106 | } 107 | ] 108 | }, 109 | { 110 | path: "/table", 111 | component: Layout, 112 | redirect: "/table/list", 113 | meta: { 114 | title: "Table", 115 | icon: "table" 116 | }, 117 | children: [ 118 | { 119 | path: "create", 120 | component: () => 121 | import( 122 | /* webpackChunkName: "table-create" */ "@/views/table/create.vue" 123 | ), 124 | name: "CreateArticle", 125 | meta: { 126 | title: "createArticle", 127 | icon: "edit" 128 | } 129 | }, 130 | { 131 | path: "edit/:id(\\d+)", 132 | component: () => 133 | import(/* webpackChunkName: "table-edit" */ "@/views/table/edit.vue"), 134 | name: "EditArticle", 135 | meta: { 136 | title: "editArticle", 137 | noCache: true, 138 | activeMenu: "/table/list", 139 | hidden: true 140 | } 141 | }, 142 | { 143 | path: "list", 144 | component: () => 145 | import( 146 | /* webpackChunkName: "table-list" */ "@/views/table/index.vue" 147 | ), 148 | name: "ArticleList", 149 | meta: { 150 | title: "articleList", 151 | icon: "list" 152 | } 153 | } 154 | ] 155 | } 156 | ]; 157 | 158 | /** 159 | * asyncRoutes 160 | * the routes that need to be dynamically loaded based on user roles 161 | */ 162 | 163 | export const asyncRoutes: RouteConfig[] = [ 164 | { 165 | path: "/permission", 166 | component: Layout, 167 | redirect: "/permission/setting", 168 | name: "Permission", 169 | meta: { 170 | title: "permission", 171 | icon: "lock", 172 | roles: ["admin", "editor"], // you can set roles in root nav 173 | alwaysShow: true // will always show the root menu 174 | }, 175 | children: [ 176 | { 177 | path: "account", 178 | component: () => 179 | import(/* webpackChunkName: "account" */ "@/views/account/index.vue"), 180 | name: "Account", 181 | meta: { 182 | title: "account", 183 | icon: "user", 184 | roles: ["admin"] // or you can only set roles in sub nav 185 | } 186 | }, 187 | { 188 | path: "setting", 189 | component: () => 190 | import(/* webpackChunkName: "setting" */ "@/views/setting/index.vue"), 191 | name: "Setting", 192 | meta: { 193 | title: "setting", 194 | icon: "setting" 195 | } 196 | } 197 | ] 198 | }, 199 | { 200 | path: "/charts", 201 | component: Layout, 202 | redirect: "/charts/index", 203 | meta: { 204 | roles: ["admin"] 205 | }, 206 | children: [ 207 | { 208 | path: "index", 209 | component: () => 210 | import(/* webpackChunkName: "charts" */ "@/views/charts/index.vue"), 211 | name: "charts", 212 | meta: { 213 | title: "charts", 214 | icon: "chart" 215 | } 216 | } 217 | ] 218 | }, 219 | { 220 | path: "*", 221 | redirect: "/404", 222 | meta: { hidden: true } 223 | } 224 | ]; 225 | 226 | const createRouter = () => 227 | new Router({ 228 | // mode: 'history', // Disabled due to Github Pages doesn't support this, enable this if you need. 229 | scrollBehavior: (to, from, savedPosition) => { 230 | if (savedPosition) { 231 | return savedPosition; 232 | } else { 233 | return { x: 0, y: 0 }; 234 | } 235 | }, 236 | base: process.env.BASE_URL, 237 | routes: constantRoutes 238 | }); 239 | 240 | const router = createRouter(); 241 | 242 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 243 | export function resetRouter() { 244 | const newRouter = createRouter(); 245 | (router as any).matcher = (newRouter as any).matcher; // reset router 246 | } 247 | 248 | export default router; 249 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 164 | 165 | 205 | 206 | 286 | --------------------------------------------------------------------------------