release: update 3.4.5

This commit is contained in:
xiaoxian521
2022-08-22 21:34:55 +08:00
parent 13427998f3
commit c07e60e114
133 changed files with 3481 additions and 3277 deletions

View File

@@ -1,5 +1,10 @@
import { http } from "../utils/http";
export const getAsyncRoutes = (params?: object) => {
return http.request("get", "/getAsyncRoutes", { params });
type Result = {
code: number;
info: Array<any>;
};
export const getAsyncRoutes = (params?: object) => {
return http.request<Result>("get", "/getAsyncRoutes", { params });
};

View File

@@ -1,14 +1,14 @@
import { http } from "../utils/http";
interface userType extends Promise<any> {
type Result = {
svg?: string;
code?: number;
info?: object;
}
};
// 获取验证码
export const getVerify = (): userType => {
return http.request("get", "/captcha");
export const getVerify = () => {
return http.request<Result>("get", "/captcha");
};
// 登录

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -2,12 +2,11 @@ import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont";
export const IconifyIconOffline = iconifyIconOffline;
export const IconifyIconOnline = iconifyIconOnline;
export const FontIcon = fontIcon;
/** 离线图标组件 */
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
/** iconfont组件 */
const FontIcon = fontIcon;
export default {
IconifyIconOffline,
IconifyIconOnline,
FontIcon
};
export { IconifyIconOffline, IconifyIconOnline, FontIcon };

View File

@@ -1,14 +1,14 @@
import { iconType } from "./types";
import { h, defineComponent, Component } from "vue";
import { IconifyIconOffline, FontIcon } from "../index";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg
* @param icon 必传 string 图标
* @param icon 必传 图标
* @param attrs 可选 iconType 属性
* @returns Component
*/
export function useRenderIcon(icon: string, attrs?: iconType): Component {
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
@@ -30,14 +30,16 @@ export function useRenderIcon(icon: string, attrs?: iconType): Component {
});
}
});
} else if (typeof icon === "function") {
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return icon;
} else {
return defineComponent({
name: "Icon",
render() {
return h(IconifyIconOffline, {
const IconifyIcon =
attrs && attrs["online"] ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});

View File

@@ -2,7 +2,7 @@ import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({
name: "fontIcon",
name: "FontIcon",
props: {
icon: {
type: String,

View File

@@ -28,22 +28,26 @@ import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import InformationLine from "@iconify-icons/ri/information-line";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import Bookmark from "@iconify-icons/ri/bookmark-2-line";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
import User from "@iconify-icons/ri/user-3-fill";
import Lock from "@iconify-icons/ri/lock-fill";
import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
addIcon("arrow-right-s-line", ArrowRightSLine);
addIcon("arrow-left-s-line", ArrowLeftSLine);
addIcon("logout-circle-r-line", LogoutCircleRLine);
addIcon("information-line", InformationLine);
addIcon("arrow-up-line", ArrowUpLine);
addIcon("arrow-down-line", ArrowDownLine);
addIcon("bookmark", Bookmark);
addIcon("bookmark-2-line", Bookmark2Line);
addIcon("user", User);
addIcon("lock", Lock);
addIcon("menu-unfold", MenuUnfold);
addIcon("menu-fold", MenuFold);
// Iconify Icon在Vue里离线使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIcon",
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
@@ -57,6 +61,9 @@ export default defineComponent({
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{

View File

@@ -1,9 +1,9 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境 https://docs.iconify.design/icon-components/vue/offline.html
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIcon",
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
@@ -17,6 +17,9 @@ export default defineComponent({
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{

View File

@@ -11,7 +11,9 @@ export interface iconType {
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
online?: boolean;
onLoad?: Function;
includes?: Function;
// all icon
style?: object;

View File

@@ -1,12 +1,7 @@
import { App } from "vue";
import reImageVerify from "./src/index.vue";
import { withInstall } from "@pureadmin/utils";
export const ReImageVerify = Object.assign(reImageVerify, {
install(app: App) {
app.component(reImageVerify.name, reImageVerify);
}
});
/** 图形验证码组件 */
export const ReImageVerify = withInstall(reImageVerify);
export default {
ReImageVerify
};
export default ReImageVerify;

View File

@@ -1,13 +1,11 @@
<script lang="ts">
export default {
name: "ReImageVerify"
};
</script>
<script setup lang="ts">
import { watch } from "vue";
import { useImageVerify } from "./hooks";
defineOptions({
name: "ReImageVerify"
});
interface Props {
code?: string;
}

View File

@@ -1,10 +1,7 @@
import { App } from "vue";
import reQrcode from "./src/index";
import { withInstall } from "@pureadmin/utils";
export const ReQrcode = Object.assign(reQrcode, {
install(app: App) {
app.component(reQrcode.name, reQrcode);
}
});
/** 二维码组件 */
export const ReQrcode = withInstall(reQrcode);
export default ReQrcode;

View File

@@ -8,8 +8,8 @@ import {
defineComponent
} from "vue";
import "./index.scss";
import { isString } from "/@/utils/is";
import { cloneDeep } from "lodash-unified";
import { isString } from "@pureadmin/utils";
import { propTypes } from "/@/utils/propTypes";
import { IconifyIconOffline } from "../../ReIcon";
import QRCode, { QRCodeRenderersOptions } from "qrcode";
@@ -78,7 +78,7 @@ export default defineComponent({
const _width: number = await getOriginWidth(unref(renderText), options);
options.scale =
props.width === 0 ? undefined : (props.width / _width) * 4;
const canvasRef: HTMLCanvasElement = await toCanvas(
const canvasRef: any = await toCanvas(
unref(wrapRef) as HTMLCanvasElement,
unref(renderText),
options
@@ -246,7 +246,7 @@ export default defineComponent({
>
<div class="absolute top-[50%] left-[50%] font-bold">
<IconifyIconOffline
class="cursor-pointer outline-none"
class="cursor-pointer"
icon="refresh-right"
width="30"
color="var(--el-color-primary)"

View File

@@ -1,7 +1,7 @@
import { Directive } from "vue";
import type { DirectiveBinding, VNode } from "vue";
import { Directive, type DirectiveBinding, type VNode } from "vue";
import elementResizeDetectorMaker from "element-resize-detector";
import type { Erd } from "element-resize-detector";
import { optimizeFps } from "@pureadmin/utils";
import { emitter } from "/@/utils/mitt";
const erd: Erd = elementResizeDetectorMaker({
@@ -14,7 +14,9 @@ export const resize: Directive = {
const width = elem.offsetWidth;
const height = elem.offsetHeight;
if (binding?.instance) {
emitter.emit("resize", { detail: { width, height } });
optimizeFps(() => {
emitter.emit("resize", { detail: { width, height } });
})();
} else {
vnode.el.dispatchEvent(
new CustomEvent("resize", { detail: { width, height } })

View File

@@ -1,23 +1,18 @@
<script setup lang="ts">
import {
h,
ref,
computed,
Transition,
defineComponent,
getCurrentInstance
} from "vue";
import { useGlobal } from "@pureadmin/utils";
import backTop from "/@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const props = defineProps({
fixedHeader: Boolean
});
const keepAlive: Boolean = ref(
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive
);
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const keepAlive = computed(() => {
return $config?.KeepAlive;
});
const transitions = computed(() => {
return route => {
@@ -26,11 +21,11 @@ const transitions = computed(() => {
});
const hideTabs = computed(() => {
return instance?.configure.hideTabs;
return $storage?.configure.hideTabs;
});
const layout = computed(() => {
return instance?.layout.layout === "vertical";
return $storage?.layout.layout === "vertical";
});
const getSectionStyle = computed(() => {

View File

@@ -1,65 +1,51 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useNav } from "../hooks/nav";
import { useRoute } from "vue-router";
import Search from "./search/index.vue";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg";
import Hamburger from "./sidebar/hamBurger.vue";
import { watch, getCurrentInstance } from "vue";
import { useNav } from "/@/layout/hooks/useNav";
import Breadcrumb from "./sidebar/breadCrumb.vue";
import { deviceDetection } from "/@/utils/deviceDetection";
import { deviceDetection } from "@pureadmin/utils";
import topCollapse from "./sidebar/topCollapse.vue";
import screenfull from "../components/screenfull/index.vue";
import { useTranslationLang } from "../hooks/useTranslationLang";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const {
layout,
device,
logout,
onPanel,
changeTitle,
toggleSideBar,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
toggleSideBar,
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
}
const { t, locale, translationCh, translationEn } = useTranslationLang();
</script>
<template>
<div class="navbar">
<Hamburger
v-if="pureApp.layout !== 'mix'"
:is-active="pureApp.sidebar.opened"
<div
class="navbar bg-[#fff] shadow-sm shadow-[rgba(0, 21, 41, 0.08)] dark:shadow-[#0d0d0d]"
>
<topCollapse
v-if="device === 'mobile'"
class="hamburger-container"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<Breadcrumb v-if="pureApp.layout !== 'mix'" class="breadcrumb-container" />
<Breadcrumb
v-if="layout !== 'mix' && device !== 'mobile'"
class="breadcrumb-container"
/>
<mixNav v-if="pureApp.layout === 'mix'" />
<mixNav v-if="layout === 'mix'" />
<div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
<div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
@@ -68,34 +54,41 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
><IconifyIconOffline
>
<IconifyIconOffline
class="check-zh"
v-show="locale === 'zh'"
icon="check"
/>简体中文</el-dropdown-item
>
/>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@@ -103,13 +96,14 @@ function translationEn() {
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
/>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
@@ -124,16 +118,12 @@ function translationEn() {
width: 100%;
height: 48px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container {
line-height: 48px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
}
.vertical-header-right {
@@ -144,31 +134,6 @@ function translationEn() {
color: #000000d9;
justify-content: flex-end;
:deep(.dropdown-badge) {
&:hover {
background: #f6f6f6;
}
}
.screen-full {
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.globalization {
height: 48px;
width: 40px;
padding: 11px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.el-dropdown-link {
height: 48px;
padding: 10px;
@@ -178,10 +143,6 @@ function translationEn() {
cursor: pointer;
color: #000000d9;
&:hover {
background: #f6f6f6;
}
p {
font-size: 14px;
}
@@ -200,15 +161,12 @@ function translationEn() {
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: #f6f6f6;
}
}
}
.breadcrumb-container {
float: left;
margin-left: 16px;
}
}

View File

@@ -15,14 +15,13 @@ notices.value.forEach(notice => {
});
function tabClick() {
// @ts-expect-error
dropdownDom.value.handleOpen();
(dropdownDom as any).value.handleOpen();
}
</script>
<template>
<el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end">
<span class="dropdown-badge">
<span class="dropdown-badge navbar-bg-hover select-none">
<el-badge :value="noticesNum" :max="99">
<span class="header-notice-icon">
<IconifyIconOffline icon="bell" />

View File

@@ -44,7 +44,9 @@ function hoverDescription(event, description) {
</script>
<template>
<div class="notice-container">
<div
class="notice-container border-b-1 border-[#f0f0f0] dark:border-[#303030]"
>
<el-avatar
v-if="props.noticeItem.avatar"
:size="30"
@@ -52,7 +54,7 @@ function hoverDescription(event, description) {
class="notice-container-avatar"
/>
<div class="notice-container-text">
<div class="notice-text-title">
<div class="notice-text-title color-[#000000d9] dark:color-white">
<el-tooltip
popper-class="notice-title-popper"
:disabled="!titleTooltip"
@@ -72,7 +74,8 @@ function hoverDescription(event, description) {
:type="props.noticeItem?.status"
size="small"
class="notice-title-extra"
>{{ props.noticeItem?.extra }}
>
{{ props.noticeItem?.extra }}
</el-tag>
</div>
@@ -90,7 +93,7 @@ function hoverDescription(event, description) {
{{ props.noticeItem.description }}
</div>
</el-tooltip>
<div class="notice-text-datetime">
<div class="notice-text-datetime color-[#00000073] dark:color-white">
{{ props.noticeItem.datetime }}
</div>
</div>
@@ -108,7 +111,7 @@ function hoverDescription(event, description) {
align-items: flex-start;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
// border-bottom: 1px solid #f0f0f0;
.notice-container-avatar {
margin-right: 16px;
@@ -127,7 +130,6 @@ function hoverDescription(event, description) {
font-weight: 400;
font-size: 14px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.85);
cursor: pointer;
.notice-title-content {
@@ -149,7 +151,6 @@ function hoverDescription(event, description) {
.notice-text-datetime {
font-size: 12px;
line-height: 1.5715;
color: rgba(0, 0, 0, 0.45);
}
.notice-text-description {

View File

@@ -5,7 +5,7 @@ import { emitter } from "/@/utils/mitt";
let show = ref<Boolean>(false);
const target = ref(null);
onClickOutside(target, event => {
onClickOutside(target, (event: any) => {
if (event.clientX > target.value.offsetLeft) return;
show.value = false;
});
@@ -18,15 +18,15 @@ emitter.on("openPanel", () => {
<template>
<div :class="{ show: show }" class="right-panel-container">
<div class="right-panel-background" />
<div ref="target" class="right-panel">
<div ref="target" class="right-panel bg-white dark:bg-dark">
<div class="right-panel-items">
<div class="project-configuration">
<h3>项目配置</h3>
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show">
<IconifyIconOffline icon="close-bold" />
<IconifyIconOffline icon="close" />
</el-icon>
</div>
<div style="border-bottom: 1px solid #dcdfe6" />
<div class="border-b-1 border-[#dcdfe6] dark:border-[#303030]" />
<slot />
</div>
</div>
@@ -62,7 +62,7 @@ emitter.on("openPanel", () => {
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
background: #fff;
// background: #fff;
z-index: 40000;
}
@@ -120,7 +120,7 @@ emitter.on("openPanel", () => {
margin-left: 10px;
i {
font-size: 16px;
font-size: 20px;
margin-right: 20px;
&:hover {

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { useFullscreen } from "@vueuse/core";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen();
</script>
<template>
<div class="screen-full" @click="toggle">
<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')
@@ -16,13 +19,3 @@ const { isFullscreen, toggle } = useFullscreen();
/>
</div>
</template>
<style lang="scss" scoped>
.screen-full {
width: 36px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="search-footer">
<div class="search-footer color-[#333] dark:color-white">
<span class="search-footer-item">
<enterOutlined class="icon" />
确认
@@ -23,7 +23,6 @@ import mdiKeyboardEsc from "/@/assets/svg/mdi_keyboard_esc.svg?component";
<style lang="scss" scoped>
.search-footer {
display: flex;
color: #333;
.search-footer-item {
display: flex;

View File

@@ -2,8 +2,9 @@
import { useRouter } from "vue-router";
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 { deleteChildren } from "@pureadmin/utils";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
@@ -17,6 +18,7 @@ interface Emits {
(e: "update:value", val: boolean): void;
}
const { device } = useNav();
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {});
const router = useRouter();
@@ -130,7 +132,12 @@ onKeyStroke("ArrowDown", handleDown);
</script>
<template>
<el-dialog top="5vh" v-model="show" :before-close="handleClose">
<el-dialog
top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'"
v-model="show"
:before-close="handleClose"
>
<el-input
ref="inputRef"
v-model="keyword"

View File

@@ -1,24 +1,3 @@
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item"
:style="{
background:
item?.path === active ? useEpThemeStoreHook().epThemeColor : '',
color: item.path === active ? '#fff' : ''
}"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? 'bookmark')" />
<span class="result-item-title">{{ t(item.meta?.title) }}</span>
<enterOutlined />
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
@@ -49,6 +28,17 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const itemStyle = computed(() => {
return item => {
return {
background:
item?.path === active.value ? useEpThemeStoreHook().epThemeColor : "",
color: item.path === active.value ? "#fff" : "",
fontSize: item.path === active.value ? "16px" : "14px"
};
};
});
const active = computed({
get() {
return props.value;
@@ -67,6 +57,24 @@ function handleTo() {
emit("enter");
}
</script>
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item dark:bg-[#1d1d1d]"
:style="itemStyle(item)"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? 'bookmark-2-line')" />
<span class="result-item-title">{{ t(item.meta?.title) }}</span>
<enterOutlined />
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.result {
padding-bottom: 12px;
@@ -78,8 +86,9 @@ function handleTo() {
margin-top: 8px;
padding: 14px;
border-radius: 4px;
background: #e5e7eb;
cursor: pointer;
border: 0.1px solid #ccc;
transition: all 0.3s;
&-title {
display: flex;

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { SearchModal } from "./components";
import useBoolean from "../../hooks/useBoolean";
import { useBoolean } from "../../hooks/useBoolean";
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
@@ -8,23 +8,11 @@ function handleSearch() {
</script>
<template>
<div class="search-container" @click="handleSearch">
<div
class="search-container w-40px h-48px flex-c cursor-pointer navbar-bg-hover"
@click="handleSearch"
>
<IconifyIconOffline icon="search" />
</div>
<SearchModal v-model:value="show" />
</template>
<style lang="scss" scoped>
.search-container {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 40px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
</style>

View File

@@ -1,75 +1,57 @@
<script setup lang="ts">
import {
reactive,
ref,
unref,
watch,
reactive,
computed,
nextTick,
useCssModule,
getCurrentInstance
useCssModule
} from "vue";
import { find } from "lodash-unified";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "/@/utils/mitt";
import { resetRouter } from "/@/router";
import { templateRef } from "@vueuse/core";
import { debounce } from "/@/utils/debounce";
import { themeColorsType } from "../../types";
import { routerArrays } from "/@/layout/types";
import { useNav } from "/@/layout/hooks/useNav";
import { useAppStoreHook } from "/@/store/modules/app";
import { shadeBgColor } from "../../theme/element-plus";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { createNewStyle, writeNewStyle } from "../../theme/element-plus";
import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
import {
useDark,
debounce,
useGlobal,
storageLocal,
storageSession
} from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component";
const router = useRouter();
const { device } = useNav();
const { isDark } = useDark();
const { isSelect } = useCssModule();
const body = document.documentElement as HTMLElement;
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
const instanceConfig =
getCurrentInstance().appContext.app.config.globalProperties.$config;
let themeColors = ref<Array<themeColorsType>>([
// 道奇蓝(默认)
{ color: "#1b2a47", themeColor: "default" },
// 亮白色
{ color: "#ffffff", themeColor: "light" },
// 猩红色
{ color: "#f5222d", themeColor: "dusk" },
// 橙红色
{ color: "#fa541c", themeColor: "volcano" },
// 金色
{ color: "#fadb14", themeColor: "yellow" },
// 绿宝石
{ color: "#13c2c2", themeColor: "mingQing" },
// 酸橙绿
{ color: "#52c41a", themeColor: "auroraGreen" },
// 深粉色
{ color: "#eb2f96", themeColor: "pink" },
// 深紫罗兰色
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const { $storage } = useGlobal<GlobalPropertiesApi>();
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
let layoutTheme =
ref(storageLocal.getItem("responsive-layout")) ||
ref({
layout: instanceConfig?.Layout ?? "vertical",
theme: instanceConfig?.Theme ?? "default"
});
const {
body,
dataTheme,
layoutTheme,
themeColors,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
} = useDataThemeChange();
// body添加layout属性作用于src/style/sidebar.scss
/* body添加layout属性作用于src/style/sidebar.scss */
if (unref(layoutTheme)) {
let layout = unref(layoutTheme).layout;
let theme = unref(layoutTheme).theme;
@@ -79,20 +61,18 @@ if (unref(layoutTheme)) {
setLayoutModel(layout);
}
// 默认灵动模式
const markValue = ref(instance.configure?.showModel ?? "smart");
/** 默认灵动模式 */
const markValue = ref($storage.configure?.showModel ?? "smart");
const logoVal = ref(instance.configure?.showLogo ?? true);
const epThemeColor = ref(useEpThemeStoreHook().getEpThemeColor);
const logoVal = ref($storage.configure?.showLogo ?? true);
const settings = reactive({
greyVal: instance.configure.grey,
weakVal: instance.configure.weak,
tabsVal: instance.configure.hideTabs,
showLogo: instance.configure.showLogo,
showModel: instance.configure.showModel,
multiTagsCache: instance.configure.multiTagsCache
greyVal: $storage.configure.grey,
weakVal: $storage.configure.weak,
tabsVal: $storage.configure.hideTabs,
showLogo: $storage.configure.showLogo,
showModel: $storage.configure.showModel,
multiTagsCache: $storage.configure.multiTagsCache
});
const getThemeColorStyle = computed(() => {
@@ -101,10 +81,17 @@ const getThemeColorStyle = computed(() => {
};
});
/** 当网页为暗黑模式时不显示亮白色切换选项 */
const showThemeColors = computed(() => {
return themeColor => {
return themeColor === "light" && isDark.value ? false : true;
};
});
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = instance.configure;
const storageConfigure = $storage.configure;
storageConfigure[key] = val;
instance.configure = storageConfigure;
$storage.configure = storageConfigure;
}
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
@@ -114,13 +101,13 @@ function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
// 灰色模式设置
/** 灰色模式设置 */
const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
storageConfigureChange("grey", value);
};
// 色弱模式设置
/** 色弱模式设置 */
const weekChange = (value): void => {
toggleClass(
settings.weakVal,
@@ -133,7 +120,7 @@ const weekChange = (value): void => {
const tagsChange = () => {
let showVal = settings.tabsVal;
storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal);
emitter.emit("tagViewsChange", showVal as unknown as string);
};
const multiTagsCacheChange = () => {
@@ -142,27 +129,19 @@ const multiTagsCacheChange = () => {
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
// 清空缓存并返回登录页
/** 清空缓存并返回登录页 */
function onReset() {
router.push("/login");
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
]);
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
storageLocal.clear();
storageSession.clear();
resetRouter();
}
function onChange(label) {
@@ -170,7 +149,7 @@ function onChange(label) {
emitter.emit("tagViewsShowModel", label);
}
// 侧边栏Logo
/** 侧边栏Logo */
function logoChange() {
unref(logoVal)
? storageConfigureChange("showLogo", true)
@@ -184,7 +163,9 @@ function setFalse(Doms): any {
});
}
watch(instance, ({ layout }) => {
watch($storage, ({ layout }) => {
/* 设置wangeditorV5主题色 */
body.style.setProperty("--w-e-toolbar-active-color", layout["epThemeColor"]);
switch (layout["layout"]) {
case "vertical":
toggleClass(true, isSelect, unref(verticalRef));
@@ -204,7 +185,7 @@ watch(instance, ({ layout }) => {
}
});
// 主题色 激活选择项
/** 主题色 激活选择项 */
const getThemeColor = computed(() => {
return current => {
if (
@@ -223,83 +204,27 @@ const getThemeColor = computed(() => {
};
});
// 设置导航模式
/** 设置导航模式 */
function setLayoutModel(layout: string) {
layoutTheme.value.layout = layout;
window.document.body.setAttribute("layout", layout);
instance.layout = {
$storage.layout = {
layout,
theme: layoutTheme.value.theme,
darkMode: instance.layout.darkMode,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
useAppStoreHook().setLayout(layout);
}
// 存放夜间主题切换前的导航主题色
let tempLayoutThemeColor;
// 设置导航主题色
function setLayoutThemeColor(theme: string) {
tempLayoutThemeColor = instance.layout.theme;
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
instance.layout = {
layout: useAppStoreHook().layout,
theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
setEpThemeColor(colors.color);
}
}
// 设置ep主题色
const setEpThemeColor = (color: string) => {
// @ts-expect-error
writeNewStyle(createNewStyle(color));
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
};
let dataTheme = ref<boolean>(instance.layout.darkMode);
// 日间、夜间主题切换
function dataThemeChange() {
if (dataTheme.value) {
body.setAttribute("data-theme", "dark");
setLayoutThemeColor("light");
} else {
body.setAttribute("data-theme", "");
tempLayoutThemeColor && setLayoutThemeColor(tempLayoutThemeColor);
instance.layout = {
layout: useAppStoreHook().layout,
theme: instance.layout.theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
}
}
//初始化项目配置
/* 初始化项目配置 */
nextTick(() => {
settings.greyVal &&
document.querySelector("html")?.setAttribute("class", "html-grey");
settings.weakVal &&
document.querySelector("html")?.setAttribute("class", "html-weakness");
settings.tabsVal && tagsChange();
// @ts-expect-error
writeNewStyle(createNewStyle(epThemeColor.value));
dataThemeChange();
});
</script>
@@ -329,7 +254,12 @@ nextTick(() => {
</li>
</el-tooltip>
<el-tooltip class="item" content="顶部模式" placement="bottom">
<el-tooltip
v-if="device !== 'mobile'"
class="item"
content="顶部模式"
placement="bottom"
>
<li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
ref="horizontalRef"
@@ -340,7 +270,12 @@ nextTick(() => {
</li>
</el-tooltip>
<el-tooltip class="item" content="混合模式" placement="bottom">
<el-tooltip
v-if="device !== 'mobile'"
class="item"
content="混合模式"
placement="bottom"
>
<li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
ref="mixRef"
@@ -352,11 +287,12 @@ nextTick(() => {
</el-tooltip>
</ul>
<el-divider v-show="!dataTheme">主题色</el-divider>
<ul class="theme-color" v-show="!dataTheme">
<el-divider>主题色</el-divider>
<ul class="theme-color">
<li
v-for="(item, index) in themeColors"
:key="index"
v-show="showThemeColors(item.themeColor)"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)"
>
@@ -372,7 +308,7 @@ nextTick(() => {
<el-divider>界面显示</el-divider>
<ul class="setting">
<li v-show="!dataTheme">
<li>
<span>灰色模式</span>
<el-switch
v-model="settings.greyVal"
@@ -383,7 +319,7 @@ nextTick(() => {
@change="greyChange"
/>
</li>
<li v-show="!dataTheme">
<li>
<span>色弱模式</span>
<el-switch
v-model="settings.weakVal"
@@ -446,13 +382,13 @@ nextTick(() => {
@click="onReset"
>
<IconifyIconOffline
icon="logout-circle-r-line"
icon="fa-sign-out"
width="15"
height="15"
style="margin-right: 4px"
/>
清空缓存并返回登录页</el-button
>
清空缓存并返回登录页
</el-button>
</panel>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { isEqual } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import { ref, watch, onMounted, toRaw } from "vue";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
@@ -9,24 +9,29 @@ import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
const route = useRoute();
const levelList = ref([]);
const router = useRouter();
const routes = router.options.routes;
const multiTags = useMultiTagsStoreHook().multiTags;
const routes: any = router.options.routes;
const multiTags: any = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string);
if (!name) {
return false;
}
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
if (!name) return false;
return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
};
const getBreadcrumb = (): void => {
// 当前路由信息
let currentRoute;
if (Object.keys(route.query).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.query, item?.query)) {
currentRoute = item;
currentRoute = toRaw(item);
}
});
} else if (Object.keys(route.params).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.params, item?.params)) {
currentRoute = toRaw(item);
}
});
} else {
@@ -38,29 +43,12 @@ const getBreadcrumb = (): void => {
let matched = [];
// 获取每个父级路径对应的路由信息
parentRoutes.forEach(path => {
if (path !== "/") {
matched.push(findRouteByPath(path, routes));
}
if (path !== "/") matched.push(findRouteByPath(path, routes));
});
if (router.currentRoute.value.meta?.refreshRedirect) {
matched.unshift(
findRouteByPath(
router.currentRoute.value.meta.refreshRedirect as string,
routes
)
);
} else {
// 过滤与子级相同标题的父级路由
matched = matched.filter(item => {
return !item.redirect || (item.redirect && item.children.length !== 1);
});
}
if (currentRoute?.path !== "/welcome") {
matched.push(currentRoute);
}
const first = matched[0];
if (!isDashboard(first)) {
if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
if (!isDashboard(matched[0])) {
matched = [
{
path: "/welcome",
@@ -70,59 +58,51 @@ const getBreadcrumb = (): void => {
].concat(matched);
}
matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return;
if (item?.children) {
item.children.forEach(v => {
if (v?.meta?.title === item?.meta?.title) {
matched.splice(index, 1);
}
});
}
});
levelList.value = matched.filter(
item => item?.meta && item?.meta.title !== false
);
};
getBreadcrumb();
const handleLink = (item: RouteLocationMatched): void => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect as any);
} else {
router.push(path);
}
};
onMounted(() => {
getBreadcrumb();
});
watch(
() => route.path,
() => getBreadcrumb()
);
watch(
() => route.query,
() => getBreadcrumb()
);
const handleLink = (item: RouteLocationMatched): any => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect.toString());
return;
() => {
getBreadcrumb();
}
router.push(path);
};
);
</script>
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb class="!leading-[50px] select-none" separator="/">
<transition-group appear name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ transformI18n(item.meta.title) }}</span
>
<a v-else @click.prevent="handleLink(item)">
<el-breadcrumb-item v-for="item in levelList" :key="item.path">
<a @click.prevent="handleLink(item)">
{{ transformI18n(item.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@@ -1,63 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const fillColor = ref<string>("");
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
:class="classes.container"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
@mouseenter="fillColor = useEpThemeStoreHook().epThemeColor"
@mouseleave="fillColor = ''"
>
<svg
:fill="fillColor"
:class="['hamburger', props.isActive ? 'is-active' : '']"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -1,70 +1,39 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useNav } from "../../hooks/nav";
import { ref, watch } from "vue";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg";
import { useNav } from "/@/layout/hooks/useNav";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { watch, nextTick, onMounted, getCurrentInstance } from "vue";
import { deviceDetection } from "@pureadmin/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const menuRef = ref();
const { t, route, locale, translationCh, translationEn } =
useTranslationLang(menuRef);
const {
title,
routers,
logout,
backHome,
onPanel,
changeTitle,
handleResize,
menuSelect,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
onMounted(() => {
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize(menuRef.value);
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize(menuRef.value);
}
</script>
<template>
@@ -74,11 +43,11 @@ function translationEn() {
<h4>{{ title }}</h4>
</div>
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="route.path"
router
ref="menuRef"
mode="horizontal"
class="horizontal-header-menu"
:default-active="route.path"
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
@@ -97,33 +66,39 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" /> </span
>简体中文
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English</el-dropdown-item
>
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@@ -132,13 +107,13 @@ function translationEn() {
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { useDark } from "@pureadmin/utils";
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const { isDark } = useDark();
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div class="container">
<el-tooltip
placement="right"
:effect="isDark ? 'dark' : 'light'"
:content="props.isActive ? '点击折叠' : '点击展开'"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="cursor-pointer inline-block align-middle color-primary hover:color-primary !dark:hover:color-white w-16px h-16px ml-4 mb-1"
@click="toggleClick"
/>
</el-tooltip>
</div>
</template>
<style lang="scss" scoped>
.container {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
line-height: 40px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
</style>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { getCurrentInstance } from "vue";
import { useNav } from "/@/layout/hooks/useNav";
const props = defineProps({
collapse: Boolean
});
const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const { title } = useNav();
</script>
<template>

View File

@@ -1,47 +1,39 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { useNav } from "../../hooks/nav";
import { templateRef } from "@vueuse/core";
import avatars from "/@/assets/avatars.jpg";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { deviceDetection } from "@pureadmin/utils";
import { ref, toRaw, watch, onMounted } from "vue";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const menuRef = ref();
let defaultActive = ref(null);
const { t, route, locale, translationCh, translationEn } =
useTranslationLang(menuRef);
const {
device,
routers,
logout,
onPanel,
changeTitle,
toggleSideBar,
handleResize,
menuSelect,
resolvePath,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
let defaultActive = ref(null);
function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus;
// 当前路由的父级路径
/** 当前路由的父级路径 */
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = findRouteByPath(
parentRoutes,
@@ -51,67 +43,24 @@ function getDefaultActive(routePath) {
onMounted(() => {
getDefaultActive(route.path);
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
watch(
() => route.path,
() => {
getDefaultActive(route.path);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize(menuRef.value);
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize(menuRef.value);
}
</script>
<template>
<div class="horizontal-header">
<div
:class="classes.container"
:title="pureApp.sidebar.opened ? '点击折叠' : '点击展开'"
@click="toggleSideBar"
>
<svg
:fill="useEpThemeStoreHook().fill"
:class="[
'hamburger',
pureApp.sidebar.opened ? 'is-active-hamburger' : ''
]"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
<div v-if="device !== 'mobile'" class="horizontal-header">
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="defaultActive"
router
ref="menuRef"
mode="horizontal"
class="horizontal-header-menu"
:default-active="defaultActive"
@select="indexPath => menuSelect(indexPath, routers)"
>
<el-menu-item
@@ -120,10 +69,15 @@ function translationEn() {
:index="resolvePath(route) || route.redirect"
>
<template #title>
<div v-show="route.meta.icon" :class="['el-icon', route.meta.icon]">
<component :is="useRenderIcon(route.meta && route.meta.icon)" />
<div
v-if="toRaw(route.meta.icon)"
:class="['sub-menu-icon', route.meta.icon]"
>
<component
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/>
</div>
<span>{{ transformI18n(route.meta.title) }}</span>
<span class="select-none">{{ transformI18n(route.meta.title) }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
@@ -144,31 +98,39 @@ function translationEn() {
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<globalization
class="navbar-bg-hover w-40px h-48px p-11px cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
><span class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></span
>简体中文</el-dropdown-item
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['!dark:color-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
><span class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></span
>English</el-dropdown-item
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登 -->
<!-- 退出登 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
<p v-if="username" class="dark:color-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
@@ -177,13 +139,13 @@ function translationEn() {
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="el-icon-setting"
class="el-icon-setting navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
@@ -193,26 +155,7 @@ function translationEn() {
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style lang="scss" scoped>
.hamburger {
width: 20px;
height: 20px;
&:hover {
cursor: pointer;
}
}
.is-active-hamburger {
transform: rotate(180deg);
}
.translation {
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;

View File

@@ -1,14 +1,12 @@
<script setup lang="ts">
import path from "path";
import { useNav } from "../../hooks/nav";
import { childrenType } from "../../types";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import { useAppStoreHook } from "/@/store/modules/app";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { ref, PropType, nextTick, computed, CSSProperties } from "vue";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
const { pureApp } = useNav();
const menuMode = ["vertical", "mix"].includes(pureApp.layout);
const { layout, isCollapse } = useNav();
const props = defineProps({
item: {
@@ -25,7 +23,7 @@ const props = defineProps({
});
const getExtraIconStyle = computed((): CSSProperties => {
if (useAppStoreHook().getSidebarStatus) {
if (!isCollapse.value) {
return {
position: "absolute",
right: "10px"
@@ -46,7 +44,7 @@ const getNoDropdownStyle = computed((): CSSProperties => {
const getDivStyle = computed((): CSSProperties => {
return {
width: pureApp.sidebar.opened ? "" : "100%",
width: !isCollapse.value ? "" : "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
@@ -54,9 +52,8 @@ const getDivStyle = computed((): CSSProperties => {
};
});
const getMenuTextStyle = computed((): CSSProperties => {
const getMenuTextStyle = computed(() => {
return {
width: pureApp.sidebar.opened ? "125px" : "",
overflow: "hidden",
textOverflow: "ellipsis",
outline: "none"
@@ -65,14 +62,14 @@ const getMenuTextStyle = computed((): CSSProperties => {
const getSubTextStyle = computed((): CSSProperties => {
return {
width: pureApp.sidebar.opened ? "125px" : "",
width: !isCollapse.value ? "210px" : "",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
};
});
const getSpanStyle = computed((): CSSProperties => {
const getSpanStyle = computed(() => {
return {
overflow: "hidden",
textOverflow: "ellipsis"
@@ -148,21 +145,31 @@ function resolvePath(routePath) {
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
>
<div class="sub-menu-icon" v-show="props.item.meta.icon">
<div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)">
<component
:is="
useRenderIcon(
onlyOneChild.meta.icon ||
(props.item.meta && props.item.meta.icon)
toRaw(onlyOneChild.meta.icon) ||
(props.item.meta && toRaw(props.item.meta.icon))
)
"
/>
</div>
<div
v-if="
!pureApp.sidebar.opened &&
pureApp.layout === 'mix' &&
props.item?.pathList?.length === 2
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</div>
<div
v-if="
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
@@ -172,9 +179,9 @@ function resolvePath(routePath) {
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="!menuMode">{{
transformI18n(onlyOneChild.meta.title)
}}</span>
<span v-if="layout === 'horizontal'">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
<el-tooltip
v-else
placement="top"
@@ -205,24 +212,21 @@ function resolvePath(routePath) {
</el-menu-item>
</template>
<el-sub-menu
v-else
ref="subMenu"
:index="resolvePath(props.item.path)"
popper-append-to-body
>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(props.item.path)">
<template #title>
<div v-show="props.item.meta.icon" class="sub-menu-icon">
<div v-if="toRaw(props.item.meta.icon)" class="sub-menu-icon">
<component
:is="useRenderIcon(props.item.meta && props.item.meta.icon)"
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
</div>
<span v-if="!menuMode">{{ transformI18n(props.item.meta.title) }}</span>
<span v-if="layout === 'horizontal'">
{{ transformI18n(props.item.meta.title) }}
</span>
<el-tooltip
v-else
placement="top"
:offset="-10"
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip"
:disabled="!isCollapse || !props.item.showTooltip"
>
<template #content>
{{ transformI18n(props.item.meta.title) }}

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
class="px-3 mr-1 navbar-bg-hover"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="inline-block align-middle hover:color-primary !dark:hover:color-white"
/>
</div>
</template>

View File

@@ -1,26 +1,28 @@
<script setup lang="ts">
import Logo from "./logo.vue";
import { useRoute } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../../hooks/nav";
import SidebarItem from "./sidebarItem.vue";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import leftCollapse from "./leftCollapse.vue";
import type { StorageConfigs } from "/#/index";
import { useNav } from "/@/layout/hooks/useNav";
import { storageLocal } from "@pureadmin/utils";
import { ref, computed, watch, onBeforeMount } from "vue";
import { findRouteByPath, getParentPaths } from "/@/router/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute();
const routers = useRouter().options.routes;
const showLogo = ref(
storageLocal.getItem("responsive-configure")?.showLogo ?? true
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showLogo ?? true
);
const { pureApp, isCollapse, menuSelect } = useNav();
const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } =
useNav();
let subMenuData = ref([]);
const menuData = computed(() => {
return pureApp.layout === "mix"
return pureApp.layout === "mix" && device.value !== "mobile"
? subMenuData.value
: usePermissionStoreHook().wholeMenus;
});
@@ -59,25 +61,33 @@ watch(
<template>
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']">
<Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-scrollbar
wrap-class="scrollbar-wrapper"
:class="[device === 'mobile' ? 'mobile' : 'pc']"
>
<el-menu
:default-active="route.path"
:collapse="isCollapse"
unique-opened
router
:collapse-transition="false"
unique-opened
mode="vertical"
class="outer-most"
class="outer-most select-none"
:collapse="isCollapse"
:default-active="route.path"
:collapse-transition="false"
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
v-for="routes in menuData"
:key="routes.path"
:item="routes"
class="outer-most"
:base-path="routes.path"
class="outer-most select-none"
/>
</el-menu>
</el-scrollbar>
<leftCollapse
v-if="device !== 'mobile'"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
</div>
</template>

View File

@@ -43,7 +43,7 @@
font-size: 14px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
color: var(--el-text-color-primary);
background: #fff;
position: relative;
box-shadow: 0 0 1px #888;
@@ -92,7 +92,7 @@
a {
text-decoration: none;
color: #666;
color: var(--el-text-color-primary);
padding: 0 4px;
}
@@ -144,7 +144,7 @@
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
color: #000000d9;
color: var(--el-text-color-primary);
font-weight: normal;
font-size: 13px;
white-space: nowrap;
@@ -160,7 +160,7 @@
align-items: center;
&:hover {
background: var(--el-color-primary-light-9);
// background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
@@ -173,8 +173,6 @@
}
.el-dropdown-menu {
padding: 0;
li {
width: 100%;
margin: 0;
@@ -207,7 +205,7 @@
}
.scroll-item.is-active {
background-color: var(--el-color-primary-light-9);
// background-color: var(--el-color-primary-light-9);
position: relative;
color: #fff;
@@ -220,7 +218,7 @@
}
a {
color: var(--el-color-primary);
color: var(--el-color-primary) !important;
}
}
@@ -228,7 +226,7 @@
.arrow-right {
width: 40px;
height: 38px;
color: #00000073;
color: var(--el-text-color-primary);
position: relative;
svg {

View File

@@ -1,115 +1,53 @@
<script setup lang="ts">
import {
ref,
watch,
unref,
toRaw,
reactive,
nextTick,
computed,
ComputedRef,
CSSProperties,
onBeforeMount,
getCurrentInstance
} from "vue";
import close from "/@/assets/svg/close.svg?component";
import refresh from "/@/assets/svg/refresh.svg?component";
import closeAll from "/@/assets/svg/close_all.svg?component";
import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag";
import { routerArrays } from "/@/layout/types";
import { isEqual, isEmpty } from "lodash-unified";
import { transformI18n, $t } from "/@/plugins/i18n";
import { RouteConfigs, tagsViewsType } from "../../types";
import { toggleClass, removeClass } from "@pureadmin/utils";
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const translateX = ref<number>(0);
const activeIndex = ref<number>(-1);
let refreshButton = "refresh-button";
const instance = getCurrentInstance();
const pureSetting = useSettingStoreHook();
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
const showTags =
ref(storageLocal.getItem("responsive-configure").hideTabs) ?? "false";
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
return useMultiTagsStoreHook()?.multiTags;
});
const {
route,
router,
visible,
showTags,
instance,
multiTags,
tagsViews,
buttonTop,
buttonLeft,
showModel,
translateX,
activeIndex,
getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n
} = useTags();
const linkIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "is-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "is-active";
} else {
return "";
}
}
};
});
const scheduleIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "schedule-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "schedule-active";
} else {
return "";
}
}
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return true;
} else {
return false;
}
} else {
if (isEqual(route?.query, item?.query)) {
return true;
} else {
return false;
}
}
};
});
const tabDom = ref();
const containerDom = ref();
const scrollbarDom = ref();
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
if (item?.query) {
return isEqual(route?.query, item?.query);
if (item.query) {
return isEqual(route.query, item.query);
} else if (item.params) {
return isEqual(route.params, item.params);
} else {
return item.path === route.path;
}
@@ -117,23 +55,9 @@ const dynamicTagView = () => {
moveToView(index);
};
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
const tabNavPadding = 10;
const moveToView = (index: number): void => {
if (!instance.refs["dynamic" + index]) {
return;
}
const tabNavPadding = 10;
if (!instance.refs["dynamic" + index]) return;
const tabItemEl = instance.refs["dynamic" + index][0];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth;
@@ -143,7 +67,6 @@ const moveToView = (index: number): void => {
: 0;
// 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
translateX.value = 0;
} else if (tabItemElOffsetLeft < -translateX.value) {
@@ -192,68 +115,6 @@ const handleScroll = (offset: number): void => {
}
};
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
// 显示模式,默认灵动模式显示
const showModel = ref(
storageLocal.getItem("responsive-configure")?.showModel || "smart"
);
if (!showModel.value) {
const configure = storageLocal.getItem("responsive-configure");
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
let visible = ref(false);
let buttonLeft = ref(0);
let buttonTop = ref(0);
// 当前右键选中的路由信息
let currentSelect = ref({});
function dynamicRouteTag(value: string, parentPath: string): void {
const hasValue = multiTags.value.some(item => {
return item.path === value;
@@ -278,11 +139,12 @@ function dynamicRouteTag(value: string, parentPath: string): void {
});
}
}
concatPath(router.options.routes, value, parentPath);
concatPath(router.options.routes as any, value, parentPath);
}
// 重新加载
/** 刷新路由 */
function onFresh() {
const refreshButton = "refresh-button";
toggleClass(true, refreshButton, document.querySelector(".rotate"));
const { fullPath, query } = unref(route);
router.replace({
@@ -302,6 +164,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (item.path === obj.path) {
return item.query === obj.query;
}
} else if (item.params) {
if (item.path === obj.path) {
return item.params === obj.params;
}
} else {
return item.path === obj.path;
}
@@ -313,23 +179,12 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean
): void => {
if (other) {
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
},
obj
]);
useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]);
} else {
// @ts-ignore
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
startIndex,
length
});
}) as any;
}
};
@@ -351,24 +206,25 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
: handleAliveRoute(route.matched, "delete");
// 如果删除当前激活tag就自动切换到最后一个tag
if (tag === "left") return;
nextTick(() => {
router.push({
path: newRoute[0].path,
query: newRoute[0].query
});
});
if (newRoute[0]?.query) {
router.push({ name: newRoute[0].name, query: newRoute[0].query });
} else if (newRoute[0]?.params) {
router.push({ name: newRoute[0].name, params: newRoute[0].params });
} else {
router.push({ path: newRoute[0].path });
}
} else {
// 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!multiTags.value.length) return;
let isHasActiveTag = multiTags.value.some(item => {
return item.path === route.path;
});
!isHasActiveTag &&
router.push({
path: newRoute[0].path,
query: newRoute[0].query
});
if (multiTags.value.some(item => item.path === route.path)) return;
if (newRoute[0]?.query) {
router.push({ name: newRoute[0].name, query: newRoute[0].query });
} else if (newRoute[0]?.params) {
router.push({ name: newRoute[0].name, params: newRoute[0].params });
} else {
router.push({ path: newRoute[0].path });
}
}
}
@@ -385,7 +241,8 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
path: selectRoute.path,
meta: selectRoute.meta,
name: selectRoute.name,
query: selectRoute.query
query: selectRoute?.query,
params: selectRoute?.params
};
} else {
selectTagRoute = { path: route.path, meta: route.meta };
@@ -394,7 +251,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
// 当前路由信息
switch (key) {
case 0:
// 重新加载
// 刷新路由
onFresh();
break;
case 1:
@@ -428,21 +285,16 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
});
}
function handleCommand(command: object) {
// @ts-expect-error
function handleCommand(command: any) {
const { key, item } = command;
onClickDrop(key, item);
}
// 触发右键中菜单的点击事件
/** 触发右键中菜单的点击事件 */
function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
}
function closeMenu() {
visible.value = false;
}
function showMenus(value: boolean) {
Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews[v].show = value;
@@ -455,7 +307,7 @@ function disabledMenus(value: boolean) {
});
}
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel(
currentPath: string,
query: object = {},
@@ -515,7 +367,7 @@ function openMenu(tag, e) {
// 右键菜单为首页,只显示刷新
showMenus(false);
tagsViews[0].show = true;
} else if (route.path !== tag.path) {
} else if (route.path !== tag.path && route.name !== tag.name) {
// 右键菜单不匹配当前路由,隐藏刷新
tagsViews[0].show = false;
showMenuModel(tag.path, tag.query);
@@ -543,63 +395,36 @@ function openMenu(tag, e) {
} else {
buttonLeft.value = left;
}
pureSetting.hiddenSideBar
useSettingStoreHook().hiddenSideBar
? (buttonTop.value = e.clientY)
: (buttonTop.value = e.clientY - 40);
setTimeout(() => {
nextTick(() => {
visible.value = true;
}, 10);
}
// 触发tags标签切换
function tagOnClick(item) {
router.push({
path: item?.path,
query: item?.query
});
showMenuModel(item?.path, item?.query);
}
// 鼠标移入
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
}
}
// 鼠标移出
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
watch(
() => visible.value,
val => {
if (val) {
document.body.addEventListener("click", closeMenu);
/** 触发tags标签切换 */
function tagOnClick(item) {
const { name, path } = item;
if (name) {
if (item.query) {
router.push({
name,
query: item.query
});
} else if (item.params) {
router.push({
name,
params: item.params
});
} else {
document.body.removeEventListener("click", closeMenu);
router.push({ name });
}
} else {
router.push({ path });
}
);
// showMenuModel(item?.path, item?.query);
}
onBeforeMount(() => {
if (!instance) return;
@@ -608,9 +433,9 @@ onBeforeMount(() => {
showMenuModel(route.fullPath);
// 触发隐藏标签页
emitter.on("tagViewsChange", key => {
if (unref(showTags) === key) return;
showTags.value = key;
emitter.on("tagViewsChange", (key: any) => {
if (unref(showTags as any) === key) return;
(showTags as any).value = key;
});
// 改变标签风格
@@ -627,14 +452,18 @@ onBeforeMount(() => {
});
});
const getTabStyle = computed((): CSSProperties => {
return {
transform: `translateX(${translateX.value}px)`
};
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
const getContextMenuStyle = computed((): CSSProperties => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
onMounted(() => {
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
});
</script>
@@ -661,8 +490,11 @@ const getContextMenuStyle = computed((): CSSProperties => {
@mouseleave.prevent="onMouseleave(index)"
@click="tagOnClick(item)"
>
<router-link :to="item.path"
>{{ transformI18n(item.meta.title) }}
<router-link
:to="item.path"
class="!dark:color-text_color_primary !dark:hover:color-primary"
>
{{ transformI18n(item.meta.title) }}
</router-link>
<span
v-if="
@@ -703,7 +535,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
>
<li v-if="item.show" @click="selectTag(key, item)">
<component :is="toRaw(item.icon)" :key="key" />
{{ t(item.text) }}
{{ transformI18n(item.text) }}
</li>
</div>
</ul>
@@ -712,7 +544,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
<ul class="right-button">
<li>
<span
:title="t('buttons.hsrefreshRoute')"
:title="transformI18n('buttons.hsrefreshRoute')"
class="el-icon-refresh-right rotate"
@click="onFresh"
>
@@ -725,7 +557,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
placement="bottom-end"
@command="handleCommand"
>
<IconifyIconOffline icon="arrow-down" />
<IconifyIconOffline icon="arrow-down" class="dark:color-white" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@@ -740,7 +572,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
:key="key"
style="margin-right: 6px"
/>
{{ t(item.text) }}
{{ transformI18n(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>

View File

@@ -1,20 +1,14 @@
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
defineOptions({
name: "FrameView"
});
const { t } = useI18n();
const loading = ref(false);
const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
@@ -22,6 +16,7 @@ const frameRef = ref<HTMLElement | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function hideLoading() {
loading.value = false;
@@ -45,11 +40,20 @@ function init() {
}
onMounted(() => {
loading.value = true;
init();
});
</script>
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<style lang="scss" scoped>
.frame {
height: calc(100vh - 88px);

View File

@@ -1,6 +1,6 @@
import { ref } from "vue";
export default function useBoolean(initValue = false) {
export function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {

View File

@@ -0,0 +1,116 @@
import { ref } from "vue";
import { getConfig } from "/@/config";
import { find } from "lodash-unified";
import { useLayout } from "./useLayout";
import { themeColorsType } from "../types";
import { TinyColor } from "@ctrl/tinycolor";
import { useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import {
darken,
lighten,
toggleTheme
} from "@pureadmin/theme/dist/browser-utils";
export function useDataThemeChange() {
const { layoutTheme, layout } = useLayout();
const themeColors = ref<Array<themeColorsType>>([
/* 道奇蓝(默认) */
{ color: "#1b2a47", themeColor: "default" },
/* 亮白色 */
{ color: "#ffffff", themeColor: "light" },
/* 猩红色 */
{ color: "#f5222d", themeColor: "dusk" },
/* 橙红色 */
{ color: "#fa541c", themeColor: "volcano" },
/* 金色 */
{ color: "#fadb14", themeColor: "yellow" },
/* 绿宝石 */
{ color: "#13c2c2", themeColor: "mingQing" },
/* 酸橙绿 */
{ color: "#52c41a", themeColor: "auroraGreen" },
/* 深粉色 */
{ color: "#eb2f96", themeColor: "pink" },
/* 深紫罗兰色 */
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const { $storage } = useGlobal<GlobalPropertiesApi>();
const dataTheme = ref<boolean>($storage?.layout?.darkMode);
const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") {
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
$storage.layout = {
layout: layout.value,
theme,
darkMode: dataTheme.value,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
setEpThemeColor(colors.color);
}
}
/**
* @description 自动计算hover和active颜色
* @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
*/
const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};
/** 设置ep主题色 */
const setEpThemeColor = (color: string) => {
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
document.documentElement.style.setProperty("--el-color-primary", color);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
lighten(color, i / 10)
);
}
for (let i = 1; i <= 2; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
darken(color, i / 10)
);
}
};
/** 日间、夜间主题切换 */
function dataThemeChange() {
/* 如果当前是light夜间主题默认切换到default主题 */
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
setLayoutThemeColor("default");
} else {
setLayoutThemeColor(useEpThemeStoreHook().epTheme);
}
if (dataTheme.value) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
return {
body,
dataTheme,
layoutTheme,
themeColors,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
};
}

View File

@@ -0,0 +1,60 @@
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { routerArrays } from "../types";
import { useGlobal } from "@pureadmin/utils";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
export function useLayout() {
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const initStorage = () => {
/** 路由 */
if (
useMultiTagsStore().multiTagsCache &&
(!$storage.tags || $storage.tags.length === 0)
) {
$storage.tags = routerArrays;
}
/** 国际化 */
if (!$storage.locale) {
$storage.locale = { locale: $config?.Locale ?? "zh" };
useI18n().locale.value = $config?.Locale ?? "zh";
}
/** 导航 */
if (!$storage.layout) {
$storage.layout = {
layout: $config?.Layout ?? "vertical",
theme: $config?.Theme ?? "default",
darkMode: $config?.DarkMode ?? false,
sidebarStatus: $config?.SidebarStatus ?? true,
epThemeColor: $config?.EpThemeColor ?? "#409EFF"
};
}
/** 灰色模式、色弱模式、隐藏标签页 */
if (!$storage.configure) {
$storage.configure = {
grey: $config?.Grey ?? false,
weak: $config?.Weak ?? false,
hideTabs: $config?.HideTabs ?? false,
showLogo: $config?.ShowLogo ?? true,
showModel: $config?.ShowModel ?? "smart",
multiTagsCache: $config?.MultiTagsCache ?? false
};
}
};
/** 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 */
const layout = computed(() => {
return $storage?.layout.layout;
});
const layoutTheme = computed(() => {
return $storage.layout;
});
return {
layout,
layoutTheme,
initStorage
};
}

View File

@@ -1,22 +1,28 @@
import { computed } from "vue";
import { router } from "/@/router";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { remainingPaths } from "/@/router";
import type { StorageConfigs } from "/#/index";
import { routerArrays } from "/@/layout/types";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths, resetRouter } from "/@/router";
import { storageSession, useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
const errorInfo = "当前路由配置不正确,请检查配置";
export function useNav() {
const pureApp = useAppStoreHook();
// 用户名
const username: string = storageSession.getItem("info")?.username;
const routers = useRouter().options.routes;
/** 用户名 */
const username: string =
storageSession.getItem<StorageConfigs>("info")?.username;
// 设置国际化选中后的样式
/** 设置国际化选中后的样式 */
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
@@ -26,6 +32,12 @@ export function useNav() {
};
});
const getDropdownItemClass = computed(() => {
return (locale, t) => {
return locale === t ? "" : "!dark:hover:color-primary";
};
});
const avatarsStyle = computed(() => {
return username ? { marginRight: "10px" } : "";
});
@@ -34,17 +46,32 @@ export function useNav() {
return !pureApp.getSidebarStatus;
});
// 动态title
const device = computed(() => {
return pureApp.getDevice;
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const layout = computed(() => {
return $storage?.layout?.layout;
});
const title = computed(() => {
return $config.Title;
});
/** 动态title */
function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) document.title = `${transformI18n(meta.title)} | ${Title}`;
else document.title = transformI18n(meta.title);
}
// 退出登录
/** 退出登录 */
function logout() {
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
storageSession.removeItem("info");
router.push("/login");
resetRouter();
}
function backHome() {
@@ -60,7 +87,7 @@ export function useNav() {
}
function handleResize(menuRef) {
menuRef.handleResize();
menuRef?.handleResize();
}
function resolvePath(route) {
@@ -81,7 +108,7 @@ export function useNav() {
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
/** 找到当前路由的信息 */
function findCurrentRoute(indexPath: string, routes) {
if (!routes) return console.error(errorInfo);
return routes.map(item => {
@@ -89,7 +116,7 @@ export function useNav() {
if (item.redirect) {
findCurrentRoute(item.redirect, item.children);
} else {
// 切换左侧菜单 通知标签页
/** 切换左侧菜单 通知标签页 */
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
@@ -103,13 +130,18 @@ export function useNav() {
findCurrentRoute(indexPath, routers);
}
// 判断路径是否参与菜单
/** 判断路径是否参与菜单 */
function isRemaining(path: string): boolean {
return remainingPaths.includes(path);
}
return {
title,
device,
layout,
logout,
routers,
$storage,
backHome,
onPanel,
changeTitle,
@@ -121,6 +153,7 @@ export function useNav() {
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
getDropdownItemStyle,
getDropdownItemClass
};
}

218
src/layout/hooks/useTag.ts Normal file
View File

@@ -0,0 +1,218 @@
import {
ref,
unref,
watch,
computed,
reactive,
onMounted,
CSSProperties,
getCurrentInstance
} from "vue";
import { tagsViewsType } from "../types";
import { isEqual } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router";
import { transformI18n, $t } from "/@/plugins/i18n";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
import close from "/@/assets/svg/close.svg?component";
import refresh from "/@/assets/svg/refresh.svg?component";
import closeAll from "/@/assets/svg/close_all.svg?component";
import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component";
export function useTags() {
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance();
const buttonTop = ref(0);
const buttonLeft = ref(0);
const translateX = ref(0);
const visible = ref(false);
const activeIndex = ref(-1);
// 当前右键选中的路由信息
const currentSelect = ref({});
/** 显示模式,默认灵动模式 */
const showModel = ref(
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel ||
"smart"
);
/** 是否隐藏标签页,默认显示 */
const showTags =
ref(
storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs
) ?? ref("false");
const multiTags: any = computed(() => {
return useMultiTagsStoreHook().multiTags;
});
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
function conditionHandle(item, previous, next) {
if (
Object.keys(route.query).length === 0 &&
Object.keys(route.params).length === 0
) {
return route.path === item.path ? previous : next;
} else if (Object.keys(route.query).length > 0) {
return isEqual(route.query, item.query) ? previous : next;
} else {
return isEqual(route.params, item.params) ? previous : next;
}
}
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
return conditionHandle(item, true, false);
};
});
const linkIsActive = computed(() => {
return item => {
return conditionHandle(item, "is-active", "");
};
});
const scheduleIsActive = computed(() => {
return item => {
return conditionHandle(item, "schedule-active", "");
};
});
const getTabStyle = computed((): CSSProperties => {
return {
transform: `translateX(${translateX.value}px)`
};
});
const getContextMenuStyle = computed((): CSSProperties => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
});
const closeMenu = () => {
visible.value = false;
};
/** 鼠标移入添加激活样式 */
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
}
}
/** 鼠标移出恢复默认样式 */
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
onMounted(() => {
if (!showModel.value) {
const configure = storageLocal.getItem<StorageConfigs>(
"responsive-configure"
);
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
});
watch(
() => visible.value,
() => {
useEventListener(document, "click", closeMenu);
}
);
return {
route,
router,
visible,
showTags,
instance,
multiTags,
showModel,
tagsViews,
buttonTop,
buttonLeft,
translateX,
activeIndex,
getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
$t,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n
};
}

View File

@@ -0,0 +1,37 @@
import { useNav } from "./useNav";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { watch, type Ref } from "vue";
export function useTranslationLang(ref?: Ref) {
const { $storage, changeTitle, handleResize } = useNav();
const { locale, t } = useI18n();
const route = useRoute();
function translationCh() {
$storage.locale = { locale: "zh" };
locale.value = "zh";
ref && handleResize(ref.value);
}
function translationEn() {
$storage.locale = { locale: "en" };
locale.value = "en";
ref && handleResize(ref.value);
}
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
return {
t,
route,
locale,
translationCh,
translationEn
};
}

View File

@@ -1,20 +1,11 @@
<script setup lang="ts">
import {
h,
reactive,
computed,
onMounted,
defineComponent,
getCurrentInstance
} from "vue";
import { setType } from "./types";
import { useI18n } from "vue-i18n";
import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt";
import { useLayout } from "./hooks/useLayout";
import { useAppStoreHook } from "/@/store/modules/app";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
import { h, reactive, computed, onMounted, defineComponent } from "vue";
import backTop from "/@/assets/svg/back_top.svg?component";
import fullScreen from "/@/assets/svg/full_screen.svg?component";
@@ -27,51 +18,11 @@ import setting from "./components/setting/index.vue";
import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue";
const { isDark } = useDark();
const { layout } = useLayout();
const isMobile = deviceDetection();
const pureSetting = useSettingStoreHook();
const instance = getCurrentInstance().appContext.app.config.globalProperties;
// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
const layout = computed(() => {
// 路由
if (
useMultiTagsStore().multiTagsCache &&
(!instance.$storage.tags || instance.$storage.tags.length === 0)
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.tags = routerArrays;
}
// 国际化
if (!instance.$storage.locale) {
// eslint-disable-next-line
instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
useI18n().locale.value = instance.$config?.Locale ?? "zh";
}
// 导航
if (!instance.$storage.layout) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.layout = {
layout: instance.$config?.Layout ?? "vertical",
theme: instance.$config?.Theme ?? "default",
darkMode: instance.$config?.DarkMode ?? false,
sidebarStatus: instance.$config?.SidebarStatus ?? true,
epThemeColor: instance.$config?.EpThemeColor ?? "#409EFF"
};
}
// 灰色模式、色弱模式、隐藏标签页
if (!instance.$storage.configure) {
// eslint-disable-next-line
instance.$storage.configure = {
grey: instance.$config?.Grey ?? false,
weak: instance.$config?.Weak ?? false,
hideTabs: instance.$config?.HideTabs ?? false,
showLogo: instance.$config?.ShowLogo ?? true,
showModel: instance.$config?.ShowModel ?? "smart",
multiTagsCache: instance.$config?.MultiTagsCache ?? false
};
}
return instance.$storage?.layout.layout;
});
const { $storage } = useGlobal<GlobalPropertiesApi>();
const set: setType = reactive({
sidebar: computed(() => {
@@ -96,18 +47,18 @@ const set: setType = reactive({
}),
hideTabs: computed(() => {
return instance.$storage?.configure.hideTabs;
return $storage?.configure.hideTabs;
})
});
function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel);
instance.$storage.layout = {
$storage.layout = {
layout: `${layoutModel}`,
theme: instance.$storage.layout?.theme,
darkMode: instance.$storage.layout?.darkMode,
sidebarStatus: instance.$storage.layout?.sidebarStatus,
epThemeColor: instance.$storage.layout?.epThemeColor
theme: $storage.layout?.theme,
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
}
@@ -123,7 +74,7 @@ let isAutoCloseSidebar = true;
emitter.on("resize", ({ detail }) => {
if (isMobile) return;
let { width } = detail;
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper类容器宽度
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
@@ -138,7 +89,7 @@ emitter.on("resize", ({ detail }) => {
isAutoCloseSidebar = false;
}
} else if (width > 990) {
if (!set.sidebar.isClickHamburger) {
if (!set.sidebar.isClickCollapse) {
toggle("desktop", true);
isAutoCloseSidebar = true;
}
@@ -165,7 +116,9 @@ const layoutHeader = defineComponent({
class: { "fixed-header": set.fixedHeader },
style: [
set.hideTabs && layout.value.includes("horizontal")
? "box-shadow: 0 1px 4px rgb(0 21 41 / 8%);"
? isDark.value
? "box-shadow: 0 1px 4px #0d0d0d"
: "box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08)"
: ""
]
},
@@ -185,10 +138,14 @@ const layoutHeader = defineComponent({
default: () => [
h(
"span",
{ onClick: onFullScreen },
{
onClick: onFullScreen
},
{
default: () => [
!pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
!pureSetting.hiddenSideBar
? h(fullScreen, { class: "dark:color-white" })
: h(exitScreen, { class: "dark:color-white" })
]
}
)
@@ -234,7 +191,8 @@ const layoutHeader = defineComponent({
<el-backtop
title="回到顶部"
target=".main-container .el-scrollbar__wrap"
><backTop />
>
<backTop />
</el-backtop>
<layout-header />
<!-- 主体内容 -->

View File

@@ -2,6 +2,10 @@
import { unref } from "vue";
import { useRouter } from "vue-router";
defineOptions({
name: "Redirect"
});
const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);

View File

@@ -1,84 +0,0 @@
/* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex";
import epCss from "./element.scss";
import { TinyColor } from "@ctrl/tinycolor";
import { convert } from "css-color-function";
// 色值表
const formula = {
"shade-1": "color(primary shade(10%))",
"light-1": "color(primary tint(10%))",
"light-2": "color(primary tint(20%))",
"light-3": "color(primary tint(30%))",
"light-4": "color(primary tint(40%))",
"light-5": "color(primary tint(50%))",
"light-6": "color(primary tint(60%))",
"light-7": "color(primary tint(70%))",
"light-8": "color(primary tint(80%))",
"light-9": "color(primary tint(90%))"
};
// 把生成的样式表写入到style中
export const writeNewStyle = (newStyle: string): void => {
const style = window.document.createElement("style");
style.innerText = newStyle;
window.document.head.appendChild(style);
};
// 根据主题色,生成最新的样式表
export const createNewStyle = (
primaryStyle: Record<string, any>
): Record<string, any> => {
// 根据主色生成色值表
const colors = createColors(primaryStyle);
// 在当前ep的默认样式表中标记需要替换的色值
let cssText = getStyleTemplate(epCss);
// 遍历生成的色值表,在 默认样式表 进行全局替换
Object.keys(colors).forEach(key => {
cssText = cssText.replace(
new RegExp("(:|\\s+)" + key, "g"),
"$1" + colors[key]
);
});
return cssText;
};
export const createColors = (
primary: Record<string, any>
): Record<string, any> => {
if (!primary) return;
const colors = {
primary
};
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/, primary);
colors[key] = "#" + rgbHex(convert(value));
});
return colors;
};
const getStyleTemplate = (data: Record<string, any>): Record<string, any> => {
const colorMap = {
"#3a8ee6": "shade-1",
"#409eff": "primary",
"#53a8ff": "light-1",
"#66b1ff": "light-2",
"#79bbff": "light-3",
"#8cc5ff": "light-4",
"#a0cfff": "light-5",
"#b3d8ff": "light-6",
"#c6e2ff": "light-7",
"#d9ecff": "light-8",
"#ecf5ff": "light-9"
};
Object.keys(colorMap).forEach(key => {
const value = colorMap[key];
data = data.replace(new RegExp(key, "ig"), value);
});
return data;
};
// 自动计算hover和active颜色 https://element-plus.gitee.io/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2-%E6%B5%8B%E8%AF%95%E7%89%88
export const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};

View File

@@ -1,2 +0,0 @@
/* 通过scss模块本地导入element-plus的全局样式文件解决vite2.7.13版本后使用 import epCss from "element-plus/dist/index.css",打包后加载不到样式的问题 */
@import "element-plus/dist/index.css";

View File

@@ -1,6 +1,20 @@
/**
* @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
*/
import { EpThemeColor } from "../../../public/serverConfig.json";
type MultipleScopeVarsItem = {
scopeName: string;
varsContent: string;
};
/** 将vxe默认主题色和ep默认主题色保持一致 */
const vxeColor = EpThemeColor;
/** 预设主题色 */
const themeColors = {
default: {
color: "#409EFF",
vxeColor,
subMenuActiveText: "#fff",
menuBg: "#001529",
menuHover: "#4091f7",
@@ -13,7 +27,7 @@ const themeColors = {
menuActiveBefore: "#4091f7"
},
light: {
color: "#409EFF",
vxeColor,
subMenuActiveText: "#409eff",
menuBg: "#fff",
menuHover: "#e0ebf6",
@@ -26,7 +40,7 @@ const themeColors = {
menuActiveBefore: "#4091f7"
},
dusk: {
color: "#f5222d",
vxeColor: "#f5222d",
subMenuActiveText: "#fff",
menuBg: "#2a0608",
menuHover: "#e13c39",
@@ -39,7 +53,7 @@ const themeColors = {
menuActiveBefore: "#e13c39"
},
volcano: {
color: "#fa541c",
vxeColor: "#fa541c",
subMenuActiveText: "#fff",
menuBg: "#2b0e05",
menuHover: "#e85f33",
@@ -52,7 +66,7 @@ const themeColors = {
menuActiveBefore: "#e85f33"
},
yellow: {
color: "#fadb14",
vxeColor: "#fadb14",
subMenuActiveText: "#d25f00",
menuBg: "#2b2503",
menuHover: "#f6da4d",
@@ -65,7 +79,7 @@ const themeColors = {
menuActiveBefore: "#f6da4d"
},
mingQing: {
color: "#13c2c2",
vxeColor: "#13c2c2",
subMenuActiveText: "#fff",
menuBg: "#032121",
menuHover: "#59bfc1",
@@ -78,7 +92,7 @@ const themeColors = {
menuActiveBefore: "#59bfc1"
},
auroraGreen: {
color: "#52c41a",
vxeColor: "#52c41a",
subMenuActiveText: "#fff",
menuBg: "#0b1e15",
menuHover: "#60ac80",
@@ -91,7 +105,7 @@ const themeColors = {
menuActiveBefore: "#60ac80"
},
pink: {
color: "#eb2f96",
vxeColor: "#eb2f96",
subMenuActiveText: "#fff",
menuBg: "#28081a",
menuHover: "#d84493",
@@ -104,7 +118,7 @@ const themeColors = {
menuActiveBefore: "#d84493"
},
saucePurple: {
color: "#722ed1",
vxeColor: "#722ed1",
subMenuActiveText: "#fff",
menuBg: "#130824",
menuHover: "#693ac9",
@@ -118,19 +132,28 @@ const themeColors = {
}
};
type MultipleScopeVarsItem = {
scopeName: string;
path: string;
varsContent: string;
};
export function genScssMultipleScopeVars(): MultipleScopeVarsItem[] {
/**
* @description 将预设主题色处理成主题插件所需格式
*/
export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => {
const result = [] as MultipleScopeVarsItem[];
Object.keys(themeColors).forEach(key => {
result.push({
scopeName: `layout-theme-${key}`,
varsContent: `$primary-color: ${themeColors[key].color} !default;$vxe-primary-color: $primary-color;$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;$menuBg: ${themeColors[key].menuBg} !default;$menuHover: ${themeColors[key].menuHover} !default;$subMenuBg: ${themeColors[key].subMenuBg} !default;$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;$navTextColor: ${themeColors[key].navTextColor} !default;$menuText: ${themeColors[key].menuText} !default;$sidebarLogo: ${themeColors[key].sidebarLogo} !default;$menuTitleHover: ${themeColors[key].menuTitleHover} !default;$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;`
varsContent: `
$vxe-primary-color: ${themeColors[key].vxeColor} !default;
$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
$menuBg: ${themeColors[key].menuBg} !default;
$menuHover: ${themeColors[key].menuHover} !default;
$subMenuBg: ${themeColors[key].subMenuBg} !default;
$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
$navTextColor: ${themeColors[key].navTextColor} !default;
$menuText: ${themeColors[key].menuText} !default;
$sidebarLogo: ${themeColors[key].sidebarLogo} !default;
$menuTitleHover: ${themeColors[key].menuTitleHover} !default;
$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
`
} as MultipleScopeVarsItem);
});
return result;
}
};

View File

@@ -12,7 +12,6 @@ export const routerArrays: Array<RouteConfigs> = [
export type routeMetaType = {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
@@ -23,6 +22,7 @@ export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
params?: object;
meta?: routeMetaType;
children?: RouteConfigs[];
name?: string;
@@ -44,7 +44,7 @@ export interface setType {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
isClickHamburger: boolean;
isClickCollapse: boolean;
};
device: string;
fixedHeader: boolean;
@@ -65,7 +65,6 @@ export type childrenType = {
meta?: {
icon?: string;
title?: string;
i18n?: boolean;
showParent?: boolean;
extraIcon?: {
svg?: boolean;

View File

@@ -6,7 +6,12 @@ import { getServerConfig } from "./config";
import { createApp, Directive } from "vue";
import { useI18n } from "../src/plugins/i18n";
import { MotionPlugin } from "@vueuse/motion";
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
// import { useEcharts } from "/@/plugins/echarts";
// import { useTable } from "../src/plugins/vxe-table";
import { injectResponsiveStorage } from "/@/utils/responsive";
// import Table from "@pureadmin/table";
// import PureDescriptions from "@pureadmin/descriptions";
import "uno.css";
import "animate.css";
@@ -17,6 +22,7 @@ import "./style/index.scss";
import "element-plus/dist/index.css";
import "@pureadmin/components/dist/index.css";
import "@pureadmin/components/dist/theme.css";
import "@pureadmin/components/dist/dark.scss";
// 导入字体图标
import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css";
@@ -45,5 +51,9 @@ getServerConfig(app).then(async config => {
injectResponsiveStorage(app, config);
setupStore(app);
app.use(MotionPlugin).use(useI18n).use(ElementPlus);
// .use(useEcharts);
// .use(Table);
// .use(PureDescriptions);
// .use(useTable);
app.mount("#app");
});

View File

@@ -1,6 +1,8 @@
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
const modules = import.meta.globEager("../mock/*.ts");
const modules: Record<string, any> = import.meta.glob("../mock/*.ts", {
eager: true
});
const mockModules = [];
Object.keys(modules).forEach(key => {

View File

@@ -0,0 +1,41 @@
import type { App } from "vue";
import * as echarts from "echarts/core";
import { SVGRenderer } from "echarts/renderers";
import { PieChart, BarChart, LineChart } from "echarts/charts";
import {
GridComponent,
TitleComponent,
LegendComponent,
GraphicComponent,
ToolboxComponent,
TooltipComponent,
DataZoomComponent,
VisualMapComponent
} from "echarts/components";
const { use } = echarts;
use([
PieChart,
BarChart,
LineChart,
SVGRenderer,
GridComponent,
TitleComponent,
LegendComponent,
GraphicComponent,
ToolboxComponent,
TooltipComponent,
DataZoomComponent,
VisualMapComponent
]);
/**
* @description 按需引入echarts
* @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6}
*/
export function useEcharts(app: App) {
app.config.globalProperties.$echarts = echarts;
}
export default echarts;

View File

@@ -1,6 +1,8 @@
import { App, Component } from "vue";
import {
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@@ -8,21 +10,30 @@ import {
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@@ -32,8 +43,22 @@ import {
ElEmpty,
ElCollapse,
ElCollapseItem,
ElDialog,
ElCard,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption,
ElTimeline,
ElTimelineItem,
ElResult,
ElSteps,
ElStep,
ElTree,
ElTreeV2,
ElPopconfirm,
ElCheckbox,
ElCheckboxGroup,
// 指令
ElLoading,
ElInfiniteScroll
@@ -44,6 +69,8 @@ const plugins = [ElLoading, ElInfiniteScroll];
const components = [
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@@ -51,21 +78,30 @@ const components = [
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@@ -75,8 +111,22 @@ const components = [
ElEmpty,
ElCollapse,
ElCollapseItem,
ElDialog,
ElCard
ElTree,
ElTreeV2,
ElPopconfirm,
ElCheckbox,
ElCheckboxGroup,
ElTable,
ElTableColumn,
ElLink,
ElColorPicker,
ElSelect,
ElOption,
ElTimeline,
ElTimelineItem,
ElResult,
ElSteps,
ElStep
];
export function useElementPlus(app: App) {

View File

@@ -1,6 +1,7 @@
// 多组件库的国际化和本地项目国际化兼容
import { App, WritableComputedRef } from "vue";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { storageLocal } from "@pureadmin/utils";
import { type I18n, createI18n } from "vue-i18n";
// element-plus国际化
@@ -9,12 +10,12 @@ import zhLocale from "element-plus/lib/locale/lang/zh-cn";
function siphonI18n(prefix = "zh-CN") {
return Object.fromEntries(
Object.entries(import.meta.globEager("../../locales/*.y(a)?ml")).map(
([key, value]) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
}
)
Object.entries(
import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
).map(([key, value]: any) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
})
)[prefix];
}
@@ -62,7 +63,8 @@ export const $t = (key: string) => key;
export const i18n: I18n = createI18n({
legacy: false,
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
locale:
storageLocal.getItem<StorageConfigs>("responsive-locale")?.locale ?? "zh",
fallbackLocale: "en",
messages: localesConfigs
});

View File

@@ -0,0 +1,6 @@
@import "vxe-table/styles/variable.scss";
@import "vxe-table/styles/modules.scss";
i {
border-color: initial;
}

View File

@@ -0,0 +1,114 @@
import "xe-utils";
import "./index.scss";
import XEUtils from "xe-utils";
import { App, unref } from "vue";
import { i18n } from "/@/plugins/i18n";
import "font-awesome/css/font-awesome.min.css";
import zh from "vxe-table/lib/locale/lang/zh-CN";
import en from "vxe-table/lib/locale/lang/en-US";
import {
// 核心
VXETable,
// 表格功能
Icon,
Filter,
Edit,
Menu,
Export,
Keyboard,
Validator,
// 可选组件
Column,
Colgroup,
Grid,
Tooltip,
Toolbar,
Pager,
Form,
FormItem,
FormGather,
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
RadioButton,
Switch,
Input,
Select,
Optgroup,
Option,
Textarea,
Button,
Modal,
List,
Pulldown,
// 表格
Table
} from "vxe-table";
// 全局默认参数
VXETable.setup({
size: "medium",
version: 0,
zIndex: 1002,
table: {
// 自动监听父元素的变化去重新计算表格
autoResize: true,
// 鼠标移到行是否要高亮显示
highlightHoverRow: true
},
input: {
clearable: true
},
i18n: (key, args) => {
return unref(i18n.global.locale) === "zh"
? XEUtils.toFormatString(XEUtils.get(zh, key), args)
: XEUtils.toFormatString(XEUtils.get(en, key), args);
},
translate(key) {
const NAMESPACED = ["el.", "buttons."];
if (key && NAMESPACED.findIndex(v => key.includes(v)) !== -1) {
return i18n.global.t.call(i18n.global.locale, key);
}
return key;
}
});
export function useTable(app: App) {
app
.use(Icon)
.use(Filter)
.use(Edit)
.use(Menu)
.use(Export)
.use(Keyboard)
.use(Validator)
// 可选组件
.use(Column)
.use(Colgroup)
.use(Grid)
.use(Tooltip)
.use(Toolbar)
.use(Pager)
.use(Form)
.use(FormItem)
.use(FormGather)
.use(Checkbox)
.use(CheckboxGroup)
.use(Radio)
.use(RadioGroup)
.use(RadioButton)
.use(Switch)
.use(Input)
.use(Select)
.use(Optgroup)
.use(Option)
.use(Textarea)
.use(Button)
.use(Modal)
.use(List)
.use(Pulldown)
// 安装表格
.use(Table);
}

View File

@@ -1,32 +1,32 @@
import { isUrl } from "/@/utils/is";
import { getConfig } from "/@/config";
import { toRouteType } from "./types";
import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress";
import { findIndex } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { buildHierarchyTree } from "/@/utils/tree";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import {
Router,
RouteMeta,
createRouter,
RouteRecordRaw,
RouteComponent,
RouteRecordName
RouteComponent
} from "vue-router";
import {
ascending,
initRouter,
getHistoryMode,
getParentPaths,
findRouteByPath,
handleAliveRoute,
formatTwoStageRoutes,
formatFlatteningRoutes
} from "./utils";
import {
buildHierarchyTree,
openLink,
isUrl,
storageSession
} from "@pureadmin/utils";
import homeRouter from "./modules/home";
import errorRouter from "./modules/error";
@@ -53,7 +53,7 @@ export const remainingPaths = Object.keys(remainingRouter).map(v => {
// 创建路由实例
export const router: Router = createRouter({
history: getHistoryMode(),
routes: constantRoutes.concat(...remainingRouter),
routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true,
scrollBehavior(to, from, savedPosition) {
return new Promise(resolve => {
@@ -70,6 +70,19 @@ export const router: Router = createRouter({
}
});
// 重置路由
export function resetRouter() {
router.getRoutes().forEach(route => {
const { name, meta } = route;
if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name);
router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
);
}
});
}
// 路由白名单
const whiteList = ["/login"];
@@ -78,13 +91,13 @@ router.beforeEach((to: toRouteType, _from, next) => {
const newMatched = to.matched;
handleAliveRoute(newMatched, "add");
// 页面整体刷新和点击标签页刷新
if (_from.name === undefined || _from.name === "redirect") {
if (_from.name === undefined || _from.name === "Redirect") {
handleAliveRoute(newMatched);
}
}
const name = storageSession.getItem("info");
const name = storageSession.getItem<StorageConfigs>("info");
NProgress.start();
const externalLink = isUrl(to?.name);
const externalLink = isUrl(to?.name as string);
if (!externalLink)
to.matched.some(item => {
if (!item.meta.title) return "";
@@ -97,7 +110,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (_from?.name) {
// name为超链接
if (externalLink) {
openLink(to?.name);
openLink(to?.name as string);
NProgress.done();
} else {
next();
@@ -107,69 +120,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (usePermissionStoreHook().wholeMenus.length === 0)
initRouter(name.username).then((router: Router) => {
if (!useMultiTagsStoreHook().getMultiTagsCache) {
const handTag = (
path: string,
parentPath: string,
name: RouteRecordName,
meta: RouteMeta
): void => {
const { path } = to;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes: any =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
// query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", {
path,
parentPath,
name,
meta
path: route.path,
name: route.name,
meta: route.meta
});
};
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对静态路由)
if (to.meta?.refreshRedirect) {
const routes = router.options.routes;
const { refreshRedirect } = to.meta;
const { name, meta } = findRouteByPath(refreshRedirect, routes);
handTag(
refreshRedirect,
getParentPaths(refreshRedirect, routes)[1],
name,
meta
);
return router.push(refreshRedirect);
} else {
const { path } = to;
const index = findIndex(remainingRouter, v => {
return v.path == path;
});
const routes =
index === -1
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
const routePartent = getParentPaths(path, routes);
// 未开启标签页缓存,刷新页面重定向到顶级路由(参考标签页操作例子,只针对动态路由)
if (
path !== routes[0].path &&
route?.meta?.rank !== 0 &&
routePartent.length === 0
) {
if (!route?.meta?.refreshRedirect) return;
const { name, meta } = findRouteByPath(
route.meta.refreshRedirect,
routes
);
handTag(
route.meta?.refreshRedirect,
getParentPaths(route.meta?.refreshRedirect, routes)[0],
name,
meta
);
return router.push(route.meta?.refreshRedirect);
} else {
handTag(
route.path,
routePartent[routePartent.length - 1],
route.name,
route.meta
);
return router.push(path);
}
}
}
router.push(to.fullPath);

View File

@@ -1,9 +1,8 @@
import { $t } from "/@/plugins/i18n";
const Layout = () => import("/@/layout/index.vue");
import type { RouteConfigsTable } from "/#/index";
const errorRouter = {
const errorRouter: RouteConfigsTable = {
path: "/error",
component: Layout,
redirect: "/error/403",
meta: {
icon: "information-line",

View File

@@ -1,9 +1,10 @@
import { $t } from "/@/plugins/i18n";
import type { RouteConfigsTable } from "/#/index";
const Layout = () => import("/@/layout/index.vue");
const homeRouter = {
const homeRouter: RouteConfigsTable = {
path: "/",
name: "home",
name: "Home",
component: Layout,
redirect: "/welcome",
meta: {
@@ -14,8 +15,8 @@ const homeRouter = {
children: [
{
path: "/welcome",
name: "welcome",
component: () => import("/@/views/welcome.vue"),
name: "Welcome",
component: () => import("/@/views/welcome/index.vue"),
meta: {
title: $t("menus.hshome")
}

View File

@@ -1,10 +1,11 @@
import { $t } from "/@/plugins/i18n";
import type { RouteConfigsTable } from "/#/index";
const Layout = () => import("/@/layout/index.vue");
const remainingRouter = [
const remainingRouter: Array<RouteConfigsTable> = [
{
path: "/login",
name: "login",
name: "Login",
component: () => import("/@/views/login/index.vue"),
meta: {
title: $t("menus.hslogin"),
@@ -24,7 +25,7 @@ const remainingRouter = [
children: [
{
path: "/redirect/:path(.*)",
name: "redirect",
name: "Redirect",
component: () => import("/@/layout/redirect.vue")
}
]

View File

@@ -3,7 +3,6 @@ import { RouteLocationNormalized } from "vue-router";
export interface toRouteType extends RouteLocationNormalized {
meta: {
keepAlive?: boolean;
refreshRedirect: string;
dynamicLevel?: string;
};
}

View File

@@ -7,12 +7,13 @@ import {
RouteRecordNormalized
} from "vue-router";
import { router } from "./index";
import { isProxy, toRaw } from "vue";
import { loadEnv } from "../../build";
import { cloneDeep } from "lodash-unified";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { buildHierarchyTree } from "/@/utils/tree";
import { buildHierarchyTree } from "@pureadmin/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const Layout = () => import("/@/layout/index.vue");
const IFrame = () => import("/@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
@@ -25,7 +26,7 @@ function ascending(arr: any[]) {
arr.forEach(v => {
if (v?.meta?.rank === null) v.meta.rank = undefined;
if (v?.meta?.rank === 0) {
if (v.name !== "home" && v.path !== "/") {
if (v.name !== "Home" && v.path !== "/") {
console.warn("rank only the home page can be 0");
}
}
@@ -39,7 +40,7 @@ function ascending(arr: any[]) {
// 过滤meta中showLink为false的路由
function filterTree(data: RouteComponent[]) {
const newTree = data.filter(
const newTree = cloneDeep(data).filter(
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
);
newTree.forEach(
@@ -86,7 +87,7 @@ function getParentPaths(path: string, routes: RouteRecordRaw[]) {
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
return res;
return isProxy(res) ? toRaw(res) : res;
} else {
for (let i = 0; i < routes.length; i++) {
if (
@@ -95,7 +96,7 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
) {
res = findRouteByPath(path, routes[i].children);
if (res) {
return res;
return isProxy(res) ? toRaw(res) : res;
}
}
}
@@ -103,14 +104,14 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
}
}
// 重置路由
function resetRouter(): void {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
function addPathMatch() {
if (!router.hasRoute("pathMatch")) {
router.addRoute({
path: "/:pathMatch(.*)",
name: "pathMatch",
redirect: "/error/404"
});
}
}
// 初始化路由
@@ -135,7 +136,7 @@ function initRouter(name: string) {
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
const flattenRouters = router
const flattenRouters: any = router
.getRoutes()
.find(n => n.path === "/");
router.addRoute(flattenRouters);
@@ -145,10 +146,7 @@ function initRouter(name: string) {
);
usePermissionStoreHook().changeSetting(info);
}
router.addRoute({
path: "/:pathMatch(.*)",
redirect: "/error/404"
});
addPathMatch();
});
});
}
@@ -229,18 +227,18 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
if (!arrRoutes || !arrRoutes.length) return;
const modulesRoutesKeys = Object.keys(modulesRoutes);
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else if (v.meta?.frameSrc) {
v.component = IFrame;
} else {
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会根path保持一致
const index = v?.component
? // @ts-expect-error
modulesRoutesKeys.findIndex(ev => ev.includes(v.component))
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];
}
// 将backstage属性加入meta标识此路由为后端返回路由
v.meta.backstage = true;
// 父级的redirect属性取值如果子级存在且父级的redirect属性不存在默认取第一个子级的path如果子级存在且父级的redirect属性存在取存在的redirect属性会覆盖默认值
if (v?.children && !v.redirect) v.redirect = v.children[0].path;
// 父级的name属性取值如果子级存在且父级的name属性不存在默认取第一个子级的name如果子级存在且父级的name属性存在取存在的name属性会覆盖默认值
if (v?.children && !v.name) v.name = v.children[0].name;
if (v.meta?.frameSrc) v.component = IFrame;
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会跟path保持一致
const index = v?.component
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];
if (v.children) {
addAsyncRoutes(v.children);
}
@@ -295,7 +293,6 @@ export {
ascending,
filterTree,
initRouter,
resetRouter,
hasPermissions,
getHistoryMode,
addAsyncRoutes,

View File

@@ -2,22 +2,23 @@ import { store } from "/@/store";
import { appType } from "./types";
import { defineStore } from "pinia";
import { getConfig } from "/@/config";
import { storageLocal } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection";
import type { StorageConfigs } from "/#/index";
import { deviceDetection, storageLocal } from "@pureadmin/utils";
export const useAppStore = defineStore({
id: "pure-app",
state: (): appType => ({
sidebar: {
opened:
storageLocal.getItem("responsive-layout")?.sidebarStatus ??
getConfig().SidebarStatus,
storageLocal.getItem<StorageConfigs>("responsive-layout")
?.sidebarStatus ?? getConfig().SidebarStatus,
withoutAnimation: false,
isClickHamburger: false
isClickCollapse: false
},
// 这里的layout用于监听容器拖拉后恢复对应的导航模式
layout:
storageLocal.getItem("responsive-layout")?.layout ?? getConfig().Layout,
storageLocal.getItem<StorageConfigs>("responsive-layout")?.layout ??
getConfig().Layout,
device: deviceDetection() ? "mobile" : "desktop"
}),
getters: {
@@ -30,7 +31,7 @@ export const useAppStore = defineStore({
},
actions: {
TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
const layout = storageLocal.getItem("responsive-layout");
const layout = storageLocal.getItem<StorageConfigs>("responsive-layout");
if (opened && resize) {
this.sidebar.withoutAnimation = true;
this.sidebar.opened = true;
@@ -42,19 +43,16 @@ export const useAppStore = defineStore({
} else if (!opened && !resize) {
this.sidebar.withoutAnimation = false;
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.isClickHamburger = !this.sidebar.opened;
this.sidebar.isClickCollapse = !this.sidebar.opened;
layout.sidebarStatus = this.sidebar.opened;
}
storageLocal.setItem("responsive-layout", layout);
},
TOGGLE_DEVICE(device: string) {
this.device = device;
},
async toggleSideBar(opened?: boolean, resize?: string) {
await this.TOGGLE_SIDEBAR(opened, resize);
},
toggleDevice(device) {
this.TOGGLE_DEVICE(device);
toggleDevice(device: string) {
this.device = device;
},
setLayout(layout) {
this.layout = layout;

View File

@@ -1,16 +1,18 @@
import { store } from "/@/store";
import { defineStore } from "pinia";
import { getConfig } from "/@/config";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { storageLocal } from "@pureadmin/utils";
export const useEpThemeStore = defineStore({
id: "pure-epTheme",
state: () => ({
epThemeColor:
storageLocal.getItem("responsive-layout")?.epThemeColor ??
storageLocal.getItem<StorageConfigs>("responsive-layout")?.epThemeColor ??
getConfig().EpThemeColor,
epTheme:
storageLocal.getItem("responsive-layout")?.theme ?? getConfig().Theme
storageLocal.getItem<StorageConfigs>("responsive-layout")?.theme ??
getConfig().Theme
}),
getters: {
getEpThemeColor() {
@@ -28,8 +30,8 @@ export const useEpThemeStore = defineStore({
}
},
actions: {
setEpThemeColor(newColor) {
const layout = storageLocal.getItem("responsive-layout");
setEpThemeColor(newColor: string): void {
const layout = storageLocal.getItem<StorageConfigs>("responsive-layout");
this.epTheme = layout?.theme;
this.epThemeColor = newColor;
layout.epThemeColor = newColor;

View File

@@ -1,27 +1,21 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { isUrl } from "/@/utils/is";
import { isEqual } from "lodash-unified";
import { storageLocal } from "/@/utils/storage";
import type { StorageConfigs } from "/#/index";
import { routerArrays } from "/@/layout/types";
import { multiType, positionType } from "./types";
import { isUrl, storageLocal } from "@pureadmin/utils";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal.getItem("responsive-configure").multiTagsCache
? storageLocal.getItem("responsive-tags")
: [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
],
multiTagsCache: storageLocal.getItem("responsive-configure").multiTagsCache
multiTags: storageLocal.getItem<StorageConfigs>("responsive-configure")
.multiTagsCache
? storageLocal.getItem<StorageConfigs>("responsive-tags")
: [...routerArrays],
multiTagsCache: storageLocal.getItem<StorageConfigs>("responsive-configure")
.multiTagsCache
}),
getters: {
getMultiTagsCache() {
@@ -54,8 +48,13 @@ export const useMultiTagsStore = defineStore({
case "push":
{
const tagVal = value as multiType;
// 不添加到标签页
if (tagVal?.meta?.hiddenTag) return;
// 如果是外链无需添加信息到标签页
if (isUrl(tagVal?.name)) return;
const tagPath = tagVal?.path;
// 如果title为空拒绝添加空信息到标签页
if (tagVal?.meta?.title.length === 0) return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagPath;
@@ -63,20 +62,24 @@ export const useMultiTagsStore = defineStore({
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag.query, tagVal?.query);
return isEqual(tag?.query, tagVal?.query);
});
if (tagHasExits && tagQueryHasExits) return;
// 判断tag中的params键值是否相等
const tagParamsHasExits = this.multiTags.some(tag => {
return isEqual(tag?.params, tagVal?.params);
});
if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
// 动态路由可打开的最大数量
const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;
if (dynamicLevel > 0) {
// dynamicLevel动态路由可打开的数量
// 获取到已经打开的动态路由数, 判断是否大于dynamicLevel
if (
this.multiTags.filter(e => e?.path === tagPath).length >=
dynamicLevel
) {
// 关闭第一个
// 如果当前已打开的动态路由数大于dynamicLevel替换第一个动态路由标签
const index = this.multiTags.findIndex(
item => item?.path === tagPath
);

View File

@@ -14,8 +14,8 @@ export type appType = {
sidebar: {
opened: boolean;
withoutAnimation: boolean;
// 判断是否手动点击Hamburger
isClickHamburger: boolean;
// 判断是否手动点击Collapse
isClickCollapse: boolean;
};
layout: string;
device: string;
@@ -27,6 +27,7 @@ export type multiType = {
name: string;
meta: any;
query?: object;
params?: object;
};
export type setType = {

View File

@@ -2,7 +2,8 @@ import { defineStore } from "pinia";
import { store } from "/@/store";
import { userType } from "./types";
import { router } from "/@/router";
import { storageSession } from "/@/utils/storage";
import { routerArrays } from "/@/layout/types";
import { storageSession } from "@pureadmin/utils";
import { getLogin, refreshToken } from "/@/api/user";
import { getToken, setToken, removeToken } from "/@/utils/auth";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
@@ -25,7 +26,7 @@ export const useUserStore = defineStore({
name,
// 前端生成的验证码(按实际需求替换)
verifyCode: "",
// 登显示组件判断 0 1手机登 2二维码登 3注册 4忘记密码默认0
// 登显示组件判断 0 1手机登 2二维码登 3注册 4忘记密码默认0
currentPage: 0
}),
actions: {
@@ -62,16 +63,7 @@ export const useUserStore = defineStore({
this.name = "";
removeToken();
storageSession.clear();
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
]);
useMultiTagsStoreHook().handleTags("equal", routerArrays);
router.push("/login");
},
// 刷新token

View File

@@ -1,28 +1,163 @@
/* 暗黑模式 */
[data-theme="dark"] {
filter: invert(0.9) hue-rotate(180deg);
@import "element-plus/theme-chalk/src/dark/css-vars.scss";
img,
.icon-svg,
.login-container {
filter: invert(1) hue-rotate(180deg);
/* 暗黑模式适配 */
html.dark {
/* 自定义深色背景颜色 */
// --el-bg-color: #020409;
$border-style: #303030;
$color-white: #fff;
.navbar,
.tags-view,
.contextmenu,
.sidebar-container,
.horizontal-header,
.sidebar-logo-container,
.horizontal-header .el-sub-menu__title,
.horizontal-header .submenu-title-noDropdown {
background: var(--el-bg-color) !important;
}
// element plus
.el-radio-button__original-radio:checked + .el-radio-button__inner,
.el-image-viewer__close,
.el-image-viewer__actions__inner,
.el-image-viewer__next,
.el-image-viewer__prev {
color: #000 !important;
.app-main {
background: #020409 !important;
}
.el-overlay {
background-color: rgb(0 0 0 / 5%) !important;
.frame {
filter: invert(0.9) hue-rotate(180deg);
}
.el-drawer {
box-shadow: 0 8px 10px -5px rgb(0 0 0 / 1%), 0 16px 24px 2px rgb(0 0 0 / 2%),
0 6px 30px 5px rgb(0 0 0 / 1%);
.ant-tabs {
background: var(--el-bg-color);
color: $color-white;
}
/* 标签页 */
.tags-view {
.arrow-left,
.arrow-right {
box-shadow: none;
}
.arrow-right {
border-left: 1px solid $border-style;
}
.arrow-left,
.arrow-right,
.right-button li {
border-right: 1px solid $border-style;
}
}
/* vxe-table */
.vxe-table--header-wrapper,
.vxe-table--body-wrapper {
color: var(--el-text-color-primary);
background: var(--el-bg-color) !important;
}
.vxe-table--render-default.border--full .vxe-header--column,
.vxe-table--render-default.border--full .vxe-body--column,
.vxe-table--render-default.border--full .vxe-footer--column {
background-image: linear-gradient(
var(--el-border-color-lighter),
var(--el-border-color-lighter)
),
linear-gradient(
var(--el-border-color-lighter),
var(--el-border-color-lighter)
);
}
/* 表头 */
.vxe-table--header-wrapper {
background: #262727 !important;
}
.vxe-table--render-wrapper,
.vxe-table--main-wrapper {
border: none;
}
.vxe-pager.is--perfect,
.vxe-table--render-default .vxe-table--border-line {
border: 1px solid var(--el-border-color-lighter);
}
.vxe-table--header-border-line {
border-bottom: 1px solid var(--el-border-color-lighter) !important;
}
.vxe-body--row.row--hover,
.vxe-pager {
background-color: #262727;
}
.vxe-input--inner,
.vxe-pager .vxe-pager--jump-prev,
.vxe-pager .vxe-pager--prev-btn,
.vxe-pager .vxe-pager--next-btn,
.vxe-pager .vxe-pager--jump-next,
.vxe-pager .vxe-pager--num-btn,
.vxe-pager .vxe-pager--jump .vxe-pager--goto {
background-color: transparent;
color: var(--el-text-color-primary);
// outline: none !important;
}
.vxe-select-option--wrapper {
background: var(--el-bg-color) !important;
}
.vxe-select-option:not(.is--disabled).is--hover {
background: var(--el-color-primary-light-6) !important;
}
.vxe-modal--wrapper.type--modal .vxe-modal--box,
.vxe-modal--wrapper.type--alert .vxe-modal--box,
.vxe-modal--wrapper.type--confirm .vxe-modal--box,
.vxe-form {
background: var(--el-bg-color) !important;
}
.vxe-modal--box,
.vxe-modal--header {
border: none;
background: var(--el-bg-color) !important;
}
.vxe-modal--title,
.vxe-button--content {
color: var(--el-text-color-primary);
}
.vxe-button.type--button.size--medium:hover {
background: var(--el-color-primary) !important;
}
/* 项目配置面板 */
.right-panel-items {
.el-divider__text {
--el-bg-color: var(--el-bg-color);
}
.el-divider--horizontal {
border-top: none;
}
}
/* element-plus */
.el-table__cell {
background: var(--el-bg-color);
}
.el-card {
--el-card-bg-color: var(--el-bg-color);
// border: none !important;
}
.el-backtop {
--el-backtop-bg-color: var(--el-color-primary-light-9);
--el-backtop-hover-bg-color: var(--el-color-primary);
}
.el-dropdown-menu__item:not(.is-disabled):hover {
background: transparent;
}
}

View File

@@ -25,7 +25,7 @@
}
.el-dropdown-menu {
padding: 2px 0 !important;
padding: 0 !important;
}
.el-range-separator {
@@ -46,13 +46,6 @@
padding: 0 !important;
}
/* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */
.el-button--primary,
.el-button--primary.is-plain {
--el-button-active-bg-color: var(--el-color-primary) !important;
--el-button-active-border-color: var(--el-color-primary) !important;
}
/* nprogress适配ep的primary */
#nprogress {
& .bar {

View File

@@ -4,6 +4,10 @@
@import "./sidebar.scss";
@import "./dark.scss";
:root {
--pure-transition-duration: 0.016s;
}
body {
width: 100%;
height: 100%;
@@ -47,3 +51,8 @@ html {
.mobile-spacing {
margin: 0;
}
/* 重置vxe-table中pager样式 */
.vxe-pager .vxe-pager--num-btn:not(.is--disabled).is--active {
color: #fff !important;
}

View File

@@ -6,6 +6,12 @@
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
@@ -20,9 +26,3 @@
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}

View File

@@ -16,17 +16,18 @@
.sub-menu-icon {
vertical-align: middle;
margin-right: 5px;
font-size: 18px;
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 5px;
}
.main-container {
height: 100vh;
min-height: 100%;
transition: margin-left 0.1s;
/* main-content属性动画 */
transition: margin-left var(--pure-transition-duration);
margin-left: $sideBarWidth;
position: relative;
background: #f0f2f5;
@@ -43,7 +44,8 @@
right: 0;
z-index: 998;
width: calc(100% - 210px);
transition: width 0.1s;
/* fixed-header属性左上角动画 */
transition: width var(--pure-transition-duration);
}
.main-hidden {
@@ -63,7 +65,8 @@
}
.sidebar-container {
transition: width 0.1s;
/* 展开动画 */
transition: width var(--pure-transition-duration);
width: $sideBarWidth !important;
background: $menuBg;
height: 100%;
@@ -80,22 +83,21 @@
overflow-x: hidden !important;
}
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
0s padding-right ease-in-out;
}
.el-scrollbar__bar.is-vertical {
right: 0;
}
.el-scrollbar {
height: 100%;
height: calc(100% - 44px);
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
.el-scrollbar.pc {
/* logo: 48px、leftCollapse: 40px、leftCollapse-shadow: 4px */
height: calc(100% - 92px);
}
.el-scrollbar.mobile {
height: 100%;
}
}
@@ -121,7 +123,7 @@
.el-sub-menu__title {
height: 50px;
color: $menuText;
padding: 0 20px 0 40px;
background-color: transparent !important;
&:hover {
color: $menuTitleHover !important;
@@ -164,7 +166,7 @@
/* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most {
background: $subMenuActiveBg;
background: $subMenuActiveBg !important;
}
/* 有子集的激活菜单背景 */
@@ -189,7 +191,7 @@
align-items: center;
padding-left: 10px;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.125s ease;
i {
font-size: 30px;
@@ -219,54 +221,46 @@
color: $subMenuActiveText;
justify-content: flex-end;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 全屏 */
.screen-full,
/* 国际化 */
.globalization,
/* 登陆名 */
.el-dropdown-link,
/* 设置 */
.el-icon-setting {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
}
.search-container {
&:hover {
background: $menuHover;
}
}
.screen-full {
cursor: pointer;
&:hover {
background: $menuHover;
}
}
.globalization {
height: 48px;
width: 40px;
height: 48px;
padding: 11px;
outline: none;
cursor: pointer;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
}
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-around;
cursor: pointer;
color: $subMenuActiveText;
&:hover {
background: $menuHover;
}
p {
font-size: 14px;
}
@@ -285,18 +279,14 @@
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: $menuHover;
}
}
}
.el-menu {
border: none;
height: 100%;
background-color: transparent;
width: 100% !important;
background-color: transparent;
}
.el-menu-item,
@@ -322,7 +312,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
border-bottom-color: #409eff;
i {
color: $subMenuActiveText !important;
@@ -332,7 +321,6 @@
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
border-bottom-color: #409eff;
}
}
@@ -344,15 +332,6 @@
.el-menu-item {
span {
font-size: 12px;
margin-left: 10px;
}
}
.el-sub-menu__title {
color: $menuText;
span {
margin-left: 10px;
}
}
}
@@ -402,15 +381,19 @@
.el-menu-item,
.el-sub-menu {
i {
width: 20px;
text-align: center;
font-size: 16px;
}
// i {
// width: 20px;
// text-align: center;
// font-size: 16px;
// }
i.fa {
margin-right: 5px;
font-size: 16px;
// i.fa {
// margin-right: 5px;
// font-size: 16px;
// }
.el-menu-tooltip__trigger {
width: 54px;
padding: 18px !important;
}
}
}
@@ -431,16 +414,11 @@
span {
font-size: 12px;
margin-left: 10px;
}
}
.el-sub-menu__title {
color: $menuText;
span {
margin-left: 10px;
}
}
}
@@ -495,7 +473,9 @@
/* 有子菜单 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before {
> .el-sub-menu__title::before,
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
top: 0;
left: 2px;
@@ -504,20 +484,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.2s ease-in-out;
transform: translateY(0);
}
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.2s ease-in-out;
transition: all 0.125s ease-in-out;
transform: translateY(0);
}
@@ -536,7 +503,7 @@
.mobile {
.fixed-header {
width: 100% !important;
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.main-container {
@@ -544,7 +511,7 @@
}
.sidebar-container {
transition: transform 0.1s;
transition: transform var(--pure-transition-duration);
width: $sideBarWidth;
}
@@ -556,14 +523,6 @@
}
}
}
/* vertical菜单下hideSidebar去除动画 */
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
body[layout="vertical"] {
@@ -581,26 +540,22 @@ body[layout="vertical"] {
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
}
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
@@ -619,20 +574,31 @@ body[layout="vertical"] {
background: transparent !important;
}
/* 有无子菜单 */
.el-sub-menu__title,
.el-menu-item [class^="el-icon"] {
right: 2px;
.el-sub-menu__title {
padding: 0 18px !important;
}
}
.el-menu-tooltip__trigger {
padding: 0 18px;
}
.sub-menu-icon {
margin-right: 0;
}
}
.sub-menu-icon {
width: 24px;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 全屏 */
.screen-full,
/* 国际化 */
.globalization,
/* 登陆名 */
.el-dropdown-link,
/* 设置 */
.el-icon-setting {
&:hover {
background: #f6f6f6;
}
}
}
@@ -650,6 +616,10 @@ body[layout="mix"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu--collapse {
width: 54px;
}
.el-menu {
--el-menu-hover-bg-color: transparent !important;
}
@@ -657,26 +627,22 @@ body[layout="mix"] {
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.1s;
transition: width var(--pure-transition-duration);
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
}
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
@@ -690,19 +656,6 @@ body[layout="mix"] {
}
}
}
/* 无子菜单 */
.el-menu-item [class^="el-icon"] {
right: 5px;
}
.el-sub-menu__title [class^="el-icon"] {
right: 2px;
}
.submenu-title-noDropdown {
background: transparent !important;
}
}
}
}

View File

@@ -40,3 +40,16 @@
.breadcrumb-leave-active {
position: absolute;
}
/**
* @description 重置el-menu的展开收起动画时长
* @see {@link https://github.com/element-plus/element-plus/issues/4509#issuecomment-980165001}
*/
.outer-most .el-collapse-transition-leave-active,
.outer-most .el-collapse-transition-enter-active {
transition: 0.12s all ease-in-out !important;
}
.horizontal-collapse-transition {
transition: var(--pure-transition-duration) all !important;
}

5
src/utils/README.md Normal file
View File

@@ -0,0 +1,5 @@
### 注意
- [文档](https://pure-admin-utils-docs.vercel.app/)
- [npm](https://www.npmjs.com/package/@pureadmin/utils)
- vue-pure-admin 从 3.3.0 版本之后(不包括 3.3.0 版本),大部分工具和 hooks 都集成到了[@pureadmin/utils](https://pure-admin-utils-docs.vercel.app/)

View File

@@ -1,39 +0,0 @@
import { unref } from "vue";
import type { Ref } from "vue";
type FunctionArgs<Args extends any[] = any[], Return = void> = (
...args: Args
) => Return;
type MaybeRef<T> = T | Ref<T>;
// 延迟函数
export const delay = (timeout: number) =>
new Promise(resolve => setTimeout(resolve, timeout));
/**
* 防抖函数
* @param fn 函数
* @param timeout 延迟时间
* @param immediate 是否立即执行
* @returns
*/
export const debounce = <T extends FunctionArgs>(
fn: T,
timeout: MaybeRef<number> = 200,
immediate = false
) => {
let timmer: TimeoutHandle;
const wait = unref(timeout);
return () => {
timmer && clearTimeout(timmer);
if (immediate) {
if (!timmer) {
fn();
}
timmer = setTimeout(() => (timmer = null), wait);
} else {
timmer = setTimeout(fn, wait);
}
};
};

View File

@@ -1,37 +0,0 @@
interface deviceInter {
match: Fn;
}
interface BrowserInter {
browser: string;
version: string;
}
// 检测设备类型(手机返回true,反之)
export const deviceDetection = () => {
const sUserAgent: deviceInter = navigator.userAgent.toLowerCase();
// const bIsIpad = sUserAgent.match(/ipad/i) == "ipad";
const bIsIphoneOs = sUserAgent.match(/iphone os/i) == "iphone os";
const bIsMidp = sUserAgent.match(/midp/i) == "midp";
const bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4";
const bIsUc = sUserAgent.match(/ucweb/i) == "ucweb";
const bIsAndroid = sUserAgent.match(/android/i) == "android";
const bIsCE = sUserAgent.match(/windows ce/i) == "windows ce";
const bIsWM = sUserAgent.match(/windows mobile/i) == "windows mobile";
return (
bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM
);
};
// 获取浏览器型号以及版本
export const getBrowserInfo = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys: BrowserInter = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};
return Sys;
};

View File

@@ -1,118 +0,0 @@
const toString = Object.prototype.toString;
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== "undefined";
}
export function isUnDef<T = unknown>(val?: T): val is T {
return !isDef(val);
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, "Object");
}
export function isEmpty<T = unknown>(val: T): val is T {
if (isArray(val) || isString(val)) {
return val.length === 0;
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0;
}
if (isObject(val)) {
return Object.keys(val).length === 0;
}
return false;
}
export function isDate(val: unknown): val is Date {
return is(val, "Date");
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
export function isNumber(val: unknown): val is number {
return is(val, "Number");
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return (
is(val, "Promise") &&
isObject(val) &&
isFunction(val.then) &&
isFunction(val.catch)
);
}
export function isString(val: unknown): val is string {
return is(val, "String");
}
export function isFunction(val: unknown): val is Function {
return typeof val === "function";
}
export function isBoolean(val: unknown): val is boolean {
return is(val, "Boolean");
}
export function isRegExp(val: unknown): val is RegExp {
return is(val, "RegExp");
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
export function isWindow(val: any): val is Window {
return typeof window !== "undefined" && is(val, "Window");
}
export function isElement(val: unknown): val is Element {
return isObject(val) && !!val.tagName;
}
export const isServer = typeof window === "undefined";
export const isClient = !isServer;
/** url链接正则 */
export function isUrl<T>(value: T): boolean {
const reg =
// eslint-disable-next-line no-useless-escape
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
// @ts-expect-error
return reg.test(value);
}
/** 手机号码正则 */
export function isPhone<T>(value: T): boolean {
const reg =
/^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/;
// @ts-expect-error
return reg.test(value);
}
/** 邮箱正则 */
export function isEmail<T>(value: T): boolean {
const reg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
// @ts-expect-error
return reg.test(value);
}

View File

@@ -1,13 +0,0 @@
export const openLink = <T>(link: T): void => {
const $a: HTMLElement = document.createElement("a");
// @ts-expect-error
$a.setAttribute("href", link);
$a.setAttribute("target", "_blank");
$a.setAttribute("rel", "noreferrer noopener");
$a.setAttribute("id", "external");
document.getElementById("external") &&
document.body.removeChild(document.getElementById("external"));
document.body.appendChild($a);
$a.click();
$a.remove();
};

View File

@@ -1,54 +0,0 @@
interface ProxyLoader {
loadCss(src: string): any;
loadScript(src: string): Promise<any>;
loadScriptConcurrent(src: Array<string>): Promise<any>;
}
class loaderProxy implements ProxyLoader {
constructor() {}
protected scriptLoaderCache: Array<string> = [];
public loadCss = (src: string): any => {
const element: HTMLLinkElement = document.createElement("link");
element.rel = "stylesheet";
element.href = src;
document.body.appendChild(element);
};
public loadScript = async (src: string): Promise<any> => {
if (this.scriptLoaderCache.includes(src)) {
return src;
} else {
const element: HTMLScriptElement = document.createElement("script");
element.src = src;
document.body.appendChild(element);
element.onload = () => {
return this.scriptLoaderCache.push(src);
};
}
};
public loadScriptConcurrent = async (
srcList: Array<string>
): Promise<any> => {
if (Array.isArray(srcList)) {
const len: number = srcList.length;
if (len > 0) {
let count = 0;
srcList.map(src => {
if (src) {
this.loadScript(src).then(() => {
count++;
if (count === len) {
return;
}
});
}
});
}
}
};
}
export const loader = new loaderProxy();

View File

@@ -1,38 +0,0 @@
import { ElMessage } from "element-plus";
// 消息
const Message = (message: string): any => {
return ElMessage({
showClose: true,
message
});
};
// 成功
const successMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "success"
});
};
// 警告
const warnMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "warning"
});
};
// 失败
const errorMessage = (message: string): any => {
return ElMessage({
showClose: true,
message,
type: "error"
});
};
export { Message, successMessage, warnMessage, errorMessage };

View File

@@ -11,7 +11,7 @@ type Events = {
openPanel: string;
tagViewsChange: string;
tagViewsShowModel: string;
logoChange: string;
logoChange: boolean;
changLayoutRoute: {
indexPath: string;
parentPath: string;

View File

@@ -1,57 +0,0 @@
import type { FunctionArgs } from "@vueuse/core";
export const hasClass = (ele: RefType<any>, cls: string): any => {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
};
export const addClass = (
ele: RefType<any>,
cls: string,
extracls?: string
): any => {
if (!hasClass(ele, cls)) ele.className += " " + cls;
if (extracls) {
if (!hasClass(ele, extracls)) ele.className += " " + extracls;
}
};
export const removeClass = (
ele: RefType<any>,
cls: string,
extracls?: string
): any => {
if (hasClass(ele, cls)) {
const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
ele.className = ele.className.replace(reg, " ").trim();
}
if (extracls) {
if (hasClass(ele, extracls)) {
const regs = new RegExp("(\\s|^)" + extracls + "(\\s|$)");
ele.className = ele.className.replace(regs, " ").trim();
}
}
};
export const toggleClass = (
flag: boolean,
clsName: string,
target?: RefType<any>
): any => {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "");
targetEl.className = flag ? `${className} ${clsName} ` : className;
};
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false;
// @ts-ignore
return function (...args) {
if (locked) return;
locked = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
locked = false;
});
};
}

View File

@@ -98,8 +98,7 @@ Print.prototype = {
const child = selects[k3].children;
for (const i in child) {
if (child[i].tagName == "OPTION") {
// @ts-ignore
if (child[i].selected == true) {
if ((child[i] as any).selected == true) {
child[i].setAttribute("selected", "selected");
} else {
child[i].removeAttribute("selected");

View File

@@ -1,35 +0,0 @@
import ResizeObserver from "resize-observer-polyfill";
const isServer = typeof window === "undefined";
const resizeHandler = (entries: any[]): void => {
for (const entry of entries) {
const listeners = entry.target.__resizeListeners__ || [];
if (listeners.length) {
listeners.forEach((fn: () => any) => {
fn();
});
}
}
};
export const addResizeListener = (element: any, fn: () => any): any => {
if (isServer) return;
if (!element.__resizeListeners__) {
element.__resizeListeners__ = [];
element.__ro__ = new ResizeObserver(resizeHandler);
element.__ro__.observe(element);
}
element.__resizeListeners__.push(fn);
};
export const removeResizeListener = (element: any, fn: () => any): any => {
if (!element || !element.__resizeListeners__) return;
element.__resizeListeners__.splice(
element.__resizeListeners__.indexOf(fn),
1
);
if (!element.__resizeListeners__.length) {
element.__ro__.disconnect();
}
};

41
src/utils/responsive.ts Normal file
View File

@@ -0,0 +1,41 @@
// 响应式storage
import { App } from "vue";
import Storage from "responsive-storage";
import { routerArrays } from "/@/layout/types";
const nameSpace = "responsive-";
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
const configObj = Object.assign(
{
// 国际化 默认中文zh
locale: Storage.getData("locale", nameSpace) ?? {
locale: config.Locale ?? "zh"
},
// layout模式以及主题
layout: Storage.getData("layout", nameSpace) ?? {
layout: config.Layout ?? "vertical",
theme: config.Theme ?? "default",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "#409EFF"
},
configure: Storage.getData("configure", nameSpace) ?? {
grey: config.Grey ?? false,
weak: config.Weak ?? false,
hideTabs: config.HideTabs ?? false,
showLogo: config.ShowLogo ?? true,
showModel: config.ShowModel ?? "smart",
multiTagsCache: config.MultiTagsCache ?? false
}
},
config.MultiTagsCache
? {
// 默认显示首页tag
tags: Storage.getData("tags", nameSpace) ?? routerArrays
}
: {}
);
app.use(Storage, { nameSpace, memory: configObj });
};

View File

@@ -1,46 +0,0 @@
interface ProxyStorage {
getItem(key: string): any;
setItem(Key: string, value: string): void;
removeItem(key: string): void;
clear(): void;
}
//sessionStorage operate
class sessionStorageProxy implements ProxyStorage {
protected storage: ProxyStorage;
constructor(storageModel: ProxyStorage) {
this.storage = storageModel;
}
// 存
public setItem(key: string, value: any): void {
this.storage.setItem(key, JSON.stringify(value));
}
// 取
public getItem(key: string): any {
return JSON.parse(this.storage.getItem(key));
}
// 删
public removeItem(key: string): void {
this.storage.removeItem(key);
}
// 清空
public clear(): void {
this.storage.clear();
}
}
//localStorage operate
class localStorageProxy extends sessionStorageProxy implements ProxyStorage {
constructor(localStorage: ProxyStorage) {
super(localStorage);
}
}
export const storageSession = new sessionStorageProxy(sessionStorage);
export const storageLocal = new localStorageProxy(localStorage);

View File

@@ -1,59 +0,0 @@
// 响应式storage
import { App } from "vue";
import Storage from "responsive-storage";
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
const configObj = Object.assign(
{
// 国际化 默认中文zh
locale: {
type: Object,
default: Storage.getData(undefined, "locale") ?? {
locale: config.Locale ?? "zh"
}
},
// layout模式以及主题
layout: {
type: Object,
default: Storage.getData(undefined, "layout") ?? {
layout: config.Layout ?? "vertical",
theme: config.Theme ?? "default",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "#409EFF"
}
},
configure: {
type: Object,
default: Storage.getData(undefined, "configure") ?? {
grey: config.Grey ?? false,
weak: config.Weak ?? false,
hideTabs: config.HideTabs ?? false,
showLogo: config.ShowLogo ?? true,
showModel: config.ShowModel ?? "smart",
multiTagsCache: config.MultiTagsCache ?? false
}
}
},
config.MultiTagsCache
? {
// 默认显示首页tag
tags: {
type: Array,
default: Storage.getData(undefined, "tags") ?? [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled"
}
}
]
}
}
: {}
);
app.use(Storage, configObj);
};

View File

@@ -1,175 +0,0 @@
/**
* 提取菜单树中的每一项uniqueId
* @param {Array} {menuTree 菜单树}
* @param {return}} expandedPaths 每一项uniqueId组成的数组
*/
const expandedPaths = [];
export function extractPathList(menuTree) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const node of menuTree) {
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
extractPathList(node.children);
}
expandedPaths.push(node.uniqueId);
}
return expandedPaths;
}
/**
* 如果父级下children的length为1删除children并自动组建唯一uniqueId
* @param {Array} {menuTree 菜单树}
* @param {Array} {pathList 每一项的id组成的数组}
* @param {return}}
*/
export function deleteChildren(menuTree, pathList = []) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const [key, node] of menuTree.entries()) {
if (node.children && node.children.length === 1) delete node.children;
node.id = key;
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
node.pathList = [...pathList, node.id];
node.uniqueId =
node.pathList.length > 1 ? node.pathList.join("-") : node.pathList[0];
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
deleteChildren(node.children, node.pathList);
}
}
return menuTree;
}
// 创建层级关系
export function buildHierarchyTree(menuTree, pathList = []) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const [key, node] of menuTree.entries()) {
node.id = key;
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
node.pathList = [...pathList, node.id];
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
buildHierarchyTree(node.children, node.pathList);
}
}
return menuTree;
}
/**
* 广度优先遍历算法,找当前节点
* @param {Array} tree 原始树,数组
* @param {Number|String} uniqueId 唯一uniqueId
* @return {Object} node
*/
export function getNodeByUniqueId(menuTree, uniqueId) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
const item = menuTree.find(node => node.uniqueId === uniqueId);
if (item) return item;
const childrenList = menuTree
.filter(node => node.children)
.map(i => i.children)
.flat(1);
return getNodeByUniqueId(childrenList, uniqueId);
}
/**
* 向当前唯一uniqueId节点追加字段
* @param {Array} {menuTree 菜单树}
* @param {Number|String} uniqueId 唯一uniqueId
* @param {Object} fields 唯一uniqueId
* @return {menuTree} 追加字段后的树
*/
export function appendFieldByUniqueId(
menuTree: Array<any>,
uniqueId: Number | String,
fields: Object
) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return {};
for (const node of menuTree) {
const hasChildren = node.children && node.children.length > 0;
if (
node.uniqueId === uniqueId &&
Object.prototype.toString.call(fields) === "[object Object]"
)
Object.assign(node, fields);
if (hasChildren) {
appendFieldByUniqueId(node.children, uniqueId, fields);
}
}
return menuTree;
}
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export function handleTree(
data,
id?: string,
parentId?: string,
children?: string
) {
const config = {
id: id || "id",
parentId: parentId || "parentId",
childrenList: children || "children"
};
const childrenListMap = {};
const nodeIds = {};
const tree = [];
for (const d of data) {
const parentId = d[config.parentId];
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = [];
}
nodeIds[d[config.id]] = d;
childrenListMap[parentId].push(d);
}
for (const d of data) {
const parentId = d[config.parentId];
if (nodeIds[parentId] == null) {
tree.push(d);
}
}
for (const t of tree) {
adaptToChildrenList(t);
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]];
}
if (o[config.childrenList]) {
for (const c of o[config.childrenList]) {
adaptToChildrenList(c);
}
}
}
return tree;
}

View File

@@ -1,42 +0,0 @@
import { getCurrentInstance, reactive, shallowRef, watchEffect } from "vue";
import type { Ref } from "vue";
interface Params {
excludeListeners?: boolean;
excludeKeys?: string[];
}
const DEFAULT_EXCLUDE_KEYS = ["class", "style"];
const LISTENER_PREFIX = /^on[A-Z]/;
export function entries<T>(obj: Recordable<T>): [string, T][] {
return Object.keys(obj).map((key: string) => [key, obj[key]]);
}
export function useAttrs(params: Params = {}): Ref<Recordable> | {} {
const instance = getCurrentInstance();
if (!instance) return {};
const { excludeListeners = false, excludeKeys = [] } = params;
const attrs = shallowRef({});
const allExcludeKeys = excludeKeys.concat(DEFAULT_EXCLUDE_KEYS);
// Since attrs are not reactive, make it reactive instead of doing in `onUpdated` hook for better performance
instance.attrs = reactive(instance.attrs);
watchEffect(() => {
const res = entries(instance.attrs).reduce((acm, [key, val]) => {
if (
!allExcludeKeys.includes(key) &&
!(excludeListeners && LISTENER_PREFIX.test(key))
) {
acm[key] = val;
}
return acm;
}, {} as Recordable);
attrs.value = res;
});
return attrs;
}

View File

@@ -1,4 +0,0 @@
import { h, resolveComponent } from "vue";
export const dynamicComponent = (component: string) =>
h(resolveComponent(component));

View File

@@ -1,72 +0,0 @@
import { ref, watch } from "vue";
import { isDef } from "/@/utils/is";
interface Options {
target?: HTMLElement;
}
export function useCopyToClipboard(initial?: string) {
const clipboardRef = ref(initial || "");
const isSuccessRef = ref(false);
const copiedRef = ref(false);
watch(
clipboardRef,
(str?: string) => {
if (isDef(str)) {
copiedRef.value = true;
isSuccessRef.value = copyTextToClipboard(str);
}
},
{ immediate: !!initial, flush: "sync" }
);
return { clipboardRef, isSuccessRef, copiedRef };
}
export function copyTextToClipboard(
input: string,
{ target = document.body }: Options = {}
) {
const element = document.createElement("textarea");
const previouslyFocusedElement = document.activeElement;
element.value = input;
element.setAttribute("readonly", "");
(element.style as any).contain = "strict";
element.style.position = "absolute";
element.style.left = "-9999px";
element.style.fontSize = "12pt";
const selection = document.getSelection();
let originalRange;
if (selection && selection.rangeCount > 0) {
originalRange = selection.getRangeAt(0);
}
target.append(element);
element.select();
element.selectionStart = 0;
element.selectionEnd = input.length;
let isSuccess = false;
try {
isSuccess = document.execCommand("copy");
} catch (e) {
throw new Error(e);
}
element.remove();
if (originalRange && selection) {
selection.removeAllRanges();
selection.addRange(originalRange);
}
if (previouslyFocusedElement) {
(previouslyFocusedElement as HTMLElement).focus();
}
return isSuccess;
}

View File

@@ -1,28 +0,0 @@
const hexList: string[] = [];
for (let i = 0; i <= 15; i++) {
hexList[i] = i.toString(16);
}
export function buildUUID(): string {
let uuid = "";
for (let i = 1; i <= 36; i++) {
if (i === 9 || i === 14 || i === 19 || i === 24) {
uuid += "-";
} else if (i === 15) {
uuid += 4;
} else if (i === 20) {
uuid += hexList[(Math.random() * 4) | 8];
} else {
uuid += hexList[(Math.random() * 16) | 0];
}
}
return uuid.replace(/-/g, "");
}
let unique = 0;
export function buildShortUUID(prefix = ""): string {
const time = Date.now();
const random = Math.floor(Math.random() * 1000000000);
unique++;
return prefix + "_" + random + unique + String(time);
}

View File

@@ -1,116 +0,0 @@
import {
ref,
Ref,
unref,
shallowRef,
onBeforeUnmount,
getCurrentInstance
} from "vue";
import { isDef } from "/@/utils/is";
import { useRafThrottle } from "/@/utils/operate";
import { addResizeListener, removeResizeListener } from "/@/utils/resize";
const domSymbol = Symbol("watermark-dom");
type attr = {
font?: string;
fillStyle?: string;
};
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>
) {
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string, attr?: attr) {
const can = document.createElement("canvas");
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext("2d");
if (cans) {
cans.rotate((-20 * Math.PI) / 120);
cans.font = attr?.font ?? "15px Reggae One";
cans.fillStyle = attr?.fillStyle ?? "rgba(0, 0, 0, 0.15)";
cans.textAlign = "left";
cans.textBaseline = "middle";
cans.fillText(str, width / 20, height);
}
return can.toDataURL("image/png");
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
attr?: attr;
} = {}
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(
options.str,
options.attr
)}) left top repeat`;
}
}
const createWatermark = (str: string, attr?: attr) => {
if (unref(watermarkEl)) {
updateWatermark({ str, attr });
return id;
}
const div = document.createElement("div");
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = "none";
div.style.top = "0px";
div.style.left = "0px";
div.style.position = "absolute";
div.style.zIndex = "100000";
const el = unref(appendEl);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height, attr });
el.appendChild(div);
return id;
};
function setWatermark(str: string, attr?: attr) {
createWatermark(str, attr);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear };
}

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import noAccess from "/@/assets/status/403.svg?component";
defineOptions({
name: "403"
});
</script>
<template>
@@ -55,8 +59,9 @@ import noAccess from "/@/assets/status/403.svg?component";
delay: 500
}
}"
>返回首页</el-button
>
返回首页
</el-button>
</div>
</div>
</template>

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import noExist from "/@/assets/status/404.svg?component";
defineOptions({
name: "404"
});
</script>
<template>
@@ -55,8 +59,9 @@ import noExist from "/@/assets/status/404.svg?component";
delay: 500
}
}"
>返回首页</el-button
>
返回首页
</el-button>
</div>
</div>
</template>

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import noServer from "/@/assets/status/500.svg?component";
defineOptions({
name: "500"
});
</script>
<template>
@@ -21,7 +25,7 @@ import noServer from "/@/assets/status/500.svg?component";
}
}"
>
403
500
</p>
<p
class="mb-4 text-gray-500"
@@ -55,8 +59,9 @@ import noServer from "/@/assets/status/500.svg?component";
delay: 500
}
}"
>返回首页</el-button
>
返回首页
</el-button>
</div>
</div>
</template>

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { ref, reactive } from "vue";
import Motion from "../utils/motion";
import { phoneRules } from "../utils/rule";
import { message } from "@pureadmin/components";
import type { FormInstance } from "element-plus";
import { $t, transformI18n } from "/@/plugins/i18n";
import { useVerifyCode } from "../utils/verifyCode";
import { useUserStoreHook } from "/@/store/modules/user";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
const { t } = useI18n();
const loading = ref(false);
const ruleForm = reactive({
phone: "",
@@ -21,9 +24,9 @@ const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
// 模拟登请求,需根据实际开发进行修改
// 模拟登请求,需根据实际开发进行修改
setTimeout(() => {
message.success("登陆成功");
message.success(transformI18n($t("login.loginSuccess")));
loading.value = false;
}, 2000);
} else {
@@ -46,7 +49,7 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.phone"
placeholder="手机号码"
:placeholder="t('login.phone')"
:prefix-icon="useRenderIcon('iphone')"
/>
</el-form-item>
@@ -58,14 +61,21 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.verifyCode"
placeholder="短信验证码"
:placeholder="t('login.smsVerifyCode')"
:prefix-icon="
useRenderIcon('ri:shield-keyhole-line', { online: true })
"
/>
<el-button
:disabled="isDisabled"
class="ml-2"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
{{ text }}
{{
text.length > 0
? text + t("login.info")
: t("login.getVerifyCode")
}}
</el-button>
</div>
</el-form-item>
@@ -80,7 +90,7 @@ function onBack() {
:loading="loading"
@click="onLogin(ruleFormRef)"
>
登陆
{{ t("login.login") }}
</el-button>
</el-form-item>
</Motion>
@@ -88,7 +98,7 @@ function onBack() {
<Motion :delay="200">
<el-form-item>
<el-button class="w-full" size="default" @click="onBack">
返回
{{ t("login.back") }}
</el-button>
</el-form-item>
</Motion>

View File

@@ -1,14 +1,17 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Motion from "../utils/motion";
import ReQrcode from "/@/components/ReQrcode";
import { useUserStoreHook } from "/@/store/modules/user";
const { t } = useI18n();
</script>
<template>
<Motion class="-mt-2 -mb-2"> <ReQrcode text="模拟测试" /> </Motion>
<Motion class="-mt-2 -mb-2"> <ReQrcode :text="t('login.test')" /> </Motion>
<Motion :delay="100">
<el-divider>
<p class="text-gray-500 text-xs">扫码后点击"确认"即可完成登录</p>
<p class="text-gray-500 text-xs">{{ t("login.tip") }}</p>
</el-divider>
</Motion>
<Motion :delay="150">
@@ -16,7 +19,7 @@ import { useUserStoreHook } from "/@/store/modules/user";
class="w-full mt-4"
@click="useUserStoreHook().SET_CURRENTPAGE(0)"
>
返回
{{ t("login.back") }}
</el-button>
</Motion>
</template>

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { ref, reactive } from "vue";
import Motion from "../utils/motion";
import { updateRules } from "../utils/rule";
import { message } from "@pureadmin/components";
import type { FormInstance } from "element-plus";
import { useVerifyCode } from "../utils/verifyCode";
import { $t, transformI18n } from "/@/plugins/i18n";
import { useUserStoreHook } from "/@/store/modules/user";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
const { t } = useI18n();
const checked = ref(false);
const loading = ref(false);
const ruleForm = reactive({
@@ -23,9 +26,9 @@ const repeatPasswordRule = [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入确认密码"));
callback(new Error(transformI18n($t("login.passwordSureReg"))));
} else if (ruleForm.password !== value) {
callback(new Error("两次密码不一致!"));
callback(new Error(transformI18n($t("login.passwordDifferentReg"))));
} else {
callback();
}
@@ -42,12 +45,12 @@ const onUpdate = async (formEl: FormInstance | undefined) => {
if (checked.value) {
// 模拟请求,需根据实际开发进行修改
setTimeout(() => {
message.success("注册成功");
message.success(transformI18n($t("login.registerSuccess")));
loading.value = false;
}, 2000);
} else {
loading.value = false;
message.warning("请勾选隐私政策");
message.warning(transformI18n($t("login.tickPrivacy")));
}
} else {
loading.value = false;
@@ -71,13 +74,19 @@ function onBack() {
>
<Motion>
<el-form-item
:rules="[{ required: true, message: '请输入账号', trigger: 'blur' }]"
:rules="[
{
required: true,
message: transformI18n($t('login.usernameReg')),
trigger: 'blur'
}
]"
prop="username"
>
<el-input
clearable
v-model="ruleForm.username"
placeholder="账号"
:placeholder="t('login.username')"
:prefix-icon="useRenderIcon('user')"
/>
</el-form-item>
@@ -88,7 +97,7 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.phone"
placeholder="手机号码"
:placeholder="t('login.phone')"
:prefix-icon="useRenderIcon('iphone')"
/>
</el-form-item>
@@ -100,14 +109,21 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.verifyCode"
placeholder="短信验证码"
:placeholder="t('login.smsVerifyCode')"
:prefix-icon="
useRenderIcon('ri:shield-keyhole-line', { online: true })
"
/>
<el-button
:disabled="isDisabled"
class="ml-2"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
{{ text }}
{{
text.length > 0
? text + t("login.info")
: t("login.getVerifyCode")
}}
</el-button>
</div>
</el-form-item>
@@ -119,7 +135,7 @@ function onBack() {
clearable
show-password
v-model="ruleForm.password"
placeholder="密码"
:placeholder="t('login.password')"
:prefix-icon="useRenderIcon('lock')"
/>
</el-form-item>
@@ -131,7 +147,7 @@ function onBack() {
clearable
show-password
v-model="ruleForm.repeatPassword"
placeholder="确认密码"
:placeholder="t('login.sure')"
:prefix-icon="useRenderIcon('lock')"
/>
</el-form-item>
@@ -139,8 +155,12 @@ function onBack() {
<Motion :delay="300">
<el-form-item>
<el-checkbox v-model="checked"> 我已仔细阅读并接受 </el-checkbox>
<el-button type="text"> 隐私政策 </el-button>
<el-checkbox v-model="checked">
{{ t("login.readAccept") }}
</el-checkbox>
<el-button link type="primary">
{{ t("login.privacyPolicy") }}
</el-button>
</el-form-item>
</Motion>
@@ -153,7 +173,7 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
确定
{{ t("login.definite") }}
</el-button>
</el-form-item>
</Motion>
@@ -161,7 +181,7 @@ function onBack() {
<Motion :delay="400">
<el-form-item>
<el-button class="w-full" size="default" @click="onBack">
返回
{{ t("login.back") }}
</el-button>
</el-form-item>
</Motion>

View File

@@ -1,13 +1,16 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { ref, reactive } from "vue";
import Motion from "../utils/motion";
import { updateRules } from "../utils/rule";
import { message } from "@pureadmin/components";
import type { FormInstance } from "element-plus";
import { useVerifyCode } from "../utils/verifyCode";
import { $t, transformI18n } from "/@/plugins/i18n";
import { useUserStoreHook } from "/@/store/modules/user";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
const { t } = useI18n();
const loading = ref(false);
const ruleForm = reactive({
phone: "",
@@ -21,9 +24,9 @@ const repeatPasswordRule = [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入确认密码"));
callback(new Error(transformI18n($t("login.passwordSureReg"))));
} else if (ruleForm.password !== value) {
callback(new Error("两次密码不一致!"));
callback(new Error(transformI18n($t("login.passwordDifferentReg"))));
} else {
callback();
}
@@ -39,7 +42,7 @@ const onUpdate = async (formEl: FormInstance | undefined) => {
if (valid) {
// 模拟请求,需根据实际开发进行修改
setTimeout(() => {
message.success("修改密码成功");
message.success(transformI18n($t("login.passwordUpdateReg")));
loading.value = false;
}, 2000);
} else {
@@ -67,7 +70,7 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.phone"
placeholder="手机号码"
:placeholder="t('login.phone')"
:prefix-icon="useRenderIcon('iphone')"
/>
</el-form-item>
@@ -79,14 +82,21 @@ function onBack() {
<el-input
clearable
v-model="ruleForm.verifyCode"
placeholder="短信验证码"
:placeholder="t('login.smsVerifyCode')"
:prefix-icon="
useRenderIcon('ri:shield-keyhole-line', { online: true })
"
/>
<el-button
:disabled="isDisabled"
class="ml-2"
@click="useVerifyCode().start(ruleFormRef, 'phone')"
>
{{ text }}
{{
text.length > 0
? text + t("login.info")
: t("login.getVerifyCode")
}}
</el-button>
</div>
</el-form-item>
@@ -98,7 +108,7 @@ function onBack() {
clearable
show-password
v-model="ruleForm.password"
placeholder="密码"
:placeholder="t('login.password')"
:prefix-icon="useRenderIcon('lock')"
/>
</el-form-item>
@@ -110,7 +120,7 @@ function onBack() {
clearable
show-password
v-model="ruleForm.repeatPassword"
placeholder="确认密码"
:placeholder="t('login.sure')"
:prefix-icon="useRenderIcon('lock')"
/>
</el-form-item>
@@ -125,7 +135,7 @@ function onBack() {
:loading="loading"
@click="onUpdate(ruleFormRef)"
>
确定
{{ t("login.definite") }}
</el-button>
</el-form-item>
</Motion>
@@ -133,7 +143,7 @@ function onBack() {
<Motion :delay="300">
<el-form-item>
<el-button class="w-full" size="default" @click="onBack">
返回
{{ t("login.back") }}
</el-button>
</el-form-item>
</Motion>

Some files were not shown because too many files have changed in this diff Show More