Browse Source

feat: first commit

main
mashirozx 7 months ago
commit
0ec27c9c14
  1. 3
      .browserslistrc
  2. 14
      .editorconfig
  3. 23
      .eslintrc.js
  4. 23
      .gitignore
  5. 3
      .prettierrc.js
  6. 29
      README.md
  7. 3
      babel.config.js
  8. 3
      jest.config.js
  9. 10
      jsconfig.json
  10. 51
      package.json
  11. BIN
      public/favicon.ico
  12. 17
      public/index.html
  13. 9
      src/App.vue
  14. 0
      src/assets/.gitkeep
  15. BIN
      src/assets/images/auth/earth.jpg
  16. BIN
      src/assets/images/auth/earth.mp4
  17. BIN
      src/assets/images/auth/mountain.jpg
  18. 11
      src/assets/images/trademarks/app-logo.svg
  19. 21
      src/components/HelloWorld.vue
  20. 5
      src/configs/index.js
  21. 26
      src/layouts/components/AppMain.vue
  22. 43
      src/layouts/components/Breadcrumb.vue
  23. 71
      src/layouts/components/Header.vue
  24. 35
      src/layouts/components/Link.vue
  25. 38
      src/layouts/components/Logo.vue
  26. 64
      src/layouts/components/SideBar.vue
  27. 23
      src/layouts/components/SideBarFooter.vue
  28. 88
      src/layouts/components/SideBarItem.vue
  29. 38
      src/layouts/components/SideBarTitle.vue
  30. 293
      src/layouts/components/Tabs.vue
  31. 2
      src/layouts/parentLayout.js
  32. 76
      src/layouts/rootLayout.vue
  33. 29
      src/main.js
  34. 65
      src/routers/base.js
  35. 45
      src/routers/constantModules/dashboard.js
  36. 11
      src/routers/constantModules/index.js
  37. 11
      src/routers/constants.js
  38. 33
      src/routers/index.js
  39. 11
      src/routers/permissionModules/index.js
  40. 93
      src/routers/permissionModules/test.js
  41. 86
      src/routers/routerGuards.js
  42. 78
      src/routers/utils.js
  43. 19
      src/stores/index.js
  44. 71
      src/stores/modules/route.js
  45. 12
      src/stores/modules/sidebar.js
  46. 46
      src/stores/modules/tabs.js
  47. 38
      src/stores/modules/user.js
  48. 47
      src/styles/_aqua.scss
  49. 15
      src/styles/_mixins.scss
  50. 11
      src/styles/_theme.scss
  51. 18
      src/styles/element.scss
  52. 34
      src/styles/global.scss
  53. 27
      src/utils/hooks/useResizeObserver.js
  54. 17
      src/utils/loopBreaker.js
  55. 72
      src/utils/normalizeWheel.js
  56. 110
      src/utils/request.js
  57. 7
      src/utils/validate.js
  58. 5
      src/views/About.vue
  59. 18
      src/views/Home.vue
  60. 130
      src/views/auth/login.vue
  61. 13
      src/views/dashboard/console.vue
  62. 3
      src/views/exception/404.vue
  63. 3
      src/views/redirect/index.vue
  64. 12
      tests/unit/example.spec.js
  65. 16
      vue.config.js
  66. 10731
      yarn.lock

3
.browserslistrc

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

14
.editorconfig

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 160
trim_trailing_whitespace = true
[*.md]
max_line_length = 0

23
.eslintrc.js

@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/essential", "eslint:recommended"],
parserOptions: {
parser: "babel-eslint",
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-process-env": "error",
},
overrides: [
{
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
env: {
jest: true,
},
},
],
};

23
.gitignore vendored

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.prettierrc.js

@ -0,0 +1,3 @@
module.exports = {
// printWidth: 80,
};

29
README.md

@ -0,0 +1,29 @@
# agk-admin
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Run your unit tests
```
yarn test:unit
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

3
babel.config.js

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

3
jest.config.js

@ -0,0 +1,3 @@
module.exports = {
preset: "@vue/cli-plugin-unit-jest",
};

10
jsconfig.json

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}

51
package.json

@ -0,0 +1,51 @@
{
"name": "agk-admin",
"version": "0.1.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"jsdoc": "./node_modules/.bin/jsdoc"
},
"dependencies": {
"@vue/composition-api": "^1.4.1",
"axios": "^0.24.0",
"axios-case-converter": "^0.9.0",
"core-js": "^3.6.5",
"element-ui": "^2.15.7",
"lodash": "^4.17.21",
"modern-normalize": "^1.1.0",
"pinia": "^2.0.6",
"resize-observer-polyfill": "^1.5.1",
"vue": "^2.6.14",
"vue-router": "^3.2.0",
"vue-tippy": "^4.13.0",
"vue2-helpers": "^1.1.6"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@types/webpack-env": "^1.16.3",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.3",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"git-describe": "^4.1.0",
"jsdoc": "^3.6.7",
"prettier": "^2.2.1",
"sass": "^1.44.0",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
public/index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

9
src/App.vue

@ -0,0 +1,9 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<style lang="sass">
@import '@/styles/global.scss'
</style>

0
src/assets/.gitkeep

BIN
src/assets/images/auth/earth.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

BIN
src/assets/images/auth/earth.mp4

Binary file not shown.

BIN
src/assets/images/auth/mountain.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

11
src/assets/images/trademarks/app-logo.svg

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:2;}
</style>
<path class="st0" d="M12,22c5.5,0,10-4.5,10-10S17.5,2,12,2S2,6.5,2,12S6.5,22,12,22z"/>
<path class="st0" d="M12,22c3.9,0,7-3.1,7-7s-3.1-7-7-7s-7,3.1-7,7S8.1,22,12,22z"/>
<path class="st0" d="M12,22c2.2,0,4-1.8,4-4s-1.8-4-4-4s-4,1.8-4,4S9.8,22,12,22z"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

21
src/components/HelloWorld.vue

@ -0,0 +1,21 @@
<script>
import { defineComponent } from "@vue/composition-api";
import { useUserStore } from "@/stores/modules/user";
export default defineComponent({
setup() {
const user = useUserStore();
return { user };
},
});
</script>
<template>
<div class="page-container">
<p>{{ user.username || "none" }}</p>
<p>{{ user.token || "none" }}</p>
<el-button type="primary" size="default" @click="user.login">Login</el-button>
<el-button type="primary" size="default" @click="user.logout">Logout</el-button>
</div>
</template>

5
src/configs/index.js

@ -0,0 +1,5 @@
/* eslint no-process-env: 0 */
export const appName = "agk-admin";
export const baseUrl = process.env.BASE_URL;
export const gitVersion = process.env.VUE_APP_GIT_HASH;
export const apiBaseUrl = process.env.VUE_APP_BASE_URL;

26
src/layouts/components/AppMain.vue

@ -0,0 +1,26 @@
<script>
import { defineComponent, computed } from "@vue/composition-api";
import { useRoute } from "vue2-helpers/vue-router";
import { useRouteStore } from "@/stores/modules/route";
export default defineComponent({
name: "AppMain",
setup() {
const route = useRoute();
const routeStore = useRouteStore();
const keepAliveComponents = computed(() => routeStore.keepAliveComponents);
const fullPath = computed(() => route.fullPath);
return { keepAliveComponents, fullPath };
},
});
</script>
<template>
<section>
<transition name="fade-transform" mode="out-in">
<keep-alive :include="keepAliveComponents">
<router-view :key="fullPath" />
</keep-alive>
</transition>
</section>
</template>

43
src/layouts/components/Breadcrumb.vue

@ -0,0 +1,43 @@
<script>
import { defineComponent, computed } from "@vue/composition-api";
export default defineComponent({
setup(props, { root }) {
const generator = (routerMap) => {
return routerMap.map((item) => {
const currentMenu = {
...item,
label: item.meta.title,
key: item.name,
disabled: item.path === "/",
};
return currentMenu;
});
};
const breadcrumbList = computed(() => {
return generator(root.$route.matched);
});
return { breadcrumbList, r: root.$route };
},
});
</script>
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="routeItem in breadcrumbList" :key="routeItem.name">
<span class="link-text">
{{ routeItem.meta.title }}
</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<style lang="scss" scoped>
@use "../../styles/theme";
::v-deep {
.el-breadcrumb__inner {
color: theme.$text-primary-color;
font-weight: 500;
}
}
</style>

71
src/layouts/components/Header.vue

@ -0,0 +1,71 @@
<script>
import { defineComponent } from "@vue/composition-api";
import Breadcrumb from "./Breadcrumb.vue";
export default defineComponent({
name: "Header",
components: { Breadcrumb },
props: {
isMenuOpen: {
type: Boolean,
default: true,
},
},
setup() {
const vTippy = { placement: "bottom", arrow: true, arrowType: "round", theme: "light" };
return { vTippy };
},
});
</script>
<template>
<header class="header">
<div class="left">
<!-- <div class="icon-button" :content="`${$props.isMenuOpen ? '折叠' : '展开'}菜单`" v-tippy="vTippy">
<i :class="`fas fa-${$props.isMenuOpen ? 'outdent' : 'indent'}`"></i>
</div> -->
<Breadcrumb></Breadcrumb>
</div>
<div class="right">
<div class="icon-button" content="设置" v-tippy="vTippy">
<i class="fas fa-sliders-h"></i>
</div>
<div class="icon-button" content="账户" v-tippy="vTippy">
<i class="fas fa-user-circle"></i>
</div>
</div>
</header>
</template>
<style lang="scss" scoped>
@use "../../styles/theme";
.header {
height: 52px;
width: 100%;
background: theme.$block-bg-primary;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
> * {
display: flex;
align-items: center;
}
> .left {
justify-content: flex-start;
> * {
margin-left: 1rem;
}
}
> .right {
justify-content: flex-end;
> * {
margin-right: 1rem;
}
}
.icon-button {
font-size: 1.2rem;
cursor: pointer;
}
}
</style>

35
src/layouts/components/Link.vue

@ -0,0 +1,35 @@
<script lang="jsx">
import { defineComponent, computed } from "@vue/composition-api";
import { isExternal as isExternalFn } from "@/utils/validate.js";
export default defineComponent({
name: "Link",
props: {
to: { type: String, required: true },
},
setup(props) {
const isExternal = computed(() => isExternalFn(props.to));
const type = computed(() => (isExternal.value ? "a" : "router-link"));
const linkAttrs = computed(() =>
isExternal.value
? {
href: props.to,
target: "_blank",
rel: "noopener noreferrer",
}
: {
to: props.to,
}
);
return { type, linkAttrs };
},
});
</script>
<template>
<component :is="type" v-bind="linkAttrs">
<slot />
</component>
</template>

38
src/layouts/components/Logo.vue

@ -0,0 +1,38 @@
<script>
import { defineComponent } from "@vue/composition-api";
import { appName } from "@/configs";
export default defineComponent({
setup() {
return { appName };
},
});
</script>
<template>
<div class="logo">
<img class="image" :src="require('@/assets/images/trademarks/app-logo.svg')" alt="" />
<div class="title">{{ appName }}</div>
</div>
</template>
<style lang="scss" scoped>
.logo {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 1rem;
.image {
width: 1.6rem;
height: 1.6rem;
object-fit: contain;
}
.title {
color: #ffffff;
font-size: 1.2rem;
font-weight: 900;
text-transform: uppercase;
}
}
</style>

64
src/layouts/components/SideBar.vue

@ -0,0 +1,64 @@
<script>
import { defineComponent, computed } from "@vue/composition-api";
import { useRouteStore } from "@/stores/modules/route";
import SideBarItem from "./SideBarItem.vue";
import Logo from "./Logo.vue";
import SideBarFooter from "./SideBarFooter.vue";
export default defineComponent({
name: "SideBar",
components: { SideBarItem, Logo, SideBarFooter },
setup() {
const routeStore = useRouteStore();
const routes = computed(() => routeStore.getRouters);
return { routes };
},
});
</script>
<template>
<div class="side-bar">
<div class="logo">
<Logo></Logo>
</div>
<el-scrollbar class="menu">
<el-menu class="el-menu" background-color="transparent" text-color="#ffffff" active-text-color="#ffffff">
<template v-for="item in routes">
<SideBarItem v-if="!item.meta.hidden" :key="item.path" :item="item" base-path="/"></SideBarItem>
</template>
</el-menu>
</el-scrollbar>
<div class="menu-footer">
<SideBarFooter></SideBarFooter>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep {
.el-scrollbar__wrap {
overflow-x: hidden;
}
.el-menu {
border-right: none;
}
}
.side-bar {
width: 200px;
display: flex;
flex-flow: column nowrap;
.logo {
flex: 0 0 auto;
}
.menu {
flex: 1 1 auto;
height: 100%;
}
.menu-footer {
flex: 0 0 auto;
align-self: flex-end;
width: 100%;
}
}
</style>

23
src/layouts/components/SideBarFooter.vue

@ -0,0 +1,23 @@
<script>
import { defineComponent } from "@vue/composition-api";
import { gitVersion } from "@/configs";
export default defineComponent({
setup() {
return { gitVersion };
},
});
</script>
<template>
<p class="footer">&copy;2021 (build {{ gitVersion }})</p>
</template>
<style lang="scss" scoped>
.footer {
text-align: center;
color: #ffffff;
font-size: 1rem;
margin: 0.5rem;
}
</style>

88
src/layouts/components/SideBarItem.vue

@ -0,0 +1,88 @@
<script>
import { defineComponent, computed } from "@vue/composition-api";
import Link from "./Link.vue";
import { isExternal } from "@/utils/validate";
import path from "path";
import SideBarTitle from "./SideBarTitle.vue";
export default defineComponent({
name: "SidebarItem",
components: { Link, SideBarTitle },
props: {
item: { type: Object, required: true },
isNest: { type: Boolean, default: false },
basePath: { type: String, default: "" },
},
setup(props, { root }) {
const hasChildren = computed(() => props.item.children && props.item.children.length > 0);
const resolvePath = (currentPath) => {
if (isExternal(currentPath)) {
return currentPath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
return path.join(props.basePath, currentPath);
};
const currentFullPath = computed(() => root.$route.fullPath);
return { hasChildren, resolvePath, currentFullPath };
},
});
</script>
<template>
<el-submenu v-if="hasChildren" :index="$props.item.path">
<template slot="title">
<SideBarTitle :icon="$props.item.meta.icon" :context="$props.item.meta.title"></SideBarTitle>
</template>
<SidebarItem
v-for="child in $props.item.children"
:key="child.path"
:isNest="true"
:item="child"
:basePath="resolvePath($props.item.path)"
class="nest-menu"
/>
</el-submenu>
<Link v-else :to="resolvePath($props.item.path)">
<el-menu-item :index="$props.item.path" :class="{ active: resolvePath($props.item.path) === currentFullPath }">
<template slot="title">
<SideBarTitle :icon="$props.item.meta.icon" :context="$props.item.meta.title"></SideBarTitle>
</template>
</el-menu-item>
</Link>
</template>
<style lang="scss" scoped>
@use "../../styles/theme";
@mixin item-common {
background-color: transparent !important;
// font-size: 1rem;
font-weight: 600;
border-radius: 0.5rem;
margin: 0.1rem 0.5rem;
min-width: unset !important;
&:hover {
box-shadow: theme.$block-box-shadow;
}
&:hover,
&.active {
background-color: theme.$block-bg-light !important;
}
}
::v-deep {
.el-menu-item {
@include item-common;
}
.el-submenu__title {
@include item-common;
}
.el-submenu__icon-arrow {
color: theme.$text-white-color !important;
}
}
</style>

38
src/layouts/components/SideBarTitle.vue

@ -0,0 +1,38 @@
<script>
import { defineComponent } from "@vue/composition-api";
export default defineComponent({
name: "SideBarTitle",
props: {
context: String,
icon: String,
},
});
</script>
<template>
<div class="title">
<template v-if="icon">
<img v-if="/(.*)\.[svg|png|jpg|jpeg|gif]/i.test($props.icon)" class="icon" :src="$props.icon" />
<i v-else :class="['icon', $props.icon]"></i>
</template>
<span>{{ $props.context }}</span>
</div>
</template>
<style lang="scss" scoped>
@use "../../styles/theme";
.title {
color: theme.$text-white-color;
display: flex;
justify-content: flex-start;
align-items: center;
font-size: 0.9rem;
.icon {
height: 1rem;
font-size: 1rem;
color: theme.$text-white-color;
margin-right: 0.5rem;
}
}
</style>

293
src/layouts/components/Tabs.vue

@ -0,0 +1,293 @@
<script>
import { defineComponent, ref, computed, onMounted, onActivated, onBeforeUnmount, onDeactivated, nextTick, watch } from "@vue/composition-api";
import useResizeObserver from "@/utils/hooks/useResizeObserver";
import normalizeWheel from "@/utils/normalizeWheel";
import { useTabsStore } from "@/stores/modules/tabs";
import { useRouteStore } from "@/stores/modules/route";
import { useRouter } from "vue2-helpers/vue-router";
import { Notification } from "element-ui";
import { throttle } from "lodash";
export default defineComponent({
name: "Tabs",
setup(props, { root }) {
const router = useRouter();
const route = computed(() => root.$route);
// Pinia devtools bug
// https://github.com/vuejs/pinia/issues/905
const tabsStore = useTabsStore();
const routeStore = useRouteStore();
// drop useless route properties
const getSimpleRoute = (route) => {
const { fullPath, hash, meta, name, params, path, query } = route;
return { fullPath, hash, meta, name, params, path, query };
};
tabsStore.initTabs([]);
const currentActive = ref();
const tabs = computed(() => tabsStore?.list ?? []);
const whiteList = [];
// close specific tab
const handleCloseThisTab = (route) => {
if (tabs.value.length === 1) {
return Notification({
message: "这已经是最后一页,不能再关闭了!",
type: "warning",
duration: 1000,
});
}
tabsStore.closeThisTab(route);
removeFromKeepAliveComponentList(route);
// handle if is closing current page
if (currentActive.value === route.fullPath) {
const lastRoute = tabs.value[Math.max(0, tabs.value.length - 1)];
currentActive.value = lastRoute.fullPath;
router.push(lastRoute).catch(() => {});
}
};
const handleClickTab = (route) => {
if (currentActive.value === route.fullPath) {
return;
}
router.push(route.fullPath).catch(() => {});
};
// remove from keep alive components list
// @TODO: not tested
const removeFromKeepAliveComponentList = (route) => {
if (route.meta.keepAlive) {
const name = router.currentRoute.matched.find((item) => item.name == route.name)?.components?.default.name;
if (name) {
routeStore.setKeepAliveComponents(routeStore.keepAliveComponents.filter((item) => item != name));
}
}
};
const scrollViewRef = ref(null);
const scrollContainerRef = ref(null);
const scrollViewSize = useResizeObserver(scrollViewRef);
const scrollContainerSize = useResizeObserver(scrollContainerRef);
const shouldDisplayScrollController = computed(() => {
if (!scrollViewSize.value || !scrollContainerSize.value) {
return false;
}
return scrollViewSize.value.width > scrollContainerSize.value.width;
});
/**
* @param {number} scrollLeft
*/
const scrollController = (scrollLeft) => {
scrollLeft = Math.max(0, Math.min(scrollLeft, scrollViewSize.value.width - scrollContainerSize.value.width));
scrollContainerRef.value.scrollTo({
top: 0,
left: scrollLeft,
behavior: "smooth",
});
};
const scrollToTab = async (route) => {
const tab = tabs.value.find((item) => item.fullPath === route.fullPath)?.fullPath;
if (!tab) {
return;
}
await nextTick();
const element = document.getElementById(`tags-${tab.split("/").join("-")}`);
if (!element) return;
// const tabRect = element.getBoundingClientRect();
const offsetLeft = element.offsetLeft;
const scrollLeft = offsetLeft; // + scrollContainerSize.value.width / 2 - tabRect.width / 2;
scrollController(scrollLeft);
};
const scrollToLeft = () => scrollController(0);
const scrollToRight = () => scrollController(scrollViewSize.value.width - scrollContainerSize.value.width);
const scrollNext = () => scrollController(scrollContainerRef.value.scrollLeft + 100);
const scrollPrev = () => scrollController(scrollContainerRef.value.scrollLeft - 100);
/**
* Event listeners to remove
* @type {number[]}
*/
const removers = [];
/**
* on wheel event
* @param {WheelEvent} event
*/
const mouseWheelHandler = (event) => {
event.preventDefault();
const _event = normalizeWheel(event);
const container = scrollContainerRef.value;
scrollController(container.scrollLeft + _event.pixelY * 3);
};
const mouseWheelHandlerThrottle = throttle(mouseWheelHandler, 200);
const componentDidMount = async () => {
await nextTick();
const container = scrollContainerRef.value;
container.addEventListener("wheel", mouseWheelHandlerThrottle);
removers.push(() => container.removeEventListener("wheel", mouseWheelHandlerThrottle));
// Old Chrome
container.addEventListener("mousewheel", mouseWheelHandlerThrottle);
removers.push(() => container.removeEventListener("mousewheel", mouseWheelHandlerThrottle));
// Old Firefox
container.addEventListener("DOMMouseScroll", mouseWheelHandlerThrottle);
removers.push(() => container.removeEventListener("DOMMouseScroll", mouseWheelHandlerThrottle));
};
const componentWillUnmount = () => {
removers.forEach((remover) => remover());
};
onMounted(componentDidMount);
onBeforeUnmount(componentWillUnmount);
onActivated(componentDidMount);
onDeactivated(componentWillUnmount);
watch(
() => route.value.fullPath,
(to) => {
// console.log(to, route.value);
if (whiteList.includes(route.value.name));
currentActive.value = to;
tabsStore.addTabs(getSimpleRoute(route.value));
scrollToTab(route.value);
},
{ immediate: true }
);
return {
tabs,
currentActive,
handleCloseThisTab,
handleClickTab,
scrollContainerRef,
scrollViewRef,
shouldDisplayScrollController,
scrollNext,
scrollPrev,
scrollToLeft,
scrollToRight,
};
},
});
</script>
<template>
<section class="tabs">
<div class="scroll-wrapper">
<el-tag v-show="shouldDisplayScrollController" class="tag scroll-controller" size="normal" effect="plain" @click="scrollPrev"
><i class="el-icon-arrow-left"></i
></el-tag>
<div ref="scrollContainerRef" class="scroll-container">
<div ref="scrollViewRef" class="scroll-view">
<el-tag
:id="`tags-${tab.fullPath.split('/').join('-')}`"
:class="['tag', { active: currentActive === tab.fullPath }]"
:data-full="[currentActive, tab.fullPath]"
v-for="tab in tabs"
:key="tab.fullPath"
size="normal"
effect="plain"
closable
@close="handleCloseThisTab(tab)"
@click="handleClickTab(tab)"
>{{ tab.meta.title }}</el-tag
>
</div>
</div>
<el-tag v-show="shouldDisplayScrollController" class="tag scroll-controller" size="normal" effect="plain" @click="scrollNext"
><i class="el-icon-arrow-right"></i
></el-tag>
</div>
</section>
</template>
<style lang="scss" scoped>
@use "../../styles/theme";
.tabs {
--side-bar-width: 200px;
--gap-size: 0.5rem;
width: 100%;
user-select: none;
.scroll-wrapper {
width: calc(100vw - var(--gap-size, 0.5rem) * 2 - var(--side-bar-width, 200px));
margin: var(--gap-size, 0.5rem);
display: flex;
justify-content: center;
align-items: center;
.tag {
cursor: pointer;
margin-right: var(--gap-size, 0.5rem);
&:last-child {
margin-right: 0;
}
&.active {
background-color: theme.$primary-color;
color: #ffffff;
::v-deep {
.el-tag__close {
color: #ffffff;
&:hover {
background-color: theme.$block-bg-light;
}
}
}
}
}
.scroll-container {
flex: 1 1 auto;
width: 100%;
border-radius: 4px;
overflow: hidden;
.scroll-view {
white-space: nowrap;
width: auto;
width: fit-content;
block-size: fit-content;
position: relative;
> * {
display: inline-block;
}
}
}
.scroll-controller {
flex: 0 0 auto;
&:last-child {
margin-right: 0;
margin-left: var(--gap-size, 0.5rem);
}
}
}
}
::v-deep {
.el-tag--plain {
background-color: theme.$block-bg-primary;
color: theme.$text-primary-color;
border: none;
.el-tag__close {
color: theme.$text-primary-color;
&:hover {
background-color: theme.$block-bg-dark;
}
}
}
}
</style>

2
src/layouts/parentLayout.js

@ -0,0 +1,2 @@
import AppMain from "./components/AppMain.vue";
export default AppMain;

76
src/layouts/rootLayout.vue

@ -0,0 +1,76 @@
<script>
import { defineComponent } from "@vue/composition-api";
import SideBar from "./components/SideBar.vue";
import AppMain from "./components/AppMain.vue";
import Header from "./components/Header.vue";
import Tabs from "./components/Tabs.vue";
export default defineComponent({
components: { SideBar, AppMain, Header, Tabs },
setup() {
return;
},
});
</script>
<template>
<div class="layout-index">
<img class="background" :src="require('@/assets/images/auth/mountain.jpg')" />
<div class="side-bar">
<SideBar></SideBar>
</div>
<div class="view-box">
<div class="header">
<Header></Header>
</div>
<div class="tabs">
<Tabs></Tabs>
</div>
<div class="main">
<AppMain class="root-app-main"></AppMain>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@use "../styles/theme";
.layout-index {
display: flex;
width: 100%;
height: 100%;
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
}
.side-bar {
flex: 0 0 auto;
height: 100%;
background: theme.$block-bg-dark;
backdrop-filter: blur(1rem);
user-select: none;
}
.view-box {
flex: 1 1 auto;
width: 100%;
height: 100%;
background: theme.$block-bg-secondary;
> * {
width: 100%;
}
.root-app-main {
width: calc(100% - 1rem);
padding: 0.5rem;
margin: 0 0.5rem;
background: theme.$block-bg-primary;
border-radius: 0.5rem;
}
}
}
</style>
e

29
src/main.js

@ -0,0 +1,29 @@
import Vue from "vue";
import VueCompositionAPI from "@vue/composition-api";
import { PiniaVuePlugin } from "pinia";
import store from "@/stores";
import App from "@/App.vue";
// eslint-disable-next-line no-unused-vars
import router from "@/routers";
import ElementUI from "element-ui";
import VueTippy, { TippyComponent } from "vue-tippy";
import "modern-normalize/modern-normalize.css";
import "@/styles/element.scss";
import "@fortawesome/fontawesome-free/css/all.css"; // cSpell:ignore fortawesome
import "tippy.js/themes/light.css";
Vue.config.productionTip = false;
Vue.use(VueCompositionAPI);
Vue.use(ElementUI);
Vue.use(VueTippy);
Vue.use(PiniaVuePlugin);
Vue.use(store);
Vue.component("tippy", TippyComponent);
new Vue({
router,
pinia: store,
render: (h) => h(App),
}).$mount("#app");

65
src/routers/base.js

@ -0,0 +1,65 @@
import { ErrorPage, RedirectName, Layout, BaseHome } from "@/routers/constants";
// 404 on a page
export const ErrorPageRoute = {
path: "/:path(.*)*",
name: "ErrorPageParent",
component: Layout,
meta: {
title: "ErrorPage",
hideBreadcrumb: true,
hidden: true,
},
children: [
{
path: "/:path(.*)*",
name: "ErrorPageChild",
component: ErrorPage,
meta: {
title: "ErrorPage",
hideBreadcrumb: true,
},
},
],
};
export const RedirectRoute = {
path: "/redirect",
name: RedirectName + "Root",
component: Layout,
meta: {
title: RedirectName,
hideBreadcrumb: true,
hidden: true,
},
children: [
{
path: "/redirect/:path(.*)",
name: RedirectName + "Child",
component: () => import(/* webpackChunkName: "redirect" */ "@/views/redirect/index.vue"),
meta: {
title: RedirectName,
hideBreadcrumb: true,
},
},
],
};
export const RootRoute = {
path: "/",
name: "Root",
redirect: BaseHome,
meta: {
title: "Root",
hidden: true,
},
};
export const LoginRoute = {
path: "/auth/login",
name: "Login",
component: () => import(/* webpackChunkName: "authLogin" */ "@/views/auth/login.vue"),
meta: {
title: "登录",
hidden: true,
},
};

45
src/routers/constantModules/dashboard.js

@ -0,0 +1,45 @@
import { Layout, BaseHomeTitle } from "../constants";
const routeName = "dashboard";
/** @constant
@type {import("vue-router").RouteRecord[]}
@default
*/
export default [
{
path: "/dashboard",
name: "routeName",
component: Layout,
meta: {
title: "主控台",
icon: "renderIcon(DashboardOutlined)",
permissions: ["admin"],
sort: 0,
},
children: [
{
path: "home",
name: `${routeName}_home`,
component: () => import("@/views/dashboard/console.vue"),
meta: {
title: BaseHomeTitle,
icon: "renderIcon(DashboardOutlined)",
permissions: ["admin"],
sort: 0,
},
},
{
path: "console",
name: `${routeName}_console`,
component: () => import("@/views/dashboard/console.vue"),
meta: {
title: "Console",
icon: "renderIcon(DashboardOutlined)",
permissions: ["admin"],
sort: 0,
},
},
],
},
];

11
src/routers/constantModules/index.js

@ -0,0 +1,11 @@
const asyncFiles = require.context("./", true, /\.js$/);
/** @constant
@type {import("vue-router").RouteRecord[]}
@default
*/
export const constantModules = asyncFiles
.keys()
.filter((key) => key !== "./index.js")
.map((key) => asyncFiles(key).default)
.flat();

11
src/routers/constants.js

@ -0,0 +1,11 @@
export const RedirectName = "Redirect";
export const LoginPath = "/auth/login";
export const BaseHome = "/dashboard/home";
export const BaseHomeTitle = "首页";
export const ErrorPage = () => import("@/views/exception/404.vue");
export const Layout = () => import("@/layouts/rootLayout.vue");
export const ParentLayout = () => import("@/layouts/parentLayout.js");

33
src/routers/index.js

@ -0,0 +1,33 @@
import Vue from "vue";
import VueRouter from "vue-router";
import { baseUrl } from "@/configs";
import { RootRoute, LoginRoute, RedirectRoute } from "./base";
import { permissionModules } from "./permissionModules";
import { constantModules } from "./constantModules";
import { createRouterGuards } from "./routerGuards";
export { useRouter, useRoute } from "vue2-helpers/vue-router";
Vue.use(VueRouter);
export const constantRoutes = [LoginRoute, RootRoute, RedirectRoute, ...constantModules];
export const asyncRoutes = [...permissionModules];
const createRouter = () =>
new VueRouter({
mode: "history",
scrollBehavior: () => ({ y: 0 }),
base: baseUrl,
routes: constantRoutes,
});
const router = createRouter();
new createRouterGuards(router);
export function resetRouter() {
const newRouter = createRouter();
router.matcher = newRouter.matcher;
}
export default router;

11
src/routers/permissionModules/index.js

@ -0,0 +1,11 @@
const asyncFiles = require.context("./", true, /\.js$/);
/** @constant
@type {import("vue-router").RouteRecord[]}
@default
*/
export const permissionModules = asyncFiles
.keys()
.filter((key) => key !== "./index.js")
.map((key) => asyncFiles(key).default)
.flat();

93
src/routers/permissionModules/test.js

@ -0,0 +1,93 @@
import { Layout, ParentLayout } from "../constants";
/** @constant
@type {import("vue-router").RouteRecord[]}
@default
*/
export default [
{
path: "/hello",
name: "hello",
component: Layout,
meta: {
title: "hello",
icon: require("@/assets/images/trademarks/app-logo.svg"),
},
children: [
{
path: "index",
component: ParentLayout,
meta: {
title: "hello-world",
icon: "fab fa-app-store",
permissions: ["admin"],
},
children: [
{
path: "1",
component: () => import("@/components/HelloWorld.vue"),
meta: {
title: "1-1",
icon: "fab fa-app-store",
permissions: ["admin"],