├── env ├── .env.development ├── .env.test ├── .env.production └── .env ├── src ├── utils │ ├── index.ts │ ├── regex.ts │ ├── previewer.ts │ ├── env.ts │ ├── message.ts │ ├── consoler.ts │ ├── http │ │ ├── index.ts │ │ ├── helper.ts │ │ ├── axiosCancel.ts │ │ └── checkStatus.ts │ └── is.ts ├── components │ ├── List │ │ ├── index.ts │ │ └── src │ │ │ └── List.vue │ ├── Avatar │ │ ├── index.ts │ │ └── src │ │ │ └── Avatar.vue │ ├── Editor │ │ ├── index.ts │ │ └── src │ │ │ └── Editor.vue │ ├── PageTitle │ │ ├── index.ts │ │ └── src │ │ │ └── PageTitle.vue │ ├── ShortCut │ │ ├── index.ts │ │ └── src │ │ │ └── ShortCut.vue │ ├── Splitter │ │ ├── index.ts │ │ └── src │ │ │ ├── Trigger.vue │ │ │ └── Splitter.vue │ ├── SearchTree │ │ ├── index.ts │ │ └── src │ │ │ └── SearchTree.vue │ ├── ThemeSwitch │ │ ├── index.ts │ │ └── src │ │ │ └── ThemeSwitch.vue │ ├── ThemeSetting │ │ ├── index.ts │ │ └── src │ │ │ └── ThemeColorPicker.vue │ ├── TableModel │ │ ├── index.ts │ │ └── src │ │ │ ├── useColumn.ts │ │ │ └── TableModel.vue │ ├── Guide │ │ ├── src │ │ │ ├── type.ts │ │ │ └── GuideStep.vue │ │ └── index.ts │ ├── Card │ │ ├── index.ts │ │ └── src │ │ │ ├── Card.vue │ │ │ └── TotalCard.vue │ ├── SearchModel │ │ ├── index.ts │ │ └── src │ │ │ ├── useSearchModel.ts │ │ │ └── useComponent.ts │ ├── VerificationCode │ │ ├── index.ts │ │ └── src │ │ │ └── VerifyDialog.vue │ ├── ECharts │ │ ├── src │ │ │ ├── Radar │ │ │ │ ├── index.vue │ │ │ │ └── option.ts │ │ │ ├── Bar │ │ │ │ ├── index.vue │ │ │ │ └── option.ts │ │ │ ├── Pie │ │ │ │ ├── index.vue │ │ │ │ └── option.ts │ │ │ ├── Line │ │ │ │ ├── index.vue │ │ │ │ └── option.ts │ │ │ └── useECharts.ts │ │ └── index.ts │ └── register.ts ├── enums │ ├── storageEnum.ts │ ├── permissionEnum.ts │ ├── menuEnum.ts │ ├── authEnum.ts │ ├── appEnum.ts │ ├── consoleEnum.ts │ └── httpEnum.ts ├── assets │ ├── fonts │ │ ├── D-DIN-Bold.ttf │ │ └── Helvetica-Neue.ttf │ └── icons │ │ ├── layout.svg │ │ ├── growth.svg │ │ ├── double-arrow.svg │ │ ├── locale.svg │ │ ├── heart.svg │ │ ├── logo.svg │ │ ├── coffee.svg │ │ ├── sun.svg │ │ └── moon.svg ├── styles │ ├── common │ │ ├── index.scss │ │ ├── var.scss │ │ ├── animation.scss │ │ ├── public.scss │ │ └── transition.scss │ └── element │ │ └── index.scss ├── config │ ├── utils.ts │ └── index.ts ├── layouts │ ├── screen │ │ └── index.vue │ └── admin │ │ ├── content │ │ ├── useContent.ts │ │ └── index.vue │ │ ├── footer │ │ └── index.vue │ │ ├── sider │ │ ├── index.vue │ │ └── components │ │ │ ├── LogoView.vue │ │ │ └── menu │ │ │ ├── MenuItem.vue │ │ │ └── index.vue │ │ ├── header │ │ └── components │ │ │ └── Breadcrumb.vue │ │ ├── index.vue │ │ └── tabs │ │ └── index.vue ├── views │ ├── screen │ │ └── index.vue │ ├── admin │ │ ├── component │ │ │ ├── editor │ │ │ │ └── index.vue │ │ │ ├── verification-code │ │ │ │ └── index.vue │ │ │ ├── split-pane │ │ │ │ └── index.vue │ │ │ ├── icon │ │ │ │ └── index.vue │ │ │ ├── form │ │ │ │ └── index.vue │ │ │ └── table │ │ │ │ └── index.vue │ │ ├── feat │ │ │ ├── watermark │ │ │ │ └── index.vue │ │ │ ├── lazy │ │ │ │ └── index.vue │ │ │ ├── image-preview │ │ │ │ └── index.vue │ │ │ └── guide │ │ │ │ └── index.vue │ │ ├── dynamic │ │ │ ├── second │ │ │ │ └── index.vue │ │ │ └── first │ │ │ │ └── index.vue │ │ ├── _system │ │ │ ├── post │ │ │ │ └── usePage.ts │ │ │ ├── role │ │ │ │ └── usePage.ts │ │ │ ├── department │ │ │ │ └── usePage.ts │ │ │ ├── dict │ │ │ │ ├── usePage.ts │ │ │ │ └── detail.vue │ │ │ ├── menu │ │ │ │ └── usePage.ts │ │ │ └── user │ │ │ │ ├── usePage.ts │ │ │ │ └── detail.vue │ │ └── home │ │ │ └── dashboard │ │ │ └── components │ │ │ └── RankList.vue │ ├── error │ │ └── PageNotFound.vue │ └── login │ │ └── index.vue ├── api │ ├── _system │ │ ├── model │ │ │ ├── postModel.ts │ │ │ ├── departmentModel.ts │ │ │ ├── roleModel.ts │ │ │ ├── dictModel.ts │ │ │ ├── userModel.ts │ │ │ └── menuModel.ts │ │ ├── dict.ts │ │ ├── post.ts │ │ ├── role.ts │ │ ├── department.ts │ │ ├── menu.ts │ │ └── user.ts │ ├── _auth │ │ ├── model │ │ │ └── index.ts │ │ └── index.ts │ └── model │ │ └── baseModel.ts ├── composables │ ├── web │ │ ├── useMessage.ts │ │ ├── useStorage.ts │ │ ├── useCssVar.ts │ │ ├── useCookie.ts │ │ └── useFullScreen.ts │ ├── logic │ │ ├── useLogin.ts │ │ ├── useDelay.ts │ │ └── useMenu.ts │ └── core │ │ └── useContext.ts ├── router │ ├── types.ts │ ├── routes │ │ ├── modules │ │ │ ├── error.ts │ │ │ ├── admin │ │ │ │ ├── index.ts │ │ │ │ ├── personal.ts │ │ │ │ ├── home.ts │ │ │ │ ├── feat.ts │ │ │ │ ├── component.ts │ │ │ │ └── system.ts │ │ │ └── screen.ts │ │ └── index.ts │ ├── index.ts │ ├── guard │ │ ├── permissionGuard.ts │ │ └── index.ts │ └── helper │ │ └── index.ts ├── App.vue ├── store │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ ├── menu.ts │ │ ├── setting.ts │ │ └── user.ts ├── directives │ ├── index.ts │ ├── lazy.ts │ ├── permission.ts │ ├── echarts.ts │ └── watermark.ts ├── main.ts └── locales │ ├── index.ts │ └── lang │ ├── zh-cn.ts │ ├── zh-tw.ts │ └── en.ts ├── plop ├── component │ ├── index.hbs │ ├── component.hbs │ └── prompt.js ├── utils.js ├── store │ ├── module.hbs │ └── prompt.js └── page │ ├── prompt.js │ └── page.hbs ├── .npmrc ├── public └── favicon.ico ├── .vscode ├── extensions.json ├── vue3.code-snippets └── settings.json ├── mock ├── static │ ├── auth.ts │ ├── users.ts │ └── menus.ts ├── types.ts ├── api.ts ├── modules │ ├── department.ts │ ├── post.ts │ ├── role.ts │ ├── auth.ts │ ├── dict.ts │ ├── user.ts │ └── menu.ts ├── util.ts ├── production-inject.ts └── index.ts ├── .gitignore ├── netlify.toml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ ├── release.yml │ └── ci.yml ├── .eslintrc.json ├── types ├── index.d.ts ├── vue-router.d.ts ├── config.d.ts ├── vite-env.d.ts └── http.d.ts ├── plopfile.js ├── tsconfig.json ├── LICENSE ├── README.zh-CN.md ├── commitlint.config.cjs ├── README.md ├── uno.config.ts ├── package.json └── vite.config.ts /env/.env.development: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /plop/component/index.hbs: -------------------------------------------------------------------------------- 1 | export { default } from './src/{{name}}.vue' 2 | -------------------------------------------------------------------------------- /src/components/List/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/List.vue' 2 | -------------------------------------------------------------------------------- /src/components/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/Avatar.vue' 2 | -------------------------------------------------------------------------------- /src/components/Editor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/Editor.vue' 2 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/PageTitle.vue' 2 | -------------------------------------------------------------------------------- /src/components/ShortCut/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/ShortCut.vue' 2 | -------------------------------------------------------------------------------- /src/components/Splitter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/Splitter.vue' 2 | -------------------------------------------------------------------------------- /src/components/SearchTree/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/SearchTree.vue' 2 | -------------------------------------------------------------------------------- /src/components/ThemeSwitch/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/ThemeSwitch.vue' 2 | -------------------------------------------------------------------------------- /src/components/ThemeSetting/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/ThemeSetting.vue' 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="v" 2 | message="chore: release v%s" 3 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhou-tao/vue-power-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /plop/utils.js: -------------------------------------------------------------------------------- 1 | export const notEmpty = name => v => 2 | !v || v.trim() === '' ? `${name} is required` : true 3 | -------------------------------------------------------------------------------- /src/enums/storageEnum.ts: -------------------------------------------------------------------------------- 1 | export enum LocalStorageEnum { 2 | VP_HAS_FP_LOADING = 'vp_has_fp_loading' 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/fonts/D-DIN-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhou-tao/vue-power-admin/HEAD/src/assets/fonts/D-DIN-Bold.ttf -------------------------------------------------------------------------------- /src/components/TableModel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/TableModel.vue' 2 | export * from './src/useColumn' 3 | -------------------------------------------------------------------------------- /src/assets/fonts/Helvetica-Neue.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhou-tao/vue-power-admin/HEAD/src/assets/fonts/Helvetica-Neue.ttf -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "antfu.unocss" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/common/index.scss: -------------------------------------------------------------------------------- 1 | @use 'animation.scss' as *; 2 | @use 'transition.scss' as *; 3 | @use 'public.scss' as *; 4 | @use 'var.scss' as *; 5 | -------------------------------------------------------------------------------- /src/components/Guide/src/type.ts: -------------------------------------------------------------------------------- 1 | export interface GuideStepProps { 2 | el: any 3 | title: string 4 | description: string 5 | placement?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/config/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from '#/config' 2 | 3 | export function defineConfig(config: AppConfig): AppConfig { 4 | return config 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | import Card from './src/Card.vue' 2 | import TotalCard from './src/TotalCard.vue' 3 | 4 | export { 5 | Card, 6 | TotalCard 7 | } 8 | -------------------------------------------------------------------------------- /src/components/SearchModel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './src/SearchModel.vue' 2 | export * from './src/useSearchModel' 3 | export * from './src/useComponent' 4 | -------------------------------------------------------------------------------- /src/enums/permissionEnum.ts: -------------------------------------------------------------------------------- 1 | // 页面内按钮权限 2 | export enum ButtonEnum { 3 | add = 'add', 4 | update = 'update', 5 | delete = 'delete', 6 | export = 'export' 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | // 覆盖 element-plus 变量 2 | :root { 3 | --el-color-primary: #377dff; 4 | } 5 | 6 | html.dark { 7 | --el-color-primary: #377dff; 8 | } 9 | -------------------------------------------------------------------------------- /src/layouts/screen/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/screen/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plop/component/component.hbs: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mock/static/auth.ts: -------------------------------------------------------------------------------- 1 | export const authToken = () => ({ 2 | access_token: 'mock_access_token', 3 | refresh_token: 'mock_refresh_token', 4 | expires_in: '6000', 5 | token_type: 'oauth2' 6 | }) 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | .history 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | -------------------------------------------------------------------------------- /src/components/Guide/index.ts: -------------------------------------------------------------------------------- 1 | import Guide from './src/Guide.vue' 2 | import GuideStep from './src/GuideStep.vue' 3 | 4 | export * from './src/type' 5 | 6 | export { 7 | Guide, 8 | GuideStep 9 | } 10 | -------------------------------------------------------------------------------- /src/components/VerificationCode/index.ts: -------------------------------------------------------------------------------- 1 | import VerificationCode from './src/VerificationCode.vue' 2 | import VerifyDialog from './src/VerifyDialog.vue' 3 | 4 | export { 5 | VerificationCode, 6 | VerifyDialog 7 | } 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "16" 3 | 4 | [build] 5 | publish = "dist" 6 | command = "pnpm run build" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 -------------------------------------------------------------------------------- /src/enums/menuEnum.ts: -------------------------------------------------------------------------------- 1 | export enum MenuLayout { 2 | VERTICAL = 'vertical', 3 | HORIZONTAL = 'horizontal' 4 | } 5 | 6 | export enum LayoutType { 7 | SCREEN_LAYOUT = 'Screen', 8 | PAGE_LAYOUT = 'Layout' 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/icons/layout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/_system/model/postModel.ts: -------------------------------------------------------------------------------- 1 | import type { DateLogModel } from '@/api/model/baseModel' 2 | 3 | export interface PostModel extends DateLogModel { 4 | id: number 5 | code: string 6 | name: string 7 | description?: string 8 | } 9 | -------------------------------------------------------------------------------- /mock/types.ts: -------------------------------------------------------------------------------- 1 | export type MockTemplate = Record 2 | 3 | export interface MockInput { 4 | body: Record 5 | } 6 | export interface Mock { 7 | [key: string]: ((input: MockInput) => MockTemplate) | MockTemplate 8 | } 9 | -------------------------------------------------------------------------------- /src/composables/web/useMessage.ts: -------------------------------------------------------------------------------- 1 | import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' 2 | 3 | export const useMessage = () => ({ 4 | $message: ElMessage, 5 | $msgbox: ElMessageBox, 6 | $notify: ElNotification 7 | }) 8 | -------------------------------------------------------------------------------- /src/router/types.ts: -------------------------------------------------------------------------------- 1 | import type { RouteMeta, RouteRecordRaw } from 'vue-router' 2 | 3 | export type AppRouteConfig = Omit & { 4 | id?: number 5 | meta?: RouteMeta 6 | children?: AppRouteConfig[] 7 | } 8 | -------------------------------------------------------------------------------- /src/api/_system/model/departmentModel.ts: -------------------------------------------------------------------------------- 1 | import type { DateLogModel } from '@/api/model/baseModel' 2 | 3 | export interface DepartmentModel extends DateLogModel { 4 | id: number 5 | code: string 6 | name: string 7 | description?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 检查密码复杂度(6-20位 数字和英文字母组合) 3 | * @param {String} password 4 | * @returns 5 | */ 6 | export const checkPassword = (password: string): boolean => { 7 | return /^(?=.*\d)(?=.*[A-Za-z])\w{6,20}$/.test(password) 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Radar/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /src/api/_system/model/roleModel.ts: -------------------------------------------------------------------------------- 1 | import type { DateLogModel } from '@/api/model/baseModel' 2 | 3 | export interface RoleModel extends DateLogModel { 4 | id: number 5 | code: string 6 | name: string 7 | menu: number[] 8 | description?: string 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Questions & Discussions 4 | url: https://github.com/zhou-tao/vue-power-admin/discussions 5 | about: Use GitHub discussions for message-board style questions and discussions. 6 | -------------------------------------------------------------------------------- /env/.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | 3 | # 访问地址前缀 4 | VITE_PUBLIC_PATH = / 5 | 6 | # 请求地址前缀 7 | VITE_BASE_API = / 8 | 9 | # 开启Mock 10 | VITE_USE_MOCK = true 11 | 12 | # 删除console、debugger、alert... 13 | VITE_DROP_CONSOLE = false 14 | 15 | # 开启vite-legacy 16 | VITE_USE_LEGACY = false 17 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "@toryz", 4 | "rules": { 5 | "import/no-named-as-default-member": "off", 6 | "@typescript-eslint/no-use-before-define": "off", 7 | "@typescript-eslint/no-invalid-this": "off", 8 | "@typescript-eslint/ban-types": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/router/routes/modules/error.ts: -------------------------------------------------------------------------------- 1 | export const PageNotFoundRoute = { 2 | path: '/:path(.*)*', 3 | name: 'PageNotFound', 4 | component: () => import('@/views/error/PageNotFound.vue'), 5 | hidden: true, 6 | meta: { 7 | title: 'menu.error.notFound', 8 | requiresAuth: false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/enums/authEnum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthTypeEnum { 2 | BASIC = 'basic' 3 | } 4 | 5 | export enum GrantTypeEnum { 6 | PASSWORD = 'password', 7 | REFRESH_TOKEN = 'refresh_token' 8 | } 9 | 10 | export enum TokenTypeEnum { 11 | ACCESS_TOKEN = 'ACCESS_TOKEN', 12 | REFRESH_TOKEN = 'REFRESH_TOKEN' 13 | } 14 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type UnDefable = T | undefined 2 | declare type Nullable = T | null 3 | declare type NonNullable = T extends null | undefined ? never : T 4 | declare type Recordable = Record 5 | declare type MaybeRef = T | Ref 6 | declare type TimeoutID = Node.Timeout | null 7 | -------------------------------------------------------------------------------- /env/.env.production: -------------------------------------------------------------------------------- 1 | # 访问地址前缀 2 | VITE_PUBLIC_PATH = / 3 | 4 | # 请求地址前缀 5 | VITE_BASE_API = / 6 | 7 | # 开启Proxy 8 | VITE_USE_PROXY = false 9 | 10 | # 开启Mock 11 | VITE_USE_MOCK = true 12 | 13 | # 删除console、debugger、alert... 14 | VITE_DROP_CONSOLE = true 15 | 16 | # 开启vite-legacy 17 | VITE_USE_LEGACY = true 18 | -------------------------------------------------------------------------------- /src/composables/web/useStorage.ts: -------------------------------------------------------------------------------- 1 | import type { LocalStorageEnum } from '@/enums/storageEnum' 2 | 3 | export const setLocalStorage = (key: LocalStorageEnum, value: any) => { 4 | localStorage[key] = value 5 | } 6 | 7 | export const getLocalStorage = (key: LocalStorageEnum) => { 8 | return localStorage[key] 9 | } 10 | -------------------------------------------------------------------------------- /src/api/_auth/model/index.ts: -------------------------------------------------------------------------------- 1 | export interface LoginParams { 2 | username: string 3 | password: string 4 | rememberMe?: boolean 5 | } 6 | 7 | export interface LoginResultModel { 8 | access_token: string 9 | refresh_token: string 10 | expires_in: number 11 | token_type: string 12 | [key: string]: any 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ECharts/index.ts: -------------------------------------------------------------------------------- 1 | import AreaLineChart from './src/Line/index.vue' 2 | import BarChart from './src/Bar/index.vue' 3 | import PieChart from './src/Pie/index.vue' 4 | import RadarChart from './src/Radar/index.vue' 5 | 6 | export { 7 | AreaLineChart, 8 | BarChart, 9 | PieChart, 10 | RadarChart 11 | } 12 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 4 | 5 | export const store = createPinia().use(piniaPluginPersistedstate) 6 | 7 | export function setupStore(app: App) { 8 | app.use(store) 9 | } 10 | -------------------------------------------------------------------------------- /types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta extends Record { 5 | title?: string 6 | icon?: string 7 | hideMenu?: boolean 8 | activeMenu?: string 9 | requiresAuth?: boolean 10 | permission?: string[] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/icons/growth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/admin/component/editor/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/SearchModel/src/useSearchModel.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from './useComponent' 2 | 3 | interface Option { 4 | label: string 5 | value: string | number | boolean 6 | } 7 | 8 | export interface SearchItemConfig { 9 | component: ComponentType 10 | label: string 11 | field: string 12 | options?: Option[] 13 | [key: string]: any 14 | } 15 | -------------------------------------------------------------------------------- /src/enums/appEnum.ts: -------------------------------------------------------------------------------- 1 | export enum ThemeEnum { 2 | LIGHT = 'light', 3 | DARK = 'dark' 4 | } 5 | 6 | export enum CSSVarEnum { 7 | COLOR_PRIMARY = '--el-color-primary', 8 | COLOR_SUCCESS = '--el-color-success', 9 | CONTENT_HEIGHT = '--content-base-height', 10 | CONTENT_MIN_HEIGHT = '--content-min-height', 11 | CONTENT_NAX_HEIGHT = '--content-max-height' 12 | } 13 | -------------------------------------------------------------------------------- /types/config.d.ts: -------------------------------------------------------------------------------- 1 | export interface App { 2 | title: string 3 | security: boolean 4 | } 5 | 6 | export interface Http { 7 | timeout: number 8 | } 9 | 10 | export interface Oauth { 11 | execute: boolean 12 | client_id: string 13 | client_secret: string 14 | } 15 | 16 | export interface AppConfig { 17 | APP: App 18 | HTTP: Http 19 | OAUTH: Oauth 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/icons/double-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/common/var.scss: -------------------------------------------------------------------------------- 1 | // custom css vars 2 | :root { 3 | // 内容区撑满页面最小高度(不出现滚动条) 4 | // 169px: header[64+3] + tab[40] + main padding[12] + footer[50] 5 | --content-base-height: calc(100vh - 169px); 6 | // 隐藏tab时高度 7 | --content-max-height: calc(100vh - 129px); 8 | // 显示tab时高度 9 | --content-min-height: calc(100vh - 169px); 10 | // tab view高度 11 | --tab-view-height: 40px; 12 | } -------------------------------------------------------------------------------- /mock/api.ts: -------------------------------------------------------------------------------- 1 | export enum MockApi { 2 | AUTH = '/oauth/token', 3 | USER_LIST = '/sysadmin/user/list', 4 | USER_INFO = '/sysadmin/user/info', 5 | MENU_LIST = '/sysadmin/menu/list', 6 | MENU_BUILD = '/sysadmin/menu/build', 7 | ROLE_LIST = '/sysadmin/role/list', 8 | DICT_LIST = '/sysadmin/dict/list', 9 | DEPT_LIST = '/sysadmin/department/list', 10 | POST_LIST = '/sysadmin/post/list' 11 | } 12 | -------------------------------------------------------------------------------- /mock/modules/department.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | 3 | export const DeptResult = { 4 | 'id|+1': 1, 5 | 'code': '@word(2, 3)', 6 | 'name': '@cword(2, 4)部', 7 | 'description': '@cword(2, 6).', 8 | 'createdBy': '@first @last', 9 | 'createdTime': '2022-@date("MM-dd")' 10 | } 11 | 12 | export const deptMockApi = ({ body = {} }) => generatePageData(body, () => DeptResult) 13 | -------------------------------------------------------------------------------- /src/api/_system/model/dictModel.ts: -------------------------------------------------------------------------------- 1 | import type { DateLogModel } from '@/api/model/baseModel' 2 | 3 | export interface DictOption { 4 | label: string 5 | value: string | number 6 | } 7 | 8 | export interface DictModel extends DateLogModel { 9 | id: number 10 | typeName: string 11 | typeCode: string 12 | name: string 13 | code: string 14 | options: DictOption[] 15 | description?: string 16 | } 17 | -------------------------------------------------------------------------------- /src/views/admin/feat/watermark/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.vscode/vue3.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Print to console": { 3 | "scope": "vue", 4 | "prefix": "v3", 5 | "body": [ 6 | "", 8 | "\n", 11 | "\n", 12 | "" 13 | ], 14 | "description": "Vue3 setup sugar snippets" 15 | } 16 | } -------------------------------------------------------------------------------- /mock/modules/post.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | 3 | export const PostResult = { 4 | 'id|+1': 1, 5 | 'code|+1': 1, 6 | 'name|+1': ['前端', '后端', '产品', '测试', 'UI', '项目经理'], 7 | 'description': '@cword(6, 12).', 8 | 'createdBy': '@first @last', 9 | 'createdTime': '2022-@date("MM-dd")' 10 | } 11 | 12 | export const postMockApi = ({ body = {} }) => generatePageData(body, () => PostResult) 13 | -------------------------------------------------------------------------------- /src/composables/web/useCssVar.ts: -------------------------------------------------------------------------------- 1 | import type { CSSVarEnum } from '@/enums/appEnum' 2 | 3 | const RootEl = document.documentElement 4 | 5 | export const getCssVar = (name: CSSVarEnum) => { 6 | const value = getComputedStyle(RootEl).getPropertyValue(name) 7 | return value?.trim() 8 | } 9 | 10 | export const setCssVar = (name: CSSVarEnum, value: string) => { 11 | RootEl.style.setProperty(name, value) 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/common/animation.scss: -------------------------------------------------------------------------------- 1 | // 模糊到清晰 2 | @keyframes fade-in { 3 | 0% { 4 | filter: blur(3px); 5 | } 6 | 7 | 50% { 8 | filter: blur(2px); 9 | } 10 | 11 | 100% { 12 | filter: blur(0); 13 | } 14 | } 15 | 16 | // 缓慢展开 17 | @keyframes fade-open { 18 | 0% { 19 | height: 0; 20 | opacity: 0; 21 | } 22 | 23 | 100% { 24 | height: 30px; 25 | opacity: 1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Avatar/src/Avatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /env/.env: -------------------------------------------------------------------------------- 1 | # 开发环境启动端口 2 | VITE_PORT = 3100 3 | 4 | # 访问地址前缀 5 | VITE_PUBLIC_PATH = / 6 | 7 | # 请求地址前缀 8 | VITE_BASE_API = / 9 | 10 | # 开启Proxy 11 | VITE_USE_PROXY = true 12 | 13 | # Proxy 前缀列表 14 | VITE_PROXY_PREFIX = ["/api", "/oauth", "/sysadmin"]|string[] 15 | 16 | # 开启Mock 17 | VITE_USE_MOCK = true 18 | 19 | # 删除console、debugger、alert... 20 | VITE_DROP_CONSOLE = false 21 | 22 | # 开启vite-legacy 23 | VITE_USE_LEGACY = false 24 | -------------------------------------------------------------------------------- /src/layouts/admin/content/useContent.ts: -------------------------------------------------------------------------------- 1 | // 刷新内容区域 2 | export const refresh = ref(false) 3 | export const componentKey = ref() 4 | 5 | export const useRefresh = () => { 6 | const route = useRoute() 7 | watch(refresh, (v) => { 8 | if (v) { 9 | componentKey.value = `${route.path}/refresh` 10 | } 11 | else { 12 | componentKey.value = route.path 13 | } 14 | }) 15 | return { componentKey } 16 | } 17 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, ObjectDirective } from 'vue' 2 | import { vPermission } from './permission' 3 | 4 | const GlobalDirectives: Record> = { 5 | permission: vPermission 6 | // more... 7 | } 8 | 9 | export function setupDirective(app: App) { 10 | for (const directiveName in GlobalDirectives) { 11 | app.directive(directiveName, GlobalDirectives[directiveName]) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/api/_system/dict.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { DictModel } from './model/dictModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | export enum Api { 6 | DICT_LIST = '/sysadmin/dict/list' 7 | } 8 | 9 | export const getDictList = (data: ListQuery) => { 10 | return useFetch.POST>({ 11 | url: Api.DICT_LIST, 12 | useMock: true, 13 | data 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/api/_system/post.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { PostModel } from './model/postModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | export enum Api { 6 | POST_LIST = '/sysadmin/post/list' 7 | } 8 | 9 | export const getPostList = (data: ListQuery) => { 10 | return useFetch.POST>({ 11 | url: Api.POST_LIST, 12 | useMock: true, 13 | data 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/api/_system/role.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { RoleModel } from './model/roleModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | export enum Api { 6 | ROLE_LIST = '/sysadmin/role/list' 7 | } 8 | 9 | export const getRoleList = (data: ListQuery) => { 10 | return useFetch.POST>({ 11 | url: Api.ROLE_LIST, 12 | useMock: true, 13 | data 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/icons/locale.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/modules/role.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | 3 | export const RoleResult = { 4 | 'id|+1': 1, 5 | 'code|+1': ['ADMIN', 'USER', 'GUEST'], 6 | 'name|+1': ['管理员', '用户', '访客'], 7 | 'menu|1-5': '@integer(1, 100)', 8 | 'description': '@cword(6, 12).', 9 | 'createdBy': '@first @last', 10 | 'createdTime': '2022-@date("MM-dd")' 11 | } 12 | 13 | export const roleMockApi = ({ body = {} }) => generatePageData(body, () => RoleResult) 14 | -------------------------------------------------------------------------------- /src/utils/previewer.ts: -------------------------------------------------------------------------------- 1 | // 图片预览器 2 | import 'viewerjs/dist/viewer.css' 3 | import Viewer from 'viewerjs' 4 | 5 | export default class Previewer { 6 | private instance: Viewer 7 | 8 | constructor(el: HTMLElement) { 9 | const viewer = new Viewer(el) 10 | this.instance = viewer 11 | } 12 | 13 | getInstance() { 14 | return this.instance 15 | } 16 | 17 | setOptions(opts: Record) { 18 | Viewer.setDefaults(opts) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/api/_system/model/userModel.ts: -------------------------------------------------------------------------------- 1 | import type { RoleModel } from './roleModel' 2 | import type { PostModel } from './postModel' 3 | 4 | export interface UserInfoModel { 5 | id: string | number 6 | name: string 7 | userId: number 8 | username: string 9 | gender: string 10 | avatar: string 11 | deptCode: Nullable 12 | deptName: Nullable 13 | mobile: Nullable 14 | roles: Partial[] 15 | posts: Partial[] 16 | } 17 | -------------------------------------------------------------------------------- /src/api/model/baseModel.ts: -------------------------------------------------------------------------------- 1 | export type ApiParam = Record 2 | 3 | export interface ListQuery { 4 | current: number 5 | size: number 6 | query?: Partial 7 | } 8 | 9 | export interface ListResult { 10 | current: number 11 | size: number 12 | total: number 13 | list: T[] 14 | } 15 | 16 | export interface DateLogModel { 17 | createTime: string 18 | createdBy: string 19 | updateTime: string 20 | updatedBy: string 21 | } 22 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/index.ts: -------------------------------------------------------------------------------- 1 | import HomeRoute from './home' 2 | import SystemRoute from './system' 3 | import ComponentRoute from './component' 4 | import FeatRoute from './feat' 5 | import PersonalRoute from './personal' 6 | import type { AppRouteConfig } from '@/router/types' 7 | 8 | const AdminRoutes: AppRouteConfig[] = [ 9 | HomeRoute, 10 | SystemRoute, 11 | ComponentRoute, 12 | FeatRoute, 13 | PersonalRoute 14 | ] 15 | 16 | export default AdminRoutes 17 | -------------------------------------------------------------------------------- /plop/store/module.hbs: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | interface {{ camelCase name }}State { 4 | foo: string 5 | } 6 | 7 | export const use{{ camelCase name }}Store = defineStore('{{ name }}', { 8 | state: (): {{ camelCase name }}State => ({ 9 | foo: 'bar' 10 | }), 11 | getters: {}, 12 | actions: {}, 13 | {{#if hasPersist }} 14 | persist: { 15 | key: '{{ upperCase name }}_STORE', 16 | storage: window.{{ persist }}Storage 17 | }{{/if}} 18 | }) 19 | -------------------------------------------------------------------------------- /src/directives/lazy.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | 3 | interface LazyOption { 4 | src: string 5 | } 6 | 7 | export const vLazy: ObjectDirective = { 8 | mounted(el, { value: { src } }) { 9 | const observer = new IntersectionObserver(([e]) => { 10 | if (e.isIntersecting) { 11 | e.target.setAttribute('src', src) 12 | observer.unobserve(e.target) 13 | } 14 | }) 15 | observer.observe(el) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/directives/permission.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | import type { ButtonEnum } from '@/enums/permissionEnum' 3 | 4 | export const vPermission: ObjectDirective = { 5 | created(el, { arg, instance }) { 6 | const permissions = (instance?.$route?.meta?.permissions || []) as ButtonEnum[] 7 | const noPermission = !permissions.includes(arg as ButtonEnum) 8 | if (noPermission) { 9 | el.style.display = 'none' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/api/_system/department.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { DepartmentModel } from './model/departmentModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | export enum Api { 6 | DEPARTMENT_LIST = '/sysadmin/department/list' 7 | } 8 | 9 | export const getDepartmentList = (data: ListQuery) => { 10 | return useFetch.POST>({ 11 | url: Api.DEPARTMENT_LIST, 12 | useMock: true, 13 | data 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* Generated by vite-plugin-env-parser */ 2 | /// 3 | 4 | declare module "vite-env" { 5 | readonly const VITE_PORT: number 6 | readonly const VITE_PUBLIC_PATH: string 7 | readonly const VITE_BASE_API: string 8 | readonly const VITE_USE_PROXY: boolean 9 | readonly const VITE_PROXY_PREFIX: string[] 10 | readonly const VITE_USE_MOCK: boolean 11 | readonly const VITE_DROP_CONSOLE: boolean 12 | readonly const VITE_USE_LEGACY: boolean 13 | } 14 | -------------------------------------------------------------------------------- /mock/modules/auth.ts: -------------------------------------------------------------------------------- 1 | import { userAccounts } from '../static/users' 2 | import { authToken } from '../static/auth' 3 | 4 | export const authMockApi = ({ body = {} }: Record) => { 5 | const { grant_type, username, password } = body 6 | if (grant_type === 'refresh_token') return authToken // 仅用于模拟,真实场景下可能存在refresh_token失效,需返回401状态 7 | if (userAccounts.some(u => u.username === username && u.password === password)) { 8 | return authToken 9 | } 10 | else { 11 | return null 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from './utils' 2 | 3 | export default defineConfig({ 4 | // 系统设置 5 | APP: { 6 | // 业务中心侧边菜单标题 7 | title: 'Vue Power Admin', 8 | // 密码安全检查 9 | security: false 10 | }, 11 | // http相关 12 | HTTP: { 13 | timeout: 10000 14 | }, 15 | OAUTH: { 16 | // 是否执行Auth登录(包括路由、接口检查token有效性) 17 | execute: true, 18 | // oauth中请求头内需加密的client_id 19 | client_id: 'client', 20 | // oauth中请求头内需加密的client_secret 21 | client_secret: '123456' 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/composables/logic/useLogin.ts: -------------------------------------------------------------------------------- 1 | import type { LoginParams, LoginResultModel } from '@/api/_auth/model' 2 | import { useUserStore } from '@/store/modules/user' 3 | 4 | type LoginResult = [success: boolean, data?: LoginResultModel] 5 | 6 | export async function useLoginByPassword( 7 | loginForm: LoginParams 8 | ): Promise { 9 | const userStore = useUserStore() 10 | try { 11 | const data = await userStore.login(loginForm) 12 | return [true, data] 13 | } 14 | catch { 15 | return [false] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/views/error/PageNotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /src/layouts/admin/footer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.organizeImports": "never" 7 | }, 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "typescript", 12 | "typescriptreact", 13 | "vue", 14 | "svelte", 15 | "html", 16 | "markdown", 17 | "json", 18 | "jsonc", 19 | "yml", 20 | "yaml" 21 | ], 22 | "scss.lint.unknownAtRules": "ignore" 23 | } 24 | -------------------------------------------------------------------------------- /src/components/PageTitle/src/PageTitle.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 开发环境 3 | */ 4 | export const devMode = 'development' 5 | 6 | /** 7 | * @description: 生产环境 8 | */ 9 | export const prodMode = 'production' 10 | 11 | /** 12 | * @description: 获取当前环境模式 13 | */ 14 | export function getEnv(): string { 15 | return import.meta.env.MODE 16 | } 17 | 18 | /** 19 | * @description: 是否为开发环境 20 | */ 21 | export function isDevMode(): boolean { 22 | return import.meta.env.DEV 23 | } 24 | 25 | /** 26 | * @description: 是否为生产环境 27 | */ 28 | export function isProdMode(): boolean { 29 | return import.meta.env.PROD 30 | } 31 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { VITE_PUBLIC_PATH } from 'vite-env' 5 | 6 | import { basicRoutes } from './routes' 7 | 8 | export const router = createRouter({ 9 | history: createWebHistory(VITE_PUBLIC_PATH), 10 | routes: basicRoutes as unknown as RouteRecordRaw[], 11 | // disabled tail-slash path 12 | strict: true, 13 | scrollBehavior: () => ({ left: 0, top: 0 }) 14 | }) 15 | 16 | export function setupRouter(app: App) { 17 | app.use(router) 18 | } 19 | -------------------------------------------------------------------------------- /src/router/routes/modules/screen.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouteConfig } from '@/router/types' 2 | import ScreenLayout from '@/layouts/screen/index.vue' 3 | 4 | const ScreenRoute: AppRouteConfig = { 5 | path: '/screen', 6 | name: 'Screen', 7 | component: ScreenLayout, 8 | redirect: '/screen/index', 9 | meta: { 10 | title: '数据大屏' 11 | }, 12 | children: [ 13 | { 14 | path: 'index', 15 | name: 'MainScreen', 16 | component: () => import('@/views/screen/index.vue'), 17 | meta: { 18 | title: '首页' 19 | } 20 | } 21 | ] 22 | } 23 | 24 | export default ScreenRoute 25 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | -------------------------------------------------------------------------------- /mock/util.ts: -------------------------------------------------------------------------------- 1 | import type { MockTemplate } from './types' 2 | 3 | const DefaultTotal = 100 4 | 5 | export const generatePageData = (body: Record, result: (q: MockTemplate) => MockTemplate) => { 6 | const { size = 10, current, query } = body 7 | const maxPage = Math.ceil(DefaultTotal / size) 8 | const overSize = size * current - DefaultTotal 9 | const cur = overSize < size ? current : maxPage 10 | const sz = overSize > 0 && overSize < size ? overSize : size 11 | return { 12 | [`list|${sz}`]: [result(query)], 13 | size, 14 | current: cur, 15 | total: DefaultTotal 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Bar/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Pie/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | import componentGenerator from './plop/component/prompt.js' 2 | import storeGenerator from './plop/store/prompt.js' 3 | import pageGenerator from './plop/page/prompt.js' 4 | 5 | export default (plop) => { 6 | plop.setHelper('upperCase', str => str ? str.toUpperCase() : str) 7 | plop.setHelper('camelCase', (str) => { 8 | if (!str) return str 9 | // upper case first letter only 10 | return `${str[0].toUpperCase()}${str.slice(1)}` 11 | }) 12 | plop.setGenerator('component', componentGenerator) 13 | plop.setGenerator('store', storeGenerator) 14 | plop.setGenerator('page', pageGenerator) 15 | } 16 | -------------------------------------------------------------------------------- /src/views/admin/feat/lazy/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mock/modules/dict.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | 3 | export const DictResult = { 4 | 'id|+1': 1, 5 | 'typeCode': '@pick(["GENDER", "JOB_LEVEL", "POST", "MENU_TYPE", "AUTH_TYPE"])', 6 | 'typeName': '@pick(["性别", "职级", "岗位", "菜单", "角色"])', 7 | 'code': '@word(2, 5)', 8 | 'name': '@cword(2, 4)', 9 | 'description': '@cword(2, 6).', 10 | 'options|2-4': [{ 11 | 'label': '@cword(2, 4)', 12 | 'value|+1': 1 13 | }], 14 | 'createdBy': '@first @last', 15 | 'createdTime': '2022-@date("MM-dd")' 16 | } 17 | 18 | export const dictMockApi = ({ body = {} }) => generatePageData(body, () => DictResult) 19 | -------------------------------------------------------------------------------- /src/components/register.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { Consoler } from '@/utils/consoler' 3 | 4 | // element dark theme style 5 | import 'element-plus/theme-chalk/src/dark/css-vars.scss' 6 | import '@/styles/element/index.scss' 7 | 8 | // 使用element-plus 自动导入需要手动引入message、loading 样式 9 | import 'element-plus/theme-chalk/el-loading.css' 10 | import 'element-plus/theme-chalk/el-message.css' 11 | import 'element-plus/theme-chalk/el-notification.css' 12 | import 'element-plus/theme-chalk/el-message-box.css' 13 | 14 | export function registerGlobComp(app: App) { 15 | Consoler.SUCCESS('registerGlobComp', app) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Card/src/Card.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/styles/common/public.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | @apply p-0 m-0; 4 | } 5 | 6 | html, 7 | body, 8 | #app { 9 | @apply h-full; 10 | } 11 | 12 | #nprogress { 13 | @apply pointer-events-none; 14 | 15 | .bar { 16 | @apply fixed top-0 left-0 z-99999 w-full h-1 bg-primary opacity-80 rounded-r; 17 | } 18 | } 19 | 20 | .icon { 21 | @apply m-x-1 text-current outline-none; 22 | } 23 | 24 | // hide all scrollbar 25 | *::-webkit-scrollbar { 26 | display: none; 27 | } 28 | 29 | // clear Remember password entry box background color By Google Chrome 30 | input:-webkit-autofill { 31 | transition:background-color 5000s ease-in-out 0s; 32 | } 33 | -------------------------------------------------------------------------------- /mock/static/users.ts: -------------------------------------------------------------------------------- 1 | // 登录账号 2 | export const userAccounts = [ 3 | { username: 'admin', password: '123456' } 4 | ] 5 | 6 | // 用户信息 7 | export const userInfo = { 8 | id: 0, 9 | name: 'admin', 10 | userId: 1, 11 | username: 'Toryz', 12 | gender: '1', 13 | avatar: 'https://avatars.githubusercontent.com/u/36221207?v=4', 14 | deptCode: '007', 15 | deptName: '开发部', 16 | mobile: '18812345678', 17 | posts: [ 18 | { id: 1, code: 'FRONT-END', name: '前端' }, 19 | { id: 4, code: 'OPEN-SOURCE', name: '开源' } 20 | ], 21 | roles: [ 22 | { id: 0, code: 'ADMIN', name: '管理员', menu: [] } 23 | ], 24 | security: true // 密码安全性 25 | } 26 | -------------------------------------------------------------------------------- /src/enums/consoleEnum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 打印颜色主题 3 | */ 4 | export enum ConsoleThemeEnum { 5 | INFO = 'background-image: linear-gradient( 135deg, #60a5fa 10%, #2563eb 100%);', 6 | SUCCESS = 'background-image: linear-gradient( 135deg, #34d399 10%, #059669 100%);', 7 | WARN = 'background-image: linear-gradient( 135deg, #fb923c 10%, #ea580c 100%);', 8 | ERROR = 'background-image: linear-gradient( 135deg, #f87171 10%, #dc2626 100%);' 9 | } 10 | 11 | /** 12 | * @description 打印类型 13 | */ 14 | export enum ConsoleTypeEnum { 15 | VITE = 'VITE', 16 | ROUTER = 'ROUTER', 17 | PINIA = 'PINIA', 18 | AXIOS = 'AXIOS', 19 | MOCK = 'MOCK' 20 | } 21 | -------------------------------------------------------------------------------- /src/api/_system/menu.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { BuildMenuModel, MenuModel } from './model/menuModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | enum Api { 6 | BUILD_MENU = '/sysadmin/menu/build', 7 | MENU_LIST = '/sysadmin/menu/list' 8 | } 9 | 10 | export const buildMenuApi = () => { 11 | return useFetch.GET({ 12 | url: Api.BUILD_MENU, 13 | useMock: true 14 | }) 15 | } 16 | 17 | export const getMenuList = (data: ListQuery) => { 18 | return useFetch.POST>({ 19 | url: Api.MENU_LIST, 20 | useMock: true, 21 | data 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/composables/logic/useDelay.ts: -------------------------------------------------------------------------------- 1 | export const useDebounce = (fn: Function, delay: number) => { 2 | let timeout: TimeoutID = null 3 | return function (this: unknown, ...args: any) { 4 | if (timeout) clearTimeout(timeout) 5 | timeout = setTimeout(() => { 6 | fn.apply(this, args) 7 | timeout = null 8 | }, delay) 9 | } 10 | } 11 | 12 | export const useThrottle = (fn: Function, delay: number) => { 13 | let timeout: TimeoutID = null 14 | return function (this: unknown, ...args: any) { 15 | if (timeout) return 16 | timeout = setTimeout(() => { 17 | fn.apply(this, args) 18 | timeout = null 19 | }, delay) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ShortCut/src/ShortCut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/views/admin/dynamic/second/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/admin/dynamic/first/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | 16 | - name: Create Release for Tag 17 | id: release_tag 18 | uses: yyx990803/release-tag@master 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | body: | 24 | Please refer to [CHANGELOG.md](https://github.com/zhou-tao/vue-power-admin/blob/main/CHANGELOG.md) for details. 25 | -------------------------------------------------------------------------------- /mock/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | import { userInfo } from '../static/users' 3 | import { MenuResult } from './menu' 4 | import { PostResult } from './post' 5 | import { RoleResult } from './role' 6 | 7 | export const userResult = { 8 | 'id|+1': 1, 9 | 'username': '@first @last', 10 | 'name': '@cname', 11 | 'gender': '@pick(["1", "0"])', 12 | 'mobile': /1[1-9]{2}\d{8}/, 13 | 'roles': [RoleResult], 14 | 'deptName': '@cword(3,5)部', 15 | 'posts': [PostResult], 16 | 'menu': [MenuResult] 17 | } 18 | 19 | export const userMockApi = ({ body = {} }) => generatePageData(body, () => userResult) 20 | 21 | export const userInfoMockApi = () => userInfo 22 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/personal.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/layouts/admin/index.vue' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | const PersonalRoute: AppRouteConfig = { 5 | path: '/personal', 6 | name: 'personal', 7 | component: AdminLayout, 8 | redirect: '/personal/index', 9 | meta: { 10 | title: 'menu.personal', 11 | icon: 'ri:checkbox-multiple-fill' 12 | }, 13 | children: [{ 14 | path: 'index', 15 | name: 'personal_page', 16 | component: () => import('@/views/admin/personal/index.vue'), 17 | meta: { 18 | title: 'menu.personal', 19 | hideMenu: true 20 | } 21 | }] 22 | } 23 | 24 | export default PersonalRoute 25 | -------------------------------------------------------------------------------- /src/layouts/admin/content/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouteConfig } from '../types' 2 | import AdminRoutes from './modules/admin/index' 3 | import { PageNotFoundRoute } from './modules/error' 4 | import ScreenRoute from './modules/screen' 5 | 6 | const RootRoute: AppRouteConfig = { 7 | path: '/', 8 | name: 'Root', 9 | redirect: '/login' 10 | } 11 | 12 | const LoginRoute: AppRouteConfig = { 13 | path: '/login', 14 | name: 'Login', 15 | component: () => import('@/views/login/index.vue'), 16 | meta: { 17 | title: 'menu.login', 18 | requiresAuth: false 19 | } 20 | } 21 | 22 | export const basicRoutes = [ 23 | RootRoute, 24 | LoginRoute, 25 | ScreenRoute, 26 | ...AdminRoutes, 27 | PageNotFoundRoute 28 | ] 29 | -------------------------------------------------------------------------------- /src/styles/common/transition.scss: -------------------------------------------------------------------------------- 1 | /* fade-slide */ 2 | .fade-slide-leave-active, 3 | .fade-slide-enter-active { 4 | @apply transition-all duration-300; 5 | } 6 | 7 | .fade-slide-enter-from { 8 | @apply op-0 transform -translate-x-30px; 9 | } 10 | 11 | .fade-slide-leave-to { 12 | @apply op-0 transform translate-x-30px; 13 | } 14 | 15 | .fade-slide-leave-active { 16 | @apply w-full absolute; 17 | } 18 | 19 | /* breadcrumb */ 20 | .breadcrumb-move, 21 | .breadcrumb-enter-active, 22 | .breadcrumb-leave-active { 23 | @apply transition-all duration-500; 24 | } 25 | 26 | .breadcrumb-enter-from, 27 | .breadcrumb-leave-to { 28 | @apply op-0 transform translate-x-30px; 29 | } 30 | 31 | .breadcrumb-leave-active { 32 | @apply absolute; 33 | } -------------------------------------------------------------------------------- /mock/modules/menu.ts: -------------------------------------------------------------------------------- 1 | import { generatePageData } from '../util' 2 | import { buildMenus } from '../static/menus' 3 | 4 | export const MenuResult = { 5 | 'id|+1': 1, 6 | 'title': '@cword(2, 4)', 7 | 'path': '/system/@word(2, 4)', 8 | 'name': '@word(2, 4)', 9 | 'icon': '@pick(["ri:dashboard-fill", "ep:home-filled", "ri:settings-4-fill", "ri:rocket-2-fill", "ri:checkbox-multiple-fill"])', 10 | 'leaf|1': true, 11 | 'order|+1': 1, 12 | 'component': 'src/views/admin/@word(2,4)', 13 | 'parentId|+1': 100, 14 | 'redirect': '/system/@word(2, 4)/index', 15 | 'children': [] 16 | } 17 | 18 | export const menuMockApi = ({ body = {} }) => generatePageData(body, () => MenuResult) 19 | 20 | export const menuBuildMockApi = () => buildMenus 21 | -------------------------------------------------------------------------------- /src/api/_system/model/menuModel.ts: -------------------------------------------------------------------------------- 1 | import type { DateLogModel } from '@/api/model/baseModel' 2 | import type { ButtonEnum } from '@/enums/permissionEnum' 3 | import type { AppRouteConfig } from '@/router/types' 4 | 5 | export interface BuildMenuModel 6 | extends Omit { 7 | name: Nullable 8 | component: string 9 | redirect?: Nullable 10 | permissions: ButtonEnum[] 11 | children?: BuildMenuModel[] 12 | } 13 | 14 | export interface MenuModel 15 | extends Omit, 16 | DateLogModel { 17 | title: string 18 | order: number 19 | parentId: number 20 | icon: string 21 | leaf: boolean 22 | [key: string]: any 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/message.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: zhoutao 3 | * @Date: 2021-01-25 15:34:15 4 | * @LastEditors: zhoutao 5 | * @LastEditTime: 2021-03-30 13:58:54 6 | * @Description: 错误码提示 7 | */ 8 | import { useMessage } from '@h/web/useMessage' 9 | import { ErrorCodeEnum } from '@/enums/httpEnum' 10 | 11 | // 后端逻辑错误码展示格式(B+xxx) 12 | export type ErrorCode_B = `${ErrorCodeEnum.B}${number}` 13 | 14 | /** 15 | * @description: 错误码消息提示 16 | * @param {ERR_CODE} code 17 | * @param {String} msg 18 | */ 19 | export const alertErrMsg: ( 20 | code: ErrorCodeEnum | ErrorCode_B, 21 | msg: UnDefable 22 | ) => void = (code = ErrorCodeEnum.C100, msg = '无异常') => { 23 | const { $message } = useMessage() 24 | $message.error(`ERROR ${code}: ${msg}`) 25 | } 26 | -------------------------------------------------------------------------------- /src/directives/echarts.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | import type { ChartDataset, ECOption } from '@/components/ECharts/src/useECharts' 3 | import echarts, { initChart, setData } from '@/components/ECharts/src/useECharts' 4 | 5 | interface ChartOption { 6 | option: ECOption 7 | data?: ChartDataset 8 | } 9 | 10 | export const vChart: ObjectDirective = { 11 | mounted(el, { value: { option, data } }) { 12 | const instance = initChart(el, option) 13 | if (data) { 14 | watch(data, (v) => { 15 | setData(instance, v) 16 | }, { 17 | deep: true, 18 | immediate: true 19 | }) 20 | } 21 | }, 22 | 23 | unmounted(el) { 24 | echarts.dispose(el) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/layouts/admin/sider/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | -------------------------------------------------------------------------------- /mock/production-inject.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | import mockTemplates from '.' 3 | 4 | export default (base: string) => { 5 | Object.entries(mockTemplates).forEach(([url, template]) => { 6 | const wrapData = data => ({ 7 | code: 0, 8 | message: 'mock production inject response successful', 9 | data 10 | }) 11 | 12 | Mock.mock( 13 | `${base}${url}`, 14 | typeof template === 'function' 15 | ? (options) => { 16 | try { 17 | options.body = JSON.parse(options.body) 18 | } 19 | catch (error) { 20 | // do nothing 21 | } 22 | return wrapData(Mock.mock(template(options))) 23 | } 24 | : wrapData(template)) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Pie/option.ts: -------------------------------------------------------------------------------- 1 | import type { ECOption } from '../useECharts' 2 | 3 | export const option: ECOption = { 4 | color: ['#f59e0b', '#f43f5e', '#10b981', '#6366f1'], 5 | legend: { 6 | bottom: '10%', 7 | left: 'center', 8 | textStyle: { 9 | color: '#6b7280' 10 | } 11 | }, 12 | series: [ 13 | { 14 | type: 'pie', 15 | seriesLayoutBy: 'row', 16 | radius: ['35%', '65%'], 17 | center: ['50%', '40%'], 18 | avoidLabelOverlap: false, 19 | label: { 20 | show: false, 21 | position: 'center' 22 | }, 23 | emphasis: { 24 | label: { 25 | show: true, 26 | fontSize: 20, 27 | fontWeight: 'bold' 28 | } 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/icons/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/_system/user.ts: -------------------------------------------------------------------------------- 1 | import type { ListQuery, ListResult } from '../model/baseModel' 2 | import type { UserInfoModel } from './model/userModel' 3 | import { useFetch } from '@/utils/http' 4 | 5 | export enum Api { 6 | ACCOUNT_INFO = '/sysadmin/user/info', 7 | USER_LIST = '/sysadmin/user/list', 8 | USER_PRE = '/sysadmin/user', 9 | USER_ROLE_PRE = '/sysadmin/user/role', 10 | UPDATE_USER_PASSWORD = '/sysadmin/user/changePassword' 11 | } 12 | 13 | export const getAccountInfo = () => { 14 | return useFetch.GET({ 15 | url: Api.ACCOUNT_INFO, 16 | useMock: true 17 | }) 18 | } 19 | 20 | export const getUserList = (data: ListQuery) => { 21 | return useFetch.POST>({ 22 | url: Api.USER_LIST, 23 | useMock: true, 24 | data 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/layouts/admin/sider/components/LogoView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { registerGlobComp } from '@c/register' 3 | import App from './App.vue' 4 | import { setupI18n } from '@/locales' 5 | import { setupStore } from '@/store' 6 | import { router, setupRouter } from '@/router' 7 | import { setupRouterGuard } from '@/router/guard' 8 | import { setupDirective } from '@/directives' 9 | import 'uno:components.css' 10 | import 'uno.css' 11 | import '@/styles/common/index.scss' 12 | import 'uno:utilities.css' 13 | 14 | const app = createApp(App) 15 | 16 | // 配置国际化 17 | setupI18n(app) 18 | 19 | // 注册全局组件或样式引入 20 | registerGlobComp(app) 21 | 22 | // 配置 store 23 | setupStore(app) 24 | 25 | // 配置路由 26 | setupRouter(app) 27 | 28 | // 配置路由守卫 29 | setupRouterGuard(router) 30 | 31 | // 配置全局指令 32 | setupDirective(app) 33 | 34 | app.mount('#app') 35 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Line/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/home.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/layouts/admin/index.vue' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | const HomeRoute: AppRouteConfig = { 5 | path: '/home', 6 | name: 'home', 7 | component: AdminLayout, 8 | redirect: '/home/dashboard', 9 | meta: { 10 | title: 'menu.home.root', 11 | icon: 'ep:home-filled' 12 | }, 13 | children: [{ 14 | path: 'dashboard', 15 | name: 'dashboard', 16 | component: () => import('@/views/admin/home/dashboard/index.vue'), 17 | meta: { 18 | title: 'menu.home.dashboard' 19 | } 20 | }, { 21 | path: 'workbench', 22 | name: 'workbench', 23 | component: () => import('@/views/admin/home/workbench/index.vue'), 24 | meta: { 25 | title: 'menu.home.workbench' 26 | } 27 | }] 28 | } 29 | 30 | export default HomeRoute 31 | -------------------------------------------------------------------------------- /src/views/admin/feat/image-preview/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/components/Guide/src/GuideStep.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /src/views/admin/_system/post/usePage.ts: -------------------------------------------------------------------------------- 1 | import type { SearchItemConfig } from '@/components/SearchModel' 2 | import { useComponent } from '@/components/SearchModel' 3 | 4 | const { ElInput } = useComponent() 5 | 6 | export enum SubmitTypeEnum { 7 | ADD = '新增', 8 | UPDATE = '编辑' 9 | } 10 | 11 | // search model config 12 | export const config: SearchItemConfig[] = [ 13 | { component: ElInput, label: '名称', field: 'name', placeholder: '请输入' } 14 | ] 15 | 16 | // table model static column config 17 | export const staticColumns = [ 18 | { fixed: true, type: 'selection', width: '50' }, 19 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 20 | { prop: 'name', label: '名称', width: '180' }, 21 | { prop: 'code', label: '代码', width: '140' }, 22 | { prop: 'description', label: '描述' }, 23 | { prop: 'createdBy', label: '创建人' }, 24 | { prop: 'createdTime', label: '创建时间' } 25 | ] 26 | -------------------------------------------------------------------------------- /src/views/admin/_system/role/usePage.ts: -------------------------------------------------------------------------------- 1 | import type { SearchItemConfig } from '@/components/SearchModel' 2 | import { useComponent } from '@/components/SearchModel' 3 | 4 | const { ElInput } = useComponent() 5 | 6 | export enum SubmitTypeEnum { 7 | ADD = '新增', 8 | UPDATE = '编辑' 9 | } 10 | 11 | // search model config 12 | export const config: SearchItemConfig[] = [ 13 | { component: ElInput, label: '名称', field: 'name', placeholder: '请输入' } 14 | ] 15 | 16 | // table model static column config 17 | export const staticColumns = [ 18 | { fixed: true, type: 'selection', width: '50' }, 19 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 20 | { prop: 'name', label: '名称', width: '180' }, 21 | { prop: 'code', label: '代码', width: '140' }, 22 | { prop: 'description', label: '描述' }, 23 | { prop: 'createdBy', label: '创建人' }, 24 | { prop: 'createdTime', label: '创建时间' } 25 | ] 26 | -------------------------------------------------------------------------------- /src/views/admin/_system/department/usePage.ts: -------------------------------------------------------------------------------- 1 | import type { SearchItemConfig } from '@/components/SearchModel' 2 | import { useComponent } from '@/components/SearchModel' 3 | 4 | const { ElInput } = useComponent() 5 | 6 | export enum SubmitTypeEnum { 7 | ADD = '新增', 8 | UPDATE = '编辑' 9 | } 10 | 11 | // search model config 12 | export const config: SearchItemConfig[] = [ 13 | { component: ElInput, label: '名称', field: 'name', placeholder: '请输入' } 14 | ] 15 | 16 | // table model static column config 17 | export const staticColumns = [ 18 | { fixed: true, type: 'selection', width: '50' }, 19 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 20 | { prop: 'name', label: '名称', width: '180' }, 21 | { prop: 'code', label: '代码', width: '140' }, 22 | { prop: 'description', label: '描述' }, 23 | { prop: 'createdBy', label: '创建人' }, 24 | { prop: 'createdTime', label: '创建时间' } 25 | ] 26 | -------------------------------------------------------------------------------- /plop/store/prompt.js: -------------------------------------------------------------------------------- 1 | import { notEmpty } from '../utils.js' 2 | 3 | export default { 4 | description: '生成store基础模板', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: '请输入module名称', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'list', 13 | name: 'persist', 14 | message: '请选择持久化方式', 15 | choices: [ 16 | { name: 'no persist', value: 'no' }, 17 | { name: 'session storage', value: 'session' }, 18 | { name: 'local storage', value: 'local' } 19 | ], 20 | default: 'no' 21 | }], 22 | actions({ name, persist }) { 23 | const actions = [{ 24 | type: 'add', 25 | path: `src/store/modules/${name}.ts`, 26 | templateFile: 'plop/store/module.hbs', 27 | data: { 28 | name, 29 | persist, 30 | hasPersist: persist !== 'no' 31 | } 32 | } 33 | ] 34 | 35 | return actions 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/composables/web/useCookie.ts: -------------------------------------------------------------------------------- 1 | import type { CookieAttributes } from 'js-cookie' 2 | import Cookie from 'js-cookie' 3 | import type { TokenTypeEnum } from '@/enums/authEnum' 4 | import { isJsonString } from '@/utils/is' 5 | 6 | // 可自行扩展更多 enmu类型 7 | type CookieKey = TokenTypeEnum 8 | 9 | export function createCookie( 10 | key: CookieKey, 11 | value: string, 12 | options?: CookieAttributes 13 | ) { 14 | Cookie.set(key, value, options) 15 | } 16 | 17 | export function removeCookies(keys: CookieKey[] = []) { 18 | keys.forEach((key) => { 19 | Cookie.remove(key) 20 | }) 21 | } 22 | 23 | export function useCookie(key: CookieKey): UnDefable { 24 | const value = Cookie.get(key) 25 | if (typeof value === 'undefined') { 26 | return undefined 27 | } 28 | else if (isJsonString(value)) { 29 | return JSON.parse(value as string) as T 30 | } 31 | else { 32 | return value as unknown as T 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/composables/core/useContext.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InjectionKey, 3 | UnwrapRef 4 | } from 'vue' 5 | import { 6 | inject, 7 | provide, 8 | reactive, 9 | readonly as defineReadonly 10 | } from 'vue' 11 | 12 | interface ContextOptions { 13 | readonly?: boolean 14 | native?: boolean 15 | } 16 | 17 | type ShallowUnwrap = { 18 | [P in keyof T]: UnwrapRef 19 | } 20 | 21 | export function createContext( 22 | key: InjectionKey = Symbol('inject_key'), 23 | context: any, 24 | options: ContextOptions = {} 25 | ) { 26 | const { readonly = true, native = false } = options 27 | const state = reactive(context) 28 | const provideData = readonly ? defineReadonly(state) : state 29 | provide(key, native ? context : provideData) 30 | } 31 | 32 | export function useContext( 33 | key: InjectionKey = Symbol('inject_key'), 34 | defaultValue?: any 35 | ): ShallowUnwrap { 36 | return inject(key, defaultValue || {}) 37 | } 38 | -------------------------------------------------------------------------------- /src/composables/web/useFullScreen.ts: -------------------------------------------------------------------------------- 1 | import { useMessage } from '@h/web/useMessage' 2 | 3 | const Full_Screen_Event = 'fullscreenchange' 4 | 5 | export const isSupported = document.fullscreenEnabled 6 | 7 | export const isFullScreen = ref(false) 8 | 9 | export function toggleFullScreen() { 10 | if (!isSupported) { 11 | const { $message } = useMessage() 12 | $message.warning('sorry, current browser does not supported!') 13 | return 14 | } 15 | 16 | // 监听点击与快捷键触发的全屏事件 17 | document.addEventListener(Full_Screen_Event, setFullScreenVal) 18 | if (isFullScreen.value) { 19 | document.exitFullscreen() 20 | } 21 | else { 22 | document.body.requestFullscreen() 23 | } 24 | } 25 | 26 | export function autoRemoveListener() { 27 | onBeforeUnmount(() => { 28 | document.removeEventListener(Full_Screen_Event, setFullScreenVal) 29 | }) 30 | } 31 | 32 | function setFullScreenVal() { 33 | isFullScreen.value = !isFullScreen.value 34 | } 35 | -------------------------------------------------------------------------------- /src/views/admin/component/verification-code/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "useDefineForClassFields": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noEmit": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "isolatedModules": true, 15 | "lib": ["esnext", "dom"], 16 | "types": ["vite/client"], 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["src/*"], 20 | "@c/*": ["src/components/*"], 21 | "@h/*": ["src/composables/*"], 22 | "#/*": ["types/*"] 23 | } 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.d.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue", 30 | "types/**/*.d.ts", 31 | "types/**/*.ts", 32 | "build/**/*.ts", 33 | "vite.config.ts" 34 | ], 35 | "exclude": ["node_modules", "dist", "**/*.js"] 36 | } 37 | -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import type { Mock } from './types' 2 | 3 | // mock api 4 | import { MockApi } from './api' 5 | 6 | // mock methods 7 | import { authMockApi } from './modules/auth' 8 | import { userMockApi, userInfoMockApi } from './modules/user' 9 | import { menuMockApi, menuBuildMockApi } from './modules/menu' 10 | import { roleMockApi } from './modules/role' 11 | import { dictMockApi } from './modules/dict' 12 | import { deptMockApi } from './modules/department' 13 | import { postMockApi } from './modules/post' 14 | 15 | // mock entry 16 | const mockTemplates: Mock = { 17 | [MockApi.AUTH]: authMockApi, 18 | [MockApi.USER_LIST]: userMockApi, 19 | [MockApi.USER_INFO]: userInfoMockApi, 20 | [MockApi.MENU_LIST]: menuMockApi, 21 | [MockApi.MENU_BUILD]: menuBuildMockApi, 22 | [MockApi.ROLE_LIST]: roleMockApi, 23 | [MockApi.DICT_LIST]: dictMockApi, 24 | [MockApi.DEPT_LIST]: deptMockApi, 25 | [MockApi.POST_LIST]: postMockApi 26 | } 27 | 28 | export default mockTemplates 29 | -------------------------------------------------------------------------------- /src/layouts/admin/header/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/List/src/List.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/utils/consoler.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleThemeEnum } from '@/enums/consoleEnum' 2 | 3 | const BASIC_STYLE = 'border-radius: 0 20px 20px 0; color: #f4f4f5; font-weight: 600' 4 | 5 | // 自定义 console 6 | export class Consoler { 7 | static INFO(type: string, content: any) { 8 | console.log( 9 | `%c [${type}] `, 10 | `${ConsoleThemeEnum.INFO}${BASIC_STYLE}`, 11 | content 12 | ) 13 | } 14 | 15 | static SUCCESS(type: string, content: any) { 16 | console.log( 17 | `%c [${type}] `, 18 | `${ConsoleThemeEnum.SUCCESS}${BASIC_STYLE}`, 19 | content 20 | ) 21 | } 22 | 23 | static WARN(type: string, content: any) { 24 | console.log( 25 | `%c [${type}] `, 26 | `${ConsoleThemeEnum.WARN}${BASIC_STYLE}`, 27 | content 28 | ) 29 | } 30 | 31 | static ERROR(type: string, content: any) { 32 | console.log( 33 | `%c [${type}] `, 34 | `${ConsoleThemeEnum.ERROR}${BASIC_STYLE}`, 35 | content 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/layouts/admin/sider/components/menu/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | -------------------------------------------------------------------------------- /src/utils/http/index.ts: -------------------------------------------------------------------------------- 1 | import config from '@/config' 2 | import { 3 | ContentTypeEnum, 4 | ErrorCodeEnum, 5 | HttpMethodEnum, 6 | ResponseTypeEnum 7 | } from '@/enums/httpEnum' 8 | import { CustomAxios } from '@/utils/http/Axios' 9 | import { getDefaultBaseURL } from '@/utils/http/helper' 10 | import { ErrorMsgMap } from '@/utils/http/checkStatus' 11 | 12 | // 默认请求配置 (优先级低于实际 api请求内 config) 13 | export const useFetch = new CustomAxios({ 14 | // 请求前缀 15 | baseURL: getDefaultBaseURL(), 16 | 17 | // 限制超时时长 18 | timeout: config.HTTP.timeout, 19 | 20 | // 超时错误提示 21 | timeoutErrorMessage: ErrorMsgMap.get(ErrorCodeEnum.A200), 22 | 23 | // 请求类型 24 | method: HttpMethodEnum.GET, 25 | 26 | // 请求头 27 | headers: { 28 | 'Content-Type': ContentTypeEnum.JSON 29 | }, 30 | 31 | // 是否携带token 32 | withToken: true, 33 | 34 | // 是否进行响应数据转换 35 | isTransformResponse: true, 36 | 37 | // 响应体类型 38 | responseType: ResponseTypeEnum.JSON, 39 | 40 | // 忽略 CancelToken 41 | ignoreCancelToken: false 42 | }) 43 | -------------------------------------------------------------------------------- /plop/page/prompt.js: -------------------------------------------------------------------------------- 1 | import { notEmpty } from '../utils.js' 2 | 3 | export default { 4 | description: '生成页面基础模版', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: '请输入页面名称(eg. user)', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'input', 13 | name: 'api', 14 | message: '请输入列表请求方法名(eg. getUserList)', 15 | default: 'YourApi' 16 | }, 17 | { 18 | type: 'input', 19 | name: 'model', 20 | message: '请输入列表数据类型名(eg. UserInfoModel)', 21 | default: 'YourModel' 22 | }, { 23 | type: 'confirm', 24 | name: 'isSystemPage', 25 | message: '是否为系统设置功能页面', 26 | default: false 27 | }], 28 | actions({ name, api, model, isSystemPage }) { 29 | const actions = [{ 30 | type: 'add', 31 | path: `src/views/admin/${isSystemPage ? '_sys/' : ''}${name}/index.vue`, 32 | templateFile: 'plop/page/page.hbs', 33 | data: { 34 | name, 35 | api, 36 | model 37 | } 38 | }] 39 | return actions 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mock/static/menus.ts: -------------------------------------------------------------------------------- 1 | export const buildMenus = [ 2 | { 3 | id: 1, 4 | title: 'menu.dynamic.root', 5 | path: '/dynamic', 6 | name: 'Dynamic', 7 | icon: 'ep:promotion', 8 | leaf: false, 9 | order: 1, 10 | component: 'Layout', 11 | parentId: null, 12 | redirect: '/dynamic/first', 13 | children: [ 14 | { 15 | id: 2, 16 | title: 'menu.dynamic.first', 17 | path: 'first', 18 | name: 'First', 19 | icon: '', 20 | leaf: true, 21 | order: 1, 22 | component: 'dynamic/first/index', 23 | parentId: null, 24 | children: [], 25 | permissions: ['add', 'update', 'delete'] 26 | }, 27 | { 28 | id: 3, 29 | title: 'menu.dynamic.second', 30 | path: 'second', 31 | name: 'Second', 32 | icon: '', 33 | leaf: true, 34 | order: 1, 35 | component: 'dynamic/second/index', 36 | parentId: null, 37 | children: [], 38 | permissions: ['update'] 39 | } 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /src/components/SearchModel/src/useComponent.ts: -------------------------------------------------------------------------------- 1 | import { ElInput, ElRadioGroup, ElRadio, ElRadioButton, ElSelect } from 'element-plus' 2 | import 'element-plus/theme-chalk/el-input.css' 3 | import 'element-plus/theme-chalk/el-select.css' 4 | import 'element-plus/theme-chalk/el-radio.css' 5 | import 'element-plus/theme-chalk/el-radio-button.css' 6 | 7 | export type ComponentType = 8 | typeof ElInput | 9 | typeof ElSelect | 10 | typeof ElRadio | 11 | typeof ElRadioButton 12 | 13 | export enum ComponentName { 14 | ElSelect = 'ElSelect', 15 | ElRadio = 'ElRadio', 16 | ElRadioButton = 'ElRadioButton' 17 | } 18 | 19 | export const isRadio = (component: ComponentType) => 20 | component.name === ComponentName.ElRadio 21 | || component.name === ComponentName.ElRadioButton 22 | 23 | // 组件转化 24 | export const getElComponent = (component: ComponentType) => { 25 | if (isRadio(component)) return ElRadioGroup 26 | return component || 'div' 27 | } 28 | 29 | export const useComponent = () => ({ 30 | ElInput, 31 | ElSelect, 32 | ElRadio, 33 | ElRadioButton 34 | }) 35 | -------------------------------------------------------------------------------- /src/enums/httpEnum.ts: -------------------------------------------------------------------------------- 1 | export enum HttpMethodEnum { 2 | GET = 'GET', 3 | POST = 'POST', 4 | PUT = 'PUT', 5 | DELETE = 'DELETE' 6 | } 7 | 8 | export enum ErrorCodeEnum { 9 | A100 = 'A100', // HTTP发送失败(axios内部未知 Error) 10 | A200 = 'A200', // HTTP无响应 (超时) 11 | B = 'B', // HTTP接口状态码异常(code不为0) 12 | H400 = 'H400', // HTTP响应异常(400、401、403、404、500...) 13 | H401 = 'H401', 14 | H403 = 'H403', 15 | H404 = 'H404', 16 | H405 = 'H405', 17 | H408 = 'H408', 18 | H500 = 'H500', 19 | H502 = 'H502', 20 | H504 = 'H504', 21 | H505 = 'H505', 22 | C100 = 'C100' // 客户端内部程序错误 23 | } 24 | 25 | export enum CodeEnum { 26 | SUCCESS = 0 27 | } 28 | 29 | export enum ContentTypeEnum { 30 | JSON = 'application/json;charset=UTF-8', 31 | FORM_DATA = 'multipart/form-data;charset=UTF-8', 32 | FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8' 33 | } 34 | 35 | export enum ResponseTypeEnum { 36 | JSON = 'json', 37 | TEXT = 'text', 38 | BLOB = 'blob', 39 | STREAM = 'stream', 40 | DOCUMENT = 'document', 41 | ARRAY_BUFFER = 'arraybuffer' 42 | } 43 | -------------------------------------------------------------------------------- /src/views/admin/_system/dict/usePage.ts: -------------------------------------------------------------------------------- 1 | import type { SearchItemConfig } from '@/components/SearchModel' 2 | import { useComponent } from '@/components/SearchModel' 3 | 4 | const { ElInput } = useComponent() 5 | 6 | export enum SubmitTypeEnum { 7 | ADD = '新增', 8 | UPDATE = '编辑' 9 | } 10 | 11 | // search model config 12 | export const config: SearchItemConfig[] = [ 13 | { component: ElInput, label: '字典名称', field: 'name', placeholder: '请输入' }, 14 | { component: ElInput, label: '字典值', field: 'code', placeholder: '请输入' } 15 | ] 16 | 17 | // table model static column config 18 | export const staticColumns = [ 19 | { fixed: true, type: 'selection', width: '50' }, 20 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 21 | { prop: 'typeName', label: '字典类型', width: '100' }, 22 | { prop: 'typeCode', label: '类型代码', width: '120' }, 23 | { prop: 'name', label: '字典名称', width: '100' }, 24 | { prop: 'code', label: '字典值', width: '80' }, 25 | { prop: 'description', label: '描述' }, 26 | { prop: 'createdBy', label: '创建人' }, 27 | { prop: 'createdTime', label: '创建时间' } 28 | ] 29 | -------------------------------------------------------------------------------- /src/components/SearchTree/src/SearchTree.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /src/components/Splitter/src/Trigger.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | -------------------------------------------------------------------------------- /plop/component/prompt.js: -------------------------------------------------------------------------------- 1 | import { notEmpty } from '../utils.js' 2 | 3 | export default { 4 | description: '生成组件基础模版', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: '请输入组件名称', 9 | validate: notEmpty('name') 10 | }, { 11 | type: 'confirm', 12 | name: 'multiple', 13 | message: '是否新建组件目录', 14 | default: false 15 | }], 16 | actions({ name, multiple }) { 17 | const defaultActions = [{ 18 | type: 'add', 19 | path: `src/components/${name}.vue`, 20 | templateFile: 'plop/component/component.hbs', 21 | data: { 22 | name 23 | } 24 | }] 25 | const multipleActions = [{ 26 | type: 'add', 27 | path: `src/components/${name}/src/${name}.vue`, 28 | templateFile: 'plop/component/component.hbs', 29 | data: { 30 | name 31 | } 32 | }, { 33 | type: 'add', 34 | path: `src/components/${name}/index.ts`, 35 | templateFile: 'plop/component/index.hbs', 36 | data: { 37 | name 38 | } 39 | }] 40 | return multiple ? multipleActions : defaultActions 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /types/http.d.ts: -------------------------------------------------------------------------------- 1 | import type { HttpMethodEnum, ResponseTypeEnum } from '@/enums/httpEnum' 2 | import type { 3 | AxiosError, 4 | AxiosRequestConfig, 5 | RawAxiosRequestHeaders, 6 | InternalAxiosRequestConfig 7 | } from 'axios' 8 | 9 | export type ResponseError = Error | AxiosError 10 | 11 | export interface RequestHeaders extends RawAxiosRequestHeaders { 12 | Authorization?: string 13 | appKey?: string 14 | } 15 | 16 | export interface RequestConfig extends AxiosRequestConfig { 17 | url: string 18 | method?: HttpMethodEnum 19 | data?: T 20 | params?: T 21 | headers?: RequestHeaders 22 | withToken?: boolean 23 | isTransformResponse?: boolean 24 | responseType?: ResponseTypeEnum 25 | ignoreCancelToken?: boolean 26 | useMock?: boolean 27 | } 28 | 29 | export interface InternalRequestConfig extends InternalAxiosRequestConfig { 30 | withToken?: boolean 31 | isTransformResponse?: boolean 32 | responseType?: ResponseTypeEnum 33 | ignoreCancelToken?: boolean 34 | useMock?: boolean 35 | } 36 | 37 | export interface Result { 38 | data: T 39 | code: number 40 | message?: string 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ThemeSwitch/src/ThemeSwitch.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /src/views/admin/component/split-pane/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT toryz 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/router/guard/permissionGuard.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import { useUserStore } from '@/store/modules/user' 3 | import { useMenuStore } from '@/store/modules/menu' 4 | import { 5 | addAsyncRoutes, 6 | checkAccessToken, 7 | isRequiresAuthRoute 8 | } from '@/router/helper' 9 | 10 | export const createPermissionGuard = (router: Router) => { 11 | router.beforeEach(async (to, from, next) => { 12 | const userStore = useUserStore() 13 | const menuStore = useMenuStore() 14 | // fix async route 404 after refresh page 15 | const nextRoute = to.matched[0].name === 'PageNotFound' ? { path: to.fullPath } : to 16 | if (isRequiresAuthRoute(to)) { 17 | if (!checkAccessToken()) { 18 | await userStore.reLogin() 19 | await addAsyncRoutes() 20 | next({ replace: true, ...nextRoute }) 21 | } 22 | else { 23 | userStore.invalid && (await userStore.setUserInfo()) 24 | if (!menuStore.hasRoutes) { 25 | await addAsyncRoutes() 26 | next({ replace: true, ...nextRoute }) 27 | } 28 | else { 29 | next() 30 | } 31 | } 32 | } 33 | else { 34 | next() 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/composables/logic/useMenu.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordName } from 'vue-router' 2 | import { ElSubMenu, ElMenuItem } from 'element-plus' 3 | import type { AppRouteConfig } from '@/router/types' 4 | 5 | export type MenuItemComponent = typeof ElSubMenu | typeof ElMenuItem 6 | 7 | export interface Menu { 8 | component: MenuItemComponent 9 | title: string 10 | index: RouteRecordName | undefined 11 | icon?: string | undefined 12 | children?: Menu[] 13 | } 14 | 15 | export const resolveFullPath = (route: AppRouteConfig): string => { 16 | if (!route.path) return '' 17 | const router = useRouter() 18 | const resolvedRoute = router.resolve(route) 19 | return resolvedRoute.fullPath 20 | } 21 | 22 | export const routeToMenu = (routes: AppRouteConfig[]): Menu[] => { 23 | return routes.filter(route => !route?.meta?.hide).map((r) => { 24 | const childMenu = r.children && r.children.filter(child => !child.meta?.hideMenu) 25 | return { 26 | title: r?.meta?.title || 'title', 27 | index: resolveFullPath(r) || r.name, 28 | component: childMenu?.length ? ElSubMenu : ElMenuItem, 29 | icon: r?.meta?.icon, 30 | ...(childMenu?.length && { children: routeToMenu(childMenu) }) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/feat.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/layouts/admin/index.vue' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | const FeatRoute: AppRouteConfig = { 5 | path: '/feat', 6 | name: 'feat', 7 | component: AdminLayout, 8 | meta: { 9 | title: 'menu.feat.root', 10 | icon: 'ri:rocket-2-fill' 11 | }, 12 | children: [{ 13 | path: 'guide', 14 | name: 'guide', 15 | component: () => import('@/views/admin/feat/guide/index.vue'), 16 | meta: { 17 | title: 'menu.feat.guide' 18 | } 19 | }, 20 | { 21 | path: 'watermark', 22 | name: 'watermark', 23 | component: () => import('@/views/admin/feat/watermark/index.vue'), 24 | meta: { 25 | title: 'menu.feat.watermark' 26 | } 27 | }, 28 | { 29 | path: 'image-preview', 30 | name: 'image_preview', 31 | component: () => import('@/views/admin/feat/image-preview/index.vue'), 32 | meta: { 33 | title: 'menu.feat.imagePreview' 34 | } 35 | }, 36 | { 37 | path: 'lazy', 38 | name: 'lazy', 39 | component: () => import('@/views/admin/feat/lazy/index.vue'), 40 | meta: { 41 | title: 'menu.feat.lazyLoad' 42 | } 43 | }] 44 | } 45 | 46 | export default FeatRoute 47 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/ThemeSetting/src/ThemeColorPicker.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | 46 | 48 | -------------------------------------------------------------------------------- /src/components/VerificationCode/src/VerifyDialog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { I18nOptions } from 'vue-i18n' 3 | import { createI18n } from 'vue-i18n' 4 | 5 | export interface LocaleType { 6 | name: string 7 | value: string 8 | } 9 | 10 | type LocaleModules = Record 11 | 12 | export const i18n = createI18n({ 13 | legacy: false, 14 | locale: 'zh-cn', 15 | fallbackLocale: 'en', 16 | messages: getLocaleData() 17 | }) 18 | 19 | export function setupI18n(app: App) { 20 | app.use(i18n) 21 | } 22 | 23 | function getLocaleData(): I18nOptions['messages'] { 24 | const modules: LocaleModules = import.meta.glob('./lang/*.ts', { eager: true }) 25 | const lang: Record = {} 26 | for (const path in modules) { 27 | const name: string = path.match(/lang\/(\S*).ts/)![1] 28 | lang[name] = modules[path].default 29 | } 30 | return lang 31 | } 32 | 33 | export function getLocaleTypes(): LocaleType[] { 34 | const modules: LocaleModules = import.meta.glob('./lang/*.ts', { eager: true }) 35 | const localeTypes: LocaleType[] = [] 36 | for (const path in modules) { 37 | const value: string = path.match(/lang\/(\S*).ts/)![1] 38 | localeTypes.push({ 39 | value, 40 | name: modules[path].default.name 41 | }) 42 | } 43 | return localeTypes 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Card/src/TotalCard.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /src/components/TableModel/src/useColumn.ts: -------------------------------------------------------------------------------- 1 | import type { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults' 2 | import type { ButtonProps, TagProps } from 'element-plus' 3 | import { ElButton, ElTag } from 'element-plus' 4 | 5 | export interface ColumnAttrs { 6 | row: T 7 | column: TableColumnCtx 8 | rowIndex: number 9 | } 10 | 11 | export type SlotButtonProps = Partial 12 | 13 | export type SlotTagProps = Partial & { style?: Partial } 14 | 15 | export const useSlotButton = (text: string, onClick?: () => void, props: SlotButtonProps = {}) => { 16 | const defaultProps: SlotButtonProps = { 17 | link: true, 18 | type: 'primary', 19 | size: 'small' 20 | } 21 | return h( 22 | ElButton, 23 | { 24 | ...defaultProps, 25 | ...props, 26 | onClick 27 | }, 28 | { 29 | default: () => text || '按钮' 30 | } 31 | ) 32 | } 33 | 34 | export const useSlotTag = (text: string, onClick?: () => void, props: SlotTagProps = {}) => { 35 | const defaultProps: SlotTagProps = { 36 | size: 'default' 37 | } 38 | return h( 39 | ElTag, 40 | { 41 | ...defaultProps, 42 | ...props, 43 | onClick 44 | }, 45 | { 46 | default: () => text || '标签' 47 | } 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/icons/coffee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | interface AppState { 5 | visitedViews: AppRouteConfig[] 6 | cachedViews: AppRouteConfig[] 7 | } 8 | 9 | export const useAppStore = defineStore('app', { 10 | state: (): AppState => ({ 11 | visitedViews: [], 12 | cachedViews: [] 13 | }), 14 | actions: { 15 | addVisitedView(view: AppRouteConfig) { 16 | if (this.visitedViews.every(r => r.path !== view.path)) { 17 | this.visitedViews.push(view) 18 | return true 19 | } 20 | else { 21 | return false 22 | } 23 | }, 24 | 25 | deleteVisitedView(view: AppRouteConfig) { 26 | const viewIndex = this.visitedViews.findIndex( 27 | ({ path }) => path === view.path 28 | ) 29 | viewIndex > -1 && this.visitedViews.splice(viewIndex, 1) 30 | }, 31 | 32 | addCachedView(view: AppRouteConfig) { 33 | if (this.cachedViews.every(r => r.path !== view.path)) { 34 | this.cachedViews.push(view) 35 | } 36 | }, 37 | 38 | deleteCachedView(view: AppRouteConfig) { 39 | const viewIndex = this.cachedViews.findIndex( 40 | ({ path }) => path === view.path 41 | ) 42 | viewIndex > -1 && this.cachedViews.splice(viewIndex, 1) 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /src/api/_auth/index.ts: -------------------------------------------------------------------------------- 1 | import { useCookie } from '@h/web/useCookie' 2 | import type { LoginParams, LoginResultModel } from './model' 3 | 4 | // import { ContentTypeEnum } from '@/enums/httpEnum' 5 | import { AuthTypeEnum, GrantTypeEnum, TokenTypeEnum } from '@/enums/authEnum' 6 | import { useFetch } from '@/utils/http' 7 | import config from '@/config' 8 | 9 | export enum Api { 10 | Auth = '/oauth/token' 11 | } 12 | 13 | const createAuthHeader = () => { 14 | const { client_id, client_secret } = config.OAUTH 15 | return { 16 | // 'Content-Type': ContentTypeEnum.FORM_URLENCODED, 17 | Authorization: `${AuthTypeEnum.BASIC} ${window.btoa( 18 | `${client_id}:${client_secret}` 19 | )}` 20 | } 21 | } 22 | 23 | export const loginApi = (data: LoginParams) => { 24 | return useFetch.POST({ 25 | url: Api.Auth, 26 | headers: createAuthHeader(), 27 | withToken: false, 28 | useMock: true, 29 | data: { 30 | ...data, 31 | grant_type: GrantTypeEnum.PASSWORD 32 | } 33 | }) 34 | } 35 | 36 | export const tokenRefresh = () => { 37 | return useFetch.POST({ 38 | url: Api.Auth, 39 | headers: createAuthHeader(), 40 | withToken: false, 41 | useMock: true, 42 | data: { 43 | refresh_token: useCookie(TokenTypeEnum.REFRESH_TOKEN), 44 | grant_type: GrantTypeEnum.REFRESH_TOKEN 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/http/helper.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from 'axios' 2 | import { VITE_BASE_API, VITE_USE_PROXY, VITE_USE_MOCK } from 'vite-env' 3 | import type { RequestConfig, Result } from '#/http' 4 | import { ErrorCodeEnum, HttpMethodEnum, CodeEnum } from '@/enums/httpEnum' 5 | import { alertErrMsg } from '@/utils/message' 6 | import { isDevMode } from '@/utils/env' 7 | 8 | // 生成token 9 | export const generateBaseToken = (token: Nullable) => `bearer ${token}` 10 | 11 | // 获取默认请求前缀地址 12 | export const getDefaultBaseURL = () => { 13 | // 使用proxy或者mock时,无需设置默认请求前缀 14 | if (isDevMode() && (VITE_USE_PROXY || VITE_USE_MOCK)) { 15 | return '' 16 | } 17 | return VITE_BASE_API 18 | } 19 | 20 | // 请求数据转换 21 | export const transformRequest = (config: RequestConfig): RequestConfig => { 22 | const { data, params, method } = config 23 | // GET方法时 params来自 自身 或者 data 24 | if (method === HttpMethodEnum.GET) { 25 | config.params = params || data 26 | config.data = undefined 27 | } 28 | return config 29 | } 30 | 31 | // 响应数据转换 32 | export const transformResponse = ( 33 | res: AxiosResponse, 34 | config: RequestConfig 35 | ) => { 36 | if (!config.isTransformResponse) { 37 | return res.data 38 | } 39 | const { code, data, message: msg } = res.data 40 | if (code === CodeEnum.SUCCESS) { 41 | return data 42 | } 43 | else { 44 | alertErrMsg(`${ErrorCodeEnum.B}${code}`, msg) 45 | throw new Error(code.toString()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/router/helper/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized, RouteRecordName, RouteRecordRaw } from 'vue-router' 2 | import { useCookie } from '@h/web/useCookie' 3 | import { useMenuStore } from '@/store/modules/menu' 4 | import { TokenTypeEnum } from '@/enums/authEnum' 5 | import { basicRoutes } from '@/router/routes' 6 | import { router } from '@/router' 7 | import config from '@/config' 8 | 9 | export function checkAccessToken() { 10 | return !config.OAUTH.execute || useCookie(TokenTypeEnum.ACCESS_TOKEN) 11 | } 12 | 13 | export function isBasicRoute(route: RouteLocationNormalized) { 14 | if (route.matched?.length > 0) { 15 | return basicRoutes.map(r => r.path).includes(route.matched[0]?.path) 16 | } 17 | else { 18 | return false 19 | } 20 | } 21 | 22 | export function isRequiresAuthRoute(route: RouteLocationNormalized) { 23 | const matched = route.matched.filter(r => r.name !== 'PageNotFound') 24 | return !matched.some(r => r.meta?.requiresAuth === false) 25 | } 26 | 27 | export async function addAsyncRoutes() { 28 | const menuStore = useMenuStore() 29 | if (!menuStore.routes?.length) { 30 | await menuStore.generateRoutes() 31 | } 32 | menuStore.routes.forEach((route) => { 33 | const routeName = route.name as RouteRecordName 34 | const hasRoute = router.hasRoute(routeName) 35 | if (hasRoute) router.removeRoute(routeName) // if already exists, remove it before adding 36 | router.addRoute(toRaw(route) as RouteRecordRaw) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/layouts/admin/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 52 | -------------------------------------------------------------------------------- /src/views/admin/_system/menu/usePage.ts: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/vue' 2 | import type { SearchItemConfig } from '@/components/SearchModel' 3 | import { useComponent } from '@/components/SearchModel' 4 | import type { MenuModel } from '@/api/_system/model/menuModel' 5 | import type { ColumnAttrs } from '@/components/TableModel' 6 | import { useSlotTag } from '@/components/TableModel' 7 | 8 | const { ElInput, ElSelect } = useComponent() 9 | 10 | export enum SubmitTypeEnum { 11 | ADD = '新增', 12 | UPDATE = '编辑' 13 | } 14 | 15 | // search model config 16 | export const config: SearchItemConfig[] = [ 17 | { component: ElInput, label: '名称', field: 'title', placeholder: '请输入' }, 18 | { component: ElSelect, label: '类型', field: 'leaf', clearable: true, options: [{ label: '目录', value: false }, { label: '菜单', value: true }] } 19 | ] 20 | 21 | // table model static column config 22 | export const staticColumns = [ 23 | { fixed: true, type: 'selection', width: '50' }, 24 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 25 | { prop: 'title', label: '名称', width: '120' }, 26 | { prop: 'leaf', label: '类型', width: '140', slot: ({ row }: ColumnAttrs) => [useSlotTag(row?.leaf ? '菜单' : '目录')] }, 27 | { prop: 'icon', label: '图标', width: '80', slot: ({ row }: ColumnAttrs) => [h(Icon, { icon: row.icon })] }, 28 | { prop: 'component', label: '组件目录', width: '220' }, 29 | { prop: 'path', label: '路由地址' }, 30 | { prop: 'redirect', label: '重定向' } 31 | ] 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 New feature proposal 2 | description: Suggest an idea for this project 3 | labels: ['enhancement: pending triage'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for your interest in the project and taking the time to fill out this feature report! 9 | 10 | - type: textarea 11 | id: feature-description 12 | attributes: 13 | label: Clear and concise description of the problem 14 | description: 'As a developer using Vue Power Admin, I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!' 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: suggested-solution 19 | attributes: 20 | label: Suggested solution 21 | description: We could provide following implementation... 22 | validations: 23 | required: true 24 | - type: checkboxes 25 | id: checkboxes 26 | attributes: 27 | label: Validations 28 | description: Before submitting the issue, please make sure you do the following 29 | options: 30 | - label: Check that there isn't [already an issue](https://github.com/zhou-tao/vue-power-admin/issues) that reports the same bug to avoid creating a duplicate. 31 | required: true 32 | - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/zhou-tao/vue-power-admin/discussions). 33 | required: true 34 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import NProgress from 'nprogress' 3 | import { createPermissionGuard } from '@/router/guard/permissionGuard' 4 | 5 | // import { isBasicRoute } from '@/router/helper' 6 | import { AxiosCanceler } from '@/utils/http/axiosCancel' 7 | import { useSettingStore } from '@/store/modules/setting' 8 | import config from '@/config' 9 | import { i18n } from '@/locales' 10 | 11 | /** 12 | * @description 设置路由守卫 13 | * @param router 14 | */ 15 | export function setupRouterGuard(router: Router) { 16 | createPermissionGuard(router) 17 | createTitleGuard(router) 18 | createHttpGuard(router) 19 | createNProgressGuard(router) 20 | } 21 | 22 | /** 23 | * @description 动态标题守卫 24 | * @param router 25 | */ 26 | function createTitleGuard(router: Router) { 27 | router.beforeEach((to) => { 28 | document.title = i18n.global.t((to.meta.title || config.APP.title) as string) 29 | }) 30 | } 31 | 32 | /** 33 | * @description 取消上一个页面未完成请求 34 | * @param router 35 | */ 36 | function createHttpGuard(router: Router) { 37 | router.beforeEach(() => { 38 | new AxiosCanceler().removeAllPending() 39 | }) 40 | } 41 | 42 | /** 43 | * @description 进度条守卫 44 | * @param router 45 | */ 46 | function createNProgressGuard(router: Router) { 47 | router.beforeEach(() => { 48 | // !isBasicRoute(from) && NProgress.start() 49 | useSettingStore().hasProgress && NProgress.start() 50 | }) 51 | router.afterEach(() => { 52 | NProgress.done() 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/locales/lang/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: '中文-简体', 3 | home: { 4 | login: '登录', 5 | account: '账号', 6 | password: '密码', 7 | remember: '记住密码', 8 | forgot: '忘记密码?', 9 | moreLoginType: '更多登录方式', 10 | welcome: '尊贵的VIP用户,您已登录成功!', 11 | usernameRule: '请输入账号', 12 | passwordRule: '请输入密码' 13 | }, 14 | header: { 15 | message: '您收到一条临时紧急加班通知 !!', 16 | setup: '账户设置', 17 | logout: '注销登录', 18 | changeLocale: '切换语言为' 19 | }, 20 | tab: { 21 | refresh: '刷新', 22 | close: '关闭当前', 23 | closeOther: '关闭其他' 24 | }, 25 | menu: { 26 | home: { 27 | root: '首页', 28 | dashboard: '仪表盘', 29 | workbench: '工作台' 30 | }, 31 | system: { 32 | root: '系统管理', 33 | menu: '菜单管理', 34 | role: '权限管理', 35 | dict: '字典管理', 36 | user: '用户管理', 37 | userDetail: '用户详情', 38 | department: '部门管理', 39 | post: '岗位管理' 40 | }, 41 | component: { 42 | root: '组件', 43 | form: '表单', 44 | table: '表格', 45 | splitPane: '分栏器', 46 | icon: '图标', 47 | editor: '编辑器', 48 | verificationCode: '验证码' 49 | }, 50 | feat: { 51 | root: '功能', 52 | guide: '页面引导', 53 | watermark: '水印', 54 | imagePreview: '图片预览', 55 | lazyLoad: '懒加载' 56 | }, 57 | dynamic: { 58 | root: '动态路由', 59 | first: '子路由-1', 60 | second: '子路由-2' 61 | }, 62 | personal: '个人中心', 63 | login: '登录', 64 | error: { 65 | notFound: '404 - 页面找不到' 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/locales/lang/zh-tw.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: '中文-繁體', 3 | home: { 4 | login: '登錄', 5 | account: '賬號', 6 | password: '密碼', 7 | remember: '記住密碼', 8 | forgot: '忘記密碼?', 9 | moreLoginType: '更多登錄方式', 10 | welcome: '尊貴的VIP用戶,您已登錄成功!', 11 | usernameRule: '請輸入賬號', 12 | passwordRule: '請輸入密碼' 13 | }, 14 | header: { 15 | message: '您收到一條臨時緊急加班通知 !!', 16 | setup: '賬號設置', 17 | logout: '註銷登錄', 18 | changeLocale: '切換語言為' 19 | }, 20 | tab: { 21 | refresh: '刷新', 22 | close: '關閉當前', 23 | closeOther: '關閉其他' 24 | }, 25 | menu: { 26 | home: { 27 | root: '首頁', 28 | dashboard: '儀錶盤', 29 | workbench: '工作台' 30 | }, 31 | system: { 32 | root: '係統管理', 33 | menu: '菜單管理', 34 | role: '權限管理', 35 | dict: '字典管理', 36 | user: '用戶管理', 37 | userDetail: '用戶詳情', 38 | department: '部門管理', 39 | post: '崗位管理' 40 | }, 41 | component: { 42 | root: '組件', 43 | form: '表單', 44 | table: '表格', 45 | splitPane: '分欄器', 46 | icon: '圖標', 47 | editor: '編輯器', 48 | verificationCode: '驗證碼' 49 | }, 50 | feat: { 51 | root: '功能', 52 | guide: '頁面引導', 53 | watermark: '水印', 54 | imagePreview: '圖片預覽', 55 | lazyLoad: '懶加載' 56 | }, 57 | dynamic: { 58 | root: '動態路由', 59 | first: '子路由-1', 60 | second: '子路由-2' 61 | }, 62 | personal: '個人中心', 63 | login: '登錄', 64 | error: { 65 | notFound: '404 - 頁面找不到' 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/directives/watermark.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | 3 | interface WatermarkerOption { 4 | text: string 5 | color?: string 6 | } 7 | 8 | export const vWatermark: ObjectDirective = { 9 | mounted(el, { value: { text, color } }) { 10 | printWaterMarker(el, text, color) 11 | const watermark = el.style.backgroundImage 12 | const observer = new MutationObserver(([e]) => { 13 | if (e.attributeName === 'style' && (e.target as HTMLElement).style.backgroundImage !== watermark) { 14 | // 防止样式被F12手动更改,赋值为之前的值 15 | el.style.cssText = e.oldValue ?? '' 16 | } 17 | }) 18 | observer.observe(el, { childList: true, subtree: true, attributes: true, attributeOldValue: true }) 19 | } 20 | } 21 | 22 | function printWaterMarker(el: HTMLDivElement, text: string, color = 'rgba(178, 190, 195, 0.3)') { 23 | const canvas = document.createElement('canvas') 24 | document.body.appendChild(canvas) 25 | canvas.width = 140 26 | canvas.height = 80 27 | canvas.style.display = 'none' 28 | const ctx = canvas.getContext('2d')! 29 | ctx.fillStyle = 'rgba(0, 0, 0, 0)' 30 | ctx.fillRect(0, 0, 140, 80) 31 | ctx.font = '24px serif' 32 | ctx.textAlign = 'center' 33 | ctx.textBaseline = 'middle' 34 | ctx.translate(70, 40) 35 | ctx.rotate(-30 * Math.PI / 180) 36 | ctx.fillStyle = color 37 | ctx.fillText(text, 0, 0) 38 | const image = canvas.toDataURL('image/png') 39 | el.style.backgroundImage = `url(${image})` 40 | document.body.removeChild(canvas) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Editor/src/Editor.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | 26 | - name: Install 27 | run: pnpm i 28 | 29 | - name: Lint 30 | run: pnpm lint 31 | 32 | typecheck: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Install pnpm 38 | uses: pnpm/action-setup@v2 39 | 40 | - name: Set node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: 16 44 | 45 | - name: Install 46 | run: pnpm i 47 | 48 | - name: Typecheck 49 | run: pnpm typecheck 50 | 51 | test: 52 | runs-on: ${{ matrix.os }} 53 | 54 | strategy: 55 | matrix: 56 | node: [16.x, 18.x] 57 | os: [ubuntu-latest, windows-latest, macos-latest] 58 | fail-fast: false 59 | 60 | steps: 61 | - uses: actions/checkout@v3 62 | 63 | - name: Install pnpm 64 | uses: pnpm/action-setup@v2 65 | 66 | - name: Set node ${{ matrix.node }} 67 | uses: actions/setup-node@v3 68 | with: 69 | node-version: ${{ matrix.node }} 70 | 71 | - name: Install 72 | run: pnpm i 73 | 74 | - name: Build 75 | run: pnpm build 76 | -------------------------------------------------------------------------------- /src/views/admin/component/icon/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | 37 | 46 | -------------------------------------------------------------------------------- /src/views/admin/feat/guide/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 64 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/component.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/layouts/admin/index.vue' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | const ComponentRoute: AppRouteConfig = { 5 | path: '/component', 6 | name: 'component', 7 | component: AdminLayout, 8 | meta: { 9 | title: 'menu.component.root', 10 | icon: 'ri:dashboard-fill' 11 | }, 12 | children: [{ 13 | path: 'form', 14 | name: 'form', 15 | component: () => import('@/views/admin/component/form/index.vue'), 16 | meta: { 17 | title: 'menu.component.form' 18 | } 19 | }, 20 | { 21 | path: 'table', 22 | name: 'table', 23 | component: () => import('@/views/admin/component/table/index.vue'), 24 | meta: { 25 | title: 'menu.component.table' 26 | } 27 | }, 28 | { 29 | path: 'split-pane', 30 | name: 'split_pane', 31 | component: () => import('@/views/admin/component/split-pane/index.vue'), 32 | meta: { 33 | title: 'menu.component.splitPane' 34 | } 35 | }, 36 | { 37 | path: 'icon', 38 | name: 'icon', 39 | component: () => import('@/views/admin/component/icon/index.vue'), 40 | meta: { 41 | title: 'menu.component.icon' 42 | } 43 | }, 44 | { 45 | path: 'editor', 46 | name: 'editor', 47 | component: () => import('@/views/admin/component/editor/index.vue'), 48 | meta: { 49 | title: 'menu.component.editor' 50 | } 51 | }, 52 | { 53 | path: 'verification-code', 54 | name: 'verification_code', 55 | component: () => import('@/views/admin/component/verification-code/index.vue'), 56 | meta: { 57 | title: 'menu.component.verificationCode' 58 | } 59 | }] 60 | } 61 | 62 | export default ComponentRoute 63 | -------------------------------------------------------------------------------- /src/utils/http/axiosCancel.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig, Canceler } from 'axios' 2 | import axios from 'axios' 3 | import { isFunction } from '@/utils/is' 4 | 5 | // 存储 pending中的请求 6 | let pendingMap = new Map() 7 | 8 | export const getPendingUrl = (config: AxiosRequestConfig) => 9 | [config.method, config.url].join('&') 10 | 11 | /** 12 | * AxiosCanceler 13 | * @description: 作用:1.取消多次重复请求,保留最后一个 2.取消切换路由后上个页面未完成的请求 14 | */ 15 | export class AxiosCanceler { 16 | /** 17 | * @description: 添加 pending状态的请求 18 | * @param {AxiosRequestConfig} config 19 | */ 20 | addPending(config: AxiosRequestConfig) { 21 | // 移除上一个重复请求 22 | this.removePending(config) 23 | const url = getPendingUrl(config) 24 | config.cancelToken 25 | = config.cancelToken 26 | || new axios.CancelToken((cancel) => { 27 | if (!pendingMap.has(url)) { 28 | // If there is no current request in pending, add it 29 | pendingMap.set(url, cancel) 30 | } 31 | }) 32 | } 33 | 34 | /** 35 | * @description: 清除所有 pending请求 36 | */ 37 | removeAllPending() { 38 | pendingMap.forEach((cancel) => { 39 | cancel && isFunction(cancel) && cancel() 40 | }) 41 | pendingMap.clear() 42 | } 43 | 44 | /** 45 | * @description: 移除 pending请求 46 | * @param {Object} config 47 | */ 48 | removePending(config: AxiosRequestConfig) { 49 | const url = getPendingUrl(config) 50 | if (pendingMap.has(url)) { 51 | const cancel = pendingMap.get(url) 52 | cancel && cancel(url) 53 | pendingMap.delete(url) 54 | } 55 | } 56 | 57 | /** 58 | * @description: 重置 pendingMap 59 | */ 60 | reset(): void { 61 | pendingMap = new Map() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Radar/option.ts: -------------------------------------------------------------------------------- 1 | import type { ECOption } from '../useECharts' 2 | 3 | // radar chart is not support dataset now! 4 | export const option: ECOption = { 5 | color: ['#f43f5e', '#6366f1'], 6 | backgroundColor: 'transparent', 7 | tooltip: {}, 8 | radar: { 9 | radius: '70%', 10 | center: ['50%', '45%'], 11 | splitNumber: 5, 12 | axisNameGap: 10, 13 | axisName: { 14 | color: '#6b7280' 15 | }, 16 | axisLine: { 17 | lineStyle: { 18 | color: '#9ca3af40' 19 | } 20 | }, 21 | splitLine: { 22 | lineStyle: { 23 | color: '#9ca3af40' 24 | } 25 | }, 26 | splitArea: { 27 | areaStyle: { 28 | color: 'rgba(127, 95, 132, 0.2)', 29 | opacity: 1, 30 | shadowBlur: 45, 31 | shadowColor: 'rgba(0, 0, 0, 0.5)', 32 | shadowOffsetX: 0, 33 | shadowOffsetY: 15 34 | } 35 | }, 36 | indicator: [{ 37 | name: 'Typescript', 38 | max: 100 39 | }, { 40 | name: 'Vue', 41 | max: 100 42 | }, { 43 | name: 'React', 44 | max: 100 45 | }, { 46 | name: 'Svelte', 47 | max: 100 48 | }, { 49 | name: 'Solid', 50 | max: 100 51 | }, { 52 | name: 'Tauri', 53 | max: 100 54 | }] 55 | }, 56 | series: [{ 57 | name: '熟练度', 58 | type: 'radar', 59 | symbolSize: 0, 60 | areaStyle: { 61 | shadowBlur: 13, 62 | shadowColor: 'rgba(0,0,0,.2)', 63 | shadowOffsetX: 0, 64 | shadowOffsetY: 10, 65 | opacity: 1 66 | }, 67 | data: [{ 68 | value: [80, 90, 80, 85, 60, 80], 69 | name: '2023年' 70 | }, { 71 | value: [90, 95, 45, 70, 80, 70], 72 | name: '2022年' 73 | }] 74 | }] 75 | } 76 | -------------------------------------------------------------------------------- /src/views/admin/component/form/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue-Power-Admin


3 |

4 | release version 5 | deploy status 6 |

7 |

Vue Power Admin

8 |
9 | 10 | [English](./README.md) | **简体中文** 11 | 12 | ### :loudspeaker: 简介 13 | 14 | > [Vue Power Admin](https://vue-power-admin.netlify.app) 是一个标准的中后台前端开发模板。基于当下最流行的 Vue3 + Typescript 技术栈构建,使用 Element-Plus 作为 UI 库。 15 | 16 | ### :rocket: 特性 17 | 18 | - **技术栈** 采用当下最流行的 Vite + Vue3 + Typescript 技术栈组合。 19 | - **主题** 可配置的运行时多主题。 20 | - **国际化** 内置主流的国际化方案。 21 | - **多布局** 封装多种经典布局通过配置切换且支持新增自定义布局。 22 | - **权限** 封装多层级权限控制,包括角色权限、菜单权限、按钮权限(在v2版本)。 23 | - **Unocss** 使用最轻量的原子化 CSS 解决方案。 24 | - **组件** 封装大量中后台常用组件。 25 | - **生成器** 集成了代码模版生成器,提高开发效率。 26 | - **规范** 使用 simple-git-hooks 集成 eslint 统一代码规范,搭配 commitlint 校验 git message(内置 cz-git 交互)。 27 | - **模拟数据** 使用 Mockjs 支持开发环境的真实接口以及生产环境的模拟数据注入,告别手动编写数据。 28 | 29 | ### :alarm_clock: 使用 30 | 31 | ```shell 32 | # 选择 `vpa-frontend` 33 | pnpm create vpa vue-power-admin 34 | 35 | # cd vue-power-admin 36 | cd vue-power-admin 37 | 38 | # install dependencies 39 | pnpm i 40 | 41 | # start up 42 | pnpm dev 43 | 44 | ``` 45 | 46 | ### :heart: Contribute 47 | 48 | 非常欢迎您为本仓库提交PR或者Issues! 49 | 50 | 特别感谢所有 [Vue Power Admin](https://github.com/zhou-tao/vue-power-admin/graphs/contributors) 的贡献者! 51 | 52 | ### :bookmark_tabs: License 53 | 54 | [MIT](./LICENSE) License © 2022-PRESENT toryz 55 | -------------------------------------------------------------------------------- /src/locales/lang/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'English', 3 | home: { 4 | login: 'Login', 5 | account: 'account', 6 | password: 'password', 7 | remember: 'remember password', 8 | forgot: 'forgot password?', 9 | moreLoginType: 'more login ways', 10 | welcome: 'Dear VIP, you have successfully logged in!', 11 | usernameRule: 'please input username', 12 | passwordRule: 'please input password' 13 | }, 14 | header: { 15 | message: 'You received a temporary overtime notice!!', 16 | setup: 'Setup', 17 | logout: 'Sign out', 18 | changeLocale: 'switch locale to' 19 | }, 20 | tab: { 21 | refresh: 'refresh', 22 | close: 'close', 23 | closeOther: 'closeOther' 24 | }, 25 | menu: { 26 | home: { 27 | root: 'home', 28 | dashboard: 'dashboard', 29 | workbench: 'workbench' 30 | }, 31 | system: { 32 | root: 'system', 33 | menu: 'menu', 34 | role: 'role', 35 | dict: 'dict', 36 | user: 'user', 37 | userDetail: 'userDetail', 38 | department: 'department', 39 | post: 'post' 40 | }, 41 | component: { 42 | root: 'component', 43 | form: 'form', 44 | table: 'table', 45 | splitPane: 'splitPane', 46 | icon: 'icon', 47 | editor: 'editor', 48 | verificationCode: 'verificationCode' 49 | }, 50 | feat: { 51 | root: 'feature', 52 | guide: 'guide', 53 | watermark: 'watermark', 54 | imagePreview: 'imagePreview', 55 | lazyLoad: 'lazyLoad' 56 | }, 57 | dynamic: { 58 | root: 'dynamic', 59 | first: 'child-first', 60 | second: 'child-second' 61 | }, 62 | personal: 'personal', 63 | login: 'login', 64 | error: { 65 | notFound: '404 - Page Not Found' 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/router/routes/modules/admin/system.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/layouts/admin/index.vue' 2 | import type { AppRouteConfig } from '@/router/types' 3 | 4 | const SystemRoute: AppRouteConfig = { 5 | path: '/system', 6 | name: 'system', 7 | component: AdminLayout, 8 | meta: { 9 | title: 'menu.system.root', 10 | icon: 'ri:settings-4-fill' 11 | }, 12 | children: [{ 13 | path: 'menu', 14 | name: 'menu', 15 | component: () => import('@/views/admin/_system/menu/index.vue'), 16 | meta: { 17 | title: 'menu.system.menu' 18 | } 19 | }, 20 | { 21 | path: 'role', 22 | name: 'role', 23 | component: () => import('@/views/admin/_system/role/index.vue'), 24 | meta: { 25 | title: 'menu.system.role' 26 | } 27 | }, 28 | { 29 | path: 'dict', 30 | name: 'dict', 31 | component: () => import('@/views/admin/_system/dict/index.vue'), 32 | meta: { 33 | title: 'menu.system.dict' 34 | } 35 | }, 36 | { 37 | path: 'user', 38 | name: 'user', 39 | component: () => import('@/views/admin/_system/user/index.vue'), 40 | meta: { 41 | title: 'menu.system.user' 42 | } 43 | }, 44 | { 45 | path: 'user/detail/:id', 46 | name: 'user_detail', 47 | component: () => import('@/views/admin/_system/user/detail.vue'), 48 | meta: { 49 | title: 'menu.system.userDetail', 50 | hideMenu: true, 51 | activeMenu: '/system/user' 52 | } 53 | }, 54 | { 55 | path: 'department', 56 | name: 'department', 57 | component: () => import('@/views/admin/_system/department/index.vue'), 58 | meta: { 59 | title: 'menu.system.department' 60 | } 61 | }, 62 | { 63 | path: 'post', 64 | name: 'post', 65 | component: () => import('@/views/admin/_system/post/index.vue'), 66 | meta: { 67 | title: 'menu.system.post' 68 | } 69 | }] 70 | } 71 | 72 | export default SystemRoute 73 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Bar/option.ts: -------------------------------------------------------------------------------- 1 | import type { ECOption } from '../useECharts' 2 | import echarts from '../useECharts' 3 | 4 | export const option: ECOption = { 5 | backgroundColor: 'transparent', 6 | tooltip: { 7 | trigger: 'axis', 8 | axisPointer: { 9 | type: 'shadow' 10 | }, 11 | valueFormatter: v => `${v}%` 12 | }, 13 | grid: { 14 | top: '10%', 15 | left: '5%', 16 | right: '2%', 17 | bottom: '20%' 18 | }, 19 | xAxis: [{ 20 | type: 'category', 21 | axisLine: { 22 | show: false 23 | }, 24 | axisTick: { 25 | show: false 26 | }, 27 | axisLabel: { 28 | show: true, 29 | fontSize: 12, 30 | color: '#6b7280', 31 | interval: 0 32 | } 33 | }], 34 | yAxis: [{ 35 | name: '', 36 | type: 'value', 37 | axisLabel: { 38 | show: true, 39 | fontSize: 14 40 | }, 41 | axisTick: { 42 | show: false 43 | }, 44 | axisLine: { 45 | show: false 46 | }, 47 | splitLine: { 48 | lineStyle: { 49 | color: '#9ca3af40', 50 | type: 'dotted' 51 | } 52 | } 53 | }], 54 | series: [{ 55 | type: 'bar', 56 | barWidth: 24, 57 | seriesLayoutBy: 'row', 58 | animationDuration(idx: number) { 59 | return idx * 500 + 1000 60 | }, 61 | backgroundStyle: { 62 | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ 63 | offset: 0, 64 | color: 'rgba(230,104,78, 0.9)' 65 | }, 66 | { 67 | offset: 0.9, 68 | color: 'transparent' 69 | } 70 | ], false), 71 | 72 | shadowColor: 'rgba(230,104,78, 0.9)', 73 | shadowBlur: 20 74 | }, 75 | itemStyle: { 76 | borderRadius: [4, 4, 0, 0], 77 | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ 78 | offset: 0, 79 | color: '#fa714e' 80 | }, { 81 | offset: 1, 82 | color: '#e43346' 83 | }]), 84 | opacity: 1 85 | } 86 | } 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /src/store/modules/menu.ts: -------------------------------------------------------------------------------- 1 | import type { RouteComponent } from 'vue-router' 2 | import { defineStore } from 'pinia' 3 | import type { AppRouteConfig } from '@/router/types' 4 | import type { BuildMenuModel } from '@/api/_system/model/menuModel' 5 | import AdminLayout from '@/layouts/admin/index.vue' 6 | import { alertErrMsg } from '@/utils/message' 7 | import { ErrorCodeEnum } from '@/enums/httpEnum' 8 | import { buildMenuApi } from '@/api/_system/menu' 9 | 10 | interface MenuState { 11 | routes: AppRouteConfig[] 12 | } 13 | 14 | type RawRouteComponent = RouteComponent | (() => Promise) 15 | 16 | const pages = import.meta.glob('../../views/**/*.vue') 17 | 18 | function componentMap(path: string): RawRouteComponent { 19 | switch (path) { 20 | case 'Layout': 21 | return AdminLayout 22 | default: { 23 | const joinPath = `admin/${path}`.replace(/\/\//, '/') 24 | return pages[(`../../views/${joinPath}.vue`)] 25 | } 26 | } 27 | } 28 | 29 | function mapRoutes(serverRoutes: BuildMenuModel[]): AppRouteConfig[] { 30 | const routes: any[] = serverRoutes.map( 31 | ({ path, name, redirect, component, children, ...rest }) => ({ 32 | path, 33 | name: name ?? '', 34 | redirect: redirect ?? '', 35 | component: componentMap(component), 36 | ...(children?.length && { children: mapRoutes(children) }), 37 | meta: rest 38 | }) 39 | ) 40 | return routes as AppRouteConfig[] 41 | } 42 | 43 | export const useMenuStore = defineStore('menu', { 44 | state: (): MenuState => ({ 45 | routes: [] 46 | }), 47 | getters: { 48 | hasRoutes: state => state.routes.length > 0 49 | }, 50 | actions: { 51 | async generateRoutes() { 52 | const serverRoutes = await buildMenuApi() 53 | try { 54 | this.routes = mapRoutes(serverRoutes) 55 | } 56 | catch (error) { 57 | alertErrMsg(ErrorCodeEnum.C100, '路由转化异常') 58 | } 59 | }, 60 | clearRoutes() { 61 | this.$reset() 62 | } 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | const path = require('node:path') 3 | const { execSync } = require('node:child_process') 4 | 5 | // 读取src下顶层目录作为默认scope选项(api、assets、components...) 6 | const scopes = fs 7 | .readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true }) 8 | .filter(dirent => dirent.isDirectory()) 9 | .map(dirent => dirent.name.replace(/s$/, '')) 10 | 11 | // 获取git状态 12 | const gitStatus = execSync('git status --porcelain || true') 13 | .toString() 14 | .trim() 15 | .split('\n') 16 | 17 | // precomputed scope 18 | const scopeComplete = gitStatus 19 | .find(r => ~r.indexOf('M src')) 20 | ?.replace(/\//g, '%%') 21 | ?.match(/src%%((\w|-)*)/)?.[1] 22 | ?.replace(/s$/, '') 23 | 24 | module.exports = { 25 | ignores: [commit => commit.includes('init')], 26 | extends: ['@commitlint/config-conventional'], 27 | rules: { 28 | 'body-leading-blank': [2, 'always'], 29 | 'footer-leading-blank': [1, 'always'], 30 | 'header-max-length': [2, 'always', 108], 31 | 'subject-empty': [2, 'never'], 32 | 'type-empty': [2, 'never'], 33 | 'subject-case': [0], 34 | 'type-enum': [ 35 | 2, 36 | 'always', 37 | [ 38 | 'feat', //新增功能 39 | 'fix', //修复BUG 40 | 'perf', //性能优化 41 | 'style', //代码格式 42 | 'docs', //文档变更 43 | 'test', //添加测试或测试改动 44 | 'refactor', //代码重构 45 | 'build', //构建相关、外部依赖变更(如升级 npm 包、修改打包配置等) 46 | 'ci', //修改 CI 配置、脚本 47 | 'chore', //架构变动 48 | 'revert', //代码回退 49 | 'wip', //正在开发中 50 | 'workflow', //工作流修改 51 | 'types' //类型定义文件修改 52 | ] 53 | ] 54 | }, 55 | prompt: { 56 | useEmoji: true, 57 | customScopesAlign: !scopeComplete ? 'top' : 'bottom', 58 | defaultScope: scopeComplete, 59 | scopes: [...scopes], 60 | typesAppend: [ 61 | { value: 'wip', name: 'wip: work in process' }, 62 | { value: 'types', name: 'types: type definition file changes' } 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/views/admin/home/dashboard/components/RankList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 51 | 52 | 73 | -------------------------------------------------------------------------------- /src/utils/http/checkStatus.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodeEnum } from '@/enums/httpEnum' 2 | import { useUserStore } from '@/store/modules/user' 3 | import { alertErrMsg } from '@/utils/message' 4 | 5 | export const ErrorMsgMap = new Map([ 6 | [ErrorCodeEnum.A100, '客户端请求错误!'], 7 | [ErrorCodeEnum.A200, '网络请求超时(Axios timeout)!'], 8 | [ErrorCodeEnum.H400, '请求参数不匹配!'], 9 | [ErrorCodeEnum.H401, '未登录或token已超时!'], 10 | [ErrorCodeEnum.H403, '权限不足!'], 11 | [ErrorCodeEnum.H404, '网络请求错误,未找到该资源!'], 12 | [ErrorCodeEnum.H405, '网络请求错误,未找到该资源!'], 13 | [ErrorCodeEnum.H408, '网络请求超时!'], 14 | [ErrorCodeEnum.H500, '服务器错误,请联系管理员!'], 15 | [ErrorCodeEnum.H502, '网络错误!'], 16 | [ErrorCodeEnum.H504, '网络超时!'], 17 | [ErrorCodeEnum.H505, 'HTTP版本不支持!'] 18 | ]) 19 | 20 | const checkStatus: (status: number, msg: string, isCancel?: boolean) => void = ( 21 | status, 22 | msg, 23 | isCancel = false 24 | ) => { 25 | if (!status) { 26 | const isAxiosTimeout = msg === ErrorMsgMap.get(ErrorCodeEnum.A200) 27 | if (isAxiosTimeout) { 28 | alertErrMsg(ErrorCodeEnum.A200, msg) 29 | } 30 | else { 31 | // 被 AxiosCanceler取消的请求不需要提示 32 | !isCancel 33 | && alertErrMsg(ErrorCodeEnum.A100, ErrorMsgMap.get(ErrorCodeEnum.A100)) 34 | } 35 | // 被 axios取消的请求:1.多次重复点击被 AxiosCanceler取消 2. 超时被自动取消 3. 其他未知 axios错误 36 | console.error('The request was cancelled by axios!') 37 | return 38 | } 39 | switch (status) { 40 | case 400: 41 | alertErrMsg(ErrorCodeEnum.H400, msg) 42 | break 43 | case 401: 44 | alertErrMsg(ErrorCodeEnum.H401, ErrorMsgMap.get(ErrorCodeEnum.H401)) 45 | useUserStore().logout('expires_token') 46 | break 47 | case 403: 48 | alertErrMsg(ErrorCodeEnum.H403, ErrorMsgMap.get(ErrorCodeEnum.H403)) 49 | break 50 | case 404: 51 | alertErrMsg(ErrorCodeEnum.H404, ErrorMsgMap.get(ErrorCodeEnum.H404)) 52 | break 53 | case 500: 54 | alertErrMsg(ErrorCodeEnum.H500, ErrorMsgMap.get(ErrorCodeEnum.H500)) 55 | break 56 | default: 57 | alertErrMsg(ErrorCodeEnum.H500, '未知错误, 请联系管理员') 58 | } 59 | } 60 | 61 | export default checkStatus 62 | -------------------------------------------------------------------------------- /src/components/TableModel/src/TableModel.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Create a report to help us improve 3 | labels: [pending triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Before You Start...** 9 | 10 | This form is only for submitting bug reports. If you have a usage question 11 | or are unsure if this is really a bug, make sure to: 12 | 13 | - Read the [README.md](https://github.com/zhou-tao/vue-power-admin#readme) 14 | - Ask on [GitHub Discussions](https://github.com/zhou-tao/vue-power-admin/discussions) 15 | 16 | - type: textarea 17 | id: bug-description 18 | attributes: 19 | label: Describe the bug 20 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! 21 | placeholder: Bug description 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: reproduction 26 | attributes: 27 | label: Reproduction 28 | description: Please provide a link to Playground or StackBlitz or a github repo that can reproduce the problem you ran into. A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. 29 | placeholder: Reproduction 30 | validations: 31 | required: false 32 | - type: textarea 33 | id: system-info 34 | attributes: 35 | label: System Info 36 | placeholder: System, Browsers, Framework 37 | validations: 38 | required: false 39 | - type: checkboxes 40 | id: checkboxes 41 | attributes: 42 | label: Validations 43 | description: Before submitting the issue, please make sure you do the following 44 | options: 45 | - label: Check that there isn't [already an issue](https://github.com/zhou-tao/vue-power-admin/issues) that reports the same bug to avoid creating a duplicate. 46 | required: true 47 | - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/zhou-tao/vue-power-admin/discussions). 48 | required: true 49 | -------------------------------------------------------------------------------- /src/store/modules/setting.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useDark } from '@vueuse/core' 3 | import { getLocalStorage, setLocalStorage } from '@h/web/useStorage' 4 | import { ThemeEnum } from '@/enums/appEnum' 5 | import { MenuLayout } from '@/enums/menuEnum' 6 | import { LocalStorageEnum } from '@/enums/storageEnum' 7 | 8 | const isDark = useDark() 9 | const computedTheme = (dark: boolean) => dark ? ThemeEnum.DARK : ThemeEnum.LIGHT 10 | 11 | interface SettingState { 12 | theme: ThemeEnum 13 | menuLayout: MenuLayout 14 | menuCollapsed: boolean 15 | hasBreadcrumb: boolean 16 | hasTabView: boolean 17 | hasFooter: boolean 18 | hasLocales: boolean 19 | hasFpLoading: boolean 20 | hasPageAnimate: boolean 21 | hasProgress: boolean 22 | } 23 | 24 | export const useSettingStore = defineStore('setting', { 25 | state: (): SettingState => ({ 26 | theme: computedTheme(isDark.value), 27 | menuLayout: MenuLayout.VERTICAL, 28 | menuCollapsed: false, 29 | hasBreadcrumb: true, 30 | hasTabView: true, 31 | hasFooter: true, 32 | hasLocales: true, 33 | hasFpLoading: getLocalStorage(LocalStorageEnum.VP_HAS_FP_LOADING), 34 | hasPageAnimate: true, 35 | hasProgress: true 36 | }), 37 | getters: { 38 | isDark: () => isDark, 39 | isVerticalMenu: state => state.menuLayout === MenuLayout.VERTICAL 40 | }, 41 | actions: { 42 | toggleDark() { 43 | isDark.value = !isDark.value 44 | this.theme = computedTheme(isDark.value) 45 | }, 46 | 47 | setLayout(layout: MenuLayout) { 48 | if (layout === MenuLayout.HORIZONTAL) { 49 | // 菜单为水平时避免折叠 50 | this.menuCollapsed = false 51 | } 52 | this.menuLayout = layout 53 | }, 54 | 55 | toggleCollapse() { 56 | this.menuCollapsed = !this.menuCollapsed 57 | }, 58 | 59 | toggleFpLoading() { 60 | this.hasFpLoading = !this.hasFpLoading 61 | setLocalStorage(LocalStorageEnum.VP_HAS_FP_LOADING, this.hasFpLoading) 62 | }, 63 | 64 | togglePageAnimate() { 65 | this.hasPageAnimate = !this.hasPageAnimate 66 | }, 67 | 68 | toggleProgress() { 69 | this.hasProgress = !this.hasProgress 70 | } 71 | }, 72 | persist: { 73 | key: 'SETTING_STORE', 74 | storage: window.sessionStorage 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /src/views/admin/_system/user/usePage.ts: -------------------------------------------------------------------------------- 1 | import type { SearchItemConfig } from '@/components/SearchModel' 2 | import { useComponent } from '@/components/SearchModel' 3 | import type { UserInfoModel } from '@/api/_system/model/userModel' 4 | import type { ColumnAttrs } from '@/components/TableModel' 5 | import { useSlotTag } from '@/components/TableModel' 6 | 7 | const { ElInput, ElSelect } = useComponent() 8 | 9 | export enum SubmitTypeEnum { 10 | ADD = '新增', 11 | UPDATE = '编辑' 12 | } 13 | 14 | // search model config 15 | export const config: SearchItemConfig[] = [ 16 | { component: ElInput, label: '用户名', field: 'username', placeholder: '请输入' }, 17 | { component: ElInput, label: '姓名', field: 'name', placeholder: '请输入' }, 18 | { component: ElSelect, label: '性别', field: 'gender', clearable: true, options: [{ label: '男', value: '1' }, { label: '女', value: '0' }] }, 19 | { component: ElSelect, label: '权限', field: 'role', clearable: true, options: [{ label: '用户', value: '1' }, { label: '管理员', value: '0' }] }, 20 | { component: ElInput, label: '部门', field: 'deptName', placeholder: '请输入' }, 21 | { component: ElInput, label: '岗位', field: 'post', placeholder: '请输入' } 22 | ] 23 | 24 | // table model static column config 25 | export const staticColumns = [ 26 | { fixed: true, type: 'selection', width: '50' }, 27 | { fixed: true, prop: 'id', label: '编号', width: '70', align: 'center' }, 28 | { prop: 'username', label: '用户名', width: '180' }, 29 | { prop: 'name', label: '姓名', width: '140' }, 30 | { 31 | prop: 'gender', 32 | label: '性别', 33 | width: '80', 34 | slot: ({ row }: ColumnAttrs) => 35 | [h('span', row.gender === '1' ? '男' : '女')] 36 | }, 37 | { prop: 'mobile', label: '联系电话' }, 38 | { 39 | prop: 'roles', 40 | label: '权限', 41 | slot: ({ row }: ColumnAttrs) => { 42 | if (!row.roles) return [h('span')] 43 | return row?.roles.map(role => useSlotTag(role.name || '', () => {}, { style: { margin: '2px' } })) 44 | } 45 | }, 46 | { prop: 'deptName', label: '所在部门' }, 47 | { 48 | prop: 'posts', 49 | label: '就职岗位', 50 | slot: ({ row }: ColumnAttrs) => { 51 | if (!row.posts) return [h('span')] 52 | return row?.posts.map(post => useSlotTag(post.name || '', () => {}, { style: { margin: '2px' }, type: 'success' })) 53 | } 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /src/components/ECharts/src/Line/option.ts: -------------------------------------------------------------------------------- 1 | import type { ECOption } from '../useECharts' 2 | import echarts from '../useECharts' 3 | 4 | export const getOption = (colors: string[][]): ECOption => ({ 5 | backgroundColor: 'transparent', 6 | tooltip: { 7 | trigger: 'axis', 8 | axisPointer: { 9 | type: 'line' 10 | } 11 | }, 12 | legend: { 13 | show: true, 14 | top: 0, 15 | itemWidth: 30, 16 | itemHeight: 10, 17 | textStyle: { 18 | color: '#6b7280', 19 | fontSize: 14 20 | } 21 | }, 22 | grid: { 23 | top: '11%', 24 | left: '4%', 25 | right: '2%', 26 | bottom: '18%' 27 | }, 28 | xAxis: { 29 | type: 'category', 30 | axisLabel: { 31 | color: '#6b7280', 32 | fontSize: 14 33 | }, 34 | axisLine: { 35 | show: false 36 | }, 37 | splitLine: { 38 | show: false 39 | }, 40 | axisTick: { 41 | show: false 42 | }, 43 | boundaryGap: false 44 | }, 45 | yAxis: { 46 | type: 'value', 47 | nameTextStyle: { 48 | padding: [0, 60, 0, 0] 49 | }, 50 | splitLine: { 51 | show: true, 52 | lineStyle: { 53 | color: '#9ca3af40', 54 | type: 'solid' 55 | } 56 | }, 57 | axisLine: { 58 | show: false 59 | }, 60 | axisLabel: { 61 | show: true, 62 | fontSize: 14 63 | }, 64 | axisTick: { 65 | show: false 66 | } 67 | }, 68 | series: Array(2).fill(0).map((_, i) => ({ 69 | type: 'line', 70 | seriesLayoutBy: 'row', 71 | symbol: 'circle', 72 | smooth: true, 73 | lineStyle: { 74 | width: 3, 75 | color: colors[i][0] 76 | }, 77 | itemStyle: { 78 | color: colors[i][1] 79 | }, 80 | areaStyle: { 81 | color: new echarts.graphic.LinearGradient( 82 | 0, 83 | 0, 84 | 0, 85 | 1, 86 | [ 87 | { 88 | offset: 0, 89 | color: `${colors[i][0]}30` 90 | }, 91 | { 92 | offset: 0.6, 93 | color: `${colors[i][0]}20` 94 | }, 95 | { 96 | offset: 1, 97 | color: `${colors[i][0]}10` 98 | } 99 | ], 100 | false 101 | ) 102 | } 103 | })) 104 | }) 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Vue-Power-Admin


3 |

4 | release version 5 | deploy status 6 |

7 |

Vue Power Admin

8 |
9 | 10 | **English** | [简体中文](./README.zh-CN.md) 11 | 12 | ### :loudspeaker: Introduction 13 | 14 | > [Vue Power Admin](https://vue-power-admin.netlify.app) is a fullstack project template for management systems. Built with Vite, Vue3 and [elements-plus](https://element-plus.org/zh-CN/). 15 | 16 | ### :rocket: Features 17 | 18 | - **I18n** - Fine integrated with [vue-i18n](https://github.com/kazupon/vue-i18n),takes zero step to make your project switchable between different languages 19 | - **Theming** - 6 presets themes and support for primary color runtime customization 20 | - **Layouts** - Built-in layouts switchable during runtime 21 | - **Fine-grained to actions level authority models** 22 | - **Rich built-in components** - Many built-in components that can help you sovle the daily usage scenarios 23 | - **Code automation** - Save time on repetitive works 24 | - **Coding with confidence** - Fine integrated with Simple-git-hooks and ESLint, ensure your code quality and make the code format more uniform in style 25 | - **Data mock** - Provide built-in mock support for realtime development api and production injection 26 | 27 | ### :alarm_clock: Getting started 28 | 29 | ```shell 30 | # download and pick `vpa-frontend` option 31 | pnpm create vpa vue-power-admin 32 | 33 | # cd vue-power-admin 34 | cd vue-power-admin 35 | 36 | # install dependencies 37 | pnpm i 38 | 39 | # start up 40 | pnpm dev 41 | 42 | ``` 43 | 44 | ### :heart: Contributing 45 | 46 | All kinds of contributions are welcomed! 47 | 48 | [Contributors here](https://github.com/zhou-tao/vue-power-admin/graphs/contributors) 49 | 50 | ### Thanks 51 | 52 | This project cannot be done without these projects: 53 | 54 | * Vite 55 | * Vue3 56 | * Vue Router 57 | * Pinia 58 | * Typescript 59 | * ElementPlus 60 | * Unocss 61 | * Vue-i18n 62 | * MockJS 63 | * Eslint 64 | * Simple-git-hooks 65 | 66 | ### LICENSE 67 | 68 | [MIT](./LICENSE) License © 2022-PRESENT toryz 69 | -------------------------------------------------------------------------------- /src/assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/admin/component/table/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/components/Splitter/src/Splitter.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 71 | 72 | 77 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { createCookie, removeCookies } from '@h/web/useCookie' 2 | import { defineStore } from 'pinia' 3 | import type { LoginParams, LoginResultModel } from '@/api/_auth/model' 4 | import type { UserInfoModel } from '@/api/_system/model/userModel' 5 | import { loginApi, tokenRefresh } from '@/api/_auth' 6 | import { checkPassword } from '@/utils/regex' 7 | import { TokenTypeEnum } from '@/enums/authEnum' 8 | import { getAccountInfo } from '@/api/_system/user' 9 | import { router } from '@/router' 10 | 11 | interface UserState extends UserInfoModel { 12 | security: boolean 13 | } 14 | 15 | function setTokenHelper({ 16 | access_token, 17 | refresh_token, 18 | expires_in 19 | }: LoginResultModel) { 20 | createCookie(TokenTypeEnum.ACCESS_TOKEN, access_token, { 21 | expires: expires_in / (24 * 60 * 60 - 10) 22 | }) 23 | createCookie(TokenTypeEnum.REFRESH_TOKEN, refresh_token) 24 | } 25 | 26 | const initialUserState = { 27 | id: 0, 28 | name: 'coder', 29 | userId: -1, 30 | username: 'Toryz', 31 | gender: '1', 32 | avatar: 'https://avatars.githubusercontent.com/u/36221207?v=4', 33 | deptCode: '007', 34 | deptName: '开发部', 35 | mobile: '18812345678', 36 | posts: [ 37 | { id: 1, code: 'FRONT-END', name: '前端' }, 38 | { id: 4, code: 'OPEN-SOURCE', name: '开源' } 39 | ], 40 | roles: [ 41 | { id: 0, code: 'ADMIN', name: '管理员', menu: [] } 42 | ], 43 | security: true // 密码安全性 44 | } 45 | 46 | export const useUserStore = defineStore('user', { 47 | state: (): UserState => initialUserState, 48 | getters: { 49 | invalid: state => state.userId === -1 50 | }, 51 | actions: { 52 | async login({ 53 | username, 54 | password 55 | }: LoginParams): Promise { 56 | const data = await loginApi({ username, password }) 57 | if (!data) { 58 | return Promise.reject(new Error('login failed!')) 59 | } 60 | setTokenHelper(data) 61 | this.security = checkPassword(password) 62 | await this.setUserInfo() 63 | return data 64 | }, 65 | 66 | async reLogin(): Promise { 67 | const data = await tokenRefresh() 68 | setTokenHelper(data) 69 | await this.setUserInfo() 70 | return data 71 | }, 72 | 73 | async setUserInfo() { 74 | const accountInfo = await getAccountInfo() 75 | this.$patch(accountInfo) 76 | }, 77 | 78 | logout(redirectUrl?: string) { 79 | this.$reset() 80 | removeCookies([TokenTypeEnum.ACCESS_TOKEN, TokenTypeEnum.REFRESH_TOKEN]) 81 | router.replace(`/login?redirect=${redirectUrl}`) 82 | } 83 | }, 84 | persist: { 85 | key: 'USER_STORE', 86 | storage: window.sessionStorage 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /src/components/ECharts/src/useECharts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core' 2 | import type { 3 | LineSeriesOption, 4 | BarSeriesOption, 5 | PieSeriesOption, 6 | RadarSeriesOption 7 | } from 'echarts/charts' 8 | import { 9 | LineChart, 10 | BarChart, 11 | PieChart, 12 | RadarChart 13 | } from 'echarts/charts' 14 | import type { 15 | TooltipComponentOption, 16 | GridComponentOption, 17 | DatasetComponentOption, 18 | LegendComponentOption 19 | } from 'echarts/components' 20 | import { 21 | TooltipComponent, 22 | GridComponent, 23 | DatasetComponent, 24 | TransformComponent, 25 | LegendComponent 26 | } from 'echarts/components' 27 | 28 | import { LabelLayout, UniversalTransition } from 'echarts/features' 29 | import { CanvasRenderer } from 'echarts/renderers' 30 | 31 | import { useThrottle } from '@h/logic/useDelay' 32 | import { useSettingStore } from '@/store/modules/setting' 33 | 34 | export type ECOption = echarts.ComposeOption< 35 | BarSeriesOption 36 | | LineSeriesOption 37 | | PieSeriesOption 38 | | RadarSeriesOption 39 | | TooltipComponentOption 40 | | GridComponentOption 41 | | DatasetComponentOption 42 | | LegendComponentOption 43 | > 44 | 45 | export type ChartInstance = echarts.ECharts 46 | export type ChartDataset = DatasetComponentOption 47 | 48 | // 注册必须的组件 49 | echarts.use([ 50 | BarChart, 51 | LineChart, 52 | PieChart, 53 | RadarChart, 54 | TooltipComponent, 55 | GridComponent, 56 | DatasetComponent, 57 | TransformComponent, 58 | LegendComponent, 59 | LabelLayout, 60 | UniversalTransition, 61 | CanvasRenderer 62 | ]) 63 | 64 | export default echarts 65 | 66 | export function initChart(root: HTMLDivElement, options: ECOption) { 67 | const myChart = echarts.init(root) 68 | myChart.setOption(options) 69 | const settingStore = useSettingStore() 70 | watch(settingStore.isDark, (v) => { 71 | myChart.setOption({ 72 | darkMode: v 73 | }) 74 | }, { 75 | immediate: true 76 | }) 77 | onResize(myChart, root) 78 | return myChart 79 | } 80 | 81 | export function setData(instance: ChartInstance, dataset: DatasetComponentOption) { 82 | if (!dataset) return 83 | instance.setOption({ 84 | dataset 85 | }) 86 | } 87 | 88 | export function onResize(instance: ChartInstance, root: HTMLDivElement) { 89 | const throttleResize = useThrottle(() => { 90 | if (instance.isDisposed()) { 91 | resizeObserver.disconnect() 92 | return 93 | } 94 | instance.resize({ 95 | width: 'auto', 96 | height: 'auto' 97 | }) 98 | }, 400) 99 | const resizeObserver = new ResizeObserver(() => throttleResize()) 100 | resizeObserver.observe(root) 101 | } 102 | -------------------------------------------------------------------------------- /src/layouts/admin/sider/components/menu/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | 103 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' 2 | import { 3 | defineConfig, 4 | presetUno, 5 | presetAttributify, 6 | presetIcons, 7 | transformerDirectives, 8 | transformerVariantGroup 9 | } from 'unocss' 10 | 11 | export default defineConfig({ 12 | // 添加 windicss 预设、属性化模式 13 | presets: [ 14 | presetUno(), 15 | presetAttributify(), 16 | presetIcons({ 17 | warn: true, 18 | collections: { 19 | app: FileSystemIconLoader('./src/assets/icons') 20 | } 21 | }) 22 | ], 23 | // 提供指令功能 24 | transformers: [transformerDirectives(), transformerVariantGroup()], 25 | theme: { 26 | colors: { 27 | root_light: '#f5f6fa', // 最底层背景 28 | root_dark: '#272727', 29 | page_light: '#ffffff', // 基础布局背景 30 | page_dark: '#18181B', 31 | primary: 'var(--el-color-primary)', 32 | main: 'var(--el-text-color-primary)', 33 | regular: 'var(--el-text-color-regular)', 34 | secondary: 'var(--el-text-color-secondary)', 35 | placeholder: 'var(--el-text-color-placeholder)', 36 | light: 'var(--el-color-info-light-9)', 37 | light_hover: 'var(--el-color-info-light-8)' 38 | }, 39 | height: { 40 | header: '64px', 41 | footer: '50px', 42 | tab: 'var(--tab-view-height)', 43 | content: 'var(--content-base-height)' 44 | }, 45 | minHeight: { 46 | content: 'var(--content-base-height)' 47 | } 48 | }, 49 | shortcuts: [ 50 | { 51 | 'bg-root': 'bg-root_light dark:bg-root_dark' 52 | }, 53 | { 54 | 'bg-page': 'bg-page_light dark:bg-page_dark' 55 | }, 56 | { 57 | 'transition-base': 'transition-all duration-150 ease-in-out' 58 | }, 59 | { 60 | 'page-base': 'min-h-content overflow-x-hidden box-border' 61 | }, 62 | { 63 | 'page-card': 'page-base bg-page rounded px-6 py-5' 64 | }, 65 | { 66 | 'page-pure': 'page-base bg-page rounded h-content' 67 | }, 68 | { 69 | 'flex-center': 'flex items-center justify-center' 70 | } 71 | ], 72 | // 自定义规则 73 | rules: [ 74 | [ 75 | 'text-brand-gradient', 76 | { 77 | 'color': 'transparent', 78 | 'background-image': 'linear-gradient(to right, #00ff8f, #00a1ff)', 79 | 'background-clip': 'text' 80 | } 81 | ], 82 | [ 83 | 'bg-gradient-light', 84 | { 85 | 'background-image': 86 | 'linear-gradient( 135deg, #ABDCFF30 20%, #0396FF20 100%)' 87 | } 88 | ], 89 | [ 90 | 'bg-gradient-dark', 91 | { 92 | 'background-image': 93 | 'linear-gradient( 135deg, #2A5470 20%, #4C4177 100%)' 94 | } 95 | ], 96 | [ 97 | 'shadow-card-dark', 98 | { 99 | 'box-shadow': '0 0 12px 0 rgb(0 0 0 / 9%)' 100 | } 101 | ] 102 | ] 103 | }) 104 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | export function is(val: unknown, type: string) { 4 | return toString.call(val) === `[object ${type}]` 5 | } 6 | 7 | export function isDef(val?: T): val is T { 8 | return typeof val !== 'undefined' 9 | } 10 | 11 | export function isUnDef(val?: T): val is T { 12 | return !isDef(val) 13 | } 14 | 15 | export function isObject(val: any): val is Record { 16 | return val !== null && is(val, 'Object') 17 | } 18 | 19 | export function isEmpty(val: T): val is T { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0 30 | } 31 | 32 | return false 33 | } 34 | 35 | export function isDate(val: unknown): val is Date { 36 | return is(val, 'Date') 37 | } 38 | 39 | export function isNull(val: unknown): val is null { 40 | return val === null 41 | } 42 | 43 | export function isNullAndUnDef(val: unknown): val is null | undefined { 44 | return isUnDef(val) && isNull(val) 45 | } 46 | 47 | export function isNullOrUnDef(val: unknown): val is null | undefined { 48 | return isUnDef(val) || isNull(val) 49 | } 50 | 51 | export function isNumber(val: unknown): val is number { 52 | return is(val, 'Number') 53 | } 54 | 55 | export function isPromise(val: unknown): val is Promise { 56 | return ( 57 | is(val, 'Promise') 58 | && isObject(val) 59 | && isFunction(val.then) 60 | && isFunction(val.catch) 61 | ) 62 | } 63 | 64 | export function isString(val: unknown): val is string { 65 | return is(val, 'String') 66 | } 67 | 68 | export function isJsonString(val: unknown): boolean { 69 | if (!isString(val)) return false 70 | try { 71 | return !!isObject(JSON.parse(val)) 72 | } 73 | catch (err) { 74 | return false 75 | } 76 | } 77 | 78 | export function isFunction(val: unknown): val is Function { 79 | return typeof val === 'function' 80 | } 81 | 82 | export function isBoolean(val: unknown): val is boolean { 83 | return is(val, 'Boolean') 84 | } 85 | 86 | export function isRegExp(val: unknown): val is RegExp { 87 | return is(val, 'RegExp') 88 | } 89 | 90 | export function isArray(val: any): val is Array { 91 | return val && Array.isArray(val) 92 | } 93 | 94 | export function isWindow(val: any): val is Window { 95 | return typeof window !== 'undefined' && is(val, 'Window') 96 | } 97 | 98 | export function isElement(val: unknown): val is Element { 99 | return isObject(val) && !!val.tagName 100 | } 101 | 102 | export function isMap(val: unknown): val is Map { 103 | return is(val, 'Map') 104 | } 105 | 106 | export const isServer = typeof window === 'undefined' 107 | 108 | export const isClient = !isServer 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-power-admin", 3 | "type": "module", 4 | "version": "1.2.0", 5 | "private": true, 6 | "packageManager": "pnpm@8.6.10", 7 | "scripts": { 8 | "dev": "vite", 9 | "commit": "git cz", 10 | "plop": "plop", 11 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 12 | "release": "npm version patch", 13 | "build": "vue-tsc && vite build", 14 | "build:test": "vue-tsc && vite build --mode test", 15 | "preview": "yarn build && vite preview", 16 | "preview:test": "yarn build:test && vite preview", 17 | "lint": "eslint .", 18 | "lint:fix": "eslint . --fix", 19 | "typecheck": "vue-tsc", 20 | "postinstall": "simple-git-hooks" 21 | }, 22 | "dependencies": { 23 | "@vueuse/core": "^10.4.1", 24 | "@wangeditor/editor": "^5.1.23", 25 | "@wangeditor/editor-for-vue": "^5.1.12", 26 | "axios": "^1.6.0", 27 | "echarts": "^5.4.3", 28 | "element-plus": "^2.3.14", 29 | "js-cookie": "^3.0.5", 30 | "lodash-es": "^4.17.21", 31 | "nprogress": "^0.2.0", 32 | "pinia": "^2.1.6", 33 | "pinia-plugin-persistedstate": "^3.2.0", 34 | "qs": "^6.11.2", 35 | "viewerjs": "^1.11.5", 36 | "vue": "3.3.4", 37 | "vue-i18n": "^9.4.1", 38 | "vue-router": "^4.2.4" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "^17.7.1", 42 | "@commitlint/config-conventional": "^17.7.0", 43 | "@iconify-json/ep": "^1.1.12", 44 | "@iconify-json/fluent-emoji-flat": "^1.1.12", 45 | "@iconify-json/ri": "^1.1.12", 46 | "@iconify/utils": "^2.1.9", 47 | "@iconify/vue": "^4.1.1", 48 | "@toryz/eslint-config": "^0.1.4", 49 | "@types/js-cookie": "^3.0.3", 50 | "@types/lodash-es": "^4.17.9", 51 | "@types/mockjs": "^1.0.7", 52 | "@types/node": "20.6.0", 53 | "@types/nprogress": "^0.2.0", 54 | "@types/qs": "^6.9.8", 55 | "@vitejs/plugin-legacy": "^4.1.1", 56 | "@vitejs/plugin-vue": "^4.3.4", 57 | "@vue/compiler-sfc": "^3.3.4", 58 | "autoprefixer": "^10.4.15", 59 | "chalk": "^5.3.0", 60 | "commitizen": "^4.3.0", 61 | "conventional-changelog-cli": "^4.1.0", 62 | "cz-git": "^1.7.1", 63 | "eslint": "^8.49.0", 64 | "lint-staged": "^14.0.1", 65 | "mockjs": "^1.1.0", 66 | "plop": "^3.1.2", 67 | "sass": "^1.55.0", 68 | "simple-git-hooks": "^2.9.0", 69 | "terser": "^5.19.4", 70 | "typescript": "5.1.6", 71 | "unocss": "^0.55.7", 72 | "unplugin-auto-import": "^0.11.2", 73 | "unplugin-vue-components": "^0.22.8", 74 | "vite": "4.5.3", 75 | "vite-plugin-env-parser": "^0.4.1", 76 | "vite-plugin-vue-setup-extend": "^0.4.0", 77 | "vue-tsc": "^1.8.11" 78 | }, 79 | "simple-git-hooks": { 80 | "pre-commit": "pnpm lint-staged", 81 | "commit-msg": "node build/verifyCommit.js" 82 | }, 83 | "lint-staged": { 84 | "*": "eslint --fix" 85 | }, 86 | "config": { 87 | "commitizen": { 88 | "path": "node_modules/cz-git" 89 | } 90 | }, 91 | "browserslist": [ 92 | "last 1 version", 93 | "> 1%", 94 | "not dead" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /src/views/admin/_system/dict/detail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/views/admin/_system/user/detail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import type { ConfigEnv } from 'vite' 3 | import { defineConfig, loadEnv } from 'vite' 4 | import { parse } from 'vite-plugin-env-parser' 5 | import autoprefixer from 'autoprefixer' 6 | import { createVitePlugins } from './build/vite/plugins' 7 | import { createProxy } from './build/vite/proxy' 8 | import { createOptimizeDeps } from './build/vite/optimize-deps' 9 | 10 | // eslint-disable-next-line no-control-regex 11 | const INVALID_CHAR_REGEX = /[\x00-\x1F\x7F<>*#"{}|^[\]`;?:&=+$,_]/g 12 | const DRIVE_LETTER_REGEX = /^[a-z]:/i 13 | 14 | export default ({ mode }: ConfigEnv) => { 15 | const root = process.cwd() 16 | const envDir = resolve(__dirname, 'env') 17 | const env = parse(loadEnv(mode, envDir)) 18 | const { 19 | VITE_PORT, 20 | VITE_BASE_API, 21 | VITE_USE_PROXY, 22 | VITE_PUBLIC_PATH, 23 | VITE_PROXY_PREFIX, 24 | VITE_DROP_CONSOLE 25 | } = env 26 | return defineConfig({ 27 | root, 28 | base: VITE_PUBLIC_PATH, 29 | resolve: { 30 | alias: { 31 | '@': resolve(__dirname, 'src'), 32 | '@c': resolve(__dirname, 'src/components'), 33 | '@h': resolve(__dirname, 'src/composables'), 34 | '#': resolve(__dirname, 'types') 35 | } 36 | }, 37 | envDir, 38 | server: { 39 | host: true, 40 | port: VITE_PORT, 41 | open: true, 42 | https: false, 43 | ...(VITE_USE_PROXY 44 | ? { proxy: createProxy(VITE_PROXY_PREFIX, VITE_BASE_API) } 45 | : {}) 46 | }, 47 | esbuild: { 48 | pure: VITE_DROP_CONSOLE ? ['console.log', 'debugger', 'alert'] : [] 49 | }, 50 | build: { 51 | reportCompressedSize: false, 52 | chunkSizeWarningLimit: 800, 53 | rollupOptions: { 54 | output: { 55 | manualChunks: { 56 | '@wangeditor/editor-for-vue': ['@wangeditor/editor-for-vue'], 57 | 'mockjs': ['mockjs'], 58 | 'echarts/core': ['echarts/core'], 59 | 'echarts/charts': ['echarts/charts'], 60 | 'echarts/components': ['echarts/components'], 61 | 'vue': ['vue'], 62 | 'vue-router': ['vue-router'], 63 | 'pinia': ['pinia'], 64 | 'element-plus': ['element-plus'], 65 | 'viewerjs': ['viewerjs'], 66 | 'vue-i18n': ['vue-i18n'], 67 | 'axios': ['axios'], 68 | 'nprogress': ['nprogress'] 69 | }, 70 | // fix: github page not found _plugin-vue_export-helper.xxx.js 71 | sanitizeFileName(name) { 72 | const match = DRIVE_LETTER_REGEX.exec(name) 73 | const driveLetter = match ? match[0] : '' 74 | return ( 75 | driveLetter 76 | + name.slice(driveLetter.length).replace(INVALID_CHAR_REGEX, '') 77 | ) 78 | } 79 | } 80 | } 81 | }, 82 | css: { 83 | devSourcemap: false, 84 | postcss: { 85 | plugins: [ 86 | autoprefixer as any 87 | ] 88 | } 89 | }, 90 | plugins: createVitePlugins(env, mode === 'production'), 91 | // fix(vite): optimized dependencies changed. reloading 92 | optimizeDeps: createOptimizeDeps() 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/assets/icons/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plop/page/page.hbs: -------------------------------------------------------------------------------- 1 | 85 | 86 | 114 | -------------------------------------------------------------------------------- /src/layouts/admin/tabs/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 84 | 85 | 114 | --------------------------------------------------------------------------------