├── .browserslistrc ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── prod.config.js ├── public └── index.html ├── src ├── App.vue ├── api │ ├── config.ts │ └── index.ts ├── assets │ └── logo.png ├── components │ ├── HelloWorld.vue │ ├── base-props.ts │ ├── create-component.ts │ ├── drag-wrapper │ │ └── index.vue │ ├── modal │ │ ├── index.ts │ │ ├── slots │ │ │ └── test.vue │ │ └── wrapper.tsx │ └── touch-verify-code │ │ └── index.vue ├── global.d.ts ├── helper │ ├── enum.ts │ ├── index.ts │ ├── permission.ts │ └── utils.ts ├── hooks │ ├── component │ │ ├── useExpose.ts │ │ ├── usePopupState.ts │ │ └── useProps.ts │ ├── useApp.ts │ ├── useCanvasApi.ts │ ├── useClipboard.ts │ ├── useDebounce.ts │ ├── useDebounceFn.ts │ ├── useFormLayout.ts │ ├── useImage.ts │ ├── useMouseEvent.ts │ ├── usePagination.ts │ ├── useQRCode.ts │ ├── useStore.ts │ ├── useThrottle.ts │ ├── useThrottleFn.ts │ └── utils │ │ └── index.ts ├── layout │ ├── components │ │ ├── AppHeader.vue │ │ ├── AppHeaderChannel.vue │ │ ├── AppHeaderUser.vue │ │ ├── AppLinks.vue │ │ ├── AppSubMenu.tsx │ │ └── AppSubMenu.vue │ └── index.vue ├── main.ts ├── plugins │ ├── createGlobal.ts │ ├── index.ts │ ├── injectionKey.ts │ ├── install.ts │ ├── smooth-dnd │ │ ├── container.ts │ │ ├── draggable.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── vue-clipboard.ts │ └── vue-qrcode.ts ├── router │ ├── async.ts │ ├── children │ │ ├── feature │ │ │ └── index.ts │ │ └── index.ts │ ├── index.ts │ ├── modules │ │ ├── constant.ts │ │ └── index.ts │ └── permission.ts ├── scss │ ├── antd.scss │ ├── config.scss │ ├── function.scss │ ├── index.scss │ ├── mixins.scss │ ├── transition.scss │ └── variables.scss ├── shims-vue.d.ts ├── store │ ├── index.ts │ └── modules │ │ └── app │ │ ├── _module.ts │ │ ├── _type.ts │ │ ├── actions.ts │ │ ├── getters.ts │ │ ├── index.ts │ │ ├── mutations.ts │ │ └── state.ts ├── types │ ├── api.d.ts │ ├── hooks.d.ts │ ├── route.d.ts │ └── window.d.ts └── views │ ├── 404.vue │ ├── About.vue │ ├── Home.vue │ ├── Login.vue │ └── feature │ ├── copy.vue │ ├── drag.vue │ ├── functional.vue │ ├── index.vue │ ├── qrcode.vue │ └── verify.vue ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV = development 2 | VUE_APP_BASE_URL = '/' 3 | VUE_APP_PUB_URL = '/' 4 | PROXY_API_URL = 'https://api.domain.com/' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | VUE_APP_BASE_URL = '/' 3 | VUE_APP_PUB_URL = './' 4 | PROXY_API_URL = 'https://api.domain/' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue3 Admin Template With Antd 2 | 3 | # Feature 4 | - 权限管理 - permission 5 | - 常用hooks 6 | - 基于clipboard.js 封装vue3版本的clipboard功能 7 | - 基于qrcode 封装的二维码生成 8 | - 基于smooth-dnd实现的拖拽排序 9 | - 滑块验证码 10 | 11 | # Optimization 12 | - 优化了打包体积 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | plugins: [ 4 | [ 5 | "import", 6 | { libraryName: "ant-design-vue", libraryDirectory: "es", style: true }, 7 | ], 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3_admin_template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "ant-design-vue": "^2.0.0-beta.15", 11 | "axios": "^0.21.0", 12 | "clipboard": "^2.0.6", 13 | "core-js": "^3.6.5", 14 | "qrcode": "^1.4.4", 15 | "smooth-dnd": "^0.12.1", 16 | "vue": "^3.0.0", 17 | "vue-router": "^4.0.0-0", 18 | "vuex": "^4.0.0-0" 19 | }, 20 | "devDependencies": { 21 | "@types/clipboard": "^2.0.1", 22 | "@types/qrcode": "^1.3.5", 23 | "@vue/cli-plugin-babel": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-typescript": "~4.5.0", 26 | "@vue/cli-plugin-vuex": "~4.5.0", 27 | "@vue/cli-service": "~4.5.0", 28 | "@vue/compiler-sfc": "^3.0.0", 29 | "babel-plugin-import": "^1.13.1", 30 | "compression-webpack-plugin": "^6.1.1", 31 | "less": "^3.12.2", 32 | "less-loader": "^7.0.2", 33 | "sass": "^1.26.5", 34 | "sass-loader": "^8.0.2", 35 | "typescript": "~3.9.3", 36 | "uglifyjs-webpack-plugin": "^2.2.0", 37 | "webpack-aliyun-oss-plugin": "^2.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /prod.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 3 | const isProd = process.env.NODE_ENV === "production"; 4 | 5 | // * 避免打包项 6 | const externals = isProd 7 | ? { 8 | vue: "Vue", 9 | vuex: "Vuex", 10 | "vue-router": "VueRouter", 11 | axios: "axios", 12 | clipboard: "Clipboard", 13 | } 14 | : {}; 15 | 16 | // * 资源地址 17 | const assets = isProd 18 | ? "static" + new Date().toLocaleDateString().replace(/\//g, "-") 19 | : "static"; 20 | 21 | const ossCDN = "/"; 22 | 23 | // * oss config 24 | const ossConfig = { 25 | buildPath: "/", 26 | region: "", 27 | ak: "", 28 | sk: "", 29 | bucket: "", 30 | }; 31 | 32 | // * 资源配置 33 | const cdns = { 34 | dev: {}, 35 | build: { 36 | css: [], 37 | js: [ 38 | `${ossCDN}/library/vue.next.min.js`, 39 | `${ossCDN}/library/vuex.next.min.js`, 40 | `${ossCDN}/library/vue-router.next.min.js`, 41 | `${ossCDN}/library/axios.min.js`, 42 | `${ossCDN}/library/clipboard.min.js`, 43 | ], 44 | }, 45 | }; 46 | 47 | // * 公共代码抽离 48 | const optimization = { 49 | splitChunks: { 50 | cacheGroups: { 51 | vendors: { 52 | name: "chunk-vendors", 53 | test: /[\\/]node_modules[\\/]/, 54 | priority: 100, 55 | chunks: "all", 56 | minChunks: 1, 57 | maxInitialRequests: 5, 58 | minSize: 0, 59 | }, 60 | common: { 61 | name: "chunk-common", 62 | test: /[\\/]src[\\/]ts[\\/]/, 63 | minChunks: 2, 64 | maxInitialRequests: 5, 65 | minSize: 0, 66 | priority: 60, 67 | chunks: "all", 68 | reuseExistingChunk: true, 69 | }, 70 | antDesignVue: { 71 | name: "chunk-antdv", 72 | test: /[\\/]node_modules[\\/]ant-design-vue[\\/]/, 73 | chunks: "initial", 74 | priority: 120, 75 | reuseExistingChunk: true, 76 | enforce: true, 77 | }, 78 | styles: { 79 | name: "styles", 80 | test: /\.(sa|sc|c)ss$/, 81 | chunks: "all", 82 | enforce: true, 83 | }, 84 | runtimeChunk: { 85 | name: "manifest", 86 | }, 87 | }, 88 | }, 89 | }; 90 | 91 | // * 打包后资源上传oss 92 | const uploadAssetsToOSS = (config) => { 93 | config 94 | .plugin("webpack-aliyun-oss-plugin") 95 | .use(require("webpack-aliyun-oss-plugin"), [ 96 | { 97 | buildPath: ossConfig.buildPath, 98 | region: ossConfig.region, 99 | ak: ossConfig.ak, 100 | sk: ossConfig.sk, 101 | bucket: ossConfig.bucket, 102 | filter: function(assets) { 103 | return !/\.html$/.test(assets); 104 | }, 105 | }, 106 | ]); 107 | }; 108 | 109 | // * 打包gzip 110 | const assetsGzip = (config) => { 111 | config 112 | .plugin("compression-webpack-plugin") 113 | .use(require("compression-webpack-plugin"), [ 114 | { 115 | filename: "[path].gz[query]", 116 | algorithm: "gzip", 117 | test: /\.js$|\.html$|\.json$|\.css/, 118 | threshold: 10240, // 只有大小大于该值的资源会被处理 10240 119 | minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理 120 | deleteOriginalAssets: true, // 删除原文件 121 | }, 122 | ]); 123 | }; 124 | 125 | // * 代码压缩 126 | const codeUglifyConfig = { 127 | uglifyOptions: { 128 | //生产环境自动删除console 129 | compress: { 130 | drop_debugger: true, 131 | drop_console: false, 132 | pure_funcs: ["console.log"], 133 | }, 134 | }, 135 | sourceMap: false, 136 | parallel: true, 137 | }; 138 | 139 | const plugins = isProd 140 | ? [ 141 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 142 | new webpack.HashedModuleIdsPlugin(), 143 | new UglifyJsPlugin(codeUglifyConfig), 144 | ] 145 | : []; 146 | 147 | module.exports = { 148 | uploadAssetsToOSS, 149 | assetsGzip, 150 | plugins, 151 | externals, 152 | optimization, 153 | cdns, 154 | assets, 155 | }; 156 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/api/config.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; 2 | import { message } from "ant-design-vue"; 3 | import store from "@/store"; 4 | import router from "@/router"; 5 | 6 | import { HTTP } from "@/helper/enum"; 7 | import { IResponse } from "@/types/api"; 8 | 9 | const instance: AxiosInstance = axios.create({ 10 | baseURL: "/api", 11 | }); 12 | 13 | instance.interceptors.request.use( 14 | (config: AxiosRequestConfig): AxiosRequestConfig => { 15 | let user = store.state.app.user; 16 | if (user.token) config.headers["x-token"] = user.token; 17 | return config; 18 | } 19 | ); 20 | 21 | instance.interceptors.response.use( 22 | ( 23 | res: AxiosResponse> 24 | ): Promise>> => { 25 | let result = res.data; 26 | let success = false; 27 | switch (result.code) { 28 | case HTTP.success: 29 | success = true; 30 | break; 31 | case HTTP.loginExpire: 32 | router.replace("/login"); 33 | break; 34 | } 35 | return success 36 | ? Promise.resolve(result) 37 | : (message.error(result.msg), Promise.reject(result)); 38 | }, 39 | (error) => { 40 | if (error?.response?.status === HTTP.serverError) 41 | message.error("服务器开小差了,请稍后再试!"); 42 | if (axios.isCancel(error)) return Promise.resolve(error.message); 43 | return Promise.reject(error); 44 | } 45 | ); 46 | 47 | export default instance; 48 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "./config"; 2 | 3 | // api list 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZhengXiaowei/vue3-admin-template/48c0d77d6a632ef3f486f3ee83d5012d94463770/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 45 | 46 | 47 | 63 | -------------------------------------------------------------------------------- /src/components/base-props.ts: -------------------------------------------------------------------------------- 1 | export const baseProps = { 2 | onClose: Function, 3 | onClosed: Function, 4 | onConfirm: Function, 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/create-component.ts: -------------------------------------------------------------------------------- 1 | import { Component, createApp, createVNode } from "vue"; 2 | 3 | let instance: any = null; 4 | let componentDestroy: any; 5 | 6 | const createComponent = (Component: Component) => { 7 | const app = createApp(Component); 8 | 9 | const root = document.createElement("div"); 10 | 11 | document.body.appendChild(root); 12 | 13 | return { 14 | instance: app.mount(root), 15 | unmount() { 16 | app.unmount(root); 17 | document.body.removeChild(root); 18 | }, 19 | }; 20 | }; 21 | 22 | const create = ( 23 | parentNode: Component, 24 | vnode: Component, 25 | props?: Record 26 | ) => { 27 | return new Promise((resolve) => { 28 | const onClosed = () => { 29 | componentDestroy(); 30 | instance = null; 31 | }; 32 | 33 | if (!instance) { 34 | const vm = createVNode( 35 | parentNode, 36 | { 37 | onClosed, 38 | }, 39 | { 40 | default: (childProps: Record) => { 41 | return createVNode(vnode, { 42 | ...childProps, 43 | onConfirm: (data: Record) => { 44 | resolve(data); 45 | instance.close(); 46 | }, 47 | onClose: () => instance.close(), 48 | onClosed, 49 | }); 50 | }, 51 | } 52 | ); 53 | 54 | ({ instance, unmount: componentDestroy } = createComponent(vm)); 55 | } 56 | 57 | instance.open(props); 58 | }); 59 | }; 60 | 61 | export default create; 62 | -------------------------------------------------------------------------------- /src/components/drag-wrapper/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "vue"; 2 | 3 | import createComponent from "@/components/create-component"; 4 | import defineInstance from "./wrapper"; 5 | 6 | import { IBaseProps } from "@/types/api"; 7 | 8 | import TestComponent from "./slots/test.vue"; 9 | 10 | const createModal = ( 11 | vnode: Component, 12 | props?: T & IBaseProps 13 | ): Promise => { 14 | return createComponent(defineInstance, vnode, props) as Promise; 15 | }; 16 | 17 | export { createModal, TestComponent }; 18 | -------------------------------------------------------------------------------- /src/components/modal/slots/test.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | -------------------------------------------------------------------------------- /src/components/modal/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue"; 2 | import { Modal } from "ant-design-vue"; 3 | 4 | import usePopupState from "@/hooks/component/usePopupState"; 5 | import useProps from "@/hooks/component/useProps"; 6 | 7 | const defineInstance = defineComponent({ 8 | setup(props, { slots }) { 9 | const { state, toggle } = usePopupState(); 10 | const { stateComponent, stateProps } = useProps(state); 11 | 12 | return () => ( 13 | 19 | {slots.default?.(stateProps.value) ?? null} 20 | 21 | ); 22 | }, 23 | }); 24 | 25 | export default defineInstance; 26 | -------------------------------------------------------------------------------- /src/components/touch-verify-code/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 333 | 334 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { MessageApi } from "ant-design-vue/lib/message"; 2 | import { ModalFunc, ModalFuncProps } from "ant-design-vue/lib/modal/Modal"; 3 | import { ComponentCustomProperties } from "vue"; 4 | 5 | declare module "@vue/runtime-core" { 6 | interface ComponentCustomProperties { 7 | $copyText(text: string, container?: HTMLElement): Promise; 8 | $confirm(options: ModalFuncProps): ModalFunc; 9 | $message: MessageApi; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/helper/enum.ts: -------------------------------------------------------------------------------- 1 | export enum HTTP { 2 | success = 0, 3 | loginExpire = 1006, 4 | serverError = 500, 5 | } 6 | -------------------------------------------------------------------------------- /src/helper/index.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | /** 4 | * json字符串格式化 5 | */ 6 | String.prototype.parse = function(this: string): T | null { 7 | let result: T | null = null; 8 | if (this) result = JSON.parse(this); 9 | return result; 10 | }; 11 | 12 | /** 13 | * 事件监听 14 | * @param el 监听dom 15 | * @param cb 回调 16 | * @param type 监听事件 默认click 17 | */ 18 | export const listener = ( 19 | el: HTMLElement | Element, 20 | cb: Function, 21 | type: string = "click" 22 | ) => { 23 | el.addEventListener(type, (e) => cb(e), false); 24 | }; 25 | 26 | /** 27 | * 移除监听事件 28 | * @param el 29 | * @param cb 30 | * @param type 31 | */ 32 | export const removeListener = ( 33 | el: HTMLElement | Element, 34 | cb: Function, 35 | type: string = "click" 36 | ) => { 37 | el.removeEventListener(type, () => cb(), false); 38 | }; 39 | 40 | /** 41 | * 判断url 42 | * @param url 43 | */ 44 | export const validateURL = (url: string) => { 45 | return /^(https?:|mailto:|tel:)/.test(url); 46 | }; 47 | 48 | /** 49 | * url处理 50 | * @param paths 51 | * @param base 52 | */ 53 | export const pathResolve = (paths: string, base: string = "/") => { 54 | if (validateURL(paths)) return paths; 55 | if (validateURL(base)) return base; 56 | return path.resolve(base, paths); 57 | }; 58 | 59 | export const onCreateRandomRange = (start: number, end: number) => { 60 | return Math.round(Math.random() * (end - start) + start); 61 | }; 62 | 63 | export const calculate = (x: number, y: number) => x + y; 64 | 65 | export const square = (x: number) => Math.pow(x, 2); 66 | -------------------------------------------------------------------------------- /src/helper/permission.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | import { RouteConfig } from "@/types/route"; 3 | 4 | /** 5 | * 通过用户角色过滤有效路由 6 | * @param routes 7 | * @param roles 8 | */ 9 | export const filterRoutesByRoles = ( 10 | routes: Array, 11 | roles: Array 12 | ): Array => { 13 | const result: Array = []; 14 | routes.forEach((route) => { 15 | const temp = { ...route }; 16 | if (checkRoutePermission(roles, temp)) { 17 | if (temp.children) { 18 | temp.children = filterRoutesByRoles(temp.children, roles); 19 | temp.redirect = path.resolve(temp.path, temp.children[0].path); 20 | } 21 | result.push(temp); 22 | } 23 | }); 24 | 25 | return result; 26 | }; 27 | 28 | /** 29 | * 判断目标路由是否符合当前用户权限 30 | * @param roles 31 | * @param route 32 | */ 33 | export const checkRoutePermission = ( 34 | roles: Array, 35 | route: RouteConfig 36 | ): boolean => { 37 | if (route.meta?.roles) 38 | return roles.some((role) => route.meta?.roles?.includes(role)); 39 | return true; 40 | }; 41 | 42 | /** 43 | * 格式化菜单路由 44 | * @param routes 45 | */ 46 | export const formatNavRoutes = (routes: RouteConfig[]) => { 47 | const validRoutes = routes.filter((route) => !route.hidden); 48 | const resultRoutes: RouteConfig[] = []; 49 | validRoutes.forEach((route) => { 50 | let tempRoute = { ...route }; 51 | if (tempRoute.children) { 52 | if (tempRoute.children.length === 1) { 53 | // 如果路由长度只有1 则提取出来当根路由 54 | const children = tempRoute.children[0]; 55 | tempRoute = { 56 | ...route, 57 | meta: children.meta, 58 | path: path.resolve(tempRoute.path, children.path), 59 | }; 60 | delete tempRoute.children; 61 | } else tempRoute.children = formatNavRoutes(tempRoute.children); 62 | } 63 | resultRoutes.push(tempRoute); 64 | }); 65 | return resultRoutes; 66 | }; 67 | -------------------------------------------------------------------------------- /src/helper/utils.ts: -------------------------------------------------------------------------------- 1 | const utilString = Object.prototype.toString; 2 | 3 | export const isDate = (date: any): date is Date => { 4 | return utilString.call(date) === "[object Date]"; 5 | }; 6 | 7 | export const isObject = (value: any): value is object => { 8 | return utilString.call(value) === "[object Object]"; 9 | }; 10 | 11 | export const isArray = (array: any[]): array is Array => { 12 | return utilString.call(array) === "[object Array]"; 13 | }; 14 | -------------------------------------------------------------------------------- /src/hooks/component/useExpose.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from "vue"; 2 | 3 | const useExpose = (apis: Record) => { 4 | const instance = getCurrentInstance(); 5 | if (instance) { 6 | Object.assign(instance.proxy, apis); 7 | } 8 | }; 9 | 10 | export default useExpose; 11 | -------------------------------------------------------------------------------- /src/hooks/component/usePopupState.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, reactive } from "vue"; 2 | import useExpose from "./useExpose"; 3 | 4 | const usePopupState = () => { 5 | const state = reactive({ 6 | visible: false, 7 | }); 8 | 9 | const toggle = (show: boolean) => { 10 | state.visible = show; 11 | }; 12 | 13 | const open = (props: Record) => { 14 | Object.assign(state, props); 15 | 16 | nextTick(() => { 17 | toggle(true); 18 | }); 19 | }; 20 | 21 | const close = () => { 22 | toggle(false); 23 | }; 24 | 25 | useExpose({ open, close, toggle }); 26 | 27 | return { 28 | open, 29 | close, 30 | state, 31 | toggle, 32 | }; 33 | }; 34 | 35 | export default usePopupState; 36 | -------------------------------------------------------------------------------- /src/hooks/component/useProps.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from "vue"; 2 | 3 | const useProps = (state: Record) => { 4 | let stateComponent = ref>({}); 5 | let stateProps = ref>({}); 6 | 7 | watch( 8 | () => state, 9 | (value) => { 10 | let stateCopy: Record = { ...value }; 11 | stateComponent.value = stateCopy?.components ?? {}; 12 | delete stateCopy?.components; 13 | delete stateCopy?.show; 14 | stateProps.value = stateCopy; 15 | }, 16 | { deep: true } 17 | ); 18 | 19 | return { stateComponent, stateProps }; 20 | }; 21 | 22 | export default useProps; 23 | -------------------------------------------------------------------------------- /src/hooks/useApp.ts: -------------------------------------------------------------------------------- 1 | import useStore from "./useStore"; 2 | import { useRoute, useRouter } from "vue-router"; 3 | 4 | const useApp = () => { 5 | const store = useStore(); 6 | const route = useRoute(); 7 | const router = useRouter(); 8 | 9 | return { store, route, router }; 10 | }; 11 | 12 | export default useApp; 13 | -------------------------------------------------------------------------------- /src/hooks/useCanvasApi.ts: -------------------------------------------------------------------------------- 1 | const useCanvasApi = (size: number, radius: number) => { 2 | const pi = Math.PI; 3 | 4 | const onDraw = ( 5 | ctx: CanvasRenderingContext2D, 6 | x: number, 7 | y: number, 8 | operator: "clip" | "fill" 9 | ) => { 10 | ctx.beginPath(); 11 | ctx.moveTo(x, y); 12 | ctx.arc(x + size / 2, y - radius + 2, radius, 0.72 * pi, 2.26 * pi); 13 | ctx.lineTo(x + size, y); 14 | ctx.arc(x + size + radius - 2, y + size / 2, radius, 1.21 * pi, 2.78 * pi); 15 | ctx.lineTo(x + size, y + size); 16 | ctx.lineTo(x, y + size); 17 | ctx.arc( 18 | x + radius - 2, 19 | y + size / 2, 20 | radius + 0.4, 21 | 2.76 * pi, 22 | 1.24 * pi, 23 | true 24 | ); 25 | ctx.lineTo(x, y); 26 | ctx.lineWidth = 2; 27 | ctx.fillStyle = "rgba(255, 255, 255, .5)"; 28 | ctx.strokeStyle = "rgba(255, 255, 255, .5)"; 29 | // 制造阴影 30 | if (operator === "clip") { 31 | ctx.shadowOffsetX = 2; 32 | ctx.shadowOffsetY = 2; 33 | ctx.shadowBlur = 2; 34 | ctx.shadowColor = "rgba(255, 255, 255, .6)"; 35 | } 36 | ctx.stroke(); 37 | ctx[operator](); 38 | 39 | ctx.globalCompositeOperation = "destination-over"; 40 | }; 41 | 42 | return { onDraw }; 43 | }; 44 | 45 | export default useCanvasApi; 46 | -------------------------------------------------------------------------------- /src/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { clipboardKey } from "@/plugins/injectionKey"; 2 | import { inject } from "vue"; 3 | 4 | /** 5 | * @description: 使用方式一 - Promise 6 | * this.$copyText(text) 7 | */ 8 | 9 | /** 10 | * @description: 使用方式二 - Directive 11 | * 14 | */ 15 | 16 | const useClipboard = () => { 17 | const { $copyText } = inject(clipboardKey)!; 18 | return $copyText; 19 | }; 20 | 21 | export default useClipboard; 22 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, watch } from "vue"; 2 | import useDebounceFn from "./useDebounceFn"; 3 | 4 | const useDebounce = (value: Ref, ms = 200) => { 5 | if (ms <= 0) return value; 6 | 7 | const debounce = ref(value.value as T) as Ref; 8 | 9 | const updater = useDebounceFn(() => (debounce.value = value.value), ms); 10 | 11 | watch(value, () => updater()); 12 | 13 | return debounce; 14 | }; 15 | 16 | export default useDebounce; 17 | -------------------------------------------------------------------------------- /src/hooks/useDebounceFn.ts: -------------------------------------------------------------------------------- 1 | import { createFilterWrapper, debounceFilter } from "./utils"; 2 | import { FunctionArgs } from "@/types/hooks"; 3 | 4 | const useDebounceFn = (fn: T, ms = 200): T => { 5 | return createFilterWrapper(debounceFilter(ms), fn); 6 | }; 7 | 8 | export default useDebounceFn; 9 | -------------------------------------------------------------------------------- /src/hooks/useFormLayout.ts: -------------------------------------------------------------------------------- 1 | const useFormLayout = (label: number = 6) => { 2 | const total = 24; 3 | const offset = 1; 4 | 5 | const wrapperSpan = label ? total - label - offset * 4 : total - offset * 2; 6 | 7 | return { 8 | labelCol: { span: label }, 9 | wrapperCol: { span: wrapperSpan, offset }, 10 | }; 11 | }; 12 | 13 | export default useFormLayout; 14 | -------------------------------------------------------------------------------- /src/hooks/useImage.ts: -------------------------------------------------------------------------------- 1 | import { onCreateRandomRange } from "@/helper"; 2 | 3 | const useImage = (resource: string[], width: number, height: number) => { 4 | const onImageCreate = ( 5 | load: ((this: GlobalEventHandlers, ev: Event) => any) | null 6 | ) => { 7 | const image = document.createElement("img"); 8 | image.crossOrigin = "Anonymous"; 9 | image.onload = load; 10 | image.onerror = () => (image.src = onCreateRandomImageByNets()); 11 | image.src = onCreateRandomImageByNets(); 12 | return image; 13 | }; 14 | 15 | // 获取随机图片 16 | const onCreateRandomImageByNets = () => { 17 | let source = ""; 18 | const resourceLength = resource.length; 19 | const IMAGE_RESOURCE = `https://picsum.photos/${width}/${height}/?&image=`; 20 | if (resourceLength) 21 | source = resource[onCreateRandomRange(0, resourceLength)]; 22 | else source = IMAGE_RESOURCE + onCreateRandomRange(0, 1024); 23 | return source; 24 | }; 25 | 26 | return { onImageCreate, onCreateRandomImageByNets }; 27 | }; 28 | 29 | export default useImage; 30 | -------------------------------------------------------------------------------- /src/hooks/useMouseEvent.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted } from "vue"; 2 | 3 | interface IMouseEvent { 4 | (event: MouseEvent): void; 5 | } 6 | 7 | const useMouseEvent = (move: IMouseEvent, leave: IMouseEvent) => { 8 | const initEvent = () => { 9 | document.addEventListener("mousemove", move, false); 10 | document.addEventListener("mouseup", leave, false); 11 | }; 12 | 13 | onUnmounted(() => { 14 | document.removeEventListener("mousemove", move, false); 15 | document.removeEventListener("mouseup", leave, false); 16 | }); 17 | 18 | return { initEvent }; 19 | }; 20 | 21 | export default useMouseEvent; 22 | -------------------------------------------------------------------------------- /src/hooks/usePagination.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, reactive } from "vue"; 2 | import { IMeta } from "@/types/api"; 3 | 4 | /** 5 | * 判断是否加载下一页的hooks 6 | * TODO 还需要根据具体情况做调整 7 | */ 8 | const usePagination = () => { 9 | let meta = reactive({ 10 | current_page: 0, 11 | is_end: true, 12 | last_page: 0, 13 | next_page: 0, 14 | next_page_url: "", 15 | per_page: 10, 16 | prev_page_url: "", 17 | total: 0, 18 | }); 19 | 20 | let loading = ref(false); 21 | let current = ref(1); 22 | let tablePageOptions = reactive({ 23 | current: current.value, 24 | pageSize: meta.per_page, 25 | total: meta.total, 26 | }); 27 | 28 | watch( 29 | () => meta.current_page, 30 | (current_page) => { 31 | console.log("change"); 32 | loading.value = current_page < meta.last_page; 33 | current.value = current_page + 1; 34 | } 35 | ); 36 | 37 | watch( 38 | () => meta.total, 39 | (total) => { 40 | if (!total) { 41 | loading.value = false; 42 | meta.last_page = 0; 43 | meta.current_page = 0; 44 | current.value = 1; 45 | } 46 | } 47 | ); 48 | 49 | return { loading, meta, current, tablePageOptions }; 50 | }; 51 | 52 | export default usePagination; 53 | -------------------------------------------------------------------------------- /src/hooks/useQRCode.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, Ref, ref, watch } from "vue"; 2 | import QRCode from "qrcode"; 3 | 4 | type MaybeRef = T | Ref | ComputedRef; 5 | 6 | const useQRCode = ( 7 | text: MaybeRef, 8 | options?: QRCode.QRCodeToDataURLOptions 9 | ) => { 10 | const src = ref(text); 11 | const result = ref(""); 12 | 13 | watch( 14 | src, 15 | async (value) => { 16 | result.value = await QRCode.toDataURL(value, options); 17 | }, 18 | { immediate: true } 19 | ); 20 | 21 | return result; 22 | }; 23 | 24 | export default useQRCode; 25 | -------------------------------------------------------------------------------- /src/hooks/useStore.ts: -------------------------------------------------------------------------------- 1 | import store, { Store } from "@/store"; 2 | 3 | const useStore = (): Store => { 4 | return store as Store; 5 | }; 6 | 7 | export default useStore; 8 | -------------------------------------------------------------------------------- /src/hooks/useThrottle.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, watch } from "vue"; 2 | import useThrottleFn from "./useThrottleFn"; 3 | 4 | const useThrottle = (value: Ref, delay = 200) => { 5 | if (delay <= 0) return value; 6 | 7 | const throttle: Ref = ref(value.value as T) as Ref; 8 | 9 | const updater = useThrottleFn(() => { 10 | throttle.value = value.value; 11 | }, delay); 12 | 13 | watch(value, () => updater()); 14 | 15 | return throttle; 16 | }; 17 | 18 | export default useThrottle; 19 | -------------------------------------------------------------------------------- /src/hooks/useThrottleFn.ts: -------------------------------------------------------------------------------- 1 | import { FunctionArgs } from "@/types/hooks"; 2 | import { createFilterWrapper, throttleFilter } from "./utils"; 3 | 4 | const useThrottleFn = ( 5 | fn: T, 6 | ms = 200, 7 | trailing = true 8 | ): T => { 9 | return createFilterWrapper(throttleFilter(ms, trailing), fn); 10 | }; 11 | 12 | export default useThrottleFn; 13 | -------------------------------------------------------------------------------- /src/hooks/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { EventFilter, FunctionArgs } from "@/types/hooks"; 2 | 3 | export function createFilterWrapper( 4 | filter: EventFilter, 5 | fn: T 6 | ) { 7 | function wrapper(this: any, ...args: any[]) { 8 | filter(() => fn.apply(this, args), { fn, args, thisArgs: this }); 9 | } 10 | 11 | return (wrapper as any) as T; 12 | } 13 | 14 | const bypassFilter: EventFilter = (invoke) => invoke(); 15 | 16 | export const throttleFilter = (ms: number, trailing = true) => { 17 | if (ms <= 0) return bypassFilter; 18 | 19 | let lastExecute = 0; 20 | let timer: ReturnType | undefined; 21 | 22 | const clear = () => { 23 | if (timer) { 24 | clearTimeout(timer); 25 | timer = undefined; 26 | } 27 | }; 28 | 29 | const filter: EventFilter = (invoke) => { 30 | const elapsed = Date.now() - lastExecute; 31 | 32 | clear(); 33 | 34 | if (elapsed > ms) { 35 | lastExecute = Date.now(); 36 | invoke(); 37 | } else if (trailing) { 38 | timer = setTimeout(() => { 39 | clear(); 40 | invoke(); 41 | }, ms); 42 | } 43 | }; 44 | 45 | return filter; 46 | }; 47 | 48 | export const debounceFilter = (ms: number) => { 49 | if (ms <= 0) return bypassFilter; 50 | 51 | let timer: ReturnType | undefined; 52 | 53 | const filter: EventFilter = (invoke) => { 54 | if (timer) clearTimeout(timer); 55 | timer = setTimeout(invoke, ms); 56 | }; 57 | 58 | return filter; 59 | }; 60 | -------------------------------------------------------------------------------- /src/layout/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 50 | -------------------------------------------------------------------------------- /src/layout/components/AppHeaderChannel.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /src/layout/components/AppHeaderUser.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /src/layout/components/AppLinks.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/layout/components/AppSubMenu.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, withModifiers } from "vue"; 2 | import { useRouter } from "vue-router"; 3 | 4 | import { pathResolve } from "@/helper"; 5 | 6 | const AppSubMenu = defineComponent({ 7 | name: "AppSubMenu", 8 | props: { 9 | menus: { 10 | type: Object, 11 | default: () => [], 12 | }, 13 | parentPath: String, 14 | }, 15 | setup(props) { 16 | const router = useRouter(); 17 | 18 | const formatRoutes = (path: string, base: string = "/") => 19 | pathResolve(path, base); 20 | 21 | const onNavigate = (to: string) => router.push(to); 22 | 23 | const renderSelf = () => { 24 | return props.menus.children.map((child: any) => { 25 | if (child.children && child.children.length) { 26 | return ( 27 | 32 | ); 33 | } else { 34 | return ( 35 | 36 |

onNavigate(formatRoutes(child.path, props.parentPath)), 39 | ["stop"] 40 | )} 41 | > 42 | {child.meta.title} 43 |

44 |
45 | ); 46 | } 47 | }); 48 | }; 49 | 50 | return () => ( 51 | 56 | {renderSelf()} 57 | 58 | ); 59 | }, 60 | }); 61 | 62 | export default AppSubMenu; 63 | -------------------------------------------------------------------------------- /src/layout/components/AppSubMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 115 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "moment/locale/zh-cn"; 2 | import "@/scss/index.scss"; 3 | import "@/router/permission"; 4 | 5 | import { createApp } from "vue"; 6 | import App from "./App.vue"; 7 | import router from "./router"; 8 | import store from "./store"; 9 | 10 | import { install, createGlobalData } from "@/plugins"; 11 | 12 | const app = createApp(App); 13 | 14 | // 初始化三方库和插件 15 | install(app); 16 | createGlobalData(app); 17 | 18 | app.use(store).use(router); 19 | 20 | router.isReady().then((_) => app.mount("#app")); 21 | -------------------------------------------------------------------------------- /src/plugins/createGlobal.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import store from "@/store"; 3 | 4 | /** 5 | * 创建一些全局数据 6 | * @param app 7 | */ 8 | const createGlobalData = async (app: App) => { 9 | try { 10 | } catch (error) { 11 | console.log("global data error", error); 12 | } 13 | }; 14 | 15 | export default createGlobalData; 16 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import install from "./install"; 2 | import createGlobalData from "./createGlobal"; 3 | 4 | export { install, createGlobalData }; 5 | -------------------------------------------------------------------------------- /src/plugins/injectionKey.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from "vue"; 2 | 3 | const clipboardKey: InjectionKey = Symbol(); 4 | 5 | export { clipboardKey }; 6 | -------------------------------------------------------------------------------- /src/plugins/install.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import { 3 | Button, 4 | ConfigProvider, 5 | Form, 6 | Input, 7 | Layout, 8 | Menu, 9 | message, 10 | Modal, 11 | Popover, 12 | } from "ant-design-vue"; 13 | 14 | import VueClipboard3 from "./vue-clipboard"; 15 | import VueQrCode from "./vue-qrcode"; 16 | 17 | const install = (app: App) => { 18 | app.config.globalProperties.$confirm = Modal.confirm; 19 | app.config.globalProperties.$message = message; 20 | 21 | return app 22 | .use(VueClipboard3) 23 | .use(VueQrCode) 24 | .use(ConfigProvider) 25 | .use(Layout) 26 | .use(Menu) 27 | .use(Form) 28 | .use(Input) 29 | .use(Button) 30 | .use(Popover) 31 | .use(Modal); 32 | }; 33 | 34 | export default install; 35 | -------------------------------------------------------------------------------- /src/plugins/smooth-dnd/container.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | onMounted, 4 | onUpdated, 5 | SetupContext, 6 | ref, 7 | onUnmounted, 8 | watch, 9 | h, 10 | } from "vue"; 11 | import { smoothDnD, dropHandlers, SmoothDnD } from "smooth-dnd"; 12 | import { getTagProps, validateTagProp } from "./utils"; 13 | 14 | smoothDnD.dropHandler = dropHandlers.reactDropHandler().handler; 15 | smoothDnD.wrapChild = false; 16 | 17 | const emitEventMap = { 18 | "drag-start": "onDragStart", 19 | "drag-end": "onDragEnd", 20 | drop: "onDrop", 21 | "drag-enter": "onDragEnter", 22 | "drag-leave": "onDragLeave", 23 | "drop-ready": "onDropReady", 24 | }; 25 | 26 | const getContainerOptions = ( 27 | props: Record, 28 | context: SetupContext 29 | ) => { 30 | const options = Object.keys(props).reduce( 31 | (result: Record, key: string) => { 32 | const optName = key; 33 | const prop = props[optName]; 34 | if (prop !== undefined) { 35 | if (typeof prop === "function") { 36 | if ((>emitEventMap)[optName]) { 37 | result[(>emitEventMap)[optName]] = ( 38 | params: any 39 | ) => { 40 | context.emit(optName, params); 41 | }; 42 | } else { 43 | result[optName] = (...params: any[]) => { 44 | return prop(...params); 45 | }; 46 | } 47 | } else result[optName] = prop; 48 | } 49 | return result; 50 | }, 51 | {} 52 | ); 53 | 54 | return options; 55 | }; 56 | 57 | const mapOptions = (appProps: Record, context: SetupContext) => { 58 | const props = Object.assign({}, appProps, context.attrs); 59 | return getContainerOptions(props, context); 60 | }; 61 | 62 | const Container = defineComponent({ 63 | props: { 64 | behaviour: String, 65 | groupName: String, 66 | orientation: String, 67 | dragHandleSelector: String, 68 | nonDragAreaSelector: String, 69 | dragBeginDelay: Number, 70 | animationDuration: Number, 71 | autoScrollEnabled: { type: Boolean, default: true }, 72 | lockAxis: String, 73 | dragClass: String, 74 | dropClass: String, 75 | removeOnDropOut: { type: Boolean, default: false }, 76 | "drag-start": Function, 77 | "drag-end": Function, 78 | drop: Function, 79 | getChildPayload: Function, 80 | shouldAnimateDrop: Function, 81 | shouldAcceptDrop: Function, 82 | "drag-enter": Function, 83 | "drag-leave": Function, 84 | tag: { 85 | validator: validateTagProp, 86 | default: "div", 87 | }, 88 | getGhostParent: Function, 89 | "drop-ready": Function, 90 | dropPlaceholder: [Object, Boolean], 91 | }, 92 | setup(props, context) { 93 | const container = ref(null); 94 | 95 | const dndContainer = ref(null); 96 | 97 | onMounted(() => { 98 | dndContainer.value = smoothDnD( 99 | container.value!, 100 | mapOptions(props, context) 101 | ); 102 | }); 103 | 104 | watch( 105 | () => container.value, 106 | () => { 107 | console.log("change dnd container"); 108 | if (dndContainer.value) dndContainer.value.dispose(); 109 | dndContainer.value = smoothDnD( 110 | container.value!, 111 | mapOptions(props, context) 112 | ); 113 | } 114 | ); 115 | 116 | onUpdated(() => dndContainer.value?.setOptions(mapOptions(props, context))); 117 | 118 | onUnmounted(() => { 119 | if (dndContainer.value) dndContainer.value.dispose(); 120 | }); 121 | 122 | const tagProps = getTagProps(props); 123 | 124 | const renderContainer = () => { 125 | return h( 126 | tagProps.value, 127 | Object.assign({}, { ref: container }, tagProps.props), 128 | { 129 | default: () => 130 | context.slots.default ? context.slots.default() : "default text", 131 | } 132 | ); 133 | }; 134 | 135 | return () => h(renderContainer); 136 | }, 137 | }); 138 | 139 | export default Container; 140 | -------------------------------------------------------------------------------- /src/plugins/smooth-dnd/draggable.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from "vue"; 2 | import { constants } from "smooth-dnd"; 3 | import { getTagProps, validateTagProp } from "./utils"; 4 | 5 | const Draggable = defineComponent({ 6 | props: { 7 | tag: { 8 | validator: validateTagProp, 9 | default: "div", 10 | }, 11 | }, 12 | setup(props, ctx) { 13 | const tagProps = getTagProps(props, constants.wrapperClass); 14 | 15 | return () => 16 | h(tagProps.value, Object.assign({}, tagProps.props), { 17 | default: () => 18 | ctx.slots.default ? ctx.slots.default() : "default wrap", 19 | }); 20 | }, 21 | }); 22 | 23 | export default Draggable; 24 | -------------------------------------------------------------------------------- /src/plugins/smooth-dnd/index.ts: -------------------------------------------------------------------------------- 1 | import Container from "./container"; 2 | import Draggable from "./draggable"; 3 | 4 | export * from "smooth-dnd"; 5 | 6 | export { Container, Draggable }; 7 | -------------------------------------------------------------------------------- /src/plugins/smooth-dnd/utils.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from "@/helper/utils"; 2 | 3 | export const getTagProps = ( 4 | props: Record, 5 | tagClasses?: string 6 | ) => { 7 | const tag = props.tag; 8 | if (tag) { 9 | if (typeof tag === "string") { 10 | const result: Record = { value: tag }; 11 | if (tagClasses) { 12 | result.props = { class: tagClasses }; 13 | } 14 | return result; 15 | } else if (typeof tag === "object") { 16 | const result = { value: tag.value || "div", props: tag.props || {} }; 17 | 18 | if (tagClasses) { 19 | if (result.props.class) { 20 | if (isArray(result.props.class)) { 21 | result.props.class.push(tagClasses); 22 | } else { 23 | result.props.class = [tagClasses, result.props.class]; 24 | } 25 | } else { 26 | result.props.class = tagClasses; 27 | } 28 | } 29 | 30 | return result; 31 | } 32 | } 33 | return { value: "div" }; 34 | }; 35 | 36 | export const validateTagProp = (tag: any) => { 37 | if (tag) { 38 | if (typeof tag === "string") return true; 39 | if (typeof tag === "object") { 40 | if ( 41 | typeof tag.value === "string" || 42 | typeof tag.value === "function" || 43 | typeof tag.value === "object" 44 | ) { 45 | return true; 46 | } 47 | } 48 | return false; 49 | } 50 | return true; 51 | }; 52 | -------------------------------------------------------------------------------- /src/plugins/vue-clipboard.ts: -------------------------------------------------------------------------------- 1 | import Clipboard from "clipboard"; 2 | import { App, DirectiveBinding, VNode } from "vue"; 3 | import { clipboardKey } from "./injectionKey"; 4 | 5 | // clipboard config 6 | const VueClipboardConfig = { 7 | autoSetContainer: false, 8 | appendToBody: true, 9 | }; 10 | 11 | // 枚举复制的状态 12 | enum status { 13 | success = "success", 14 | error = "error", 15 | } 16 | 17 | // 方法调用 返回promise 18 | const $copyText = ( 19 | text: string, 20 | container?: HTMLElement 21 | ): Promise => { 22 | return new Promise((resolve, reject) => { 23 | const fakeElement = document.createElement("button"); 24 | const clipboard = new Clipboard(fakeElement, { 25 | text: () => text, 26 | action: () => "copy", 27 | container: typeof container === "object" ? container : document.body, 28 | }); 29 | 30 | clipboard.on(status.success, (e: ClipboardJS.Event) => { 31 | clipboard.destroy(); 32 | resolve(e); 33 | }); 34 | 35 | clipboard.on(status.error, (e: ClipboardJS.Event) => { 36 | clipboard.destroy(); 37 | reject(e); 38 | }); 39 | 40 | if (VueClipboardConfig.appendToBody) document.body.appendChild(fakeElement); 41 | fakeElement.click(); 42 | if (VueClipboardConfig.appendToBody) document.body.removeChild(fakeElement); 43 | }); 44 | }; 45 | 46 | // v-direction bind 47 | const bind = ( 48 | el: ClipboardElement, 49 | binding: DirectiveBinding, 50 | vnode: VNode 51 | ) => { 52 | if (binding.arg === status.success) el._vClipboard_success = binding.value; 53 | else if (binding.arg === status.error) el._vClipboard_error = binding.value; 54 | else { 55 | const clipboard = new Clipboard(el, { 56 | text: () => binding.value, 57 | action: () => (binding.arg === "cut" ? "cut" : "copy"), 58 | container: VueClipboardConfig.autoSetContainer ? el : undefined, 59 | }); 60 | 61 | clipboard.on(status.success, (e) => { 62 | const cb = el._vClipboard_success; 63 | cb && cb(e); 64 | }); 65 | 66 | clipboard.on(status.error, (e) => { 67 | const cb = el._vClipboard_error; 68 | cb && cb(e); 69 | }); 70 | 71 | el._vClipboard = clipboard; 72 | } 73 | }; 74 | 75 | // v-direction update 76 | const update = (el: ClipboardElement, binding: DirectiveBinding) => { 77 | if (binding.arg === status.success) el._vClipboard_success = binding.value; 78 | else if (binding.arg === status.error) el._vClipboard_error = binding.value; 79 | else { 80 | (el._vClipboard).text = () => binding.value; 81 | (el._vClipboard).action = () => 82 | binding.arg === "cut" ? "cut" : "copy"; 83 | } 84 | }; 85 | 86 | // v-direction unbind 87 | const unbind = (el: ClipboardElement, binding: DirectiveBinding) => { 88 | if (binding.arg === status.success) delete el._vClipboard_success; 89 | else if (binding.arg === status.error) delete el._vClipboard_error; 90 | else { 91 | (el._vClipboard).destroy(); 92 | delete el._vClipboard; 93 | } 94 | }; 95 | 96 | const VueClipboard3 = { 97 | install: (app: App) => { 98 | // const copySymbol = Symbol(); 99 | // provide注入 方便组合api调用 100 | app.provide(clipboardKey, { $copyText }); 101 | app.config.globalProperties.$clipboardConfig = VueClipboardConfig; 102 | app.config.globalProperties.$copyText = $copyText; 103 | 104 | app.directive("clipboard", { 105 | beforeMount: bind, 106 | updated: update, 107 | unmounted: unbind, 108 | }); 109 | }, 110 | config: VueClipboardConfig, 111 | }; 112 | 113 | export default VueClipboard3; 114 | -------------------------------------------------------------------------------- /src/plugins/vue-qrcode.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding, VNode, App } from "vue"; 2 | import QRCode from "qrcode"; 3 | 4 | const insertQrCode2Element = (el: HTMLElement | Element, qrCode: string) => { 5 | if (el.nodeName === "IMG") el.setAttribute("src", qrCode); 6 | else { 7 | // 判断原来的node是否存在 存在就删除 8 | const prevChild = el.lastElementChild; 9 | if (prevChild) el.removeChild(prevChild); 10 | const imgElement = document.createElement("img"); 11 | imgElement.src = qrCode; 12 | el.appendChild(imgElement); 13 | } 14 | }; 15 | 16 | const bind = async ( 17 | el: HTMLElement | Element, 18 | binding: DirectiveBinding, 19 | vnode: VNode 20 | ) => { 21 | const qrText = binding.value; 22 | if (!qrText) return; 23 | if (typeof qrText === "string") { 24 | const code = await QRCode.toDataURL(qrText); 25 | insertQrCode2Element(el, code); 26 | } else console.log("v-qrode 需要一个string类型的值"); 27 | }; 28 | 29 | const update = async (el: HTMLElement | Element, binding: DirectiveBinding) => { 30 | if (binding.oldValue === binding.value) return; 31 | const qrText = binding.value; 32 | if (!qrText) return; 33 | if (typeof qrText === "string") { 34 | const code = await QRCode.toDataURL(qrText); 35 | insertQrCode2Element(el, code); 36 | } else console.log("v-qrode 需要一个string类型的值"); 37 | }; 38 | 39 | const VueQrCode = { 40 | install: (app: App) => { 41 | app.directive("qrcode", { 42 | mounted: bind, 43 | updated: update, 44 | }); 45 | }, 46 | }; 47 | 48 | export default VueQrCode; 49 | -------------------------------------------------------------------------------- /src/router/async.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from "@/types/route"; 2 | import { 3 | FeatureCopy, 4 | FeatureDrag, 5 | FeatureQrCode, 6 | FeatureVerify, 7 | Functional, 8 | } from "./children"; 9 | 10 | export const Home: RouteConfig = { 11 | name: "Home", 12 | path: "/home", 13 | meta: { 14 | title: "Home", 15 | }, 16 | component: () => import("@/views/Home.vue"), 17 | }; 18 | 19 | export const About: RouteConfig = { 20 | name: "About", 21 | path: "/about", 22 | meta: { 23 | title: "About", 24 | }, 25 | component: () => import("@/views/About.vue"), 26 | }; 27 | 28 | export const Feature: RouteConfig = { 29 | name: "Feature", 30 | path: "/feature", 31 | meta: { 32 | title: "Feature", 33 | }, 34 | children: [ 35 | FeatureCopy, 36 | FeatureQrCode, 37 | FeatureDrag, 38 | FeatureVerify, 39 | Functional, 40 | ], 41 | component: () => import("@/views/feature/index.vue"), 42 | }; 43 | 44 | const asyncRoutes: RouteConfig[] = [Home, Feature, About]; 45 | 46 | export default asyncRoutes; 47 | -------------------------------------------------------------------------------- /src/router/children/feature/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from "@/types/route"; 2 | 3 | export const FeatureCopy: RouteConfig = { 4 | name: "FeatureCopy", 5 | path: "copy", 6 | meta: { 7 | title: "复制", 8 | }, 9 | component: () => import("@/views/feature/copy.vue"), 10 | }; 11 | 12 | export const FeatureQrCode: RouteConfig = { 13 | name: "FeatureQrCode", 14 | path: "qrcode", 15 | meta: { 16 | title: "二维码生成", 17 | }, 18 | component: () => import("@/views/feature/qrcode.vue"), 19 | }; 20 | 21 | export const FeatureDrag: RouteConfig = { 22 | name: "FeatureDrag", 23 | path: "drag", 24 | meta: { 25 | title: "拖拽排序", 26 | }, 27 | component: () => import("@/views/feature/drag.vue"), 28 | }; 29 | 30 | export const FeatureVerify: RouteConfig = { 31 | name: "FeatureVerify", 32 | path: "verify", 33 | meta: { 34 | title: "滑块验证码", 35 | }, 36 | component: () => import("@/views/feature/verify.vue"), 37 | }; 38 | 39 | export const Functional: RouteConfig = { 40 | name: "Functional", 41 | path: "functional", 42 | meta: { 43 | title: "函数式组件", 44 | }, 45 | component: () => import("@/views/feature/functional.vue"), 46 | }; 47 | -------------------------------------------------------------------------------- /src/router/children/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./feature"; -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import constantRoutes from "./modules/constant"; 3 | 4 | const routes = constantRoutes; 5 | 6 | const router = createRouter({ 7 | history: createWebHistory(process.env.VUE_APP_BASE_URL), 8 | routes, 9 | }); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/router/modules/constant.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from "@/types/route"; 2 | 3 | export const Login: RouteConfig = { 4 | name: "Login", 5 | path: "/login", 6 | hidden: true, 7 | meta: { 8 | title: "登录", 9 | }, 10 | component: () => import("@/views/Login.vue"), 11 | }; 12 | 13 | export const NotFound: RouteConfig = { 14 | name: "NotFound", 15 | path: "/:pathMatch(.*)*", 16 | hidden: true, 17 | component: () => import("@/views/404.vue"), 18 | }; 19 | 20 | const constantRoutes: RouteConfig[] = [Login]; 21 | 22 | export default constantRoutes; 23 | -------------------------------------------------------------------------------- /src/router/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | 3 | // 获取所有路由模块 4 | const routes = require.context(".", false, /\.ts$/); 5 | 6 | let configRoutes: RouteRecordRaw[] = []; 7 | 8 | // 合并所有模块 9 | routes.keys().forEach((key) => { 10 | // 过滤掉主入口文件 11 | if (key === "./index.ts") return; 12 | configRoutes = configRoutes.concat(routes(key).default); 13 | }); 14 | 15 | export default configRoutes; 16 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import { NavigationGuardNext, RouteLocationNormalized } from "vue-router"; 2 | 3 | import router from "./index"; 4 | import store from "@/store"; 5 | 6 | import Layout from "@/layout/index.vue"; 7 | 8 | import { ActionType } from "@/store/modules/app/_type"; 9 | import { NotFound } from "./modules/constant"; 10 | import { RouteConfig } from "@/types/route"; 11 | 12 | const WHITE_URL_LIST: string[] = ["/login"]; 13 | 14 | /** 15 | * 未登录的情况下 路由的处理 16 | * @param to 17 | * @param next 18 | */ 19 | const redirectRoutes = ( 20 | to: RouteLocationNormalized, 21 | next: NavigationGuardNext 22 | ) => { 23 | if (!!~WHITE_URL_LIST.indexOf(to.path)) next(); 24 | else 25 | next({ 26 | path: "/login", 27 | query: { 28 | redirect: encodeURIComponent(to.path), 29 | }, 30 | }); 31 | }; 32 | 33 | router.beforeEach( 34 | async ( 35 | to: RouteLocationNormalized, 36 | _: RouteLocationNormalized, 37 | next: NavigationGuardNext 38 | ) => { 39 | const user = store.state.app.user; 40 | if (user.token) { 41 | if (to.path === "/login") next({ path: "/", replace: true }); 42 | else { 43 | const roles = store.state.app.roles; 44 | if (roles.length) next(); 45 | else { 46 | const userRoles: string[] = await store.dispatch( 47 | ActionType.getUserRoles 48 | ); 49 | const webRoutes: RouteConfig[] = await store.dispatch( 50 | ActionType.generatorRoutes, 51 | userRoles 52 | ); 53 | const AppRoute: RouteConfig = { 54 | name: "home", 55 | path: "", 56 | redirect: webRoutes[0].path, 57 | component: Layout, 58 | children: webRoutes, 59 | }; 60 | console.log(AppRoute); 61 | router.addRoute(AppRoute); 62 | // 添加全局Not Found Page 63 | router.addRoute(NotFound); 64 | next({ ...to, replace: true, name: to.name! }); 65 | } 66 | } 67 | } else redirectRoutes(to, next); 68 | } 69 | ); 70 | 71 | router.afterEach(() => { 72 | // NProgress.done(); 73 | }); 74 | -------------------------------------------------------------------------------- /src/scss/antd.scss: -------------------------------------------------------------------------------- 1 | .ant-table-tbody > tr > td, 2 | .ant-table-thead > tr > th { 3 | border-bottom: none !important; 4 | } 5 | 6 | .ant-form-item { 7 | &:last-child { 8 | margin-bottom: 0; 9 | } 10 | } 11 | 12 | .ant-menu-vertical > .ant-menu-item, 13 | .ant-menu-vertical-left > .ant-menu-item, 14 | .ant-menu-vertical-right > .ant-menu-item, 15 | .ant-menu-inline > .ant-menu-item, 16 | .ant-menu-vertical > .ant-menu-submenu > .ant-menu-submenu-title, 17 | .ant-menu-vertical-left > .ant-menu-submenu > .ant-menu-submenu-title, 18 | .ant-menu-vertical-right > .ant-menu-submenu > .ant-menu-submenu-title, 19 | .ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title { 20 | height: 60px !important; 21 | line-height: 60px !important; 22 | } 23 | 24 | .ant-menu-vertical .ant-menu-item:not(:last-child), 25 | .ant-menu-vertical-left .ant-menu-item:not(:last-child), 26 | .ant-menu-vertical-right .ant-menu-item:not(:last-child), 27 | .ant-menu-inline .ant-menu-item:not(:last-child) { 28 | margin-bottom: 0 !important; 29 | } 30 | 31 | .ant-menu-vertical .ant-menu-item, 32 | .ant-menu-vertical-left .ant-menu-item, 33 | .ant-menu-vertical-right .ant-menu-item, 34 | .ant-menu-inline .ant-menu-item, 35 | .ant-menu-vertical .ant-menu-submenu-title, 36 | .ant-menu-vertical-left .ant-menu-submenu-title, 37 | .ant-menu-vertical-right .ant-menu-submenu-title, 38 | .ant-menu-inline .ant-menu-submenu-title { 39 | margin-top: 0 !important; 40 | margin-bottom: 0 !important; 41 | } 42 | -------------------------------------------------------------------------------- /src/scss/config.scss: -------------------------------------------------------------------------------- 1 | $namespace: "x"; 2 | $element-separator: "__"; 3 | $modifier-separator: "--"; 4 | $state-prefix: "is-"; 5 | -------------------------------------------------------------------------------- /src/scss/function.scss: -------------------------------------------------------------------------------- 1 | @import "./config"; 2 | 3 | /* BEM support Func 4 | -------------------------- */ 5 | @function selectorToString($selector) { 6 | $selector: inspect($selector); 7 | $selector: str-slice($selector, 2, -2); 8 | @return $selector; 9 | } 10 | 11 | @function containsModifier($selector) { 12 | $selector: selectorToString($selector); 13 | 14 | @if str-index($selector, $modifier-separator) { 15 | @return true; 16 | } @else { 17 | @return false; 18 | } 19 | } 20 | 21 | @function containWhenFlag($selector) { 22 | $selector: selectorToString($selector); 23 | 24 | @if str-index($selector, "." + $state-prefix) { 25 | @return true; 26 | } @else { 27 | @return false; 28 | } 29 | } 30 | 31 | @function containPseudoClass($selector) { 32 | $selector: selectorToString($selector); 33 | 34 | @if str-index($selector, ":") { 35 | @return true; 36 | } @else { 37 | @return false; 38 | } 39 | } 40 | 41 | @function hitAllSpecialNestRule($selector) { 42 | @return containsModifier($selector) or containWhenFlag($selector) or 43 | containPseudoClass($selector); 44 | } 45 | -------------------------------------------------------------------------------- /src/scss/index.scss: -------------------------------------------------------------------------------- 1 | // @import "./common.scss"; 2 | @import "./transition.scss"; 3 | @import "./antd.scss"; 4 | // @import "./components/index.scss"; 5 | // @import "./views/index.scss"; 6 | 7 | body { 8 | min-width: 1300px; 9 | overflow-x: auto; 10 | 11 | p { 12 | margin-bottom: 0; 13 | } 14 | } 15 | 16 | .web-wrapper { 17 | height: 100vh; 18 | 19 | .web-menu { 20 | height: 100%; 21 | overflow-y: auto; 22 | } 23 | 24 | .web-header { 25 | position: relative; 26 | 27 | .web-title { 28 | font-size: 32px; 29 | color: #fff; 30 | display: inline-block; 31 | } 32 | 33 | .user-wrap { 34 | position: absolute; 35 | right: 30px; 36 | top: 50%; 37 | transform: translate(0, -50%); 38 | color: #fff; 39 | font-size: $font-size-bigger; 40 | cursor: pointer; 41 | @include flex(); 42 | 43 | .user { 44 | margin: 0 10px; 45 | line-height: 1.8; 46 | } 47 | } 48 | } 49 | 50 | .web-container { 51 | padding: 23px; 52 | } 53 | } 54 | 55 | .clearfix { 56 | &:after { 57 | visibility: hidden; 58 | display: block; 59 | font-size: 0; 60 | content: " "; 61 | clear: both; 62 | height: 0; 63 | } 64 | } 65 | 66 | .full-width { 67 | width: 100%; 68 | } 69 | 70 | .page-wrap { 71 | padding: 23px; 72 | } 73 | 74 | .cursor { 75 | cursor: pointer; 76 | } 77 | 78 | .theme { 79 | color: $theme-color; 80 | } 81 | -------------------------------------------------------------------------------- /src/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "./function.scss"; 2 | 3 | @mixin flex($space: space-between, $direction: row, $align: center) { 4 | display: flex; 5 | align-items: $align; 6 | flex-direction: $direction; 7 | justify-content: $space; 8 | } 9 | 10 | @mixin multi-line($line) { 11 | -webkit-line-clamp: $line; 12 | display: -webkit-box; 13 | -webkit-box-orient: vertical; 14 | overflow: hidden; 15 | } 16 | 17 | @mixin single-line($line-height: 1.5) { 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | line-height: $line-height; 22 | } 23 | 24 | @mixin extend-click() { 25 | position: relative; 26 | 27 | &::before { 28 | content: ""; 29 | position: absolute; 30 | top: -10px; 31 | left: -10px; 32 | right: -10px; 33 | bottom: -10px; 34 | } 35 | } 36 | 37 | /*------- bem ---------*/ 38 | @mixin b($block) { 39 | $B: $namespace + "-" + $block !global; 40 | 41 | .#{$B} { 42 | @content; 43 | } 44 | } 45 | 46 | @mixin e($element) { 47 | $E: $element !global; 48 | $selector: &; 49 | $currentSelector: ""; 50 | @each $unit in $element { 51 | $currentSelector: #{$currentSelector + 52 | "." + 53 | $B + 54 | $element-separator + 55 | $unit + 56 | ","}; 57 | } 58 | 59 | @if hitAllSpecialNestRule($selector) { 60 | @at-root { 61 | #{$selector} { 62 | #{$currentSelector} { 63 | @content; 64 | } 65 | } 66 | } 67 | } @else { 68 | @at-root { 69 | #{$currentSelector} { 70 | @content; 71 | } 72 | } 73 | } 74 | } 75 | 76 | @mixin m($modifier) { 77 | $selector: &; 78 | $currentSelector: ""; 79 | @each $unit in $modifier { 80 | $currentSelector: #{$currentSelector + 81 | & + 82 | $modifier-separator + 83 | $unit + 84 | ","}; 85 | } 86 | 87 | @at-root { 88 | #{$currentSelector} { 89 | @content; 90 | } 91 | } 92 | } 93 | 94 | @mixin meb($modifier: false, $element: $E, $block: $B) { 95 | $selector: &; 96 | $modifierCombo: ""; 97 | 98 | @if $modifier { 99 | $modifierCombo: $modifier-separator + $modifier; 100 | } 101 | 102 | @at-root { 103 | #{$selector} { 104 | .#{$block + $element-separator + $element + $modifierCombo} { 105 | @content; 106 | } 107 | } 108 | } 109 | } 110 | 111 | @mixin when($state) { 112 | @at-root { 113 | &.#{$state-prefix + $state} { 114 | @content; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/scss/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter-from, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all 0.5s; 18 | } 19 | 20 | .fade-transform-enter-from { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all 0.5s; 34 | } 35 | 36 | .breadcrumb-enter-from, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all 0.5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | 50 | .slide-leave-active, 51 | .slide-enter-active { 52 | transition: all 0.5s; 53 | } 54 | 55 | .slide-enter-from { 56 | transform: translateY(100%); 57 | } 58 | 59 | .slide-leave-to { 60 | transform: translateY(0%); 61 | } 62 | 63 | @keyframes rotate { 64 | to { 65 | transform: rotate(360deg); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/scss/variables.scss: -------------------------------------------------------------------------------- 1 | // 导入一些常用mixin 2 | @import "./mixins.scss"; 3 | 4 | // 变量 5 | $font-size-small: 12px; 6 | $font-size-base: 13px; 7 | $font-size-medium: 14px; 8 | $font-size-big: 15px; 9 | $font-size-bigger: 16px; 10 | $font-size-large: 18px; 11 | 12 | $theme-color: #39a4ff; 13 | $theme-color_2: #38b2ff; 14 | 15 | $red: #ff6d6d; 16 | 17 | $navBarWidth: 240px; 18 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | 7 | // 定义clipboard注入的元素 8 | interface ClipboardElement extends HTMLElement { 9 | _vClipboard_success: any; 10 | _vClipboard_error: any; 11 | _vClipboard: ClipboardJS | ClipboardJS.Options | undefined; 12 | } 13 | 14 | // 定义provide提供的方法 15 | interface ClipboardMethod { 16 | $copyText( 17 | string: string, 18 | container?: HTMLElement 19 | ): Promise; 20 | } -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, createLogger } from "vuex"; 2 | 3 | import app, { State as appState, Store as appStore } from "./modules/app"; 4 | 5 | const debug = process.env.NODE_ENV !== "production"; 6 | 7 | export type RootState = { 8 | app: appState; 9 | }; 10 | 11 | export type Store = appStore>; 12 | 13 | const store = createStore({ 14 | modules: { 15 | app, 16 | }, 17 | strict: debug, 18 | plugins: debug ? [createLogger()] : [], 19 | }); 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /src/store/modules/app/_module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionContext, 3 | Store as VuexStore, 4 | CommitOptions, 5 | DispatchOptions, 6 | } from "vuex"; 7 | import { State, Actions, Getters, Mutations } from "./_type"; 8 | import { RootState } from "@/store"; 9 | 10 | export type AugmentedActionContext = { 11 | commit( 12 | key: M, 13 | payload: Parameters[1] 14 | ): ReturnType; 15 | } & Omit, "commit">; 16 | 17 | export type Store = Omit< 18 | VuexStore, 19 | "commit" | "dispatch" | "getters" 20 | > & { 21 | commit[1]>( 22 | key: M, 23 | payload: P, 24 | options?: CommitOptions 25 | ): ReturnType; 26 | } & { 27 | dispatch( 28 | key: A, 29 | payload: Parameters[1], 30 | options?: DispatchOptions 31 | ): ReturnType; 32 | } & { 33 | getters: { 34 | [G in keyof Getters]: ReturnType; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/store/modules/app/_type.ts: -------------------------------------------------------------------------------- 1 | import { AugmentedActionContext } from "./_module"; 2 | import { RouteConfig } from "@/types/route"; 3 | import { RouteRecordRaw } from "vue-router"; 4 | import { IUser } from "@/types/api"; 5 | 6 | type State = { 7 | store: Storage; 8 | pageTitle: string; 9 | roles: string[]; 10 | routes: RouteConfig[]; 11 | addRoutes: RouteConfig[]; 12 | user: Partial; 13 | }; 14 | 15 | // mutation 定义 16 | enum MutationType { 17 | setRoles = "setRoles", 18 | setRoutes = "setRoutes", 19 | setUser = "setUser", 20 | } 21 | 22 | type Mutations = { 23 | [MutationType.setRoles](state: S, roles: string[]): void; 24 | [MutationType.setRoutes](state: S, routes: RouteConfig[]): void; 25 | [MutationType.setUser](state: S, user: Partial): void; 26 | }; 27 | 28 | // action 定义 29 | enum ActionType { 30 | generatorRoutes = "generatorRoutes", 31 | getUserRoles = "getUserRoles", 32 | } 33 | 34 | type Actions = { 35 | [ActionType.generatorRoutes]( 36 | { commit }: AugmentedActionContext, 37 | roles: string[] 38 | ): Promise; 39 | [ActionType.getUserRoles]({ 40 | commit, 41 | }: AugmentedActionContext): Promise; 42 | }; 43 | 44 | type Getters = { 45 | permissionRoutes(state: State): RouteRecordRaw[]; 46 | userRoles(state: State): string[]; 47 | user(state: State): Partial; 48 | pageTitle(state: State): string; 49 | }; 50 | 51 | export { State, Mutations, MutationType, Actions, ActionType, Getters }; 52 | -------------------------------------------------------------------------------- /src/store/modules/app/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from "vuex"; 2 | import { RootState } from "@/store"; 3 | import { Actions, ActionType, MutationType, State } from "./_type"; 4 | import { filterRoutesByRoles } from "@/helper/permission"; 5 | import asyncRoutes from "@/router/async"; 6 | 7 | const actions: ActionTree & Actions = { 8 | [ActionType.generatorRoutes]({ commit }, roles) { 9 | return new Promise(async (resolve) => { 10 | let resultRoutes = filterRoutesByRoles(asyncRoutes, roles) || []; 11 | await commit(MutationType.setRoutes, resultRoutes); 12 | resolve(resultRoutes); 13 | }); 14 | }, 15 | [ActionType.getUserRoles]({ commit }) { 16 | return new Promise(async (resolve) => { 17 | const roles = ["admin"]; 18 | await commit(MutationType.setRoles, roles); 19 | resolve(roles); 20 | }); 21 | }, 22 | }; 23 | 24 | export default actions; 25 | -------------------------------------------------------------------------------- /src/store/modules/app/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from "vuex"; 2 | import { RootState } from "@/store"; 3 | import { Getters, State } from "./_type"; 4 | 5 | const getters: GetterTree & Getters = { 6 | permissionRoutes: (state) => state.routes, 7 | userRoles: (state) => state.roles, 8 | user: (state) => state.user, 9 | pageTitle: (state) => state.pageTitle, 10 | }; 11 | 12 | export default getters; 13 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | import state from "./state"; 3 | import mutations from "./mutations"; 4 | import actions from "./actions"; 5 | import getters from "./getters"; 6 | 7 | import type { State } from "./_type"; 8 | import type { Store } from "./_module"; 9 | 10 | import { RootState } from "@/store"; 11 | 12 | const appModule: Module = { 13 | // namespaced: true, 14 | state, 15 | getters, 16 | mutations, 17 | actions, 18 | }; 19 | 20 | export { State, Store }; 21 | 22 | export default appModule; 23 | -------------------------------------------------------------------------------- /src/store/modules/app/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from "vuex"; 2 | import { State, Mutations, MutationType } from "./_type"; 3 | 4 | const mutations: MutationTree & Mutations = { 5 | [MutationType.setRoles](state, roles) { 6 | state.roles = roles; 7 | }, 8 | [MutationType.setRoutes](state, routes) { 9 | state.routes = routes; 10 | }, 11 | [MutationType.setUser](state, user) { 12 | state.user = user; 13 | state.store.setItem("user", JSON.stringify(user)); 14 | }, 15 | }; 16 | 17 | export default mutations; 18 | -------------------------------------------------------------------------------- /src/store/modules/app/state.ts: -------------------------------------------------------------------------------- 1 | import { State } from "./_type"; 2 | import { IUser } from "@/types/api"; 3 | 4 | /** 5 | * TODO: 待优化 6 | * json字符串格式化 7 | */ 8 | String.prototype.parse = function(this: string): T | null { 9 | let result: T | null = null; 10 | if (this) result = JSON.parse(this); 11 | return result; 12 | }; 13 | 14 | const store = localStorage; 15 | 16 | const state: State = { 17 | store: store, 18 | pageTitle: "Vue3管理模板", 19 | roles: [], 20 | routes: [], 21 | addRoutes: [], 22 | user: store.getItem("user")?.parse() ?? {}, 23 | }; 24 | 25 | export default state; 26 | -------------------------------------------------------------------------------- /src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | import { ModalFuncProps } from "ant-design-vue/lib/modal/Modal"; 2 | 3 | export interface IResponse extends Promise { 4 | code: number; 5 | data?: T; 6 | msg: string; 7 | } 8 | 9 | export interface IUser { 10 | uid: number | string; 11 | nickname: string; 12 | token: string; 13 | } 14 | 15 | export interface IBaseProps { 16 | components?: ModalFuncProps; 17 | [key: string]: any; 18 | } 19 | -------------------------------------------------------------------------------- /src/types/hooks.d.ts: -------------------------------------------------------------------------------- 1 | export type Fn2 = () => void; 2 | 3 | export type FunctionArgs = ( 4 | ...args: Args 5 | ) => Return; 6 | 7 | export type EventFilter = ( 8 | invoke: Fn2, 9 | options: FunctionWrapperOptions 10 | ) => void; 11 | 12 | export interface FunctionWrapperOptions< 13 | Args extends any[] = any[], 14 | This = any 15 | > { 16 | fn: FunctionArgs; 17 | args: Args; 18 | thisArgs: This; 19 | } 20 | -------------------------------------------------------------------------------- /src/types/route.d.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | 3 | type RouteMeta = { 4 | title: string; 5 | icon: string; 6 | roles: string[]; 7 | activeMenu: string; 8 | }; 9 | 10 | export type RouteConfig = RouteRecordRaw & { 11 | hidden?: boolean; 12 | children?: RouteConfig[]; 13 | meta?: Partial; 14 | }; 15 | -------------------------------------------------------------------------------- /src/types/window.d.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | 3 | declare global { 4 | declare interface Window { 5 | __APP__: App; 6 | } 7 | declare interface String { 8 | parse(): T | null | undefined; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | 55 | 71 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 83 | 84 | -------------------------------------------------------------------------------- /src/views/feature/copy.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/feature/drag.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 52 | 53 | -------------------------------------------------------------------------------- /src/views/feature/functional.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /src/views/feature/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/feature/qrcode.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/feature/verify.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 42 | 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const prodConfig = require("./prod.config"); 2 | 3 | module.exports = { 4 | publicPath: process.env.VUE_APP_PUB_URL, 5 | assetsDir: prodConfig.assets, 6 | productionSourceMap: false, 7 | devServer: { 8 | // * 接口跨域处理 9 | proxy: { 10 | "/api": { 11 | target: process.env.PROXY_API_URL, 12 | changeOrigin: true, 13 | }, 14 | }, 15 | disableHostCheck: true, 16 | }, 17 | css: { 18 | sourceMap: false, 19 | loaderOptions: { 20 | scss: { 21 | prependData: `@import "~@/scss/variables.scss";`, 22 | }, 23 | less: { 24 | lessOptions: { 25 | modifyVars: { 26 | "primary-color": "#39a4ff", 27 | "link-color": "#39a4ff", 28 | }, 29 | javascriptEnabled: true, 30 | }, 31 | }, 32 | }, 33 | }, 34 | configureWebpack: { 35 | devtool: "source-map", 36 | externals: prodConfig.externals, 37 | optimization: prodConfig.optimization, 38 | plugins: prodConfig.plugins, 39 | resolve: { 40 | extensions: [".js", ".vue", ".json", ".ts"], 41 | }, 42 | performance: { 43 | hints: false, 44 | }, 45 | }, 46 | chainWebpack: (config) => { 47 | // * 移除prefetch和preload 48 | config.plugins.delete("prefetch"); 49 | config.plugins.delete("preload"); 50 | if (process.env.NODE_ENV === "production") { 51 | // config.entry("index").add("babel-polyfill"); 52 | prodConfig.uploadAssetsToOSS(config); 53 | // prodConfig.assetsGzip(config); 54 | config.optimization.delete("splitChunks"); 55 | config.plugin("html").tap((args) => { 56 | // 加上属性引号 57 | args[0].minify.removeAttributeQuotes = false; 58 | args[0].cdn = prodConfig.cdns.build; 59 | return args; 60 | }); 61 | } 62 | }, 63 | }; 64 | --------------------------------------------------------------------------------