├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .vscode └── extensions.json ├── Dockerfile ├── Makefile ├── README.md ├── default.conf ├── docker-entrypoint.sh ├── docs ├── group │ ├── groupdisablelist.png │ └── grouplist.png ├── home │ ├── dark.png │ ├── home.png │ └── theme.png ├── message │ ├── prohibitwords.png │ └── sendmsglist.png ├── report │ ├── group.png │ └── user.png ├── setting │ ├── currencysetting.png │ └── updatepwd.png ├── tool │ └── appupdate.png └── user │ ├── adduser.png │ ├── disablelist.png │ └── userlist.png ├── index.html ├── nginx.conf.template ├── package.json ├── plop-templates ├── component │ ├── index.hbs │ └── prompt.js ├── page │ ├── index.hbs │ └── prompt.js └── store │ ├── index.hbs │ └── prompt.js ├── plopfile.js ├── pnpm-lock.yaml ├── public ├── logo.png └── tsdd-config.js ├── src ├── App.vue ├── api │ ├── basic.ts │ ├── file.ts │ ├── group.ts │ ├── message.ts │ ├── report.ts │ ├── setting.ts │ ├── statistic.ts │ ├── tool.ts │ ├── user.ts │ └── workplace │ │ ├── app.ts │ │ ├── banner.ts │ │ └── category.ts ├── assets │ ├── heder.png │ ├── images │ │ └── bg.svg │ ├── logo.png │ └── vue.svg ├── components │ ├── BdAppVersion │ │ └── index.vue │ ├── BdMsg │ │ └── index.vue │ ├── BdPage │ │ └── index.vue │ ├── BdProhitWords │ │ └── index.vue │ ├── BdSandAllMsg │ │ └── index.vue │ ├── BdSendMsg │ │ └── index.vue │ └── SwitchDark │ │ └── index.vue ├── config │ ├── index.ts │ └── modules │ │ ├── dev.ts │ │ └── prod.ts ├── directives │ ├── index.ts │ └── modules │ │ ├── auth.ts │ │ ├── copy.ts │ │ ├── debounce.ts │ │ ├── draggable.ts │ │ ├── longpress.ts │ │ ├── throttle.ts │ │ └── waterMarker.ts ├── hooks │ ├── interface │ │ └── index.ts │ ├── useEcharts.ts │ └── useTheme.ts ├── i18n │ ├── index.ts │ └── modules │ │ ├── en.ts │ │ └── zh.ts ├── layouts │ ├── components │ │ ├── Footer.vue │ │ ├── Header │ │ │ ├── ToolBarLeft.vue │ │ │ ├── ToolBarRight.vue │ │ │ └── components │ │ │ │ ├── AssemblySize.vue │ │ │ │ ├── Avatar.vue │ │ │ │ ├── Breadcrumb.vue │ │ │ │ ├── Fullscreen.vue │ │ │ │ ├── Language.vue │ │ │ │ └── ThemeSetting.vue │ │ ├── LayoutClassic.vue │ │ ├── LayoutColumns.vue │ │ ├── LayoutTransverse.vue │ │ ├── LayoutVertical.vue │ │ ├── Main.vue │ │ ├── Menu │ │ │ └── SubMenu.vue │ │ ├── Tabs.vue │ │ └── ThemeDrawer.vue │ └── index.vue ├── main.ts ├── menu │ ├── index.ts │ └── modules │ │ ├── group.ts │ │ ├── home.ts │ │ ├── message.ts │ │ ├── redpacket.ts │ │ ├── report.ts │ │ ├── setting.ts │ │ ├── tool.ts │ │ ├── user.ts │ │ └── workplace.ts ├── pages │ ├── group │ │ ├── groupblacklist.vue │ │ ├── groupdisablelist.vue │ │ ├── grouplist.vue │ │ └── groupmembers.vue │ ├── home │ │ ├── components │ │ │ ├── AddGroup.vue │ │ │ ├── AddUser.vue │ │ │ └── Statistics.vue │ │ └── index.vue │ ├── login │ │ └── index.vue │ ├── message │ │ ├── components │ │ │ └── Devices.vue │ │ ├── prohibitwords.vue │ │ ├── record.vue │ │ ├── recordpersonal.vue │ │ └── sendmsglist.vue │ ├── my │ │ └── bb.vue │ ├── redpacket │ │ └── list.vue │ ├── report │ │ ├── group.vue │ │ └── user.vue │ ├── setting │ │ ├── currencysetting.vue │ │ └── updatepwd.vue │ ├── tool │ │ └── appupdate.vue │ ├── user │ │ ├── adduser.vue │ │ ├── administrator │ │ │ ├── components │ │ │ │ └── AdminAdd.vue │ │ │ └── index.vue │ │ ├── disablelist.vue │ │ ├── friends.vue │ │ ├── userblacklist.vue │ │ └── userlist.vue │ └── workplace │ │ ├── configuration │ │ ├── components │ │ │ ├── AppDialog.vue │ │ │ ├── Banner.vue │ │ │ ├── BannerDialog.vue │ │ │ ├── CategoryDialog.vue │ │ │ ├── CustomGroup.vue │ │ │ └── Recommend.vue │ │ └── index.vue │ │ └── manage │ │ ├── components │ │ └── Apply.vue │ │ └── index.vue ├── router │ ├── index.ts │ ├── routers.ts │ └── staticRouter.ts ├── stores │ ├── index.ts │ ├── interface │ │ └── index.ts │ └── modules │ │ ├── auth.ts │ │ ├── global.ts │ │ ├── keepAlive.ts │ │ ├── tabs.ts │ │ └── user.ts ├── styles │ ├── element.scss │ ├── index.scss │ ├── reset.scss │ ├── theme │ │ ├── aside.ts │ │ └── element-dark.scss │ └── var.scss ├── types │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── env.d.ts │ └── global.d.ts └── utils │ ├── axios.ts │ ├── color.ts │ ├── index.ts │ ├── mittBus.ts │ ├── nprogress.ts │ ├── piniaPersist.ts │ └── system-copyright.ts ├── tsconfig.json ├── uno.config.ts └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | # 文件 2 | node_modules 3 | test 4 | .git 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] # 表示所有文件适用 6 | charset = utf-8 # 设置文件字符集为 utf-8 7 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 8 | insert_final_newline = true # 始终在文件末尾插入一个新行 9 | indent_style = space # 缩进风格(tab | space) 10 | indent_size = 2 # 缩进大小 11 | max_line_length = 130 # 最大行长度 12 | 13 | [*.md] # 表示仅对 md 文件适用以下规则 14 | max_line_length = off # 关闭最大行长度限制 15 | trim_trailing_whitespace = false # 关闭末尾空格修剪 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | dist -------------------------------------------------------------------------------- /.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 | // eslint-config-prettier 的缩写 15 | 'prettier', 16 | 'vue-global-api' 17 | ], 18 | parserOptions: { 19 | ecmaVersion: 12, 20 | parser: '@typescript-eslint/parser', 21 | sourceType: 'module', 22 | ecmaFeatures: { 23 | jsx: true 24 | } 25 | }, 26 | // eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写 27 | plugins: ['vue', '@typescript-eslint', 'prettier'], 28 | rules: { 29 | '@typescript-eslint/ban-ts-ignore': 'off', 30 | '@typescript-eslint/no-unused-vars': 'off', 31 | '@typescript-eslint/explicit-function-return-type': 'off', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-var-requires': 'off', 34 | '@typescript-eslint/no-empty-function': 'off', 35 | '@typescript-eslint/no-use-before-define': 'off', 36 | '@typescript-eslint/ban-ts-comment': 'off', 37 | '@typescript-eslint/ban-types': 'off', 38 | '@typescript-eslint/no-non-null-assertion': 'off', 39 | '@typescript-eslint/explicit-module-boundary-types': 'off', 40 | 'vue/multi-word-component-names': 'off', 41 | 'vue/no-mutating-props': 'off', 42 | 'no-undef': 'off' 43 | }, 44 | globals: { 45 | defineProps: 'readonly', 46 | defineEmits: 'readonly', 47 | defineExpose: 'readonly', 48 | withDefaults: 'readonly' 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | yarn.lock 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 130, // 超过最大值换行 3 | tabWidth: 2, // 缩进字节数 4 | useTabs: false, // 缩进使用tab,不使用空格 5 | semi: true, // 句尾添加分号 6 | singleQuote: true, // 使用单引号代替双引号 7 | proseWrap: "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行 8 | arrowParens: "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 9 | bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }" 10 | endOfLine: "auto", // 结尾是 \n \r \n\r auto 11 | jsxSingleQuote: false, // 在jsx中使用单引号代替双引号 12 | trailingComma: "none", // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号) 13 | overrides: [ 14 | { 15 | files: "*.html", 16 | options: { 17 | parser: "html", 18 | }, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.20.1 as builder 2 | WORKDIR /app 3 | RUN npm config set registry https://registry.npm.taobao.org 4 | RUN npm install pnpm -g 5 | 6 | COPY . . 7 | 8 | RUN pnpm install && pnpm build 9 | 10 | 11 | FROM nginx:latest 12 | COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint2.sh 13 | COPY --from=builder /app/nginx.conf.template / 14 | COPY --from=builder /app/dist /usr/share/nginx/html 15 | ENTRYPOINT ["sh", "/docker-entrypoint2.sh"] 16 | CMD ["nginx","-g","daemon off;"] 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t tangsengdaodaomanager . 3 | deploy: 4 | docker build -t tangsengdaodaomanager . 5 | docker tag tangsengdaodaomanager registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaomanager:latest 6 | docker push registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaomanager:latest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 唐僧叨叨后台管理 2 | 3 | ### 介绍 📖 4 | 5 | 唐僧叨叨后台管理一款基于 Vue3.3、TypeScript、Vite5、Pinia、Element-Plus 开源的后台管理框架,使用目前最新技术栈开发;是唐僧叨叨业务管理后台。 6 | 7 | - 使用 Vue3.3 + TypeScript 开发,单文件组件<script setup> 8 | - 采用 Vite 作为项目开发、打包工具(配置 gzip/brotli 打包、tsx 语法、跨域代理…) 9 | - 使用 Pinia 替代 Vuex,轻量、简单、易用,集成 Pinia 持久化插件 10 | - 使用 TypeScript 对 Axios 整个二次封装(请求拦截、取消、常用请求封装…) 11 | - 支持 Element 组件大小切换、多主题布局、暗黑模式、i18n 国际化 12 | - 使用 VueRouter 配置动态路由权限拦截、路由懒加载,支持页面按钮权限控制 13 | - 使用 KeepAlive 对页面进行缓存,支持多级嵌套路由缓存 14 | - 常用自定义指令开发(权限、复制、水印、拖拽、节流、防抖、长按…) 15 | - 使用 Prettier 统一格式化代码,集成 ESLint、Stylelint 代码校验规范 16 | - 使用 husky、lint-staged、commitlint、czg、cz-git 规范提交信息 17 | 18 | ### 安装使用步骤 📔 19 | 20 | 环境变量 21 | - NODE_ENV node环境变量 22 | - APP_ENV 应用环境变量 23 | - dev 开发环境 24 | - pord 生产环境 25 | - IS_CONFIG 是否添加配置文件 26 | 27 | 1. 安装 28 | 29 | ```sh 30 | pnpm install 31 | ``` 32 | 33 | 2. 本地开发 34 | 35 | ``` sh 36 | pnpm dev 37 | ``` 38 | 39 | 3. 编译 40 | 41 | ``` sh 42 | pnpm build 43 | ``` 44 | 45 | 4. 配置文件编译 46 | 47 | ``` sh 48 | pnpm build:config 49 | ``` 50 | 51 | 5. 本地预览 52 | > 先执行编译再执行该命令 53 | 54 | ``` sh 55 | pnpm serve 56 | ``` 57 | 58 | ### 功能特色 🔨 59 | 60 | - [x] 首页 61 | - [x] 仪表盘 62 | - [x] 主题设置 63 | - [x] 用户 64 | - [x] 新增用户 65 | - [x] 用户列表 66 | - [x] 发消息 67 | - [x] 好友列表 68 | - [x] 封禁 69 | - [x] 封禁用户列表 70 | - [x] 封禁 71 | - [x] 解禁 72 | - [x] 群组 73 | - [x] 群列表 74 | - [x] 发消息 75 | - [x] 群成员 76 | - [x] 聊天记录 77 | - [x] 黑名单成员 78 | - [x] 禁言 79 | - [x] 封禁 80 | - [x] 封禁群列表 81 | - [x] 消息 82 | - [x] 消息记录 83 | - [x] 违禁词列表 84 | - [x] 举报 85 | - [x] 举报用户 86 | - [x] 举报群聊 87 | - [x] 工具 88 | - [x] APP升级 89 | - [x] 设置 90 | - [x] 通用设置 91 | - [x] 修改登录密码 92 | ### 功能截图 📷 93 | 94 | 95 | - 首页 96 | 97 | ![home](./docs/home/home.png) 98 | 99 | ![theme](./docs/home/theme.png) 100 | ![theme](./docs/home/dark.png) 101 | 102 | - 用户 103 | 104 | ![adduser](./docs/user/adduser.png) 105 | 106 | ![userlist](./docs/user/userlist.png) 107 | 108 | ![userlist](./docs/user/disablelist.png) 109 | 110 | - 群组 111 | 112 | ![grouplist](./docs/group/grouplist.png) 113 | 114 | ![groupdisablelist](./docs/group/groupdisablelist.png) 115 | 116 | - 消息 117 | 118 | ![sendmsglist](./docs/message/sendmsglist.png) 119 | 120 | ![prohibitwords](./docs/message/prohibitwords.png) 121 | 122 | - 举报 123 | 124 | ![report-user](./docs/report/user.png) 125 | 126 | ![report-group](./docs/report/group.png) 127 | 128 | - 工具 129 | 130 | ![appupdate](./docs/tool/appupdate.png) 131 | 132 | - 设置 133 | 134 | ![currencysetting](./docs/setting/currencysetting.png) 135 | 136 | ![currencysetting](./docs/setting/updatepwd.png) 137 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8084; 3 | 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html =404; 8 | } 9 | 10 | error_page 500 502 503 504 /50x.html; 11 | location = /50x.html { 12 | root html; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | envsubst '${API_URL}' < /nginx.conf.template > /etc/nginx/conf.d/default.conf 6 | 7 | 8 | exec "$@" -------------------------------------------------------------------------------- /docs/group/groupdisablelist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/group/groupdisablelist.png -------------------------------------------------------------------------------- /docs/group/grouplist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/group/grouplist.png -------------------------------------------------------------------------------- /docs/home/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/home/dark.png -------------------------------------------------------------------------------- /docs/home/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/home/home.png -------------------------------------------------------------------------------- /docs/home/theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/home/theme.png -------------------------------------------------------------------------------- /docs/message/prohibitwords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/message/prohibitwords.png -------------------------------------------------------------------------------- /docs/message/sendmsglist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/message/sendmsglist.png -------------------------------------------------------------------------------- /docs/report/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/report/group.png -------------------------------------------------------------------------------- /docs/report/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/report/user.png -------------------------------------------------------------------------------- /docs/setting/currencysetting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/setting/currencysetting.png -------------------------------------------------------------------------------- /docs/setting/updatepwd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/setting/updatepwd.png -------------------------------------------------------------------------------- /docs/tool/appupdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/tool/appupdate.png -------------------------------------------------------------------------------- /docs/user/adduser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/user/adduser.png -------------------------------------------------------------------------------- /docs/user/disablelist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/user/disablelist.png -------------------------------------------------------------------------------- /docs/user/userlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/docs/user/userlist.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%- title %> 9 | <%- injectScript %> 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /nginx.conf.template: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_min_length 1k; 3 | gzip_disable msie6; 4 | gzip_vary on; 5 | gzip_proxied any; 6 | gzip_comp_level 2; 7 | gzip_buffers 16 8k; 8 | gzip_http_version 1.1; 9 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 10 | 11 | 12 | server { 13 | listen 80; 14 | server_name localhost; 15 | 16 | location /api/ { 17 | proxy_pass ${API_URL}; 18 | client_max_body_size 1000m; 19 | client_body_buffer_size 500m; 20 | } 21 | 22 | location / { 23 | root /usr/share/nginx/html; 24 | index index.html index.htm; 25 | try_files $uri $uri/ /index.html; 26 | } 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdd-control", 3 | "private": true, 4 | "version": "1.5.0", 5 | "scripts": { 6 | "dev": "cross-env APP_ENV=dev vite", 7 | "build": "cross-env APP_ENV=prod vite build", 8 | "build:config": "cross-env APP_ENV=prod IS_CONFIG=true vite build", 9 | "preview": "vite preview", 10 | "new": "plop", 11 | "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx,", 12 | "lint:prettier": "prettier --write src" 13 | }, 14 | "dependencies": { 15 | "@element-plus/icons-vue": "^2.1.0", 16 | "@fancyapps/ui": "^5.0.22", 17 | "@icon-park/vue-next": "^1.4.2", 18 | "@lottiefiles/lottie-player": "^2.0.2", 19 | "@vueuse/core": "^10.1.2", 20 | "axios": "^1.4.0", 21 | "dayjs": "^1.11.9", 22 | "echarts": "^5.4.3", 23 | "element-plus": "^2.3.7", 24 | "mitt": "^3.0.0", 25 | "nprogress": "^0.2.0", 26 | "pinia": "^2.0.36", 27 | "pinia-plugin-persistedstate": "^3.1.0", 28 | "sortablejs": "^1.15.0", 29 | "vue": "^3.3.4", 30 | "vue-color-kit": "^1.0.5", 31 | "vue-grid-layout": "^3.0.0-beta1", 32 | "vue-i18n": "^9.2.2", 33 | "vue-router": "^4.2.0", 34 | "vue3-count-to": "^1.1.2", 35 | "vue3-lottie": "^2.7.4" 36 | }, 37 | "devDependencies": { 38 | "@types/sortablejs": "^1.15.1", 39 | "@typescript-eslint/eslint-plugin": "^5.59.5", 40 | "@typescript-eslint/parser": "^5.59.5", 41 | "@unocss/preset-uno": "^0.51.12", 42 | "@unocss/vite": "^0.51.12", 43 | "@vitejs/plugin-vue": "^4.2.3", 44 | "@vitejs/plugin-vue-jsx": "^3.0.1", 45 | "cross-env": "^7.0.3", 46 | "eslint": "^8.40.0", 47 | "eslint-config-prettier": "^8.8.0", 48 | "eslint-plugin-prettier": "^4.2.1", 49 | "eslint-plugin-vue": "^9.12.0", 50 | "plop": "^3.1.2", 51 | "prettier": "^2.8.8", 52 | "sass": "^1.62.1", 53 | "typescript": "^5.0.4", 54 | "unplugin-auto-import": "^0.16.4", 55 | "unplugin-vue-components": "^0.25.1", 56 | "unplugin-vue-router": "^0.6.4", 57 | "unplugin-vue-setup-extend-plus": "^1.0.0", 58 | "vite": "^4.4.1", 59 | "vite-plugin-compression": "^0.5.1", 60 | "vite-plugin-html": "^3.2.0", 61 | "vite-plugin-html-template": "^1.1.5", 62 | "vite-plugin-pages": "^0.31.0", 63 | "vite-plugin-vue-devtools": "^0.4.14", 64 | "vite-plugin-vue-meta-layouts": "^0.2.2", 65 | "vue-global-api": "^0.4.1", 66 | "vue-tsc": "^1.6.4" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | function getFolder(path) { 4 | const components = []; 5 | const files = fs.readdirSync(path); 6 | files.forEach(item => { 7 | const stat = fs.lstatSync(`${path}/${item}`); 8 | if (stat.isDirectory() === true && item !== 'components') { 9 | components.push(`${path}/${item}`); 10 | components.push(...getFolder(`${path}/${item}`)); 11 | } 12 | }); 13 | return components; 14 | } 15 | 16 | module.exports = { 17 | description: '创建组件', 18 | prompts: [ 19 | { 20 | type: 'confirm', 21 | name: 'isGlobal', 22 | message: '是否为全局组件', 23 | default: false 24 | }, 25 | { 26 | type: 'list', 27 | name: 'path', 28 | message: '请选择组件创建目录', 29 | choices: getFolder('src/pages'), 30 | when: answers => { 31 | return !answers.isGlobal; 32 | } 33 | }, 34 | { 35 | type: 'input', 36 | name: 'name', 37 | message: '请输入组件名称', 38 | validate: v => { 39 | if (!v || v.trim === '') { 40 | return '组件名称不能为空'; 41 | } else { 42 | return true; 43 | } 44 | } 45 | } 46 | ], 47 | actions: data => { 48 | let path = ''; 49 | if (data.isGlobal) { 50 | path = 'src/components/{{properCase name}}/index.vue'; 51 | } else { 52 | path = `${data.path}/components/{{properCase name}}/index.vue`; 53 | } 54 | const actions = [ 55 | { 56 | type: 'add', 57 | path, 58 | templateFile: 'plop-templates/component/index.hbs' 59 | } 60 | ]; 61 | return actions; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /plop-templates/page/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{#if isFilesystem}} 8 | 9 | meta: 10 | title: 页面标题 11 | 12 | {{/if}} 13 | 14 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /plop-templates/page/prompt.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | function getFolder(path) { 5 | const components = []; 6 | const files = fs.readdirSync(path); 7 | files.forEach(item => { 8 | const stat = fs.lstatSync(`${path}/${item}`); 9 | if (stat.isDirectory() === true && item !== 'components') { 10 | components.push(`${path}/${item}`); 11 | components.push(...getFolder(`${path}/${item}`)); 12 | } 13 | }); 14 | return components; 15 | } 16 | 17 | module.exports = { 18 | description: '创建页面', 19 | prompts: [ 20 | { 21 | type: 'list', 22 | name: 'path', 23 | message: '请选择页面创建目录', 24 | choices: getFolder('src/pages') 25 | }, 26 | { 27 | type: 'input', 28 | name: 'name', 29 | message: '请输入文件名', 30 | validate: v => { 31 | if (!v || v.trim === '') { 32 | return '文件名不能为空'; 33 | } else { 34 | return true; 35 | } 36 | } 37 | }, 38 | { 39 | type: 'confirm', 40 | name: 'isFilesystem', 41 | message: '是否为基于文件系统的路由页面', 42 | default: false 43 | } 44 | ], 45 | actions: data => { 46 | const relativePath = path.relative('src/pages', data.path); 47 | const actions = [ 48 | { 49 | type: 'add', 50 | path: `${data.path}/{{dotCase name}}.vue`, 51 | templateFile: 'plop-templates/page/index.hbs', 52 | data: { 53 | componentName: `${relativePath} ${data.name}` 54 | } 55 | } 56 | ]; 57 | return actions; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /plop-templates/store/index.hbs: -------------------------------------------------------------------------------- 1 | const use{{ properCase name }}Store = defineStore( 2 | // 唯一ID 3 | '{{ camelCase name }}', 4 | { 5 | state: () => ({}), 6 | getters: {}, 7 | actions: {}, 8 | }, 9 | ) 10 | 11 | export default use{{ properCase name }}Store -------------------------------------------------------------------------------- /plop-templates/store/prompt.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '创建全局状态', 3 | prompts: [ 4 | { 5 | type: 'input', 6 | name: 'name', 7 | message: '请输入模块名称', 8 | validate: v => { 9 | if (!v || v.trim === '') { 10 | return '模块名称不能为空'; 11 | } else { 12 | return true; 13 | } 14 | } 15 | } 16 | ], 17 | actions: () => { 18 | const actions = [ 19 | { 20 | type: 'add', 21 | path: 'src/store/modules/{{camelCase name}}.ts', 22 | templateFile: 'plop-templates/store/index.hbs' 23 | } 24 | ]; 25 | return actions; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (plop) { 2 | plop.setWelcomeMessage('请选择需要创建的模式:'); 3 | plop.setGenerator('page', require('./plop-templates/page/prompt')); 4 | plop.setGenerator('component', require('./plop-templates/component/prompt')); 5 | plop.setGenerator('store', require('./plop-templates/store/prompt')); 6 | }; 7 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/public/logo.png -------------------------------------------------------------------------------- /public/tsdd-config.js: -------------------------------------------------------------------------------- 1 | var TSDD_CONFIG = { 2 | APP_URL: 'https://api.botgate.cn/v1/' 3 | }; 4 | 5 | window.TSDD_CONFIG = TSDD_CONFIG; 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /src/api/basic.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 登录 4 | export function loginPost(data: any) { 5 | return request({ 6 | url: '/manager/login', 7 | method: 'post', 8 | data 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/api/file.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 获取文件路径 4 | export function feileGet(params?: any) { 5 | return request({ 6 | url: '/file/upload', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/api/group.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 群列表 4 | export function groupListGet(params: any) { 5 | return request({ 6 | url: '/manager/group/list', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 封禁群列表 13 | export function groupDisablelistGet(params: any) { 14 | return request({ 15 | url: '/manager/group/disablelist', 16 | method: 'get', 17 | params 18 | }); 19 | } 20 | 21 | // 群成员 22 | export function groupGroupmembersGet(params: any, groupNo: string) { 23 | return request({ 24 | url: `/manager/groups/${groupNo}/members`, 25 | method: 'get', 26 | params 27 | }); 28 | } 29 | 30 | // 移除成员 31 | export function groupGroupmembersDelete(data: any, groupNo: string) { 32 | return request({ 33 | url: `/manager/groups/${groupNo}/members`, 34 | method: 'delete', 35 | data 36 | }); 37 | } 38 | 39 | // 黑名单列表 40 | export function groupBlacklistGet(params: any, groupNo: string) { 41 | return request({ 42 | url: `/manager/groups/${groupNo}/members/blacklist`, 43 | method: 'get', 44 | params 45 | }); 46 | } 47 | 48 | // 禁言/解除禁言 49 | export function groupForbiddenPut(params: any) { 50 | return request({ 51 | url: `/manager/groups/${params.groupNo}/forbidden/${params.forbidden}`, 52 | method: 'put' 53 | }); 54 | } 55 | 56 | // 封禁/解禁 57 | export function groupLiftbanPut(params: any) { 58 | return request({ 59 | url: `/manager/group/liftban/${params.groupNo}/${params.status}`, 60 | method: 'put' 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/api/message.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 消息记录 4 | export function messageGet(params: any) { 5 | return request({ 6 | url: '/manager/message', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 发消息 13 | export function messageSendPost(data: any) { 14 | return request({ 15 | url: '/manager/message/send', 16 | method: 'post', 17 | data 18 | }); 19 | } 20 | 21 | // 删除消息 22 | export function messageDelete(data: any) { 23 | return request({ 24 | url: '/manager/message', 25 | method: 'delete', 26 | data 27 | }); 28 | } 29 | 30 | // 发全员消息 31 | export function messageSendAllPost(data: any) { 32 | return request({ 33 | url: '/manager/message/sendall', 34 | method: 'post', 35 | data 36 | }); 37 | } 38 | 39 | // 违禁词列表 40 | export function messageProhibitWordsGet(params: any) { 41 | return request({ 42 | url: '/manager/message/prohibit_words', 43 | method: 'get', 44 | params 45 | }); 46 | } 47 | // 新增违禁词 48 | export function messageProhibitWordsPost(params: any) { 49 | return request({ 50 | url: '/manager/message/prohibit_words', 51 | method: 'post', 52 | params 53 | }); 54 | } 55 | // 删除违禁词 56 | export function messageProhibitWordsDelete(params: any) { 57 | return request({ 58 | url: '/manager/message/prohibit_words', 59 | method: 'delete', 60 | params 61 | }); 62 | } 63 | 64 | // 单聊天消息 65 | export function messageRecordpersonalGet(params: any) { 66 | return request({ 67 | url: '/manager/message/recordpersonal', 68 | method: 'get', 69 | params 70 | }); 71 | } 72 | 73 | // 群聊天消息 74 | export function messageRecordGet(params: any) { 75 | return request({ 76 | url: '/manager/message/record', 77 | method: 'get', 78 | params 79 | }); 80 | } 81 | 82 | // 查看设备 83 | export function messageUserDevices(params: any) { 84 | return request({ 85 | url: '/manager/user/devices', 86 | method: 'get', 87 | params 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/api/report.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 举报列表 4 | export function reportListGet(params: any) { 5 | return request({ 6 | url: '/manager/report/list', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/api/setting.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 更新密码 4 | export function userUpdatepasswordPost(data: any) { 5 | return request({ 6 | url: '/manager/user/updatepassword', 7 | method: 'post', 8 | data 9 | }); 10 | } 11 | 12 | // 获取通用设置 13 | export function getAppconfigGet(params?: any) { 14 | return request({ 15 | url: '/manager/common/appconfig', 16 | method: 'get', 17 | params 18 | }); 19 | } 20 | 21 | // 更新通用设置 22 | export function updateAppconfigPost(data: any) { 23 | return request({ 24 | url: '/manager/common/appconfig', 25 | method: 'post', 26 | data 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/api/statistic.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 统计 4 | export function statisticsCountnumGet(params: any) { 5 | return request({ 6 | url: '/statistics/countnum', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 用户注册统计 13 | export function statisticsRegisteruserGet(start: string, end: string) { 14 | return request({ 15 | url: `/statistics/registeruser/${start}/${end}`, 16 | method: 'get' 17 | }); 18 | } 19 | 20 | // 新建群统计 21 | export function statisticsCreatedgroupGet(start: string, end: string) { 22 | return request({ 23 | url: `/statistics/createdgroup/${start}/${end}`, 24 | method: 'get' 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/api/tool.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // APP列表 4 | export function commonAppversionListGet(params: any) { 5 | return request({ 6 | url: '/common/appversion/list', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 新增App版本 13 | export function commonAppversionPost(data: any) { 14 | return request({ 15 | url: '/common/appversion', 16 | method: 'post', 17 | data 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 添加用户 4 | export function userAddPost(data: any) { 5 | return request({ 6 | url: '/manager/user/add', 7 | method: 'post', 8 | data 9 | }); 10 | } 11 | 12 | // 用户列表 13 | export function userListGet(params: any) { 14 | return request({ 15 | url: '/manager/user/list', 16 | method: 'get', 17 | params 18 | }); 19 | } 20 | 21 | // 封禁用户列表 22 | export function userDisablelistGet(params: any) { 23 | return request({ 24 | url: '/manager/user/disablelist', 25 | method: 'get', 26 | params 27 | }); 28 | } 29 | 30 | // 好友列表 31 | export function userFriendsGet(params: any) { 32 | return request({ 33 | url: 'manager/user/friends', 34 | method: 'get', 35 | params 36 | }); 37 | } 38 | 39 | // 黑名单列表 40 | export function userBlacklistGet(params: any) { 41 | return request({ 42 | url: 'manager/user/blacklist', 43 | method: 'get', 44 | params 45 | }); 46 | } 47 | 48 | // 用户封禁/解禁 49 | export function userLiftbanPut(params: any) { 50 | return request({ 51 | url: `manager/user/liftban/${params.uid}/${params.status}`, 52 | method: 'put' 53 | }); 54 | } 55 | 56 | // 管理员-列表 57 | export function adminList() { 58 | return request({ 59 | url: `manager/user/admin`, 60 | method: 'get' 61 | }); 62 | } 63 | 64 | // 管理员-新增 65 | export function adminAdd(data: any) { 66 | return request({ 67 | url: `manager/user/admin`, 68 | method: 'post', 69 | data 70 | }); 71 | } 72 | 73 | // 管理员-删除 74 | export function adminDelete(params: any) { 75 | return request({ 76 | url: `manager/user/admin`, 77 | method: 'delete', 78 | params 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/api/workplace/app.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 获取应用 4 | export function appGet(params?: any) { 5 | return request({ 6 | url: '/manager/workplace/app', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 新增应用 13 | export function appPost(data: any) { 14 | return request({ 15 | url: '/manager/workplace/app', 16 | method: 'post', 17 | data 18 | }); 19 | } 20 | 21 | // 编辑应用 22 | export function appPut(data: any, app_id: string) { 23 | return request({ 24 | url: `/manager/workplace/apps/${app_id}`, 25 | method: 'put', 26 | data 27 | }); 28 | } 29 | 30 | // 删除应用 31 | export function appDelete(app_id: string) { 32 | return request({ 33 | url: `/manager/workplace/apps/${app_id}`, 34 | method: 'delete' 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/api/workplace/banner.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 获取轮播 4 | export function bannerGet(params?: any) { 5 | return request({ 6 | url: '/manager/workplace/banner', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 新增轮播 13 | export function bannerPost(data: any) { 14 | return request({ 15 | url: '/manager/workplace/banner', 16 | method: 'post', 17 | data 18 | }); 19 | } 20 | 21 | // 编辑轮播 22 | export function bannerPut(data: any, banner_no: string) { 23 | return request({ 24 | url: `/manager/workplace/banners/${banner_no}`, 25 | method: 'put', 26 | data 27 | }); 28 | } 29 | 30 | // 删除轮播 31 | export function bannerDelete(banner_no: string) { 32 | return request({ 33 | url: `/manager/workplace/banners/${banner_no}`, 34 | method: 'delete' 35 | }); 36 | } 37 | 38 | // 轮播排序 39 | export function bannerReorderPut(data: any) { 40 | return request({ 41 | url: `/manager/workplace/banner/reorder`, 42 | method: 'put', 43 | data 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/api/workplace/category.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | // 获取分类 4 | export function categoryGet(params?: any) { 5 | return request({ 6 | url: '/manager/workplace/category', 7 | method: 'get', 8 | params 9 | }); 10 | } 11 | 12 | // 新增分类 13 | export function categoryPost(data: any) { 14 | return request({ 15 | url: '/manager/workplace/category', 16 | method: 'post', 17 | data 18 | }); 19 | } 20 | 21 | // 分类编辑 22 | export function categoryPut(data: any, category_no: string) { 23 | return request({ 24 | url: `/manager/workplace/categorys/${category_no}`, 25 | method: 'put', 26 | data 27 | }); 28 | } 29 | 30 | // 删除分类 31 | export function categoryDelete(category_no: string) { 32 | return request({ 33 | url: `/manager/workplace/categorys/${category_no}`, 34 | method: 'delete' 35 | }); 36 | } 37 | 38 | // 分类排序 39 | export function categoryReorderPut(data: any) { 40 | return request({ 41 | url: '/manager/workplace/category/reorder', 42 | method: 'put', 43 | data 44 | }); 45 | } 46 | 47 | // 分类获取应用 48 | export function categoryAppGet(category_no: string) { 49 | return request({ 50 | url: `/manager/workplace/categorys/${category_no}/app`, 51 | method: 'get' 52 | }); 53 | } 54 | 55 | // 分类新增应用 56 | export function categoryAppPost(data: any, category_no: string) { 57 | return request({ 58 | url: `/manager/workplace/categorys/${category_no}/app`, 59 | method: 'post', 60 | data 61 | }); 62 | } 63 | 64 | // 分类删除应用 65 | export function categoryAppDelete(category_no: string, app_id: string) { 66 | return request({ 67 | url: `/manager/workplace/categorys/${category_no}/apps/${app_id}`, 68 | method: 'delete' 69 | }); 70 | } 71 | 72 | // 分类应用排序 73 | export function categorysAppsReorderPut(data: any, category_no: string) { 74 | return request({ 75 | url: `/manager/workplace/categorys/${category_no}/app/reorder`, 76 | method: 'put', 77 | data 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/assets/heder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/src/assets/heder.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoManager/8f123aed80561f2c11111e075b55ad539dc1af08/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/BdAppVersion/index.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 169 | -------------------------------------------------------------------------------- /src/components/BdMsg/index.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 118 | 119 | 136 | -------------------------------------------------------------------------------- /src/components/BdPage/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /src/components/BdProhitWords/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 83 | -------------------------------------------------------------------------------- /src/components/BdSandAllMsg/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 86 | -------------------------------------------------------------------------------- /src/components/BdSendMsg/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 97 | -------------------------------------------------------------------------------- /src/components/SwitchDark/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // 首页地址(默认) 2 | export const HOME_URL = '/home'; 3 | 4 | // 登录页地址(默认) 5 | export const LOGIN_URL = '/login'; 6 | 7 | // 默认主题颜色 8 | export const DEFAULT_PRIMARY = '#E4633B'; 9 | 10 | // 路由白名单地址(必须是本地存在的路由 staticRouter.ts 中) 11 | export const ROUTER_WHITE_LIST: string[] = ['/login']; 12 | 13 | // 自定义应用根据运行环境获取配置 14 | const modules: any = {}; 15 | const moduleFiles = import.meta.glob('./modules/*.ts', { import: 'default', eager: true }); 16 | 17 | Object.keys(moduleFiles).forEach(name => { 18 | const key = name.replace('./modules/', '').replace('.ts', '').trim(); 19 | modules[key] = moduleFiles[name]; 20 | }); 21 | 22 | const TSDD_CONFIG = window.TSDD_CONFIG ? window.TSDD_CONFIG : {}; 23 | // 默认应用配置 24 | export const BU_DOU_CONFIG = { 25 | APP_TITLE: '唐僧叨叨后台管理', 26 | APP_TITLE_SHORT: '唐', 27 | ...modules[process.env.APP_ENV as any], 28 | ...TSDD_CONFIG 29 | // APP_URL: '/api/v1/' // 正式环境地址 (通用打包镜像,用此相对地址) 30 | }; 31 | -------------------------------------------------------------------------------- /src/config/modules/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | APP_ENV: 'dev', 3 | APP_URL: 'https://api.botgate.cn/v1/' 4 | }; 5 | -------------------------------------------------------------------------------- /src/config/modules/prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | APP_ENV: 'prod', 3 | APP_URL: '/api/v1/' 4 | }; 5 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { App, Directive } from 'vue'; 2 | import auth from './modules/auth'; 3 | import copy from './modules/copy'; 4 | import waterMarker from './modules/waterMarker'; 5 | import draggable from './modules/draggable'; 6 | import debounce from './modules/debounce'; 7 | import throttle from './modules/throttle'; 8 | import longpress from './modules/longpress'; 9 | 10 | const directivesList: { [key: string]: Directive } = { 11 | auth, 12 | copy, 13 | waterMarker, 14 | draggable, 15 | debounce, 16 | throttle, 17 | longpress 18 | }; 19 | 20 | const directives = { 21 | install: function (app: App) { 22 | Object.keys(directivesList).forEach(key => { 23 | app.directive(key, directivesList[key]); 24 | }); 25 | } 26 | }; 27 | 28 | export default directives; 29 | -------------------------------------------------------------------------------- /src/directives/modules/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * v-auth 3 | * 按钮权限指令 4 | */ 5 | import { useAuthStore } from '@/stores/modules/auth'; 6 | import type { Directive, DirectiveBinding } from 'vue'; 7 | 8 | const auth: Directive = { 9 | mounted(el: HTMLElement, binding: DirectiveBinding) { 10 | const { value } = binding; 11 | const authStore = useAuthStore(); 12 | const currentPageRoles = authStore.authButtonListGet[authStore.routeName] ?? []; 13 | if (value instanceof Array && value.length) { 14 | const hasPermission = value.every(item => currentPageRoles.includes(item)); 15 | if (!hasPermission) el.remove(); 16 | } else { 17 | if (!currentPageRoles.includes(value)) el.remove(); 18 | } 19 | } 20 | }; 21 | 22 | export default auth; 23 | -------------------------------------------------------------------------------- /src/directives/modules/copy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * v-copy 3 | * 复制某个值至剪贴板 4 | * 接收参数:string类型/Ref类型/Reactive类型 5 | */ 6 | import type { Directive, DirectiveBinding } from 'vue'; 7 | import { ElMessage } from 'element-plus'; 8 | interface ElType extends HTMLElement { 9 | copyData: string | number; 10 | __handleClick__: any; 11 | } 12 | const copy: Directive = { 13 | mounted(el: ElType, binding: DirectiveBinding) { 14 | el.copyData = binding.value; 15 | el.addEventListener('click', handleClick); 16 | }, 17 | updated(el: ElType, binding: DirectiveBinding) { 18 | el.copyData = binding.value; 19 | }, 20 | beforeUnmount(el: ElType) { 21 | el.removeEventListener('click', el.__handleClick__); 22 | } 23 | }; 24 | 25 | function handleClick(this: any) { 26 | const input = document.createElement('input'); 27 | input.value = this.copyData.toLocaleString(); 28 | document.body.appendChild(input); 29 | input.select(); 30 | document.execCommand('Copy'); 31 | document.body.removeChild(input); 32 | ElMessage({ 33 | type: 'success', 34 | message: '复制成功' 35 | }); 36 | } 37 | 38 | export default copy; 39 | -------------------------------------------------------------------------------- /src/directives/modules/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * v-debounce 3 | * 按钮防抖指令,可自行扩展至input 4 | * 接收参数:function类型 5 | */ 6 | import type { Directive, DirectiveBinding } from 'vue'; 7 | interface ElType extends HTMLElement { 8 | __handleClick__: () => any; 9 | } 10 | const debounce: Directive = { 11 | mounted(el: ElType, binding: DirectiveBinding) { 12 | if (typeof binding.value !== 'function') { 13 | throw 'callback must be a function'; 14 | } 15 | let timer: NodeJS.Timeout | null = null; 16 | el.__handleClick__ = function () { 17 | if (timer) { 18 | clearInterval(timer); 19 | } 20 | timer = setTimeout(() => { 21 | binding.value(); 22 | }, 500); 23 | }; 24 | el.addEventListener('click', el.__handleClick__); 25 | }, 26 | beforeUnmount(el: ElType) { 27 | el.removeEventListener('click', el.__handleClick__); 28 | } 29 | }; 30 | 31 | export default debounce; 32 | -------------------------------------------------------------------------------- /src/directives/modules/draggable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。 3 | 4 | 思路: 5 | 1、设置需要拖拽的元素为absolute,其父元素为relative。 6 | 2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。 7 | 3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值 8 | 4、鼠标松开(onmouseup)时完成一次拖拽 9 | 10 | 使用:在 Dom 上加上 v-draggable 即可 11 |
12 | */ 13 | import type { Directive } from 'vue'; 14 | interface ElType extends HTMLElement { 15 | parentNode: any; 16 | } 17 | const draggable: Directive = { 18 | mounted: function (el: ElType) { 19 | el.style.cursor = 'move'; 20 | el.style.position = 'absolute'; 21 | el.onmousedown = function (e) { 22 | const disX = e.pageX - el.offsetLeft; 23 | const disY = e.pageY - el.offsetTop; 24 | document.onmousemove = function (e) { 25 | let x = e.pageX - disX; 26 | let y = e.pageY - disY; 27 | const maxX = el.parentNode.offsetWidth - el.offsetWidth; 28 | const maxY = el.parentNode.offsetHeight - el.offsetHeight; 29 | if (x < 0) { 30 | x = 0; 31 | } else if (x > maxX) { 32 | x = maxX; 33 | } 34 | 35 | if (y < 0) { 36 | y = 0; 37 | } else if (y > maxY) { 38 | y = maxY; 39 | } 40 | el.style.left = x + 'px'; 41 | el.style.top = y + 'px'; 42 | }; 43 | document.onmouseup = function () { 44 | document.onmousemove = document.onmouseup = null; 45 | }; 46 | }; 47 | } 48 | }; 49 | export default draggable; 50 | -------------------------------------------------------------------------------- /src/directives/modules/longpress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * v-longpress 3 | * 长按指令,长按时触发事件 4 | */ 5 | import type { Directive, DirectiveBinding } from 'vue'; 6 | 7 | const directive: Directive = { 8 | mounted(el: HTMLElement, binding: DirectiveBinding) { 9 | if (typeof binding.value !== 'function') { 10 | throw 'callback must be a function'; 11 | } 12 | // 定义变量 13 | let pressTimer: any = null; 14 | // 创建计时器( 2秒后执行函数 ) 15 | const start = (e: any) => { 16 | if (e.button) { 17 | if (e.type === 'click' && e.button !== 0) { 18 | return; 19 | } 20 | } 21 | if (pressTimer === null) { 22 | pressTimer = setTimeout(() => { 23 | handler(e); 24 | }, 1000); 25 | } 26 | }; 27 | // 取消计时器 28 | const cancel = () => { 29 | if (pressTimer !== null) { 30 | clearTimeout(pressTimer); 31 | pressTimer = null; 32 | } 33 | }; 34 | // 运行函数 35 | const handler = (e: MouseEvent | TouchEvent) => { 36 | binding.value(e); 37 | }; 38 | // 添加事件监听器 39 | el.addEventListener('mousedown', start); 40 | el.addEventListener('touchstart', start); 41 | // 取消计时器 42 | el.addEventListener('click', cancel); 43 | el.addEventListener('mouseout', cancel); 44 | el.addEventListener('touchend', cancel); 45 | el.addEventListener('touchcancel', cancel); 46 | } 47 | }; 48 | 49 | export default directive; 50 | -------------------------------------------------------------------------------- /src/directives/modules/throttle.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。 3 | 4 | 思路: 5 | 1、第一次点击,立即调用方法并禁用按钮,等延迟结束再次激活按钮 6 | 2、将需要触发的方法绑定在指令上 7 | 8 | 使用:给 Dom 加上 v-throttle 及回调函数即可 9 | 10 | */ 11 | import type { Directive, DirectiveBinding } from 'vue'; 12 | interface ElType extends HTMLElement { 13 | __handleClick__: () => any; 14 | disabled: boolean; 15 | } 16 | const throttle: Directive = { 17 | mounted(el: ElType, binding: DirectiveBinding) { 18 | if (typeof binding.value !== 'function') { 19 | throw 'callback must be a function'; 20 | } 21 | let timer: NodeJS.Timeout | null = null; 22 | el.__handleClick__ = function () { 23 | if (timer) { 24 | clearTimeout(timer); 25 | } 26 | if (!el.disabled) { 27 | el.disabled = true; 28 | binding.value(); 29 | timer = setTimeout(() => { 30 | el.disabled = false; 31 | }, 1000); 32 | } 33 | }; 34 | el.addEventListener('click', el.__handleClick__); 35 | }, 36 | beforeUnmount(el: ElType) { 37 | el.removeEventListener('click', el.__handleClick__); 38 | } 39 | }; 40 | 41 | export default throttle; 42 | -------------------------------------------------------------------------------- /src/directives/modules/waterMarker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 需求:给整个页面添加背景水印。 3 | 4 | 思路: 5 | 1、使用 canvas 特性生成 base64 格式的图片文件,设置其字体大小,颜色等。 6 | 2、将其设置为背景图片,从而实现页面或组件水印效果 7 | 8 | 使用:设置水印文案,颜色,字体大小即可 9 |
10 | */ 11 | 12 | import type { Directive, DirectiveBinding } from 'vue'; 13 | const addWaterMarker: Directive = (str: string, parentNode: any, font: any, textColor: string) => { 14 | // 水印文字,父元素,字体,文字颜色 15 | const can: HTMLCanvasElement = document.createElement('canvas'); 16 | parentNode.appendChild(can); 17 | can.width = 205; 18 | can.height = 140; 19 | can.style.display = 'none'; 20 | const cans = can.getContext('2d') as CanvasRenderingContext2D; 21 | cans.rotate((-20 * Math.PI) / 180); 22 | cans.font = font || '16px Microsoft JhengHei'; 23 | cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'; 24 | cans.textAlign = 'left'; 25 | cans.textBaseline = 'Middle' as CanvasTextBaseline; 26 | cans.fillText(str, can.width / 10, can.height / 2); 27 | parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'; 28 | }; 29 | 30 | const waterMarker = { 31 | mounted(el: DirectiveBinding, binding: DirectiveBinding) { 32 | addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor); 33 | } 34 | }; 35 | 36 | export default waterMarker; 37 | -------------------------------------------------------------------------------- /src/hooks/interface/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-namespace 2 | export namespace Table { 3 | export interface Pageable { 4 | pageNum: number; 5 | pageSize: number; 6 | total: number; 7 | } 8 | export interface TableStateProps { 9 | tableData: any[]; 10 | pageable: Pageable; 11 | searchParam: { 12 | [key: string]: any; 13 | }; 14 | searchInitParam: { 15 | [key: string]: any; 16 | }; 17 | totalParam: { 18 | [key: string]: any; 19 | }; 20 | icon?: { 21 | [key: string]: any; 22 | }; 23 | } 24 | } 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-namespace 27 | export namespace HandleData { 28 | export type MessageType = '' | 'success' | 'warning' | 'info' | 'error'; 29 | } 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-namespace 32 | export namespace Theme { 33 | export type GreyOrWeakType = 'grey' | 'weak'; 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import { onDeactivated, onBeforeUnmount } from 'vue'; 2 | import * as echarts from 'echarts'; 3 | 4 | /** 5 | * @description 使用 Echarts (只是为了添加图表响应式) 6 | * @param {Element} myChart Echarts实例 (必传) 7 | * @param {Object} options 绘制Echarts的参数 (必传) 8 | * */ 9 | export const useEcharts = (myChart: echarts.ECharts, options: echarts.EChartsCoreOption) => { 10 | if (options && typeof options === 'object') { 11 | myChart.setOption(options); 12 | } 13 | const echartsResize = () => { 14 | myChart && myChart.resize(); 15 | }; 16 | 17 | window.addEventListener('resize', echartsResize); 18 | 19 | // 防止 echarts 页面 keepAlive 时,还在继续监听页面 20 | onDeactivated(() => { 21 | window.removeEventListener('resize', echartsResize); 22 | }); 23 | 24 | onBeforeUnmount(() => { 25 | window.removeEventListener('resize', echartsResize); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia'; 2 | import { Theme } from './interface'; 3 | import { ElMessage } from 'element-plus'; 4 | import { DEFAULT_PRIMARY } from '@/config'; 5 | import { useGlobalStore } from '@/stores/modules/global'; 6 | import { getLightColor, getDarkColor } from '@/utils/color'; 7 | import { asideTheme, AsideThemeType } from '@/styles/theme/aside'; 8 | 9 | /** 10 | * @description 全局主题 hooks 11 | * */ 12 | export const useTheme = () => { 13 | const globalStore = useGlobalStore(); 14 | const { primary, isDark, isGrey, isWeak, asideInverted, layout } = storeToRefs(globalStore); 15 | 16 | // 切换暗黑模式 ==> 并带修改主题颜色、侧边栏颜色 17 | const switchDark = () => { 18 | const html = document.documentElement as HTMLElement; 19 | if (isDark.value) html.setAttribute('class', 'dark'); 20 | else html.setAttribute('class', ''); 21 | changePrimary(primary.value); 22 | setAsideTheme(); 23 | }; 24 | 25 | // 修改主题颜色 26 | const changePrimary = (val: any | null) => { 27 | if (!val) { 28 | val = DEFAULT_PRIMARY; 29 | ElMessage({ type: 'success', message: `主题颜色已重置为 ${DEFAULT_PRIMARY}` }); 30 | } 31 | val = val.hex ? val.hex : val; 32 | // 计算主题颜色变化 33 | document.documentElement.style.setProperty('--el-color-primary', val); 34 | document.documentElement.style.setProperty( 35 | '--el-color-primary-dark-2', 36 | isDark.value ? `${getLightColor(val, 0.2)}` : `${getDarkColor(val, 0.3)}` 37 | ); 38 | for (let i = 1; i <= 9; i++) { 39 | const primaryColor = isDark.value ? `${getDarkColor(val, i / 10)}` : `${getLightColor(val, i / 10)}`; 40 | document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, primaryColor); 41 | } 42 | globalStore.setGlobalState('primary', val); 43 | }; 44 | 45 | // 灰色和弱色切换 46 | const changeGreyOrWeak = (type: Theme.GreyOrWeakType, value: boolean) => { 47 | const body = document.body as HTMLElement; 48 | if (!value) return body.removeAttribute('style'); 49 | const styles: Record = { 50 | grey: 'filter: grayscale(1)', 51 | weak: 'filter: invert(80%)' 52 | }; 53 | body.setAttribute('style', styles[type]); 54 | const propName = type === 'grey' ? 'isWeak' : 'isGrey'; 55 | globalStore.setGlobalState(propName, false); 56 | }; 57 | 58 | // 设置侧边栏样式 ==> light、inverted、dark 59 | const setAsideTheme = () => { 60 | // 默认所有侧边栏为 light 模式 61 | let type: AsideThemeType = 'light'; 62 | // transverse 布局下菜单栏为 inverted 模式 63 | if (layout.value == 'transverse') type = 'inverted'; 64 | // 侧边栏反转色目前只支持在 vertical 布局模式下生效 65 | if (layout.value == 'vertical' && asideInverted.value) type = 'inverted'; 66 | // 侧边栏 dark 模式 67 | if (isDark.value) type = 'dark'; 68 | const theme = asideTheme[type!]; 69 | for (const [key, value] of Object.entries(theme)) { 70 | document.documentElement.style.setProperty(key, value); 71 | } 72 | }; 73 | 74 | // init theme 75 | const initTheme = () => { 76 | switchDark(); 77 | if (isGrey.value) changeGreyOrWeak('grey', true); 78 | if (isWeak.value) changeGreyOrWeak('weak', true); 79 | }; 80 | 81 | return { 82 | initTheme, 83 | switchDark, 84 | changePrimary, 85 | changeGreyOrWeak, 86 | setAsideTheme 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import { getBrowserLang } from '@/utils'; 3 | 4 | import zh from './modules/zh'; 5 | import en from './modules/en'; 6 | 7 | const i18n = createI18n({ 8 | allowComposition: true, 9 | legacy: false, 10 | locale: getBrowserLang(), 11 | messages: { 12 | zh, 13 | en 14 | } 15 | }); 16 | 17 | export default i18n; 18 | -------------------------------------------------------------------------------- /src/i18n/modules/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | home: { 3 | welcome: 'Welcome' 4 | }, 5 | tabs: { 6 | more: 'More', 7 | refresh: 'Refresh', 8 | maximize: 'Maximize', 9 | closeCurrent: 'Close current', 10 | closeOther: 'Close other', 11 | closeAll: 'Close All' 12 | }, 13 | header: { 14 | componentSize: 'Component size', 15 | language: 'Language', 16 | theme: 'theme', 17 | layoutConfig: 'Layout config', 18 | primary: 'primary', 19 | darkMode: 'Dark Mode', 20 | greyMode: 'Grey mode', 21 | weakMode: 'Weak mode', 22 | fullScreen: 'Full Screen', 23 | exitFullScreen: 'Exit Full Screen', 24 | personalData: 'Personal Data', 25 | changePassword: 'Change Password', 26 | logout: 'Logout' 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/i18n/modules/zh.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | home: { 3 | welcome: '欢迎使用' 4 | }, 5 | tabs: { 6 | more: '更多', 7 | refresh: '刷新', 8 | maximize: '最大化', 9 | closeCurrent: '关闭当前', 10 | closeOther: '关闭其它', 11 | closeAll: '关闭所有' 12 | }, 13 | header: { 14 | componentSize: '组件大小', 15 | language: '国际化', 16 | theme: '全局主题', 17 | layoutConfig: '布局设置', 18 | primary: 'primary', 19 | darkMode: '暗黑模式', 20 | greyMode: '灰色模式', 21 | weakMode: '色弱模式', 22 | fullScreen: '全屏', 23 | exitFullScreen: '退出全屏', 24 | personalData: '个人信息', 25 | changePassword: '修改密码', 26 | logout: '退出登录' 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/layouts/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/layouts/components/Header/ToolBarLeft.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/layouts/components/Header/ToolBarRight.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 35 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/AssemblySize.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Avatar.vue: -------------------------------------------------------------------------------- 1 | 28 | 59 | 78 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 45 | 46 | 88 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Fullscreen.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Language.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/ThemeSetting.vue: -------------------------------------------------------------------------------- 1 | 6 | 12 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutClassic.vue: -------------------------------------------------------------------------------- 1 | 2 | 38 | 39 | 58 | 59 | 142 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutTransverse.vue: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 58 | 59 | 130 | -------------------------------------------------------------------------------- /src/layouts/components/LayoutVertical.vue: -------------------------------------------------------------------------------- 1 | 2 | 32 | 33 | 52 | 53 | 109 | -------------------------------------------------------------------------------- /src/layouts/components/Main.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 95 | 96 | 132 | -------------------------------------------------------------------------------- /src/layouts/components/Menu/SubMenu.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 92 | -------------------------------------------------------------------------------- /src/layouts/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from '@/App.vue'; 3 | import ElementPlus from 'element-plus'; 4 | import VueGridLayout from 'vue-grid-layout'; 5 | // icon-park 6 | import { install } from '@icon-park/vue-next/es/all'; 7 | // element icons 8 | import * as Icons from '@element-plus/icons-vue'; 9 | // custom directives 10 | import directives from '@/directives'; 11 | // vue Router 12 | import router from '@/router'; 13 | // pinia store 14 | import pinia from '@/stores'; 15 | // vue i18n 16 | import I18n from '@/i18n'; 17 | import 'vue-global-api'; 18 | // 额外引入图标库 19 | import 'element-plus/dist/index.css'; 20 | import 'element-plus/theme-chalk/dark/css-vars.css'; 21 | import '@icon-park/vue-next/styles/index.css'; 22 | import '@fancyapps/ui/dist/fancybox/fancybox.css'; 23 | import '@/styles/index.scss'; 24 | import 'uno.css'; 25 | 26 | import '@/utils/system-copyright'; 27 | 28 | const app = createApp(App); 29 | install(app, 'i-bd'); 30 | // register the element Icons component 31 | Object.keys(Icons).forEach(key => { 32 | app.component(key, Icons[key as keyof typeof Icons]); 33 | }); 34 | app.use(ElementPlus); 35 | app.use(VueGridLayout); 36 | app.use(directives); 37 | app.use(router); 38 | app.use(I18n); 39 | app.use(pinia); 40 | app.mount('#app'); 41 | -------------------------------------------------------------------------------- /src/menu/index.ts: -------------------------------------------------------------------------------- 1 | const menuModuleList: any[] = []; 2 | const modules = import.meta.glob('./modules/**/*.ts', { import: 'default', eager: true }); 3 | Object.keys(modules).forEach(key => { 4 | const mod = modules[key] || {}; 5 | const modList = Array.isArray(mod) ? [...mod] : [mod]; 6 | menuModuleList.push(...modList); 7 | }); 8 | // 按照index 升序 9 | menuModuleList.sort((a, b) => { 10 | return a.meta.index - b.meta.index; 11 | }); 12 | export default [...menuModuleList]; 13 | -------------------------------------------------------------------------------- /src/menu/modules/group.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/group/adduser', 3 | name: 'group', 4 | path: '/group', 5 | meta: { 6 | icon: 'i-bd-peoples-two', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 3, 13 | title: '群组' 14 | }, 15 | children: [ 16 | { 17 | component: '/group/grouplist', 18 | name: 'groupGrouplist', 19 | path: '/group/grouplist', 20 | meta: { 21 | icon: 'i-bd-group', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '群列表' 28 | } 29 | }, 30 | { 31 | component: '/group/groupdisablelist', 32 | name: 'groupGroupdisablelist', 33 | path: '/group/groupdisablelist', 34 | meta: { 35 | icon: 'i-bd-ungroup', 36 | isAffix: false, 37 | isFull: false, 38 | isHide: false, 39 | isKeepAlive: true, 40 | isLink: '', 41 | title: '封禁群列表' 42 | } 43 | } 44 | ] 45 | }; 46 | export default home; 47 | -------------------------------------------------------------------------------- /src/menu/modules/home.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/home/index', 3 | name: 'homeIndex', 4 | path: '/home', 5 | meta: { 6 | icon: 'i-bd-home', 7 | isAffix: true, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 1, 13 | title: '首页' 14 | } 15 | }; 16 | export default home; 17 | -------------------------------------------------------------------------------- /src/menu/modules/message.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/message/sendmsglist', 3 | name: 'message', 4 | path: '/message', 5 | meta: { 6 | icon: 'i-bd-message', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 4, 13 | title: '通知' 14 | }, 15 | children: [ 16 | { 17 | component: '/message/sendmsglist', 18 | name: 'messageSendmsglist', 19 | path: '/message/sendmsglist', 20 | meta: { 21 | icon: 'i-bd-communication', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '通知记录' 28 | } 29 | }, 30 | { 31 | component: '/message/prohibitwords', 32 | name: 'messageProhibitwords', 33 | path: '/message/prohibitwords', 34 | meta: { 35 | icon: 'i-bd-message-security', 36 | isAffix: false, 37 | isFull: false, 38 | isHide: false, 39 | isKeepAlive: true, 40 | isLink: '', 41 | title: '违禁词列表' 42 | } 43 | } 44 | ] 45 | }; 46 | export default home; 47 | -------------------------------------------------------------------------------- /src/menu/modules/redpacket.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/redpacket/list', 3 | name: 'redpacket', 4 | path: '/redpacket', 5 | meta: { 6 | icon: 'i-bd-red-envelopes', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: true, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 5, 13 | title: '红包' 14 | }, 15 | children: [ 16 | { 17 | component: '/redpacket/list', 18 | name: 'redpacketList', 19 | path: '/redpacket/list', 20 | meta: { 21 | icon: 'i-bd-doc-search', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '红包记录' 28 | } 29 | } 30 | ] 31 | }; 32 | export default home; 33 | -------------------------------------------------------------------------------- /src/menu/modules/report.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/report/user', 3 | name: 'report', 4 | path: '/report', 5 | meta: { 6 | icon: 'i-bd-report', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 6, 13 | title: '举报' 14 | }, 15 | children: [ 16 | { 17 | component: '/report/user', 18 | name: 'reportUser', 19 | path: '/report/user', 20 | meta: { 21 | icon: 'i-bd-wrong-user', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '举报用户' 28 | } 29 | }, 30 | { 31 | component: '/report/group', 32 | name: 'reportGroup', 33 | path: '/report/group', 34 | meta: { 35 | icon: 'i-bd-user-to-user-transmission', 36 | isAffix: false, 37 | isFull: false, 38 | isHide: false, 39 | isKeepAlive: true, 40 | isLink: '', 41 | title: '举报群聊' 42 | } 43 | } 44 | ] 45 | }; 46 | export default home; 47 | -------------------------------------------------------------------------------- /src/menu/modules/setting.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/setting/currencysetting', 3 | name: 'setting', 4 | path: '/setting', 5 | meta: { 6 | icon: 'i-bd-setting', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 9, 13 | title: '设置' 14 | }, 15 | children: [ 16 | { 17 | component: '/setting/currencysetting', 18 | name: 'settingCurrencysetting', 19 | path: '/setting/currencysetting', 20 | meta: { 21 | icon: 'i-bd-setting-config', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '通用设置' 28 | } 29 | }, 30 | { 31 | component: '/setting/updatepwd', 32 | name: 'settingUpdatepwd', 33 | path: '/setting/updatepwd', 34 | meta: { 35 | icon: 'i-bd-shield', 36 | isAffix: false, 37 | isFull: false, 38 | isHide: false, 39 | isKeepAlive: true, 40 | isLink: '', 41 | title: '修改登录密码' 42 | } 43 | } 44 | ] 45 | }; 46 | export default home; 47 | -------------------------------------------------------------------------------- /src/menu/modules/tool.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/tool/appupdate', 3 | name: 'tool', 4 | path: '/tool', 5 | meta: { 6 | icon: 'i-bd-tool', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 8, 13 | title: '工具' 14 | }, 15 | children: [ 16 | { 17 | component: '/tool/appupdate', 18 | name: 'toolAppupdate', 19 | path: '/tool/appupdate', 20 | meta: { 21 | icon: 'i-bd-application-one', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: 'APP升级' 28 | } 29 | } 30 | ] 31 | }; 32 | export default home; 33 | -------------------------------------------------------------------------------- /src/menu/modules/user.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | component: '/user/adduser', 3 | name: 'use', 4 | path: '/user', 5 | meta: { 6 | icon: 'i-bd-user', 7 | isAffix: false, 8 | isFull: false, 9 | isHide: false, 10 | isKeepAlive: true, 11 | isLink: '', 12 | index: 2, 13 | title: '用户' 14 | }, 15 | children: [ 16 | { 17 | component: '/user/adduser', 18 | name: 'userAdduser', 19 | path: '/user/adduser', 20 | meta: { 21 | icon: 'i-bd-add-user', 22 | isAffix: false, 23 | isFull: false, 24 | isHide: false, 25 | isKeepAlive: true, 26 | isLink: '', 27 | title: '新增用户' 28 | } 29 | }, 30 | { 31 | component: '/user/userlist', 32 | name: 'userUserlist', 33 | path: '/user/userlist', 34 | meta: { 35 | icon: 'i-bd-user', 36 | isAffix: false, 37 | isFull: false, 38 | isHide: false, 39 | isKeepAlive: true, 40 | isLink: '', 41 | title: '用户列表' 42 | } 43 | }, 44 | { 45 | component: '/user/disablelist', 46 | name: 'userDisablelist', 47 | path: '/user/disablelist', 48 | meta: { 49 | icon: 'i-bd-wrong-user', 50 | isAffix: false, 51 | isFull: false, 52 | isHide: false, 53 | isKeepAlive: true, 54 | isLink: '', 55 | title: '封禁用户列表' 56 | } 57 | }, 58 | { 59 | component: '/user/administrator', 60 | name: 'userAdministrator', 61 | path: '/user/administrator', 62 | meta: { 63 | icon: 'i-bd-user-business', 64 | isAffix: false, 65 | isFull: false, 66 | isHide: false, 67 | isKeepAlive: true, 68 | isLink: '', 69 | auth: ['superAdmin'], 70 | title: '管理员' 71 | } 72 | } 73 | ] 74 | }; 75 | export default home; 76 | -------------------------------------------------------------------------------- /src/menu/modules/workplace.ts: -------------------------------------------------------------------------------- 1 | const home: Menu.MenuOptions = { 2 | name: 'tool', 3 | path: '/workplace', 4 | meta: { 5 | icon: 'i-bd-all-application', 6 | isAffix: false, 7 | isFull: false, 8 | isHide: false, 9 | isKeepAlive: true, 10 | isLink: '', 11 | index: 7, 12 | title: '工作台' 13 | }, 14 | children: [ 15 | { 16 | name: 'workplaceManage', 17 | path: '/workplace/manage', 18 | meta: { 19 | icon: 'i-bd-application', 20 | isAffix: false, 21 | isFull: false, 22 | isHide: false, 23 | isKeepAlive: true, 24 | isLink: '', 25 | title: '应用管理' 26 | } 27 | }, 28 | { 29 | name: 'workplaceConfiguration', 30 | path: '/workplace/configuration', 31 | meta: { 32 | icon: 'i-bd-setting-config', 33 | isAffix: false, 34 | isFull: false, 35 | isHide: false, 36 | isKeepAlive: true, 37 | isLink: '', 38 | title: '工作台设置' 39 | } 40 | } 41 | ] 42 | }; 43 | export default home; 44 | -------------------------------------------------------------------------------- /src/pages/home/components/AddGroup.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 152 | 153 | 184 | -------------------------------------------------------------------------------- /src/pages/home/components/AddUser.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 130 | 131 | 160 | -------------------------------------------------------------------------------- /src/pages/home/components/Statistics.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /src/pages/home/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | meta: 22 | title: 首页 23 | isAffix: true 24 | 25 | 26 | 123 | 124 | 129 | -------------------------------------------------------------------------------- /src/pages/message/components/Devices.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 98 | -------------------------------------------------------------------------------- /src/pages/my/bb.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | meta: 9 | title: 页面标题 10 | 11 | 12 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /src/pages/report/group.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | meta: 52 | title: 举报群聊 53 | isAffix: false 54 | 55 | 56 | 161 | 162 | 168 | -------------------------------------------------------------------------------- /src/pages/report/user.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | meta: 52 | title: 举报用户 53 | isAffix: false 54 | 55 | 56 | 161 | 162 | 168 | -------------------------------------------------------------------------------- /src/pages/setting/updatepwd.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | meta: 36 | title: 修改登录密码 37 | 38 | 39 | 110 | 111 | 116 | -------------------------------------------------------------------------------- /src/pages/tool/appupdate.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 59 | meta: 60 | title: APP升级 61 | isAffix: false 62 | 63 | 64 | 146 | 147 | 153 | -------------------------------------------------------------------------------- /src/pages/user/adduser.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | meta: 46 | title: 添加用户 47 | 48 | 49 | 79 | 80 | 85 | -------------------------------------------------------------------------------- /src/pages/user/administrator/components/AdminAdd.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 111 | -------------------------------------------------------------------------------- /src/pages/user/administrator/index.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 160 | 161 | 167 | 168 | 169 | meta: 170 | title: 管理员 171 | isAffix: false 172 | 173 | -------------------------------------------------------------------------------- /src/pages/user/userblacklist.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 63 | meta: 64 | title: 黑名单列表 65 | isAffix: false 66 | 67 | 68 | 157 | 158 | 164 | -------------------------------------------------------------------------------- /src/pages/workplace/configuration/components/CategoryDialog.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 134 | -------------------------------------------------------------------------------- /src/pages/workplace/configuration/components/Recommend.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 155 | 156 | 162 | -------------------------------------------------------------------------------- /src/pages/workplace/configuration/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | meta: 15 | title: 工作台设置 16 | isAffix: false 17 | 18 | 19 | 42 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import { useUserStore } from '@/stores/modules/user'; 3 | import { useAuthStore } from '@/stores/modules/auth'; 4 | import { LOGIN_URL, ROUTER_WHITE_LIST } from '@/config'; 5 | import routes from './routers'; 6 | import NProgress from '@/utils/nprogress'; 7 | /** 8 | * @description 📚 路由参数配置简介 9 | * @param path ==> 菜单路径 10 | * @param name ==> 菜单别名 11 | * @param redirect ==> 重定向地址 12 | * @param component ==> 视图文件路径 13 | * @param meta ==> 菜单信息 14 | * @param meta.icon ==> 菜单图标 15 | * @param meta.title ==> 菜单标题 16 | * @param meta.activeMenu ==> 当前路由为详情页时,需要高亮的菜单 17 | * @param meta.isLink ==> 是否外链 18 | * @param meta.isHide ==> 是否隐藏 19 | * @param meta.isFull ==> 是否全屏(示例:数据大屏页面) 20 | * @param meta.isAffix ==> 是否固定在 tabs nav 21 | * @param meta.isKeepAlive ==> 是否缓存 22 | * */ 23 | const router = createRouter({ 24 | history: createWebHistory(), 25 | routes, 26 | strict: false, 27 | scrollBehavior: () => ({ left: 0, top: 0 }) 28 | }); 29 | 30 | /** 31 | * @description 路由拦截 beforeEach 32 | * */ 33 | router.beforeEach(async (to, from, next) => { 34 | const authStore = useAuthStore(); 35 | const userStore = useUserStore(); 36 | // NProgress 开始 37 | NProgress.start(); 38 | 39 | /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */ 40 | function toCorrectRoute() { 41 | ROUTER_WHITE_LIST.includes(to.fullPath) ? next(from.fullPath) : next(); 42 | } 43 | 44 | if (userStore.token) { 45 | // 正常访问页面 46 | if (!authStore.authMenuListGet.length) { 47 | await authStore.getAuthMenuList(); 48 | } 49 | toCorrectRoute(); 50 | } else { 51 | if (to.path !== LOGIN_URL) { 52 | if (ROUTER_WHITE_LIST.indexOf(to.path) !== -1) { 53 | next(); 54 | } else { 55 | next({ path: LOGIN_URL, replace: true }); 56 | } 57 | } else { 58 | next(); 59 | } 60 | } 61 | }); 62 | /** 63 | * @description 路由跳转错误 64 | * */ 65 | router.onError(error => { 66 | NProgress.done(); 67 | console.warn('路由错误', error.message); 68 | }); 69 | /** 70 | * @description 路由跳转结束 71 | * */ 72 | router.afterEach(() => { 73 | NProgress.done(); 74 | }); 75 | 76 | /** 77 | * @description 重置路由 78 | * */ 79 | export const resetRouter = () => { 80 | const authStore = useAuthStore(); 81 | authStore.flatMenuListGet.forEach(route => { 82 | const { name } = route; 83 | if (name && router.hasRoute(name)) router.removeRoute(name); 84 | }); 85 | }; 86 | 87 | export default router; 88 | -------------------------------------------------------------------------------- /src/router/routers.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | import { setupLayouts } from 'virtual:meta-layouts'; 3 | import generatedRoutes from 'virtual:generated-pages'; 4 | import { staticRouter } from '@/router/staticRouter'; 5 | 6 | const routes: RouteRecordRaw[] = setupLayouts( 7 | generatedRoutes.filter(item => { 8 | return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false; 9 | }) 10 | ); 11 | 12 | export default [...staticRouter, ...routes]; 13 | -------------------------------------------------------------------------------- /src/router/staticRouter.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | import { HOME_URL, LOGIN_URL } from '@/config'; 3 | 4 | /** 5 | * staticRouter (静态路由) 6 | */ 7 | export const staticRouter: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | redirect: HOME_URL 11 | }, 12 | { 13 | path: LOGIN_URL, 14 | name: 'login', 15 | component: () => import('@/pages/login/index.vue'), 16 | meta: { 17 | title: '登录' 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 3 | 4 | const pinia = createPinia(); 5 | pinia.use(piniaPluginPersistedstate); 6 | 7 | export default pinia; 8 | -------------------------------------------------------------------------------- /src/stores/interface/index.ts: -------------------------------------------------------------------------------- 1 | export type LayoutType = 'vertical' | 'classic' | 'transverse' | 'columns'; 2 | 3 | export type AssemblySizeType = 'large' | 'default' | 'small'; 4 | 5 | export type LanguageType = 'zh' | 'en' | null; 6 | 7 | /* GlobalState */ 8 | export interface GlobalState { 9 | layout: LayoutType; 10 | assemblySize: AssemblySizeType; 11 | language: LanguageType; 12 | maximize: boolean; 13 | primary: string; 14 | isDark: boolean; 15 | isGrey: boolean; 16 | isWeak: boolean; 17 | asideInverted: boolean; 18 | isCollapse: boolean; 19 | breadcrumb: boolean; 20 | breadcrumbIcon: boolean; 21 | tabs: boolean; 22 | tabsIcon: boolean; 23 | footer: boolean; 24 | } 25 | 26 | /* UserState */ 27 | export interface UserState { 28 | token: string; 29 | userInfo: { name: string; uid: string; role?: Menu.IRole }; 30 | } 31 | 32 | /* tabsMenuProps */ 33 | export interface TabsMenuProps { 34 | icon: string; 35 | title: string; 36 | path: string; 37 | name: string; 38 | close: boolean; 39 | isKeepAlive: boolean; 40 | } 41 | 42 | /* TabsState */ 43 | export interface TabsState { 44 | tabsMenuList: TabsMenuProps[]; 45 | } 46 | 47 | /* AuthState */ 48 | export interface AuthState { 49 | routeName: string; 50 | authButtonList: { 51 | [key: string]: string[]; 52 | }; 53 | authMenuList: Menu.MenuOptions[]; 54 | } 55 | 56 | /* KeepAliveState */ 57 | export interface KeepAliveState { 58 | keepAliveName: string[]; 59 | } 60 | -------------------------------------------------------------------------------- /src/stores/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { AuthState } from '@/stores/interface'; 3 | import menu from '@/menu'; 4 | import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList } from '@/utils'; 5 | 6 | import { useUserStore } from './user'; 7 | 8 | /** 9 | * 菜单权限处理 10 | * @param menuList 11 | */ 12 | function getAuthMenu(menuList: Menu.MenuOptions[]) { 13 | const newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList)); 14 | const userStore = useUserStore(); 15 | const role = userStore.userInfo?.role; 16 | 17 | return newMenuList.filter(item => { 18 | item.children?.length && (item.children = getAuthMenu(item.children)); 19 | if (item.meta?.auth && role) { 20 | return item.meta?.auth.indexOf(role) !== -1; 21 | } 22 | return item; 23 | }); 24 | } 25 | 26 | export const useAuthStore = defineStore({ 27 | id: 'budou-auth', 28 | state: (): AuthState => ({ 29 | // 按钮权限列表 30 | authButtonList: {}, 31 | // 菜单权限列表 32 | authMenuList: [], 33 | // 当前页面的 router name,用来做按钮权限筛选 34 | routeName: '' 35 | }), 36 | getters: { 37 | // 按钮权限列表 38 | authButtonListGet: state => state.authButtonList, 39 | // 菜单权限列表 ==> 这里的菜单没有经过任何处理 40 | authMenuListGet: state => state.authMenuList, 41 | // 菜单权限列表 ==> 左侧菜单栏渲染,需要剔除 isHide == true 42 | showMenuListGet: state => getShowMenuList(state.authMenuList), 43 | // 菜单权限列表 ==> 扁平化之后的一维数组菜单,主要用来添加动态路由 44 | flatMenuListGet: state => getFlatMenuList(state.authMenuList), 45 | // 递归处理后的所有面包屑导航列表 46 | breadcrumbListGet: state => getAllBreadcrumbList(state.authMenuList) 47 | }, 48 | actions: { 49 | // Get AuthButtonList 50 | async getAuthButtonList() { 51 | this.authButtonList = {}; 52 | }, 53 | // Get AuthMenuList 54 | async getAuthMenuList() { 55 | this.authMenuList = getAuthMenu(menu); 56 | }, 57 | 58 | // Set RouteName 59 | async setRouteName(name: string) { 60 | this.routeName = name; 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /src/stores/modules/global.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { GlobalState } from '@/stores/interface'; 3 | import { DEFAULT_PRIMARY } from '@/config'; 4 | import piniaPersistConfig from '@/utils/piniaPersist'; 5 | 6 | export const useGlobalStore = defineStore({ 7 | id: 'budou-global', 8 | // 修改默认值之后,需清除 localStorage 数据 9 | state: (): GlobalState => ({ 10 | // 布局模式 (纵向:vertical | 经典:classic | 横向:transverse | 分栏:columns) 11 | layout: 'columns', 12 | // element 组件大小 13 | assemblySize: 'default', 14 | // 当前系统语言 15 | language: null, 16 | // 当前页面是否全屏 17 | maximize: false, 18 | // 主题颜色 19 | primary: DEFAULT_PRIMARY, 20 | // 深色模式 21 | isDark: false, 22 | // 灰色模式 23 | isGrey: false, 24 | // 色弱模式 25 | isWeak: false, 26 | // 侧边栏反转 (目前仅支持 'vertical' 模式) 27 | asideInverted: false, 28 | // 折叠菜单 29 | isCollapse: false, 30 | // 面包屑导航 31 | breadcrumb: true, 32 | // 面包屑导航图标 33 | breadcrumbIcon: true, 34 | // 标签页 35 | tabs: true, 36 | // 标签页图标 37 | tabsIcon: true, 38 | // 页脚 39 | footer: true 40 | }), 41 | getters: {}, 42 | actions: { 43 | // Set GlobalState 44 | setGlobalState(...args: ObjToKeyValArray) { 45 | this.$patch({ [args[0]]: args[1] }); 46 | } 47 | }, 48 | persist: piniaPersistConfig('budou-global') 49 | }); 50 | -------------------------------------------------------------------------------- /src/stores/modules/keepAlive.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { KeepAliveState } from '@/stores/interface'; 3 | 4 | export const useKeepAliveStore = defineStore({ 5 | id: 'budou-keepAlive', 6 | state: (): KeepAliveState => ({ 7 | keepAliveName: [] 8 | }), 9 | actions: { 10 | // Add KeepAliveName 11 | async addKeepAliveName(name: string) { 12 | !this.keepAliveName.includes(name) && this.keepAliveName.push(name); 13 | }, 14 | // Remove KeepAliveName 15 | async removeKeepAliveName(name: string) { 16 | this.keepAliveName = this.keepAliveName.filter(item => item !== name); 17 | }, 18 | // Set KeepAliveName 19 | async setKeepAliveName(keepAliveName: string[] = []) { 20 | this.keepAliveName = keepAliveName; 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/stores/modules/tabs.ts: -------------------------------------------------------------------------------- 1 | import router from '@/router'; 2 | import { defineStore } from 'pinia'; 3 | import { TabsState, TabsMenuProps } from '@/stores/interface'; 4 | import piniaPersistConfig from '@/utils/piniaPersist'; 5 | 6 | import { useKeepAliveStore } from './keepAlive'; 7 | const keepAliveStore = useKeepAliveStore(); 8 | 9 | export const useTabsStore = defineStore({ 10 | id: 'budou-tabs', 11 | state: (): TabsState => ({ 12 | tabsMenuList: [] 13 | }), 14 | actions: { 15 | // Add Tabs 16 | async addTabs(tabItem: TabsMenuProps) { 17 | if (this.tabsMenuList.every(item => item.path !== tabItem.path)) { 18 | this.tabsMenuList.push(tabItem); 19 | } 20 | 21 | if (!keepAliveStore.keepAliveName.includes(tabItem.name) && tabItem.isKeepAlive) { 22 | await keepAliveStore.addKeepAliveName(tabItem.path); 23 | } 24 | }, 25 | // Remove Tabs 26 | async removeTabs(tabPath: string, isCurrent = true) { 27 | const tabsMenuList = this.tabsMenuList; 28 | if (isCurrent) { 29 | tabsMenuList.forEach((item, index) => { 30 | if (item.path !== tabPath) return; 31 | const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1]; 32 | if (!nextTab) return; 33 | router.push(nextTab.path); 34 | }); 35 | } 36 | this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath); 37 | }, 38 | // Close MultipleTab 39 | async closeMultipleTab(tabsMenuValue?: string) { 40 | this.tabsMenuList = this.tabsMenuList.filter(item => { 41 | return item.path === tabsMenuValue || !item.close; 42 | }); 43 | 44 | const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive); 45 | await keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.path)); 46 | }, 47 | // Set Tabs 48 | async setTabs(tabsMenuList: TabsMenuProps[]) { 49 | this.tabsMenuList = tabsMenuList; 50 | }, 51 | // Set Tabs Title 52 | async setTabsTitle(title: string) { 53 | const nowFullPath = location.hash.substring(1); 54 | this.tabsMenuList.forEach(item => { 55 | if (item.path == nowFullPath) item.title = title; 56 | }); 57 | } 58 | }, 59 | persist: piniaPersistConfig('budou-tabs') 60 | }); 61 | -------------------------------------------------------------------------------- /src/stores/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { UserState } from '@/stores/interface'; 3 | import piniaPersistConfig from '@/utils/piniaPersist'; 4 | 5 | export const useUserStore = defineStore({ 6 | id: 'budou-user', 7 | state: (): UserState => ({ 8 | token: '', 9 | userInfo: { name: '您好,超管', uid: '' } 10 | }), 11 | getters: {}, 12 | actions: { 13 | // Set Token 14 | setToken(token: string) { 15 | this.token = token; 16 | }, 17 | // Set setUserInfo 18 | setUserInfo(userInfo: UserState['userInfo']) { 19 | this.userInfo = userInfo; 20 | } 21 | }, 22 | persist: piniaPersistConfig('budou-user') 23 | }); 24 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './reset.scss'; 2 | @import './theme/element-dark.scss'; 3 | @import './element.scss'; 4 | 5 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-size: 14px; 4 | font-family: v-sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 5 | 'Segoe UI Symbol'; 6 | line-height: 1.6; 7 | -webkit-text-size-adjust: 100%; 8 | -webkit-tap-highlight-color: transparent; 9 | } 10 | 11 | html, 12 | body, 13 | #app { 14 | width: 100%; 15 | height: 100%; 16 | padding: 0; 17 | margin: 0; 18 | } 19 | 20 | /* fade-transform */ 21 | .fade-transform-leave-active, 22 | .fade-transform-enter-active { 23 | transition: all 0.2s; 24 | } 25 | .fade-transform-enter-from { 26 | opacity: 0; 27 | transition: all 0.2s; 28 | transform: translateX(-30px); 29 | } 30 | .fade-transform-leave-to { 31 | opacity: 0; 32 | transition: all 0.2s; 33 | transform: translateX(30px); 34 | } 35 | 36 | /* 解决 h1 标签在 webkit 内核浏览器中文字大小失效问题 */ 37 | :-webkit-any(article, aside, nav, section) h1 { 38 | font-size: 2em; 39 | } 40 | 41 | /* 42 | 滚动条美化 43 | */ 44 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ 45 | // ::-webkit-scrollbar { 46 | // width: 7px; 47 | // height: 7px; 48 | // background-color: #78767628; 49 | // } 50 | 51 | // // /*定义滑块 内阴影+圆角*/ 52 | // ::-webkit-scrollbar-thumb { 53 | // border-radius: 10px; 54 | // box-shadow: inset 0 0 6px #868687; 55 | // -webkit-box-shadow: inset 0 0 6px #868687; 56 | // background-color: #2080f0; 57 | // } 58 | 59 | /* scroll bar */ 60 | ::-webkit-scrollbar { 61 | width: 6px; 62 | height: 6px; 63 | } 64 | ::-webkit-scrollbar-thumb { 65 | background-color: var(--el-border-color-darker); 66 | border-radius: 20px; 67 | } 68 | 69 | /* nprogress */ 70 | #nprogress .bar { 71 | background: var(--el-color-primary) !important; 72 | } 73 | #nprogress .spinner-icon { 74 | border-top-color: var(--el-color-primary) !important; 75 | border-left-color: var(--el-color-primary) !important; 76 | } 77 | #nprogress .peg { 78 | box-shadow: 0 0 10px var(--el-color-primary), 0 0 5px var(--el-color-primary) !important; 79 | } 80 | -------------------------------------------------------------------------------- /src/styles/theme/aside.ts: -------------------------------------------------------------------------------- 1 | export type AsideThemeType = 'light' | 'inverted' | 'dark'; 2 | 3 | export const asideTheme: Record = { 4 | light: { 5 | '--el-logo-text-color': '#303133', 6 | '--el-menu-bg-color': '#ffffff', 7 | '--el-menu-hover-bg-color': '#cccccc', 8 | '--el-menu-active-bg-color': 'var(--el-color-primary-light-9)', 9 | '--el-menu-text-color': '#333333', 10 | '--el-menu-active-color': 'var(--el-color-primary)', 11 | '--el-menu-hover-text-color': '#333333', 12 | '--el-menu-horizontal-sub-item-height': '55px' 13 | }, 14 | inverted: { 15 | '--el-logo-text-color': '#dadada', 16 | '--el-menu-bg-color': '#191a20', 17 | '--el-menu-hover-bg-color': '#000000', 18 | '--el-menu-active-bg-color': '#000000', 19 | '--el-menu-text-color': '#bdbdc0', 20 | '--el-menu-active-color': '#ffffff', 21 | '--el-menu-hover-text-color': '#ffffff', 22 | '--el-menu-horizontal-sub-item-height': '55px' 23 | }, 24 | dark: { 25 | '--el-logo-text-color': '#dadada', 26 | '--el-menu-bg-color': '#141414', 27 | '--el-menu-hover-bg-color': '#000000', 28 | '--el-menu-active-bg-color': '#000000', 29 | '--el-menu-text-color': '#bdbdc0', 30 | '--el-menu-active-color': '#ffffff', 31 | '--el-menu-hover-text-color': '#ffffff', 32 | '--el-menu-horizontal-sub-item-height': '55px' 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/styles/theme/element-dark.scss: -------------------------------------------------------------------------------- 1 | /* 自定义 element 暗黑模式 */ 2 | html.dark { 3 | /* wangEditor */ 4 | --w-e-toolbar-color: #eeeeee; 5 | --w-e-toolbar-bg-color: #141414; 6 | --w-e-textarea-bg-color: #141414; 7 | --w-e-textarea-color: #eeeeee; 8 | 9 | /* login */ 10 | .login-container { 11 | background-color: #191919 !important; 12 | .login-box { 13 | background-color: rgb(0 0 0 / 80%) !important; 14 | .login-form { 15 | box-shadow: rgb(255 255 255 / 12%) 0 2px 10px 2px !important; 16 | .logo-text { 17 | color: var(--el-text-color-primary) !important; 18 | } 19 | } 20 | } 21 | } 22 | 23 | /* layout */ 24 | .el-container { 25 | // columns layout 26 | .aside-split { 27 | background-color: var(--el-bg-color) !important; 28 | } 29 | .el-header { 30 | background-color: var(--el-bg-color) !important; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/var.scss: -------------------------------------------------------------------------------- 1 | $primary-color: var(--el-color-primary); 2 | -------------------------------------------------------------------------------- /src/types/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const createPinia: typeof import('pinia')['createPinia'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const defineStore: typeof import('pinia')['defineStore'] 16 | const effectScope: typeof import('vue')['effectScope'] 17 | const getActivePinia: typeof import('pinia')['getActivePinia'] 18 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 19 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 20 | const h: typeof import('vue')['h'] 21 | const inject: typeof import('vue')['inject'] 22 | const isProxy: typeof import('vue')['isProxy'] 23 | const isReactive: typeof import('vue')['isReactive'] 24 | const isReadonly: typeof import('vue')['isReadonly'] 25 | const isRef: typeof import('vue')['isRef'] 26 | const mapActions: typeof import('pinia')['mapActions'] 27 | const mapGetters: typeof import('pinia')['mapGetters'] 28 | const mapState: typeof import('pinia')['mapState'] 29 | const mapStores: typeof import('pinia')['mapStores'] 30 | const mapWritableState: typeof import('pinia')['mapWritableState'] 31 | const markRaw: typeof import('vue')['markRaw'] 32 | const nextTick: typeof import('vue')['nextTick'] 33 | const onActivated: typeof import('vue')['onActivated'] 34 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 35 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 36 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 37 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 38 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 39 | const onDeactivated: typeof import('vue')['onDeactivated'] 40 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 41 | const onMounted: typeof import('vue')['onMounted'] 42 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 43 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 44 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 45 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 46 | const onUnmounted: typeof import('vue')['onUnmounted'] 47 | const onUpdated: typeof import('vue')['onUpdated'] 48 | const provide: typeof import('vue')['provide'] 49 | const reactive: typeof import('vue')['reactive'] 50 | const readonly: typeof import('vue')['readonly'] 51 | const ref: typeof import('vue')['ref'] 52 | const resolveComponent: typeof import('vue')['resolveComponent'] 53 | const setActivePinia: typeof import('pinia')['setActivePinia'] 54 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 55 | const shallowReactive: typeof import('vue')['shallowReactive'] 56 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 57 | const shallowRef: typeof import('vue')['shallowRef'] 58 | const storeToRefs: typeof import('pinia')['storeToRefs'] 59 | const toRaw: typeof import('vue')['toRaw'] 60 | const toRef: typeof import('vue')['toRef'] 61 | const toRefs: typeof import('vue')['toRefs'] 62 | const toValue: typeof import('vue')['toValue'] 63 | const triggerRef: typeof import('vue')['triggerRef'] 64 | const unref: typeof import('vue')['unref'] 65 | const useAttrs: typeof import('vue')['useAttrs'] 66 | const useCssModule: typeof import('vue')['useCssModule'] 67 | const useCssVars: typeof import('vue')['useCssVars'] 68 | const useLink: typeof import('vue-router')['useLink'] 69 | const useRoute: typeof import('vue-router')['useRoute'] 70 | const useRouter: typeof import('vue-router')['useRouter'] 71 | const useSlots: typeof import('vue')['useSlots'] 72 | const watch: typeof import('vue')['watch'] 73 | const watchEffect: typeof import('vue')['watchEffect'] 74 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 75 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 76 | } 77 | // for type re-export 78 | declare global { 79 | // @ts-ignore 80 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' 81 | } 82 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | BdAppVersion: typeof import('./../components/BdAppVersion/index.vue')['default'] 11 | BdMsg: typeof import('./../components/BdMsg/index.vue')['default'] 12 | BdPage: typeof import('./../components/BdPage/index.vue')['default'] 13 | BdProhitWords: typeof import('./../components/BdProhitWords/index.vue')['default'] 14 | BdSandAllMsg: typeof import('./../components/BdSandAllMsg/index.vue')['default'] 15 | BdSendMsg: typeof import('./../components/BdSendMsg/index.vue')['default'] 16 | RouterLink: typeof import('vue-router')['RouterLink'] 17 | RouterView: typeof import('vue-router')['RouterView'] 18 | SwitchDark: typeof import('./../components/SwitchDark/index.vue')['default'] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare module '*.vue' { 6 | import type { DefineComponent } from 'vue'; 7 | const component: DefineComponent<{}, {}, any>; 8 | export default component; 9 | } 10 | 11 | // vue-grid-layout 12 | declare module 'vue-grid-layout'; 13 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* Menu */ 2 | declare namespace Menu { 3 | type IRole = 'superAdmin' | 'admin'; 4 | interface MenuOptions { 5 | path: string; 6 | name: string; 7 | component?: string | (() => Promise); 8 | redirect?: string; 9 | meta: MetaProps; 10 | children?: MenuOptions[]; 11 | } 12 | interface MetaProps { 13 | icon: string; 14 | title: string; 15 | activeMenu?: string; 16 | auth?: IRole[]; 17 | isLink?: string; 18 | index?: number; 19 | isHide?: boolean; 20 | isFull?: boolean; 21 | isAffix?: boolean; 22 | isKeepAlive?: boolean; 23 | } 24 | } 25 | 26 | /* FileType */ 27 | declare namespace File { 28 | type ImageMimeType = 29 | | 'image/apng' 30 | | 'image/bmp' 31 | | 'image/gif' 32 | | 'image/jpeg' 33 | | 'image/pjpeg' 34 | | 'image/png' 35 | | 'image/svg+xml' 36 | | 'image/tiff' 37 | | 'image/webp' 38 | | 'image/x-icon'; 39 | 40 | type ExcelMimeType = 'application/vnd.ms-excel' | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; 41 | } 42 | 43 | /* Vite */ 44 | declare type Recordable = Record; 45 | 46 | declare interface ViteEnv { 47 | VITE_USER_NODE_ENV: 'development' | 'production' | 'test'; 48 | VITE_GLOB_APP_TITLE: string; 49 | VITE_PORT: number; 50 | VITE_OPEN: boolean; 51 | VITE_REPORT: boolean; 52 | VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'gzip,brotli' | 'none'; 53 | VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean; 54 | VITE_DROP_CONSOLE: boolean; 55 | VITE_PUBLIC_PATH: string; 56 | VITE_API_URL: string; 57 | VITE_PROXY: [string, string][]; 58 | } 59 | 60 | interface ImportMetaEnv extends ViteEnv { 61 | __: unknown; 62 | } 63 | 64 | /* __APP_INFO__ */ 65 | declare const __APP_INFO__: { 66 | pkg: { 67 | name: string; 68 | version: string; 69 | dependencies: Recordable; 70 | devDependencies: Recordable; 71 | }; 72 | lastBuildTime: string; 73 | }; 74 | 75 | /* Generic Tools */ 76 | type ObjToKeyValUnion = { 77 | [K in keyof T]: { key: K; value: T[K] }; 78 | }[keyof T]; 79 | 80 | type ObjToKeyValArray = { 81 | [K in keyof T]: [K, T[K]]; 82 | }[keyof T]; 83 | declare namespace Column { 84 | interface ColumnOptions { 85 | prop?: string; 86 | label?: string; 87 | type?: 'selection' | 'index' | 'expand'; 88 | fixed?: true | 'left' | 'right'; 89 | width?: string | number; 90 | minWidth?: string | number; 91 | align?: 'left' | 'center' | 'right'; 92 | formatter?: (scope: any) => void; 93 | render?: (scope?: any) => void; 94 | } 95 | } 96 | 97 | interface Window { 98 | TSDD_CONFIG: { 99 | APP_TITLE: string; 100 | APP_URL: string; 101 | }; 102 | } 103 | 104 | declare const windos: Window & typeof globalThis; 105 | -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosResponse } from 'axios'; 2 | import { BU_DOU_CONFIG } from '@/config'; 3 | import { useUserStore } from '@/stores/modules/user'; 4 | import router from '@/router'; 5 | const userStore = useUserStore(); 6 | 7 | const axiosInstance: AxiosInstance = axios.create({ 8 | baseURL: BU_DOU_CONFIG.APP_URL, // BASE_MAIN_URL 9 | withCredentials: false // 跨域请求时是否需要使用凭证 10 | }); 11 | 12 | // request 拦截器 13 | axiosInstance.interceptors.request.use( 14 | config => { 15 | // 添加token 16 | if (userStore.token) { 17 | (config as any).headers['token'] = userStore.token; 18 | } 19 | return config; 20 | }, 21 | (error: any) => { 22 | return Promise.reject(error); 23 | } 24 | ); 25 | 26 | // respone 拦截器 27 | axiosInstance.interceptors.response.use( 28 | (response: AxiosResponse) => { 29 | return Promise.resolve(response.data); 30 | }, 31 | (error: any) => { 32 | const code = error.response.status; 33 | if (code == 401) { 34 | userStore.setToken(''); 35 | userStore.setUserInfo({ name: '您好,超管', uid: '' }); 36 | router.replace('/login'); 37 | } 38 | if (code == 400) { 39 | return Promise.reject(error.response.data); 40 | } 41 | // 响应失败 42 | return Promise.reject(error); 43 | } 44 | ); 45 | 46 | export default axiosInstance; 47 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus'; 2 | 3 | /** 4 | * @description hex颜色转rgb颜色 5 | * @param {String} str 颜色值字符串 6 | * @returns {String} 返回处理后的颜色值 7 | */ 8 | export function hexToRgb(str: any) { 9 | let hexs: any = ''; 10 | const reg = /^#?[0-9A-Fa-f]{6}$/; 11 | if (!reg.test(str)) return ElMessage.warning('输入错误的hex'); 12 | str = str.replace('#', ''); 13 | hexs = str.match(/../g); 14 | for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16); 15 | return hexs; 16 | } 17 | 18 | /** 19 | * @description rgb颜色转Hex颜色 20 | * @param {*} r 代表红色 21 | * @param {*} g 代表绿色 22 | * @param {*} b 代表蓝色 23 | * @returns {String} 返回处理后的颜色值 24 | */ 25 | export function rgbToHex(r: any, g: any, b: any) { 26 | const reg = /^\d{1,3}$/; 27 | if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值'); 28 | const hexs = [r.toString(16), g.toString(16), b.toString(16)]; 29 | for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`; 30 | return `#${hexs.join('')}`; 31 | } 32 | 33 | /** 34 | * @description 加深颜色值 35 | * @param {String} color 颜色值字符串 36 | * @param {Number} level 加深的程度,限0-1之间 37 | * @returns {String} 返回处理后的颜色值 38 | */ 39 | export function getDarkColor(color: string, level: number) { 40 | const reg = /^#?[0-9A-Fa-f]{6}$/; 41 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值'); 42 | const rgb = hexToRgb(color); 43 | for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)); 44 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 45 | } 46 | 47 | /** 48 | * @description 变浅颜色值 49 | * @param {String} color 颜色值字符串 50 | * @param {Number} level 加深的程度,限0-1之间 51 | * @returns {String} 返回处理后的颜色值 52 | */ 53 | export function getLightColor(color: string, level: number) { 54 | const reg = /^#?[0-9A-Fa-f]{6}$/; 55 | if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值'); 56 | const rgb = hexToRgb(color); 57 | for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)); 58 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 获取localStorage 3 | * @param {String} key Storage名称 4 | * @returns {String} 5 | */ 6 | export function localGet(key: string) { 7 | const value = window.localStorage.getItem(key); 8 | try { 9 | return JSON.parse(window.localStorage.getItem(key) as string); 10 | } catch (error) { 11 | return value; 12 | } 13 | } 14 | 15 | /** 16 | * @description 存储localStorage 17 | * @param {String} key Storage名称 18 | * @param {*} value Storage值 19 | * @returns {void} 20 | */ 21 | export function localSet(key: string, value: any) { 22 | window.localStorage.setItem(key, JSON.stringify(value)); 23 | } 24 | 25 | /** 26 | * @description 清除localStorage 27 | * @param {String} key Storage名称 28 | * @returns {void} 29 | */ 30 | export function localRemove(key: string) { 31 | window.localStorage.removeItem(key); 32 | } 33 | 34 | /** 35 | * @description 清除所有localStorage 36 | * @returns {void} 37 | */ 38 | export function localClear() { 39 | window.localStorage.clear(); 40 | } 41 | 42 | /** 43 | * @description 判断数据类型 44 | * @param {*} val 需要判断类型的数据 45 | * @returns {String} 46 | */ 47 | export function isType(val: any) { 48 | if (val === null) return 'null'; 49 | if (typeof val !== 'object') return typeof val; 50 | else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase(); 51 | } 52 | 53 | /** 54 | * @description 生成唯一 uuid 55 | * @returns {String} 56 | */ 57 | export function generateUUID() { 58 | let uuid = ''; 59 | for (let i = 0; i < 32; i++) { 60 | const random = (Math.random() * 16) | 0; 61 | if (i === 8 || i === 12 || i === 16 || i === 20) uuid += '-'; 62 | uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16); 63 | } 64 | return uuid; 65 | } 66 | 67 | /** 68 | * @description 获取浏览器默认语言 69 | * @returns {String} 70 | */ 71 | export function getBrowserLang() { 72 | const browserLang = navigator.language ? navigator.language : (navigator as any).browserLanguage; 73 | let defaultBrowserLang = ''; 74 | if (['cn', 'zh', 'zh-cn'].includes(browserLang.toLowerCase())) { 75 | defaultBrowserLang = 'zh'; 76 | } else { 77 | defaultBrowserLang = 'en'; 78 | } 79 | return defaultBrowserLang; 80 | } 81 | 82 | /** 83 | * @description 使用递归扁平化菜单,方便添加动态路由 84 | * @param {Array} menuList 菜单列表 85 | * @returns {Array} 86 | */ 87 | export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] { 88 | const newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList)); 89 | return newMenuList.flatMap(item => [item, ...(item.children ? getFlatMenuList(item.children) : [])]); 90 | } 91 | 92 | /** 93 | * @description 使用递归过滤出需要渲染在左侧菜单的列表 (需剔除 isHide == true 的菜单) 94 | * @param {Array} menuList 菜单列表 95 | * @returns {Array} 96 | * */ 97 | export function getShowMenuList(menuList: Menu.MenuOptions[]) { 98 | const newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList)); 99 | return newMenuList.filter(item => { 100 | item.children?.length && (item.children = getShowMenuList(item.children)); 101 | return !item.meta?.isHide; 102 | }); 103 | } 104 | 105 | /** 106 | * @description 使用递归找出所有面包屑存储到 pinia/vuex 中 107 | * @param {Array} menuList 菜单列表 108 | * @param {Array} parent 父级菜单 109 | * @param {Object} result 处理后的结果 110 | * @returns {Object} 111 | */ 112 | export const getAllBreadcrumbList = (menuList: Menu.MenuOptions[], parent = [], result: { [key: string]: any } = {}) => { 113 | for (const item of menuList) { 114 | result[item.path] = [...parent, item]; 115 | if (item.children) getAllBreadcrumbList(item.children, result[item.path], result); 116 | } 117 | return result; 118 | }; 119 | 120 | /** 121 | * @description 使用递归处理路由菜单 path,生成一维数组 (第一版本地路由鉴权会用到,该函数暂未使用) 122 | * @param {Array} menuList 所有菜单列表 123 | * @param {Array} menuPathArr 菜单地址的一维数组 ['**','**'] 124 | * @returns {Array} 125 | */ 126 | export function getMenuListPath(menuList: Menu.MenuOptions[], menuPathArr: string[] = []): string[] { 127 | for (const item of menuList) { 128 | if (typeof item === 'object' && item.path) menuPathArr.push(item.path); 129 | if (item.children?.length) getMenuListPath(item.children, menuPathArr); 130 | } 131 | return menuPathArr; 132 | } 133 | 134 | /** 135 | * @description 递归查询当前 path 所对应的菜单对象 (该函数暂未使用) 136 | * @param {Array} menuList 菜单列表 137 | * @param {String} path 当前访问地址 138 | * @returns {Object | null} 139 | */ 140 | export function findMenuByPath(menuList: Menu.MenuOptions[], path: string): Menu.MenuOptions | null { 141 | for (const item of menuList) { 142 | if (item.path === path) return item; 143 | if (item.children) { 144 | const res = findMenuByPath(item.children, path); 145 | if (res) return res; 146 | } 147 | } 148 | return null; 149 | } 150 | 151 | /** 152 | * @description 使用递归过滤需要缓存的菜单 name (该函数暂未使用) 153 | * @param {Array} menuList 所有菜单列表 154 | * @param {Array} keepAliveNameArr 缓存的菜单 name ['**','**'] 155 | * @returns {Array} 156 | * */ 157 | export function getKeepAliveRouterName(menuList: Menu.MenuOptions[], keepAliveNameArr: string[] = []) { 158 | menuList.forEach(item => { 159 | item.meta.isKeepAlive && item.name && keepAliveNameArr.push(item.name); 160 | item.children?.length && getKeepAliveRouterName(item.children, keepAliveNameArr); 161 | }); 162 | return keepAliveNameArr; 163 | } 164 | -------------------------------------------------------------------------------- /src/utils/mittBus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | const mittBus = mitt(); 4 | 5 | export default mittBus; 6 | -------------------------------------------------------------------------------- /src/utils/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress'; 2 | import 'nprogress/nprogress.css'; 3 | 4 | NProgress.configure({ 5 | easing: 'ease', // 动画方式 6 | speed: 500, // 递增进度条的速度 7 | showSpinner: true, // 是否显示加载ico 8 | trickleSpeed: 200, // 自动递增间隔 9 | minimum: 0.3 // 初始化时的最小百分比 10 | }); 11 | 12 | export default NProgress; 13 | -------------------------------------------------------------------------------- /src/utils/piniaPersist.ts: -------------------------------------------------------------------------------- 1 | import { PersistedStateOptions } from 'pinia-plugin-persistedstate'; 2 | 3 | /** 4 | * @description pinia 持久化参数配置 5 | * @param {String} key 存储到持久化的 name 6 | * @param {Array} paths 需要持久化的 state name 7 | * @return persist 8 | * */ 9 | const piniaPersistConfig = (key: string, paths?: string[]) => { 10 | const persist: PersistedStateOptions = { 11 | key, 12 | storage: localStorage, 13 | paths 14 | }; 15 | return persist; 16 | }; 17 | 18 | export default piniaPersistConfig; 19 | -------------------------------------------------------------------------------- /src/utils/system-copyright.ts: -------------------------------------------------------------------------------- 1 | if (import.meta.env.PROD) { 2 | const copyright_common_style = 'font-size: 14px; margin-bottom: 2px; padding: 6px 8px; color: #fff;'; 3 | const copyright_main_style = `${copyright_common_style} background: #e24329;`; 4 | const copyright_sub_style = `${copyright_common_style} background: #707070;`; 5 | console.info( 6 | '%c由%c唐僧叨叨%c驱动', 7 | copyright_sub_style, 8 | copyright_main_style, 9 | copyright_sub_style, 10 | '\nhttps://tangsengdaodao.com/' 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Node", 11 | "allowImportingTsExtensions": true, 12 | "allowSyntheticDefaultImports": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "baseUrl": "./", 24 | "paths": { 25 | "@/*": ["src/*"], 26 | "~/*": ["./*"] 27 | }, 28 | }, 29 | "include": [ 30 | "vite.config.*", 31 | "uno.config.*", 32 | "src/**/*.ts", 33 | "src/**/*.d.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue" 36 | ], 37 | "exclude": ["/dist/**", "node_modules"] 38 | } 39 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@unocss/vite'; 2 | import presetUno from '@unocss/preset-uno'; 3 | 4 | export default defineConfig({ 5 | exclude: ['node_modules', 'dist', '.git', '.husky', '.vscode', 'public', 'build', 'mock', './stats.html'], 6 | presets: [presetUno({ dark: 'class' })], 7 | shortcuts: { 8 | 'wh-full': 'w-full h-full', 9 | 'flex-center': 'flex justify-center items-center', 10 | 'flex-col-center': 'flex-center flex-col', 11 | 'flex-x-center': 'flex justify-center', 12 | 'flex-y-center': 'flex items-center', 13 | 'i-flex-center': 'inline-flex justify-center items-center', 14 | 'i-flex-x-center': 'inline-flex justify-center', 15 | 'i-flex-y-center': 'inline-flex items-center', 16 | 'flex-col': 'flex flex-col', 17 | 'flex-col-stretch': 'flex-col items-stretch', 18 | 'i-flex-col': 'inline-flex flex-col', 19 | 'i-flex-col-stretch': 'i-flex-col items-stretch', 20 | 'flex-1-hidden': 'flex-1 overflow-hidden', 21 | 'absolute-lt': 'absolute left-0 top-0', 22 | 'absolute-lb': 'absolute left-0 bottom-0', 23 | 'absolute-rt': 'absolute right-0 top-0', 24 | 'absolute-rb': 'absolute right-0 bottom-0', 25 | 'absolute-tl': 'absolute-lt', 26 | 'absolute-tr': 'absolute-rt', 27 | 'absolute-bl': 'absolute-lb', 28 | 'absolute-br': 'absolute-rb', 29 | 'absolute-center': 'absolute-lt flex-center wh-full', 30 | 'fixed-lt': 'fixed left-0 top-0', 31 | 'fixed-lb': 'fixed left-0 bottom-0', 32 | 'fixed-rt': 'fixed right-0 top-0', 33 | 'fixed-rb': 'fixed right-0 bottom-0', 34 | 'fixed-tl': 'fixed-lt', 35 | 'fixed-tr': 'fixed-rt', 36 | 'fixed-bl': 'fixed-lb', 37 | 'fixed-br': 'fixed-rb', 38 | 'fixed-center': 'fixed-lt flex-center wh-full', 39 | 'nowrap-hidden': 'whitespace-nowrap overflow-hidden', 40 | 'ellipsis-text': 'nowrap-hidden overflow-ellipsis', 41 | 'transition-base': 'transition-all duration-300 ease-in-out' 42 | }, 43 | theme: {} 44 | }); 45 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, ConfigEnv, UserConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | // vite插件 4 | import VueDevTools from 'vite-plugin-vue-devtools'; 5 | import vue from '@vitejs/plugin-vue'; 6 | import vueJsx from '@vitejs/plugin-vue-jsx'; 7 | import unocss from '@unocss/vite'; 8 | import { createHtmlPlugin } from 'vite-plugin-html'; 9 | import AutoImport from 'unplugin-auto-import/vite'; 10 | import Components from 'unplugin-vue-components/vite'; 11 | import setupExtend from 'unplugin-vue-setup-extend-plus/vite'; 12 | import Layouts from 'vite-plugin-vue-meta-layouts'; 13 | import Pages from 'vite-plugin-pages'; 14 | import compression from 'vite-plugin-compression'; 15 | 16 | const getPlugins = (_command?: string) => { 17 | return [ 18 | AutoImport({ 19 | include: [/\.[tj]sx?$/, /\.vue\?vue/, /\.md$/], 20 | imports: ['vue', 'vue-router', 'pinia'], 21 | resolvers: [], 22 | dts: 'src/types/auto-imports.d.ts' 23 | }), 24 | Components({ 25 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 26 | resolvers: [], 27 | dts: 'src/types/components.d.ts' 28 | }), 29 | VueDevTools(), 30 | vue({ 31 | template: { 32 | compilerOptions: { 33 | isCustomElement: tag => /^tgs-player/.test(tag) 34 | } 35 | } 36 | }), 37 | vueJsx(), 38 | createHtmlPlugin({ 39 | inject: { 40 | data: { 41 | title: '唐僧叨叨后台管理', 42 | injectScript: process.env.IS_CONFIG ? `` : null 43 | } 44 | } 45 | }), 46 | unocss(), 47 | setupExtend({}), 48 | Layouts({ 49 | defaultLayout: 'index' 50 | }), 51 | Pages({ 52 | dirs: 'src/pages', 53 | exclude: ['**/components/*.vue'] 54 | }), 55 | compression({ 56 | ext: '.gz', 57 | deleteOriginFile: false 58 | }) 59 | ]; 60 | }; 61 | 62 | export default defineConfig(({ command }: ConfigEnv): UserConfig => { 63 | return { 64 | resolve: { 65 | alias: { 66 | '@': resolve(__dirname, 'src'), 67 | 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js' 68 | } 69 | }, 70 | define: { 71 | 'process.env': { 72 | APP_ENV: process.env.APP_ENV 73 | } 74 | }, 75 | plugins: getPlugins(command), 76 | css: { 77 | postcss: { 78 | plugins: [ 79 | { 80 | postcssPlugin: 'internal:charset-removal', 81 | AtRule: { 82 | charset: atRule => { 83 | if (atRule.name === 'charset') { 84 | atRule.remove(); 85 | } 86 | } 87 | } 88 | } 89 | ] 90 | }, 91 | preprocessorOptions: { 92 | scss: { 93 | additionalData: `@import "@/styles/var.scss";` 94 | } 95 | } 96 | }, 97 | server: { 98 | host: '0.0.0.0' 99 | }, 100 | build: { 101 | cssCodeSplit: false, 102 | sourcemap: false, 103 | emptyOutDir: true, 104 | chunkSizeWarningLimit: 1500, 105 | rollupOptions: { 106 | output: { 107 | chunkFileNames: 'static/js/[name]-[hash].js', 108 | entryFileNames: 'static/js/[name]-[hash].js', 109 | assetFileNames: 'static/[ext]/[name]-[hash].[ext]', 110 | manualChunks: { 111 | // 分包配置,配置完成自动按需加载 112 | vue: ['vue', 'vue-router', 'pinia', 'vue-i18n', 'element-plus'], 113 | echarts: ['echarts'], 114 | 'tgs-player': ['@lottiefiles/lottie-player/dist/tgs-player'] 115 | } 116 | } 117 | } 118 | } 119 | }; 120 | }); 121 | --------------------------------------------------------------------------------