├── src ├── views │ ├── level │ │ ├── Menu1.vue │ │ └── Menu2.vue │ ├── page │ │ └── result │ │ │ ├── Fail.vue │ │ │ └── Success.vue │ ├── system │ │ ├── profile │ │ │ └── index.vue │ │ ├── menu │ │ │ └── index.vue │ │ ├── password │ │ │ └── index.vue │ │ ├── account │ │ │ └── index.vue │ │ ├── dept │ │ │ └── index.vue │ │ └── About.vue │ ├── comp │ │ ├── button │ │ │ └── index.vue │ │ ├── table │ │ │ ├── acrossTable │ │ │ │ └── index.vue │ │ │ └── seniorSearchTable │ │ │ │ ├── type.d.ts │ │ │ │ ├── formComponent │ │ │ │ ├── CtInput.vue │ │ │ │ ├── CtSelect.vue │ │ │ │ └── CtDate.vue │ │ │ │ ├── utils.ts │ │ │ │ ├── demoList.ts │ │ │ │ ├── SearchScheme.vue │ │ │ │ ├── SchemeDialog.vue │ │ │ │ └── SearchItem.vue │ │ ├── svg │ │ │ └── index.vue │ │ ├── form │ │ │ └── SelectorWithPage.vue │ │ ├── dictComp │ │ │ └── dictTag.vue │ │ └── upload │ │ │ └── upload.vue │ ├── permission │ │ └── ButtonPermission.vue │ ├── demo │ │ ├── provideInject │ │ │ ├── son.vue │ │ │ ├── grandson.vue │ │ │ └── index.vue │ │ └── jsx │ │ │ └── SimpleJxs.vue │ ├── feat │ │ ├── direct │ │ │ ├── Adaptive.vue │ │ │ ├── Watermark.vue │ │ │ └── index.vue │ │ ├── media │ │ │ ├── Videojs.vue │ │ │ ├── TestComp.vue │ │ │ └── PreviewPDF.vue │ │ ├── icons │ │ │ ├── IconLib.vue │ │ │ └── index.vue │ │ ├── GSAP.vue │ │ ├── highlight.vue │ │ ├── Avatar.vue │ │ ├── download.vue │ │ └── import.vue │ ├── helloJest │ │ ├── hello.vue │ │ └── hello.spec.ts │ ├── 404.vue │ ├── 403.vue │ ├── editor │ │ ├── Markdown.vue │ │ ├── Codemirror.vue │ │ ├── WangEditor.vue │ │ └── Tinymce.vue │ ├── dashboard │ │ ├── components │ │ │ ├── PieChart.vue │ │ │ └── CountCard.vue │ │ └── Analysis.vue │ ├── messageCenter │ │ ├── MessageList.vue │ │ ├── NoticeList.vue │ │ └── index.vue │ ├── chart │ │ └── EChart.vue │ └── login │ │ └── login.vue ├── enums │ └── roleEnum.ts ├── assets │ ├── logo.png │ ├── images │ │ ├── bg.avif │ │ ├── ico.png │ │ ├── img.jpg │ │ ├── avatar1.png │ │ ├── avatar2.png │ │ ├── login-vector.png │ │ └── copy.svg │ └── css │ │ ├── icon.css │ │ ├── dark │ │ └── dark.scss │ │ ├── color-light.scss │ │ ├── theme.css │ │ ├── element │ │ ├── index.scss │ │ └── element.scss │ │ ├── color-dark.scss │ │ ├── variables.scss │ │ ├── index.scss │ │ └── main.scss ├── store │ ├── index.ts │ ├── sidebar.ts │ ├── locale.ts │ ├── theme.ts │ ├── tags.ts │ ├── dict.ts │ └── user.ts ├── directive │ ├── focus.ts │ ├── color.ts │ ├── index.ts │ ├── debounce.ts │ ├── auth.ts │ ├── throttle.ts │ ├── permiss.ts │ ├── tooptip.ts │ ├── adaptive.ts │ ├── watermark.ts │ ├── inputNumber.ts │ ├── resizable.ts │ └── copy.ts ├── api │ ├── index.ts │ ├── basicData.ts │ ├── user.ts │ ├── todo.ts │ ├── map.ts │ ├── client.ts │ ├── users.ts │ └── system.ts ├── env.d.ts ├── components │ ├── UI │ │ └── PageHeader.vue │ ├── MoIcon.vue │ ├── gsap │ │ └── GsapNumber.vue │ ├── echarts │ │ ├── MyEchart.vue │ │ └── echarts.ts │ ├── svgIcon │ │ ├── index.vue │ │ ├── svgIcons │ │ │ └── moon.svg │ │ └── svg.ts │ ├── MoCountTo.vue │ ├── Pagination.vue │ ├── MoDict.vue │ ├── EditableInput.vue │ ├── UserSelector.vue │ ├── MoOverTooltip.vue │ ├── MoDict.spec.ts │ └── MoMoreText.vue ├── utils │ ├── sum.ts │ ├── http │ │ ├── errorHandler.ts │ │ └── axios.ts │ ├── test │ │ └── sum.test.ts │ ├── index.ts │ └── permission.ts ├── vite-env.d.ts ├── App.vue ├── layout │ ├── theme │ │ └── index.ts │ ├── components │ │ ├── Message.vue │ │ ├── Language.vue │ │ ├── sidebar │ │ │ ├── index.vue │ │ │ └── SidebarItem.vue │ │ ├── header │ │ │ └── SideHeader.vue │ │ └── SiteSearch.vue │ ├── index.vue │ ├── vertical │ │ └── index.vue │ └── default │ │ └── index.vue ├── router │ ├── modules │ │ ├── about.ts │ │ ├── permission.ts │ │ ├── page.ts │ │ ├── chart.ts │ │ ├── dashboard.ts │ │ ├── demo.ts │ │ ├── editor.ts │ │ ├── level.ts │ │ ├── system.ts │ │ └── feat.ts │ ├── permission.ts │ └── index.ts ├── hooks │ ├── useDict.ts │ └── useTable.ts ├── locales │ ├── index.ts │ ├── zh.json │ └── en.json └── main.ts ├── public ├── 1.pdf ├── ico.png ├── home1.png ├── 演示文稿1.pdf ├── avatar2.png ├── favicon.ico ├── L16 心动-编配谱.pdf ├── template.xlsx ├── 新建 PPT 演示文稿.pdf ├── Javascript中的函数.pdf └── table.json ├── auto-imports.d.ts ├── .babelrc ├── types ├── axios.d.ts ├── shims.vue.d.ts ├── dict.d.ts ├── index.d.ts └── vue-router.d.ts ├── postcss.config.js ├── coverage ├── lcov-report │ ├── favicon.png │ ├── sort-arrow-sprite.png │ ├── prettify.css │ └── block-navigation.js └── lcov.info ├── README_EN.md ├── .env.development ├── .prettierrc ├── tsconfig.node.json ├── .env.production ├── .gitignore ├── .editorconfig ├── tailwind.config.js ├── README.md ├── tsconfig.json ├── index.html ├── LICENSE ├── jest.config.js ├── .eslintrc.js ├── package copy.json └── package.json /src/views/level/Menu1.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/level/Menu2.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/page/result/Fail.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/1.pdf -------------------------------------------------------------------------------- /src/views/system/profile/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/ico.png -------------------------------------------------------------------------------- /src/views/comp/button/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/page/result/Success.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/home1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/home1.png -------------------------------------------------------------------------------- /public/演示文稿1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/演示文稿1.pdf -------------------------------------------------------------------------------- /src/enums/roleEnum.ts: -------------------------------------------------------------------------------- 1 | export enum RoleEnum { 2 | SUPER = 'super', 3 | USER = 'user' 4 | } 5 | -------------------------------------------------------------------------------- /public/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/avatar2.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /public/L16 心动-编配谱.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/L16 心动-编配谱.pdf -------------------------------------------------------------------------------- /public/template.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/template.xlsx -------------------------------------------------------------------------------- /public/新建 PPT 演示文稿.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/新建 PPT 演示文稿.pdf -------------------------------------------------------------------------------- /src/views/system/menu/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/views/system/password/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/Javascript中的函数.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/public/Javascript中的函数.pdf -------------------------------------------------------------------------------- /src/assets/images/bg.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/bg.avif -------------------------------------------------------------------------------- /src/assets/images/ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/ico.png -------------------------------------------------------------------------------- /src/assets/images/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/img.jpg -------------------------------------------------------------------------------- /src/views/permission/ButtonPermission.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /types/axios.d.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | code: number 3 | message: string 4 | result: T 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/css/icon.css: -------------------------------------------------------------------------------- 1 | [class*=" el-icon-lx"], 2 | [class^=el-icon-lx] { 3 | font-family: lx-iconfont !important; 4 | } -------------------------------------------------------------------------------- /src/assets/images/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/avatar1.png -------------------------------------------------------------------------------- /src/assets/images/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/avatar2.png -------------------------------------------------------------------------------- /coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const pinia = createPinia() 4 | 5 | export default pinia 6 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # mocha-vue3-system 2 | A vue3 management system 3 | 4 | ## project screenshot 5 | ![screenshot](/public/home1.png) 6 | -------------------------------------------------------------------------------- /src/assets/images/login-vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/src/assets/images/login-vector.png -------------------------------------------------------------------------------- /src/directive/focus.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted(el: HTMLElement) { 3 | el.querySelector('input')?.focus() 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidity99/mocha-vue3-system/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /src/assets/css/dark/dark.scss: -------------------------------------------------------------------------------- 1 | html.dark { 2 | .main-content { 3 | background-color: black; 4 | } 5 | 6 | .tags { 7 | background-color: black; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /types/shims.vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const component: ReturnType 4 | export default Component 5 | } 6 | -------------------------------------------------------------------------------- /src/views/comp/table/acrossTable/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /types/dict.d.ts: -------------------------------------------------------------------------------- 1 | // 单个字典选项 2 | export interface DictItem { 3 | label: string 4 | value: string 5 | dictType?: string 6 | effect: string 7 | type?: string 8 | class?: string 9 | } 10 | -------------------------------------------------------------------------------- /src/directive/color.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | 3 | export default { 4 | mounted(el: HTMLElement, binding: DirectiveBinding) { 5 | el.style.color = binding.value 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境 2 | NODE_ENV = dev 3 | # 后台请求前缀,这是mock地址 4 | VITE_BASE_URL = '/api/' 5 | 6 | VITE_PERMISSION_MODE = 'CONSTANT' 7 | # VITE_PERMISSION_MODE = 'FRONT' 8 | # VITE_PERMISSION_MODE = 'BACK' 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false 9 | } 10 | -------------------------------------------------------------------------------- /src/views/demo/provideInject/son.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/assets/css/color-light.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .tags-li { 4 | &.active { 5 | background-color: $primary-color; 6 | color: #fff; 7 | .tags-li-title { 8 | color: #fff; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | NODE_ENV = prod 3 | # 后台请求前缀,这是mock地址 4 | VITE_BASE_URL = 'https://mock.apifox.cn/m1/2700315-0-default/' 5 | 6 | VITE_PERMISSION_MODE = 'CONSTANT' 7 | # VITE_PERMISSION_MODE = 'FRONT' 8 | # VITE_PERMISSION_MODE = 'BACK' 9 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export const fetchData = () => { 4 | return request({ 5 | url: 'https://console-mock.apipost.cn/mock/92e978f3-7c40-4e4a-b24d-0d322e6a4337/getList', 6 | method: 'get' 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/api/basicData.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export default { 4 | // 获取客户列表数据 5 | getHomeMapDataApi(params: {}) { 6 | return request({ 7 | url: 'getProvinceMapData', 8 | 9 | params 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface SelectOptionItem { 2 | value: string 3 | label: string 4 | } 5 | 6 | export interface UserInfo { 7 | userid: number 8 | username: string 9 | password?: string 10 | role?: string 11 | permiss?: [] 12 | routes?: [] 13 | } 14 | -------------------------------------------------------------------------------- /types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | import { _RouteRecordBase, RouteMeta } from 'vue-router' 2 | 3 | declare module 'vue-router' { 4 | interface _RouteRecordBase { 5 | hidden?: boolean 6 | } 7 | 8 | interface RouteMeta { 9 | order?: number 10 | roles?: array 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/views/comp/svg/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/views/system/account/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /src/store/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useSidebarStore = defineStore('sidebar', { 4 | state: () => { 5 | return { 6 | collapse: false 7 | } 8 | }, 9 | getters: {}, 10 | actions: { 11 | handleCollapse() { 12 | this.collapse = !this.collapse 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export function login(data: {}) { 4 | return request({ 5 | url: '/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | export function fetchUser(data: {}) { 11 | return request({ 12 | url: '/api/getUser', 13 | method: 'get', 14 | data 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/views/feat/direct/Adaptive.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/UI/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/utils/sum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | @Author: lucidity99 lucidity929@163.com 3 | @Date: 2024-01-27 14:26:29 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 15:26:36 6 | * @FilePath: /mocha-vue3-system/src/utils/sum.ts 7 | @Description: 8 | @ 9 | @ 10 | */ 11 | function sum(a: any, b: any) { 12 | return a + b 13 | } 14 | export default sum 15 | -------------------------------------------------------------------------------- /src/components/MoIcon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/assets/css/theme.css: -------------------------------------------------------------------------------- 1 | .blue #app { 2 | --el-color-primary: #0052d9; 3 | } 4 | 5 | .red #app { 6 | --el-color-primary: #ff2551; 7 | } 8 | 9 | .pink #app { 10 | --el-color-primary: #f47983; 11 | } 12 | 13 | .green #app { 14 | --el-color-primary: #0c8918; 15 | } 16 | 17 | .brown #app { 18 | --el-color-primary: #ae7000; 19 | } 20 | 21 | .grape #app { 22 | --el-color-primary: #725e82; 23 | } 24 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | 9 | declare const __APP_INFO__: { 10 | pkg: { 11 | dependencies: {} 12 | devDependencies: {} 13 | } 14 | lastBuildTime: string 15 | } 16 | 17 | declare module 'vue-cropperjs' 18 | -------------------------------------------------------------------------------- /src/views/comp/form/SelectorWithPage.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /src/assets/css/element/index.scss: -------------------------------------------------------------------------------- 1 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 2 | $colors: ( 3 | 'primary': ( 4 | 'base': #0052d9 5 | ), 6 | 'success': ( 7 | 'base': #2ba471 8 | ), 9 | 'danger': ( 10 | 'base': #d54941 11 | ) 12 | ), 13 | $menu: ( 14 | ('item-height': 50px) 15 | ) 16 | ); 17 | 18 | // 如果只是按需导入,则可以忽略以下内容。 19 | // 如果你想导入所有样式: 20 | @use 'element-plus/theme-chalk/src/index.scss' as *; 21 | -------------------------------------------------------------------------------- /src/assets/css/color-dark.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #0e42d2; 2 | $base-bgcolor: #262f3e; 3 | 4 | .v-header { 5 | 6 | border-bottom: 1px solid $base-bgcolor; 7 | } 8 | .login-wrap { 9 | background: #324157; 10 | } 11 | 12 | .el-upload--text em { 13 | color: $primary-color; 14 | } 15 | 16 | .tags-li.active { 17 | border: 1px solid $primary-color; 18 | background-color: $primary-color; 19 | } 20 | 21 | .collapse-btn:hover { 22 | background: rgb(40, 52, 70); 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | # 表示是最顶层的 EditorConfig 配置文件 4 | root = true 5 | 6 | [*] # 表示所有文件适用 7 | charset = utf-8 # 设置文件字符集为 utf-8 8 | indent_style = space # 缩进风格(tab | space) 9 | indent_size = 2 # 缩进大小 10 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 11 | trim_trailing_whitespace = true # 去除行首的任意空白字符 12 | insert_final_newline = true # 始终在文件末尾插入一个新行 13 | 14 | [*.md] # 表示仅 md 文件适用以下规则 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/api/todo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2024-02-22 22:57:33 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-02-22 22:57:52 6 | * @FilePath: /mocha-vue3-system/src/api/todo.ts 7 | * @Description: 8 | * 9 | * 10 | */ 11 | import request from '~/utils/http/axios' 12 | 13 | export default { 14 | getTodoList(params: {}) { 15 | return request({ 16 | url: 'getTodoList', 17 | params 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface SearchColumnItem { 2 | label: string 3 | code: string 4 | type: string 5 | condition: string 6 | andOr?: string 7 | isDefault?: boolean 8 | } 9 | 10 | export interface SearchFormItem { 11 | code: string 12 | condition: string 13 | value: string | number 14 | type: string 15 | andOr?: string 16 | } 17 | 18 | export interface SchemeItem { 19 | title: string 20 | isDefault: boolean 21 | data: SearchFormItem 22 | } 23 | -------------------------------------------------------------------------------- /src/views/feat/direct/Watermark.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 4 | darkMode:'class', 5 | important: 'body', 6 | theme: { 7 | extend:{ 8 | colors: { 9 | primary: 'rgba(var(--color-primary), )', 10 | } 11 | } 12 | 13 | }, 14 | plugins: [], 15 | corePlugins: { 16 | container: false // 如果您不打算在您的项目中使用 container 类,您可以通过在配置文件的 corePlugins 部分将 container 属性设置为 false 来完全禁用它 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/images/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | 3 | const modules = import.meta.glob('../directive/**/*.ts', { 4 | eager: true 5 | }) 6 | 7 | let mapDirective = new Map() 8 | 9 | Object.keys(modules).forEach((key) => { 10 | if (modules[key] && modules[key].default) { 11 | const newKey = key.replace(/^\.\/|\.ts|\.js/g, '') 12 | mapDirective.set(newKey, modules[key].default) 13 | } 14 | }) 15 | 16 | export default (app: App) => { 17 | mapDirective.forEach((value, key) => { 18 | app.directive(key, value) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/views/helloJest/hello.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mocha-vue3-system 2 | mocha-vue3-system是一个基于Vue3、Vite、Element Plus、Typescript、Tailwind CSS的后台管理系统模板,目标是提供轻松简单的后台开发方案。 3 | 可以使用它作为项目的启动模板,帮助你快速搭建后台管理系统,也可以作为学习的demo,尝试更多可能性。 4 | 5 | 项目仍在快速迭代中,作者会持续更新和优化,完善使用体验。 6 | 7 | 8 | ## 示例 9 | [在线预览](http://118.89.81.22:9527/) 10 | 11 | ## 文档 12 | 目前还没有发布单独的文档, 13 | 14 | 现阶段请参考掘金专栏:https://juejin.cn/column/7226940127885869114 15 | 16 | ## 项目截图 17 | ![home1](https://github.com/lucidity99/mocha-vue3-system/assets/44628665/b54f73cd-ab9b-4500-83a9-29048b662484) 18 | 19 | ## 联系我 20 | 欢迎通过掘金联系我,或者直接发送邮件。 21 | 22 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/formComponent/CtInput.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/formComponent/CtSelect.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/formComponent/CtDate.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #0052d9; 2 | $success-color: #2ba471; 3 | $danger-color: #d54941; 4 | $base-bgcolor: #262f3e; 5 | $base-border-color: #e8e8e8; 6 | 7 | $bg-color-page: #f0f0f0; 8 | 9 | // 循环1 10 | $sizes: 20px, 22px, 24px, 30px; 11 | @each $size in $sizes { 12 | .font-#{$size} { 13 | font-size: $size; 14 | } 15 | } 16 | // 循环2 17 | $themes: ( 18 | 'blue': blue, 19 | 'red': red, 20 | 'green': green 21 | ); 22 | @each $key, $val in $themes { 23 | .sect-#{$key} { 24 | background-color: $val; 25 | color: #fff; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/directive/debounce.ts: -------------------------------------------------------------------------------- 1 | // 节流 2 | // 防止按钮多次点击,多次请求 3 | import { DirectiveBinding } from 'vue' 4 | 5 | export default { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | const time = binding.value?.time || 1000 8 | const func = binding.value?.func || null 9 | el.timer = null 10 | 11 | el.addEventListener('click', () => { 12 | if (el.timer !== null) { 13 | clearTimeout(el.timer) 14 | el.timer = null 15 | } 16 | el.timer = setTimeout(() => { 17 | func && func() 18 | }, time) 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/layout/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { Pinia } from 'pinia' 2 | import { useThemeStore } from '~/store/theme' 3 | 4 | export default (pinia: Pinia) => { 5 | const useTheme = useThemeStore(pinia) 6 | // const el = document.documentElement 7 | 8 | // 如果有缓存的主题方案 9 | if (useTheme.scheme) { 10 | document.getElementsByTagName('html')[0].className = useTheme.scheme 11 | } 12 | // 如果有缓存的自定义设置 13 | if (useTheme.css) { 14 | Object.keys(useTheme.css).forEach((val) => { 15 | document.documentElement.style.setProperty(val, useTheme.css[val]) 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/css/index.scss: -------------------------------------------------------------------------------- 1 | @use './color-light.scss'; 2 | @use './icon.css'; 3 | @use './main.scss'; 4 | @use './element/element.scss'; 5 | @use './dark/dark.scss'; 6 | 7 | @tailwind base; 8 | @tailwind components; 9 | @tailwind utilities; 10 | 11 | @layer base { 12 | :root { 13 | /* For rgb(255 115 179 / ) */ 14 | --color-primary: 255 115 179; 15 | 16 | /* For hsl(198deg 93% 60% / ) */ 17 | --color-primary: 198deg 93% 60%; 18 | 19 | /* For rgba(255, 115, 179, ) */ 20 | --color-primary: 255, 115, 179; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/store/locale.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed, ref } from 'vue' 3 | import i18n from '~/locales' 4 | 5 | export const useLocaleStore = defineStore( 6 | 'locale', 7 | () => { 8 | let locale = ref(i18n.global.locale.value) 9 | let currentLocale = computed(() => { 10 | return locale 11 | }) 12 | // 设置locale 13 | function setLocale(lang) { 14 | locale.value = lang 15 | i18n.global.locale.value = lang 16 | } 17 | 18 | return { locale, currentLocale, setLocale } 19 | }, 20 | { 21 | persist: true 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /src/api/map.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export default { 4 | fetchChinaMapData() { 5 | return request({ 6 | url: 'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json', 7 | method: 'get', 8 | responseType: 'json', 9 | isReturnNativeData: true 10 | }) 11 | }, 12 | 13 | fetchProvinceMapData(code: string) { 14 | return request({ 15 | url: `https://geo.datav.aliyun.com/areas_v3/bound/${code}_full.json`, 16 | method: 'get', 17 | responseType: 'json', 18 | isReturnNativeData: true 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/directive/auth.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | 3 | import { useUserStore } from '~/store/user' 4 | 5 | export default { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | const val = binding.value 8 | const useUser = useUserStore() 9 | const role = useUser.role 10 | const parentEl = el.parentElement 11 | 12 | let flag = true 13 | // 可以传入字符串和数组 14 | if (typeof val === 'string') { 15 | flag = role === val 16 | } else { 17 | flag = val.includes(role) 18 | } 19 | 20 | if (!flag) parentEl?.removeChild(el) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/directive/throttle.ts: -------------------------------------------------------------------------------- 1 | // 节流 2 | // 防止按钮多次点击,多次请求 3 | import { DirectiveBinding } from 'vue' 4 | 5 | export default { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | const time = binding.value?.time || 1000 8 | el.timer = null 9 | 10 | el.addEventListener('click', () => { 11 | el.disabled = true 12 | 13 | if (el.timer !== null) { 14 | clearTimeout(el.timer) 15 | el.timer = null 16 | el.disabled = true 17 | } 18 | el.timer = setTimeout(() => { 19 | el.disabled = false 20 | }, time) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/css/element/element.scss: -------------------------------------------------------------------------------- 1 | .card-header { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | 7 | // 没有头部的drawer 8 | .el-drawer.no-header { 9 | overflow: visible; 10 | .el-drawer__header { 11 | height: 0; 12 | padding: 0; 13 | margin: 0; 14 | } 15 | .el-drawer__close-btn { 16 | background-color: var(--el-color-primary); 17 | position: absolute; 18 | left: -36px; 19 | top: 30%; 20 | border-radius: 4px 0 0 4px; 21 | color: #fff; 22 | padding: 8px; 23 | } 24 | .el-drawer__body { 25 | padding: 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/gsap/GsapNumber.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /src/views/demo/provideInject/grandson.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/403.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "baseUrl": ".", 16 | "paths": { "~/*": ["src/*"] , "#/*": ["types/*"]} 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /src/directive/permiss.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | 3 | import { useUserStore } from '~/store/user' 4 | 5 | export default { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | const val = binding.value 8 | const useUser = useUserStore() 9 | const permiss = useUser.permiss 10 | const parentEl = el.parentElement 11 | 12 | let flag = true 13 | // 可以传入字符串和数组 14 | if (typeof val === 'string') { 15 | flag = permiss.includes(val) 16 | } else { 17 | flag = val.some((item: string) => permiss.includes(item)) 18 | } 19 | 20 | if (!flag) parentEl?.removeChild(el) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/layout/components/Message.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/views/demo/jsx/SimpleJxs.vue: -------------------------------------------------------------------------------- 1 | 19 | 22 | 33 | -------------------------------------------------------------------------------- /src/router/modules/about.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/about', 7 | name: 'about', 8 | component: Layout, 9 | redirect: '/about/index', 10 | meta: { 11 | title: 'about', 12 | icon: 'ep-mic', 13 | order: 100 14 | }, 15 | children: [ 16 | { 17 | path: 'index', 18 | name: 'aboutPage', 19 | component: () => import('~/views/system/About.vue'), 20 | meta: { 21 | title: 'about' 22 | } 23 | } 24 | ] 25 | } 26 | ] 27 | 28 | export default routes 29 | -------------------------------------------------------------------------------- /src/views/demo/provideInject/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /src/hooks/useDict.ts: -------------------------------------------------------------------------------- 1 | import { reactive, onMounted } from 'vue' 2 | import system from '~/api/system' 3 | import systemApi from '~/api/system' 4 | 5 | export function useDict(dictType: Array) { 6 | let dicts: any = reactive({}) 7 | 8 | onMounted(() => { 9 | let ps: Promise[] = [] 10 | 11 | dictType.forEach((dt, index) => { 12 | ps[index] = systemApi.getDicts(dt).then((res) => { 13 | return { type: dt, values: res } 14 | }) 15 | }) 16 | 17 | Promise.all(ps).then((res) => { 18 | res.forEach((val) => { 19 | dicts[val.type] = val.values 20 | }) 21 | }) 22 | }) 23 | 24 | return { dicts } 25 | } 26 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /src/api/client.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export default { 4 | // 获取客户列表数据 5 | getClientList(data: {}) { 6 | return request({ 7 | url: 'getClientList', 8 | method: 'post', 9 | data 10 | }) 11 | }, 12 | 13 | // 获取客户详情 14 | getClientDetails(id: string) { 15 | return request({ 16 | url: 'getClientDetails', 17 | method: 'get', 18 | params: { id } 19 | }) 20 | }, 21 | 22 | updateClient(data: {}) { 23 | return request({ 24 | url: '/client/update', 25 | method: 'post', 26 | data 27 | }) 28 | }, 29 | // 删除客户 30 | deleteClient(data: []) { 31 | return request({}) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/router/modules/permission.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw = { 5 | path: '/permission', 6 | name: 'permissions', 7 | component: Layout, 8 | redirect: '/permission/index', 9 | meta: { 10 | title: 'permission', 11 | icon: 'ep-lock', 12 | order: 4 13 | }, 14 | children: [ 15 | { 16 | path: 'index', 17 | name: 'buttonPermission', 18 | component: () => 19 | import(/* webpackChunkName: "permission" */ '~/views/permission/ButtonPermission.vue'), 20 | meta: { 21 | title: 'permission' 22 | } 23 | } 24 | ] 25 | } 26 | 27 | export default routes 28 | -------------------------------------------------------------------------------- /src/router/modules/page.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/page', 7 | name: 'page', 8 | component: Layout, 9 | redirect: '/page/tabs', 10 | meta: { 11 | title: 'pages', 12 | icon: 'ep-brush', 13 | order: 3 14 | }, 15 | 16 | children: [ 17 | { 18 | path: 'tabs', 19 | name: 'tabs', 20 | meta: { 21 | title: 'tab', 22 | icon: 'ep-price-tag' 23 | }, 24 | 25 | component: () => import(/* webpackChunkName: "tabs" */ '~/views/pageDemo/tabs.vue') 26 | } 27 | ] 28 | } 29 | ] 30 | 31 | export default routes 32 | -------------------------------------------------------------------------------- /src/api/users.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http' 2 | 3 | interface OrgTree { 4 | id: string 5 | name: string 6 | parentId: string 7 | type: string 8 | children: OrgTree[] 9 | } 10 | 11 | export default { 12 | getStaffList(params: {}) { 13 | return request({ 14 | url: '/staff', 15 | params: params 16 | }) 17 | }, 18 | 19 | updateAccess2PC(params: { staffId: string; status: string }) { 20 | return request({ 21 | url: `/staff/enablePassport?staffId${params.staffId}&enableStatus${params.status}`, 22 | params: params, 23 | method: 'post' 24 | }) 25 | }, 26 | 27 | fetchOrgTreeData() { 28 | return request({ 29 | url: `/org/tree?orgType=VAZYME` 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/views/feat/media/Videojs.vue: -------------------------------------------------------------------------------- 1 | 11 | 15 | 16 | 30 | -------------------------------------------------------------------------------- /src/utils/http/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage, ElNotification } from 'element-plus' 2 | 3 | // 根据错误代码,获取对应文字 4 | const errorMsgHandler = (errStatus: number): string => { 5 | if (errStatus === 500) return '服务器内部错误' 6 | if ((errStatus = 400)) return '没有权限' 7 | return '未知错误' 8 | } 9 | 10 | // 根据mode,返回错误信息 11 | const errorHandler = (errMsg: string, mode: string = 'modal') => { 12 | const msg = errMsg || '未知错误' 13 | 14 | if (mode === 'modal') { 15 | ElMessage(msg + '|' + mode) 16 | } 17 | if (mode === 'toast') { 18 | ElNotification({ 19 | title: 'Error', 20 | message: msg, 21 | type: 'error' 22 | }) 23 | } 24 | if (mode === 'hiden') { 25 | } 26 | } 27 | 28 | export { errorHandler, errorMsgHandler } 29 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/utils.ts: -------------------------------------------------------------------------------- 1 | export const andOrOptions = [ 2 | { 3 | value: 'and', 4 | label: '并且' 5 | }, 6 | { 7 | value: 'or', 8 | label: '或者' 9 | } 10 | ] 11 | 12 | export const conditionOptions = [ 13 | { 14 | value: 'equal', 15 | label: '等于' 16 | }, 17 | { 18 | value: 'notEqual', 19 | label: '不等于' 20 | }, 21 | { 22 | value: 'like', 23 | label: '包含' 24 | }, 25 | { 26 | value: 'llike', 27 | label: '左包含' 28 | }, 29 | { 30 | value: 'rlike', 31 | label: '右包含' 32 | }, 33 | { 34 | value: 'greater', 35 | label: '大于' 36 | }, 37 | { 38 | value: 'less', 39 | label: '小于' 40 | }, 41 | { 42 | value: 'null', 43 | label: '为空' 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /src/views/editor/Markdown.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | mocha vue3 admin 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import zh from './zh.json' 3 | import en from './en.json' 4 | 5 | // 获取浏览器界面语言,默认语言 6 | // https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/language 7 | let currentLanguage = navigator.language.replace(/-(\S*)/, '') 8 | 9 | // 如果本地缓存记录了语言环境,则使用本地缓存 10 | let lsLocale = localStorage.getItem('locale') || '' 11 | if (lsLocale) { 12 | currentLanguage = JSON.parse(lsLocale)?.locale 13 | } 14 | 15 | export default createI18n({ 16 | locale: currentLanguage, 17 | legacy: false, // you must set `false`, to use Composition API 18 | globalInjection: true, // 全局注册 $t 19 | messages: { 20 | zh, 21 | en 22 | } 23 | }) 24 | 25 | export const langs = [ 26 | { key: 'zh', title: '中文' }, 27 | { key: 'en', title: 'English' } 28 | ] 29 | -------------------------------------------------------------------------------- /src/api/system.ts: -------------------------------------------------------------------------------- 1 | import request from '~/utils/http/axios' 2 | 3 | export default { 4 | getRoutes: (data: {}) => { 5 | return request({ 6 | url: '/getRoutes', 7 | method: 'post', 8 | data 9 | }) 10 | }, 11 | 12 | getDeptTree: (data: {}) => { 13 | return request({ 14 | url: '/getDeptTree', 15 | method: 'post', 16 | data, 17 | repeatRequest: false, 18 | isReturnNativeData: true, 19 | errorMode: 'hidden' 20 | }) 21 | }, 22 | 23 | getMessageList: (data: {}) => { 24 | return request({ 25 | url: '/getMessageList', 26 | method: 'post', 27 | data 28 | }) 29 | }, 30 | 31 | getDicts: (type: string) => { 32 | return request({ 33 | url: '/getDicts', 34 | method: 'get', 35 | params: { type } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/router/modules/chart.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/chart', 7 | name: 'chart', 8 | component: Layout, 9 | redirect: '/chart/echart', 10 | meta: { 11 | title: 'chart', 12 | icon: 'ep-pie-chart', 13 | order: 9 14 | }, 15 | children: [ 16 | { 17 | path: 'echart', 18 | name: 'echart', 19 | meta: { 20 | title: 'eChart' 21 | }, 22 | component: () => import('~/views/chart/EChart.vue') 23 | }, 24 | { 25 | path: 'map', 26 | name: 'map', 27 | meta: { 28 | title: 'map' 29 | }, 30 | component: () => import('~/views/chart/echartMap.vue') 31 | } 32 | ] 33 | } 34 | ] 35 | 36 | export default routes 37 | -------------------------------------------------------------------------------- /src/layout/components/Language.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /src/views/feat/media/TestComp.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 41 | -------------------------------------------------------------------------------- /src/directive/tooptip.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding, h, render } from 'vue' 2 | import { ElTooltip, ElTag } from 'element-plus' 3 | import { QuestionFilled } from '@element-plus/icons-vue' 4 | 5 | export default { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | const message = binding.value.message 8 | const placement = binding.value.placement || 'top' 9 | const effect = binding.value.effect || 'light' 10 | const position = binding.value.position || 'left' 11 | if (binding.value.message) { 12 | const vnode = h( 13 | ElTooltip, 14 | { content: message, placement, effect }, 15 | h(QuestionFilled, { style: { width: '16px' } }) 16 | ) 17 | 18 | const dom = document.createElement('span') 19 | if (position === 'left') el.prepend(dom) 20 | else el.append(dom) 21 | 22 | render(vnode, dom) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/echarts/MyEchart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/router/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/dashboard', 7 | name: 'dashboard', 8 | component: Layout, 9 | redirect: '/dashboard/workbench', 10 | meta: { 11 | title: 'dashboard', 12 | icon: 'ep-sunrise', 13 | order: 1 14 | }, 15 | 16 | children: [ 17 | { 18 | path: 'workbench', 19 | name: 'workbench', 20 | meta: { 21 | title: 'workbench' 22 | }, 23 | component: () => import('~/views/dashboard/Workbench.vue') 24 | }, 25 | { 26 | path: 'analysis', 27 | name: 'analysis', 28 | meta: { 29 | title: 'analysis' 30 | }, 31 | component: () => import('~/views/dashboard/Analysis.vue') 32 | } 33 | ] 34 | } 35 | ] 36 | 37 | export default routes 38 | -------------------------------------------------------------------------------- /src/directive/adaptive.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | 3 | interface ExHTMLElement extends HTMLElement { 4 | resizeListener: EventListener 5 | } 6 | 7 | export default { 8 | mounted: (el: ExHTMLElement, binding: DirectiveBinding) => { 9 | el.resizeListener = () => { 10 | setHeight(el, binding) 11 | } 12 | 13 | setHeight(el, binding) 14 | 15 | window.addEventListener('resize', el.resizeListener) 16 | }, 17 | unmounted(el: ExHTMLElement) { 18 | window.removeEventListener('resize', el.resizeListener) 19 | }, 20 | updated(el: ExHTMLElement, binding: DirectiveBinding) { 21 | setHeight(el, binding) 22 | } 23 | } 24 | 25 | // set el-table height 26 | function setHeight(el: ExHTMLElement, binding: DirectiveBinding) { 27 | const top = el.offsetTop 28 | const bottom = binding?.value?.bottom || 84 29 | const pageHeight = window.innerHeight 30 | el.style.height = pageHeight - top - bottom + 'px' 31 | el.style.overflowY = 'auto' 32 | } 33 | -------------------------------------------------------------------------------- /src/router/modules/demo.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/demo', 7 | name: 'demo', 8 | component: Layout, 9 | meta: { 10 | title: 'demo', 11 | icon: 'ep-collection', 12 | order: 11 13 | }, 14 | redirect: '/demo/clientList', 15 | children: [ 16 | { 17 | path: 'provideAndInject', 18 | name: 'provide&provideAndInject', 19 | meta: { 20 | title: 'provideAndInject' 21 | }, 22 | component: () => 23 | import(/* webpackChunkName: "feat" */ '~/views/demo/provideInject/index.vue') 24 | }, 25 | { 26 | path: 'jsx', 27 | name: 'jsx', 28 | meta: { 29 | title: 'jsx' 30 | }, 31 | component: () => import(/* webpackChunkName: "feat" */ '~/views/demo/jsx/SimpleJxs.vue') 32 | } 33 | ] 34 | } 35 | ] 36 | 37 | export default routes 38 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/demoList.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | label: '用户名', 4 | code: 'name', 5 | type: 'input', 6 | condition: 'equal', 7 | default: true 8 | }, 9 | { 10 | label: '职位', 11 | code: 'position', 12 | type: 'select', 13 | condition: 'equal' 14 | }, 15 | { 16 | label: '部门', 17 | code: 'dept', 18 | type: 'select', 19 | condition: 'equal' 20 | }, 21 | { 22 | label: '签约日期', 23 | code: 'signDate', 24 | type: 'date', 25 | condition: 'greater', 26 | default: true 27 | } 28 | ] 29 | 30 | export const demoOptioins = { 31 | position: [ 32 | { label: 'CEO', value: 'CEO' }, 33 | { label: 'CTO', value: 'CTO' }, 34 | { label: 'COO', value: 'COO' } 35 | ], 36 | dept: [ 37 | { label: 'IT Dept.', value: 'it' }, 38 | { label: 'Financial Dept.', value: 'financial' }, 39 | { label: 'Sales division I', value: 'sale1' }, 40 | { label: 'Sales division II', value: 'sale2' } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/components/svgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 39 | 46 | -------------------------------------------------------------------------------- /src/store/theme.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useThemeStore = defineStore( 5 | 'theme', 6 | () => { 7 | let css = ref({}) 8 | // UI主题 9 | let scheme = ref('') 10 | // 布局方案 11 | let layoutScheme = ref('default') 12 | 13 | // 设置配色主题 14 | function setScheme(str: string) { 15 | scheme.value = str 16 | } 17 | 18 | // 设置CSS Vars 19 | function setCSS(property: string, value: string) { 20 | css.value[property] = value 21 | } 22 | 23 | // 设置用户定义的CSS变量 24 | function setCustomized(vars: any) { 25 | Object.keys(vars).forEach((item) => { 26 | setCSS(vars[item].key, vars[item].value) 27 | }) 28 | } 29 | 30 | // 设置布局方案 31 | function setLayoutScheme(layout: string) { 32 | layoutScheme.value = layout 33 | } 34 | 35 | return { scheme, css, layoutScheme, setCSS, setScheme, setCustomized, setLayoutScheme } 36 | }, 37 | { 38 | persist: true 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /src/components/echarts/echarts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core' 2 | 3 | /** 引入需要的图表,后缀都为Chart */ 4 | import { BarChart, LineChart, PieChart } from 'echarts/charts' 5 | 6 | // 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component 7 | import { 8 | TitleComponent, 9 | TooltipComponent, 10 | GridComponent, 11 | DatasetComponent, 12 | TransformComponent, 13 | LegendComponent, 14 | ToolboxComponent 15 | } from 'echarts/components' 16 | 17 | // 标签自动布局,全局过渡动画等特性 18 | import { LabelLayout, UniversalTransition } from 'echarts/features' 19 | 20 | // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 21 | import { CanvasRenderer } from 'echarts/renderers' 22 | 23 | // 注册必须的组件 24 | echarts.use([ 25 | ToolboxComponent, 26 | LegendComponent, 27 | TitleComponent, 28 | TooltipComponent, 29 | GridComponent, 30 | DatasetComponent, 31 | TransformComponent, 32 | LabelLayout, 33 | UniversalTransition, 34 | CanvasRenderer, 35 | BarChart, 36 | LineChart, 37 | PieChart 38 | ]) 39 | 40 | // 导出 41 | export default echarts 42 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 29 | -------------------------------------------------------------------------------- /src/store/tags.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | 4 | interface ListItem { 5 | name: string 6 | path: string 7 | title: string 8 | } 9 | 10 | export const useTagsStore = defineStore( 11 | 'tags', 12 | () => { 13 | let list = ref([]) 14 | 15 | let show = computed(() => { 16 | return list.value.length > 0 17 | }) 18 | let nameList = computed(() => { 19 | return list.value.map((item: ListItem) => item.name) 20 | }) 21 | 22 | function delTagsItem(index: number) { 23 | list.value.splice(index, 1) 24 | } 25 | function setTagsItem(data: ListItem) { 26 | list.value.push(data) 27 | } 28 | function clearTags() { 29 | list.value = [] 30 | } 31 | function closeTagsOther(data: ListItem[]) { 32 | list.value = data 33 | } 34 | 35 | return { list, show, nameList, delTagsItem, setTagsItem, clearTags, closeTagsOther } 36 | }, 37 | { 38 | persist: { 39 | storage: sessionStorage 40 | } 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /src/views/feat/icons/IconLib.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /src/components/MoCountTo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 40 | -------------------------------------------------------------------------------- /src/views/feat/GSAP.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import router from '~/router' 2 | import { useUserStore } from '~/store/user' 3 | import { addAsyncRoutes } from '~/utils/permission' 4 | 5 | // 路由白名单 6 | const whiteList = ['/login'] 7 | 8 | router.beforeEach(async (to) => { 9 | document.title = `${to.meta.title} | mocha vue3 admin` 10 | 11 | const useUser = useUserStore() 12 | const role = useUser.role 13 | 14 | // 用户已登录 15 | if (useUser.userid) { 16 | if (to.path === '/login') { 17 | return '/' 18 | } 19 | // 前端固定路由模式,如果没有权限,进入403页面 20 | if ( 21 | import.meta.env.VITE_PERMISSIOIN_MODE === 'CONSTANT' && 22 | to.meta.roles && 23 | !to.meta.roles.includes(role) 24 | ) { 25 | return '/403' 26 | } 27 | // 前端动态路由和后端动态路由,动态挂载路由 28 | else { 29 | if (!to.redirectedFrom) { 30 | await addAsyncRoutes(router) 31 | 32 | return { ...to, replace: true } 33 | } else return true 34 | } 35 | } else { 36 | // 白名单,直接放行 37 | if (whiteList.indexOf(to.path) > -1) return true 38 | // 非白名单,去登录 39 | else return '/login' 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /src/utils/test/sum.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | @Author: lucidity99 lucidity929@163.com 3 | @Date: 2024-01-27 14:26:10 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 15:30:57 6 | * @FilePath: /mocha-vue3-system/src/utils/test/sum.test.ts 7 | @Description: 8 | @ 9 | @ 10 | */ 11 | 12 | import sum from './../sum' 13 | 14 | describe('sum.js', () => { 15 | test('两数之和', () => { 16 | expect(sum(1, 2)).toBe(3) 17 | }) 18 | test('字符串拼接', () => { 19 | expect(sum('a', 'b')).toBe('ab') 20 | }) 21 | }) 22 | 23 | // 模拟一个异步操作 24 | function getName() { 25 | return new Promise((resolve) => { 26 | setTimeout(() => { 27 | resolve('bar') 28 | }, 1000) 29 | }) 30 | } 31 | 32 | describe('test promise', () => { 33 | // 写法1 34 | test('style1', async () => { 35 | const res = await getName() 36 | expect(res).toBe('bar') 37 | }) 38 | // 写法2 39 | test('style2', () => { 40 | return expect(getName()).resolves.toBe('bar') 41 | }) 42 | // 写法3 43 | test('style3', () => { 44 | return getName().then((res) => { 45 | expect(res).toBe('bar') 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lucidity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/directive/watermark.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | 3 | // 水印 4 | export default { 5 | mounted(el: HTMLElement, binding: DirectiveBinding) { 6 | let txt = 'hello world' 7 | let style = {} 8 | if (binding && binding.value) { 9 | txt = binding.value.txt 10 | style = binding.value.style 11 | } 12 | genWatermark(el, txt, style) 13 | } 14 | } 15 | 16 | interface CanvasTextStyle { 17 | font?: string 18 | color?: string 19 | } 20 | 21 | function genWatermark(el: HTMLElement, txt: string, style?: CanvasTextStyle) { 22 | const defaultStyle = { 23 | font: '14px arial', 24 | color: 'rgba(0,0,0,0.2)' 25 | } 26 | 27 | let canvas = document.createElement('canvas') 28 | let ctx = canvas.getContext('2d') 29 | 30 | ctx.translate(150, 75) 31 | ctx.rotate((Math.PI / 180) * 25) 32 | ctx.translate(-150, -75) 33 | 34 | ctx.font = style?.font || defaultStyle.font 35 | ctx.fillStyle = style?.color || defaultStyle.color 36 | ctx.textAlign = 'center' 37 | ctx.textBaseline = 'middle' 38 | 39 | ctx.fillText(txt, canvas.width / 2, canvas.height / 2) 40 | 41 | el.style.backgroundImage = `url(${canvas.toDataURL('image/png')})` 42 | } 43 | -------------------------------------------------------------------------------- /public/table.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [{ 3 | "id": 1, 4 | "name": "张三", 5 | "money": 123, 6 | "address": "广东省东莞市长安镇", 7 | "state": "成功", 8 | "date": "2019-11-1", 9 | "thumb": "https://lin-xin.gitee.io/images/post/wms.png" 10 | }, 11 | { 12 | "id": 2, 13 | "name": "李四", 14 | "money": 456, 15 | "address": "广东省广州市白云区", 16 | "state": "成功", 17 | "date": "2019-10-11", 18 | "thumb": "https://lin-xin.gitee.io/images/post/node3.png" 19 | }, 20 | { 21 | "id": 3, 22 | "name": "王五", 23 | "money": 789, 24 | "address": "湖南省长沙市", 25 | "state": "失败", 26 | "date": "2019-11-11", 27 | "thumb": "https://lin-xin.gitee.io/images/post/parcel.png" 28 | }, 29 | { 30 | "id": 4, 31 | "name": "赵六", 32 | "money": 1011, 33 | "address": "福建省厦门市鼓浪屿", 34 | "state": "成功", 35 | "date": "2019-10-20", 36 | "thumb": "https://lin-xin.gitee.io/images/post/notice.png" 37 | } 38 | ], 39 | "pageTotal": 4 40 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js - Vue 3 project Jest configuration 2 | 3 | 4 | 5 | 6 | module.exports = { 7 | globals: { 8 | 'ts-jest': { 9 | useESM: true, // Enable ESM support for TypeScript 10 | }, 11 | }, 12 | testEnvironment: 'jsdom', 13 | transform: { 14 | '^.+\\.vue$': '@vue/vue3-jest', 15 | '^.+\\.js$': 'babel-jest', // Use Babel for JavaScript files 16 | '^.+\\.tsx?$': 'ts-jest', 17 | }, 18 | transformIgnorePatterns: [ 19 | 'node_modules/', 20 | ], 21 | testEnvironmentOptions: { 22 | "customExportConditions": [ 23 | "node", 24 | "node-addons" 25 | ] 26 | }, 27 | 28 | collectCoverage: true, 29 | collectCoverageFrom: [ 30 | 'src/utils/**/*.{js,ts,vue}', // Explicitly specify the file types to include in coverage reports 31 | 'src/views/**/*.{vue}', 32 | 'src/components/**/*.{vue}' 33 | ], 34 | extensionsToTreatAsEsm: ['.tsx', '.jsx', '.ts','.vue'], // File extensions to treat as ECMAScript Modules (ESM) 35 | 36 | // Set coverage threshold goals 37 | coverageThreshold: { 38 | global: { 39 | branches: 60, // 60% branch coverage 40 | functions: 90, // 90% function coverage 41 | statements: 90, // 90% statement coverage 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/svgIcon/svgIcons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 10 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/views/system/dept/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 52 | -------------------------------------------------------------------------------- /src/views/helloJest/hello.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2024-01-27 16:30:46 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 23:10:10 6 | * @FilePath: /mocha-vue3-system/src/views/helloJest/hello.spec.ts 7 | * @Description: 8 | * 9 | * 10 | */ 11 | import hello from './Hello.vue' 12 | 13 | import { shallowMount } from '@vue/test-utils' // 明确import shallowMount方法 14 | 15 | // 假设你使用"@vue/test-utils"版本为1.1.0 16 | // 假设你的Jest版本为26.6.3 17 | // 假设你的Vue版本为3.0.0 18 | 19 | describe('Hello.vue', () => { 20 | const msg = 'Hello,jest' 21 | const wrapper = shallowMount(hello, { 22 | propsData: { msg } 23 | }) 24 | const msgElement = wrapper.find('.msg')! // 使用非空断言操作符 (!) 25 | 26 | it('renders props.msg when passed', () => { 27 | // 假设.msg是用于展示msg的元素class名 28 | 29 | expect(msgElement.exists()).toBe(true) 30 | expect(msgElement.text()).toBe(msg) 31 | }) 32 | 33 | it('should hide the box element after 1000ms', async () => { 34 | const wrapper = shallowMount(hello) 35 | const box = wrapper.find('.box') 36 | // expect(wrapper.classes()).toContain('hello') 37 | expect(wrapper.vm.visible).toBe(true) 38 | await wrapper.vm.$nextTick() // wait for the timeout to trigger 39 | expect(box.classes()).not.toContain('hidden') 40 | expect(wrapper.vm.visible).toBe(false) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/directive/inputNumber.ts: -------------------------------------------------------------------------------- 1 | // 限制输入数字 2 | import { DirectiveBinding } from 'vue' 3 | 4 | interface ExHTMLElement extends HTMLElement { 5 | inputListener: EventListener 6 | } 7 | 8 | export default { 9 | mounted(el: ExHTMLElement, binding: DirectiveBinding) { 10 | const decimal = binding.value?.decimal || 2 11 | const elInput = el.getElementsByTagName('input')[0] 12 | 13 | let regDecimal: RegExp 14 | if (decimal > 0) regDecimal = new RegExp(`^\\d*(.?\\d{0,${decimal}})`, 'g') 15 | else regDecimal = new RegExp(`^\\d*`, 'g') 16 | 17 | let locking = false 18 | elInput.onkeyup = (e) => { 19 | if (locking) { 20 | return 21 | } 22 | let val = elInput.value 23 | elInput.value = 24 | val 25 | .replace(/[^\d^\.]+/g, '') 26 | .replace(/^0+(\d)/, '$1') 27 | .replace(/^\./, '0.') 28 | .match(regDecimal)[0] || '' 29 | if (val !== elInput.value && !locking) { 30 | elInput.dispatchEvent(new Event('input')) 31 | } 32 | } 33 | 34 | elInput.addEventListener('compositionstart', () => { 35 | locking = true //解决中文输入双向绑定失效 36 | }) 37 | elInput.addEventListener('compositionend', () => { 38 | locking = false //解决中文输入双向绑定失效 39 | }) 40 | }, 41 | unmounted(el: ExHTMLElement) { 42 | el.getElementsByTagName('keyup')[0].removeEventListener('input', el.inputListener) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/router/modules/editor.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/editor', 7 | name: 'editor', 8 | component: Layout, 9 | meta: { 10 | title: 'editor', 11 | icon: 'ep-document', 12 | order: 8 13 | }, 14 | children: [ 15 | { 16 | path: 'tinymce', 17 | name: 'tinymce', 18 | meta: { 19 | title: 'tinymce' 20 | }, 21 | component: () => import(/* webpackChunkName: "editor" */ '~/views/editor/Tinymce.vue') 22 | }, 23 | { 24 | path: 'wangEditor', 25 | name: 'wangEditor', 26 | meta: { 27 | title: 'wangEditor' 28 | }, 29 | component: () => import(/* webpackChunkName: "editor" */ '~/views/editor/WangEditor.vue') 30 | }, 31 | { 32 | path: 'markdown', 33 | name: 'markdown', 34 | meta: { 35 | title: 'markdownEditor' 36 | }, 37 | component: () => import(/* webpackChunkName: "editor" */ '~/views/editor/Markdown.vue') 38 | }, 39 | { 40 | path: 'codemirror', 41 | name: 'codemirror', 42 | meta: { 43 | title: 'codemirror' 44 | }, 45 | component: () => import(/* webpackChunkName: "editor" */ '~/views/editor/Codemirror.vue') 46 | } 47 | ] 48 | } 49 | ] 50 | 51 | export default routes 52 | -------------------------------------------------------------------------------- /src/views/comp/dictComp/dictTag.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 53 | -------------------------------------------------------------------------------- /src/components/MoDict.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 55 | -------------------------------------------------------------------------------- /src/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | 7 | 8 | } 9 | 10 | 11 | body { 12 | font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'microsoft yahei', arial, STHeiTi, 13 | sans-serif; 14 | font-size: 14px; 15 | 16 | svg { display: inline-block;} 17 | 18 | a { color:var(--el-color-primary)} 19 | a:hover { text-decoration: underline;} 20 | } 21 | 22 | 23 | 24 | .move-enter-active { 25 | animation: run-scale .1s ease-out 0s; 26 | } 27 | 28 | .move-leave-active { 29 | animation: run-scale .1s ease-in 0s reverse; 30 | } 31 | 32 | .move-enter-from, 33 | .move-leave-to { 34 | opacity: 0; 35 | } 36 | 37 | @keyframes run-scale { 38 | 0% { 39 | opacity: 0; 40 | } 41 | 42 | 100% { 43 | opacity: 1; 44 | } 45 | } 46 | 47 | /*BaseForm*/ 48 | 49 | .el-time-panel__content::after, 50 | .el-time-panel__content::before { 51 | margin-top: -7px; 52 | } 53 | 54 | .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { 55 | padding-bottom: 0; 56 | } 57 | 58 | [class*=' el-icon-'], 59 | [class^='el-icon-'] { 60 | speak: none; 61 | font-style: normal; 62 | font-weight: 400; 63 | font-variant: normal; 64 | text-transform: none; 65 | line-height: 1; 66 | vertical-align: baseline; 67 | display: inline-block; 68 | -webkit-font-smoothing: antialiased; 69 | -moz-osx-font-smoothing: grayscale; 70 | } 71 | 72 | [hidden] { 73 | display: none !important; 74 | } 75 | 76 | .container { margin:16px;} 77 | 78 | -------------------------------------------------------------------------------- /src/views/messageCenter/MessageList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es2021: true 7 | }, 8 | parser: 'vue-eslint-parser', 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:vue/vue3-recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:prettier/recommended' 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | parser: '@typescript-eslint/parser', 18 | sourceType: 'module', 19 | ecmaFeatures: { 20 | jsx: true 21 | } 22 | }, 23 | // eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写 24 | plugins: ['vue', '@typescript-eslint', 'prettier'], 25 | rules: { 26 | 'vue/multi-word-component-names': 'off', 27 | 'no-console': 'off', 28 | 'no-plusplus': 'off', 29 | 'import/extensions': 'off', 30 | 'import/no-unresolved': 'off', 31 | 'import/no-dynamic-require': 'off', 32 | 'global-require': 'off', 33 | 'vue/valid-v-bind-sync': 'off', 34 | 'no-underscore-dangle': 'off', 35 | 'no-param-reassign': 'off', 36 | 'prefer-destructuring': 'off', 37 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 38 | 'no-unused-expressions': ['error', { allowShortCircuit: true }], 39 | 'vue/html-self-closing': [ 40 | 'error', 41 | { 42 | html: { 43 | void: 'always', 44 | normal: 'never', 45 | component: 'always' 46 | }, 47 | svg: 'always', 48 | math: 'always' 49 | } 50 | ] 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/views/feat/icons/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 43 | -------------------------------------------------------------------------------- /src/views/messageCenter/NoticeList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /package copy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-admin", 3 | "version": "1.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@element-plus/icons-vue": "^2.1.0", 12 | "@vueuse/components": "^9.10.0", 13 | "@vueuse/core": "^9.10.0", 14 | "axios": "^0.27.2", 15 | "echarts": "^5.4.2", 16 | "element-plus": "^2.2.14", 17 | "md-editor-v3": "^2.2.1", 18 | "pinia": "^2.0.20", 19 | "pinia-plugin-persistedstate": "^3.1.0", 20 | "svg-sprite-loader": "^6.0.11", 21 | "vue": "^3.2.37", 22 | "vue-cropperjs": "^5.0.0", 23 | "vue-router": "^4.1.3", 24 | "wangeditor": "^4.7.15", 25 | "xlsx": "^0.18.5" 26 | }, 27 | "devDependencies": { 28 | "@iconify-json/ant-design": "^1.1.5", 29 | "@iconify-json/el": "^1.1.4", 30 | "@iconify-json/ep": "^1.1.10", 31 | "@types/node": "^18.11.18", 32 | "@vitejs/plugin-vue": "^3.0.0", 33 | "@vue/compiler-sfc": "^3.1.2", 34 | "autoprefixer": "^10.4.14", 35 | "iconify-icon": "^1.0.7", 36 | "postcss": "^8.4.23", 37 | "sass": "^1.57.1", 38 | "tailwindcss": "^3.3.1", 39 | "typescript": "^4.6.4", 40 | "unplugin-auto-import": "^0.11.2", 41 | "unplugin-icons": "^0.16.1", 42 | "unplugin-vue-components": "^0.22.4", 43 | "vite": "^3.0.0", 44 | "vite-plugin-vue-setup-extend": "^0.4.0", 45 | "vue-tsc": "^0.38.4" 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not dead" 51 | ] 52 | } -------------------------------------------------------------------------------- /src/router/modules/level.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import Layout from '~/layout/index.vue' 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/level', 7 | name: 'level', 8 | component: Layout, 9 | redirect: '/level/menu1', 10 | meta: { 11 | title: 'level', 12 | icon: 'ep-connection', 13 | order: 10 14 | }, 15 | children: [ 16 | { 17 | path: 'menu1', 18 | name: 'menu1', 19 | meta: { 20 | title: 'menu1' 21 | }, 22 | children: [ 23 | { 24 | path: 'menu1-1', 25 | name: 'menu1-1', 26 | meta: { 27 | title: 'menu11' 28 | }, 29 | component: () => import('~/views/level/Menu1.vue') 30 | } 31 | ] 32 | }, 33 | { 34 | path: 'menu2', 35 | name: 'menu2', 36 | meta: { 37 | title: 'menu2' 38 | }, 39 | children: [ 40 | { 41 | path: 'menu2-1', 42 | name: 'menu2-1', 43 | meta: { 44 | title: 'menu21' 45 | }, 46 | 47 | children: [ 48 | { 49 | path: 'menu2-1-1', 50 | name: 'menu2-1-1', 51 | meta: { 52 | title: 'menu211' 53 | }, 54 | component: () => import('~/views/level/Menu2.vue') 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | ] 63 | 64 | export default routes 65 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/SearchScheme.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 57 | -------------------------------------------------------------------------------- /src/views/comp/upload/upload.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 46 | 47 | 59 | -------------------------------------------------------------------------------- /src/views/messageCenter/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const modules = import.meta.glob('./modules/**/*.ts', { eager: true }) 4 | 5 | let routeModuleList: RouteRecordRaw[] = [] 6 | 7 | // 获取模块路由 8 | Object.values(modules).forEach((key: any) => { 9 | const mod = key.default || [] 10 | const modList = Array.isArray(mod) ? [...mod] : [mod] 11 | routeModuleList.push(...modList) 12 | }) 13 | 14 | const constantRoutes: RouteRecordRaw[] = [ 15 | { 16 | path: '/', 17 | name: 'Home', 18 | redirect: '/dashboard', 19 | hidden: true, 20 | meta: { 21 | title: 'home' 22 | } 23 | }, 24 | { 25 | path: '/login', 26 | name: 'Login', 27 | hidden: true, 28 | meta: { 29 | title: 'signIn' 30 | }, 31 | component: () => import(/* webpackChunkName: "login" */ '../views/login/login.vue') 32 | }, 33 | { 34 | path: '/403', 35 | name: '403', 36 | hidden: true, 37 | meta: { 38 | title: '没有权限' 39 | }, 40 | component: () => import(/* webpackChunkName: "400" */ '../views/403.vue') 41 | } 42 | ] 43 | const lastRoutes = [ 44 | { 45 | path: '/:pathMatch(.*)*', 46 | name: '404', 47 | hidden: true, 48 | meta: { 49 | title: '404' 50 | }, 51 | component: () => import(/* webpackChunkName: "400" */ '../views/404.vue') 52 | } 53 | ] 54 | 55 | let routes = constantRoutes 56 | 57 | // 前端固定路由模式 58 | if (import.meta.env.VITE_PERMISSION_MODE === 'CONSTANT') { 59 | routes = [...routeModuleList, ...constantRoutes] 60 | } 61 | 62 | const router = createRouter({ 63 | history: createWebHashHistory(), 64 | routes 65 | }) 66 | 67 | export default router 68 | export { constantRoutes, routeModuleList, lastRoutes } 69 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "home": "首页", 4 | "about": "关于", 5 | "tobeCoded": "待开发", 6 | "signIn": "登录", 7 | "signUp": "注册" 8 | }, 9 | "route": { 10 | "dashboard": "仪表盘", 11 | "analysis": "分析", 12 | "workbench": "工作台", 13 | "feats": "功能", 14 | "download": "下载", 15 | "media": "媒体", 16 | "previewPDF": "预览PDF", 17 | "videojs": "视频播放", 18 | "GSAP": "GSAP动画", 19 | "customDirectives": "自定义指令", 20 | "adaptiveHeight": "自适应高度", 21 | "watermark": "水印指令", 22 | "avatar": "头像", 23 | "infinite": "无限滚动", 24 | "table": "表格", 25 | "basicTable": "基础表格", 26 | "seniorSearch": "高级搜索", 27 | "accrossTable": "跨页选择", 28 | "form": "表单", 29 | "basicForm": "基础表单", 30 | "upload": "上传", 31 | "icons": "Icons", 32 | "svg": "SVG", 33 | "canvas": "Canvas", 34 | "tab": "Tab", 35 | "components": "组件", 36 | "pages": "页面", 37 | "permission": "权限控制", 38 | "system": "系统管理", 39 | "accountManagement": "账号管理", 40 | "roleManagement": "角色管理", 41 | "menuManagement": "菜单管理", 42 | "deptManagement": "部门管理", 43 | "editPassword": "修改密码", 44 | "personalCenter": "个人中心", 45 | "messageCenter": "消息中心", 46 | "editor": "编辑器", 47 | "tinymce": "Tinymce", 48 | "wangEditor": "Wang Editor", 49 | "markdownEditor": "Markdown编辑器", 50 | "codemirror": "Codemirror", 51 | "chart": "图表", 52 | "eChart": "eChart", 53 | "map": "地图", 54 | "level": "多级菜单", 55 | "menu1": "Menu 1", 56 | "menu11": "Menu 1-1", 57 | "menu2": "Menu 2", 58 | "menu21": "Menu 2-1", 59 | "menu211": "Menu 2-1-1", 60 | "about": "关于", 61 | "demo": "示例", 62 | "jsx": "jsx", 63 | "provideAndInject": "依赖注入", 64 | "cssHighlight": "CSS 高亮" 65 | } 66 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2023-04-23 13:20:11 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 22:56:39 6 | * @FilePath: /mocha-vue3-system/src/utils/index.ts 7 | * @Description: 8 | * 9 | * 10 | */ 11 | /** 12 | * @param {string} url 13 | * @returns {Object} 14 | */ 15 | export function param2Obj(url: string) { 16 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 17 | if (!search) { 18 | return {} 19 | } 20 | const obj = {} 21 | const searchArr = search.split('&') 22 | searchArr.forEach((v) => { 23 | const index = v.indexOf('=') 24 | if (index !== -1) { 25 | const name = v.substring(0, index) 26 | const val = v.substring(index + 1, v.length) 27 | obj[name] = val 28 | } 29 | }) 30 | return obj 31 | } 32 | 33 | export function deepClone(target: object): any { 34 | // 定义一个变量 35 | let result 36 | // 如果当前需要深拷贝的是一个对象的话 37 | if (typeof target === 'object') { 38 | // 如果是一个数组的话 39 | if (Array.isArray(target)) { 40 | result = [] // 将result赋值为一个数组,并且执行遍历 41 | for (let i in target) { 42 | // 递归克隆数组中的每一项 43 | result.push(deepClone(target[i])) 44 | } 45 | // 判断如果当前的值是null的话;直接赋值为null 46 | } else if (target === null) { 47 | result = null 48 | // 判断如果当前的值是一个RegExp对象的话,直接赋值 49 | } else if (target.constructor === RegExp) { 50 | result = target 51 | } else { 52 | // 否则是普通对象,直接for in循环,递归赋值对象的所有值 53 | result = {} 54 | for (let i in target) { 55 | result[i] = deepClone(target[i]) 56 | } 57 | } 58 | // 如果不是对象的话,就是基本数据类型,那么直接赋值 59 | } else { 60 | result = target 61 | } 62 | // 返回最终结果 63 | return result 64 | } 65 | -------------------------------------------------------------------------------- /src/directive/resizable.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding, ref, unref } from 'vue' 2 | 3 | interface ExHTMLElement extends HTMLElement { 4 | resizeListener: EventListener 5 | } 6 | 7 | export default { 8 | mounted: (el: ExHTMLElement, binding: DirectiveBinding) => { 9 | el.resizeListener = () => { 10 | setHeight(el, binding) 11 | } 12 | 13 | setHeight(el, binding) 14 | 15 | observeElementSize(el) 16 | window.addEventListener('resize', el.resizeListener) 17 | }, 18 | unmounted(el: ExHTMLElement) { 19 | unobserveElementSize() 20 | window.removeEventListener('resize', el.resizeListener) 21 | }, 22 | updated(el: ExHTMLElement, binding: DirectiveBinding) { 23 | observeElementSize(el) 24 | setHeight(el, binding) 25 | } 26 | } 27 | 28 | // set el-table height 29 | function setHeight(el: ExHTMLElement, binding: DirectiveBinding) { 30 | const top = el.offsetTop 31 | const bottom = binding?.value?.bottom || 84 32 | const pageHeight = window.innerHeight 33 | el.style.height = pageHeight - top - bottom + 'px' 34 | el.style.overflowY = 'auto' 35 | } 36 | 37 | const width = ref(0) 38 | const height = ref(0) 39 | let resizeObserver: ResizeObserver | null 40 | 41 | const observeElementSize = (element: HTMLElement) => { 42 | if (element) { 43 | resizeObserver = new ResizeObserver((entries) => { 44 | for (let entry of entries) { 45 | width.value = entry.contentRect.width 46 | height.value = entry.contentRect.height 47 | // console.log('width, height', width.value, height.value) 48 | } 49 | }) 50 | resizeObserver.observe(element) 51 | } 52 | } 53 | 54 | const unobserveElementSize = () => { 55 | if (resizeObserver) { 56 | resizeObserver.disconnect() 57 | resizeObserver = null 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/layout/vertical/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 17 | 18 | 77 | -------------------------------------------------------------------------------- /src/views/editor/Codemirror.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 57 | -------------------------------------------------------------------------------- /src/store/dict.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | import systemApi from '~/api/system' 4 | 5 | interface PromiseRequestMap { 6 | [key: string]: Promise | null 7 | } 8 | 9 | interface DictMap { 10 | [key: string]: [] 11 | } 12 | 13 | export const useDictStore = defineStore( 14 | 'dict', 15 | () => { 16 | const dicts = ref({}) 17 | 18 | const getDictData = async (dictType: string, refresh: boolean = false) => { 19 | return new Promise((resolve, reject) => { 20 | let data: any = dicts.value[dictType] 21 | if (data && !refresh) { 22 | try { 23 | resolve(data) 24 | } catch (e) { 25 | reject(e) 26 | } 27 | } else { 28 | const p = handleRepeatedRequest(dictType, new Date().getTime()) 29 | resolve(p) 30 | } 31 | }) 32 | } 33 | 34 | // 处理重复的 Promise 请求 35 | 36 | let promiseRecords = {} 37 | 38 | const handleRepeatedRequest = (key: string, timestamp: number) => { 39 | if (!promiseRecords[key]) { 40 | console.log('no repeated request') 41 | promiseRecords[key] = systemApi 42 | .getDicts(key) 43 | .then((res: any) => { 44 | // 存入缓存 45 | dicts.value[key] = res 46 | return res 47 | }) 48 | .catch((e: Error) => {}) 49 | .finally(() => { 50 | // 请求完毕,重置,否则会一直保留该promise 51 | promiseRecords[key] = null 52 | }) 53 | } else { 54 | console.log('already has repeated request') 55 | } 56 | return promiseRecords[key] 57 | } 58 | 59 | return { dicts, getDictData } 60 | }, 61 | { 62 | persist: { 63 | storage: sessionStorage 64 | } 65 | } 66 | ) 67 | -------------------------------------------------------------------------------- /src/components/svgIcon/svg.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, readdirSync } from 'fs' 2 | let idPerfix = '' 3 | const svgTitle = /+].*?)>/ 4 | const clearHeightWidth = /(width|height)="([^>+].*?)"/g 5 | const hasViewBox = /(viewBox="[^>+].*?")/g 6 | const clearReturn = /(\r)|(\n)/g 7 | // 查找svg文件 8 | const svgFind = (e: any): any => { 9 | const arr = [] 10 | const dirents = readdirSync(e, { withFileTypes: true }) 11 | for (const dirent of dirents) { 12 | if (dirent.isDirectory()) arr.push(...svgFind(e + dirent.name + '/')) 13 | else { 14 | const svg = readFileSync(e + dirent.name) 15 | .toString() 16 | .replace(clearReturn, '') 17 | .replace(svgTitle, ($1, $2) => { 18 | let width = 0, 19 | height = 0, 20 | content = $2.replace(clearHeightWidth, (s1: any, s2: any, s3: any) => { 21 | if (s2 === 'width') width = s3 22 | else if (s2 === 'height') height = s3 23 | return '' 24 | }) 25 | if (!hasViewBox.test($2)) content += `viewBox="0 0 ${width} ${height}"` 26 | return `` 27 | }) 28 | .replace('', '') 29 | arr.push(svg) 30 | } 31 | } 32 | return arr 33 | } 34 | // 生成svg 35 | export const createSvg = (path: any, perfix = 'icon') => { 36 | if (path === '') return 37 | idPerfix = perfix 38 | const res = svgFind(path) 39 | return { 40 | name: 'svg-transform', 41 | transformIndexHtml(dom: String) { 42 | return dom.replace( 43 | '', 44 | `${res.join( 45 | '' 46 | )}` 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/directive/copy.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | import { copyText } from 'vue3-clipboard' 3 | import { ElMessage } from 'element-plus' 4 | 5 | interface ExHTMLElement extends HTMLElement { 6 | clickListener: EventListener 7 | trigger?: HTMLElement 8 | } 9 | 10 | // 复制图标 11 | const svg = 12 | '' 13 | 14 | export default { 15 | mounted(el: ExHTMLElement, binding: DirectiveBinding) { 16 | // 动态增加复制图标 17 | el.trigger = document.createElement('span') 18 | el.trigger.style.marginLeft = '4px' 19 | el.trigger.style.cursor = 'pointer' 20 | el.trigger.innerHTML = svg 21 | 22 | // 复制图标的位置 23 | if (binding.value?.position === 'out') el.after(el.trigger) 24 | else el.append(el.trigger) 25 | 26 | el.clickListener = () => { 27 | const text = el.innerText 28 | copyText(text, undefined, (error: string, event: Event) => { 29 | if (error) { 30 | ElMessage({ type: 'error', message: '未能复制', duration: 2000 }) 31 | console.log(error) 32 | } else { 33 | ElMessage({ type: 'success', message: '复制成功', duration: 2000 }) 34 | console.log(event) 35 | } 36 | }) 37 | } 38 | el.trigger.addEventListener('click', el.clickListener) 39 | }, 40 | 41 | unmounted(el: ExHTMLElement) { 42 | el.trigger?.removeEventListener('resize', el.clickListener) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/layout/default/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 19 | 20 | 77 | -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | import { login } from '~/api/user' 4 | import { UserInfo } from '~/types/index' 5 | import { RouteRecordRaw } from 'vue-router' 6 | import { routeModuleList } from '~/router' 7 | import router from '~/router' 8 | import systemApi from '~/api/system' 9 | 10 | export const useUserStore = defineStore( 11 | 'user', 12 | () => { 13 | let userid = ref() 14 | let username = ref('') 15 | let permiss = ref(['btn_more', 'btn-edit', 'btn-delelte']) 16 | let role = ref('') 17 | let routes = ref() 18 | 19 | // 用户登录 20 | async function userLogin(param: UserInfo) { 21 | const res = await login({ 22 | username: param.username, 23 | password: param.password 24 | }) 25 | 26 | if (res.userid) { 27 | setUserInfo(res) 28 | 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | function setUserInfo(userData: UserInfo) { 35 | userid.value = userData.userid 36 | username.value = userData.username 37 | userData.permiss && (permiss.value = userData.permiss) 38 | userData.role && (role.value = userData.role) 39 | } 40 | 41 | // 获取动态路由 42 | function setAsyncRoutes(data: RouteRecordRaw[]) { 43 | routes.value = data 44 | } 45 | 46 | const menuRoutes = computed(() => routes.value.filter((item: RouteRecordRaw) => !item.hidden)) 47 | 48 | // 退出登录 49 | function userLogout() { 50 | username.value = '' 51 | role.value = '' 52 | permiss.value = [] 53 | routes.value = [] 54 | } 55 | 56 | return { 57 | userid, 58 | username, 59 | permiss, 60 | role, 61 | routes, 62 | menuRoutes, 63 | userLogin, 64 | userLogout, 65 | setUserInfo, 66 | setAsyncRoutes 67 | } 68 | }, 69 | { 70 | persist: true 71 | } 72 | ) 73 | -------------------------------------------------------------------------------- /src/views/dashboard/components/CountCard.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /src/views/editor/WangEditor.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 71 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | // pinia 4 | import { createPinia } from 'pinia' 5 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 6 | const pinia = createPinia() 7 | pinia.use(piniaPluginPersistedstate) 8 | 9 | import App from './App.vue' 10 | import router from './router' 11 | 12 | import './assets/css/index.scss' 13 | import './assets/css/element/index.scss' 14 | 15 | import 'element-plus/theme-chalk/dark/css-vars.css' 16 | 17 | // 自定义主题方案 18 | import './assets/css/theme.css' 19 | 20 | const app = createApp(App) 21 | 22 | // i18n 23 | import i18n from './locales' 24 | app.use(i18n) 25 | 26 | app.use(pinia) 27 | app.use(router) 28 | 29 | import '~/router/permission' 30 | 31 | // 在 main.ts文件中设置svg-icon为全局组件 32 | import svgIcon from '~/components/svgIcon/index.vue' 33 | app.component('svg-icon', svgIcon) 34 | import 'iconify-icon' 35 | 36 | // 注册指令 37 | import directive from './directive' 38 | directive(app) 39 | 40 | // theme 41 | import initTheme from '~/layout/theme' 42 | initTheme(pinia) 43 | 44 | // 引入VForm 设计器需全局引入Element Plus 45 | // import ElementPlus from 'element-plus' //引入element-plus库 46 | // import 'element-plus/dist/index.css' //引入element-plus样式 47 | // // 引入并全局注册VForm 3组件 48 | // import VForm3 from 'vform3-builds' //引入VForm 3库 49 | // import 'vform3-builds/dist/designer.style.css' //引入VForm3样式 50 | // app.use(ElementPlus) //全局注册element-plus 51 | // app.use(VForm3) 52 | 53 | // 应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项, 54 | // 例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误: 55 | app.config.errorHandler = (err, instance, info) => { 56 | console.error('error ---', err) 57 | console.error('instance ---', instance) 58 | console.error('info ---', info) 59 | } 60 | // 为 Vue 的运行时警告指定一个自定义处理函数 61 | app.config.warnHandler = (msg, instance, trace) => { 62 | // `trace` is the component hierarchy trace 63 | } 64 | 65 | app.config.globalProperties.$globalMsg = 'hello' 66 | 67 | app.provide('$globalName', 'mocha-vue3-admin') 68 | app.mount('#app') 69 | -------------------------------------------------------------------------------- /src/views/system/About.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/SchemeDialog.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 69 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "home": "home", 4 | "about": "About", 5 | "tobeCoded": "To be coded", 6 | "signIn": "sign in", 7 | "signUp": "sign out" 8 | }, 9 | "route": { 10 | "dashboard": "Dashboard", 11 | "analysis": "Analysis", 12 | "workbench": "Workbench", 13 | "feats": "Feats", 14 | "download": "Download", 15 | "media": "Media", 16 | "previewPDF": "Priview PDF", 17 | "videojs": "Video Player", 18 | "GSAP": "GSAP", 19 | "customDirectives": "Custom Directives", 20 | "adaptiveHeight": "Adaptive Height", 21 | "watermark": "Watermark", 22 | "avatar": "avatar", 23 | "infinite": "Infinite Scroll", 24 | "table": "Table", 25 | "basicTable": "Basic table", 26 | "seniorSearch": "Senior Search", 27 | "accrossTable": "Accross Select", 28 | "form": "Form", 29 | "basicForm": "Basic form", 30 | "upload": "Upload", 31 | "icons": "Icons", 32 | "svg": "SVG", 33 | "canvas": "Canvas", 34 | "tab": "Tab", 35 | "components": "Components", 36 | "pages": "Pages", 37 | "permission": "Permission", 38 | "system": "System", 39 | "accountManagement": "Account Management", 40 | "roleManagement": "Role Management", 41 | "menuManagement": "Menu Management", 42 | "deptManagement": "Dept Management", 43 | "editPassword": "Edit Password", 44 | "personalCenter": "Personal Center", 45 | "messageCenter": "Message Center", 46 | "editor": "Editor", 47 | "tinymce": "Tinymce", 48 | "wangEditor": "Wang Editor", 49 | "markdownEditor": "Markdown Editor", 50 | "codemirror": "Codemirror", 51 | "chart": "Chart", 52 | "eChart": "eChart", 53 | "map": "map", 54 | "level": "Level", 55 | "menu1": "Menu 1", 56 | "menu11": "Menu 1-1", 57 | "menu2": "Menu 2", 58 | "menu21": "Menu 2-1", 59 | "menu211": "Menu 2-1-1", 60 | "about": "About", 61 | "demo": "Demo", 62 | "jsx": "jsx", 63 | "provideAndInject": "Provide/Inject", 64 | "cssHighlight": "CSS Highlight" 65 | } 66 | } -------------------------------------------------------------------------------- /src/components/EditableInput.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 79 | -------------------------------------------------------------------------------- /src/components/UserSelector.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 90 | -------------------------------------------------------------------------------- /src/router/modules/system.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import { RoleEnum } from '~/enums/roleEnum' 3 | import Layout from '~/layout/index.vue' 4 | 5 | const routes: RouteRecordRaw[] = [ 6 | { 7 | path: '/system', 8 | name: 'system', 9 | component: Layout, 10 | meta: { 11 | title: 'system', 12 | icon: 'ep-setting', 13 | order: 5 14 | }, 15 | redirect: '/system/account', 16 | children: [ 17 | { 18 | path: 'account', 19 | name: 'account', 20 | meta: { 21 | title: 'accountManagement', 22 | roles: [RoleEnum.USER] 23 | }, 24 | component: () => import('~/views/system/account/index.vue') 25 | }, 26 | { 27 | path: 'role', 28 | name: 'role', 29 | meta: { 30 | title: 'roleManagement', 31 | roles: [RoleEnum.SUPER] 32 | }, 33 | component: () => import('~/views/system/role/index.vue') 34 | }, 35 | 36 | { 37 | path: 'menu', 38 | name: 'menu', 39 | meta: { 40 | title: 'menuManagement' 41 | }, 42 | component: () => import('~/views/system/menu/index.vue') 43 | }, 44 | { 45 | path: 'dept', 46 | name: 'dept', 47 | meta: { 48 | title: 'deptManagement', 49 | roles: [RoleEnum.SUPER] 50 | }, 51 | component: () => import('~/views/system/dept/index.vue') 52 | }, 53 | { 54 | path: 'password', 55 | name: 'password', 56 | meta: { 57 | title: 'editPassword', 58 | roles: [RoleEnum.USER] 59 | }, 60 | component: () => import('~/views/system/password/index.vue') 61 | }, 62 | { 63 | path: 'profile', 64 | name: 'profile', 65 | meta: { 66 | title: 'personalCenter', 67 | roles: [RoleEnum.USER] 68 | }, 69 | component: () => import('~/views/system/profile/index.vue') 70 | }, 71 | { 72 | path: 'messageCenter', 73 | name: 'messageCenter', 74 | meta: { 75 | title: 'messageCenter' 76 | }, 77 | component: () => import('~/views/messageCenter/index.vue') 78 | } 79 | ] 80 | } 81 | ] 82 | 83 | export default routes 84 | -------------------------------------------------------------------------------- /src/utils/http/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig } from 'axios' 2 | import { errorHandler, errorMsgHandler } from './errorHandler' 3 | 4 | import qs from 'qs' 5 | 6 | declare module 'axios' { 7 | export interface AxiosRequestConfig { 8 | isReturnNativeData?: boolean 9 | errorMode?: string 10 | repeatRequest?: boolean 11 | } 12 | } 13 | 14 | let pendingMap = new Map() 15 | 16 | function getRequestKey(config: AxiosRequestConfig) { 17 | return ( 18 | (config.method || '') + 19 | config.url + 20 | '?' + 21 | qs.stringify(config?.data) + 22 | qs.stringify(config?.params) 23 | ) 24 | } 25 | 26 | function setPendingMap(config: AxiosRequestConfig) { 27 | const controller = new AbortController() 28 | config.signal = controller.signal 29 | const key = getRequestKey(config) 30 | if (pendingMap.has(key)) { 31 | pendingMap.get(key).abort() 32 | pendingMap.delete(key) 33 | } else { 34 | pendingMap.set(key, controller) 35 | } 36 | } 37 | 38 | const service: AxiosInstance = axios.create({ 39 | timeout: 1000 * 30, 40 | baseURL: import.meta.env.VITE_BASE_URL 41 | }) 42 | 43 | service.interceptors.request.use( 44 | (config) => { 45 | if (!config.repeatRequest) { 46 | setPendingMap(config) 47 | } 48 | return config 49 | }, 50 | (error: AxiosError) => { 51 | console.log(error) 52 | return Promise.reject() 53 | } 54 | ) 55 | 56 | service.interceptors.response.use((response: AxiosResponse) => { 57 | const config = response.config 58 | const key = getRequestKey(config) 59 | pendingMap.delete(key) 60 | 61 | if (response.status === 200) { 62 | if (config?.isReturnNativeData) { 63 | return response.data 64 | } else { 65 | const { result, code, message } = response.data 66 | 67 | if (code === 200) { 68 | return result 69 | } else { 70 | errorHandler(message || errorMsgHandler(code), config.errorMode) 71 | } 72 | } 73 | } else { 74 | const errMsg = errorMsgHandler(response.status) 75 | // errorHandler(errMsg, config.errorMode) 76 | Promise.reject() 77 | } 78 | }) 79 | 80 | // 错误处理 81 | service.interceptors.response.use(undefined, (e) => { 82 | errorHandler(e?.response?.status || '') 83 | }) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /src/components/MoOverTooltip.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 79 | 80 | 96 | -------------------------------------------------------------------------------- /src/components/MoDict.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2024-01-27 23:27:44 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 23:32:15 6 | * @FilePath: /mocha-vue3-system/src/components/MoDict.spec.ts 7 | * @Description: 8 | * 9 | * 10 | */ 11 | import { shallowMount } from '@vue/test-utils' 12 | import MoDict from './MoDict.vue' 13 | 14 | describe('MoDict', () => { 15 | it('should render correct tag type', () => { 16 | const wrapper = shallowMount(MoDict, { 17 | props: { 18 | value: 'some value', 19 | dicts: [ 20 | { 21 | value: 'some value', 22 | label: '1', 23 | type: 'primary', 24 | class: 'tag-primary', 25 | effect: 'light' 26 | }, 27 | { value: 'some other value', label: '2', type: '', class: 'tag-danger', effect: 'dark' }, 28 | { value: 'some different value', label: '3', type: 'success', class: '', effect: 'light' } 29 | ] 30 | } 31 | }) 32 | expect(wrapper.find('el-tag').attributes('type')).toBe('primary') 33 | expect(wrapper.find('el-tag').attributes('class')).toBe('tag-primary') 34 | expect(wrapper.find('el-tag').attributes('effect')).toBe('light') 35 | }) 36 | 37 | it('should render default tag type if not found in dicts', () => { 38 | const wrapper = shallowMount(MoDict, { 39 | props: { 40 | value: 'nonexistent value', 41 | dicts: [ 42 | { 43 | value: 'some value', 44 | label: '1', 45 | type: 'primary', 46 | class: 'tag-primary', 47 | effect: 'light' 48 | }, 49 | { value: 'some other value', label: '2', type: '', class: 'tag-danger', effect: 'dark' }, 50 | { value: 'some different value', label: '3', type: 'success', class: '', effect: 'light' } 51 | ] 52 | } 53 | }) 54 | expect(wrapper.find('el-tag').attributes('type')).toBe('') 55 | expect(wrapper.find('el-tag').attributes('class')).toBe('') 56 | expect(wrapper.find('el-tag').attributes('effect')).toBe('light') 57 | }) 58 | 59 | it('should render correct tag type if only value is provided', () => { 60 | const wrapper = shallowMount(MoDict, { 61 | props: { 62 | value: 'some value' 63 | } 64 | }) 65 | expect(wrapper.find('el-tag').attributes('type')).toBe('') 66 | expect(wrapper.find('el-tag').attributes('class')).toBe('') 67 | expect(wrapper.find('el-tag').attributes('effect')).toBe('light') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/views/feat/direct/index.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-vue3-admin", 3 | "version": "1.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview", 9 | "test": "jest" 10 | }, 11 | "dependencies": { 12 | "@codemirror/lang-css": "^6.2.0", 13 | "@codemirror/lang-html": "^6.4.3", 14 | "@codemirror/lang-javascript": "^6.1.8", 15 | "@codemirror/lang-python": "^6.1.2", 16 | "@codemirror/theme-one-dark": "^6.1.2", 17 | "@element-plus/icons-vue": "^2.1.0", 18 | "@tinymce/tinymce-vue": "^5.1.0", 19 | "@vueuse/components": "^9.10.0", 20 | "@vueuse/core": "^9.10.0", 21 | "@wangeditor/editor-for-vue": "^5.1.12", 22 | "axios": "^0.27.2", 23 | "dayjs": "^1.11.7", 24 | "echarts": "^5.4.2", 25 | "element-plus": "^2.5.0", 26 | "gsap": "^3.11.5", 27 | "md-editor-v3": "^2.2.1", 28 | "pinia": "^2.0.20", 29 | "pinia-plugin-persistedstate": "^3.1.0", 30 | "pnpm": "^8.15.1", 31 | "qs": "^6.11.2", 32 | "svg-sprite-loader": "^6.0.11", 33 | "vform3-builds": "^3.0.10", 34 | "vue": "^3.3.4", 35 | "vue-codemirror": "^6.1.1", 36 | "vue-cropperjs": "^5.0.0", 37 | "vue-i18n": "^9.2.2", 38 | "vue-pdf-embed": "^1.1.6", 39 | "vue-router": "^4.1.3", 40 | "vue3-clipboard": "^1.0.0", 41 | "wangeditor": "^4.7.15", 42 | "xlsx": "^0.18.5" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.23.9", 46 | "@babel/preset-env": "^7.23.9", 47 | "@babel/preset-typescript": "^7.23.3", 48 | "@iconify-json/ant-design": "^1.1.5", 49 | "@iconify-json/el": "^1.1.4", 50 | "@iconify-json/ep": "^1.1.10", 51 | "@types/codemirror": "^5.60.7", 52 | "@types/jest": "^29.5.11", 53 | "@types/node": "^18.11.18", 54 | "@types/qs": "^6.9.7", 55 | "@vitejs/plugin-vue": "^4.2.0", 56 | "@vitejs/plugin-vue-jsx": "^3.0.1", 57 | "@vue/babel-preset-app": "^5.0.8", 58 | "@vue/compiler-sfc": "^3.1.2", 59 | "@vue/test-utils": "^2.4.0-alpha.2", 60 | "@vue/vue3-jest": "^29.2.6", 61 | "autoprefixer": "^10.4.14", 62 | "iconify-icon": "^1.0.7", 63 | "jest": "^29.7.0", 64 | "jest-environment-jsdom": "^29.7.0", 65 | "postcss": "^8.4.23", 66 | "sass": "^1.63.6", 67 | "tailwindcss": "^3.3.1", 68 | "ts-jest": "^29.1.2", 69 | "typescript": "^4.6.4", 70 | "unplugin-auto-import": "^0.11.2", 71 | "unplugin-icons": "^0.16.1", 72 | "unplugin-vue-components": "^0.22.4", 73 | "vite": "^4.3.5", 74 | "vite-plugin-vue-setup-extend": "^0.4.0", 75 | "vue-tsc": "^0.38.4" 76 | }, 77 | "browserslist": [ 78 | "> 1%", 79 | "last 2 versions", 80 | "not dead" 81 | ] 82 | } -------------------------------------------------------------------------------- /src/views/feat/highlight.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 88 | 89 | 95 | -------------------------------------------------------------------------------- /src/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | import { ref, reactive, onMounted } from 'vue' 2 | import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' 3 | 4 | export function useTable( 5 | loadDataFunc: Function, 6 | queryForm: {}, 7 | deleteDataFunc?: Function, 8 | options?: { 9 | immediate?: boolean 10 | } 11 | ) { 12 | let loading = ref(true) 13 | let tableData = ref() 14 | let total = ref(0) 15 | 16 | const pagination = reactive({ 17 | page: 1, 18 | pageSize: 20 19 | }) 20 | 21 | const getData = async () => { 22 | loading.value = true 23 | const res = await loadDataFunc({ ...queryForm, ...pagination }) 24 | console.log(res, pagination) 25 | tableData.value = res.list 26 | total.value = res.total 27 | loading.value = false 28 | } 29 | 30 | onMounted(() => { 31 | console.log(options?.immediate, options?.immediate !== false) 32 | if (options?.immediate === undefined || options?.immediate === true) getData() 33 | }) 34 | 35 | // 搜索 36 | const handleSearch = () => { 37 | pagination.page = 1 38 | getData() 39 | } 40 | 41 | // 切换页码 42 | const handlePageChange = (val: number) => { 43 | pagination.page = val 44 | getData() 45 | } 46 | 47 | let multipleSelection = ref([]) 48 | const handleSelectionChange = (val: []) => { 49 | multipleSelection.value = val 50 | } 51 | 52 | // 单个删除、批量删除 53 | const handleDelete = (id?: string) => { 54 | let ids: string[] | null = null 55 | if (id !== undefined) { 56 | ids = [id] 57 | } else { 58 | ids = multipleSelection.value 59 | } 60 | if (!ids || ids.length === 0) { 61 | ElMessage.warning('请选择需要删除的数据!') 62 | return 63 | } 64 | ElMessageBox.confirm('确定删除?此操作不可恢复。', '提示', { 65 | type: 'warning' 66 | }) 67 | .then(async () => { 68 | await deleteDataFunc({ ids }) 69 | 70 | ElMessage.success('删除成功') 71 | getData() 72 | }) 73 | .catch(() => {}) 74 | } 75 | 76 | // 导出表格数据 77 | const handleExport = () => { 78 | ElMessageBox.confirm('确定导出所选数据?', '提示') 79 | .then(() => { 80 | ElNotification({ 81 | title: '数据导出', 82 | message: '正在为您导出数据,请稍后', 83 | position: 'bottom-right' 84 | }) 85 | }) 86 | .catch(() => {}) 87 | } 88 | 89 | return { 90 | loading, 91 | tableData, 92 | total, 93 | pagination, 94 | multipleSelection, 95 | getData, 96 | handleSearch, 97 | handleExport, 98 | handleSelectionChange, 99 | handlePageChange, 100 | handleDelete 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/views/feat/Avatar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 100 | -------------------------------------------------------------------------------- /src/views/comp/table/seniorSearchTable/SearchItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 96 | -------------------------------------------------------------------------------- /src/utils/permission.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2023-05-31 15:27:55 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-01-27 22:59:01 6 | * @FilePath: /mocha-vue3-system/src/utils/permission.ts 7 | * @Description: 8 | * 9 | * 10 | */ 11 | 12 | import { RouteComponent, RouteMeta, RouteRecordRaw } from 'vue-router' 13 | import systemApi from '~/api/system' 14 | import Layout from '~/layout/index.vue' 15 | import { routeModuleList } from '~/router' 16 | import { useUserStore } from '~/store/user' 17 | 18 | // 后端接口返回的路由元素 19 | interface AsyncRouteItem { 20 | path: string 21 | name: string 22 | component: string | RouteComponent 23 | meta?: RouteMeta 24 | children?: AsyncRouteItem[] 25 | } 26 | 27 | const modules = import.meta.glob('~/views/**/**.vue') 28 | 29 | // 后端路由模式 30 | async function getBackAsyncRoutes() { 31 | const useUser = useUserStore() 32 | return await systemApi.getRoutes({ userid: useUser.userid }) 33 | } 34 | 35 | // 把接口路由组装成前端可用结构 36 | function formatAsyncRoutes(routes: any[]) { 37 | routes.forEach((r) => { 38 | if (r.component === 'layout' || !r.component) { 39 | r.component = Layout 40 | } else { 41 | r.component = modules[`/src/views${r.component}`] 42 | } 43 | if (r.children && r.children.length > 0) { 44 | r.children = formatAsyncRoutes(r.children) 45 | } 46 | }) 47 | return routes 48 | } 49 | 50 | // 过滤没有角色权限的路由 51 | function filterRoute(route: RouteRecordRaw): boolean { 52 | const useUser = useUserStore() 53 | if (!route.meta?.roles) return true 54 | else if (route.meta.roles.includes(useUser.role)) return true 55 | else return false 56 | } 57 | 58 | function filterFunc(routes: RouteRecordRaw[]) { 59 | routes = routes.filter(filterRoute) 60 | 61 | routes.forEach((element) => { 62 | if (element.children) { 63 | element.children = filterFunc(element.children) 64 | } 65 | if ( 66 | element.children && 67 | !element.children?.some((val) => element.path + '/' + val.path === element.redirect) 68 | ) { 69 | element.redirect = element.path + '/' + element.children[0].path 70 | } 71 | }) 72 | return routes 73 | } 74 | 75 | export async function addAsyncRoutes(router: any) { 76 | const useUser = useUserStore() 77 | const permissionMode = import.meta.env.VITE_PERMISSION_MODE 78 | 79 | let filteredRoutes = [] 80 | // 前端动态路由模式 81 | if (permissionMode === 'FRONT') { 82 | filteredRoutes = filterFunc(routeModuleList) 83 | } 84 | // 后端动态路由模式 85 | if (permissionMode === 'BACK') { 86 | let routes: any = await getBackAsyncRoutes() 87 | filteredRoutes = formatAsyncRoutes(routes) 88 | } 89 | filteredRoutes.forEach((val) => router.addRoute(val)) 90 | useUser.setAsyncRoutes(filteredRoutes) 91 | } 92 | -------------------------------------------------------------------------------- /src/views/feat/download.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 81 | 82 | 111 | -------------------------------------------------------------------------------- /src/views/editor/Tinymce.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 87 | -------------------------------------------------------------------------------- /src/views/feat/media/PreviewPDF.vue: -------------------------------------------------------------------------------- 1 | 11 | 49 | 50 | 96 | -------------------------------------------------------------------------------- /coverage/lcov-report/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | if ( 67 | document.getElementById('fileSearch') === document.activeElement && 68 | document.activeElement != null 69 | ) { 70 | // if we're currently focused on the search input, we don't want to navigate 71 | return; 72 | } 73 | 74 | switch (event.which) { 75 | case 78: // n 76 | case 74: // j 77 | goToNext(); 78 | break; 79 | case 66: // b 80 | case 75: // k 81 | case 80: // p 82 | goToPrevious(); 83 | break; 84 | } 85 | }; 86 | })(); 87 | window.addEventListener('keydown', jumpToCode); 88 | -------------------------------------------------------------------------------- /src/layout/components/header/SideHeader.vue: -------------------------------------------------------------------------------- 1 | 42 | 92 | -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:src/utils/permission.ts 3 | FN:30,getBackAsyncRoutes 4 | FN:36,formatAsyncRoutes 5 | FN:37,(anonymous_2) 6 | FN:51,filterRoute 7 | FN:58,filterFunc 8 | FN:61,(anonymous_5) 9 | FN:67,(anonymous_6) 10 | FN:75,addAsyncRoutes 11 | FN:89,(anonymous_8) 12 | FNF:9 13 | FNH:0 14 | FNDA:0,getBackAsyncRoutes 15 | FNDA:0,formatAsyncRoutes 16 | FNDA:0,(anonymous_2) 17 | FNDA:0,filterRoute 18 | FNDA:0,filterFunc 19 | FNDA:0,(anonymous_5) 20 | FNDA:0,(anonymous_6) 21 | FNDA:0,addAsyncRoutes 22 | FNDA:0,(anonymous_8) 23 | DA:27,0 24 | DA:31,0 25 | DA:32,0 26 | DA:37,0 27 | DA:38,0 28 | DA:39,0 29 | DA:41,0 30 | DA:43,0 31 | DA:44,0 32 | DA:47,0 33 | DA:52,0 34 | DA:53,0 35 | DA:54,0 36 | DA:55,0 37 | DA:59,0 38 | DA:61,0 39 | DA:62,0 40 | DA:63,0 41 | DA:65,0 42 | DA:67,0 43 | DA:69,0 44 | DA:72,0 45 | DA:76,0 46 | DA:77,0 47 | DA:79,0 48 | DA:81,0 49 | DA:82,0 50 | DA:85,0 51 | DA:86,0 52 | DA:87,0 53 | DA:89,0 54 | DA:90,0 55 | LF:32 56 | LH:0 57 | BRDA:38,0,0,0 58 | BRDA:38,0,1,0 59 | BRDA:38,1,0,0 60 | BRDA:38,1,1,0 61 | BRDA:43,2,0,0 62 | BRDA:43,3,0,0 63 | BRDA:43,3,1,0 64 | BRDA:53,4,0,0 65 | BRDA:53,4,1,0 66 | BRDA:54,5,0,0 67 | BRDA:54,5,1,0 68 | BRDA:62,6,0,0 69 | BRDA:65,7,0,0 70 | BRDA:66,8,0,0 71 | BRDA:66,8,1,0 72 | BRDA:81,9,0,0 73 | BRDA:85,10,0,0 74 | BRF:17 75 | BRH:0 76 | end_of_record 77 | TN: 78 | SF:src/utils/sum.ts 79 | FN:11,sum 80 | FNF:1 81 | FNH:1 82 | FNDA:2,sum 83 | DA:12,2 84 | DA:14,1 85 | LF:2 86 | LH:2 87 | BRF:0 88 | BRH:0 89 | end_of_record 90 | TN: 91 | SF:src/utils/http/axios.ts 92 | FN:16,getRequestKey 93 | FN:26,setPendingMap 94 | FN:44,(anonymous_2) 95 | FN:50,(anonymous_3) 96 | FN:56,(anonymous_4) 97 | FN:81,(anonymous_5) 98 | FNF:6 99 | FNH:0 100 | FNDA:0,getRequestKey 101 | FNDA:0,setPendingMap 102 | FNDA:0,(anonymous_2) 103 | FNDA:0,(anonymous_3) 104 | FNDA:0,(anonymous_4) 105 | FNDA:0,(anonymous_5) 106 | DA:14,0 107 | DA:17,0 108 | DA:27,0 109 | DA:28,0 110 | DA:29,0 111 | DA:30,0 112 | DA:31,0 113 | DA:32,0 114 | DA:34,0 115 | DA:38,0 116 | DA:43,0 117 | DA:45,0 118 | DA:46,0 119 | DA:48,0 120 | DA:51,0 121 | DA:52,0 122 | DA:56,0 123 | DA:57,0 124 | DA:58,0 125 | DA:59,0 126 | DA:61,0 127 | DA:62,0 128 | DA:63,0 129 | DA:65,0 130 | DA:67,0 131 | DA:68,0 132 | DA:70,0 133 | DA:74,0 134 | DA:76,0 135 | DA:81,0 136 | DA:82,0 137 | LF:31 138 | LH:0 139 | BRDA:18,0,0,0 140 | BRDA:18,0,1,0 141 | BRDA:30,1,0,0 142 | BRDA:30,1,1,0 143 | BRDA:45,2,0,0 144 | BRDA:61,3,0,0 145 | BRDA:61,3,1,0 146 | BRDA:62,4,0,0 147 | BRDA:62,4,1,0 148 | BRDA:67,5,0,0 149 | BRDA:67,5,1,0 150 | BRDA:70,6,0,0 151 | BRDA:70,6,1,0 152 | BRDA:82,7,0,0 153 | BRDA:82,7,1,0 154 | BRF:15 155 | BRH:0 156 | end_of_record 157 | TN: 158 | SF:src/utils/http/errorHandler.ts 159 | FN:4,(anonymous_0) 160 | FN:11,(anonymous_1) 161 | FNF:2 162 | FNH:0 163 | FNDA:0,(anonymous_0) 164 | FNDA:0,(anonymous_1) 165 | DA:4,0 166 | DA:5,0 167 | DA:6,0 168 | DA:7,0 169 | DA:11,0 170 | DA:12,0 171 | DA:14,0 172 | DA:15,0 173 | DA:17,0 174 | DA:18,0 175 | DA:24,0 176 | LF:11 177 | LH:0 178 | BRDA:5,0,0,0 179 | BRDA:6,1,0,0 180 | BRDA:11,2,0,0 181 | BRDA:12,3,0,0 182 | BRDA:12,3,1,0 183 | BRDA:14,4,0,0 184 | BRDA:17,5,0,0 185 | BRDA:24,6,0,0 186 | BRF:8 187 | BRH:0 188 | end_of_record 189 | -------------------------------------------------------------------------------- /src/views/feat/import.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 102 | 103 | 117 | -------------------------------------------------------------------------------- /src/views/chart/EChart.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 139 | -------------------------------------------------------------------------------- /src/components/MoMoreText.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 93 | 94 | 134 | -------------------------------------------------------------------------------- /src/views/dashboard/Analysis.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 145 | -------------------------------------------------------------------------------- /src/views/login/login.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 92 | 93 | 118 | -------------------------------------------------------------------------------- /src/router/modules/feat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lucidity99 lucidity929@163.com 3 | * @Date: 2023-06-05 22:42:28 4 | * @LastEditors: lucidity99 lucidity929@163.com 5 | * @LastEditTime: 2024-05-30 22:00:19 6 | * @FilePath: /mocha-vue3-system/src/router/modules/feat.ts 7 | * @Description: 8 | * 9 | * 湖南灵之心, All Rights Reserved. 10 | */ 11 | import { RouteRecordRaw } from 'vue-router' 12 | import Layout from '~/layout/index.vue' 13 | 14 | const routes: RouteRecordRaw[] = [ 15 | { 16 | path: '/feat', 17 | name: 'feat', 18 | component: Layout, 19 | meta: { 20 | title: 'feats', 21 | icon: 'ep-reading', 22 | order: 2 23 | }, 24 | children: [ 25 | { 26 | path: 'download', 27 | name: 'download', 28 | meta: { 29 | title: 'download' 30 | }, 31 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/download.vue') 32 | }, 33 | { 34 | path: 'media', 35 | name: 'media', 36 | meta: { 37 | title: 'media' 38 | }, 39 | redirect: 'previewPDF', 40 | children: [ 41 | { 42 | path: 'previewPDF', 43 | name: 'previewPDF', 44 | meta: { 45 | title: 'previewPDF' 46 | }, 47 | component: () => 48 | import(/* webpackChunkName: "feat" */ '~/views/feat/media/PreviewPDF.vue') 49 | }, 50 | { 51 | path: 'videojs', 52 | name: 'videojs', 53 | meta: { 54 | title: 'videojs' 55 | }, 56 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/media/Videojs.vue') 57 | } 58 | ] 59 | }, 60 | 61 | { 62 | path: 'directives', 63 | name: 'directives', 64 | meta: { 65 | title: 'customDirectives' 66 | }, 67 | children: [ 68 | { 69 | path: 'index', 70 | name: 'diretivesAll', 71 | meta: { 72 | title: 'customDirectives' 73 | }, 74 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/direct/index.vue') 75 | }, 76 | { 77 | path: 'watermark', 78 | name: 'watermark', 79 | meta: { 80 | title: 'watermark' 81 | }, 82 | component: () => 83 | import(/* webpackChunkName: "feat" */ '~/views/feat/direct/Watermark.vue') 84 | }, 85 | { 86 | path: 'adaptive', 87 | name: 'adaptive', 88 | meta: { 89 | title: 'adaptiveHeight' 90 | }, 91 | component: () => 92 | import(/* webpackChunkName: "feat" */ '~/views/feat/direct/Adaptive.vue') 93 | } 94 | ] 95 | }, 96 | { 97 | path: 'avatar', 98 | name: 'avatar', 99 | meta: { 100 | title: 'avatar' 101 | }, 102 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/Avatar.vue') 103 | }, 104 | { 105 | path: 'GSAP', 106 | name: 'GSAP', 107 | meta: { 108 | title: 'GSAP' 109 | }, 110 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/GSAP.vue') 111 | }, 112 | { 113 | path: 'highlight', 114 | name: 'highlight', 115 | meta: { 116 | title: 'cssHighlight' 117 | }, 118 | component: () => import(/* webpackChunkName: "feat" */ '~/views/feat/highlight.vue') 119 | } 120 | ] 121 | } 122 | ] 123 | 124 | export default routes 125 | -------------------------------------------------------------------------------- /src/layout/components/SiteSearch.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 114 | 115 | 129 | --------------------------------------------------------------------------------