├── .stylelintignore ├── .npmrc ├── src ├── directives │ ├── index.ts │ ├── auth │ │ └── index.ts │ └── elResizeDetector │ │ └── index.ts ├── assets │ ├── login │ │ ├── bg.jpg │ │ ├── logo.svg │ │ └── avatar.svg │ ├── iconfont │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css │ │ └── iconfont.json │ └── svg │ │ ├── dark.svg │ │ ├── full_screen.svg │ │ ├── exit_screen.svg │ │ ├── enter_outlined.svg │ │ ├── keyboard_esc.svg │ │ ├── day.svg │ │ └── back_top.svg ├── components │ ├── ReAuth │ │ ├── index.ts │ │ └── src │ │ │ └── auth.tsx │ ├── RePureTableBar │ │ ├── src │ │ │ └── svg │ │ │ │ ├── expand.svg │ │ │ │ ├── refresh.svg │ │ │ │ ├── drag.svg │ │ │ │ ├── collapse.svg │ │ │ │ └── settings.svg │ │ └── index.ts │ ├── ReEdit │ │ ├── type.ts │ │ └── index.ts │ ├── ReIcon │ │ ├── index.ts │ │ └── src │ │ │ ├── offlineIcon.ts │ │ │ ├── types.ts │ │ │ ├── iconifyIconOnline.ts │ │ │ ├── iconifyIconOffline.ts │ │ │ ├── iconfont.ts │ │ │ └── hooks.ts │ ├── ReCol │ │ └── index.ts │ ├── ReDialog │ │ ├── index.ts │ │ └── index.vue │ └── RePager │ │ └── index.vue ├── views │ ├── login │ │ └── utils │ │ │ ├── static.ts │ │ │ ├── motion.ts │ │ │ └── rule.ts │ ├── welcome │ │ └── index.vue │ ├── sys │ │ ├── user │ │ │ ├── utils │ │ │ │ ├── rules.ts │ │ │ │ └── hook.tsx │ │ │ ├── components │ │ │ │ ├── assign.vue │ │ │ │ └── edit.vue │ │ │ └── index.vue │ │ ├── role │ │ │ ├── utils │ │ │ │ ├── rules.ts │ │ │ │ └── hook.tsx │ │ │ ├── index.vue │ │ │ └── components │ │ │ │ ├── assign.vue │ │ │ │ └── edit.vue │ │ └── menu │ │ │ ├── utils │ │ │ ├── rules.ts │ │ │ └── hook.tsx │ │ │ └── index.vue │ └── error │ │ ├── 404.vue │ │ ├── 500.vue │ │ └── 403.vue ├── layout │ ├── components │ │ ├── search │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── SearchFooter.vue │ │ │ │ └── SearchResult.vue │ │ │ └── index.vue │ │ ├── sidebar │ │ │ ├── extraIcon.vue │ │ │ ├── topCollapse.vue │ │ │ ├── leftCollapse.vue │ │ │ ├── logo.vue │ │ │ ├── breadCrumb.vue │ │ │ ├── horizontal.vue │ │ │ ├── vertical.vue │ │ │ └── mixNav.vue │ │ ├── notice │ │ │ ├── noticeList.vue │ │ │ ├── index.vue │ │ │ └── data.ts │ │ ├── navbar.vue │ │ ├── panel │ │ │ └── index.vue │ │ └── appMain.vue │ ├── redirect.vue │ ├── hooks │ │ ├── useBoolean.ts │ │ ├── useLayout.ts │ │ └── useDataThemeChange.ts │ ├── frameView.vue │ ├── types.ts │ └── theme │ │ └── index.ts ├── api │ ├── routes.ts │ ├── utils.ts │ ├── user.ts │ └── system.ts ├── store │ ├── index.ts │ └── modules │ │ ├── types.ts │ │ ├── settings.ts │ │ ├── epTheme.ts │ │ ├── permission.ts │ │ ├── app.ts │ │ ├── user.ts │ │ └── multiTags.ts ├── utils │ ├── globalPolyfills.ts │ ├── progress │ │ └── index.ts │ ├── mitt.ts │ ├── time.ts │ ├── request.ts │ ├── propTypes.ts │ ├── http │ │ └── types.d.ts │ ├── responsive.ts │ ├── sso.ts │ ├── message.ts │ └── auth.ts ├── style │ ├── index.scss │ ├── tailwind.css │ ├── transition.scss │ ├── login.css │ ├── dark.scss │ ├── element-plus.scss │ └── reset.scss ├── mockProdServer.ts ├── router │ └── modules │ │ ├── home.ts │ │ ├── remaining.ts │ │ └── error.ts ├── App.vue ├── plugins │ ├── echarts │ │ └── index.ts │ └── element-plus │ │ └── index.ts ├── config │ └── index.ts └── main.ts ├── public ├── favicon.ico ├── serverConfig.json └── logo.svg ├── Caddyfile ├── .env ├── .husky ├── commit-msg ├── common.sh ├── pre-commit └── lintstagedrc.js ├── .prettierrc.js ├── .eslintignore ├── .env.development ├── .markdownlint.json ├── postcss.config.js ├── types ├── shims-vue.d.ts ├── shims-tsx.d.ts └── index.d.ts ├── .editorconfig ├── .gitignore ├── .vscode ├── vue3.2.code-snippets ├── extensions.json ├── vue3.0.code-snippets └── settings.json ├── .env.production ├── tailwind.config.js ├── .env.staging ├── Dockerfile ├── commitlint.config.js ├── mock ├── refreshToken.ts ├── login.ts └── asyncRoutes.ts ├── LICENSE ├── tsconfig.json ├── .drone.yml ├── README.en-US.md ├── README.md ├── stylelint.config.js ├── index.html ├── vite.config.ts └── .eslintrc.js /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | src/style/reset.scss -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./elResizeDetector"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anerg2046/go-admin-front/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | :80 { 2 | encode zstd gzip 3 | file_server { 4 | root /var/www/html 5 | } 6 | } -------------------------------------------------------------------------------- /src/assets/login/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anerg2046/go-admin-front/HEAD/src/assets/login/bg.jpg -------------------------------------------------------------------------------- /src/components/ReAuth/index.ts: -------------------------------------------------------------------------------- 1 | import auth from "./src/auth"; 2 | 3 | const Auth = auth; 4 | 5 | export { Auth }; 6 | -------------------------------------------------------------------------------- /src/views/login/utils/static.ts: -------------------------------------------------------------------------------- 1 | import avatar from "@/assets/login/logo.svg?component"; 2 | 3 | export { avatar }; 4 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anerg2046/go-admin-front/HEAD/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anerg2046/go-admin-front/HEAD/src/assets/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anerg2046/go-admin-front/HEAD/src/assets/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/layout/components/search/components/index.ts: -------------------------------------------------------------------------------- 1 | import SearchModal from "./SearchModal.vue"; 2 | 3 | export { SearchModal }; 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置) 5 | VITE_HIDE_HOME = false 6 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | npx --no-install commitlint --edit "$1" 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: false, 4 | arrowParens: "avoid", 5 | trailingComma: "none" 6 | }; 7 | -------------------------------------------------------------------------------- /src/views/welcome/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | dist 3 | *.d.ts 4 | /src/assets 5 | package.json 6 | .eslintrc.js 7 | .prettierrc.js 8 | commitlint.config.js 9 | postcss.config.js 10 | tailwind.config.js 11 | stylelint.config.js -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/routes.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | import { baseUrlApi, ApiResult } from "./utils"; 3 | 4 | export const getAsyncRoutes = () => { 5 | return http.request("get", baseUrlApi("/get_routers")); 6 | }; 7 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 平台本地运行端口号 2 | VITE_PORT = 8848 3 | 4 | # 开发环境读取配置文件路径 5 | VITE_PUBLIC_PATH = / 6 | 7 | # 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Pnpm 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { createPinia } from "pinia"; 3 | const store = createPinia(); 4 | 5 | export function setupStore(app: App) { 6 | app.use(store); 7 | } 8 | 9 | export { store }; 10 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": false, 4 | "MD033": false, 5 | "MD013": false, 6 | "MD001": false, 7 | "MD025": false, 8 | "MD024": false, 9 | "MD007": { "indent": 4 }, 10 | "no-hard-tabs": false 11 | } 12 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | 10 | npm run lint:pretty 11 | -------------------------------------------------------------------------------- /src/views/sys/user/utils/rules.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | 4 | /** 自定义表单规则校验 */ 5 | export const formRules = reactive({ 6 | username: [{ required: true, message: "必填项", trigger: "blur" }] 7 | }); 8 | -------------------------------------------------------------------------------- /src/assets/svg/dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/index.ts: -------------------------------------------------------------------------------- 1 | import pureTableBar from "./src/bar"; 2 | import { withInstall } from "@pureadmin/utils"; 3 | 4 | /** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */ 5 | export const PureTableBar = withInstall(pureTableBar); 6 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | 7 | declare module "*.scss" { 8 | const scss: Record; 9 | export default scss; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/globalPolyfills.ts: -------------------------------------------------------------------------------- 1 | // 如果项目出现 `global is not defined` 报错,可能是您引入某个库的问题,比如 aws-sdk-js https://github.com/aws/aws-sdk-js 2 | // 解决办法就是将该文件引入 src/main.ts 即可 import "@/utils/globalPolyfills"; 3 | if (typeof (window as any).global === "undefined") { 4 | (window as any).global = window; 5 | } 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/views/sys/role/utils/rules.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | 4 | /** 自定义表单规则校验 */ 5 | export const formRules = reactive({ 6 | code: [{ required: true, message: "必填项", trigger: "blur" }], 7 | name: [{ required: true, message: "必填项", trigger: "blur" }] 8 | }); 9 | -------------------------------------------------------------------------------- /src/api/utils.ts: -------------------------------------------------------------------------------- 1 | export interface ApiResult { 2 | code: number; 3 | msg?: string; 4 | data?: any; 5 | } 6 | 7 | export const baseUrlApi = (url: string): string => { 8 | return process.env.NODE_ENV === "development" 9 | ? `http://172.20.174.21:8020/v1${url}` 10 | : `https://admin-api.fabraze.com/v1${url}`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/assets/svg/full_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .eslintcache 7 | report.html 8 | .history 9 | 10 | yarn.lock 11 | npm-debug.log* 12 | .pnpm-error.log* 13 | .pnpm-debug.log 14 | tests/**/coverage/ 15 | 16 | # Editor directories and files 17 | .idea 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /src/assets/svg/exit_screen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/login/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/enter_outlined.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/progress/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import "nprogress/nprogress.css"; 3 | 4 | NProgress.configure({ 5 | // 动画方式 6 | easing: "ease", 7 | // 递增进度条的速度 8 | speed: 500, 9 | // 是否显示加载ico 10 | showSpinner: false, 11 | // 自动递增间隔 12 | trickleSpeed: 200, 13 | // 初始化时的最小百分比 14 | minimum: 0.3 15 | }); 16 | 17 | export default NProgress; 18 | -------------------------------------------------------------------------------- /.vscode/vue3.2.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.2+快速生成模板": { 3 | "prefix": "Vue3.2+", 4 | "body": [ 5 | "\n", 7 | "\n", 11 | "", 13 | "$2" 14 | ], 15 | "description": "Vue3.2+" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "./transition"; 2 | @import "./element-plus"; 3 | @import "./sidebar"; 4 | @import "./dark"; 5 | 6 | /* 自定义全局 CssVar */ 7 | :root { 8 | /* 左侧菜单展开、收起动画时长 */ 9 | --pure-transition-duration: 0.3s; 10 | } 11 | 12 | /* 灰色模式 */ 13 | .html-grey { 14 | filter: grayscale(100%); 15 | } 16 | 17 | /* 色弱模式 */ 18 | .html-weakness { 19 | filter: invert(80%); 20 | } 21 | -------------------------------------------------------------------------------- /.husky/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 3 | "{!(package)*.json}": ["prettier --write--parser json"], 4 | "package.json": ["prettier --write"], 5 | "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], 6 | "*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"], 7 | "*.md": ["prettier --write"] 8 | }; 9 | -------------------------------------------------------------------------------- /src/assets/svg/keyboard_esc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ReEdit/type.ts: -------------------------------------------------------------------------------- 1 | import { FormInstance } from "element-plus"; 2 | import { Ref } from "vue"; 3 | 4 | type EditProps = { 5 | defaultFormData: any; 6 | formData: any; 7 | formVisible: Ref; 8 | isAdd: Ref; 9 | ruleFormRef: Ref; 10 | title: Ref; 11 | titleExt: string; 12 | doneFn?: Function; 13 | }; 14 | 15 | export type { EditProps }; 16 | -------------------------------------------------------------------------------- /src/views/sys/menu/utils/rules.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | 4 | /** 自定义表单规则校验 */ 5 | export const formRules = reactive({ 6 | "meta.title": [{ required: true, message: "必填项", trigger: "blur" }], 7 | name: [{ required: true, message: "必填项", trigger: "blur" }], 8 | path: [{ required: true, message: "必填项", trigger: "blur" }] 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer"; 2 | 3 | const modules: Record = import.meta.glob("../mock/*.ts", { 4 | eager: true 5 | }); 6 | const mockModules = []; 7 | 8 | Object.keys(modules).forEach(key => { 9 | mockModules.push(...modules[key].default); 10 | }); 11 | 12 | export function setupProdMockServer() { 13 | createProdMockServer(mockModules); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ReIcon/index.ts: -------------------------------------------------------------------------------- 1 | import iconifyIconOffline from "./src/iconifyIconOffline"; 2 | import iconifyIconOnline from "./src/iconifyIconOnline"; 3 | import fontIcon from "./src/iconfont"; 4 | 5 | /** 本地图标组件 */ 6 | const IconifyIconOffline = iconifyIconOffline; 7 | /** 在线图标组件 */ 8 | const IconifyIconOnline = iconifyIconOnline; 9 | /** iconfont组件 */ 10 | const FontIcon = fontIcon; 11 | 12 | export { IconifyIconOffline, IconifyIconOnline, FontIcon }; 13 | -------------------------------------------------------------------------------- /src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .flex-c { 7 | @apply flex justify-center items-center; 8 | } 9 | 10 | .flex-ac { 11 | @apply flex justify-around items-center; 12 | } 13 | 14 | .flex-bc { 15 | @apply flex justify-between items-center; 16 | } 17 | 18 | .navbar-bg-hover { 19 | @apply dark:text-white dark:hover:!bg-[#242424]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/collapse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/directives/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { hasAuth } from "@/router/utils"; 2 | import { Directive, type DirectiveBinding } from "vue"; 3 | 4 | export const auth: Directive = { 5 | mounted(el: HTMLElement, binding: DirectiveBinding) { 6 | const { value } = binding; 7 | if (value) { 8 | !hasAuth(value) && el.parentNode?.removeChild(el); 9 | } else { 10 | throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\""); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/assets/svg/day.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/mitt.ts: -------------------------------------------------------------------------------- 1 | import type { Emitter } from "mitt"; 2 | import mitt from "mitt"; 3 | 4 | type Events = { 5 | resize: { 6 | detail: { 7 | width: number; 8 | height: number; 9 | }; 10 | }; 11 | openPanel: string; 12 | tagViewsChange: string; 13 | tagViewsShowModel: string; 14 | logoChange: boolean; 15 | changLayoutRoute: { 16 | indexPath: string; 17 | parentPath: string; 18 | }; 19 | }; 20 | 21 | export const emitter: Emitter = mitt(); 22 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境平台打包路径 2 | VITE_PUBLIC_PATH = / 3 | 4 | # 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 5 | VITE_ROUTER_HISTORY = "hash" 6 | 7 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 8 | VITE_CDN = true 9 | 10 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 11 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 12 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 13 | VITE_COMPRESSION = "none" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "christian-kohler.path-intellisense", 4 | "vscode-icons-team.vscode-icons", 5 | "davidanson.vscode-markdownlint", 6 | "stylelint.vscode-stylelint", 7 | "bradlc.vscode-tailwindcss", 8 | "dbaeumer.vscode-eslint", 9 | "esbenp.prettier-vscode", 10 | "redhat.vscode-yaml", 11 | "csstools.postcss", 12 | "mikestead.dotenv", 13 | "eamodio.gitlens", 14 | "antfu.iconify", 15 | "Vue.volar" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/vue3.0.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Vue3.0快速生成模板": { 3 | "prefix": "Vue3.0", 4 | "body": [ 5 | "\n", 9 | "\n", 16 | "", 18 | "$2" 19 | ], 20 | "description": "Vue3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ReAuth/src/auth.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, Fragment } from "vue"; 2 | import { hasAuth } from "@/router/utils"; 3 | 4 | export default defineComponent({ 5 | name: "Auth", 6 | props: { 7 | value: { 8 | type: undefined, 9 | default: [] 10 | } 11 | }, 12 | setup(props, { slots }) { 13 | return () => { 14 | if (!slots) return null; 15 | return hasAuth(props.value) ? ( 16 | {slots.default?.()} 17 | ) : null; 18 | }; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /src/layout/redirect.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/layout/hooks/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export function useBoolean(initValue = false) { 4 | const bool = ref(initValue); 5 | 6 | function setBool(value: boolean) { 7 | bool.value = value; 8 | } 9 | function setTrue() { 10 | setBool(true); 11 | } 12 | function setFalse() { 13 | setBool(false); 14 | } 15 | function toggle() { 16 | setBool(!bool.value); 17 | } 18 | 19 | return { 20 | bool, 21 | setBool, 22 | setTrue, 23 | setFalse, 24 | toggle 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/offlineIcon.ts: -------------------------------------------------------------------------------- 1 | import { addIcon } from "@iconify/vue/dist/offline"; 2 | 3 | /** 4 | * 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载 5 | */ 6 | 7 | // 本地菜单图标,后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标 8 | import HomeFilled from "@iconify-icons/ep/home-filled"; 9 | import InformationLine from "@iconify-icons/ri/information-line"; 10 | import Lollipop from "@iconify-icons/ep/lollipop"; 11 | 12 | addIcon("homeFilled", HomeFilled); 13 | addIcon("informationLine", InformationLine); 14 | addIcon("lollipop", Lollipop); 15 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/extraIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: "class", 4 | corePlugins: { 5 | preflight: false 6 | }, 7 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 8 | theme: { 9 | extend: { 10 | colors: { 11 | bg_color: "var(--el-bg-color)", 12 | primary: "var(--el-color-primary)", 13 | text_color_primary: "var(--el-text-color-primary)", 14 | text_color_regular: "var(--el-text-color-regular)" 15 | } 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare module "*.tsx" { 4 | import Vue from "compatible-vue"; 5 | export default Vue; 6 | } 7 | 8 | declare global { 9 | namespace JSX { 10 | interface Element extends VNode {} 11 | interface ElementClass extends Vue {} 12 | interface ElementAttributesProperty { 13 | $props: any; 14 | } 15 | interface IntrinsicElements { 16 | [elem: string]: any; 17 | } 18 | interface IntrinsicAttributes { 19 | [elem: string]: any; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface iconType { 2 | // iconify (https://docs.iconify.design/icon-components/vue/#properties) 3 | inline?: boolean; 4 | width?: string | number; 5 | height?: string | number; 6 | horizontalFlip?: boolean; 7 | verticalFlip?: boolean; 8 | flip?: string; 9 | rotate?: number | string; 10 | color?: string; 11 | horizontalAlign?: boolean; 12 | verticalAlign?: boolean; 13 | align?: string; 14 | onLoad?: Function; 15 | includes?: Function; 16 | 17 | // all icon 18 | style?: object; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import daysj from "dayjs"; 2 | 3 | /** 返回格式化的时间,如果出错返回一个默认值 */ 4 | export const TimeDefault = ( 5 | date: string, 6 | format?: string, 7 | defaultString?: string 8 | ) => { 9 | if (!defaultString) { 10 | defaultString = "/"; 11 | } 12 | if (date == undefined || date == "") { 13 | return defaultString; 14 | } 15 | if (!format) { 16 | format = "YYYY-MM-DD"; 17 | } 18 | 19 | const t = daysj(new Date(date)); 20 | if (t.year() == 1 || t.year() == 0) { 21 | return defaultString; 22 | } 23 | return t.format(format); 24 | }; 25 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | # 预发布也需要生产环境的行为 2 | # https://cn.vitejs.dev/guide/env-and-mode.html#modes 3 | NODE_ENV=production 4 | 5 | VITE_PUBLIC_PATH = / 6 | 7 | # 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | 10 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 11 | VITE_CDN = true 12 | 13 | # 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件) 14 | # 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 15 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 16 | VITE_COMPRESSION = "none" 17 | -------------------------------------------------------------------------------- /src/assets/svg/back_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/notice/noticeList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /public/serverConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "4.1.0", 3 | "Title": "GoAdmin", 4 | "FixedHeader": true, 5 | "HiddenSideBar": false, 6 | "MultiTagsCache": true, 7 | "KeepAlive": true, 8 | "Layout": "vertical", 9 | "Theme": "light", 10 | "DarkMode": false, 11 | "Grey": false, 12 | "Weak": false, 13 | "HideTabs": false, 14 | "SidebarStatus": true, 15 | "EpThemeColor": "#409EFF", 16 | "ShowLogo": true, 17 | "ShowModel": "smart", 18 | "MenuArrowIconNoTransition": true, 19 | "CachingAsyncRoutes": false, 20 | "TooltipEffect": "light", 21 | "ResponsiveStorageNameSpace": "responsive-" 22 | } 23 | -------------------------------------------------------------------------------- /src/layout/components/search/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | const { VITE_HIDE_HOME } = import.meta.env; 2 | const Layout = () => import("@/layout/index.vue"); 3 | 4 | export default { 5 | path: "/", 6 | name: "Home", 7 | component: Layout, 8 | redirect: "/welcome", 9 | meta: { 10 | icon: "homeFilled", 11 | title: "首页", 12 | rank: 0 13 | }, 14 | children: [ 15 | { 16 | path: "/welcome", 17 | name: "Welcome", 18 | component: () => import("@/views/welcome/index.vue"), 19 | meta: { 20 | title: "首页", 21 | showLink: VITE_HIDE_HOME === "true" ? false : true 22 | } 23 | } 24 | ] 25 | } as RouteConfigsTable; 26 | -------------------------------------------------------------------------------- /src/components/ReCol/index.ts: -------------------------------------------------------------------------------- 1 | import { ElCol } from "element-plus"; 2 | import { h, defineComponent } from "vue"; 3 | 4 | // 封装element-plus的el-col组件 5 | export default defineComponent({ 6 | name: "ReCol", 7 | props: { 8 | value: { 9 | type: Number, 10 | default: 24 11 | } 12 | }, 13 | render() { 14 | const attrs = this.$attrs; 15 | const val = this.value; 16 | return h( 17 | ElCol, 18 | { 19 | xs: val, 20 | sm: val, 21 | md: val, 22 | lg: val, 23 | xl: val, 24 | ...attrs 25 | }, 26 | { default: () => this.$slots.default() } 27 | ); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | RUN set -xe \ 7 | && node -v \ 8 | && npm install -g pnpm \ 9 | && pnpm install \ 10 | && pnpm build 11 | 12 | # 运行环境 13 | FROM caddy:2-alpine 14 | 15 | ENV TZ Asia/Shanghai 16 | 17 | RUN set -xe \ 18 | && apk -U upgrade \ 19 | && apk add --update --no-cache --virtual .build-deps \ 20 | tzdata \ 21 | && cp /usr/share/zoneinfo/${TZ} /etc/localtime \ 22 | # 删除不必要的东西 23 | && apk del .build-deps \ 24 | && rm -rf /tmp/* 25 | 26 | COPY --from=builder /app/dist /var/www/html 27 | 28 | ADD ./Caddyfile /etc/caddy/Caddyfile 29 | 30 | RUN /usr/bin/caddy fmt --overwrite /etc/caddy/Caddyfile 31 | 32 | EXPOSE 80 -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2208059 */ 3 | src: url("iconfont.woff2?t=1671895108120") format("woff2"), 4 | url("iconfont.woff?t=1671895108120") format("woff"), 5 | url("iconfont.ttf?t=1671895108120") format("truetype"); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .pure-iconfont-tabs:before { 17 | content: "\e63e"; 18 | } 19 | 20 | .pure-iconfont-logo:before { 21 | content: "\e620"; 22 | } 23 | 24 | .pure-iconfont-new:before { 25 | content: "\e615"; 26 | } 27 | -------------------------------------------------------------------------------- /src/router/modules/remaining.ts: -------------------------------------------------------------------------------- 1 | const Layout = () => import("@/layout/index.vue"); 2 | 3 | export default [ 4 | { 5 | path: "/login", 6 | name: "Login", 7 | component: () => import("@/views/login/index.vue"), 8 | meta: { 9 | title: "登录", 10 | showLink: false, 11 | rank: 101 12 | } 13 | }, 14 | { 15 | path: "/redirect", 16 | component: Layout, 17 | meta: { 18 | title: "加载中...", 19 | showLink: false, 20 | rank: 102 21 | }, 22 | children: [ 23 | { 24 | path: "/redirect/:path(.*)", 25 | name: "Redirect", 26 | component: () => import("@/layout/redirect.vue") 27 | } 28 | ] 29 | } 30 | ] as Array; 31 | -------------------------------------------------------------------------------- /src/assets/login/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2208059", 3 | "name": "pure-admin", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "pure-iconfont-", 6 | "description": "pure-admin-iconfont", 7 | "glyphs": [ 8 | { 9 | "icon_id": "20594647", 10 | "name": "Tabs", 11 | "font_class": "tabs", 12 | "unicode": "e63e", 13 | "unicode_decimal": 58942 14 | }, 15 | { 16 | "icon_id": "22129506", 17 | "name": "PureLogo", 18 | "font_class": "logo", 19 | "unicode": "e620", 20 | "unicode_decimal": 58912 21 | }, 22 | { 23 | "icon_id": "7795615", 24 | "name": "New", 25 | "font_class": "new", 26 | "unicode": "e615", 27 | "unicode_decimal": 58901 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { ApiResult } from "@/api/utils"; 2 | import { AxiosError } from "axios"; 3 | import { message } from "./message"; 4 | 5 | export const requestHook = async (fn: Promise) => { 6 | try { 7 | const { code, msg, data } = await fn; 8 | if (code !== 0) { 9 | message(msg, { type: "error" }); 10 | return; 11 | } 12 | return { code: code, msg: msg, data: data }; 13 | } catch (e) { 14 | if ((e as AxiosError)?.response?.status !== 200) { 15 | const msg = e.response?.data?.msg ?? e; 16 | message(msg, { type: "error" }); 17 | } else { 18 | message(e, { type: "error" }); 19 | } 20 | return { 21 | code: -1, 22 | msg: "error", 23 | data: null 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconifyIconOnline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon } from "@iconify/vue"; 3 | 4 | // Iconify Icon在Vue里在线使用(用于外网环境) 5 | export default defineComponent({ 6 | name: "IconifyIconOnline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | type: String, 11 | default: "" 12 | } 13 | }, 14 | render() { 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: `${this.icon}`, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [commit => commit.includes("init")], 3 | extends: ["@commitlint/config-conventional"], 4 | rules: { 5 | "body-leading-blank": [2, "always"], 6 | "footer-leading-blank": [1, "always"], 7 | "header-max-length": [2, "always", 108], 8 | "subject-empty": [2, "never"], 9 | "type-empty": [2, "never"], 10 | "type-enum": [ 11 | 2, 12 | "always", 13 | [ 14 | "feat", 15 | "fix", 16 | "perf", 17 | "style", 18 | "docs", 19 | "test", 20 | "refactor", 21 | "build", 22 | "ci", 23 | "chore", 24 | "revert", 25 | "wip", 26 | "workflow", 27 | "types", 28 | "release" 29 | ] 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /mock/refreshToken.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock"; 2 | 3 | // 模拟刷新token接口 4 | export default [ 5 | { 6 | url: "/refreshToken", 7 | method: "post", 8 | response: ({ body }) => { 9 | if (body.refreshToken) { 10 | return { 11 | success: true, 12 | data: { 13 | accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin", 14 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh", 15 | // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。 16 | expires: "2023/10/30 23:59:59" 17 | } 18 | }; 19 | } else { 20 | return { 21 | success: false, 22 | data: {} 23 | }; 24 | } 25 | } 26 | } 27 | ] as MockMethod[]; 28 | -------------------------------------------------------------------------------- /src/router/modules/error.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | path: "/error", 3 | redirect: "/error/403", 4 | meta: { 5 | icon: "informationLine", 6 | title: "异常页面", 7 | showLink: false, 8 | rank: 9 9 | }, 10 | children: [ 11 | { 12 | path: "/error/403", 13 | name: "403", 14 | component: () => import("@/views/error/403.vue"), 15 | meta: { 16 | title: "403" 17 | } 18 | }, 19 | { 20 | path: "/error/404", 21 | name: "404", 22 | component: () => import("@/views/error/404.vue"), 23 | meta: { 24 | title: "404" 25 | } 26 | }, 27 | { 28 | path: "/error/500", 29 | name: "500", 30 | component: () => import("@/views/error/500.vue"), 31 | meta: { 32 | title: "500" 33 | } 34 | } 35 | ] 36 | } as RouteConfigsTable; 37 | -------------------------------------------------------------------------------- /src/layout/components/sidebar/topCollapse.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/store/modules/types.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordName } from "vue-router"; 2 | 3 | export type cacheType = { 4 | mode: string; 5 | name?: RouteRecordName; 6 | }; 7 | 8 | export type positionType = { 9 | startIndex?: number; 10 | length?: number; 11 | }; 12 | 13 | export type appType = { 14 | sidebar: { 15 | opened: boolean; 16 | withoutAnimation: boolean; 17 | // 判断是否手动点击Collapse 18 | isClickCollapse: boolean; 19 | }; 20 | layout: string; 21 | device: string; 22 | }; 23 | 24 | export type multiType = { 25 | path: string; 26 | parentPath: string; 27 | name: string; 28 | meta: any; 29 | query?: object; 30 | params?: object; 31 | }; 32 | 33 | export type setType = { 34 | title: string; 35 | fixedHeader: boolean; 36 | hiddenSideBar: boolean; 37 | }; 38 | 39 | export type userType = { 40 | username?: string; 41 | roles?: Array; 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconifyIconOffline.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; 3 | 4 | // Iconify Icon在Vue里本地使用(用于内网环境)https://docs.iconify.design/icon-components/vue/offline.html 5 | export default defineComponent({ 6 | name: "IconifyIconOffline", 7 | components: { IconifyIcon }, 8 | props: { 9 | icon: { 10 | default: null 11 | } 12 | }, 13 | render() { 14 | if (typeof this.icon === "object") addIcon(this.icon, this.icon); 15 | const attrs = this.$attrs; 16 | return h( 17 | IconifyIcon, 18 | { 19 | icon: this.icon, 20 | style: attrs?.style 21 | ? Object.assign(attrs.style, { outline: "none" }) 22 | : { outline: "none" }, 23 | ...attrs 24 | }, 25 | { 26 | default: () => [] 27 | } 28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/directives/elResizeDetector/index.ts: -------------------------------------------------------------------------------- 1 | import { Directive, type DirectiveBinding, type VNode } from "vue"; 2 | import elementResizeDetectorMaker from "element-resize-detector"; 3 | import type { Erd } from "element-resize-detector"; 4 | import { emitter } from "@/utils/mitt"; 5 | 6 | const erd: Erd = elementResizeDetectorMaker({ 7 | strategy: "scroll" 8 | }); 9 | 10 | export const resize: Directive = { 11 | mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) { 12 | erd.listenTo(el, elem => { 13 | const width = elem.offsetWidth; 14 | const height = elem.offsetHeight; 15 | if (binding?.instance) { 16 | emitter.emit("resize", { detail: { width, height } }); 17 | } else { 18 | vnode.el.dispatchEvent( 19 | new CustomEvent("resize", { detail: { width, height } }) 20 | ); 21 | } 22 | }); 23 | }, 24 | unmounted(el: HTMLElement) { 25 | erd.uninstall(el); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/views/login/utils/motion.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent, withDirectives, resolveDirective } from "vue"; 2 | 3 | /** 封装@vueuse/motion动画库中的自定义指令v-motion */ 4 | export default defineComponent({ 5 | name: "Motion", 6 | props: { 7 | delay: { 8 | type: Number, 9 | default: 50 10 | } 11 | }, 12 | render() { 13 | const { delay } = this; 14 | const motion = resolveDirective("motion"); 15 | return withDirectives( 16 | h( 17 | "div", 18 | {}, 19 | { 20 | default: () => [this.$slots.default()] 21 | } 22 | ), 23 | [ 24 | [ 25 | motion, 26 | { 27 | initial: { opacity: 0, y: 100 }, 28 | enter: { 29 | opacity: 1, 30 | y: 0, 31 | transition: { 32 | delay 33 | } 34 | } 35 | } 36 | ] 37 | ] 38 | ); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": true, 3 | "editor.formatOnSave": true, 4 | "[vue]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "editor.tabSize": 2, 8 | "editor.formatOnPaste": true, 9 | "editor.guides.bracketPairs": "active", 10 | "files.autoSave": "afterDelay", 11 | "git.confirmSync": false, 12 | "workbench.startupEditor": "newUntitledFile", 13 | "editor.suggestSelection": "first", 14 | "editor.acceptSuggestionOnCommitCharacter": false, 15 | "css.lint.propertyIgnoredDueToDisplay": "ignore", 16 | "editor.quickSuggestions": { 17 | "other": true, 18 | "comments": true, 19 | "strings": true 20 | }, 21 | "files.associations": { 22 | "editor.snippetSuggestions": "top" 23 | }, 24 | "[css]": { 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "editor.codeActionsOnSave": { 28 | "source.fixAll.eslint": true 29 | }, 30 | "iconify.excludes": ["el"] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/RePureTableBar/src/svg/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/modules/settings.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { store } from "@/store"; 3 | import { setType } from "./types"; 4 | import { getConfig } from "@/config"; 5 | 6 | export const useSettingStore = defineStore({ 7 | id: "pure-setting", 8 | state: (): setType => ({ 9 | title: getConfig().Title, 10 | fixedHeader: getConfig().FixedHeader, 11 | hiddenSideBar: getConfig().HiddenSideBar 12 | }), 13 | getters: { 14 | getTitle(state) { 15 | return state.title; 16 | }, 17 | getFixedHeader(state) { 18 | return state.fixedHeader; 19 | }, 20 | getHiddenSideBar(state) { 21 | return state.hiddenSideBar; 22 | } 23 | }, 24 | actions: { 25 | CHANGE_SETTING({ key, value }) { 26 | if (Reflect.has(this, key)) { 27 | this[key] = value; 28 | } 29 | }, 30 | changeSetting(data) { 31 | this.CHANGE_SETTING(data); 32 | } 33 | } 34 | }); 35 | 36 | export function useSettingStoreHook() { 37 | return useSettingStore(store); 38 | } 39 | -------------------------------------------------------------------------------- /mock/login.ts: -------------------------------------------------------------------------------- 1 | // 根据角色动态生成路由 2 | import { MockMethod } from "vite-plugin-mock"; 3 | 4 | export default [ 5 | { 6 | url: "/login", 7 | method: "post", 8 | response: ({ body }) => { 9 | if (body.username === "admin") { 10 | return { 11 | success: true, 12 | data: { 13 | username: "admin", 14 | // 一个用户可能有多个角色 15 | roles: ["admin"], 16 | accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", 17 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", 18 | expires: "2023/10/30 00:00:00" 19 | } 20 | }; 21 | } else { 22 | return { 23 | success: true, 24 | data: { 25 | username: "common", 26 | // 一个用户可能有多个角色 27 | roles: ["common"], 28 | accessToken: "eyJhbGciOiJIUzUxMiJ9.common", 29 | refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", 30 | expires: "2023/10/30 00:00:00" 31 | } 32 | }; 33 | } 34 | } 35 | } 36 | ] as MockMethod[]; 37 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 39 | -------------------------------------------------------------------------------- /mock/asyncRoutes.ts: -------------------------------------------------------------------------------- 1 | // 模拟后端动态生成路由 2 | import { MockMethod } from "vite-plugin-mock"; 3 | 4 | /** 5 | * roles:页面级别权限,这里模拟二种 "admin"、"common" 6 | * admin:管理员角色 7 | * common:普通角色 8 | */ 9 | 10 | const permissionRouter = { 11 | path: "/permission", 12 | meta: { 13 | title: "权限管理", 14 | icon: "lollipop", 15 | rank: 10 16 | }, 17 | children: [ 18 | { 19 | path: "/permission/page/index", 20 | name: "PermissionPage", 21 | meta: { 22 | title: "页面权限", 23 | roles: ["admin", "common"] 24 | } 25 | }, 26 | { 27 | path: "/permission/button/index", 28 | name: "PermissionButton", 29 | meta: { 30 | title: "按钮权限", 31 | roles: ["admin", "common"], 32 | auths: ["btn_add", "btn_edit", "btn_delete"] 33 | } 34 | } 35 | ] 36 | }; 37 | 38 | export default [ 39 | { 40 | url: "/getAsyncRoutes", 41 | method: "get", 42 | response: () => { 43 | return { 44 | success: true, 45 | data: [permissionRouter] 46 | }; 47 | } 48 | } 49 | ] as MockMethod[]; 50 | -------------------------------------------------------------------------------- /src/views/login/utils/rule.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { FormRules } from "element-plus"; 3 | 4 | /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ 5 | export const REGEXP_PWD = 6 | /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; 7 | 8 | /** 登录校验 */ 9 | const loginRules = reactive({ 10 | password: [ 11 | { 12 | validator: (rule, value, callback) => { 13 | if (value === "") { 14 | callback(new Error("请输入密码")); 15 | } else if (!REGEXP_PWD.test(value)) { 16 | callback( 17 | new Error("密码格式应为8-18位数字、字母、符号的任意两种组合") 18 | ); 19 | } else { 20 | callback(); 21 | } 22 | }, 23 | trigger: "blur" 24 | } 25 | ], 26 | turnstileToken: [ 27 | { 28 | validator: (rule, value, callback) => { 29 | if (value === "") { 30 | callback(new Error("请进行人机验证")); 31 | } else { 32 | callback(); 33 | } 34 | } 35 | } 36 | ] 37 | }); 38 | 39 | export { loginRules }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present, pure-admin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/propTypes.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, VNodeChild } from "vue"; 2 | import { 3 | createTypes, 4 | toValidableType, 5 | VueTypesInterface, 6 | VueTypeValidableDef 7 | } from "vue-types"; 8 | 9 | export type VueNode = VNodeChild | JSX.Element; 10 | 11 | type PropTypes = VueTypesInterface & { 12 | readonly style: VueTypeValidableDef; 13 | readonly VNodeChild: VueTypeValidableDef; 14 | }; 15 | 16 | const newPropTypes = createTypes({ 17 | func: undefined, 18 | bool: undefined, 19 | string: undefined, 20 | number: undefined, 21 | object: undefined, 22 | integer: undefined 23 | }) as PropTypes; 24 | 25 | // 从 vue-types v5.0 开始,extend()方法已经废弃,当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method 26 | export default class propTypes extends newPropTypes { 27 | // a native-like validator that supports the `.validable` method 28 | static get style() { 29 | return toValidableType("style", { 30 | type: [String, Object] 31 | }); 32 | } 33 | 34 | static get VNodeChild() { 35 | return toValidableType("VNodeChild", { 36 | type: undefined 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/utils/http"; 2 | import { baseUrlApi } from "./utils"; 3 | 4 | export type LoginResult = { 5 | code: number; 6 | msg: string; 7 | data: { 8 | /** 用户名 */ 9 | username: string; 10 | /** 当前登陆用户的角色 */ 11 | roles: Array; 12 | /** `token` */ 13 | accessToken: string; 14 | /** 用于调用刷新`accessToken`的接口时所需的`token` */ 15 | refreshToken: string; 16 | /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ 17 | expires: Date; 18 | }; 19 | }; 20 | 21 | export type RefreshTokenResult = { 22 | code: number; 23 | msg: string; 24 | data: { 25 | /** `token` */ 26 | accessToken: string; 27 | /** 用于调用刷新`accessToken`的接口时所需的`token` */ 28 | refreshToken: string; 29 | /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ 30 | expires: Date; 31 | }; 32 | }; 33 | 34 | /** 登录 */ 35 | export const getLogin = (data?: object) => { 36 | return http.request("post", baseUrlApi("/login"), { data }); 37 | }; 38 | 39 | /** 刷新token */ 40 | export const refreshTokenApi = (data?: object) => { 41 | return http.request("post", baseUrlApi("/refreshToken"), { 42 | data 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/style/transition.scss: -------------------------------------------------------------------------------- 1 | /* fade */ 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.28s; 5 | } 6 | 7 | .fade-enter, 8 | .fade-leave-active { 9 | opacity: 0; 10 | } 11 | 12 | /* fade-transform */ 13 | .fade-transform-leave-active, 14 | .fade-transform-enter-active { 15 | transition: all 0.5s; 16 | } 17 | 18 | .fade-transform-enter-from { 19 | opacity: 0; 20 | transform: translateX(-30px); 21 | } 22 | 23 | .fade-transform-leave-to { 24 | opacity: 0; 25 | transform: translateX(30px); 26 | } 27 | 28 | /* breadcrumb transition */ 29 | .breadcrumb-enter-active { 30 | transition: all 0.4s; 31 | } 32 | 33 | .breadcrumb-leave-active { 34 | position: absolute; 35 | transition: all 0.3s; 36 | } 37 | 38 | .breadcrumb-enter-from, 39 | .breadcrumb-leave-active { 40 | opacity: 0; 41 | transform: translateX(20px); 42 | } 43 | 44 | /** 45 | * @description 重置el-menu的展开收起动画时长 46 | */ 47 | .outer-most .el-collapse-transition-leave-active, 48 | .outer-most .el-collapse-transition-enter-active { 49 | transition: 0.2s all ease-in-out !important; 50 | } 51 | 52 | .horizontal-collapse-transition { 53 | transition: var(--pure-transition-duration) all !important; 54 | } 55 | -------------------------------------------------------------------------------- /src/components/ReDialog/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import reDialog from "./index.vue"; 3 | import { useTimeoutFn } from "@vueuse/core"; 4 | import { withInstall } from "@pureadmin/utils"; 5 | import type { 6 | EventType, 7 | ArgsType, 8 | DialogProps, 9 | ButtonProps, 10 | DialogOptions 11 | } from "./type"; 12 | 13 | const dialogStore = ref>([]); 14 | 15 | const addDialog = (options: DialogOptions) => { 16 | const open = () => 17 | dialogStore.value.push(Object.assign(options, { visible: true })); 18 | if (options?.openDelay) { 19 | useTimeoutFn(() => { 20 | open(); 21 | }, options.openDelay); 22 | } else { 23 | open(); 24 | } 25 | }; 26 | 27 | const closeDialog = (options: DialogOptions, index: number, args?: any) => { 28 | dialogStore.value.splice(index, 1); 29 | options.closeCallBack && options.closeCallBack({ options, index, args }); 30 | }; 31 | 32 | const closeAllDialog = () => { 33 | dialogStore.value = []; 34 | }; 35 | 36 | const ReDialog = withInstall(reDialog); 37 | 38 | export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions }; 39 | export { ReDialog, dialogStore, addDialog, closeDialog, closeAllDialog }; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "strict": false, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "experimentalDecorators": true, 10 | "strictFunctionTypes": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "isolatedModules": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "allowJs": false, 19 | "resolveJsonModule": true, 20 | "lib": ["dom", "esnext"], 21 | "paths": { 22 | "@/*": ["src/*"], 23 | "@build/*": ["build/*"] 24 | }, 25 | "types": [ 26 | "node", 27 | "vite/client", 28 | "element-plus/global", 29 | "@pureadmin/table/volar", 30 | "@pureadmin/descriptions/volar" 31 | ], 32 | "typeRoots": ["./types", "./node_modules/@types/"] 33 | }, 34 | "include": [ 35 | "mock/*.ts", 36 | "src/**/*.ts", 37 | "src/**/*.tsx", 38 | "src/**/*.vue", 39 | "types/*.d.ts", 40 | "vite.config.ts" 41 | ], 42 | "exclude": ["dist", "**/*.js", "node_modules"] 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ReEdit/index.ts: -------------------------------------------------------------------------------- 1 | import { clone } from "@pureadmin/utils"; 2 | import { FormInstance } from "element-plus"; 3 | import { EditProps } from "./type"; 4 | import deepmerge from "deepmerge"; 5 | 6 | export const ReEdit = (props: EditProps) => { 7 | const showEdit = (row: any) => { 8 | props.formVisible.value = true; 9 | if (row?.id) { 10 | props.title.value = "编辑" + props.titleExt; 11 | props.formData.value = clone(row, true); 12 | props.isAdd.value = false; 13 | } else { 14 | props.formData.value = clone(props.defaultFormData.value, true); 15 | if (row) { 16 | props.formData.value = deepmerge(props.defaultFormData.value, row); 17 | } 18 | props.title.value = "添加" + props.titleExt; 19 | props.isAdd.value = true; 20 | } 21 | }; 22 | 23 | const resetForm = (formEl: FormInstance | undefined) => { 24 | if (!formEl) return; 25 | formEl.resetFields(); 26 | }; 27 | 28 | const closeDialog = () => { 29 | props.formVisible.value = false; 30 | resetForm(props.ruleFormRef.value); 31 | if (props.doneFn) { 32 | props.doneFn(); 33 | } 34 | }; 35 | 36 | return { 37 | showEdit, 38 | closeDialog 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/RePager/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /src/utils/http/types.d.ts: -------------------------------------------------------------------------------- 1 | import Axios, { 2 | Method, 3 | AxiosError, 4 | AxiosResponse, 5 | AxiosRequestConfig 6 | } from "axios"; 7 | 8 | export type resultType = { 9 | accessToken?: string; 10 | }; 11 | 12 | export type RequestMethods = Extract< 13 | Method, 14 | "get" | "post" | "put" | "delete" | "patch" | "option" | "head" 15 | >; 16 | 17 | export interface PureHttpError extends AxiosError { 18 | isCancelRequest?: boolean; 19 | } 20 | 21 | export interface PureHttpResponse extends AxiosResponse { 22 | config: PureHttpRequestConfig; 23 | } 24 | 25 | export interface PureHttpRequestConfig extends AxiosRequestConfig { 26 | beforeRequestCallback?: (request: PureHttpRequestConfig) => void; 27 | beforeResponseCallback?: (response: PureHttpResponse) => void; 28 | } 29 | 30 | export default class PureHttp { 31 | request( 32 | method: RequestMethods, 33 | url: string, 34 | param?: AxiosRequestConfig, 35 | axiosConfig?: PureHttpRequestConfig 36 | ): Promise; 37 | post( 38 | url: string, 39 | params?: T, 40 | config?: PureHttpRequestConfig 41 | ): Promise

; 42 | get( 43 | url: string, 44 | params?: T, 45 | config?: PureHttpRequestConfig 46 | ): Promise

; 47 | } 48 | -------------------------------------------------------------------------------- /src/plugins/echarts/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import * as echarts from "echarts/core"; 3 | import { CanvasRenderer } from "echarts/renderers"; 4 | import { PieChart, BarChart, LineChart } from "echarts/charts"; 5 | import { 6 | GridComponent, 7 | TitleComponent, 8 | LegendComponent, 9 | GraphicComponent, 10 | ToolboxComponent, 11 | TooltipComponent, 12 | DataZoomComponent, 13 | VisualMapComponent 14 | } from "echarts/components"; 15 | 16 | const { use } = echarts; 17 | 18 | use([ 19 | PieChart, 20 | BarChart, 21 | LineChart, 22 | CanvasRenderer, 23 | GridComponent, 24 | TitleComponent, 25 | LegendComponent, 26 | GraphicComponent, 27 | ToolboxComponent, 28 | TooltipComponent, 29 | DataZoomComponent, 30 | VisualMapComponent 31 | ]); 32 | 33 | /** 34 | * @description 按需引入echarts 35 | * @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6} 36 | * @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,为了配合 https://pure-admin-utils.netlify.app/hooks/useEcharts/useEcharts.html 使用 37 | */ 38 | export function useEcharts(app: App) { 39 | app.config.globalProperties.$echarts = echarts; 40 | } 41 | 42 | export default echarts; 43 | -------------------------------------------------------------------------------- /src/components/ReIcon/src/iconfont.ts: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from "vue"; 2 | 3 | // 封装iconfont组件,默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 (https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code) 4 | export default defineComponent({ 5 | name: "FontIcon", 6 | props: { 7 | icon: { 8 | type: String, 9 | default: "" 10 | } 11 | }, 12 | render() { 13 | const attrs = this.$attrs; 14 | if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") { 15 | return h( 16 | "i", 17 | { 18 | class: "iconfont", 19 | ...attrs 20 | }, 21 | this.icon 22 | ); 23 | } else if ( 24 | Object.keys(attrs).includes("svg") || 25 | attrs?.iconType === "svg" 26 | ) { 27 | return h( 28 | "svg", 29 | { 30 | class: "icon-svg", 31 | "aria-hidden": true 32 | }, 33 | { 34 | default: () => [ 35 | h("use", { 36 | "xlink:href": `#${this.icon}` 37 | }) 38 | ] 39 | } 40 | ); 41 | } else { 42 | return h("i", { 43 | class: `iconfont ${this.icon}`, 44 | ...attrs 45 | }); 46 | } 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/layout/components/search/components/SearchFooter.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /src/utils/responsive.ts: -------------------------------------------------------------------------------- 1 | // 响应式storage 2 | import { App } from "vue"; 3 | import Storage from "responsive-storage"; 4 | import { routerArrays } from "@/layout/types"; 5 | import { responsiveStorageNameSpace } from "@/config"; 6 | 7 | export const injectResponsiveStorage = (app: App, config: ServerConfigs) => { 8 | const nameSpace = responsiveStorageNameSpace(); 9 | const configObj = Object.assign( 10 | { 11 | // layout模式以及主题 12 | layout: Storage.getData("layout", nameSpace) ?? { 13 | layout: config.Layout ?? "vertical", 14 | theme: config.Theme ?? "default", 15 | darkMode: config.DarkMode ?? false, 16 | sidebarStatus: config.SidebarStatus ?? true, 17 | epThemeColor: config.EpThemeColor ?? "#409EFF" 18 | }, 19 | configure: Storage.getData("configure", nameSpace) ?? { 20 | grey: config.Grey ?? false, 21 | weak: config.Weak ?? false, 22 | hideTabs: config.HideTabs ?? false, 23 | showLogo: config.ShowLogo ?? true, 24 | showModel: config.ShowModel ?? "smart", 25 | multiTagsCache: config.MultiTagsCache ?? false 26 | } 27 | }, 28 | config.MultiTagsCache 29 | ? { 30 | // 默认显示顶级菜单tag 31 | tags: Storage.getData("tags", nameSpace) ?? routerArrays 32 | } 33 | : {} 34 | ); 35 | 36 | app.use(Storage, { nameSpace, memory: configObj }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/views/sys/menu/utils/hook.tsx: -------------------------------------------------------------------------------- 1 | import { listMenu, delMenu } from "@/api/system"; 2 | import { ref, onMounted } from "vue"; 3 | import { message } from "@/utils/message"; 4 | import { requestHook } from "@/utils/request"; 5 | 6 | export function useSysMenuManagement(editRef: any) { 7 | const dataList = ref([]); 8 | const loading = ref(true); 9 | 10 | async function fetchData() { 11 | loading.value = true; 12 | const { data } = await requestHook(listMenu()); 13 | dataList.value = data; 14 | loading.value = false; 15 | } 16 | 17 | function handleEdit(row: any) { 18 | editRef.value.fetchData(); 19 | if (row.id) { 20 | editRef.value.showEdit(row); 21 | } else { 22 | editRef.value.showEdit(); 23 | } 24 | } 25 | 26 | function handleEditChild(row: any) { 27 | if (row.id) { 28 | editRef.value.fetchData(); 29 | editRef.value.showEditWithParent(row.id); 30 | } 31 | } 32 | 33 | async function handleDelete(row: any) { 34 | const { code } = await requestHook(delMenu({ id: row.id })); 35 | if (code === 0) { 36 | message("删除成功", { type: "success" }); 37 | fetchData(); 38 | } 39 | } 40 | 41 | onMounted(() => { 42 | fetchData(); 43 | }); 44 | 45 | return { 46 | dataList, 47 | loading, 48 | handleEdit, 49 | handleEditChild, 50 | handleDelete, 51 | fetchData 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/views/sys/user/utils/hook.tsx: -------------------------------------------------------------------------------- 1 | import { delUser, listUser } from "@/api/system"; 2 | import { message } from "@/utils/message"; 3 | import { requestHook } from "@/utils/request"; 4 | import { onMounted, ref } from "vue"; 5 | 6 | export const useSysUserManagement = (props: any) => { 7 | const dataList = ref([]); 8 | const dataTotal = ref(0); 9 | const loading = ref(true); 10 | 11 | const handleEdit = row => { 12 | if (row.id) { 13 | props.editRef.value.showEdit(row); 14 | } else { 15 | props.editRef.value.showEdit(); 16 | } 17 | }; 18 | 19 | const handleAssign = row => { 20 | if (row.id) { 21 | props.assignRef.value.showAssign(row); 22 | } 23 | }; 24 | 25 | async function handleDelete(row: any) { 26 | const { code } = await requestHook(delUser({ id: row.id })); 27 | if (code === 0) { 28 | message("删除成功", { type: "success" }); 29 | fetchData(); 30 | } 31 | } 32 | 33 | const fetchData = async () => { 34 | loading.value = true; 35 | const { data } = await requestHook(listUser(props.queryForm)); 36 | dataList.value = data?.list; 37 | dataTotal.value = data?.total; 38 | loading.value = false; 39 | }; 40 | 41 | onMounted(() => { 42 | fetchData(); 43 | }); 44 | 45 | return { 46 | dataList, 47 | dataTotal, 48 | loading, 49 | handleAssign, 50 | handleEdit, 51 | handleDelete, 52 | fetchData 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/style/login.css: -------------------------------------------------------------------------------- 1 | .wave { 2 | position: fixed; 3 | height: 100%; 4 | left: 0; 5 | bottom: 0; 6 | z-index: -1; 7 | } 8 | 9 | .login-container { 10 | width: 100vw; 11 | height: 100vh; 12 | display: grid; 13 | grid-template-columns: repeat(2, 1fr); 14 | grid-gap: 18rem; 15 | padding: 0 2rem; 16 | } 17 | 18 | .img { 19 | display: flex; 20 | justify-content: flex-end; 21 | align-items: center; 22 | } 23 | 24 | .img img { 25 | width: 500px; 26 | } 27 | 28 | .login-box { 29 | display: flex; 30 | align-items: center; 31 | text-align: center; 32 | } 33 | 34 | .login-form { 35 | width: 360px; 36 | } 37 | 38 | .avatar { 39 | width: 350px; 40 | height: 80px; 41 | } 42 | 43 | .login-form h2 { 44 | text-transform: uppercase; 45 | margin: 15px 0; 46 | color: #999; 47 | font: bold 200% Consolas, Monaco, monospace; 48 | } 49 | 50 | @media screen and (max-width: 1180px) { 51 | .login-container { 52 | grid-gap: 9rem; 53 | } 54 | 55 | .login-form { 56 | width: 290px; 57 | } 58 | 59 | .login-form h2 { 60 | font-size: 2.4rem; 61 | margin: 8px 0; 62 | } 63 | 64 | .img img { 65 | width: 360px; 66 | } 67 | 68 | .avatar { 69 | width: 280px; 70 | height: 80px; 71 | } 72 | } 73 | 74 | @media screen and (max-width: 968px) { 75 | .wave { 76 | display: none; 77 | } 78 | 79 | .img { 80 | display: none; 81 | } 82 | 83 | .login-container { 84 | grid-template-columns: 1fr; 85 | } 86 | 87 | .login-box { 88 | justify-content: center; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/views/error/404.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/views/error/500.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/views/error/403.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 71 | -------------------------------------------------------------------------------- /src/utils/sso.ts: -------------------------------------------------------------------------------- 1 | import { removeToken, setToken, type DataInfo } from "./auth"; 2 | import { subBefore, getQueryMap } from "@pureadmin/utils"; 3 | 4 | /** 5 | * 简版前端单点登录,根据实际业务自行编写 6 | * 划重点: 7 | * 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理 8 | * 1.清空本地旧信息; 9 | * 2.获取url中的重要参数信息,然后通过 setToken 保存在本地; 10 | * 3.删除不需要显示在 url 的参数 11 | * 4.使用 window.location.replace 跳转正确页面 12 | */ 13 | (function () { 14 | // 获取 url 中的参数 15 | const params = getQueryMap(location.href) as DataInfo; 16 | const must = ["username", "roles", "accessToken"]; 17 | const mustLength = must.length; 18 | if (Object.keys(params).length !== mustLength) return; 19 | 20 | // url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环 21 | let sso = []; 22 | let start = 0; 23 | 24 | while (start < mustLength) { 25 | if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) { 26 | sso.push(must[start]); 27 | } else { 28 | sso = []; 29 | } 30 | start++; 31 | } 32 | 33 | if (sso.length === mustLength) { 34 | // 判定为单点登录 35 | 36 | // 清空本地旧信息 37 | removeToken(); 38 | 39 | // 保存新信息到本地 40 | setToken(params); 41 | 42 | // 删除不需要显示在 url 的参数 43 | delete params["roles"]; 44 | delete params["accessToken"]; 45 | 46 | const newUrl = `${location.origin}${location.pathname}${subBefore( 47 | location.hash, 48 | "?" 49 | )}?${JSON.stringify(params) 50 | .replace(/["{}]/g, "") 51 | .replace(/:/g, "=") 52 | .replace(/,/g, "&")}`; 53 | 54 | // 替换历史记录项 55 | window.location.replace(newUrl); 56 | } else { 57 | return; 58 | } 59 | })(); 60 | -------------------------------------------------------------------------------- /src/store/modules/epTheme.ts: -------------------------------------------------------------------------------- 1 | import { store } from "@/store"; 2 | import { defineStore } from "pinia"; 3 | import { storageLocal } from "@pureadmin/utils"; 4 | import { getConfig, responsiveStorageNameSpace } from "@/config"; 5 | 6 | export const useEpThemeStore = defineStore({ 7 | id: "pure-epTheme", 8 | state: () => ({ 9 | epThemeColor: 10 | storageLocal().getItem( 11 | `${responsiveStorageNameSpace()}layout` 12 | )?.epThemeColor ?? getConfig().EpThemeColor, 13 | epTheme: 14 | storageLocal().getItem( 15 | `${responsiveStorageNameSpace()}layout` 16 | )?.theme ?? getConfig().Theme 17 | }), 18 | getters: { 19 | getEpThemeColor(state) { 20 | return state.epThemeColor; 21 | }, 22 | /** 用于mix导航模式下hamburger-svg的fill属性 */ 23 | fill(state) { 24 | if (state.epTheme === "light") { 25 | return "#409eff"; 26 | } else if (state.epTheme === "yellow") { 27 | return "#d25f00"; 28 | } else { 29 | return "#fff"; 30 | } 31 | } 32 | }, 33 | actions: { 34 | setEpThemeColor(newColor: string): void { 35 | const layout = storageLocal().getItem( 36 | `${responsiveStorageNameSpace()}layout` 37 | ); 38 | this.epTheme = layout?.theme; 39 | this.epThemeColor = newColor; 40 | if (!layout) return; 41 | layout.epThemeColor = newColor; 42 | storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout); 43 | } 44 | } 45 | }); 46 | 47 | export function useEpThemeStoreHook() { 48 | return useEpThemeStore(store); 49 | } 50 | -------------------------------------------------------------------------------- /src/layout/frameView.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 |