Browse Source

chore: 同步完整版代码

i18n
xiaoxian521 2 years ago
parent
commit
f220db4eed
  1. 2
      LICENSE
  2. 4
      README.en-US.md
  3. 4
      README.md
  4. 60
      package.json
  5. 1781
      pnpm-lock.yaml
  6. 1
      public/logo.svg
  7. 24
      src/assets/iconfont/iconfont.css
  8. 93
      src/assets/iconfont/iconfont.js
  9. 33
      src/assets/iconfont/iconfont.json
  10. BIN
      src/assets/iconfont/iconfont.ttf
  11. BIN
      src/assets/iconfont/iconfont.woff
  12. BIN
      src/assets/iconfont/iconfont.woff2
  13. 3
      src/layout/components/notice/noticeItem.vue
  14. 21
      src/layout/components/screenfull/index.vue
  15. 17
      src/layout/components/search/components/SearchModal.vue
  16. 20
      src/layout/components/sidebar/extraIcon.vue
  17. 4
      src/layout/components/sidebar/horizontal.vue
  18. 2
      src/layout/components/sidebar/leftCollapse.vue
  19. 40
      src/layout/components/sidebar/logo.vue
  20. 17
      src/layout/components/sidebar/mixNav.vue
  21. 170
      src/layout/components/sidebar/sidebarItem.vue
  22. 4
      src/layout/components/tag/index.scss
  23. 13
      src/layout/components/tag/index.vue
  24. 2
      src/layout/hooks/useDataThemeChange.ts
  25. 13
      src/layout/hooks/useNav.ts
  26. 9
      src/layout/hooks/useTag.ts
  27. 5
      src/layout/types.ts
  28. 2
      src/router/index.ts
  29. 3
      src/router/utils.ts
  30. 6
      src/store/modules/multiTags.ts
  31. 3
      src/style/index.scss
  32. 454
      src/style/sidebar.scss
  33. 45
      src/utils/print.ts
  34. 7
      types/global.d.ts

2
LICENSE

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 啝裳
Copyright (c) 2023 pure-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

4
README.en-US.md

@ -18,6 +18,10 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
- [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
- [Click me to view foreign document site](https://pure-admin.github.io/pure-admin-doc)
## Preview
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
## Usage
### Installation dependencies

4
README.md

@ -22,6 +22,10 @@
- [点我查看国内文档站](https://yiming_chang.gitee.io/pure-admin-doc)
- [点我查看国外文档站](https://pure-admin.github.io/pure-admin-doc)
## 预览
- [点我查看预览站](https://pure-admin-thin.netlify.app/#/login)
## 维护者
[xiaoxian521](https://github.com/xiaoxian521)

60
package.json

@ -30,36 +30,36 @@
],
"dependencies": {
"@pureadmin/descriptions": "^1.1.0",
"@pureadmin/table": "^1.9.0",
"@pureadmin/table": "^2.0.0",
"@pureadmin/utils": "^1.8.5",
"@vueuse/core": "^9.6.0",
"@vueuse/core": "^9.13.0",
"@vueuse/motion": "2.0.0-beta.12",
"animate.css": "^4.1.1",
"axios": "^1.2.0",
"dayjs": "^1.11.6",
"echarts": "^5.4.0",
"element-plus": "^2.2.27",
"axios": "1.2.2",
"dayjs": "^1.11.7",
"echarts": "^5.4.1",
"element-plus": "^2.2.32",
"element-resize-detector": "^1.2.4",
"js-cookie": "^3.0.1",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.0.28",
"pinia": "^2.0.32",
"qs": "^6.11.0",
"responsive-storage": "^2.1.0",
"vue": "^3.2.45",
"responsive-storage": "^2.2.0",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vue-types": "^4.2.1"
"vue-types": "^5.0.2"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.2.7",
"@iconify-icons/ri": "^1.2.3",
"@iconify/vue": "^4.0.0",
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@iconify-icons/ep": "^1.2.10",
"@iconify-icons/ri": "^1.2.4",
"@iconify/vue": "^4.1.0",
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@pureadmin/theme": "^3.0.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
@ -67,30 +67,30 @@
"@types/node": "^18.11.9",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"autoprefixer": "^10.4.13",
"cloc": "^2.10.0",
"cloc": "^2.11.0",
"cssnano": "^5.1.14",
"eslint": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.7.0",
"eslint-plugin-vue": "^9.9.0",
"husky": "^7.0.4",
"lint-staged": "11.1.2",
"picocolors": "^1.0.0",
"postcss": "^8.4.18",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-import": "^15.0.0",
"postcss-scss": "^4.0.5",
"postcss-import": "^15.1.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.5.1",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"rollup-plugin-visualizer": "^5.8.3",
"sass": "^1.56.1",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"stylelint": "^14.3.0",
"stylelint-config-html": "^1.0.0",
@ -99,18 +99,18 @@
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"svgo": "^3.0.2",
"tailwindcss": "^3.2.4",
"tailwindcss": "^3.2.7",
"terser": "^5.16.1",
"typescript": "^4.9.3",
"unplugin-vue-define-options": "^1.1.1",
"vite": "^4.0.3",
"typescript": "^4.9.5",
"unplugin-vue-define-options": "^1.0.0",
"vite": "^4.1.4",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^1.3.0",
"vite-svg-loader": "^3.6.0",
"vite-plugin-remove-console": "^2.1.0",
"vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.9"
"vue-tsc": "^1.2.0"
},
"pnpm": {
"peerDependencyRules": {

1781
pnpm-lock.yaml
File diff suppressed because it is too large
View File

1
public/logo.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>

24
src/assets/iconfont/iconfont.css

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src: url("iconfont.woff2?t=1638023560828") format("woff2"),
url("iconfont.woff?t=1638023560828") format("woff"),
url("iconfont.ttf?t=1638023560828") format("truetype");
src: url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
@ -13,26 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.team-icontabs::before {
.pure-iconfont-tabs:before {
content: "\e63e";
}
.team-iconlogo::before {
.pure-iconfont-logo:before {
content: "\e620";
}
.team-iconxinpin::before {
content: "\e614";
}
.team-iconxinpinrenqiwang::before {
.pure-iconfont-new:before {
content: "\e615";
}
.team-iconexit-fullscreen::before {
content: "\e62a";
}
.team-iconfullscreen::before {
content: "\e62b";
}

93
src/assets/iconfont/iconfont.js
File diff suppressed because it is too large
View File

33
src/assets/iconfont/iconfont.json

@ -2,50 +2,29 @@
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "team-icon",
"description": "pure-admin",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "标签页",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "水能",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795613",
"name": "新品",
"font_class": "xinpin",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "7795615",
"name": "新品人气王",
"font_class": "xinpinrenqiwang",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "5698509",
"name": "全屏缩小",
"font_class": "exit-fullscreen",
"unicode": "e62a",
"unicode_decimal": 58922
},
{
"icon_id": "5698510",
"name": "全屏显示",
"font_class": "fullscreen",
"unicode": "e62b",
"unicode_decimal": 58923
}
]
}

BIN
src/assets/iconfont/iconfont.ttf

BIN
src/assets/iconfont/iconfont.woff

BIN
src/assets/iconfont/iconfont.woff2

3
src/layout/components/notice/noticeItem.vue

@ -2,6 +2,7 @@
import { ListItem } from "./data";
import { ref, PropType, nextTick } from "vue";
import { useNav } from "@/layout/hooks/useNav";
import { deviceDetection } from "@pureadmin/utils";
const props = defineProps({
noticeItem: {
@ -15,6 +16,7 @@ const titleTooltip = ref(false);
const descriptionRef = ref(null);
const descriptionTooltip = ref(false);
const { tooltipEffect } = useNav();
const isMobile = deviceDetection();
function hoverTitle() {
nextTick(() => {
@ -63,6 +65,7 @@ function hoverDescription(event, description) {
:disabled="!titleTooltip"
:content="props.noticeItem.title"
placement="top-start"
:enterable="!isMobile"
>
<div
ref="titleRef"

21
src/layout/components/screenfull/index.vue

@ -1,21 +0,0 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useFullscreen } from "@vueuse/core";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen();
</script>
<template>
<div
class="screen-full w-[36px] h-[48px] flex-ac cursor-pointer navbar-bg-hover"
@click="toggle"
>
<FontIcon
:title="
isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
"
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/>
</div>
</template>

17
src/layout/components/search/components/SearchModal.vue

@ -3,11 +3,10 @@ import { useRouter } from "vue-router";
import { cloneDeep } from "@pureadmin/utils";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "@/utils/tree";
import { useNav } from "@/layout/hooks/useNav";
import { transformI18n } from "@/plugins/i18n";
import { ref, computed, shallowRef } from "vue";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "@/store/modules/permission";
import Search from "@iconify-icons/ep/search";
@ -33,7 +32,7 @@ const handleSearch = useDebounceFn(search, 300);
/** 菜单树形结构 */
const menusData = computed(() => {
return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus));
return cloneDeep(usePermissionStoreHook().wholeMenus);
});
const show = computed({
@ -45,14 +44,6 @@ const show = computed({
}
});
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
function flatTree(arr) {
const res = [];
@ -136,9 +127,11 @@ onKeyStroke("ArrowDown", handleDown);
<template>
<el-dialog
top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'"
v-model="show"
:width="device === 'mobile' ? '80vw' : '50vw'"
:before-close="handleClose"
@opened="inputRef.focus()"
@closed="inputRef.blur()"
>
<el-input
ref="inputRef"

20
src/layout/components/sidebar/extraIcon.vue

@ -0,0 +1,20 @@
<script setup lang="ts">
import { toRaw } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const props = defineProps({
extraIcon: {
type: String,
default: ""
}
});
</script>
<template>
<div v-if="props.extraIcon" class="flex justify-center items-center">
<component
:is="useRenderIcon(toRaw(props.extraIcon))"
class="w-[30px] h-[30px]"
/>
</div>
</template>

4
src/layout/components/sidebar/horizontal.vue

@ -46,8 +46,8 @@ watch(
class="horizontal-header"
>
<div class="horizontal-header-left" @click="backHome">
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<h4>{{ title }}</h4>
<img src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<el-menu
router

2
src/layout/components/sidebar/leftCollapse.vue

@ -24,7 +24,7 @@ const iconClass = computed(() => {
"align-middle",
"text-primary",
"cursor-pointer",
"duration-[360ms]",
"duration-[100ms]",
"hover:text-primary",
"dark:hover:!text-white"
];

40
src/layout/components/sidebar/logo.vue

@ -18,7 +18,7 @@ const { title } = useNav();
class="sidebar-logo-link"
to="/"
>
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
<router-link
@ -28,7 +28,7 @@ const { title } = useNav();
class="sidebar-logo-link"
to="/"
>
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
</transition>
@ -37,37 +37,33 @@ const { title } = useNav();
<style lang="scss" scoped>
.sidebar-logo-container {
position: relative;
width: 100%;
height: 48px;
text-align: center;
overflow: hidden;
position: relative;
.sidebar-logo-link {
height: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-top: 5px;
display: flex;
flex-wrap: nowrap;
align-items: center;
img {
height: 32px;
display: inline-block;
}
.sidebar-title {
display: block;
width: 160px;
height: 32px;
line-height: 32px;
margin: 2px 0 0 12px;
color: $subMenuActiveText;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
color: #1890ff;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
font-size: 20px;
margin-top: 10px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
.collapses {
.sidebar-logo {
margin-right: 0;
}
}
}

17
src/layout/components/sidebar/mixNav.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import extraIcon from "./extraIcon.vue";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { useNav } from "@/layout/hooks/useNav";
@ -26,6 +27,7 @@ const {
menuSelect,
resolvePath,
username,
getDivStyle,
avatarsStyle,
getDropdownItemStyle,
getDropdownItemClass
@ -85,15 +87,12 @@ watch(
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/>
</div>
<span class="select-none">{{ transformI18n(route.meta.title) }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false"
/>
<div :style="getDivStyle">
<span class="select-none">
{{ transformI18n(route.meta.title) }}
</span>
<extraIcon :extraIcon="route.meta.extraIcon" />
</div>
</template>
</el-menu-item>
</el-menu>

170
src/layout/components/sidebar/sidebarItem.vue

@ -1,18 +1,19 @@
<script setup lang="ts">
import path from "path";
import { getConfig } from "@/config";
import extraIcon from "./extraIcon.vue";
import { childrenType } from "../../types";
import { useNav } from "@/layout/hooks/useNav";
import { transformI18n } from "@/plugins/i18n";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
import ArrowUp from "@iconify-icons/ep/arrow-up";
import EpArrowDown from "@iconify-icons/ep/arrow-down";
import ArrowLeft from "@iconify-icons/ep/arrow-left";
import ArrowRight from "@iconify-icons/ep/arrow-right";
import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
const { layout, isCollapse, tooltipEffect } = useNav();
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
const props = defineProps({
item: {
@ -28,17 +29,11 @@ const props = defineProps({
}
});
const getExtraIconStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
const getSpanStyle = computed((): CSSProperties => {
return {
position: "absolute",
right: "10px"
width: "100%",
textAlign: "center"
};
} else {
return {
position: "static"
};
}
});
const getNoDropdownStyle = computed((): CSSProperties => {
@ -48,16 +43,6 @@ const getNoDropdownStyle = computed((): CSSProperties => {
};
});
const getDivStyle = computed((): CSSProperties => {
return {
width: !isCollapse.value ? "" : "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
};
});
const getMenuTextStyle = computed(() => {
return {
overflow: "hidden",
@ -66,19 +51,54 @@ const getMenuTextStyle = computed(() => {
};
});
const getsubMenuIconStyle = computed((): CSSProperties => {
return {
display: "flex",
justifyContent: "center",
alignItems: "center",
margin:
layout.value === "horizontal"
? "0 5px 0 0"
: isCollapse.value
? "0 auto"
: "0 5px 0 0"
};
});
const getSubTextStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
return {
width: !isCollapse.value ? "210px" : "",
width: "210px",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
};
} else {
return {
width: ""
};
}
});
const getSpanStyle = computed(() => {
return {
overflow: "hidden",
textOverflow: "ellipsis"
const getSubMenuDivStyle = computed((): any => {
return item => {
return !isCollapse.value
? {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
}
: {
width: "100%",
textAlign:
item?.parentId === null
? "center"
: layout.value === "mix" && item?.pathList?.length === 2
? "center"
: ""
};
};
});
@ -115,6 +135,20 @@ function hoverMenu(key) {
});
}
//
function overflowSlice(text, item?: any) {
const newText =
(text?.length > 1 ? text.toString().slice(0, 1) : text) + "...";
if (item && !(isCollapse.value && item?.parentId === null)) {
return layout.value === "mix" &&
item?.pathList?.length === 2 &&
isCollapse.value
? newText
: text;
}
return newText;
}
function hasOneShowingChild(
children: childrenType[] = [],
parent: childrenType
@ -151,18 +185,20 @@ function resolvePath(routePath) {
</script>
<template>
<template
<el-menu-item
v-if="
hasOneShowingChild(props.item.children, props.item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
"
>
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
>
<div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)">
<div
v-if="toRaw(props.item.meta.icon)"
class="sub-menu-icon"
:style="getsubMenuIconStyle"
>
<component
:is="
useRenderIcon(
@ -172,28 +208,28 @@ function resolvePath(routePath) {
"
/>
</div>
<div
<span
v-if="
!props.item?.meta.icon &&
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
"
:style="getDivStyle"
:style="getSpanStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
{{ overflowSlice(transformI18n(onlyOneChild.meta.title)) }}
</span>
</div>
<div
<span
v-if="
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2
!onlyOneChild.meta.icon &&
isCollapse &&
layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getDivStyle"
:style="getSpanStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
{{ overflowSlice(transformI18n(onlyOneChild.meta.title)) }}
</span>
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="layout === 'horizontal'">
@ -217,18 +253,10 @@ function resolvePath(routePath) {
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</el-tooltip>
<FontIcon
v-if="onlyOneChild.meta.extraIcon"
width="30px"
height="30px"
:style="getExtraIconStyle"
:icon="onlyOneChild.meta.extraIcon.name"
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
/>
<extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
</div>
</template>
</el-menu-item>
</template>
<el-sub-menu
v-else
@ -237,7 +265,11 @@ function resolvePath(routePath) {
:index="resolvePath(props.item.path)"
>
<template #title>
<div v-if="toRaw(props.item.meta.icon)" class="sub-menu-icon">
<div
v-if="toRaw(props.item.meta.icon)"
:style="getsubMenuIconStyle"
class="sub-menu-icon"
>
<component
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
@ -245,8 +277,18 @@ function resolvePath(routePath) {
<span v-if="layout === 'horizontal'">
{{ transformI18n(props.item.meta.title) }}
</span>
<div
:style="getSubMenuDivStyle(props.item)"
v-if="
!(
isCollapse &&
toRaw(props.item.meta.icon) &&
props.item.parentId === null
)
"
>
<el-tooltip
v-else
v-if="layout !== 'horizontal'"
placement="top"
:effect="tooltipEffect"
:offset="-10"
@ -255,24 +297,18 @@ function resolvePath(routePath) {
<template #content>
{{ transformI18n(props.item.meta.title) }}
</template>
<div
<span
ref="menuTextRef"
:style="getSubTextStyle"
@mouseover="hoverMenu(props.item)"
>
<span :style="getSpanStyle">
{{ transformI18n(props.item.meta.title) }}
{{
overflowSlice(transformI18n(props.item.meta.title), props.item)
}}
</span>
</div>
</el-tooltip>
<FontIcon
v-if="props.item.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="props.item.meta.extraIcon.name"
:svg="props.item.meta.extraIcon.svg ? true : false"
/>
<extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
</div>
</template>
<sidebar-item
v-for="child in props.item.children"

4
src/layout/components/tag/index.scss

@ -281,7 +281,7 @@
left: 0;
bottom: 0;
background: var(--el-color-primary);
animation: scheduleInWidth 400ms ease-in;
animation: scheduleInWidth 200ms ease-in;
}
/* 灵动模式下鼠标移出隐藏蓝色进度条 */
@ -292,5 +292,5 @@
left: 0;
bottom: 0;
background: var(--el-color-primary);
animation: scheduleOutWidth 400ms ease-in;
animation: scheduleOutWidth 200ms ease-in;
}

13
src/layout/components/tag/index.vue

@ -65,7 +65,7 @@ const dynamicTagView = () => {
moveToView(index);
};
const moveToView = (index: number): void => {
const moveToView = async (index: number): Promise<void> => {
const tabNavPadding = 10;
if (!instance.refs["dynamic" + index]) return;
const tabItemEl = instance.refs["dynamic" + index][0];
@ -75,8 +75,13 @@ const moveToView = (index: number): void => {
const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value?.offsetWidth
: 0;
// dom
await nextTick();
//
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
scrollbarDomWidth <= tabDomWidth
? (isShowArrow.value = true)
: (isShowArrow.value = false);
@ -194,6 +199,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
length
}) as any;
}
dynamicTagView();
};
if (tag === "other") {
@ -490,6 +496,11 @@ watch([route], () => {
dynamicTagView();
});
watch(isFullscreen, () => {
tagsViews[6].icon = Fullscreen;
tagsViews[6].text = $t("buttons.hswholeFullScreen");
});
onMounted(() => {
useResizeObserver(
scrollbarDom,

2
src/layout/hooks/useDataThemeChange.ts

@ -38,7 +38,7 @@ export function useDataThemeChange() {
const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") {
function setLayoutThemeColor(theme = getConfig().Theme ?? "default") {
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`

13
src/layout/hooks/useNav.ts

@ -1,4 +1,3 @@
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { getConfig } from "@/config";
import { useRouter } from "vue-router";
@ -7,6 +6,7 @@ import { routeMetaType } from "../types";
import { useGlobal } from "@pureadmin/utils";
import { transformI18n } from "@/plugins/i18n";
import { router, remainingPaths } from "@/router";
import { computed, type CSSProperties } from "vue";
import { useAppStoreHook } from "@/store/modules/app";
import { useUserStoreHook } from "@/store/modules/user";
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
@ -21,6 +21,16 @@ export function useNav() {
/** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */
const tooltipEffect = getConfig()?.TooltipEffect ?? "light";
const getDivStyle = computed((): CSSProperties => {
return {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
};
});
/** 用户名 */
const username = computed(() => {
return useUserStoreHook()?.username;
@ -146,6 +156,7 @@ export function useNav() {
$storage,
backHome,
onPanel,
getDivStyle,
changeTitle,
toggleSideBar,
menuSelect,

9
src/layout/hooks/useTag.ts

@ -12,10 +12,15 @@ import { tagsViewsType } from "../types";
import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router";
import { transformI18n, $t } from "@/plugins/i18n";
import { isEqual, isBoolean } from "@pureadmin/utils";
import { useSettingStoreHook } from "@/store/modules/settings";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
import {
isEqual,
isBoolean,
storageLocal,
toggleClass,
hasClass
} from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import CloseAllTags from "@iconify-icons/ri/subtract-line";

5
src/layout/types.ts

@ -67,10 +67,7 @@ export type childrenType = {
icon?: string;
title?: string;
showParent?: boolean;
extraIcon?: {
svg?: boolean;
name?: string;
};
extraIcon?: string;
};
showTooltip?: boolean;
parentId?: number;

2
src/router/index.ts

@ -61,7 +61,7 @@ export const remainingPaths = Object.keys(remainingRouter).map(v => {
/** 创建路由实例 */
export const router: Router = createRouter({
history: getHistoryMode(),
history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY),
routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true,
scrollBehavior(to, from, savedPosition) {

3
src/router/utils.ts

@ -322,8 +322,7 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
}
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
function getHistoryMode(): RouterHistory {
const routerHistory = import.meta.env.VITE_ROUTER_HISTORY;
function getHistoryMode(routerHistory): RouterHistory {
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
const leftMode = historyMode[0];

6
src/store/modules/multiTags.ts

@ -1,9 +1,8 @@
import { defineStore } from "pinia";
import { store } from "@/store";
import { isEqual } from "@pureadmin/utils";
import { routerArrays } from "@/layout/types";
import { multiType, positionType } from "./types";
import { isUrl, storageLocal } from "@pureadmin/utils";
import { isEqual, isBoolean, isUrl, storageLocal } from "@pureadmin/utils";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
@ -54,6 +53,9 @@ export const useMultiTagsStore = defineStore({
if (isUrl(tagVal?.name)) return;
// 如果title为空拒绝添加空信息到标签页
if (tagVal?.meta?.title.length === 0) return;
// showLink:false 不添加到标签页
if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {

3
src/style/index.scss

@ -6,7 +6,8 @@
/* 自定义全局 CssVar */
:root {
--pure-transition-duration: 0.016s;
/* 左侧菜单展开、收起动画时长 */
--pure-transition-duration: 0.3s;
}
/* 灰色模式 */

454
src/style/sidebar.scss

@ -14,12 +14,13 @@
}
.sub-menu-icon {
vertical-align: middle;
font-size: 18px;
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 5px;
svg {
width: 18px;
height: 18px;
}
}
.set-icon {
@ -161,6 +162,24 @@
color: $subMenuActiveText !important;
}
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
.el-menu-item.is-active.nest-menu::before {
content: "";
clear: both;
position: absolute;
left: 8px;
right: 8px;
margin: 4px 0;
top: 0;
bottom: 0;
border-radius: 3px;
background: var(--el-color-primary) !important;
}
.el-menu .el-menu--inline .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
font-size: 12px;
@ -168,152 +187,48 @@
background-color: $subMenuBg !important;
}
/* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most {
background: $subMenuActiveBg !important;
}
/* 有子集的激活菜单背景 */
.is-active.nest-menu {
background: $subMenuActiveBg !important;
}
}
.horizontal-header {
display: flex;
justify-content: space-around;
background: $menuBg;
width: 100%;
height: 48px;
align-items: center;
.horizontal-header-left {
display: flex;
height: 100%;
width: auto;
min-width: 200px;
align-items: center;
padding-left: 10px;
cursor: pointer;
transition: all 0.125s ease;
i {
font-size: 30px;
color: #1890ff;
margin-right: 4px;
}
h4 {
font-size: 16px;
font-weight: 700;
color: $subMenuActiveText;
transition: all 0.5s;
}
}
.horizontal-header-menu {
height: 100%;
min-width: 0;
flex: 1;
align-items: center;
}
.horizontal-header-right {
display: flex;
min-width: 340px;
align-items: center;
color: $subMenuActiveText;
justify-content: flex-end;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 国际化 */
.globalization,
/* 用户名 */
.el-dropdown-link,
/* 设置 */
.set-icon {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
}
.globalization {
width: 40px;
height: 48px;
padding: 11px;
outline: none;
cursor: pointer;
color: $subMenuActiveText;
}
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-around;
color: $subMenuActiveText;
p {
font-size: 14px;
}
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
}
.el-menu {
border: none;
/* 有子集的激活菜单左侧小竖条 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
width: 100% !important;
background-color: transparent;
}
.el-menu-item,
.el-sub-menu__title {
color: $menuText;
&:hover {
color: $menuTitleHover !important;
}
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all var(--pure-transition-duration) ease-in-out;
transform: translateY(0);
}
.submenu-title-noDropdown,
.el-sub-menu__title {
height: 48px;
line-height: 48px;
background: $menuBg;
svg {
position: static !important;
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before {
content: "";
display: block;
position: absolute;
height: 0;
width: 3px;
transform: translateY(-50%);
top: 50%;
}
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
i {
color: $subMenuActiveText !important;
}
/* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most > * {
z-index: 1;
color: #fff;
}
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
.is-active.submenu-title-noDropdown.outer-most::before {
content: "";
clear: both;
position: absolute;
left: 8px;
right: 8px;
margin: 4px 0;
top: 0;
bottom: 0;
border-radius: 3px;
background: var(--el-color-primary) !important;
}
}
@ -368,25 +283,32 @@
color: $subMenuActiveText !important;
}
.el-menu-item.is-active.nest-menu {
background: $subMenuActiveBg !important;
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
.el-menu-item.is-active.nest-menu::before {
content: "";
clear: both;
position: absolute;
left: 8px;
right: 8px;
top: 0;
bottom: 0;
border-radius: 3px;
background: var(--el-color-primary) !important;
}
.el-menu-item,
.el-sub-menu {
// i {
// width: 20px;
// text-align: center;
// font-size: 16px;
// }
// i.fa {
// margin-right: 5px;
// font-size: 16px;
// }
.iconfont {
font-size: 18px;
}
.el-menu-tooltip__trigger {
width: 54px;
padding: 18px !important;
padding: 0;
}
}
}
@ -448,48 +370,175 @@
}
}
/* 有子集的激活菜单背景 */
.is-active.nest-menu {
background: $subMenuActiveBg !important;
}
.el-menu-item.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
}
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: $sideBarWidth !important;
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
/* 有子菜单 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before,
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
.el-menu-item.is-active.nest-menu::before {
content: "";
clear: both;
position: absolute;
left: 5px;
right: 5px;
top: 0;
left: 2px;
width: 2px;
bottom: 0;
border-radius: 3px;
background: var(--el-color-primary) !important;
}
}
.horizontal-header {
display: flex;
justify-content: space-around;
background: $menuBg;
width: 100%;
height: 48px;
align-items: center;
.horizontal-header-left {
display: flex;
height: 100%;
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.125s ease-in-out;
transform: translateY(0);
width: auto;
min-width: 200px;
align-items: center;
padding-left: 10px;
cursor: pointer;
transition: all var(--pure-transition-duration) ease;
img {
height: 32px;
display: inline-block;
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before,
.el-menu--collapse .submenu-title-noDropdown.outer-most::before {
content: "";
display: block;
position: absolute;
height: 0;
width: 3px;
transform: translateY(-50%);
top: 50%;
span {
height: 32px;
line-height: 32px;
margin: 2px 0 0 12px;
color: $subMenuActiveText;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
}
}
.horizontal-header-menu {
height: 100%;
min-width: 0;
flex: 1;
align-items: center;
}
.horizontal-header-right {
display: flex;
min-width: 340px;
align-items: center;
color: $subMenuActiveText;
justify-content: flex-end;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 国际化 */
.globalization,
/* 用户名 */
.el-dropdown-link,
/* 设置 */
.set-icon {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
}
.globalization {
width: 40px;
height: 48px;
padding: 11px;
outline: none;
cursor: pointer;
color: $subMenuActiveText;
}
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-around;
color: $subMenuActiveText;
p {
font-size: 14px;
}
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
background-color: transparent;
}
.el-menu-item,
.el-sub-menu__title {
padding-right: var(--el-menu-base-level-padding);
color: $menuText;
&:hover {
color: $menuTitleHover !important;
}
}
.submenu-title-noDropdown,
.el-sub-menu__title {
height: 48px;
line-height: 48px;
background: $menuBg;
svg {
position: static !important;
}
}
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
i {
color: $subMenuActiveText !important;
}
}
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
}
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: $sideBarWidth !important;
}
/* 手机端 */
@ -537,7 +586,7 @@ body[layout="vertical"] {
}
.sidebar-container {
transition: width 0.125s;
transition: width var(--pure-transition-duration);
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
@ -554,11 +603,10 @@ body[layout="vertical"] {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
height: 100%;
width: 100%;
text-align: center;
visibility: visible;
}
}
}
@ -568,7 +616,7 @@ body[layout="vertical"] {
}
.el-sub-menu__title {
padding: 0 18px !important;
padding: 0;
}
}
@ -597,9 +645,13 @@ body[layout="horizontal"] {
$sideBarWidth: 0;
@include merge-style($sideBarWidth);
.fixed-header,
.main-container {
transition: none !important;
}
.fixed-header {
width: 100%;
transition: none !important;
}
}
@ -622,7 +674,7 @@ body[layout="mix"] {
}
.sidebar-container {
transition: width 0.125s;
transition: width var(--pure-transition-duration);
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
@ -638,12 +690,12 @@ body[layout="mix"] {
.el-menu--collapse {
.el-sub-menu {
& > .el-sub-menu__title {
padding: 0;
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
height: 100%;
width: 100%;
text-align: center;
visibility: visible;
}
}
}

45
src/utils/print.ts

@ -13,8 +13,6 @@ const Print = function (dom, options?: object): PrintFunction {
styleStr: "",
// Elements that need to dynamically get and set the height
setDomHeightArr: [],
// Echart dom List
echartDomArr: [],
// Callback before printing
printBeforeFn: null,
// Callback after printing
@ -73,6 +71,8 @@ Print.prototype = {
const inputs = document.querySelectorAll("input");
const selects = document.querySelectorAll("select");
const textareas = document.querySelectorAll("textarea");
const canvass = document.querySelectorAll("canvas");
for (let k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
@ -108,6 +108,15 @@ Print.prototype = {
}
}
for (let k4 = 0; k4 < canvass.length; k4++) {
const imageURL = canvass[k4].toDataURL("image/png");
const img = document.createElement("img");
img.src = imageURL;
img.setAttribute("style", "max-width: 100%;");
img.className = "isNeedRemove";
canvass[k4].parentNode.insertBefore(img, canvass[k4].nextElementSibling);
}
return this.dom.outerHTML;
},
/**
@ -130,6 +139,12 @@ Print.prototype = {
doc.open();
doc.write(content);
doc.close();
const removes = document.querySelectorAll(".isNeedRemove");
for (let k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]);
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
iframe.onload = function (): void {
@ -137,8 +152,6 @@ Print.prototype = {
if (_this.conf.printBeforeFn) {
_this.conf.printBeforeFn({ doc });
}
_this.drawEchartImg(doc).then(() => {
_this.toPrint(w);
setTimeout(function () {
document.body.removeChild(iframe);
@ -147,33 +160,9 @@ Print.prototype = {
_this.conf.printDoneCallBack();
}
}, 100);
});
};
},
/**
* echarts printing
* @param {Object} doc iframe window
*/
drawEchartImg(doc): Promise<void> {
return new Promise<void>(resolve => {
if (this.conf.echartDomArr && this.conf.echartDomArr.length > 0) {
this.conf.echartDomArr.forEach(e => {
const dom = doc.querySelector("#" + e.$el.id);
const img = new Image();
const w = dom.offsetWidth + "px";
const H = dom.offsetHeight + "px";
img.style.width = w;
img.style.height = H;
img.src = e.imgSrc;
dom.innerHTML = "";
dom.appendChild(img);
});
}
resolve();
});
},
/**
Print
*/
toPrint: function (frameWindow): void {

7
types/global.d.ts

@ -176,11 +176,8 @@ declare global {
title: string;
/** 菜单图标 `可选` */
icon?: string | FunctionalComponent | IconifyIcon;
/** 菜单名称右侧的额外图标,支持`fontawesome`、`iconfont`、`element-plus-icon` `可选` */
extraIcon?: {
svg?: boolean;
name?: string;
};
/** 菜单名称右侧的额外图标 */
extraIcon?: string | FunctionalComponent | IconifyIcon;
/** 是否在菜单中显示(默认`true`)`可选` */
showLink?: boolean;
/** 是否显示父级菜单 `可选` */

Loading…
Cancel
Save