├── .stylelintignore ├── .vscode └── extensions.json ├── public ├── favicon.ico └── img │ ├── bg.jpg │ ├── bg1.jpg │ ├── bg2.jpg │ ├── bg3.jpg │ ├── bg4.jpg │ ├── logo.png │ ├── star-squashed.jpg │ └── login-bg.svg ├── src ├── assets │ ├── 111.jpg │ ├── logo.png │ ├── login │ │ ├── BG.png │ │ ├── login_bg.png │ │ └── login_box.png │ └── svg │ │ └── icon-msg.svg ├── config │ ├── env.ts │ └── website.ts ├── styles │ ├── variables.scss │ ├── theme │ │ ├── index.scss │ │ ├── bule.scss │ │ ├── renren.scss │ │ ├── hey.scss │ │ ├── beautiful.scss │ │ ├── dark.scss │ │ ├── d2.scss │ │ ├── iview.scss │ │ ├── vip.scss │ │ ├── star.scss │ │ ├── white.scss │ │ └── cool.scss │ ├── iconfont.scss │ ├── element-ui.scss │ ├── media.scss │ ├── sidebar.scss │ ├── mixin.scss │ ├── common.scss │ ├── tags.scss │ └── top.scss ├── views │ ├── util │ │ ├── deep.vue │ │ ├── test.vue │ │ ├── logs.vue │ │ ├── cache.vue │ │ ├── top.vue │ │ ├── tags.vue │ │ ├── params.vue │ │ ├── detail.vue │ │ ├── form.vue │ │ ├── import-dialog.vue │ │ ├── affix.vue │ │ ├── crud.vue │ │ ├── about.vue │ │ ├── table.vue │ │ └── store.vue │ ├── system │ │ └── menu │ │ │ └── index.vue │ ├── form │ │ ├── display.vue │ │ ├── title-width.vue │ │ ├── data-default.vue │ │ ├── control.vue │ │ ├── group.vue │ │ ├── tabs.vue │ │ ├── data-type.vue │ │ ├── data-filter.vue │ │ ├── custom-slot.vue │ │ └── data-validate.vue │ ├── crud │ │ └── page.vue │ ├── wel │ │ └── index.vue │ └── user │ │ └── setting.vue ├── components │ ├── data-v │ │ └── screen-container │ │ │ ├── index.ts │ │ │ └── main.vue │ ├── error-page │ │ ├── 403.vue │ │ ├── 500.vue │ │ ├── 404.vue │ │ └── style.scss │ ├── index.ts │ ├── icon-svg │ │ └── index.vue │ ├── basic-container │ │ └── main.vue │ ├── iframe │ │ └── main.vue │ ├── basic-video │ │ ├── plugin.ts │ │ └── main.vue │ ├── basic-block │ │ └── main.vue │ └── import-excel │ │ └── index.vue ├── page │ ├── index │ │ ├── layout.vue │ │ ├── top │ │ │ ├── top-full.vue │ │ │ ├── top-lang.vue │ │ │ ├── top-menu.vue │ │ │ ├── top-theme.vue │ │ │ ├── top-lock.vue │ │ │ ├── top-logs.vue │ │ │ └── top-search.vue │ │ ├── logo.vue │ │ ├── sidebar │ │ │ ├── index.vue │ │ │ └── sidebarItem.vue │ │ └── index.vue │ ├── login │ │ ├── authredirect.vue │ │ ├── facelogin.vue │ │ ├── thirdlogin.vue │ │ └── index.vue │ └── lock │ │ └── index.vue ├── mockProdServer.ts ├── store │ ├── index.ts │ ├── logs.ts │ ├── tags.ts │ └── common.ts ├── utils │ ├── mitt.ts │ ├── token.ts │ └── store.ts ├── env.d.ts ├── mac │ ├── mode │ │ └── index.ts │ ├── lock.vue │ ├── login.vue │ └── login.css ├── App.vue ├── error.ts ├── hooks │ ├── useAutoResize.ts │ ├── useRefreshToken.ts │ └── useDownload.ts ├── enums │ └── http.ts ├── lang │ ├── index.ts │ ├── zh.ts │ └── en.ts ├── api │ ├── crud │ │ └── index.ts │ ├── user.ts │ └── auth.ts ├── main.ts ├── router │ ├── index.ts │ ├── page │ │ └── index.ts │ └── views │ │ └── index.ts ├── option │ ├── crud │ │ └── index.ts │ └── form │ │ └── basics.ts ├── typings │ ├── axios.d.ts │ └── global.d.ts ├── http │ ├── cancel.ts │ └── axios.ts └── permission.ts ├── .prettierignore ├── .env.test ├── .husky ├── commit-msg └── pre-commit ├── .env.production ├── .env.development ├── tsconfig.node.json ├── .eslintignore ├── .env ├── .gitignore ├── lint-staged.config.js ├── .editorconfig ├── mock ├── crud.ts └── user.ts ├── tsconfig.json ├── README.md ├── .prettierrc.js ├── vite.config.ts ├── .stylelintrc.js ├── index.html └── .eslintrc.js /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/bg.jpg -------------------------------------------------------------------------------- /public/img/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/bg1.jpg -------------------------------------------------------------------------------- /public/img/bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/bg2.jpg -------------------------------------------------------------------------------- /public/img/bg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/bg3.jpg -------------------------------------------------------------------------------- /public/img/bg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/bg4.jpg -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/logo.png -------------------------------------------------------------------------------- /src/assets/111.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/src/assets/111.jpg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | let baseUrl = import.meta.env.VITE_API_BASE_URL; 2 | export { baseUrl }; 3 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $sidebar_width: 230px; 2 | $sidebar_collapse:60px; 3 | $top_height:50px; -------------------------------------------------------------------------------- /src/assets/login/BG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/src/assets/login/BG.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | .local 3 | /node_modules/** 4 | 5 | **/*.svg 6 | **/*.sh 7 | 8 | /public/* 9 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 测试环境 2 | NODE_ENV = "test" 3 | 4 | # 测试环境接口地址 5 | VITE_API_URL = "" 6 | 7 | VITE_BASE_URL = '/api' -------------------------------------------------------------------------------- /public/img/star-squashed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/public/img/star-squashed.jpg -------------------------------------------------------------------------------- /src/assets/login/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/src/assets/login/login_bg.png -------------------------------------------------------------------------------- /src/assets/login/login_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtual1680/q-admin/HEAD/src/assets/login/login_box.png -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | NODE_ENV = "production" 3 | 4 | # 线上环境接口地址 5 | VITE_API_URL = "" 6 | 7 | VITE_API_BASE_URL = '/' 8 | 9 | VITE_BASE_URL = '/' -------------------------------------------------------------------------------- /src/views/util/deep.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/util/test.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | NODE_ENV = 'development' 3 | 4 | # 本地环境接口地址 5 | VITE_API_URL = '' 6 | # /api 7 | VITE_API_BASE_URL = '/' 8 | 9 | VITE_BASE_URL = './' -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | echo "# 🚀 开始检测代码格式!" 5 | npm run lint:lint-staged 6 | echo "# ⚡️ 恭喜您,代码写的很规范,没有一个错误,过关!✔ " 7 | # npm run deploy 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | /public 10 | /docs 11 | .husky 12 | .local 13 | /bin 14 | .eslintrc.js 15 | .prettierrc.js 16 | /src/mock/* 17 | 18 | -------------------------------------------------------------------------------- /src/views/system/menu/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 10 | -------------------------------------------------------------------------------- /src/components/data-v/screen-container/index.ts: -------------------------------------------------------------------------------- 1 | import ScreenContainer from './main.vue'; 2 | import { App } from 'vue'; 3 | export default { 4 | install(app: App) { 5 | app.component(ScreenContainer.name, ScreenContainer); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # title 2 | VITE_GLOB_APP_TITLE = '管理后台模板' 3 | 4 | # port 5 | VITE_PORT = 3000 6 | 7 | # open 运行 npm run dev 时自动打开浏览器 8 | VITE_OPEN = true 9 | 10 | # 是否生成包预览文件 11 | VITE_REPORT = true 12 | 13 | # 是否删除生产环境 console 14 | VITE_DROP_CONSOLE = true 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/page/index/layout.vue: -------------------------------------------------------------------------------- 1 | 8 | 12 | -------------------------------------------------------------------------------- /src/views/util/logs.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'; 2 | 3 | import menuModule from '../mock/menu'; 4 | import userModule from '../mock/user'; 5 | import areaModule from '../mock/area'; 6 | export const setupProdMockServer = () => { 7 | createProdMockServer([...menuModule, ...userModule, ...areaModule]); 8 | }; 9 | -------------------------------------------------------------------------------- /.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 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | stats.html 15 | src/qv-vue 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 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], 3 | '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'], 4 | 'package.json': ['prettier --write'], 5 | '*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'], 6 | '*.{scss,less,styl}': ['stylelint --fix', 'prettier --write'], 7 | '*.md': ['prettier --write'] 8 | }; 9 | -------------------------------------------------------------------------------- /.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 = tab # 缩进风格(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 # 关闭末尾空格修剪 16 | -------------------------------------------------------------------------------- /src/views/util/cache.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/page/login/authredirect.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import { useCommonStore } from './common'; 3 | import { useLogsStore } from './logs'; 4 | import { useTagsStore } from './tags'; 5 | import { useUserStore } from './user'; 6 | 7 | const pinia = createPinia(); 8 | import piniaPersisted from 'pinia-plugin-persistedstate'; 9 | pinia.use(piniaPersisted); 10 | 11 | export { useCommonStore, useLogsStore, useTagsStore, useUserStore }; 12 | export default pinia; 13 | -------------------------------------------------------------------------------- /src/styles/theme/index.scss: -------------------------------------------------------------------------------- 1 | // white 2 | @import './white.scss'; 3 | 4 | // star 5 | @import './star.scss'; 6 | 7 | // vip 8 | @import './vip.scss'; 9 | 10 | // d2 11 | @import './d2.scss'; 12 | 13 | // bule 14 | @import './bule.scss'; 15 | 16 | //iview 17 | @import './iview.scss'; 18 | 19 | //heyui 20 | @import './hey.scss'; 21 | 22 | //dark 23 | @import './dark.scss'; 24 | 25 | //renren 26 | @import './renren.scss'; 27 | 28 | //cool 29 | @import './cool.scss'; 30 | 31 | //beautiful 32 | @import './beautiful.scss' 33 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | import { App } from 'vue'; 3 | const emitter = mitt(); 4 | 5 | export default { 6 | install: (app: App) => { 7 | app.config.globalProperties.$emitter = emitter; 8 | } 9 | }; 10 | 11 | /** 12 | * 13 | * 跨组件通信 14 | * 15 | * 16 | emitter.on('change-msg',(value)=>{ 17 | console.log(value); 18 | }) 19 | 20 | onUnmounted(()=>{ 21 | emitter.off('change-msg',()=>{ 22 | 23 | }) 24 | }) 25 | 26 | emitter.emit('change-msg',123) 27 | 28 | * 29 | * 30 | * 31 | */ 32 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | // import { Router } from ''; 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue'; 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 7 | const component: DefineComponent<{}, {}, any>; 8 | export default component; 9 | } 10 | declare module '@smallwei/avue'; 11 | declare module '@smallwei/avue/*'; 12 | declare module 'nprogress'; 13 | declare module 'crypto-js/*'; 14 | -------------------------------------------------------------------------------- /src/mac/mode/index.ts: -------------------------------------------------------------------------------- 1 | import MessageConstructor from './index.vue'; 2 | import { createVNode, render } from 'vue'; 3 | export default (function () { 4 | return (opts = {}) => { 5 | let options = { 6 | app: opts 7 | }; 8 | const parent = document.createElement('div'); 9 | let instance = createVNode(MessageConstructor, options); 10 | instance.props!.onDestroy = () => { 11 | render(null, parent); 12 | }; 13 | render(instance, parent); 14 | document.body.appendChild(parent.firstElementChild!); 15 | return instance; 16 | }; 17 | })(); 18 | -------------------------------------------------------------------------------- /src/components/error-page/403.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/components/error-page/500.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /src/views/util/top.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/components/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /src/styles/iconfont.scss: -------------------------------------------------------------------------------- 1 | 2 | [class^="icon-"]{ 3 | font-family: "iconfont" !important; 4 | font-size: 18px !important; 5 | font-style: normal; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | .el-menu-item [class^=icon-] { 10 | margin-right: 5px; 11 | width: 24px; 12 | text-align: center; 13 | font-size: 18px; 14 | vertical-align: middle; 15 | } 16 | .el-sub-menu [class^=icon-] { 17 | vertical-align: middle; 18 | margin-right: 5px; 19 | width: 24px; 20 | text-align: center; 21 | font-size: 18px; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | // * svg 图标 3 | import SVG from './icon-svg/index.vue'; 4 | // * 子窗口容器 5 | import basicContainer from './basic-container/main.vue'; 6 | import basicBlock from './basic-block/main.vue'; 7 | // * 数据大屏容器 8 | import ScreenContainer from './data-v/screen-container/main.vue'; 9 | const component = { 10 | install(app: App) { 11 | app.component('VueSvg', SVG); 12 | app.component('BasicContainer', basicContainer); 13 | app.component('BasicBlock', basicBlock); 14 | app.component(ScreenContainer.name, ScreenContainer); 15 | } 16 | }; 17 | export default component; 18 | -------------------------------------------------------------------------------- /src/page/index/top/top-full.vue: -------------------------------------------------------------------------------- 1 | 4 | 22 | -------------------------------------------------------------------------------- /src/views/util/tags.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { useLogsStore } from 'store/index'; 3 | export default { 4 | install: (app: App) => { 5 | app.config.errorHandler = (err, vm, info) => { 6 | useLogsStore().ADD_LOGS({ 7 | type: 'error', 8 | message: (err as Error).message, 9 | stack: (err as Error).stack || '', 10 | info 11 | }); 12 | if (process.env.NODE_ENV === 'development') { 13 | console.group('>>>>>> 错误信息 >>>>>>'); 14 | console.log(info); 15 | console.groupEnd(); 16 | console.group('>>>>>> Vue 实例 >>>>>>'); 17 | console.log(vm); 18 | console.groupEnd(); 19 | console.group('>>>>>> Error >>>>>>'); 20 | console.log(err); 21 | console.groupEnd(); 22 | } 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /mock/crud.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | url: '/crud/list', 4 | method: 'get', 5 | response: () => { 6 | return { 7 | msg: '操作成功', 8 | success: true, 9 | data: { 10 | total: 10, 11 | record: Array(10).fill({ 12 | name: 'small', 13 | sex: 1, 14 | status: 0 15 | }) 16 | } 17 | }; 18 | } 19 | }, 20 | { 21 | url: '/crud', 22 | method: 'post', 23 | response: () => { 24 | return { 25 | data: {} 26 | }; 27 | } 28 | }, 29 | { 30 | url: '/crud', 31 | method: 'put', 32 | response: () => { 33 | return { 34 | data: {} 35 | }; 36 | } 37 | }, 38 | { 39 | url: '/crud', 40 | method: 'delete', 41 | response: () => { 42 | return { 43 | data: {} 44 | }; 45 | } 46 | } 47 | ]; 48 | -------------------------------------------------------------------------------- /src/hooks/useAutoResize.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash-es'; 2 | import { onUnmounted } from 'vue'; 3 | 4 | export const useAutoResize = (dom: HTMLElement) => { 5 | const viewResize = () => { 6 | const currentWidth = document.body.clientWidth; 7 | let width = dom.clientWidth || 0; 8 | let height = dom.clientHeight || 0; 9 | if (!width || !height) { 10 | console.warn('Component width or height is 0px, rendering abnormality may occur!'); 11 | } 12 | dom.style.transform = `scale(${currentWidth / width})`; 13 | }; 14 | viewResize(); 15 | const debounceViewResize = debounce(viewResize, 100); 16 | 17 | window.addEventListener('resize', debounceViewResize, false); 18 | 19 | onUnmounted(() => window.removeEventListener('resize', debounceViewResize)); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/error-page/style.scss: -------------------------------------------------------------------------------- 1 | .error-page { 2 | background: #f0f2f5; 3 | margin-top: -30px; 4 | height: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | .img { 9 | margin-right: 80px; 10 | height: 360px; 11 | width: 100%; 12 | max-width: 430px; 13 | background-repeat: no-repeat; 14 | background-position: 50% 50%; 15 | background-size: contain; 16 | } 17 | .content { 18 | h1 { 19 | color: #434e59; 20 | font-size: 72px; 21 | font-weight: 600; 22 | line-height: 72px; 23 | margin-bottom: 24px; 24 | } 25 | .desc { 26 | color: rgba(0, 0, 0, 0.45); 27 | font-size: 20px; 28 | line-height: 28px; 29 | margin-bottom: 16px; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/enums/http.ts: -------------------------------------------------------------------------------- 1 | // * http 响应 2 | export enum ResultEnum { 3 | SUCCESS = 200, 4 | ERROR = 500, 5 | OVERDUE = 599, 6 | TIMEOUT = 10000, 7 | TYPE = 'success' 8 | } 9 | 10 | // * 请求方法 11 | export enum RequestEnum { 12 | GET = 'GET', 13 | POST = 'POST', 14 | PATCH = 'PATCH', 15 | PUT = 'PUT', 16 | DELETE = 'DELETE' 17 | } 18 | 19 | // *常用的contentType类型 20 | export enum ContentTypeEnum { 21 | // json 22 | JSON = 'application/json;charset=UTF-8', 23 | // text 24 | TEXT = 'text/plain;charset=UTF-8', 25 | // form-data 一般配合qs 26 | FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', 27 | // form-data 上传 28 | FORM_DATA = 'multipart/form-data;charset=UTF-8' 29 | } 30 | 31 | // * responseType类型 32 | export enum ResponseTypeEnum { 33 | ARRAY_BUFFER = 'arraybuffer', 34 | BLOB = 'blob' 35 | } 36 | -------------------------------------------------------------------------------- /src/lang/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | import elementEnLocale from 'element-plus/es/locale/lang/en'; 3 | import elementZhLocale from 'element-plus/es/locale/lang/zh-cn'; 4 | import enLocale from './en'; 5 | import zhLocale from './zh'; 6 | import AvueEnLocale from '@smallwei/avue/lib/locale/lang/en'; 7 | import AvueZhLocale from '@smallwei/avue/lib/locale/lang/zh'; 8 | import { getStore } from '../utils/store'; 9 | export const messages = { 10 | en: { 11 | ...enLocale, 12 | ...AvueEnLocale, 13 | ...elementEnLocale 14 | }, 15 | 'zh-cn': { 16 | ...zhLocale, 17 | ...AvueZhLocale, 18 | ...elementZhLocale 19 | } 20 | }; 21 | 22 | const i18n = createI18n({ 23 | locale: getStore({ name: 'language' })?.content || 'zh-cn', 24 | messages 25 | }); 26 | 27 | export default i18n; 28 | -------------------------------------------------------------------------------- /src/components/icon-svg/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /src/api/crud/index.ts: -------------------------------------------------------------------------------- 1 | import { DataPage } from '@/typings/axios'; 2 | import axios from 'http/axios'; 3 | export const list = (data: unknown) => { 4 | return axios.request>({ 5 | url: '/crud/list', 6 | method: 'get', 7 | meta: { 8 | isSerialize: true 9 | }, 10 | params: data 11 | }); 12 | }; 13 | export const del = (id: string) => 14 | axios.delete('/crud', { 15 | params: { 16 | id 17 | } 18 | }); 19 | export const add = (data: unknown) => 20 | axios.request({ 21 | url: '/crud', 22 | method: 'post', 23 | meta: { 24 | isSerialize: true 25 | }, 26 | data: data 27 | }); 28 | export const update = (data: unknown) => 29 | axios.request({ 30 | url: '/crud', 31 | method: 'put', 32 | meta: { 33 | isSerialize: true 34 | }, 35 | data: data 36 | }); 37 | -------------------------------------------------------------------------------- /src/config/website.ts: -------------------------------------------------------------------------------- 1 | // * 全局配置文件 2 | export default { 3 | title: 'Avue', 4 | logo: 'A', 5 | key: 'avue', //配置主键,目前用于存储 6 | indexTitle: '后台快速开发模版', 7 | tokenTime: 6000, //token过期时间 8 | Authorization: 'Authorization', 9 | //http的status默认放行不才用统一处理的, 10 | statusWhiteList: [400], 11 | //配置首页不可关闭 12 | setting: { 13 | sidebar: 'vertical', 14 | tag: true, 15 | debug: true, 16 | collapse: true, 17 | search: true, 18 | lock: true, 19 | screenshot: true, 20 | fullscren: true, 21 | theme: true, 22 | menu: true 23 | }, 24 | fistPage: { 25 | name: '首页', 26 | path: '/wel/index' 27 | }, 28 | //配置菜单的属性 29 | menu: { 30 | iconDefault: 'icon-caidan', 31 | label: 'label', 32 | path: 'path', 33 | icon: 'icon', 34 | children: 'children', 35 | query: 'query', 36 | href: 'href', 37 | meta: 'meta' 38 | } 39 | } as Website; 40 | -------------------------------------------------------------------------------- /src/page/index/logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | -------------------------------------------------------------------------------- /src/styles/theme/bule.scss: -------------------------------------------------------------------------------- 1 | .theme-bule{ 2 | 3 | .avue-top,.avue-logo{ 4 | background: #004ca7; 5 | } 6 | .avue-sidebar{ 7 | background: #004ca7; 8 | .el-menu-item.is-active, .el-sub-menu__title.is-active { 9 | background-color: rgba(0, 0, 0, 0.2); 10 | } 11 | } 12 | .el-dropdown{ 13 | color: #fff; 14 | } 15 | .avue-logo{ 16 | .avue-logo_title{ 17 | color:#fff; 18 | } 19 | } 20 | .avue-breadcrumb{ 21 | i{ 22 | color:#fff; 23 | } 24 | } 25 | 26 | .top-bar__item { 27 | i { 28 | color: #fff; 29 | } 30 | } 31 | .avue-top{ 32 | 33 | .el-menu-item { 34 | i, 35 | span { 36 | color: #fff; 37 | } 38 | &:hover { 39 | i, 40 | span { 41 | color: #fff; 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/styles/theme/renren.scss: -------------------------------------------------------------------------------- 1 | .theme-renren { 2 | .avue-logo{ 3 | background-color: #409EFF; 4 | .avue-logo_title{ 5 | font-weight: 500; 6 | font-size: 20px; 7 | color:#fff; 8 | } 9 | } 10 | .avue-tags{ 11 | .el-tabs__item{ 12 | font-size: 12px !important; 13 | font-weight: 500 !important; 14 | } 15 | } 16 | .avue-sidebar{ 17 | background-color: #263238; 18 | .el-menu-item,.el-sub-menu__title{ 19 | i,span{ 20 | color: #8a979e; 21 | } 22 | &:hover{ 23 | background-color: #2b373d; 24 | } 25 | &.is-active { 26 | &:before { 27 | display: none; 28 | } 29 | background-color: #2b373d; 30 | i,span{ 31 | color:#fff; 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/page/login/facelogin.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/util/params.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/form/display.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | 43 | -------------------------------------------------------------------------------- /src/components/data-v/screen-container/main.vue: -------------------------------------------------------------------------------- 1 | 6 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/util/detail.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/views/util/form.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/util/import-dialog.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 34 | -------------------------------------------------------------------------------- /src/views/form/title-width.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/views/util/affix.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qinhongyang virtual1680@gmail.com 3 | * @Date: 2022-09-19 16:58:59 4 | * @LastEditTime: 2022-10-28 14:24:23 5 | * @Description: 暂无 6 | */ 7 | import { createApp } from 'vue'; 8 | import App from 'app/App.vue'; 9 | import store from 'store/index'; 10 | import router from 'router/index'; 11 | import 'virtual:svg-icons-register'; 12 | import i18n from './lang/index'; 13 | import ElementPlus from 'element-plus'; 14 | import Avue from '@smallwei/avue'; 15 | import QvVue from 'qv-vue'; 16 | import 'qv-vue/theme-chalk/index.css'; 17 | import axios from './http/axios'; 18 | import componentList from './components'; 19 | import './permission'; 20 | import error from './error'; 21 | import './styles/common.scss'; 22 | 23 | const app = createApp(App); 24 | app.use(i18n); 25 | app.use(store); 26 | app.use(router); 27 | app.use(componentList); 28 | app.use(error); 29 | app.use(ElementPlus); 30 | app.use(Avue, { axios }); 31 | app.use(QvVue, { axios }); 32 | app.mount('#app'); 33 | -------------------------------------------------------------------------------- /public/img/login-bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/form/data-default.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/styles/theme/hey.scss: -------------------------------------------------------------------------------- 1 | .theme-hey{ 2 | .avue-sidebar{ 3 | background-color: #fff; 4 | box-shadow: 0 1px 4px rgba(0,21,41,.08); 5 | .el-menu-item,.el-sub-menu__title{ 6 | i,span{ 7 | color: rgba(49,58,70,.8); 8 | } 9 | &:hover{ 10 | background: transparent; 11 | i,span{ 12 | color:#409eff; 13 | } 14 | } 15 | &.is-active { 16 | &:before { 17 | left:auto; 18 | right: 0 ; 19 | } 20 | background-color: #f0f6ff; 21 | i,span{ 22 | color:#409eff; 23 | } 24 | } 25 | } 26 | } 27 | .avue-logo{ 28 | background-color: #fff; 29 | box-shadow: none; 30 | .avue-logo_title{ 31 | color:#409eff; 32 | font-size: 20px; 33 | } 34 | } 35 | .avue-tags{ 36 | background: #f3f6f8; 37 | .el-tabs__item{ 38 | color: rgba(0,0,0,.65) !important; 39 | } 40 | .is-active{ 41 | background-color: #fff; 42 | border-bottom: none !important; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "noImplicitThis": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "lib": ["esnext", "dom"], 16 | "skipLibCheck": true, 17 | "removeComments": true, 18 | "paths": { 19 | "@/*": ["./src/*"], 20 | "app/*": ["./src/*"], 21 | "config/*": ["./src/config"], 22 | "components/*": ["./src/components/*"], 23 | "assets/*": ["./src/assets/*"], 24 | "api/*": ["./src/api/*"], 25 | "router/*": ["./src/router/*"], 26 | "http/*": ["./src/http/*"], 27 | "store/*": ["./src/store/*"], 28 | "views/*": ["./src/views/*"], 29 | "utils/*": ["./src/utils/*"] 30 | } 31 | }, 32 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "build/*.ts"], 33 | "exclude": ["node_modules"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/basic-container/main.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | 36 | 54 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | .el-card.is-always-shadow { 2 | box-shadow: none; 3 | border: none !important; 4 | } 5 | .el-menu { 6 | border-right: none; 7 | } 8 | 9 | .el-message__icon, 10 | .el-message__content { 11 | display: inline-block; 12 | } 13 | 14 | .el-date-editor .el-range-input, 15 | .el-date-editor .el-range-separator { 16 | height: auto; 17 | overflow: hidden; 18 | } 19 | 20 | .el-dialog__wrapper { 21 | z-index: 2048; 22 | } 23 | 24 | 25 | .el-col { 26 | margin-bottom: 8px; 27 | } 28 | 29 | .el-main { 30 | padding: 0 !important; 31 | } 32 | .el-dropdown-menu__item--divided:before, .el-menu, .el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .el-menu--horizontal>.el-sub-menu .el-sub-menu__title:hover { 33 | background-color:transparent; 34 | } 35 | 36 | 37 | .el-dropdown-menu__item--divided:before, .el-menu, .el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .el-menu--horizontal>.el-sub-menu .el-sub-menu__title:hover{ 38 | background-color: transparent !important; 39 | } 40 | -------------------------------------------------------------------------------- /src/hooks/useRefreshToken.ts: -------------------------------------------------------------------------------- 1 | import website from '@/config/website'; 2 | import { validatenull } from '@/utils/validate'; 3 | import dayjs from 'dayjs'; 4 | import { ref, onBeforeMount, onUnmounted, Ref } from 'vue'; 5 | import { useUserStore } from 'store/index'; 6 | import { getStore } from 'utils/store'; 7 | 8 | /** 9 | * 刷新token 10 | */ 11 | export const useRefreshToken = () => { 12 | const refreshLock = ref(false); 13 | const refreshTime: Ref = ref(-1); 14 | const userStore = useUserStore(); 15 | const refreshToken = () => { 16 | refreshTime.value = setInterval(() => { 17 | const token = getStore({ name: 'token' }); 18 | let date1 = dayjs(token?.datetime); 19 | let date2 = dayjs(); 20 | const date = date1.diff(date2, 'month'); 21 | if (validatenull(date)) return; 22 | if (date >= website.tokenTime && !refreshLock.value) { 23 | refreshLock.value = true; 24 | userStore.RefreshToken().finally(() => { 25 | refreshLock.value = false; 26 | }); 27 | } 28 | }, 1000); 29 | }; 30 | onBeforeMount(refreshToken); 31 | onUnmounted(() => { 32 | clearTimeout(refreshTime.value as number); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory, Router, RouteRecordRaw } from 'vue-router'; 2 | import PageRouter from './page/index'; 3 | import ViewsRouter from './views/index'; 4 | import AvueRouter from './avue-router'; 5 | import i18n from 'app/lang/index'; 6 | import { getStore } from 'utils/store'; 7 | import { VueI18n } from 'vue-i18n'; 8 | 9 | export interface ARouter { 10 | generateTitle: (item: RouterTag, props?: Partial) => string; 11 | setTitle: (title?: string) => string; 12 | closeTag: (value?: string) => void; 13 | formatRoutes: (aMenu: RouterMenu[], first: boolean) => RouteRecordRaw[]; 14 | } 15 | export interface AVueRouter extends Router { 16 | avueRouter?: ARouter; 17 | } 18 | 19 | //创建路由 20 | const Router = createRouter({ 21 | history: createWebHistory(), 22 | routes: [...PageRouter, ...ViewsRouter] // 23 | }); 24 | 25 | const aRouter = new AvueRouter({ 26 | router: Router, 27 | i18n: i18n.global as VueI18n 28 | }); 29 | //解决pinia未挂载去调用useUserStore的问题 30 | const menuAll = getStore({ name: 'menuAll' }); 31 | aRouter.$router.avueRouter?.formatRoutes(menuAll?.content || [], true); 32 | 33 | export default aRouter.$router; 34 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import axios from 'http/axios'; 2 | 3 | export const loginByUsername = (username: string, password: string, code: string, redomStr: string) => 4 | axios.request({ 5 | url: 'user/login', 6 | method: 'post', 7 | meta: { 8 | isToken: false 9 | }, 10 | data: { 11 | username, 12 | password, 13 | code, 14 | redomStr 15 | } 16 | }); 17 | 18 | export const getUserInfo = () => 19 | axios.request({ 20 | url: 'user/getUserInfo', 21 | method: 'get' 22 | }); 23 | 24 | export const refeshToken = () => 25 | axios.request({ 26 | url: '/user/refresh', 27 | method: 'post' 28 | }); 29 | 30 | export const getMenu = (type: string | number = 0) => 31 | axios.request({ 32 | url: 'user/getMenu', 33 | method: 'get', 34 | params: { 35 | type 36 | } 37 | }); 38 | 39 | export const getTopMenu = () => 40 | axios.request({ 41 | url: 'user/getTopMenu', 42 | method: 'get' 43 | }); 44 | 45 | export const sendLogs = (list: unknown) => 46 | axios.request({ 47 | url: '/user/logout', 48 | method: 'post', 49 | data: list 50 | }); 51 | 52 | export const logout = () => 53 | axios.request({ 54 | url: '/user/logout', 55 | meta: { 56 | isToken: false 57 | }, 58 | method: 'get' 59 | }); 60 | -------------------------------------------------------------------------------- /src/option/crud/index.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return { 3 | height: 'auto', 4 | calcHeight: 80, 5 | translate: false, 6 | searchLabelWidth: 100, 7 | excelBtn: true, 8 | labelWidth: 110, 9 | selection: true, 10 | tip: false, 11 | index: true, 12 | align: 'center', 13 | headerAlign: 'center', 14 | border: true, 15 | stripe: true, 16 | column: [ 17 | { label: '姓名', searchLabel: 'kkk', prop: 'name', search: true }, 18 | { 19 | label: '状态', 20 | prop: 'status', 21 | type: 'select', 22 | dicData: [ 23 | { label: '不正常', value: 0 }, 24 | { label: '正常', value: 1 } 25 | ], 26 | search: true 27 | }, 28 | { 29 | label: '性别', 30 | prop: 'sex', 31 | type: 'select', 32 | dicData: [ 33 | { label: '男', value: 0 }, 34 | { label: '女', value: 1 } 35 | ], 36 | search: true 37 | }, 38 | { 39 | label: '创建时间', 40 | type: 'datetime', 41 | searchRange: true, 42 | search: true, 43 | startPlaceholder: '开始时间', 44 | endPlaceholder: '结束时间', 45 | format: 'YYYY-MM-DD HH:mm:ss', 46 | valueFormat: 'YYYY-MM-DD HH:mm:ss', 47 | prop: 'queryTime', 48 | addDisplay: false, 49 | editDisplay: false, 50 | hide: true 51 | } 52 | ] 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/option/form/basics.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return { 3 | height: 'auto', 4 | calcHeight: 80, 5 | translate: false, 6 | searchLabelWidth: 100, 7 | excelBtn: true, 8 | labelWidth: 110, 9 | selection: true, 10 | tip: false, 11 | index: true, 12 | align: 'center', 13 | headerAlign: 'center', 14 | border: true, 15 | stripe: true, 16 | column: [ 17 | { label: '姓名', searchLabel: 'kkk', prop: 'name', search: true }, 18 | { 19 | label: '状态', 20 | prop: 'status', 21 | type: 'select', 22 | dicData: [ 23 | { label: '不正常', value: 0 }, 24 | { label: '正常', value: 1 } 25 | ], 26 | search: true 27 | }, 28 | { 29 | label: '性别', 30 | prop: 'sex', 31 | type: 'select', 32 | dicData: [ 33 | { label: '男', value: 0 }, 34 | { label: '女', value: 1 } 35 | ], 36 | search: true 37 | }, 38 | { 39 | label: '创建时间', 40 | type: 'datetime', 41 | searchRange: true, 42 | search: true, 43 | startPlaceholder: '开始时间', 44 | endPlaceholder: '结束时间', 45 | format: 'YYYY-MM-DD HH:mm:ss', 46 | valueFormat: 'YYYY-MM-DD HH:mm:ss', 47 | prop: 'queryTime', 48 | addDisplay: false, 49 | editDisplay: false, 50 | hide: true 51 | } 52 | ] 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Q-Admin 中后台管理系统 2 | 3 | > 🎉🎉🎉 q-admin 是基于 vue3、TypeScript、vite2、element-plus、avue3、vue-router 实现的后台管理系统工程化模板;主体模块是由 avue-cli 改造 ts 版本;适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。有问题欢迎大家一起交流(如果对您有帮助,请动动鼠标在右上角给一颗星) 4 | 5 | #### 一、预览地址 6 | 7 | - https://virtual1680.github.io 8 | 9 | #### 二、文档地址 10 | 11 | [文档](https://www.kancloud.cn/smallwei/avue/579870) 12 | 13 | #### 三、仓库地址 14 | 15 | - gitee:https://gitee.com/virtual1680/q-admin 16 | - github:https://github.com/virtual1680/q-admin 17 | 18 | #### 四、项目功能 19 | 20 | - 🚀 avue 对列表分页、表单等操作全部进行封装,能快速提高开发速度(本人亲测能提高 80%以上,你值得拥有) 21 | - 📦️ 前端全局异常监控机制 22 | - 灵活的 10+多款主题自由配置(mac 主题) 23 | - 路由权限、菜单权限、登录权限 24 | - pinia 状态管理器,对数据进行缓存并使用插件本地化持久存储 25 | - 动态控制 keep-alive 对页面进行缓存 26 | - 面向全屏幕尺寸的响应式适配能力 27 | - 对国际化的支持(vue-i18n) 28 | - 前端路由动态服务端加载 29 | - 无限极动态路由加载 30 | - 模块的可拆卸化,达到开箱即用 31 | - axios 封装,可控的数据类型返回、全局错误拦截、取消重复请求 32 | - 定义 hooks useAutoResize,自动刷新 token 等 33 | - eslint、stylelint、prettier 代码格式化及校验规范控制 34 | - vite2 打包(组件依赖分析、svg 加载、cdn 引入资源、图片压缩) 35 | 36 | #### 五、页面 37 | 38 | ###### 登陆 39 | 40 | 41 | 42 | ###### 主页 43 | 44 | 45 | 46 | #### 参考项目 47 | 48 | - Avue-cli:https://gitee.com/smallweigit/avue-cli 49 | -------------------------------------------------------------------------------- /src/styles/theme/beautiful.scss: -------------------------------------------------------------------------------- 1 | .theme-beautiful{ 2 | .avue-sidebar{ 3 | background: linear-gradient(90deg,#006cff,#399efd)!important; 4 | .el-menu-item, .el-sub-menu__title{ 5 | background: linear-gradient(90deg,#006cff,#399efd)!important; 6 | } 7 | .el-menu-item,.el-sub-menu__title{ 8 | i,span{ 9 | color:#fff 10 | } 11 | .is-active{ 12 | background: #399efd!important; 13 | } 14 | &:hover,&.is-active{ 15 | background: #399efd!important; 16 | i,span{ 17 | background: #399efd!important; 18 | } 19 | } 20 | } 21 | } 22 | .avue-logo{ 23 | background: linear-gradient(90deg,#006cff,#399efd)!important; 24 | } 25 | 26 | .avue-tags{ 27 | .el-tabs__item{ 28 | font-size: 14px !important; 29 | color: #303133 !important; 30 | font-weight: 500 !important; 31 | border:1px solid #dcdfe6 !important; 32 | border-radius: 3px; 33 | height: 35px !important; 34 | line-height: 35px !important; 35 | margin: 5px 3px 8px 3px !important; 36 | &:hover{ 37 | color:#409EFF !important; 38 | border-color: #409EFF !important; 39 | } 40 | } 41 | .is-active{ 42 | color:#409EFF !important; 43 | border-color: #409EFF !important; 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/styles/theme/dark.scss: -------------------------------------------------------------------------------- 1 | .theme-dark { 2 | .avue-logo{ 3 | color: #fff; 4 | background-color: #2c3643; 5 | box-shadow: none; 6 | .avue-logo_title{ 7 | font-size: 20px; 8 | font-weight: 400; 9 | } 10 | } 11 | .avue-top{ 12 | background-color: #2c3643; 13 | box-shadow: none; 14 | color:#ccc; 15 | i, span{ 16 | color:#ccc; 17 | } 18 | } 19 | .avue-main{ 20 | padding: 0 5px; 21 | } 22 | .avue-tags{ 23 | padding-left: 0; 24 | background-color: #2c3643; 25 | border-color: transparent; 26 | .el-tabs__item{ 27 | margin: 0 !important; 28 | background-color: #262d37; 29 | &.is-active{ 30 | color:#262d37 !important; 31 | background-color:#fff !important; 32 | border-color: #262d37 !important; 33 | } 34 | } 35 | } 36 | .avue-main{ 37 | background-color: #2c3643; 38 | } 39 | .avue-sidebar{ 40 | background-color: #2c3643; 41 | box-shadow: none; 42 | .el-menu-item,.el-sub-menu__title{ 43 | i,span{ 44 | color:#ccc; 45 | } 46 | &:hover,&.is-active{ 47 | background: #262d37; 48 | i,span{ 49 | color: #fff; 50 | } 51 | &:before{ 52 | background-color: #000; 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/page/index/top/top-lang.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // @see: https://www.prettier.cn 2 | 3 | module.exports = { 4 | // 超过最大值换行 5 | printWidth: 150, 6 | // 缩进字节数 7 | tabWidth: 2, 8 | // 使用制表符而不是空格缩进行 9 | useTabs: true, 10 | // 结尾不用分号(true有,false没有) 11 | semi: true, 12 | // 使用单引号(true单双引号,false双引号) 13 | singleQuote: true, 14 | // 更改引用对象属性的时间 可选值"" 15 | quoteProps: 'as-needed', 16 | // 在对象,数组括号与文字之间加空格 "{ foo: bar }" 17 | bracketSpacing: true, 18 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 19 | trailingComma: 'none', 20 | // 在JSX中使用单引号而不是双引号 21 | jsxSingleQuote: false, 22 | // 多行HTML中的>放置在最后一行的结尾,而不是另起一行(默认false) 23 | bracketSameLine: true, 24 | // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 ,always:不省略括号 25 | arrowParens: 'avoid', 26 | // 如果文件顶部已经有一个 doclock,这个选项将新建一行注释,并打上@format标记。 27 | insertPragma: false, 28 | // 指定要使用的解析器,不需要写文件开头的 @prettier 29 | requirePragma: false, 30 | // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行 31 | proseWrap: 'preserve', 32 | // 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的 33 | htmlWhitespaceSensitivity: 'css', 34 | //将>多行 HTML元素的 放在最后一行的末尾, 35 | bracketSameLine: true, 36 | // 换行符使用 lf 结尾是 可选值"" 37 | endOfLine: 'auto', 38 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 39 | rangeStart: 0, 40 | rangeEnd: Infinity, 41 | // Vue文件脚本和样式标签缩进 42 | vueIndentScriptAndStyle: false 43 | }; 44 | -------------------------------------------------------------------------------- /src/page/index/top/top-menu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | -------------------------------------------------------------------------------- /src/styles/media.scss: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 992px) { 2 | .avue-sidebar{ 3 | position: fixed; 4 | top:0; 5 | left: -265px; 6 | z-index:1024; 7 | } 8 | .avue--collapse{ 9 | .avue-sidebar{ 10 | left:0; 11 | width: $sidebar_width; 12 | } 13 | .avue-logo{ 14 | width: $sidebar_width; 15 | } 16 | .avue-main{ 17 | margin-left:$sidebar_width; 18 | } 19 | } 20 | // ele的自适应 21 | .el-dialog, 22 | .el-message-box { 23 | width: 98%; 24 | } 25 | //登录页面 26 | .login-left { 27 | width: 100%; 28 | min-height: auto; 29 | .title{ 30 | margin-top: 20px; 31 | font-size: 20px; 32 | text-shadow:#000 1px 0 0,#000 0 1px 0,#000 -1px 0 0,#000 0 -1px 0; 33 | } 34 | .img{ 35 | width: 50px; 36 | } 37 | } 38 | .login-logo { 39 | padding-top: 30px; 40 | margin-left: -30px; 41 | } 42 | .login-border { 43 | border-radius: 5px; 44 | padding: 20px; 45 | margin: 0 auto; 46 | width: 100%; 47 | } 48 | .login-main { 49 | width: 100%; 50 | background-color: #fff; 51 | padding: 10px 20px; 52 | box-shadow:none 53 | } 54 | .login-container{ 55 | &::before { 56 | margin-left:0 57 | } 58 | } 59 | .top-bar__item { 60 | display: none; 61 | } 62 | } -------------------------------------------------------------------------------- /src/typings/axios.d.ts: -------------------------------------------------------------------------------- 1 | import type { Axios, AxiosRequestConfig } from 'axios'; 2 | // * http 3 | declare interface Result { 4 | code: number; 5 | msg: string; 6 | data: T; 7 | } 8 | type Params = Partial>; 9 | type Config = Omit; 10 | type RequestConfig = AxiosRequestConfig & { meta?: { isToken?: boolean; isSerialize?: boolean; returnType?: 'response' | 'data' } }; 11 | declare module 'axios' { 12 | declare interface AxiosInstance extends Axios { 13 | request>(config: RequestConfig): Promise; 14 | get>(url: string, params?: Params, config?: Config): Promise; 15 | post>(url: string, params?: Params, config?: Config): Promise; 16 | put>(url: string, params?: Params, config?: Config): Promise; 17 | delete>(url: string, params?: Params, config?: Config): Promise; 18 | head>(url: string, config?: AxiosRequestConfig): Promise; 19 | options>(url: string, config?: AxiosRequestConfig): Promise; 20 | patch>(url: string, data?: any, config?: AxiosRequestConfig): Promise; 21 | } 22 | } 23 | declare interface DataPage { 24 | current: number; 25 | pages: number; 26 | size: number; 27 | total: number; 28 | record: T[]; 29 | } 30 | // type DataPage = BaseDataPage & R; 31 | -------------------------------------------------------------------------------- /src/styles/theme/d2.scss: -------------------------------------------------------------------------------- 1 | .theme-d2 { 2 | .avue-logo{ 3 | color: #409EFF; 4 | background-color: #ebf1f6; 5 | box-shadow: none; 6 | .avue-logo_title{ 7 | font-size: 20px; 8 | font-weight: 400; 9 | } 10 | } 11 | .avue-top{ 12 | background-color: #ebf1f6; 13 | box-shadow: none; 14 | } 15 | .avue-main{ 16 | padding: 0 5px; 17 | } 18 | .avue-tags{ 19 | margin-left: 6px; 20 | padding: 0; 21 | border: 1px solid #e4e7ed; 22 | border-radius: 3px; 23 | background-color: #ebf1f6; 24 | box-shadow: none; 25 | .el-tabs__item{ 26 | border-left: 1px solid #cfd7e5 !important; 27 | margin: 0 !important; 28 | background-color: rgba(0,0,0,.03) !important; 29 | color: #606266 !important; 30 | font-size: 14px !important; 31 | font-weight: 500 !important; 32 | &:first-child{ 33 | border-left: none !important; 34 | } 35 | } 36 | .is-active{ 37 | border-bottom:1px solid #fff !important; 38 | background-color: #fff !important; 39 | color: #409EFF !important; 40 | } 41 | } 42 | .avue-sidebar{ 43 | background-color: #ebf1f6; 44 | box-shadow: none; 45 | .el-menu-item,.el-sub-menu__title{ 46 | i,span{ 47 | color:#606266 48 | } 49 | &:hover,&.is-active{ 50 | background: hsla(0,0%,100%,.5); 51 | i,span{ 52 | color: #409EFF; 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | url: '/user/login', 4 | method: 'post', 5 | response: () => { 6 | return { 7 | data: new Date().getTime() + '' 8 | }; 9 | } 10 | }, 11 | { 12 | url: '/user/logout', 13 | method: 'get', 14 | response: () => { 15 | return { 16 | data: new Date().getTime() + '' 17 | }; 18 | } 19 | }, 20 | { 21 | url: '/user/logout', 22 | method: 'post', 23 | response: () => { 24 | return { 25 | data: new Date().getTime() + '' 26 | }; 27 | } 28 | }, 29 | { 30 | url: '/user/refresh', 31 | method: 'post', 32 | response: () => { 33 | return { 34 | data: new Date().getTime() + '' 35 | }; 36 | } 37 | }, 38 | { 39 | url: '/user/getUserInfo', 40 | method: 'get', 41 | response: () => { 42 | return { 43 | data: { 44 | userInfo: { 45 | username: 'admin', 46 | name: 'avue', 47 | avatar: 48 | 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/656c960dca2e4a9796d8d341297efcbd~tplv-k3u1fbpfcp-no-mark:180:180:180:180.awebp?' 49 | }, 50 | roles: 'admin', 51 | permission: [ 52 | 'sys_crud_btn_add', 53 | 'sys_crud_btn_export', 54 | 'sys_menu_btn_add', 55 | 'sys_menu_btn_edit', 56 | 'sys_menu_btn_del', 57 | 'sys_role_btn1', 58 | 'sys_role_btn2', 59 | 'sys_role_btn3', 60 | 'sys_role_btn4', 61 | 'sys_role_btn5', 62 | 'sys_role_btn6' 63 | ] 64 | } 65 | }; 66 | } 67 | } 68 | ]; 69 | -------------------------------------------------------------------------------- /src/hooks/useDownload.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { ElNotification } from 'element-plus'; 3 | import { rest } from 'lodash-es'; 4 | 5 | type Nav = Navigator & { 6 | msSaveOrOpenBlob: (blob: Blob, fileName: string) => void; 7 | browserLanguage: string; 8 | }; 9 | /** 10 | * @description 接收数据流生成blob,创建链接,下载文件 11 | * @param api 导出表格的api接口方法(必传) 12 | * @param isNotify 是否有导出消息提示(默认为 true) 13 | * */ 14 | export const useDownload = async (api: (params: unknown) => Promise>, isNotify: boolean = false) => { 15 | if (isNotify) { 16 | ElNotification({ 17 | title: '温馨提示', 18 | message: '如果数据庞大会导致下载缓慢哦,请您耐心等待!', 19 | type: 'info', 20 | duration: 3000 21 | }); 22 | } 23 | try { 24 | const res = await api.call(api, rest); 25 | console.log(res); 26 | let blob = new Blob([res.data], { type: 'application/octet-stream;charset=utf-8;' }); 27 | let filename = res.headers['content-disposition'].split(';')[1]; 28 | // 兼容edge不支持createObjectURL方法 29 | if ('msSaveOrOpenBlob' in navigator) return (window.navigator as Nav).msSaveOrOpenBlob(blob, filename); 30 | 31 | let fileUrl = URL.createObjectURL(blob); 32 | let a = document.createElement('a'); 33 | document.body.appendChild(a); 34 | a.style.display = 'none'; 35 | a.href = fileUrl; 36 | a.download = filename; 37 | a.click(); 38 | document.body.removeChild(a); 39 | window.URL.revokeObjectURL(fileUrl); 40 | } catch (error) { 41 | console.log(error); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/http/cancel.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, Canceler } from 'axios'; 2 | import qs from 'qs'; 3 | // * 1.取消重复请求 2.在某个阶段取消所有请求 4 | 5 | let pendingMap = new Map(); 6 | export const getPendingUrl = (config: AxiosRequestConfig) => 7 | [config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&'); 8 | 9 | export class AxiosCanceler { 10 | /** 11 | * @description: 添加请求 12 | * @param {Object} config 13 | */ 14 | addPending(config: AxiosRequestConfig) { 15 | //检测是否有同一请求在执行中,有则移除 16 | this.removePending(config); 17 | const url = getPendingUrl(config); 18 | config.cancelToken = 19 | config.cancelToken || 20 | new axios.CancelToken(cancel => { 21 | if (!pendingMap.has(url)) { 22 | // 如果 pending 中不存在当前请求,则添加进去 23 | pendingMap.set(url, cancel); 24 | } 25 | }); 26 | } 27 | 28 | /** 29 | * @description: 移除请求 30 | * @param {Object} config 31 | */ 32 | removePending(config: AxiosRequestConfig) { 33 | const url = getPendingUrl(config); 34 | if (pendingMap.has(url)) { 35 | // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除 36 | const cancel = pendingMap.get(url); 37 | cancel && cancel(); 38 | pendingMap.delete(url); 39 | } 40 | } 41 | // * 清空所有pending 42 | removeAllPending() { 43 | pendingMap.forEach(cancel => { 44 | cancel && cancel(); 45 | }); 46 | pendingMap.clear(); 47 | } 48 | 49 | // * 重置 50 | reset(): void { 51 | pendingMap = new Map(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/router/page/index.ts: -------------------------------------------------------------------------------- 1 | import { useCommonStore } from 'store/common'; 2 | import { RouteRecordRaw } from 'vue-router'; 3 | export default [ 4 | { 5 | path: '/login', 6 | name: '登录页', 7 | component: () => (useCommonStore().getIsMacOs ? import('app/mac/login.vue') : import('app/page/login/index.vue')), 8 | meta: { 9 | keepAlive: true, 10 | isTab: false, 11 | isAuth: false 12 | } 13 | }, 14 | { 15 | path: '/lock', 16 | name: '锁屏页', 17 | component: () => (useCommonStore().getIsMacOs ? import('app/mac/lock.vue') : import('app/page/lock/index.vue')), 18 | meta: { 19 | keepAlive: true, 20 | isTab: false, 21 | isAuth: false 22 | } 23 | }, 24 | { 25 | path: '/404', 26 | component: () => import(/* webpackChunkName: "page" */ 'app/components/error-page/404.vue'), 27 | name: '404', 28 | meta: { 29 | keepAlive: true, 30 | isTab: false, 31 | isAuth: false 32 | } 33 | }, 34 | { 35 | path: '/403', 36 | component: () => import(/* webpackChunkName: "page" */ 'app/components/error-page/403.vue'), 37 | name: '403', 38 | meta: { 39 | keepAlive: true, 40 | isTab: false, 41 | isAuth: false 42 | } 43 | }, 44 | { 45 | path: '/500', 46 | component: () => import(/* webpackChunkName: "page" */ 'app/components/error-page/500.vue'), 47 | name: '500', 48 | meta: { 49 | keepAlive: true, 50 | isTab: false, 51 | isAuth: false 52 | } 53 | }, 54 | { 55 | path: '/', 56 | name: '主页', 57 | redirect: '/wel' 58 | } 59 | ] as RouteRecordRaw[]; 60 | -------------------------------------------------------------------------------- /src/views/util/crud.vue: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/store/logs.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { defineStore } from 'pinia'; 3 | import { PersistedStateOptions } from 'pinia-plugin-persistedstate'; 4 | import { sendLogs } from '@/api/user'; 5 | interface LogsStore { 6 | logsList: unknown[]; 7 | } 8 | 9 | // pinia持久化参数配置 10 | export const piniaPersistConfig = (key: string) => { 11 | const persist: PersistedStateOptions = { 12 | key, 13 | storage: window.localStorage 14 | }; 15 | return persist; 16 | }; 17 | 18 | export const useLogsStore = defineStore({ 19 | id: 'LogsStore', 20 | state: (): LogsStore => ({ logsList: [] }), 21 | getters: { 22 | getLogsList: state => state.logsList, 23 | getLogsLen: state => state.logsList.length || 0, 24 | getLogsFlag: state => state.logsList.length === 0 25 | }, 26 | actions: { 27 | SendLogs() { 28 | return new Promise((resolve: (value?: unknown) => void, reject) => { 29 | sendLogs(this.getLogsList) 30 | .then(() => { 31 | this.CLEAR_LOGS(); 32 | resolve(); 33 | }) 34 | .catch(error => { 35 | reject(error); 36 | }); 37 | }); 38 | }, 39 | ADD_LOGS({ type, message, stack, info }: Record) { 40 | this.logsList.push( 41 | Object.assign( 42 | { 43 | url: window.location.href, 44 | time: dayjs().format('YYYY-MM-DD HH:mm:ss') 45 | }, 46 | { 47 | type, 48 | message, 49 | stack, 50 | info: info.toString() 51 | } 52 | ) 53 | ); 54 | }, 55 | CLEAR_LOGS() { 56 | this.logsList = []; 57 | } 58 | }, 59 | persist: piniaPersistConfig('LogsStore') 60 | }); 61 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | 2 | .avue-sidebar { 3 | width: $sidebar_width; 4 | height: 100%; 5 | user-select: none; 6 | position: relative; 7 | height: 100%; 8 | position: relative; 9 | background-color: #031527; 10 | transition: width .2s; 11 | box-sizing: border-box; 12 | box-shadow: 2px 0 6px rgba(0,21,41,.35); 13 | .el-scrollbar__wrap { 14 | overflow-x: hidden; 15 | } 16 | .avue-menu{ 17 | height: calc(100% - #{$top_height}); 18 | } 19 | &--tip{ 20 | width:90%; 21 | height: 140px; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | border-radius: 5px; 26 | position: absolute; 27 | top:5px; 28 | left:5%; 29 | color:#ccc; 30 | z-index: 2; 31 | text-align: center; 32 | font-size: 14px; 33 | background-color: rgba(0,0,0,.4); 34 | } 35 | .el-menu-item,.el-sub-menu__title{ 36 | i,span{ 37 | color:hsla(0,0%,100%,.7); 38 | } 39 | &:hover{ 40 | background: transparent; 41 | i,span{ 42 | color:#fff; 43 | } 44 | } 45 | &.is-active { 46 | &:before { 47 | content: ''; 48 | top: 0; 49 | left: 0; 50 | bottom: 0; 51 | width: 4px; 52 | background: #409eff; 53 | position: absolute; 54 | } 55 | background-color: rgba(0,0,0,.8); 56 | i,span{ 57 | color:#fff; 58 | } 59 | } 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/page/login/thirdlogin.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 36 | 37 | 51 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | // * 路由菜单 2 | declare interface RouterMenu { 3 | id?: string; 4 | parentId: string; 5 | iconDefault: string; 6 | label: string; 7 | path: string; 8 | icon: string; 9 | iconBgColor: string; 10 | children: RouterMenu[]; 11 | query: Record; 12 | params: Record; 13 | href: string; 14 | fullPath: string; 15 | meta: { keepAlive?: boolean; isTab?: boolean; isAuth?: boolean; i18n?: string; roles?: string[]; parentId?: string }; 16 | name?: string | ((query: LocationQueryRaw) => string); 17 | component: string; 18 | iconColor?: string; 19 | hideInDesktop?: boolean; 20 | } 21 | // * 路由 tag 标签 22 | declare type RouterTag = Pick; 23 | 24 | // * 系统设置状态 25 | declare interface Setting { 26 | sidebar: string; 27 | tag: boolean; 28 | debug: boolean; 29 | collapse: boolean; 30 | search: boolean; 31 | lock: boolean; 32 | screenshot: boolean; 33 | fullscren: boolean; 34 | theme: boolean; 35 | menu: boolean; 36 | } 37 | // * 加载路由数据的key 38 | declare interface MenuKey { 39 | iconDefault: string; 40 | label: string; 41 | path: string; 42 | icon: string; 43 | children: string; 44 | query: string; 45 | href: string; 46 | meta: string; 47 | } 48 | // * 站点配置 49 | declare interface Website { 50 | title: string; 51 | logo: string; 52 | key: string; //配置主键,目前用于存储 53 | indexTitle: string; 54 | tokenTime: number; //token过期时间 55 | Authorization: string; 56 | //http的status默认放行不才用统一处理的, 57 | statusWhiteList: number[]; 58 | //配置首页不可关闭 59 | setting: Setting; 60 | fistPage: { 61 | name: string; 62 | path: string; 63 | }; 64 | //配置菜单的属性 65 | menu: MenuKey; 66 | } 67 | -------------------------------------------------------------------------------- /src/page/index/sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 52 | 53 | -------------------------------------------------------------------------------- /src/views/util/about.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite'; 2 | import { resolve } from 'path'; 3 | import { pluginList } from './build/plugins-config'; 4 | // import { server, preview } from './build/server-config'; 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(({ command, mode }) => { 7 | console.log(command, mode, '开始加载配置文件-=-=-=-=-=-='); 8 | const env = loadEnv(mode, process.cwd()); 9 | console.log(env.VITE_GLOB_APP_TITLE); 10 | 11 | return { 12 | base: env.VITE_BASE_URL, 13 | resolve: { 14 | alias: { 15 | '@': resolve(__dirname, 'src'), 16 | app: resolve(__dirname, 'src'), 17 | config: resolve(__dirname, 'config'), 18 | assets: resolve(__dirname, 'src/assets'), 19 | components: resolve(__dirname, 'src/components'), 20 | api: resolve(__dirname, 'src/api'), 21 | router: resolve(__dirname, 'src/router'), 22 | views: resolve(__dirname, 'src/views'), 23 | http: resolve(__dirname, 'src/http'), 24 | store: resolve(__dirname, 'src/store'), 25 | utils: resolve(__dirname, 'src/utils') 26 | } 27 | }, 28 | // server: server(env), 29 | // preview: preview(env), 30 | server: { 31 | host: true, 32 | open: 7098, 33 | https: false 34 | }, 35 | esbuild: { 36 | pure: env.VITE_DROP_CONSOLE ? ['console.log', 'debugger'] : [] 37 | }, 38 | build: { 39 | sourcemap: true, 40 | minify: 'terser', //esbuild // esbuild 打包更快,但是不能去除 console.log 41 | rollupOptions: { 42 | output: { 43 | // format: 'es', 44 | // manualChunks: { 45 | // 'element-plus': ['element-plus'] 46 | // }, 47 | chunkFileNames: 'assets/js/[name]-[hash].js', 48 | entryFileNames: 'assets/js/[name]-[hash].js', 49 | assetFileNames: 'assets/[ext]/[name]-[hash].[ext]' 50 | } 51 | } 52 | }, 53 | plugins: pluginList(env) 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/iframe/main.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 70 | 71 | 80 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | const TokenKey = 'saber-access-token'; 3 | const RefreshTokenKey = 'saber-refresh-token'; 4 | export function getToken() { 5 | return Cookies.get(TokenKey); 6 | } 7 | 8 | export function setToken(token: string) { 9 | return Cookies.set(TokenKey, token); 10 | } 11 | 12 | export function getRefreshToken() { 13 | return Cookies.get(RefreshTokenKey); 14 | } 15 | 16 | export function setRefreshToken(token: string) { 17 | return Cookies.set(RefreshTokenKey, token); 18 | } 19 | 20 | export function removeToken() { 21 | return Cookies.remove(TokenKey); 22 | } 23 | 24 | export function removeRefreshToken() { 25 | return Cookies.remove(RefreshTokenKey); 26 | } 27 | 28 | export const calcDate = (date1: number, date2: number) => { 29 | let date3 = date2 - date1; 30 | 31 | let days = Math.floor(date3 / (24 * 3600 * 1000)); 32 | 33 | let leave1 = date3 % (24 * 3600 * 1000); //计算天数后剩余的毫秒数 34 | let hours = Math.floor(leave1 / (3600 * 1000)); 35 | 36 | let leave2 = leave1 % (3600 * 1000); //计算小时数后剩余的毫秒数 37 | let minutes = Math.floor(leave2 / (60 * 1000)); 38 | 39 | let leave3 = leave2 % (60 * 1000); //计算分钟数后剩余的毫秒数 40 | let seconds = Math.round(date3 / 1000); 41 | return { 42 | leave1, 43 | leave2, 44 | leave3, 45 | days: days, 46 | hours: hours, 47 | minutes: minutes, 48 | seconds: seconds 49 | }; 50 | }; 51 | 52 | /** 53 | * 判断是否为空 54 | */ 55 | export function validatenull(val: unknown) { 56 | if (typeof val == 'boolean') { 57 | return false; 58 | } 59 | if (typeof val == 'number') { 60 | return false; 61 | } 62 | if (val instanceof Array) { 63 | if (val.length == 0) return true; 64 | } else if (val instanceof Object) { 65 | if (JSON.stringify(val) === '{}') return true; 66 | } else { 67 | if (val == 'null' || val == null || val == 'undefined' || val == undefined || val == '') return true; 68 | return false; 69 | } 70 | return false; 71 | } 72 | -------------------------------------------------------------------------------- /src/page/index/top/top-theme.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/styles/theme/iview.scss: -------------------------------------------------------------------------------- 1 | .theme-iview { 2 | .avue-logo{ 3 | background: #001529; 4 | box-shadow: none; 5 | text-align: center; 6 | .avue-logo_title{ 7 | padding: 5px 8px 8px 8px; 8 | border-top-left-radius: 5px; 9 | border-top-right-radius: 5px; 10 | border-bottom-left-radius: 3px; 11 | border-bottom-right-radius: 3px; 12 | font-size: 20px; 13 | color:#fff; 14 | font-weight: 500; 15 | display: inline; 16 | background-color: #409EFF; 17 | } 18 | } 19 | .avue-tags{ 20 | padding: 3px 5px 5px 0; 21 | background: #f0f0f0; 22 | box-shadow: inset 0 0 3px 2px hsla(0,0%,39.2%,.1); 23 | .is-active{ 24 | &:before{ 25 | background: #409EFF !important; 26 | } 27 | } 28 | .el-tabs__item{ 29 | padding: 0 15px !important; 30 | position: relative; 31 | height: 32px !important; 32 | line-height:32px !important; 33 | border: 1px solid #e8eaec!important; 34 | color: #515a6e!important; 35 | background: #fff!important; 36 | border-radius: 3px; 37 | &:before{ 38 | content:''; 39 | display: inline-block; 40 | width: 12px; 41 | height: 12px; 42 | margin-right:10px; 43 | border-radius: 50%; 44 | background: #e8eaec; 45 | } 46 | } 47 | } 48 | 49 | .avue-sidebar{ 50 | background: #001529; 51 | .el-menu-item{ 52 | &.is-active { 53 | background-color: #000c17; 54 | &:before { 55 | display: none; 56 | } 57 | i,span{ 58 | color:#409EFF; 59 | } 60 | } 61 | } 62 | .el-sub-menu{ 63 | .el-menu-item{ 64 | &.is-active { 65 | background-color: #409EFF; 66 | &:before { 67 | display: none; 68 | } 69 | i,span{ 70 | color:#fff; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/mac/lock.vue: -------------------------------------------------------------------------------- 1 | 17 | 63 | 66 | -------------------------------------------------------------------------------- /src/views/form/control.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 81 | 82 | 94 | -------------------------------------------------------------------------------- /src/mac/login.vue: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | 20 | 60 | 63 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | ::-webkit-scrollbar-track-piece { 11 | background-color: transparent; 12 | } 13 | ::-webkit-scrollbar { 14 | width: 7px; 15 | height: 7px; 16 | background-color: transparent; 17 | } 18 | ::-webkit-scrollbar-thumb { 19 | border-radius: 5px; 20 | background-color: hsla(220, 4%, 58%, .3); 21 | } 22 | } 23 | 24 | @mixin radius($width, $size, $color) { 25 | width: $width; 26 | height: $width; 27 | line-height: $width; 28 | border-radius: $width; 29 | text-align: center; 30 | border-width: $size; 31 | border-style: solid; 32 | border-color: $color; 33 | } 34 | 35 | @mixin relative { 36 | position: relative; 37 | width: 100%; 38 | height: 100%; 39 | } 40 | 41 | @mixin pct($pct) { 42 | width: #{$pct}; 43 | position: relative; 44 | margin: 0 auto; 45 | } 46 | 47 | @mixin triangle($width, $height, $color, $direction) { 48 | $width: $width/2; 49 | $color-border-style: $height solid $color; 50 | $transparent-border-style: $width solid transparent; 51 | height: 0; 52 | width: 0; 53 | @if $direction==up { 54 | border-bottom: $color-border-style; 55 | border-left: $transparent-border-style; 56 | border-right: $transparent-border-style; 57 | } 58 | @else if $direction==right { 59 | border-left: $color-border-style; 60 | border-top: $transparent-border-style; 61 | border-bottom: $transparent-border-style; 62 | } 63 | @else if $direction==down { 64 | border-top: $color-border-style; 65 | border-left: $transparent-border-style; 66 | border-right: $transparent-border-style; 67 | } 68 | @else if $direction==left { 69 | border-right: $color-border-style; 70 | border-top: $transparent-border-style; 71 | border-bottom: $transparent-border-style; 72 | } 73 | } -------------------------------------------------------------------------------- /src/page/index/top/top-lock.vue: -------------------------------------------------------------------------------- 1 | 7 | 22 | 23 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/store/tags.ts: -------------------------------------------------------------------------------- 1 | import website from 'app/config/website'; 2 | import { defineStore } from 'pinia'; 3 | import { PersistedStateOptions } from 'pinia-plugin-persistedstate'; 4 | // import { isFunction } from 'lodash-es'; 5 | const tagWel = website.fistPage; 6 | interface TagsStore { 7 | tagList: RouterTag[]; 8 | tag: RouterTag; 9 | tagWel: { name: string; path: string }; 10 | // 点击 tag item 时是否级联top-menu and sidebar -> default value false 11 | isCascade: boolean; 12 | } 13 | 14 | // pinia持久化参数配置 15 | export const piniaPersistConfig = (key: string) => { 16 | const persist: PersistedStateOptions = { 17 | key, 18 | storage: window.localStorage 19 | }; 20 | return persist; 21 | }; 22 | 23 | export const useTagsStore = defineStore({ 24 | id: 'TagsStore', 25 | state: (): TagsStore => ({ 26 | tagList: [], 27 | tag: {} as RouterMenu, 28 | tagWel: tagWel, 29 | isCascade: false 30 | }), 31 | getters: { 32 | getTagList: state => state.tagList, 33 | getTag: state => state.tag, 34 | getTagWel: state => state.tagWel, 35 | getTagsKeep: state => { 36 | return state.tagList 37 | .filter(ele => { 38 | return (ele.meta || {}).keepAlive; 39 | }) 40 | .map(ele => ele.fullPath); 41 | }, 42 | getIsCascade: state => state.isCascade 43 | }, 44 | actions: { 45 | SET_IS_CASCADE(isCascade: boolean) { 46 | this.isCascade = isCascade; 47 | }, 48 | ADD_TAG(action: RouterTag) { 49 | // if (isFunction(action.name)) action.name = action.name(action.query); 50 | this.tag = action; 51 | if (this.tagList.some(ele => ele.fullPath == action.fullPath)) return; 52 | this.tagList.push(action); 53 | }, 54 | DEL_TAG(fullPath?: string) { 55 | this.tagList = this.tagList.filter(item => { 56 | return item?.fullPath !== fullPath; 57 | }); 58 | }, 59 | DEL_ALL_TAG(tagList = []) { 60 | this.tagList = tagList; 61 | }, 62 | DEL_TAG_OTHER() { 63 | this.tagList = this.tagList.filter(item => { 64 | return [this.tag.fullPath, website.fistPage.path].includes(item.fullPath); 65 | }); 66 | } 67 | }, 68 | persist: piniaPersistConfig('TagsStore') 69 | }); 70 | -------------------------------------------------------------------------------- /src/views/form/group.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 76 | 77 | 89 | -------------------------------------------------------------------------------- /src/views/form/tabs.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 78 | 79 | 91 | -------------------------------------------------------------------------------- /src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { RequestEnum } from 'app/enums/http'; 2 | import axios from 'http/axios'; 3 | // import md5 from "js-md5"; 4 | 5 | export const info = (domain: string) => { 6 | return axios.get('/blade-system/tenant/info', { 7 | domain 8 | }); 9 | }; 10 | interface LoginResult { 11 | access_token: string; 12 | user_id: string; 13 | } 14 | 15 | /** 16 | * 登录 17 | * @param tenantId 18 | * @param username 19 | * @param password 20 | * @param type 21 | * @param key 22 | * @param code 23 | */ 24 | export const login = (tenantId: string, username: string, password: string, type: string, key: string, code: string) => { 25 | return axios.request({ 26 | url: '/blade-auth/oauth/token', 27 | method: RequestEnum.POST, 28 | params: { 29 | tenantId, 30 | username, 31 | password, 32 | grant_type: 'captcha', 33 | scope: 'all', 34 | type 35 | }, 36 | headers: { 37 | 'Tenant-Id': tenantId, 38 | 'Captcha-Key': key, 39 | 'Captcha-Code': code, 40 | 'User-Type': 'web_admin' 41 | } 42 | }); 43 | }; 44 | export const getExcel = (params: unknown) => { 45 | return axios.request({ 46 | url: `/lecent-mall/report/export`, 47 | responseType: 'arraybuffer', 48 | method: 'get', 49 | params 50 | }); 51 | }; 52 | 53 | /** 54 | * 登录图片验证码 55 | * @returns 56 | */ 57 | interface ImageCode { 58 | key: string; 59 | image: string; 60 | } 61 | // * 无 ResultData 包裹 62 | export const GetCaptcha = () => axios.get('/blade-auth/oauth/captcha'); 63 | // * 有 ResultData 包裹 64 | export const GetCaptcha1 = () => 65 | axios.request({ 66 | url: '/blade-auth/oauth/captcha?111' 67 | }); 68 | 69 | /** 70 | * 退出登录 71 | * @returns 72 | */ 73 | export const Logout = () => axios.get('/blade-auth/oauth/logout'); 74 | 75 | /** 76 | * 刷新token 77 | * @param refresh_token 78 | * @param tenantId 79 | * @returns 80 | */ 81 | export const RefreshToken = (refresh_token: string) => 82 | axios.post( 83 | '/blade-auth/oauth/token', 84 | { 85 | tenantId: '000000', 86 | refresh_token, 87 | grant_type: 'refresh_token', 88 | scope: 'all' 89 | }, 90 | { 91 | headers: { 92 | 'Tenant-Id': '000000' 93 | } 94 | } 95 | ); 96 | -------------------------------------------------------------------------------- /src/styles/theme/vip.scss: -------------------------------------------------------------------------------- 1 | .theme-vip { 2 | $color:rgba(246,202,157,.7); 3 | $is_active_color:#f6ca9d; 4 | .avue-top{ 5 | background-color: #20222a; 6 | } 7 | .el-dropdown{ 8 | color: $color; 9 | } 10 | .avue-logo{ 11 | .avue-logo_title{ 12 | background-image:-webkit-gradient(linear,left top,left bottom,from($color),to( $is_active_color)); 13 | -webkit-background-clip:text; 14 | -webkit-text-fill-color:transparent; 15 | font-weight: 400; 16 | } 17 | } 18 | .avue-breadcrumb{ 19 | i{ 20 | color: $color; 21 | } 22 | } 23 | .avue-sidebar{ 24 | .el-menu-item{ 25 | &.is-active { 26 | &:before { 27 | background: $color; 28 | } 29 | i,span{ 30 | color: $is_active_color; 31 | } 32 | } 33 | } 34 | } 35 | .avue-tags{ 36 | .el-tabs__item{ 37 | color: rgba(0, 0, 0, 0.4) !important; 38 | &.is-active{ 39 | color:$is_active_color !important; 40 | border-color: $is_active_color !important; 41 | } 42 | &:before{ 43 | background: $is_active_color; 44 | } 45 | } 46 | } 47 | .top-search { 48 | .el-input__inner{ 49 | color: $color; 50 | } 51 | input::-webkit-input-placeholder, 52 | textarea::-webkit-input-placeholder { 53 | /* WebKit browsers */ 54 | color: $color; 55 | } 56 | input:-moz-placeholder, 57 | textarea:-moz-placeholder { 58 | /* Mozilla Firefox 4 to 18 */ 59 | color: $color; 60 | } 61 | input::-moz-placeholder, 62 | textarea::-moz-placeholder { 63 | /* Mozilla Firefox 19+ */ 64 | color: $color; 65 | } 66 | input:-ms-input-placeholder, 67 | textarea:-ms-input-placeholder { 68 | /* Internet Explorer 10+ */ 69 | color: $color; 70 | } 71 | } 72 | .top-bar__item { 73 | i { 74 | color: $color; 75 | } 76 | } 77 | .avue-top{ 78 | 79 | .el-menu-item { 80 | i, 81 | span { 82 | color: $color; 83 | } 84 | &:hover { 85 | i, 86 | span { 87 | color:$is_active_color; 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/components/basic-video/plugin.ts: -------------------------------------------------------------------------------- 1 | export default class RecordVideo { 2 | video: HTMLVideoElement; 3 | mediaRecorder: MediaRecorder | null; 4 | chunks: BlobPart[]; 5 | /** 6 | * 构造函数 7 | * 8 | * @param {Object} videoObj 视频对象 9 | */ 10 | constructor(videoObj: HTMLVideoElement) { 11 | this.video = videoObj; 12 | this.mediaRecorder = null; 13 | this.chunks = []; 14 | } 15 | 16 | /** 17 | * 初始化 18 | * 19 | * @return {Object} promise 20 | */ 21 | init() { 22 | // 返回Promise对象 23 | // resolve 正常处理 24 | // reject 处理异常情况 25 | return new Promise((resolve: (value?: unknown) => void, reject) => { 26 | navigator.mediaDevices 27 | .getUserMedia({ 28 | audio: true, 29 | video: true 30 | // video: { 31 | // width: this.videoWidth, 32 | // height: this.videoHeight 33 | // } 34 | }) 35 | // 返回一个媒体内容的流 36 | .then(stream => { 37 | // 检测是否支持 srcObject,该属性在新的浏览器支持 38 | if ('srcObject' in this.video) { 39 | this.video.srcObject = stream; 40 | } else { 41 | // 兼容旧的浏览器 42 | // this.video.src = window.URL.createObjectURL(stream); 43 | } 44 | 45 | // 当视频的元数据已经加载时触发 46 | this.video.addEventListener('loadmetadata', () => { 47 | this.video.play(); 48 | }); 49 | this.mediaRecorder = new MediaRecorder(stream); 50 | // TODO 51 | this.mediaRecorder.addEventListener('dataavailable', (e: Event & { data: BlobPart }) => { 52 | this.chunks.push(e.data); 53 | }); 54 | resolve(); 55 | }) 56 | // 异常抓取,包括用于禁用麦克风、摄像头 57 | .catch(error => { 58 | reject(error); 59 | }); 60 | }); 61 | } 62 | 63 | /** 64 | * 视频开始录制 65 | */ 66 | startRecord() { 67 | if (this.mediaRecorder?.state === 'inactive') { 68 | this.mediaRecorder.start(); 69 | } 70 | } 71 | 72 | /** 73 | * 视频结束录制 74 | */ 75 | stopRecord() { 76 | if (this.mediaRecorder?.state === 'recording') { 77 | this.mediaRecorder.stop(); 78 | } 79 | } 80 | 81 | /** 82 | * 检测当前浏览器对否支持 83 | * 84 | * @return {boolean} 当前浏览器是否支持 85 | */ 86 | async isSupport() { 87 | // TODO 88 | const flag = navigator.mediaDevices && navigator.mediaDevices.getUserMedia; 89 | if (await flag()) { 90 | return true; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/page/login/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 72 | -------------------------------------------------------------------------------- /src/permission.ts: -------------------------------------------------------------------------------- 1 | import router, { AVueRouter } from './router/'; 2 | import { useUserStore, useCommonStore, useTagsStore } from 'store/index'; 3 | import { AxiosCanceler } from 'http/cancel'; 4 | import NProgress from 'nprogress'; // progress bar 5 | import 'nprogress/nprogress.css'; // progress bar style 6 | import { LocationQuery } from 'vue-router'; 7 | NProgress.configure({ showSpinner: false }); 8 | 9 | const axiosCanceler = new AxiosCanceler(); 10 | const lockPage = '/lock'; //锁屏页 11 | router.beforeEach((to, from, next) => { 12 | //路由切换时取消所有正在执行的请求 13 | axiosCanceler.removeAllPending(); 14 | const uStore = useUserStore(); 15 | const cStore = useCommonStore(); 16 | const tStore = useTagsStore(); 17 | const meta = to.meta || {}; 18 | const isMenu = meta.menu === undefined ? to.query.menu : meta.menu; 19 | cStore.SET_IS_MENU(isMenu === undefined); 20 | if (uStore.token) { 21 | if (cStore.getIsLock && to.path != lockPage) { 22 | //如果系统激活锁屏,全部跳转到锁屏页 23 | next({ path: lockPage }); 24 | } else if (to.path === '/login') { 25 | //如果登录成功访问登录页跳转到主页 26 | next({ path: '/' }); 27 | } else { 28 | //如果用户信息为空则获取用户信息,获取用户信息失败,跳转到登录页 29 | if (uStore.roles.length === 0) { 30 | uStore 31 | .GetUserInfo() 32 | .then(() => { 33 | next({ ...to, replace: true }); 34 | }) 35 | .catch(() => { 36 | uStore.FedLogOut().then(() => { 37 | next({ path: '/login' }); 38 | }); 39 | }); 40 | } else { 41 | const meta = to.meta || {}; 42 | const query: LocationQuery = to.query; 43 | if (meta.target) { 44 | window.open((query.url as string).replace(/#/g, '&')); 45 | return; 46 | } else if (meta.isTab !== false) { 47 | const name: string = query.name ? (query.name as string) : (to.name as string) || '-'; 48 | tStore.ADD_TAG({ 49 | label: name, 50 | path: to.path, 51 | fullPath: to.fullPath, 52 | params: to.params, 53 | query: to.query, 54 | meta: meta 55 | }); 56 | } 57 | next(); 58 | } 59 | } 60 | } else { 61 | //判断是否需要认证,没有登录访问去登录页 62 | if (meta.isAuth === false) { 63 | next(); 64 | } else { 65 | next('/login'); 66 | } 67 | } 68 | }); 69 | 70 | router.afterEach(to => { 71 | const cStore = useCommonStore(); 72 | NProgress.done(); 73 | let title = (router as AVueRouter).avueRouter?.generateTitle({ label: (to.name as string) || '-', ...to }); 74 | (router as AVueRouter).avueRouter?.setTitle(title); 75 | cStore.SET_IS_SEARCH(false); 76 | }); 77 | -------------------------------------------------------------------------------- /src/styles/theme/star.scss: -------------------------------------------------------------------------------- 1 | .theme-star { 2 | .avue-main{ 3 | background: transparent; 4 | } 5 | .avue-contail { 6 | background-image: url("/img/star-squashed.jpg"); 7 | background-size: 100% 100%; 8 | } 9 | .avue-logo{ 10 | color: #fff; 11 | } 12 | .avue-top, 13 | .avue-logo, 14 | .tags-container { 15 | background-color: transparent; 16 | } 17 | .el-card,.error-page { 18 | opacity: .9; 19 | } 20 | .avue-tabs { 21 | padding: 0 20px ; 22 | } 23 | .avue-tags{ 24 | background-color: transparent; 25 | border-top: none; 26 | } 27 | .avue-top{ 28 | .avue-breadcrumb{ 29 | color:#fff; 30 | } 31 | .el-menu-item{ 32 | i,span{ 33 | color:#fff; 34 | } 35 | &.is-active{ 36 | background-color: rgba(0,0,0,.4) 37 | } 38 | } 39 | .el-dropdown{ 40 | color:#fff; 41 | } 42 | } 43 | .avue-sidebar{ 44 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.15); 45 | background-color:transparent; 46 | .el-menu-item,.el-sub-menu__title{ 47 | i,span{ 48 | color:#fff 49 | } 50 | &:hover{ 51 | background: transparent; 52 | i,span{ 53 | color:#409EFF; 54 | } 55 | } 56 | &.is-active { 57 | background-color: rgba(0,0,0,.4); 58 | i,span{ 59 | color:#fff; 60 | } 61 | } 62 | } 63 | } 64 | 65 | .top-search { 66 | .el-input__inner{ 67 | color: #333; 68 | } 69 | input::-webkit-input-placeholder, 70 | textarea::-webkit-input-placeholder { 71 | /* WebKit browsers */ 72 | color: #fff; 73 | } 74 | input:-moz-placeholder, 75 | textarea:-moz-placeholder { 76 | /* Mozilla Firefox 4 to 18 */ 77 | color: #fff; 78 | } 79 | input::-moz-placeholder, 80 | textarea::-moz-placeholder { 81 | /* Mozilla Firefox 19+ */ 82 | color: #fff; 83 | } 84 | input:-ms-input-placeholder, 85 | textarea:-ms-input-placeholder { 86 | /* Internet Explorer 10+ */ 87 | color: #fff; 88 | } 89 | } 90 | .top-bar__item { 91 | i { 92 | color: #fff; 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | // 全局变量 2 | @import './variables.scss'; 3 | a { 4 | color: #333333; 5 | text-decoration: none; 6 | } 7 | * { 8 | outline: none; 9 | } 10 | .avue-sidebar, 11 | .avue-top, 12 | .avue-logo, 13 | .avue-layout .login-logo, 14 | .avue-main { 15 | transition: all 0.3s; 16 | } 17 | .avue-layout { 18 | display: flex; 19 | height: 100%; 20 | overflow: hidden; 21 | &--horizontal { 22 | flex-direction: column; 23 | .avue-sidebar { 24 | display: flex; 25 | width: 100%; 26 | height: $top_height; 27 | .avue-menu, 28 | .el-menu-item, 29 | .el-sub-menu__title { 30 | height: $top_height; 31 | line-height: $top_height; 32 | } 33 | .is-active::before { 34 | display: none; 35 | } 36 | } 37 | .avue-logo { 38 | width: $sidebar_width; 39 | } 40 | } 41 | } 42 | .avue-contail { 43 | width: 100%; 44 | height: 100%; 45 | background: #f0f2f5; 46 | background-repeat: no-repeat; 47 | background-size: 100%; 48 | } 49 | .avue--collapse { 50 | .avue-sidebar, 51 | .avue-logo { 52 | width: $sidebar_collapse; 53 | } 54 | } 55 | .avue-main { 56 | position: relative; 57 | box-sizing: border-box; 58 | display: flex; 59 | flex: 1; 60 | flex-direction: column; 61 | height: 100%; 62 | overflow: hidden; 63 | background: #f0f2f5; 64 | } 65 | #avue-view { 66 | flex: auto; 67 | margin-bottom: 10px; 68 | overflow-x: hidden; 69 | overflow-y: auto; 70 | } 71 | .avue-view { 72 | box-sizing: border-box; 73 | width: 100%; 74 | } 75 | .avue-footer { 76 | position: absolute; 77 | bottom: 0; 78 | width: 100%; 79 | text-align: center; 80 | .copyright { 81 | padding: 5px 0; 82 | font-size: 12px; 83 | color: #666666; 84 | } 85 | } 86 | .mac_bg { 87 | position: fixed; 88 | top: 0; 89 | right: 0; 90 | bottom: 0; 91 | left: 0; 92 | background-color: #000000; 93 | background-image: url('/img/bg.jpg'); 94 | } 95 | 96 | // ele样式覆盖 97 | @import './element-ui.scss'; 98 | 99 | // 顶部右侧显示 100 | @import './top.scss'; 101 | 102 | // 导航标签 103 | @import './tags.scss'; 104 | 105 | // 工具类函数 106 | @import './mixin.scss'; 107 | 108 | // 侧面导航栏 109 | @import './sidebar.scss'; 110 | 111 | //主题 112 | @import './theme/index.scss'; 113 | 114 | //通用配置 115 | @import './normalize.scss'; 116 | 117 | //图标配置 118 | @import './iconfont.scss'; 119 | 120 | //登录样式 121 | @import './login.scss'; 122 | 123 | //适配 124 | @import './media.scss'; 125 | 126 | //滚动条样式 127 | @include scrollBar; 128 | -------------------------------------------------------------------------------- /src/assets/svg/icon-msg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/page/index/top/top-logs.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 74 | 75 | 84 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: qinhongyang virtual1680@gmail.com 3 | * @Date: 2022-08-11 17:21:48 4 | * @LastEditTime: 2022-09-19 17:03:32 5 | * @Description: 暂无 6 | */ 7 | // @see: https://stylelint.io 8 | 9 | module.exports = { 10 | /* 继承某些已有的规则 */ 11 | extends: [ 12 | 'stylelint-config-standard', // 配置stylelint拓展插件 13 | 'stylelint-config-html/vue', // 配置 vue 中 template 样式格式化 14 | 'stylelint-config-standard-scss', // 配置stylelint scss插件 15 | 'stylelint-config-recommended-vue/scss', // 配置 vue 中 scss 样式格式化 16 | 'stylelint-config-recess-order', // 配置stylelint css属性书写顺序插件, 17 | 'stylelint-config-prettier' // 配置stylelint和prettier兼容 18 | ], 19 | overrides: [ 20 | // 扫描 .vue/html 文件中的 119 | -------------------------------------------------------------------------------- /src/components/import-excel/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 88 | -------------------------------------------------------------------------------- /src/styles/tags.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .avue-tags { 4 | user-select: none; 5 | position: relative; 6 | padding: 0 10px; 7 | margin-bottom: 10px; 8 | box-sizing: border-box; 9 | border-top: 1px solid #f6f6f6; 10 | background-color: #fff; 11 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05); 12 | .el-tabs--card>.el-tabs__header { 13 | margin: 0; 14 | } 15 | .el-tabs--card>.el-tabs__header .el-tabs__nav, 16 | .el-tabs--card>.el-tabs__header .el-tabs__item, 17 | .el-tabs--card>.el-tabs__header { 18 | border: none; 19 | } 20 | .el-tabs--card>.el-tabs__header .el-tabs__item:first-child { 21 | border-left-width: 1px 22 | } 23 | .el-tabs--card>.el-tabs__header .el-tabs__item { 24 | margin: 0 3px; 25 | height: 40px; 26 | line-height:40px; 27 | font-size: 13px; 28 | font-weight: normal; 29 | color: #ccc; 30 | &.is-active { 31 | color: #409EFF; 32 | border-bottom: 3px solid #409EFF; 33 | } 34 | } 35 | .el-tabs__nav-prev, 36 | .el-tabs__nav-next { 37 | width: 20px; 38 | line-height: 40px; 39 | font-size: 18px; 40 | text-align: center; 41 | } 42 | &__box { 43 | position: relative; 44 | box-sizing: border-box; 45 | padding-right: 106px; 46 | width: 100%; 47 | .el-tabs__item { 48 | &:first-child { 49 | .is-icon-close { 50 | display: none; 51 | } 52 | } 53 | } 54 | } 55 | &__contentmenu{ 56 | position: fixed; 57 | width:120px; 58 | background-color: #fff; 59 | z-index:1024; 60 | border-radius: 5px; 61 | box-shadow: 1px 2px 10px #ccc; 62 | .item{ 63 | cursor: pointer; 64 | font-size: 14px; 65 | padding: 8px 20px 8px 15px; 66 | color: #606266; 67 | &:first-child{ 68 | border-top-left-radius: 5px; 69 | border-top-right-radius: 5px; 70 | } 71 | &:last-child{ 72 | border-bottom-left-radius: 5px; 73 | border-bottom-right-radius: 5px; 74 | } 75 | &:hover{ 76 | background-color: #409EFF; 77 | color:#fff; 78 | } 79 | } 80 | } 81 | &__menu { 82 | position: absolute !important; 83 | top: 3px; 84 | right: 0; 85 | padding: 1px 0 0 15px; 86 | box-sizing: border-box; 87 | } 88 | } -------------------------------------------------------------------------------- /src/views/crud/page.vue: -------------------------------------------------------------------------------- 1 | 7 | 24 | 25 | 97 | 103 | -------------------------------------------------------------------------------- /src/lang/zh.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | tip: '提示', 3 | title: 'Avue 管理后台快速开发框架', 4 | logoutTip: '退出系统, 是否继续?', 5 | submitText: '确定', 6 | cancelText: '取消', 7 | search: '请输入搜索内容', 8 | menuTip: '没有发现菜单', 9 | feedback: '意见反馈', 10 | submit: '提交', 11 | wel: { 12 | info: 'Hello, Virtual1680, Avue一款超乎你想象的框架!', 13 | dept: '我是avue框架爱好者,为了满足avue-cli TS 版本的需求,特意改写了avue-cli,希望能给大家带来帮助,有问题可以一起交流', 14 | team: '团队内排名', 15 | project: '项目访问', 16 | count: '项目数', 17 | data: { 18 | subtitle: '实时', 19 | column1: '分类统计', 20 | column2: '附件统计', 21 | column3: '文章统计', 22 | key1: '分', 23 | key2: '附', 24 | key3: '评', 25 | text1: '当前分类总记录数', 26 | text2: '当前上传的附件数', 27 | text3: '评论次数' 28 | }, 29 | data2: { 30 | column1: '今日注册', 31 | column2: '今日登录', 32 | column3: '今日订阅', 33 | column4: '今日评论' 34 | }, 35 | data3: { 36 | column1: '转化率(日同比 28%)', 37 | column2: '签到率(日同比 11%)', 38 | column3: '签到率(日同比 11%)' 39 | }, 40 | data4: { 41 | column1: '错误日志', 42 | column2: '数据展示', 43 | column3: '权限管理', 44 | column4: '用户管理' 45 | }, 46 | table: { 47 | rw: '工作任务', 48 | nr: '工作内容', 49 | sj: '工作时间' 50 | } 51 | }, 52 | route: { 53 | setting: '个人设置', 54 | detail: '详情页', 55 | info: '个人信息', 56 | website: '官方网站(内嵌页面)', 57 | dashboard: '首页', 58 | more: '更多', 59 | tags: '标签', 60 | store: '本地存储', 61 | api: '全局函数(外链页面)', 62 | logs: '日志监控', 63 | table: '表格', 64 | crud: '模板', 65 | params: '参数', 66 | form: '表单', 67 | data: '数据展示', 68 | permission: '权限', 69 | top: '返回顶部', 70 | affix: '图钉', 71 | cache: '缓冲', 72 | error: '异常页面', 73 | test: '测试页面', 74 | out: '外部页面', 75 | about: '关于' 76 | }, 77 | login: { 78 | title: '登录 ', 79 | info: '通用管理系统快速开发框架', 80 | username: '请输入账号', 81 | password: '请输入密码', 82 | wechat: '微信', 83 | qq: 'QQ', 84 | phone: '请输入手机号', 85 | code: '请输入验证码', 86 | submit: '登录', 87 | faceLogin: '刷脸登录', 88 | userLogin: '账号密码', 89 | phoneLogin: '手机号登录', 90 | thirdLogin: '第三方登录', 91 | msgText: '发送验证码', 92 | msgSuccess: '秒后重发' 93 | }, 94 | navbar: { 95 | setting: '个人设置', 96 | logOut: '退出登录', 97 | userinfo: '个人信息', 98 | dashboard: '首页', 99 | lock: '锁屏', 100 | bug: '没有错误日志', 101 | bugs: '条错误日志', 102 | screenfullF: '退出全屏', 103 | screenfull: '全屏', 104 | language: '中英文', 105 | notice: '消息通知', 106 | theme: '主题', 107 | color: '换色' 108 | }, 109 | tagsView: { 110 | search: '搜索', 111 | menu: '更多', 112 | closeOthers: '关闭其它', 113 | closeAll: '关闭所有' 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/views/util/table.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/page/index/sidebar/sidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 74 | -------------------------------------------------------------------------------- /src/store/common.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { PersistedStateOptions } from 'pinia-plugin-persistedstate'; 3 | import website from 'app/config/website'; 4 | import { setStore } from 'utils/store'; 5 | 6 | interface CommonStore { 7 | language: string; 8 | isCollapse: boolean; 9 | isFullScren: boolean; 10 | isMenu: boolean; 11 | isSearch: boolean; 12 | isRefresh: boolean; 13 | isLock: boolean; 14 | themeName: string; 15 | lockPasswd: string; 16 | website: Website; 17 | setting: Setting; 18 | } 19 | 20 | // pinia持久化参数配置 21 | export const piniaPersistConfig = (key: string) => { 22 | const persist: PersistedStateOptions = { 23 | key, 24 | storage: window.localStorage 25 | }; 26 | return persist; 27 | }; 28 | 29 | export const useCommonStore = defineStore({ 30 | id: 'CommonStore', 31 | state: (): CommonStore => ({ 32 | language: 'zh-cn', 33 | isCollapse: false, 34 | isFullScren: false, 35 | isMenu: true, 36 | isSearch: false, 37 | isRefresh: true, 38 | isLock: false, 39 | themeName: 'default', 40 | lockPasswd: '', 41 | website: website, 42 | setting: website.setting 43 | }), 44 | getters: { 45 | getLanguage: state => state.language, 46 | getSetting: state => state.setting, 47 | getThemeName: state => state.themeName, 48 | getIsMacOs: state => state.themeName === 'mac-os', 49 | getIsRefresh: state => state.isRefresh, 50 | getIsSearch: state => state.isSearch, 51 | getIsHorizontal: state => state.setting.sidebar === 'horizontal', 52 | getIsCollapse: state => state.isCollapse, 53 | getIsLock: state => state.isLock, 54 | getIsFullScren: state => state.isFullScren, 55 | getIsMenu: state => state.isMenu, 56 | getLockPasswd: state => state.lockPasswd 57 | }, 58 | actions: { 59 | SET_LANGUAGE(language: string) { 60 | this.language = language; 61 | setStore({ 62 | name: 'language', 63 | content: language 64 | }); 65 | }, 66 | SET_COLLAPSE() { 67 | this.isCollapse = !this.isCollapse; 68 | }, 69 | SET_IS_MENU(menu: boolean) { 70 | this.isMenu = menu; 71 | }, 72 | SET_IS_REFRESH(refresh: boolean) { 73 | this.isRefresh = refresh; 74 | }, 75 | SET_IS_SEARCH(search: boolean) { 76 | this.isSearch = search; 77 | }, 78 | SET_FULLSCREN() { 79 | this.isFullScren = !this.isFullScren; 80 | }, 81 | SET_LOCK() { 82 | this.isLock = true; 83 | }, 84 | SET_THEME_NAME(themeName: string) { 85 | this.themeName = themeName; 86 | }, 87 | SET_LOCK_PASSWD(lockPasswd: string) { 88 | this.lockPasswd = lockPasswd; 89 | }, 90 | CLEAR_LOCK() { 91 | this.isLock = false; 92 | this.lockPasswd = ''; 93 | } 94 | }, 95 | persist: piniaPersistConfig('CommonStore') 96 | }); 97 | -------------------------------------------------------------------------------- /src/views/form/data-type.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 97 | 98 | 110 | -------------------------------------------------------------------------------- /src/views/form/data-filter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 99 | 100 | 112 | -------------------------------------------------------------------------------- /src/styles/top.scss: -------------------------------------------------------------------------------- 1 | .avue-top { 2 | position: relative; 3 | box-sizing: border-box; 4 | height: $top_height; 5 | font-size: 28px; 6 | line-height: $top_height; 7 | color: rgb(0 0 0 / 65%); 8 | white-space: nowrap; 9 | background-color: #ffffff; 10 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 15%); 11 | .el-menu-item { 12 | i, 13 | span { 14 | font-size: 13px; 15 | } 16 | } 17 | } 18 | .avue-breadcrumb { 19 | height: 100%; 20 | i { 21 | font-size: 30px !important; 22 | } 23 | &--active { 24 | transform: rotate(90deg); 25 | } 26 | } 27 | .top-user { 28 | display: flex; 29 | align-items: center; 30 | margin-left: 20px; 31 | 32 | // i{ 33 | // margin-left: 5px; 34 | // } 35 | } 36 | .top-menu { 37 | flex: 1; 38 | .el-menu-item { 39 | padding: 0 10px; 40 | border: none; 41 | } 42 | } 43 | .top-search { 44 | position: absolute !important; 45 | top: 0; 46 | right: 200px; 47 | width: 300px; 48 | line-height: $top_height; 49 | .el-input__wrapper { 50 | font-size: 13px; 51 | background-color: transparent; 52 | border: none; 53 | box-shadow: none; 54 | } 55 | } 56 | .top-bar__img { 57 | box-sizing: border-box; 58 | width: 30px; 59 | height: 30px; 60 | padding: 2px; 61 | margin: 0 5px; 62 | vertical-align: middle; 63 | border: 1px solid #eeeeee; 64 | border-radius: 100%; 65 | } 66 | .top-bar__left, 67 | .top-bar__right { 68 | position: absolute; 69 | top: 0; 70 | height: $top_height; 71 | margin-top: 2px; 72 | i { 73 | line-height: $top_height; 74 | } 75 | } 76 | .top-bar__left { 77 | left: 10px; 78 | } 79 | .top-bar__right { 80 | right: 10px; 81 | display: flex; 82 | align-items: center; 83 | } 84 | .top-bar__item { 85 | position: relative; 86 | display: inline-block; 87 | height: $top_height; 88 | margin: 0 7px; 89 | font-size: 16px; 90 | .el-badge__content.is-fixed { 91 | top: 12px; 92 | right: 5px; 93 | } 94 | } 95 | .top-bar__title { 96 | box-sizing: border-box; 97 | height: 100%; 98 | padding-left: 50px; 99 | overflow: hidden; 100 | font-size: inherit; 101 | font-weight: 400; 102 | text-overflow: ellipsis; 103 | white-space: nowrap; 104 | } 105 | .avue-logo { 106 | box-sizing: border-box; 107 | height: $top_height; 108 | overflow: hidden; 109 | font-size: 20px; 110 | line-height: $top_height; 111 | color: #ffffff; 112 | background-color: #031527; 113 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 15%); 114 | &_title { 115 | display: block; 116 | font-size: 20px; 117 | font-weight: bold; 118 | text-align: center; 119 | } 120 | &_subtitle { 121 | display: block; 122 | font-size: 18px; 123 | font-weight: bold; 124 | color: #ffffff; 125 | text-align: center; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%- title %> 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
正在加载 <%- title %>
18 |
19 |
20 | 21 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/mac/login.css: -------------------------------------------------------------------------------- 1 | @keyframes loginErrorAnimation { 2 | 0% { 3 | margin-left: -30px; 4 | } 5 | 50% { 6 | margin-left: 30px; 7 | } 8 | 100% { 9 | margin-left: 0; 10 | } 11 | } 12 | .login { 13 | position: fixed; 14 | left: 0; 15 | right: 0; 16 | top: 0; 17 | bottom: 0; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | color: white; 23 | margin-top: -100px; 24 | z-index: 99999; 25 | backdrop-filter: blur(20px); 26 | } 27 | 28 | .head { 29 | padding: 3px; 30 | background-size: 40% auto; 31 | background-position: center center; 32 | height: 150px; 33 | width: 150px; 34 | border-radius: 100%; 35 | box-sizing: border-box; 36 | box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.1); 37 | margin-top: -50px; 38 | overflow: hidden; 39 | } 40 | .head img { 41 | border-radius: 100%; 42 | width: 100%; 43 | height: 100%; 44 | } 45 | 46 | .message { 47 | margin-top: 20px; 48 | font-size: 20px; 49 | text-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.3); 50 | color: #eee; 51 | margin-bottom: 50px; 52 | } 53 | 54 | .password { 55 | transition: width 0.3s; 56 | } 57 | 58 | .password-in { 59 | width: 155px; 60 | } 61 | 62 | .login-button { 63 | position: absolute; 64 | top: 5px; 65 | right: -50px; 66 | transition: right 0.3s; 67 | } 68 | 69 | .click-enable { 70 | right: 0; 71 | } 72 | 73 | .error { 74 | animation: loginErrorAnimation 0.2s ease 3; 75 | } 76 | 77 | ::-webkit-input-placeholder { 78 | color: #fff; 79 | } 80 | 81 | ::-moz-placeholder { 82 | color: #fff; 83 | } 84 | 85 | :-ms-input-placeholder { 86 | color: #fff; 87 | } 88 | 89 | .form { 90 | display: flex; 91 | flex-direction: column; 92 | align-items: center; 93 | } 94 | 95 | .item { 96 | position: relative; 97 | width: 280px; 98 | display: flex; 99 | align-items: center; 100 | } 101 | .item input { 102 | color: white; 103 | outline: none; 104 | border: none; 105 | margin: 5px 0; 106 | font-size: 16px; 107 | background-color: rgba(255, 255, 255, 0.3); 108 | padding: 8px 24px; 109 | border-radius: 20px; 110 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); 111 | flex: 1; 112 | } 113 | 114 | .item .iconfont { 115 | margin-left: 10px; 116 | vertical-align: middle; 117 | display: inline-block; 118 | background-color: rgba(255, 255, 255, 0.3); 119 | font-size: 18px; 120 | border-radius: 100%; 121 | width: 36px; 122 | height: 36px; 123 | text-align: center; 124 | line-height: 36px; 125 | cursor: pointer; 126 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1); 127 | } 128 | 129 | .item .iconfont:hover { 130 | background-color: rgba(255, 255, 255, 0.5); 131 | } -------------------------------------------------------------------------------- /src/router/views/index.ts: -------------------------------------------------------------------------------- 1 | import Layout from 'app/page/index/index.vue'; 2 | import { useCommonStore } from 'store/common'; 3 | import { RouteRecordRaw, LocationQuery } from 'vue-router'; 4 | export default [ 5 | { 6 | path: '/wel', 7 | // 8 | component: () => (useCommonStore().getIsMacOs ? import('@/mac/index.vue') : import('@/page/index/index.vue')), 9 | redirect: '/wel/index', 10 | children: [ 11 | { 12 | path: 'index', 13 | name: '首页', 14 | meta: { 15 | i18n: 'dashboard' 16 | }, 17 | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue') 18 | }, 19 | { 20 | path: 'more', 21 | name: '控制台', 22 | meta: { 23 | i18n: 'more', 24 | menu: false 25 | }, 26 | component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue') 27 | } 28 | ] 29 | }, 30 | { 31 | path: '/iframe', 32 | component: Layout, 33 | redirect: '/iframe', 34 | children: [ 35 | { 36 | path: '', 37 | name: '', 38 | component: () => import(/* webpackChunkName: "views" */ '@/components/iframe/main.vue') 39 | } 40 | ] 41 | }, 42 | { 43 | path: '/info', 44 | component: Layout, 45 | redirect: '/info/index', 46 | children: [ 47 | { 48 | path: 'index', 49 | name: '个人信息', 50 | meta: { 51 | i18n: 'info' 52 | }, 53 | component: () => import(/* webpackChunkName: "views" */ '@/views/user/info.vue') 54 | }, 55 | { 56 | path: 'setting', 57 | name: '个人设置', 58 | meta: { 59 | i18n: 'setting' 60 | }, 61 | component: () => import(/* webpackChunkName: "views" */ '@/views/user/setting.vue') 62 | } 63 | ] 64 | }, 65 | { 66 | path: '/query', 67 | component: Layout, 68 | children: [ 69 | { 70 | path: ':params', 71 | name: '参数传递', 72 | meta: { 73 | activeMenu: '/params' 74 | }, 75 | component: () => import(/* webpackChunkName: "views" */ '@/views/util/params.vue') 76 | } 77 | ] 78 | }, 79 | { 80 | path: '/tabs', 81 | component: Layout, 82 | children: [ 83 | { 84 | path: 'index', 85 | name: '测试页面1', 86 | meta: { 87 | isTab: false 88 | }, 89 | component: () => import(/* webpackChunkName: "views" */ '@/views/util/test.vue') 90 | } 91 | ] 92 | }, 93 | { 94 | path: '/detail/create', 95 | component: Layout, 96 | children: [ 97 | { 98 | path: '', 99 | name: (query: LocationQuery) => { 100 | return query.id ? '编辑页面' : '新增页面'; 101 | }, 102 | meta: { 103 | activeMenu: '/detail' 104 | }, 105 | component: () => import(/* webpackChunkName: "views" */ '@/views/util/detail.vue') 106 | } 107 | ] 108 | } 109 | ] as RouteRecordRaw[]; 110 | -------------------------------------------------------------------------------- /src/views/util/store.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 66 | 67 | 75 | -------------------------------------------------------------------------------- /src/views/form/custom-slot.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 85 | 86 | 103 | -------------------------------------------------------------------------------- /src/views/form/data-validate.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 103 | 104 | 116 | -------------------------------------------------------------------------------- /src/styles/theme/white.scss: -------------------------------------------------------------------------------- 1 | .theme-white { 2 | .el-menu--popup{ 3 | .el-menu-item{ 4 | background-color: #fff; 5 | i,span{ 6 | color:#666; 7 | } 8 | &:hover{ 9 | i,span{ 10 | color:#333; 11 | } 12 | } 13 | &.is-active { 14 | background-color: #409EFF; 15 | &:before { 16 | content: ''; 17 | top: 0; 18 | left: 0; 19 | bottom: 0; 20 | width: 4px; 21 | background: #409eff; 22 | position: absolute; 23 | } 24 | i,span{ 25 | color:#fff; 26 | } 27 | } 28 | } 29 | } 30 | .avue-top, 31 | .avue-logo, 32 | .tags-container { 33 | background-color: #409EFF; 34 | } 35 | .avue-sidebar--tip{ 36 | background-color:transparent; 37 | color:#333; 38 | } 39 | .el-dropdown{ 40 | color:#fff; 41 | } 42 | .avue-logo_title{ 43 | font-weight: 400; 44 | color:#fff; 45 | } 46 | .logo_title, 47 | .avue-breadcrumb 48 | { 49 | color: #fff ; 50 | i { 51 | color: #fff; 52 | } 53 | } 54 | .avue-top{ 55 | .el-menu-item { 56 | i, 57 | span { 58 | color: #fff ; 59 | } 60 | &:hover { 61 | i, 62 | span { 63 | color: #fff ; 64 | } 65 | } 66 | } 67 | } 68 | .avue-sidebar{ 69 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.15); 70 | background-color: #fff; 71 | .el-menu-item,.el-sub-menu__title{ 72 | i,span{ 73 | color:#666 74 | } 75 | &:hover{ 76 | background: transparent; 77 | i,span{ 78 | color:#333; 79 | } 80 | } 81 | &.is-active { 82 | background-color: #409EFF; 83 | i,span{ 84 | color:#fff; 85 | } 86 | } 87 | } 88 | } 89 | .top-search { 90 | .el-input__inner{ 91 | color: #333; 92 | } 93 | input::-webkit-input-placeholder, 94 | textarea::-webkit-input-placeholder { 95 | /* WebKit browsers */ 96 | color: #fff; 97 | } 98 | input:-moz-placeholder, 99 | textarea:-moz-placeholder { 100 | /* Mozilla Firefox 4 to 18 */ 101 | color: #fff; 102 | } 103 | input::-moz-placeholder, 104 | textarea::-moz-placeholder { 105 | /* Mozilla Firefox 19+ */ 106 | color: #fff; 107 | } 108 | input:-ms-input-placeholder, 109 | textarea:-ms-input-placeholder { 110 | /* Internet Explorer 10+ */ 111 | color: #fff; 112 | } 113 | } 114 | .top-bar__item { 115 | i { 116 | color: #fff; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { validatenull } from 'utils/validate'; 2 | import website from '@/config/website'; 3 | // import { number } from '@intlify/core-base'; 4 | 5 | const keyName = website.key + '-'; 6 | /** 7 | * 存储 localStorage or sessionStorage 8 | */ 9 | interface _BaseStorage { 10 | readonly datetime: number; 11 | readonly dataType: string; 12 | } 13 | export interface StorageParams { 14 | name: string; 15 | content: T; 16 | type?: 'local' | 'session'; 17 | } 18 | 19 | export const setStore = (params: Pick, 'name' | 'content' | 'type'>) => { 20 | let { name, content, type } = params; 21 | name = keyName + name; 22 | let obj = { 23 | dataType: typeof content, 24 | content: content, 25 | type: type || 'local', 26 | datetime: new Date().getTime() 27 | }; 28 | window[`${type || 'local'}Storage`].setItem(name, JSON.stringify(obj)); 29 | }; 30 | /** 31 | * 获取 Storage 32 | */ 33 | type ResultData = StorageParams & _BaseStorage; 34 | 35 | export const getStore = (params: Pick, 'name' | 'type'>): ResultData | null => { 36 | let { name, type } = params; 37 | name = keyName + name; 38 | let obj: ResultData, strJson: string | null; 39 | strJson = window[`${type || 'local'}Storage`].getItem(name); 40 | if (validatenull(strJson)) return null; 41 | try { 42 | obj = JSON.parse(strJson!); 43 | } catch { 44 | `${name} getItem error`; 45 | return null; 46 | } 47 | // if (debug) { 48 | // return obj; 49 | // } 50 | // if (T extends number) { 51 | // obj.content = Number(obj.content); 52 | // } else if (obj.dataType == 'boolean') { 53 | // obj.content = eval(obj.content as string); 54 | // } 55 | return obj; 56 | }; 57 | /** 58 | * 删除 Storage 59 | */ 60 | export const removeStore = (params: Pick, 'name' | 'type'>) => { 61 | let { name, type } = params; 62 | name = keyName + name; 63 | window[`${type || 'local'}Storage`].removeItem(name); 64 | }; 65 | 66 | /** 67 | * 获取全部 Storage 68 | */ 69 | export const getAllStore = (params: Pick) => { 70 | let list = []; 71 | let { type } = params; 72 | if (type === 'session') { 73 | for (let i = 0; i <= window.sessionStorage.length; i++) { 74 | list.push({ 75 | name: window.sessionStorage.key(i), 76 | content: getStore({ 77 | name: window.sessionStorage.key(i)!, 78 | type: 'session' 79 | }) 80 | }); 81 | } 82 | } else { 83 | for (let i = 0; i <= window.localStorage.length; i++) { 84 | list.push({ 85 | name: window.localStorage.key(i), 86 | content: getStore({ 87 | name: window.localStorage.key(i)! 88 | }) 89 | }); 90 | } 91 | } 92 | return list; 93 | }; 94 | 95 | /** 96 | * 清空全部 Storage 97 | */ 98 | export const clearStore = (params: Pick) => { 99 | let { type } = params; 100 | window[`${type || 'local'}Storage`].clear(); 101 | }; 102 | -------------------------------------------------------------------------------- /src/page/lock/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/http/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosResponse, AxiosRequestHeaders } from 'axios'; 2 | import { ElMessage } from 'element-plus'; 3 | import { getToken } from 'utils/token'; 4 | import router from 'router/index'; 5 | import { AxiosCanceler } from './cancel'; 6 | import { ResultEnum } from 'app/enums/http'; 7 | import { useUserStore } from 'store/index'; 8 | import { RequestConfig } from '@/typings/axios'; 9 | const axiosCanceler = new AxiosCanceler(); 10 | // import { baseUrl } from '@/config/env'; 11 | const config = { 12 | baseURL: '/', 13 | timeout: ResultEnum.TIMEOUT as number, // 请求超时时间 14 | withCredentials: true, // 跨域时候允许携带凭证 15 | validateStatus: (status: number) => { 16 | return status >= 200 && status <= 500; 17 | } 18 | }; 19 | 20 | let lock: number = 0; 21 | const instance = axios.create(config); 22 | instance.interceptors.request.use( 23 | (config: RequestConfig) => { 24 | const headers = config.headers; 25 | //添加请求 26 | axiosCanceler.addPending(config); 27 | (config.headers as AxiosRequestHeaders)['Authorization'] = `Basic c2FiZXI6c2FiZXJfc2VjcmV0`; 28 | if (getToken() && !headers?.isToken) { 29 | (config.headers as AxiosRequestHeaders)['Blade-Auth'] = 'bearer ' + getToken(); 30 | } 31 | (config.headers as AxiosRequestHeaders)['Content-Type'] = 'application/json;charset=UTF-8'; 32 | 33 | return config; 34 | }, 35 | (error: AxiosError) => { 36 | return Promise.reject(error); 37 | } 38 | ); 39 | instance.interceptors.response.use( 40 | (response: AxiosResponse) => { 41 | const { status: code, data } = response; 42 | const config: RequestConfig = response.config; 43 | //移除已执行完的请求(也就是当前请求) 44 | axiosCanceler.removePending(config); 45 | const status = data.code || code; 46 | const message = data.msg || data.error_description || '未知错误'; 47 | if (status === 401) { 48 | if (lock === 1) return false; 49 | lock === 0 && ElMessage.error(message); 50 | lock = 1; 51 | const userStore = useUserStore(); 52 | userStore.LogOut(); 53 | router.replace({ path: '/login' }); 54 | return Promise.reject(data); 55 | } else if (status !== ResultEnum.SUCCESS) { 56 | ElMessage.error(message); 57 | return Promise.reject(data); 58 | } 59 | lock = 0; 60 | if (config.headers?.notice) { 61 | ElMessage.success(message); 62 | } 63 | if (data.hasOwnProperty('error_code')) { 64 | ElMessage.error(message); 65 | return Promise.reject(data); 66 | } 67 | if (config.meta?.returnType === 'response') { 68 | return response; 69 | } else { 70 | return data; 71 | } 72 | }, 73 | (error: AxiosError) => { 74 | console.log(error); 75 | // if (error.message.includes("timeout")) { 76 | // ElMessage.error("请求超时,请刷新网页重试"); 77 | // } else if (error.response) { 78 | // if (error.response.status === 403) { 79 | // ElMessage.error("拒绝访问"); 80 | // } else if (error.response.status === 401) { 81 | // ElMessage.error("未登录"); 82 | // } else { 83 | // ElMessage.error(error.message); 84 | // } 85 | // } 86 | ElMessage.error(error.message); 87 | return Promise.reject(error); 88 | } 89 | ); 90 | 91 | export default instance; 92 | -------------------------------------------------------------------------------- /src/page/index/top/top-search.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 84 | 85 | 113 | -------------------------------------------------------------------------------- /src/lang/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Avue is a framework', 3 | tip: 'tip', 4 | logoutTip: 'Exit the system, do you want to continue?', 5 | submitText: 'submit', 6 | cancelText: 'cancel', 7 | search: 'Please input search content', 8 | menuTip: 'none menu list', 9 | feedback: 'feedback', 10 | submit: 'submit', 11 | wel: { 12 | info: 'Good morning, Smallwei, Avue is a framework', 13 | dept: 'a certain technology department', 14 | team: 'Team ranking', 15 | project: 'Project access', 16 | count: 'Item number', 17 | data: { 18 | subtitle: 'real time', 19 | column1: 'Classified statistics', 20 | column2: 'Annex statistics', 21 | column3: 'Article statistics', 22 | key1: 'C', 23 | key2: 'A', 24 | key3: 'A', 25 | text1: 'Total Record Number of Classifications', 26 | text2: 'Number of attachments Uploaded', 27 | text3: 'Comment frequency' 28 | }, 29 | data2: { 30 | column1: 'Registration today', 31 | column2: 'Login today', 32 | column3: 'Subscription today', 33 | column4: 'Todays review' 34 | }, 35 | data3: { 36 | column1: 'Conversion rate(Day 28%)', 37 | column2: 'Attendance rate(Day 11%)', 38 | column3: 'Attendance rate(Day 33%)' 39 | }, 40 | data4: { 41 | column1: 'Error log', 42 | column2: 'Data display', 43 | column3: 'Privilege management', 44 | column4: 'user management' 45 | }, 46 | table: { 47 | rw: 'Work Tasks', 48 | nr: 'Work content', 49 | sj: 'Working hours' 50 | } 51 | }, 52 | route: { 53 | setting: 'setting', 54 | detail: 'detail', 55 | info: 'info', 56 | website: 'website', 57 | dashboard: 'dashboard', 58 | more: 'more', 59 | tags: 'tags', 60 | store: 'store', 61 | permission: 'permission', 62 | api: 'api', 63 | logs: 'logs', 64 | table: 'table', 65 | crud: 'crud', 66 | params: 'params', 67 | form: 'form', 68 | top: 'backtop', 69 | affix: 'affix', 70 | data: 'data', 71 | cache: 'cache', 72 | error: 'error', 73 | test: 'test', 74 | out: 'out', 75 | about: 'about' 76 | }, 77 | login: { 78 | title: 'Login ', 79 | info: 'Rapid Development Framework of General Management System', 80 | username: 'Please input username', 81 | password: 'Please input a password', 82 | wechat: 'Wechat', 83 | qq: 'QQ', 84 | phone: 'Please input a phone', 85 | code: 'Please input a code', 86 | submit: 'Login', 87 | userLogin: 'userLogin', 88 | phoneLogin: 'phoneLogin', 89 | thirdLogin: 'thirdLogin', 90 | faceLogin: 'faceLogin', 91 | msgText: 'send code', 92 | msgSuccess: 'reissued code' 93 | }, 94 | navbar: { 95 | setting: 'setting', 96 | info: 'info', 97 | logOut: 'logout', 98 | userinfo: 'userinfo', 99 | dashboard: 'dashboard', 100 | lock: 'lock', 101 | bug: 'none bug', 102 | bugs: 'bug', 103 | screenfullF: 'exit screenfull', 104 | screenfull: 'screenfull', 105 | language: 'language', 106 | notice: 'notice', 107 | theme: 'theme', 108 | color: 'color' 109 | }, 110 | tagsView: { 111 | search: 'Search', 112 | menu: 'menu', 113 | closeOthers: 'Close Others', 114 | closeAll: 'Close All' 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /src/components/basic-video/main.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 74 | 125 | -------------------------------------------------------------------------------- /src/views/wel/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 54 | 55 | 138 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | /* 指定如何解析语法 */ 7 | parser: 'vue-eslint-parser', 8 | /* 继承某些已有的规则 */ 9 | extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:prettier/recommended'], 10 | // extends: [ 11 | // 'plugin:vue/vue3-essential', 12 | // 'eslint:recommended', 13 | // '@vue/typescript/recommended', 14 | // '@vue/prettier', 15 | // '@vue/prettier/@typescript-eslint', 16 | // ], 17 | parserOptions: { 18 | parser: '@typescript-eslint/parser', 19 | ecmaVersion: 2020, 20 | sourceType: 'module', 21 | jsxPragma: 'React', 22 | ecmaFeatures: { 23 | jsx: true 24 | } 25 | }, 26 | /** 27 | * "off" 或 0 -> 关闭规则 28 | * "warn" 或 1 -> 打开的规则作为警告(不影响代码执行) 29 | * "error" 或 2 -> 规则作为一个错误(代码不能执行,界面报错) 30 | */ 31 | rules: { 32 | // 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 33 | // 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 34 | 'prettier/prettier': ['error', { singleQuote: true }], 35 | // eslint (http://eslint.cn/docs/rules) 36 | 'no-var': 'error', // 要求使用 let 或 const 而不是 var 37 | 'no-multiple-empty-lines': ['error', { max: 1 }], // 不允许多个空行 38 | 'no-use-before-define': 'off', // 禁止在 函数/类/变量 定义之前使用它们 39 | 'prefer-const': 'off', // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const 40 | 'no-irregular-whitespace': 'off', // 禁止不规则的空白 41 | 42 | // typeScript (https://typescript-eslint.io/rules) 43 | '@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量 44 | '@typescript-eslint/no-inferrable-types': 'off', // 可以轻松推断的显式类型可能会增加不必要的冗长 45 | '@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。 46 | '@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型 47 | '@typescript-eslint/ban-ts-ignore': 'off', // 禁止使用 @ts-ignore 48 | '@typescript-eslint/ban-types': 'off', // 禁止使用特定类型 49 | '@typescript-eslint/explicit-function-return-type': 'off', // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明 50 | '@typescript-eslint/no-var-requires': 'off', // 不允许在 import 语句中使用 require 语句 51 | '@typescript-eslint/no-empty-function': 'off', // 禁止空函数 52 | '@typescript-eslint/no-use-before-define': 'off', // 禁止在变量定义之前使用它们 53 | '@typescript-eslint/ban-ts-comment': 'off', // 禁止 @ts- 使用注释或要求在指令后进行描述 54 | '@typescript-eslint/no-non-null-assertion': 'off', // 不允许使用后缀运算符的非空断言(!) 55 | '@typescript-eslint/explicit-module-boundary-types': 'off', // 要求导出函数和类的公共类方法的显式返回和参数类型 56 | 57 | // vue (https://eslint.vuejs.org/rules) 58 | 'vue/script-setup-uses-vars': 'error', // 防止 85 | 86 | 123 | -------------------------------------------------------------------------------- /src/page/index/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 109 | --------------------------------------------------------------------------------