diff --git a/.eslintrc.js b/.eslintrc.js index a126598..8c7c5db 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,8 +19,7 @@ module.exports = { 'plugin:vue/vue3-strongly-recommended', 'plugin:@typescript-eslint/recommended', '@vue/standard', - '@vue/typescript/recommended', - 'plugin:prettier/recommended' // 添加 prettier 插件,必须放在数组最后 + '@vue/typescript/recommended' ], rules: { 'vue/multi-word-component-names': 'off', @@ -40,18 +39,18 @@ module.exports = { } } ], - 'vue/html-self-closing': [ - 'error', - { - html: { - void: 'always', - normal: 'always', - component: 'always' - }, - svg: 'always', - math: 'always' - } - ], + // 'vue/html-self-closing': [ + // 'error', + // { + // html: { + // void: 'always', + // normal: 'always', + // component: 'always' + // }, + // svg: 'always', + // math: 'always' + // } + // ], '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-var-requires': 'off', 'prefer-regex-literals': 'off', diff --git a/package.json b/package.json index 6ced0da..195c6bf 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,7 @@ "build:prod": "vue-tsc --noEmit && vite build", "preview:stage": "pnpm build:stage && vite preview", "preview:prod": "pnpm build:prod && vite preview", - "lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", - "lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", - "lint": "pnpm lint:eslint && pnpm lint:prettier" + "lint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix" }, "dependencies": { "@element-plus/icons-vue": "^1.1.4", @@ -38,14 +36,11 @@ "@vue/eslint-config-standard": "^6.1.0", "@vue/eslint-config-typescript": "^10.0.0", "eslint": "^8.13.0", - "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-vue": "^8.6.0", "lint-staged": "^12.4.0", - "prettier": "^2.6.2", "sass": "^1.50.1", "typescript": "^4.6.3", "unplugin-auto-import": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec01ab6..1aad6f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,8 @@ specifiers: dayjs: ^1.11.1 element-plus: ^2.1.10 eslint: ^8.13.0 - eslint-config-prettier: ^8.5.0 eslint-plugin-import: ^2.26.0 eslint-plugin-node: ^11.1.0 - eslint-plugin-prettier: ^4.0.0 eslint-plugin-promise: ^6.0.0 eslint-plugin-vue: ^8.6.0 js-cookie: ^3.0.1 @@ -28,7 +26,6 @@ specifiers: nprogress: ^0.2.0 path-to-regexp: ^6.2.0 pinia: ^2.0.13 - prettier: ^2.6.2 sass: ^1.50.1 screenfull: ^6.0.1 typescript: ^4.6.3 @@ -66,14 +63,11 @@ devDependencies: '@vue/eslint-config-standard': 6.1.0_4095fd5e90f154d95a11e7814517bad8 '@vue/eslint-config-typescript': 10.0.0_a62cbc2f4797496d74696b1f6538012a eslint: 8.13.0 - eslint-config-prettier: 8.5.0_eslint@8.13.0 eslint-plugin-import: 2.26.0_eslint@8.13.0 eslint-plugin-node: 11.1.0_eslint@8.13.0 - eslint-plugin-prettier: 4.0.0_1815ac95b7fb26c13c7d48a8eef62d0f eslint-plugin-promise: 6.0.0_eslint@8.13.0 eslint-plugin-vue: 8.6.0_eslint@8.13.0 lint-staged: 12.4.0 - prettier: 2.6.2 sass: 1.50.1 typescript: 4.6.3 unplugin-auto-import: 0.7.1_vite@2.9.5 @@ -1450,15 +1444,6 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier/8.5.0_eslint@8.13.0: - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.13.0 - dev: true - /eslint-config-standard/16.0.3_4e3767bb061a43bc3978ec6596bc3350: resolution: {integrity: sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==} peerDependencies: @@ -1557,23 +1542,6 @@ packages: semver: 6.3.0 dev: true - /eslint-plugin-prettier/4.0.0_1815ac95b7fb26c13c7d48a8eef62d0f: - resolution: {integrity: sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==} - engines: {node: '>=6.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.13.0 - eslint-config-prettier: 8.5.0_eslint@8.13.0 - prettier: 2.6.2 - prettier-linter-helpers: 1.0.0 - dev: true - /eslint-plugin-promise/6.0.0_eslint@8.13.0: resolution: {integrity: sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1797,10 +1765,6 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true - /fast-diff/1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} - dev: true - /fast-glob/3.2.11: resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} engines: {node: '>=8.6.0'} @@ -2984,19 +2948,6 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prettier-linter-helpers/1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - dependencies: - fast-diff: 1.2.0 - dev: true - - /prettier/2.6.2: - resolution: {integrity: sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true - /punycode/2.1.1: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} diff --git a/public/favicon.ico b/public/favicon.ico index df36fcf..294777f 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/src/App.vue b/src/App.vue index c45762b..491b522 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,30 +1,15 @@ - - - + diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..1e61d84 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils/service' + +interface IUserRequestData { + username: string + password: string +} + +/** 登录,返回 token */ +export function accountLogin(data: IUserRequestData) { + return request({ + url: 'users/login', + method: 'post', + data + }) +} +/** 获取用户详情 */ +export function userInfoRequest() { + return request({ + url: 'users/info', + method: 'post' + }) +} diff --git a/src/assets/docs/qq.png b/src/assets/docs/qq.png new file mode 100644 index 0000000..5cbeffe Binary files /dev/null and b/src/assets/docs/qq.png differ diff --git a/src/assets/layout/discard1.png b/src/assets/layout/discard1.png new file mode 100644 index 0000000..b53be4a Binary files /dev/null and b/src/assets/layout/discard1.png differ diff --git a/src/assets/layout/discard2.png b/src/assets/layout/discard2.png new file mode 100644 index 0000000..b19aa20 Binary files /dev/null and b/src/assets/layout/discard2.png differ diff --git a/src/assets/layout/discard3.png b/src/assets/layout/discard3.png new file mode 100644 index 0000000..ca12b3a Binary files /dev/null and b/src/assets/layout/discard3.png differ diff --git a/src/assets/layout/logo-text-1.png b/src/assets/layout/logo-text-1.png new file mode 100644 index 0000000..1ff19af Binary files /dev/null and b/src/assets/layout/logo-text-1.png differ diff --git a/src/assets/layout/logo-text-2.png b/src/assets/layout/logo-text-2.png new file mode 100644 index 0000000..c68c41c Binary files /dev/null and b/src/assets/layout/logo-text-2.png differ diff --git a/src/assets/layout/logo-text-3.png b/src/assets/layout/logo-text-3.png new file mode 100644 index 0000000..6267f82 Binary files /dev/null and b/src/assets/layout/logo-text-3.png differ diff --git a/src/assets/layout/logo.png b/src/assets/layout/logo.png new file mode 100644 index 0000000..cf09312 Binary files /dev/null and b/src/assets/layout/logo.png differ diff --git a/src/assets/layout/logo.svg b/src/assets/layout/logo.svg new file mode 100644 index 0000000..0d29ca6 --- /dev/null +++ b/src/assets/layout/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logo.png b/src/assets/logo.png deleted file mode 100644 index f3d2503..0000000 Binary files a/src/assets/logo.png and /dev/null differ diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index d8a42a1..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue new file mode 100644 index 0000000..d6db1b5 --- /dev/null +++ b/src/components/Screenfull/index.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/components/ThemeSwitch/index.vue b/src/components/ThemeSwitch/index.vue new file mode 100644 index 0000000..e8afc36 --- /dev/null +++ b/src/components/ThemeSwitch/index.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/config/layout.ts b/src/config/layout.ts new file mode 100644 index 0000000..e81d942 --- /dev/null +++ b/src/config/layout.ts @@ -0,0 +1,26 @@ +/** 布局配置 */ +interface ILayoutSettings { + /** 控制 settings panel 显示 */ + showSettings: boolean + /** 控制 tagsview 显示 */ + showTagsView: boolean + /** 控制 siderbar logo 显示 */ + showSidebarLogo: boolean + /** 如果为真,将固定 header */ + fixedHeader: boolean + /** 控制 换肤按钮 显示 */ + showThemeSwitch: boolean + /** 控制 全屏按钮 显示 */ + showScreenfull: boolean +} + +const layoutSettings: ILayoutSettings = { + showSettings: true, + showTagsView: true, + fixedHeader: false, + showSidebarLogo: true, + showThemeSwitch: true, + showScreenfull: true +} + +export default layoutSettings diff --git a/src/config/roles.ts b/src/config/roles.ts new file mode 100644 index 0000000..f79a4f9 --- /dev/null +++ b/src/config/roles.ts @@ -0,0 +1,14 @@ +/** 角色配置 */ +interface IRolesSettings { + /** 是否开启角色功能(开启后需要后端配合,在查询用户详情接口返回当前用户的所属角色) */ + openRoles: boolean + /** 当角色功能关闭时,当前登录用户的默认角色将生效(默认为admin,拥有所有权限) */ + defaultRoles: Array +} + +const rolesSettings: IRolesSettings = { + openRoles: true, + defaultRoles: ['admin'] +} + +export default rolesSettings diff --git a/src/config/theme.ts b/src/config/theme.ts new file mode 100644 index 0000000..90fc2a4 --- /dev/null +++ b/src/config/theme.ts @@ -0,0 +1,13 @@ +/** 注册的主题 */ +const themeList = [ + { + title: '默认', + name: 'normal' + }, + { + title: '黑暗', + name: 'dark' + } +] + +export default themeList diff --git a/src/config/white-list.ts b/src/config/white-list.ts new file mode 100644 index 0000000..8fc6eaf --- /dev/null +++ b/src/config/white-list.ts @@ -0,0 +1,4 @@ +/** 免登录白名单 */ +const whiteList = ['/login'] + +export { whiteList } diff --git a/src/constant/key.ts b/src/constant/key.ts new file mode 100644 index 0000000..3557cf7 --- /dev/null +++ b/src/constant/key.ts @@ -0,0 +1,8 @@ +class Keys { + static sidebarStatus = 'v3-admin-sidebar-status-key' + static language = 'v3-admin-language-key' + static token = 'v3-admin-token-key' + static activeThemeName = 'v3-admin-active-theme-name' +} + +export default Keys diff --git a/src/directives/index.ts b/src/directives/index.ts new file mode 100644 index 0000000..22ea82a --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1 @@ +export * from './permission' diff --git a/src/directives/permission/index.ts b/src/directives/permission/index.ts new file mode 100644 index 0000000..60965c4 --- /dev/null +++ b/src/directives/permission/index.ts @@ -0,0 +1,21 @@ +import { useUserStoreHook } from '@/store/modules/user' +import { Directive } from 'vue' + +/** 权限指令 */ +export const permission: Directive = { + mounted(el, binding) { + const { value } = binding + const roles = useUserStoreHook().roles + if (value && value instanceof Array && value.length > 0) { + const permissionRoles = value + const hasPermission = roles.some((role: any) => { + return permissionRoles.includes(role) + }) + if (!hasPermission) { + el.style.display = 'none' + } + } else { + throw new Error("need roles! Like v-permission=\"['admin','editor']\"") + } + } +} diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue new file mode 100644 index 0000000..2462d64 --- /dev/null +++ b/src/layout/components/AppMain.vue @@ -0,0 +1,48 @@ + + + + + + diff --git a/src/layout/components/BreadCrumb/index.vue b/src/layout/components/BreadCrumb/index.vue new file mode 100644 index 0000000..f648d5c --- /dev/null +++ b/src/layout/components/BreadCrumb/index.vue @@ -0,0 +1,83 @@ + + + + + + diff --git a/src/layout/components/Hamburger/index.vue b/src/layout/components/Hamburger/index.vue new file mode 100644 index 0000000..952938d --- /dev/null +++ b/src/layout/components/Hamburger/index.vue @@ -0,0 +1,32 @@ + + + + + + diff --git a/src/layout/components/NavigationBar/index.vue b/src/layout/components/NavigationBar/index.vue new file mode 100644 index 0000000..f5b774a --- /dev/null +++ b/src/layout/components/NavigationBar/index.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/src/layout/components/RightPanel/index.vue b/src/layout/components/RightPanel/index.vue new file mode 100644 index 0000000..4f2fd58 --- /dev/null +++ b/src/layout/components/RightPanel/index.vue @@ -0,0 +1,45 @@ + + + + + + diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..fdc2485 --- /dev/null +++ b/src/layout/components/Settings/index.vue @@ -0,0 +1,118 @@ + + + + + + diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..24de9ec --- /dev/null +++ b/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,118 @@ + + + + + + diff --git a/src/layout/components/Sidebar/SidebarItemLink.vue b/src/layout/components/Sidebar/SidebarItemLink.vue new file mode 100644 index 0000000..a5218ee --- /dev/null +++ b/src/layout/components/Sidebar/SidebarItemLink.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/layout/components/Sidebar/SidebarLogo.vue b/src/layout/components/Sidebar/SidebarLogo.vue new file mode 100644 index 0000000..7ec5850 --- /dev/null +++ b/src/layout/components/Sidebar/SidebarLogo.vue @@ -0,0 +1,77 @@ + + + + + + diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..5cb1a42 --- /dev/null +++ b/src/layout/components/Sidebar/index.vue @@ -0,0 +1,142 @@ + + + + + + + diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue new file mode 100644 index 0000000..eec31e4 --- /dev/null +++ b/src/layout/components/TagsView/ScrollPane.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..b290303 --- /dev/null +++ b/src/layout/components/TagsView/index.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/src/layout/components/index.ts b/src/layout/components/index.ts new file mode 100644 index 0000000..8368a29 --- /dev/null +++ b/src/layout/components/index.ts @@ -0,0 +1,6 @@ +export { default as AppMain } from './AppMain.vue' +export { default as NavigationBar } from './navigation-bar/index.vue' +export { default as Settings } from './settings/index.vue' +export { default as Sidebar } from './sidebar/index.vue' +export { default as TagsView } from './tags-view/index.vue' +export { default as RightPanel } from './right-panel/index.vue' diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 0000000..c280bf5 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,159 @@ + + + + + + diff --git a/src/layout/useResize.ts b/src/layout/useResize.ts new file mode 100644 index 0000000..d831378 --- /dev/null +++ b/src/layout/useResize.ts @@ -0,0 +1,68 @@ +import { useAppStore, DeviceType } from '@/store/modules/app' +import { computed, watch } from 'vue' +import { useRoute } from 'vue-router' + +/** 参考 Bootstrap 的响应式设计 width = 992 */ +const WIDTH = 992 + +/** 根据大小变化重新布局 */ +export default function() { + const appStore = useAppStore() + + const device = computed(() => { + return appStore.device + }) + + const sidebar = computed(() => { + return appStore.sidebar + }) + + const currentRoute = useRoute() + + const watchRouter = watch( + () => currentRoute.name, + () => { + if (appStore.device === DeviceType.Mobile && appStore.sidebar.opened) { + appStore.closeSidebar(false) + } + } + ) + + const isMobile = () => { + const rect = document.body.getBoundingClientRect() + return rect.width - 1 < WIDTH + } + + const resizeMounted = () => { + if (isMobile()) { + appStore.toggleDevice(DeviceType.Mobile) + appStore.closeSidebar(true) + } + } + + const resizeHandler = () => { + if (!document.hidden) { + appStore.toggleDevice(isMobile() ? DeviceType.Mobile : DeviceType.Desktop) + if (isMobile()) { + appStore.closeSidebar(true) + } + } + } + + const addEventListenerOnResize = () => { + window.addEventListener('resize', resizeHandler) + } + + const removeEventListenerResize = () => { + window.removeEventListener('resize', resizeHandler) + } + + return { + device, + sidebar, + resizeMounted, + addEventListenerOnResize, + removeEventListenerResize, + watchRouter + } +} diff --git a/src/main.ts b/src/main.ts index 01433bc..9a97399 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,19 @@ -import { createApp } from 'vue' +import { createApp, Directive } from 'vue' import App from './App.vue' +import router from './router' +import store from './store' +import '@/styles/index.scss' +import 'normalize.css' +import * as directives from '@/directives' +import '@/router/permission' +import loadSvg from '@/icons' -createApp(App).mount('#app') +const app = createApp(App) +// 加载全局 SVG +loadSvg(app) +// 自定义指令 +Object.keys(directives).forEach((key) => { + app.directive(key, (directives as { [key: string]: Directive })[key]) +}) + +app.use(store).use(router).mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..b038dd3 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,134 @@ +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' +const Layout = () => import('@/layout/index.vue') + +/** 常驻路由 */ +export const constantRoutes: Array = [ + { + path: '/redirect', + component: Layout, + meta: { + hidden: true + }, + children: [ + { + path: '/redirect/:path(.*)', + component: () => import('@/views/redirect/index.vue') + } + ] + }, + { + path: '/login', + component: () => import('@/views/login/index.vue'), + meta: { + hidden: true + } + }, + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + component: () => import('@/views/dashboard/index.vue'), + name: 'Dashboard', + meta: { + title: '首页', + icon: 'dashboard', + affix: true + } + } + ] + } +] + +/** + * 动态路由 + * 用来放置有权限的路由 + * 必须带有 name 属性 + */ +export const asyncRoutes: Array = [ + { + path: '/permission', + component: Layout, + redirect: '/permission/page', + name: 'Permission', + meta: { + title: '权限管理', + icon: 'lock', + roles: ['admin', 'editor'], // 可以在根路由中设置角色 + alwaysShow: true // 将始终显示根菜单 + }, + children: [ + { + path: 'page', + component: () => import('@/views/permission/page.vue'), + name: 'PagePermission', + meta: { + title: '页面权限', + roles: ['admin'] // 或者在子导航中设置角色 + } + }, + { + path: 'directive', + component: () => import('@/views/permission/directive.vue'), + name: 'DirectivePermission', + meta: { + title: '指令权限' // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色 + } + } + ] + }, + { + path: '/:pathMatch(.*)*', // 必须将 'ErrorPage' 路由放在最后, Must put the 'ErrorPage' route at the end + component: Layout, + redirect: '/404', + name: 'ErrorPage', + meta: { + title: '错误页面', + icon: '404', + hidden: true + }, + children: [ + { + path: '401', + component: () => import('@/views/error-page/401.vue'), + name: '401', + meta: { + title: '401' + } + }, + { + path: '404', + component: () => import('@/views/error-page/404.vue'), + name: '404', + meta: { + title: '404' + } + } + ] + } +] + +const router = createRouter({ + history: createWebHashHistory(), + routes: constantRoutes +}) + +/** 重置路由 */ +export function resetRouter() { + // 注意:所有动态路由路由必须带有 name 属性,否则可能会不能完全重置干净 + try { + router.getRoutes().forEach((route) => { + const { name, meta } = route + if (name && meta.roles?.length) { + router.hasRoute(name) && router.removeRoute(name) + } + }) + } catch (error) { + // 强制刷新浏览器,不过体验不是很好 + window.location.reload() + } +} + +export default router diff --git a/src/router/permission.ts b/src/router/permission.ts new file mode 100644 index 0000000..3f102ca --- /dev/null +++ b/src/router/permission.ts @@ -0,0 +1,73 @@ +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import router from '@/router' +import { RouteLocationNormalized } from 'vue-router' +import { useUserStoreHook } from '@/store/modules/user' +import { usePermissionStoreHook } from '@/store/modules/permission' +import { ElMessage } from 'element-plus' +import { whiteList } from '@/config/white-list' +import rolesSettings from '@/config/roles' +import { getToken } from '@/utils/cookies' + +const userStore = useUserStoreHook() +const permissionStore = usePermissionStoreHook() +NProgress.configure({ showSpinner: false }) + +router.beforeEach(async(to: RouteLocationNormalized, _: RouteLocationNormalized, next: any) => { + NProgress.start() + // 判断该用户是否登录 + if (getToken()) { + if (to.path === '/login') { + // 如果登录,并准备进入 login 页面,则重定向到主页 + next({ path: '/' }) + NProgress.done() + } else { + // 检查用户是否已获得其权限角色 + if (userStore.roles.length === 0) { + try { + if (rolesSettings.openRoles) { + // 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor'] + await userStore.getInfo() + // 获取接口返回的 roles + const roles = userStore.roles + // 根据角色生成可访问的 routes + permissionStore.setRoutes(roles) + } else { + // 没有开启角色功能,则启用默认角色 + userStore.setRoles(rolesSettings.defaultRoles) + permissionStore.setRoutes(rolesSettings.defaultRoles) + } + // 动态地添加可访问的 routes + permissionStore.dynamicRoutes.forEach((route) => { + router.addRoute(route) + }) + // 确保添加路由已完成 + // 设置 replace: true, 因此导航将不会留下历史记录 + next({ ...to, replace: true }) + } catch (err: any) { + // 删除 token,并重定向到登录页面 + userStore.resetToken() + ElMessage.error(err.message || 'Has Error') + next('/login') + NProgress.done() + } + } else { + next() + } + } + } else { + // 如果没有 token + if (whiteList.indexOf(to.path) !== -1) { + // 如果在免登录的白名单中,则直接进入 + next() + } else { + // 其他没有访问权限的页面将被重定向到登录页面 + next('/login') + NProgress.done() + } + } +}) + +router.afterEach(() => { + NProgress.done() +}) diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..94e2432 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,5 @@ +import { createPinia } from 'pinia' + +const store = createPinia() + +export default store diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts new file mode 100644 index 0000000..729231b --- /dev/null +++ b/src/store/modules/app.ts @@ -0,0 +1,68 @@ +import { defineStore } from 'pinia' +import { getSidebarStatus, getActiveThemeName, setSidebarStatus, setActiveThemeName } from '@/utils/cookies' +import themeList from '@/config/theme' + +export enum DeviceType { + Mobile, + Desktop +} + +interface IAppState { + device: DeviceType + sidebar: { + opened: boolean + withoutAnimation: boolean + } + /** 主题列表 */ + themeList: { title: string, name: string }[] + /** 正在应用的主题的名字 */ + activeThemeName: string +} + +export const useAppStore = defineStore({ + id: 'app', + state: (): IAppState => { + return { + device: DeviceType.Desktop, + sidebar: { + opened: getSidebarStatus() !== 'closed', + withoutAnimation: false + }, + themeList: themeList, + activeThemeName: getActiveThemeName() || 'normal' + } + }, + actions: { + toggleSidebar(withoutAnimation: boolean) { + this.sidebar.opened = !this.sidebar.opened + this.sidebar.withoutAnimation = withoutAnimation + if (this.sidebar.opened) { + setSidebarStatus('opened') + } else { + setSidebarStatus('closed') + } + }, + closeSidebar(withoutAnimation: boolean) { + this.sidebar.opened = false + this.sidebar.withoutAnimation = withoutAnimation + setSidebarStatus('closed') + }, + toggleDevice(device: DeviceType) { + this.device = device + }, + setTheme(activeThemeName: string) { + // 检查这个主题在主题列表里是否存在 + this.activeThemeName = this.themeList.find((theme) => theme.name === activeThemeName) + ? activeThemeName + : this.themeList[0].name + // 应用到 dom + document.body.className = `theme-${this.activeThemeName}` + // 持久化 + setActiveThemeName(this.activeThemeName) + }, + initTheme() { + // 初始化 + document.body.className = `theme-${this.activeThemeName}` + } + } +}) diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts new file mode 100644 index 0000000..df3cfa9 --- /dev/null +++ b/src/store/modules/permission.ts @@ -0,0 +1,64 @@ +import store from '@/store' +import { defineStore } from 'pinia' +import { RouteRecordRaw } from 'vue-router' +import { constantRoutes, asyncRoutes } from '@/router' + +interface IPermissionState { + routes: RouteRecordRaw[] + dynamicRoutes: RouteRecordRaw[] +} + +const hasPermission = (roles: string[], route: RouteRecordRaw) => { + if (route.meta && route.meta.roles) { + return roles.some((role) => { + if (route.meta?.roles !== undefined) { + return route.meta.roles.includes(role) + } else { + return false + } + }) + } else { + return true + } +} + +const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { + const res: RouteRecordRaw[] = [] + routes.forEach((route) => { + const r = { ...route } + if (hasPermission(roles, r)) { + if (r.children) { + r.children = filterAsyncRoutes(r.children, roles) + } + res.push(r) + } + }) + return res +} + +export const usePermissionStore = defineStore({ + id: 'permission', + state: (): IPermissionState => { + return { + routes: [], + dynamicRoutes: [] + } + }, + actions: { + setRoutes(roles: string[]) { + let accessedRoutes + if (roles.includes('admin')) { + accessedRoutes = asyncRoutes + } else { + accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) + } + this.routes = constantRoutes.concat(accessedRoutes) + this.dynamicRoutes = accessedRoutes + } + } +}) + +/** 在 setup 外使用 */ +export function usePermissionStoreHook() { + return usePermissionStore(store) +} diff --git a/src/store/modules/settings.ts b/src/store/modules/settings.ts new file mode 100644 index 0000000..39961ef --- /dev/null +++ b/src/store/modules/settings.ts @@ -0,0 +1,52 @@ +import { defineStore } from 'pinia' +import layoutSettings from '@/config/layout' + +interface ISettingsState { + fixedHeader: boolean + showSettings: boolean + showTagsView: boolean + showSidebarLogo: boolean + showThemeSwitch: boolean + showScreenfull: boolean +} + +export const useSettingsStore = defineStore({ + id: 'settings', + state: (): ISettingsState => { + return { + fixedHeader: layoutSettings.fixedHeader, + showSettings: layoutSettings.showSettings, + showTagsView: layoutSettings.showTagsView, + showSidebarLogo: layoutSettings.showSidebarLogo, + showThemeSwitch: layoutSettings.showThemeSwitch, + showScreenfull: layoutSettings.showScreenfull + } + }, + actions: { + changeSetting(payload: { key: string, value: any }) { + const { key, value } = payload + switch (key) { + case 'fixedHeader': + this.fixedHeader = value + break + case 'showSettings': + this.showSettings = value + break + case 'showSidebarLogo': + this.showSidebarLogo = value + break + case 'showTagsView': + this.showTagsView = value + break + case 'showThemeSwitch': + this.showThemeSwitch = value + break + case 'showScreenfull': + this.showScreenfull = value + break + default: + break + } + } + } +}) diff --git a/src/store/modules/tags-view.ts b/src/store/modules/tags-view.ts new file mode 100644 index 0000000..fa88bad --- /dev/null +++ b/src/store/modules/tags-view.ts @@ -0,0 +1,56 @@ +import { defineStore } from 'pinia' +import { _RouteLocationBase, RouteLocationNormalized } from 'vue-router' + +export interface ITagView extends Partial { + title?: string + to?: _RouteLocationBase +} + +interface ITagsViewState { + visitedViews: ITagView[] +} + +export const useTagsViewStore = defineStore({ + id: 'tags-view', + state: (): ITagsViewState => { + return { + visitedViews: [] + } + }, + actions: { + addVisitedView(view: ITagView) { + if (this.visitedViews.some((v) => v.path === view.path)) return + this.visitedViews.push( + Object.assign({}, view, { + title: view.meta?.title || 'no-name' + }) + ) + }, + delVisitedView(view: ITagView) { + for (const [i, v] of this.visitedViews.entries()) { + if (v.path === view.path) { + this.visitedViews.splice(i, 1) + break + } + } + }, + delOthersVisitedViews(view: ITagView) { + this.visitedViews = this.visitedViews.filter((v) => { + return v.meta?.affix || v.path === view.path + }) + }, + delAllVisitedViews() { + // keep affix tags + const affixTags = this.visitedViews.filter((tag) => tag.meta?.affix) + this.visitedViews = affixTags + }, + updateVisitedView(view: ITagView) { + for (let v of this.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + } + } +}) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..8482a90 --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,89 @@ +import store from '@/store' +import { defineStore } from 'pinia' +import { usePermissionStore } from './permission' +import { getToken, removeToken, setToken } from '@/utils/cookies' +import router, { resetRouter } from '@/router' +import { accountLogin, userInfoRequest } from '@/api/login' +import { RouteRecordRaw } from 'vue-router' + +interface IUserState { + token: string + roles: string[] +} + +export const useUserStore = defineStore({ + id: 'user', + state: (): IUserState => { + return { + token: getToken() || '', + roles: [] + } + }, + actions: { + /** 设置角色数组 */ + setRoles(roles: string[]) { + this.roles = roles + }, + /** 登录 */ + login(userInfo: { username: string, password: string }) { + return new Promise((resolve, reject) => { + accountLogin({ + username: userInfo.username.trim(), + password: userInfo.password + }) + .then((res: any) => { + setToken(res.data.accessToken) + this.token = res.data.accessToken + resolve(true) + }) + .catch((error) => { + reject(error) + }) + }) + }, + /** 获取用户详情 */ + getInfo() { + return new Promise((resolve, reject) => { + userInfoRequest() + .then((res: any) => { + this.roles = res.data.user.roles + resolve(res) + }) + .catch((error) => { + reject(error) + }) + }) + }, + /** 切换角色 */ + async changeRoles(role: string) { + const token = role + '-token' + this.token = token + setToken(token) + await this.getInfo() + const permissionStore = usePermissionStore() + permissionStore.setRoutes(this.roles) + resetRouter() + permissionStore.dynamicRoutes.forEach((item: RouteRecordRaw) => { + router.addRoute(item) + }) + }, + /** 登出 */ + logout() { + removeToken() + this.token = '' + this.roles = [] + resetRouter() + }, + /** 重置 token */ + resetToken() { + removeToken() + this.token = '' + this.roles = [] + } + } +}) + +/** 在 setup 外使用 */ +export function useUserStoreHook() { + return useUserStore(store) +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..f65306f --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,42 @@ +@import './mixins.scss'; // mixins +@import './transition.scss'; // transition +@import './theme/register.scss'; // 注册主题 + +.app-container { + padding: 20px; +} + +html { + height: 100%; +} + +body { + height: 100%; + background-color: #f0f2f5; // 全局背景色 + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, + sans-serif; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: border-box; +} + +a, +a:focus, +a:hover { + color: inherit; + outline: none; + text-decoration: none; +} + +div:focus { + outline: none; +} diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss new file mode 100644 index 0000000..8044266 --- /dev/null +++ b/src/styles/mixins.scss @@ -0,0 +1,7 @@ +@mixin clearfix { + &:after { + content: ''; + display: table; + clear: both; + } +} diff --git a/src/styles/theme/dark/index.scss b/src/styles/theme/dark/index.scss new file mode 100644 index 0000000..7b69d57 --- /dev/null +++ b/src/styles/theme/dark/index.scss @@ -0,0 +1,2 @@ +@import './setting.scss'; +@import '../theme.scss'; diff --git a/src/styles/theme/dark/setting.scss b/src/styles/theme/dark/setting.scss new file mode 100644 index 0000000..cfa8410 --- /dev/null +++ b/src/styles/theme/dark/setting.scss @@ -0,0 +1,14 @@ +// 主题名称 +$theme-name: 'dark'; +// 主题背景颜色 +$theme-bg-color: #151515; +// active 状态下主题背景颜色 +$active-theme-bg-color: #409eff; +// 默认文字颜色 +$font-color: #c0c4cc; +// active 状态下文字颜色 +$active-font-color: #fff; +// hover 状态下文字颜色 +$hover-color: #fff; +// 边框颜色 +$border-color: #303133; diff --git a/src/styles/theme/register.scss b/src/styles/theme/register.scss new file mode 100644 index 0000000..e12560e --- /dev/null +++ b/src/styles/theme/register.scss @@ -0,0 +1,2 @@ +// 注册的主题 +@import '@/styles/theme/dark/index.scss'; diff --git a/src/styles/theme/theme.scss b/src/styles/theme/theme.scss new file mode 100644 index 0000000..6acad90 --- /dev/null +++ b/src/styles/theme/theme.scss @@ -0,0 +1,214 @@ +.theme-#{$theme-name} { + /** Layout */ + + .app-wrapper { + background-color: $theme-bg-color; + color: $font-color; + // 侧边栏 + .sidebar-container { + .sidebar-logo-container { + background-color: lighten($theme-bg-color, 2%) !important; + } + .el-menu { + background-color: lighten($theme-bg-color, 4%) !important; + .el-menu-item { + background-color: lighten($theme-bg-color, 4%) !important; + &.is-active, + &:hover { + background-color: lighten($theme-bg-color, 8%) !important; + color: $active-font-color !important; + } + } + } + .el-sub-menu__title { + background-color: lighten($theme-bg-color, 4%) !important; + } + } + + // 顶部导航栏 + .navbar { + background-color: $theme-bg-color; + .el-breadcrumb__inner { + a { + color: $font-color; + &:hover { + color: $hover-color; + } + } + .no-redirect { + color: $font-color; + } + } + .right-menu { + .el-icon { + color: $font-color; + } + .el-avatar { + background: lighten($theme-bg-color, 20%); + .el-icon { + color: #fff; + } + } + } + } + + // tags-view + .tags-view-container { + background-color: $theme-bg-color !important; + border-bottom: 1px solid lighten($theme-bg-color, 10%) !important; + .tags-view-item { + background-color: $theme-bg-color !important; + color: $font-color !important; + border: 1px solid $border-color !important; + &.active { + background-color: $active-theme-bg-color !important; + color: $active-font-color !important; + border-color: $border-color !important; + } + } + .contextmenu { + // 右键菜单 + background-color: lighten($theme-bg-color, 8%); + color: $font-color; + li:hover { + background-color: lighten($theme-bg-color, 16%); + color: $active-font-color; + } + } + } + + // 右侧设置面板 + .handle-button { + background-color: lighten($theme-bg-color, 20%) !important; + } + .el-drawer.rtl { + background-color: $theme-bg-color; + .drawer-title, + .drawer-item { + color: $font-color; + } + } + } + + /** app-main 主要写 view 页面的黑暗样式 */ + + .app-main { + // 指令权限页面 /permission/directive + .permission-alert { + background-color: lighten($theme-bg-color, 8%); + } + // 监控页面 /monitor + .monitor { + background-color: $theme-bg-color; + } + } + + /** login 页面 */ + + .login-container { + background-color: $theme-bg-color; + color: $font-color; + .login-card { + background-color: lighten($theme-bg-color, 4%) !important; + } + .el-icon { + color: $font-color; + } + } + + /** element-plus */ + + // 侧边栏的 item 的 popper + .el-popper { + border: none !important; + .el-menu { + background-color: lighten($theme-bg-color, 4%) !important; + .el-menu-item { + background-color: lighten($theme-bg-color, 4%) !important; + &.is-active, + &:hover { + background-color: lighten($theme-bg-color, 8%) !important; + color: $active-font-color !important; + } + } + .el-sub-menu__title { + background-color: lighten($theme-bg-color, 4%) !important; + } + } + } + + // 下拉菜单 + .el-dropdown__popper .el-dropdown__list { + background-color: lighten($theme-bg-color, 8%); + .el-dropdown-menu { + background-color: lighten($theme-bg-color, 8%); + .el-dropdown-menu__item { + color: $font-color; + &.is-disabled { + color: #606266; + } + &:not(.is-disabled):hover { + background-color: lighten($theme-bg-color, 16%); + color: $active-font-color; + } + } + .el-dropdown-menu__item--divided:before { + background-color: lighten($theme-bg-color, 8%); + } + } + } + .el-popper__arrow::before { + // 下拉菜单顶部三角区域 + background-color: lighten($theme-bg-color, 8%) !important; + border: lighten($theme-bg-color, 8%) !important; + } + + // 单选框按钮样式 + .el-radio-button__inner { + background-color: lighten($theme-bg-color, 8%); + color: $active-font-color; + border: 1px solid $border-color; + } + .el-radio-button:first-child .el-radio-button__inner { + border-left: none; + } + + // el-tag + .el-tag { + background-color: lighten($theme-bg-color, 8%); + border-color: $border-color; + color: $active-font-color; + &.el-tag--info { + background-color: lighten($theme-bg-color, 8%); + border-color: $border-color; + color: $active-font-color; + } + } + + // tabs 标签页 + .el-tabs--border-card { + background: lighten($theme-bg-color, 8%); + border: 1px solid $border-color; + .el-tabs__header { + background-color: lighten($theme-bg-color, 8%); + border-bottom: 1px solid $border-color; + .el-tabs__item.is-active { + background-color: lighten($theme-bg-color, 8%); + border-right-color: $border-color; + border-left-color: $border-color; + } + } + } + + // 卡片 card + .el-card { + background: lighten($theme-bg-color, 8%); + border: 1px solid $border-color; + color: $font-color; + } + + // 输入框 input + .el-input__wrapper { + background: lighten($theme-bg-color, 8%) !important; + } +} diff --git a/src/styles/transition.scss b/src/styles/transition.scss new file mode 100644 index 0000000..92c966f --- /dev/null +++ b/src/styles/transition.scss @@ -0,0 +1,48 @@ +// See https://vuejs.org/v2/guide/transitions.html for detail + +// fade +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +// fade-transform +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all 0.5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +// breadcrumb +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all 0.5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all 0.5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/src/types/components.d.ts b/src/types/components.d.ts index ca0e674..78771e8 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -6,10 +6,19 @@ import '@vue/runtime-core' declare module '@vue/runtime-core' { export interface GlobalComponents { ElButton: typeof import('element-plus/es')['ElButton'] - HelloWorld: typeof import('./../components/HelloWorld.vue')['default'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + Screenfull: typeof import('./../components/Screenfull/index.vue')['default'] SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default'] + ThemeSwitch: typeof import('./../components/ThemeSwitch/index.vue')['default'] } } diff --git a/src/types/env.d.ts b/src/types/env.d.ts index b9071b5..73c071b 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -1,13 +1,5 @@ /// -/** 声明自动引入的 vue 组件 */ -declare module '*.vue' { - import type { DefineComponent } from 'vue' - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types - const component: DefineComponent<{}, {}, any> - export default component -} - /** 声明 vite 环境变量的类型(如果未声明则默认是 any) */ declare interface ImportMetaEnv { readonly VITE_BASE_API: string diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..9eec8b1 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,10 @@ +/** 项目类型声明 */ +declare module '*.svg' +declare module '*.png' +declare module '*.jpg' +declare module '*.jpeg' +declare module '*.gif' +declare module '*.bmp' +declare module '*.tiff' +declare module '*.yaml' +declare module '*.json' diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts new file mode 100644 index 0000000..c82a607 --- /dev/null +++ b/src/types/shims-vue.d.ts @@ -0,0 +1,15 @@ +/** 声明自动引入的 vue 组件 */ +declare module '*.vue' { + import { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +declare module '*.gif' { + export const gif: any +} + +declare module '*.svg' { + const content: any + export default content +} diff --git a/src/types/vue-proptery.d.ts b/src/types/vue-proptery.d.ts new file mode 100644 index 0000000..2fc8499 --- /dev/null +++ b/src/types/vue-proptery.d.ts @@ -0,0 +1,14 @@ +import { ElMessage } from 'element-plus' + +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + $message: ElMessage + } +} + +declare module 'vue-router' { + interface RouteMeta { + roles?: string[] + activeMenu?: string + } +} diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts new file mode 100644 index 0000000..39247fa --- /dev/null +++ b/src/utils/cookies.ts @@ -0,0 +1,16 @@ +/** cookies 封装 */ + +import Keys from '@/constant/key' +import Cookies from 'js-cookie' + +export const getSidebarStatus = () => Cookies.get(Keys.sidebarStatus) +export const setSidebarStatus = (sidebarStatus: string) => Cookies.set(Keys.sidebarStatus, sidebarStatus) + +export const getToken = () => Cookies.get(Keys.token) +export const setToken = (token: string) => Cookies.set(Keys.token, token) +export const removeToken = () => Cookies.remove(Keys.token) + +export const getActiveThemeName = () => Cookies.get(Keys.activeThemeName) +export const setActiveThemeName = (themeName: string) => { + Cookies.set(Keys.activeThemeName, themeName) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..1165452 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,10 @@ +import dayjs from 'dayjs' + +/** 格式化时间 */ +export const formatDateTime = (time: any) => { + if (time == null || time === '') { + return 'N/A' + } + const date = new Date(time) + return dayjs(date).format('YYYY-MM-DD HH:mm:ss') +} diff --git a/src/utils/permission.ts b/src/utils/permission.ts new file mode 100644 index 0000000..0d3405d --- /dev/null +++ b/src/utils/permission.ts @@ -0,0 +1,15 @@ +import { useUserStoreHook } from '@/store/modules/user' + +/** 全局权限判断函数,和指令 v-permission 功能类似 */ +export const checkPermission = (value: string[]): boolean => { + if (value && value instanceof Array && value.length > 0) { + const roles = useUserStoreHook().roles + const permissionRoles = value + return roles.some((role) => { + return permissionRoles.includes(role) + }) + } else { + console.error("need roles! Like v-permission=\"['admin','editor']\"") + return false + } +} diff --git a/src/utils/service.ts b/src/utils/service.ts new file mode 100644 index 0000000..245efe0 --- /dev/null +++ b/src/utils/service.ts @@ -0,0 +1,112 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' +import { get } from 'lodash-es' +import { ElMessage } from 'element-plus' +import { getToken } from '@/utils/cookies' +import { useUserStoreHook } from '@/store/modules/user' + +/** 创建请求实例 */ +function createService() { + // 创建一个 axios 实例 + const service = axios.create() + // 请求拦截 + service.interceptors.request.use( + (config) => config, + // 发送失败 + (error) => Promise.reject(error) + ) + // 响应拦截(可根据具体业务作出相应的调整) + service.interceptors.response.use( + (response) => { + // apiData 是 api 返回的数据 + const apiData = response.data as any + // 这个 code 是和后端约定的业务 code + const code = apiData.code + // 如果没有 code, 代表这不是项目后端开发的 api + if (code === undefined) { + ElMessage.error('非本系统的接口') + return Promise.reject(new Error('非本系统的接口')) + } else { + switch (code) { + case 0: + // code === 0 代表没有错误 + return apiData + case 20000: + // code === 20000 代表没有错误 + return apiData + default: + // 不是正确的 code + ElMessage.error(apiData.msg || 'Error') + return Promise.reject(new Error('Error')) + } + } + }, + (error) => { + // status 是 HTTP 状态码 + const status = get(error, 'response.status') + switch (status) { + case 400: + error.message = '请求错误' + break + case 401: + error.message = '未授权,请登录' + break + case 403: + // token 过期时,直接退出登录并强制刷新页面(会重定向到登录页) + useUserStoreHook().logout() + location.reload() + break + case 404: + error.message = '请求地址出错' + break + case 408: + error.message = '请求超时' + break + case 500: + error.message = '服务器内部错误' + break + case 501: + error.message = '服务未实现' + break + case 502: + error.message = '网关错误' + break + case 503: + error.message = '服务不可用' + break + case 504: + error.message = '网关超时' + break + case 505: + error.message = 'HTTP版本不受支持' + break + default: + break + } + ElMessage.error(error.message) + return Promise.reject(error) + } + ) + return service +} + +/** 创建请求方法 */ +function createRequestFunction(service: AxiosInstance) { + return function(config: AxiosRequestConfig) { + const configDefault = { + headers: { + // 携带 token + 'X-Access-Token': getToken(), + 'Content-Type': get(config, 'headers.Content-Type', 'application/json') + }, + timeout: 5000, + baseURL: process.env.VUE_APP_BASE_API, + data: {} + } + return service(Object.assign(configDefault, config)) + } +} + +/** 用于网络请求的实例 */ +export const service = createService() +/** 用于网络请求的方法 */ +export const request = createRequestFunction(service) diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 0000000..4175e07 --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,14 @@ +export const isExternal = (path: string) => /^(https?:|mailto:|tel:)/.test(path) + +export const isArray = (arg: any) => { + if (typeof Array.isArray === 'undefined') { + return Object.prototype.toString.call(arg) === '[object Array]' + } + return Array.isArray(arg) +} + +export const isValidURL = (url: string) => { + const reg = + /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return reg.test(url) +} diff --git a/src/views/dashboard/admin/index.vue b/src/views/dashboard/admin/index.vue new file mode 100644 index 0000000..229d072 --- /dev/null +++ b/src/views/dashboard/admin/index.vue @@ -0,0 +1,6 @@ + + diff --git a/src/views/dashboard/editor/index.vue b/src/views/dashboard/editor/index.vue new file mode 100644 index 0000000..66b985f --- /dev/null +++ b/src/views/dashboard/editor/index.vue @@ -0,0 +1,6 @@ + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..673502c --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/error-page/401.vue b/src/views/error-page/401.vue new file mode 100644 index 0000000..360d385 --- /dev/null +++ b/src/views/error-page/401.vue @@ -0,0 +1,10 @@ + diff --git a/src/views/error-page/404.vue b/src/views/error-page/404.vue new file mode 100644 index 0000000..53ebf5d --- /dev/null +++ b/src/views/error-page/404.vue @@ -0,0 +1,10 @@ + diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..ef60d34 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/src/views/permission/components/switch-roles.vue b/src/views/permission/components/switch-roles.vue new file mode 100644 index 0000000..95e04ff --- /dev/null +++ b/src/views/permission/components/switch-roles.vue @@ -0,0 +1,29 @@ + + + + diff --git a/src/views/permission/directive.vue b/src/views/permission/directive.vue new file mode 100644 index 0000000..fb47032 --- /dev/null +++ b/src/views/permission/directive.vue @@ -0,0 +1,92 @@ + + + + + + diff --git a/src/views/permission/page.vue b/src/views/permission/page.vue new file mode 100644 index 0000000..87ef54b --- /dev/null +++ b/src/views/permission/page.vue @@ -0,0 +1,21 @@ + + + + diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue new file mode 100644 index 0000000..aedd81e --- /dev/null +++ b/src/views/redirect/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/vite.config.ts b/vite.config.ts index d3202ee..c08205a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { ConfigEnv, UserConfigExport } from 'vite' +import { UserConfigExport } from 'vite' import path, { resolve } from 'path' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' @@ -7,7 +7,7 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' /** 配置项文档:https://vitejs.dev/config */ -export default (env: ConfigEnv): UserConfigExport => { +export default (): UserConfigExport => { return { /** build 打包时根据实际情况修改 base */ base: '/',