├── .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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
16 |
Essential Links
17 |
24 |
Ecosystem
25 |
32 |
33 |
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 |
2 |
3 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
{{message || "默认内容"}}
4 |
关闭并销毁
6 |
7 |
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 |
2 |
4 |
5 |
8 |
12 |
14 |
15 |
16 |
17 | 验证成功~本次验证共计{{ verifyUse }}秒
19 |
20 |
21 | 图片加载中...请稍等
23 |
24 |
25 |
26 |
28 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
39 |
40 |
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 |
2 |
20 |
21 |
22 |
50 |
--------------------------------------------------------------------------------
/src/layout/components/AppHeaderChannel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 | Jack
10 |
11 |
12 | Lucy
13 |
14 |
15 | Tom
16 |
17 |
18 |
20 |
21 | 确定创建新站点吗?
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/layout/components/AppHeaderUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
推广员:{{name}}
13 |
14 |
15 |
16 |
17 |
18 |
39 |
--------------------------------------------------------------------------------
/src/layout/components/AppLinks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
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 |
2 |
5 |
6 |
7 |
10 |
11 |
12 |
16 |
17 | {{child.meta.title}}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
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 |
2 |
3 |
404
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This is an about page
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
54 |
55 |
71 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{title}}
5 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 登录
33 |
34 |
35 |
36 |
37 |
38 |
39 |
83 |
84 |
--------------------------------------------------------------------------------
/src/views/feature/copy.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/feature/drag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
拖拽排序
4 |
6 |
7 | {{ data.label }}
8 |
9 |
10 |
11 |
12 |
13 |
52 |
53 |
--------------------------------------------------------------------------------
/src/views/feature/functional.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
37 |
--------------------------------------------------------------------------------
/src/views/feature/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/views/feature/qrcode.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/feature/verify.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
滑块验证码
4 |
7 |
重置滑块
10 |
11 |
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 |
--------------------------------------------------------------------------------