mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-02 19:15:26 +08:00
前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题
This commit is contained in:
66
jeecgboot-vue3/src/layouts/default/content/index.vue
Normal file
66
jeecgboot-vue3/src/layouts/default/content/index.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div :class="[prefixCls, getLayoutContentMode]" v-loading="getOpenPageLoading && getPageLoading">
|
||||
<PageLayout />
|
||||
<!-- update-begin-author:zyf date:20211129 for:qiankun 挂载子应用盒子 -->
|
||||
<div id="content" class="app-view-box" v-if="openQianKun == 'true'"></div>
|
||||
<!-- update-end-author:zyf date:20211129 for: qiankun 挂载子应用盒子-->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
import PageLayout from '/@/layouts/page/index.vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
|
||||
import { useContentViewHeight } from './useContentViewHeight';
|
||||
// import registerApps from '/@/qiankun';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
export default defineComponent({
|
||||
name: 'LayoutContent',
|
||||
components: { PageLayout },
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('layout-content');
|
||||
const { getOpenPageLoading } = useTransitionSetting();
|
||||
const { getLayoutContentMode, getPageLoading } = useRootSetting();
|
||||
const globSetting = useGlobSetting();
|
||||
const openQianKun = globSetting.openQianKun;
|
||||
useContentViewHeight();
|
||||
onMounted(() => {
|
||||
// //注册openQianKun
|
||||
// if (openQianKun == 'true') {
|
||||
// if (!window.qiankunStarted) {
|
||||
// window.qiankunStarted = true;
|
||||
// registerApps();
|
||||
// }
|
||||
// }
|
||||
});
|
||||
return {
|
||||
prefixCls,
|
||||
openQianKun,
|
||||
getOpenPageLoading,
|
||||
getLayoutContentMode,
|
||||
getPageLoading,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-content';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
&.fixed {
|
||||
width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&-loading {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
z-index: @page-loading-z-index;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,17 @@
|
||||
import type { InjectionKey, ComputedRef } from 'vue';
|
||||
import { createContext, useContext } from '/@/hooks/core/useContext';
|
||||
|
||||
export interface ContentContextProps {
|
||||
contentHeight: ComputedRef<number>;
|
||||
setPageHeight: (height: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const key: InjectionKey<ContentContextProps> = Symbol();
|
||||
|
||||
export function createContentContext(context: ContentContextProps) {
|
||||
return createContext<ContentContextProps>(context, key, { native: true });
|
||||
}
|
||||
|
||||
export function useContentContext() {
|
||||
return useContext<ContentContextProps>(key);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { ref, computed, unref } from 'vue';
|
||||
import { createPageContext } from '/@/hooks/component/usePageContext';
|
||||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
|
||||
|
||||
const headerHeightRef = ref(0);
|
||||
const footerHeightRef = ref(0);
|
||||
|
||||
export function useLayoutHeight() {
|
||||
function setHeaderHeight(val) {
|
||||
headerHeightRef.value = val;
|
||||
}
|
||||
function setFooterHeight(val) {
|
||||
footerHeightRef.value = val;
|
||||
}
|
||||
return { headerHeightRef, footerHeightRef, setHeaderHeight, setFooterHeight };
|
||||
}
|
||||
|
||||
export function useContentViewHeight() {
|
||||
const contentHeight = ref(window.innerHeight);
|
||||
const pageHeight = ref(window.innerHeight);
|
||||
const getViewHeight = computed(() => {
|
||||
return unref(contentHeight) - unref(headerHeightRef) - unref(footerHeightRef) || 0;
|
||||
});
|
||||
|
||||
useWindowSizeFn(
|
||||
() => {
|
||||
contentHeight.value = window.innerHeight;
|
||||
},
|
||||
100,
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function setPageHeight(height: number) {
|
||||
pageHeight.value = height;
|
||||
}
|
||||
|
||||
createPageContext({
|
||||
contentHeight: getViewHeight,
|
||||
setPageHeight,
|
||||
pageHeight,
|
||||
});
|
||||
}
|
||||
82
jeecgboot-vue3/src/layouts/default/feature/index.vue
Normal file
82
jeecgboot-vue3/src/layouts/default/feature/index.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { BackTop } from 'ant-design-vue';
|
||||
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||
|
||||
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
import SessionTimeoutLogin from '/@/views/sys/login/SessionTimeoutLogin.vue';
|
||||
export default defineComponent({
|
||||
name: 'LayoutFeatures',
|
||||
components: {
|
||||
BackTop,
|
||||
LayoutLockPage: createAsyncComponent(() => import('/@/views/sys/lock/index.vue')),
|
||||
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue')),
|
||||
SessionTimeoutLogin,
|
||||
},
|
||||
setup() {
|
||||
const { getUseOpenBackTop, getShowSettingButton, getSettingButtonPosition, getFullContent } = useRootSetting();
|
||||
const userStore = useUserStoreWithOut();
|
||||
const { prefixCls } = useDesign('setting-drawer-fearure');
|
||||
const { getShowHeader } = useHeaderSetting();
|
||||
|
||||
const getIsSessionTimeout = computed(() => userStore.getSessionTimeout);
|
||||
|
||||
const getIsFixedSettingDrawer = computed(() => {
|
||||
if (!unref(getShowSettingButton)) {
|
||||
return false;
|
||||
}
|
||||
const settingButtonPosition = unref(getSettingButtonPosition);
|
||||
|
||||
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
|
||||
return !unref(getShowHeader) || unref(getFullContent);
|
||||
}
|
||||
return settingButtonPosition === SettingButtonPositionEnum.FIXED;
|
||||
});
|
||||
|
||||
return {
|
||||
getTarget: () => document.body,
|
||||
getUseOpenBackTop,
|
||||
getIsFixedSettingDrawer,
|
||||
prefixCls,
|
||||
getIsSessionTimeout,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutLockPage />
|
||||
<BackTop v-if="getUseOpenBackTop" :target="getTarget" />
|
||||
<SettingDrawer v-if="getIsFixedSettingDrawer" :class="prefixCls" />
|
||||
<SessionTimeoutLogin v-if="getIsSessionTimeout" />
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-setting-drawer-fearure';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
color: @white;
|
||||
cursor: pointer;
|
||||
background-color: @primary-color;
|
||||
border-radius: 6px 0 0 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
jeecgboot-vue3/src/layouts/default/footer/index.vue
Normal file
103
jeecgboot-vue3/src/layouts/default/footer/index.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<Footer :class="prefixCls" v-if="getShowLayoutFooter" ref="footerRef">
|
||||
<div :class="`${prefixCls}__links`">
|
||||
<a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
|
||||
|
||||
<GithubFilled @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
|
||||
|
||||
<a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
|
||||
</div>
|
||||
<div>Copyright ©2021 JEECG开源社区 出品</div>
|
||||
</Footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, unref, ref } from 'vue';
|
||||
import { Layout } from 'ant-design-vue';
|
||||
|
||||
import { GithubFilled } from '@ant-design/icons-vue';
|
||||
|
||||
import { DOC_URL, GITHUB_URL, SITE_URL } from '/@/settings/siteSetting';
|
||||
import { openWindow } from '/@/utils';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useLayoutHeight } from '../content/useContentViewHeight';
|
||||
import { ThemeEnum } from '/@/enums/appEnum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutFooter',
|
||||
components: { Footer: Layout.Footer, GithubFilled },
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { getShowFooter } = useRootSetting();
|
||||
const { currentRoute } = useRouter();
|
||||
const { prefixCls } = useDesign('layout-footer');
|
||||
|
||||
const footerRef = ref<ComponentRef>(null);
|
||||
const { setFooterHeight } = useLayoutHeight();
|
||||
//当前主题
|
||||
const { getDarkMode } = useRootSetting();
|
||||
const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK);
|
||||
|
||||
const getShowLayoutFooter = computed(() => {
|
||||
if (unref(getShowFooter)) {
|
||||
const footerEl = unref(footerRef)?.$el;
|
||||
setFooterHeight(footerEl?.offsetHeight || 0);
|
||||
} else {
|
||||
setFooterHeight(0);
|
||||
}
|
||||
return unref(getShowFooter) && !unref(currentRoute).meta?.hiddenFooter;
|
||||
});
|
||||
//鼠标移入的颜色设置
|
||||
const hoverColor = computed(() => {
|
||||
return unref(isDark) ? 'rgba(255, 255, 255, 1)' : 'rgba(0, 0, 0, 0.85)';
|
||||
});
|
||||
return {
|
||||
getShowLayoutFooter,
|
||||
prefixCls,
|
||||
t,
|
||||
DOC_URL,
|
||||
GITHUB_URL,
|
||||
SITE_URL,
|
||||
openWindow,
|
||||
footerRef,
|
||||
hoverColor,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-layout-footer';
|
||||
|
||||
@normal-color: rgba(0, 0, 0, 0.45);
|
||||
// update-begin-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失
|
||||
@hover-color: v-bind(hoverColor);
|
||||
// update-end-author:liusq date:2023-7-12 for: [issues/608] dark 模式下底部 footer 文字 hover 样式导致文字消失
|
||||
.@{prefix-cls} {
|
||||
color: @normal-color;
|
||||
text-align: center;
|
||||
|
||||
&__links {
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: @normal-color;
|
||||
|
||||
&:hover {
|
||||
color: @hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__github {
|
||||
margin: 0 30px;
|
||||
|
||||
&:hover {
|
||||
color: @hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
130
jeecgboot-vue3/src/layouts/default/header/MultipleHeader.vue
Normal file
130
jeecgboot-vue3/src/layouts/default/header/MultipleHeader.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div :style="getPlaceholderDomStyle" v-if="getIsShowPlaceholderDom"></div>
|
||||
<div :style="getWrapStyle" :class="getClass">
|
||||
<LayoutHeader v-if="getShowInsetHeaderRef" />
|
||||
<MultipleTabs v-if="getShowTabs" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, computed, CSSProperties } from 'vue';
|
||||
|
||||
import LayoutHeader from './index.vue';
|
||||
import MultipleTabs from '../tabs/index.vue';
|
||||
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useFullContent } from '/@/hooks/web/useFullContent';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useLayoutHeight } from '../content/useContentViewHeight';
|
||||
import { TabsThemeEnum } from '/@/enums/appEnum';
|
||||
// update-begin--author:liaozhiyang---date:20240407---for:【QQYUN-8774】网站header区域加高
|
||||
const HEADER_HEIGHT = 60;
|
||||
// update-begin--author:liaozhiyang---date:20240407---for:【【QQYUN-8774】网站header区域加高
|
||||
|
||||
// updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式:更改高度
|
||||
const TABS_HEIGHT = 32;
|
||||
const TABS_HEIGHT_CARD = 50;
|
||||
const TABS_HEIGHT_SMOOTH = 50;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutMultipleHeader',
|
||||
components: { LayoutHeader, MultipleTabs },
|
||||
setup() {
|
||||
const { setHeaderHeight } = useLayoutHeight();
|
||||
const { prefixCls } = useDesign('layout-multiple-header');
|
||||
|
||||
const { getCalcContentWidth, getSplit } = useMenuSetting();
|
||||
const { getIsMobile } = useAppInject();
|
||||
const { getFixed, getShowInsetHeaderRef, getShowFullHeaderRef, getHeaderTheme, getShowHeader } = useHeaderSetting();
|
||||
|
||||
const { getFullContent } = useFullContent();
|
||||
|
||||
const { getShowMultipleTab, getTabsTheme } = useMultipleTabSetting();
|
||||
|
||||
const getShowTabs = computed(() => {
|
||||
return unref(getShowMultipleTab) && !unref(getFullContent);
|
||||
});
|
||||
|
||||
const getIsShowPlaceholderDom = computed(() => {
|
||||
return unref(getFixed) || unref(getShowFullHeaderRef);
|
||||
});
|
||||
|
||||
const getWrapStyle = computed((): CSSProperties => {
|
||||
const style: CSSProperties = {};
|
||||
if (unref(getFixed)) {
|
||||
style.width = unref(getIsMobile) ? '100%' : unref(getCalcContentWidth);
|
||||
}
|
||||
if (unref(getShowFullHeaderRef)) {
|
||||
style.top = `${HEADER_HEIGHT}px`;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
const getIsFixed = computed(() => {
|
||||
return unref(getFixed) || unref(getShowFullHeaderRef);
|
||||
});
|
||||
|
||||
// updateBy:sunjianlei---updateDate:2021-09-08---根据主题的不同,动态计算tabs高度
|
||||
const getTabsThemeHeight = computed(() => {
|
||||
let tabsTheme = unref(getTabsTheme);
|
||||
if (tabsTheme === TabsThemeEnum.CARD) {
|
||||
return TABS_HEIGHT_CARD;
|
||||
} else if (tabsTheme === TabsThemeEnum.SMOOTH) {
|
||||
return TABS_HEIGHT_SMOOTH;
|
||||
} else {
|
||||
return TABS_HEIGHT;
|
||||
}
|
||||
});
|
||||
|
||||
const getPlaceholderDomStyle = computed((): CSSProperties => {
|
||||
let height = 0;
|
||||
if ((unref(getShowFullHeaderRef) || !unref(getSplit)) && unref(getShowHeader) && !unref(getFullContent)) {
|
||||
height += HEADER_HEIGHT;
|
||||
}
|
||||
if (unref(getShowMultipleTab) && !unref(getFullContent)) {
|
||||
height += unref(getTabsThemeHeight);
|
||||
}
|
||||
setHeaderHeight(height);
|
||||
return {
|
||||
height: `${height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [prefixCls, `${prefixCls}--${unref(getHeaderTheme)}`, { [`${prefixCls}--fixed`]: unref(getIsFixed) }];
|
||||
});
|
||||
|
||||
return {
|
||||
getClass,
|
||||
prefixCls,
|
||||
getPlaceholderDomStyle,
|
||||
getIsFixed,
|
||||
getWrapStyle,
|
||||
getIsShowPlaceholderDom,
|
||||
getShowTabs,
|
||||
getShowInsetHeaderRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-layout-multiple-header';
|
||||
|
||||
.@{prefix-cls} {
|
||||
transition: width 0.2s;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&--dark {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: @multiple-tab-fixed-z-index;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div :class="[prefixCls, `${prefixCls}--${theme}`]">
|
||||
<a-breadcrumb :routes="routes">
|
||||
<template #itemRender="{ route, routes: routesMatched, paths }">
|
||||
<Icon :icon="getIcon(route)" v-if="getShowBreadCrumbIcon && getIcon(route)" />
|
||||
<span v-if="!hasRedirect(routesMatched, route)">
|
||||
{{ t(route.name || route.meta.title) }}
|
||||
</span>
|
||||
<router-link v-else to="" @click="handleClick(route, paths, $event)" style="color: inherit">
|
||||
{{ t(route.name || route.meta.title) }}
|
||||
</router-link>
|
||||
</template>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { RouteLocationMatched } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { Menu } from '/@/router/types';
|
||||
|
||||
import { defineComponent, ref, watchEffect } from 'vue';
|
||||
|
||||
import { Breadcrumb } from 'ant-design-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { isString } from '/@/utils/is';
|
||||
import { filter } from '/@/utils/helper/treeHelper';
|
||||
import { getMenus } from '/@/router/menus';
|
||||
|
||||
import { REDIRECT_NAME } from '/@/router/constant';
|
||||
import { getAllParentPath } from '/@/router/helper/menuHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutBreadcrumb',
|
||||
components: { Icon, [Breadcrumb.name]: Breadcrumb },
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
},
|
||||
setup() {
|
||||
const routes = ref<RouteLocationMatched[]>([]);
|
||||
const { currentRoute } = useRouter();
|
||||
const { prefixCls } = useDesign('layout-breadcrumb');
|
||||
const { getShowBreadCrumbIcon } = useRootSetting();
|
||||
const go = useGo();
|
||||
|
||||
const { t } = useI18n();
|
||||
watchEffect(async () => {
|
||||
if (currentRoute.value.name === REDIRECT_NAME) return;
|
||||
const menus = await getMenus();
|
||||
|
||||
const routeMatched = currentRoute.value.matched;
|
||||
const cur = routeMatched?.[routeMatched.length - 1];
|
||||
let path = currentRoute.value.path;
|
||||
|
||||
if (cur && cur?.meta?.currentActiveMenu) {
|
||||
path = cur.meta.currentActiveMenu as string;
|
||||
}
|
||||
|
||||
const parent = getAllParentPath(menus, path);
|
||||
const filterMenus = menus.filter((item) => item.path === parent[0]);
|
||||
const matched = getMatched(filterMenus, parent) as any;
|
||||
|
||||
if (!matched || matched.length === 0) return;
|
||||
|
||||
const breadcrumbList = filterItem(matched);
|
||||
|
||||
if (currentRoute.value.meta?.currentActiveMenu) {
|
||||
breadcrumbList.push({
|
||||
...currentRoute.value,
|
||||
name: currentRoute.value.meta?.title || currentRoute.value.name,
|
||||
} as unknown as RouteLocationMatched);
|
||||
}
|
||||
routes.value = breadcrumbList;
|
||||
});
|
||||
|
||||
function getMatched(menus: Menu[], parent: string[]) {
|
||||
const metched: Menu[] = [];
|
||||
menus.forEach((item) => {
|
||||
if (parent.includes(item.path)) {
|
||||
metched.push({
|
||||
...item,
|
||||
name: item.meta?.title || item.name,
|
||||
});
|
||||
}
|
||||
if (item.children?.length) {
|
||||
metched.push(...getMatched(item.children, parent));
|
||||
}
|
||||
});
|
||||
return metched;
|
||||
}
|
||||
|
||||
function filterItem(list: RouteLocationMatched[]) {
|
||||
return filter(list, (item) => {
|
||||
const { meta, name } = item;
|
||||
if (!meta) {
|
||||
return !!name;
|
||||
}
|
||||
const { title, hideBreadcrumb } = meta;
|
||||
if (!title || hideBreadcrumb) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).filter((item) => !item.meta?.hideBreadcrumb);
|
||||
}
|
||||
|
||||
function handleClick(route: RouteLocationMatched, paths: string[], e: Event) {
|
||||
e?.preventDefault();
|
||||
const { children, redirect, meta } = route;
|
||||
|
||||
if (children?.length && !redirect) {
|
||||
e?.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (meta?.carryParam) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redirect && isString(redirect)) {
|
||||
go(redirect);
|
||||
} else {
|
||||
let goPath = '';
|
||||
if (paths.length === 1) {
|
||||
goPath = paths[0];
|
||||
} else {
|
||||
const ps = paths.slice(1);
|
||||
const lastPath = ps.pop() || '';
|
||||
goPath = `${lastPath}`;
|
||||
}
|
||||
goPath = /^\//.test(goPath) ? goPath : `/${goPath}`;
|
||||
go(goPath);
|
||||
}
|
||||
}
|
||||
|
||||
function hasRedirect(routes: RouteLocationMatched[], route: RouteLocationMatched) {
|
||||
return routes.indexOf(route) !== routes.length - 1;
|
||||
}
|
||||
|
||||
function getIcon(route) {
|
||||
return route.icon || route.meta?.icon;
|
||||
}
|
||||
|
||||
return { routes, t, prefixCls, getIcon, getShowBreadCrumbIcon, handleClick, hasRedirect };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-breadcrumb';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
align-items: center;
|
||||
|
||||
.ant-breadcrumb-link {
|
||||
.anticon {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240408---for:【QQYUN-8922】面包屑样式调整
|
||||
&--light {
|
||||
.ant-breadcrumb-link {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-breadcrumb-separator,
|
||||
.anticon {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
&--dark {
|
||||
.ant-breadcrumb-link {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
&:hover {
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-breadcrumb-separator,
|
||||
.anticon {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
html[data-theme='light'] {
|
||||
.@{prefix-cls} {
|
||||
&--dark {
|
||||
.ant-breadcrumb-link {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
a {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
&:hover {
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-breadcrumb-separator,
|
||||
.anticon {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20240408---for:【QQYUN-8922】面包屑样式调整
|
||||
</style>
|
||||
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<Tooltip :title="t('layout.header.tooltipErrorLog')" placement="bottom" :mouseEnterDelay="0.5" @click="handleToErrorList">
|
||||
<Badge :count="getCount" :offset="[0, 10]" :overflowCount="99">
|
||||
<Icon icon="ion:bug-outline" />
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { Tooltip, Badge } from 'ant-design-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useErrorLogStore } from '/@/store/modules/errorLog';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorAction',
|
||||
components: { Icon, Tooltip, Badge },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { push } = useRouter();
|
||||
const errorLogStore = useErrorLogStore();
|
||||
|
||||
const getCount = computed(() => errorLogStore.getErrorLogListCount);
|
||||
|
||||
function handleToErrorList() {
|
||||
push(PageEnum.ERROR_LOG_PAGE).then(() => {
|
||||
errorLogStore.setErrorLogListCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
getCount,
|
||||
handleToErrorList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<Tooltip :title="getTitle" placement="bottom" :mouseEnterDelay="0.5">
|
||||
<span @click="toggle">
|
||||
<FullscreenOutlined v-if="!isFullscreen" />
|
||||
<FullscreenExitOutlined v-else />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
|
||||
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
|
||||
export default defineComponent({
|
||||
name: 'FullScreen',
|
||||
components: { FullscreenExitOutlined, FullscreenOutlined, Tooltip },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { toggle, isFullscreen } = useFullscreen();
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return unref(isFullscreen) ? t('layout.header.tooltipExitFull') : t('layout.header.tooltipEntryFull');
|
||||
});
|
||||
|
||||
return {
|
||||
getTitle,
|
||||
isFullscreen,
|
||||
toggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<Tooltip :title="t('layout.header.tooltipLock')" placement="bottom" :mouseEnterDelay="0.5" @click="handleLock">
|
||||
<LockOutlined />
|
||||
</Tooltip>
|
||||
<LockModal ref="modalRef" v-if="lockModalVisible" @register="register" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { LockOutlined } from '@ant-design/icons-vue';
|
||||
import Icon from '/@/components/Icon';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { getRefPromise } from '/@/utils/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LockScreen',
|
||||
inheritAttrs: false,
|
||||
components: {
|
||||
Icon,
|
||||
Tooltip,
|
||||
LockOutlined,
|
||||
LockModal: createAsyncComponent(() => import('./lock/LockModal.vue')),
|
||||
},
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const [register, { openModal }] = useModal();
|
||||
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
const lockModalVisible = ref(false);
|
||||
const modalRef = ref(null);
|
||||
async function handleLock() {
|
||||
lockModalVisible.value = true;
|
||||
await getRefPromise(modalRef);
|
||||
openModal(true);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
return {
|
||||
t,
|
||||
register,
|
||||
handleLock,
|
||||
lockModalVisible,
|
||||
modalRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,16 @@
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import FullScreen from './FullScreen.vue';
|
||||
|
||||
export const UserDropDown = createAsyncComponent(() => import('./user-dropdown/index.vue'), {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.vue'));
|
||||
|
||||
export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
|
||||
|
||||
export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
|
||||
|
||||
export const LockScreen = createAsyncComponent(() => import('./LockScreen.vue'));
|
||||
|
||||
export { FullScreen };
|
||||
@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<BasicModal :footer="null" :title="t('layout.header.lockScreen')" v-bind="$attrs" :class="prefixCls" @register="register" :canFullscreen="false">
|
||||
<div :class="`${prefixCls}__entry`">
|
||||
<div :class="`${prefixCls}__header`">
|
||||
<img :src="avatar" :class="`${prefixCls}__header-img`" />
|
||||
<p :class="`${prefixCls}__header-name`">
|
||||
{{ getRealName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BasicForm @register="registerForm" />
|
||||
|
||||
<div :class="`${prefixCls}__footer`">
|
||||
<a-button type="primary" block class="mt-2" @click="handleLock">
|
||||
{{ t('layout.header.lockScreenBtn') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal/index';
|
||||
import BasicForm from '/@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '/@/components/Form/src/hooks/useForm';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useLockStore } from '/@/store/modules/lock';
|
||||
import headerImg from '/@/assets/images/header.jpg';
|
||||
export default defineComponent({
|
||||
name: 'LockModal',
|
||||
components: { BasicModal, BasicForm },
|
||||
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { prefixCls } = useDesign('header-lock-modal');
|
||||
const userStore = useUserStore();
|
||||
const lockStore = useLockStore();
|
||||
|
||||
const getRealName = computed(() => userStore.getUserInfo?.realname);
|
||||
const [register, { closeModal }] = useModalInner();
|
||||
|
||||
const [registerForm, { validateFields, resetFields }] = useForm({
|
||||
//update-begin---author:wangshuai---date:2024-04-08---for:【QQYUN-8895】锁屏样式修改---
|
||||
labelWidth: 74,
|
||||
labelAlign:'left',
|
||||
wrapperCol:{},
|
||||
//update-end---author:wangshuai---date:2024-04-08---for:【QQYUN-8895】锁屏样式修改---
|
||||
showActionButtonGroup: false,
|
||||
schemas: [
|
||||
{
|
||||
field: 'password',
|
||||
label: t('layout.header.lockScreenPassword'),
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
autocomplete: "new-password",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
async function handleLock() {
|
||||
const values = (await validateFields()) as any;
|
||||
const password: string | undefined = values.password;
|
||||
closeModal();
|
||||
|
||||
lockStore.setLockInfo({
|
||||
isLock: true,
|
||||
pwd: password,
|
||||
});
|
||||
await resetFields();
|
||||
}
|
||||
|
||||
const avatar = computed(() => {
|
||||
const { avatar } = userStore.getUserInfo;
|
||||
return avatar || headerImg;
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
getRealName,
|
||||
register,
|
||||
registerForm,
|
||||
handleLock,
|
||||
avatar,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-lock-modal';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&__entry {
|
||||
position: relative;
|
||||
//height: 240px;
|
||||
padding: 130px 30px 30px 30px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - 45px);
|
||||
width: auto;
|
||||
text-align: center;
|
||||
|
||||
&-img {
|
||||
width: 70px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&-name {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<a-list :class="prefixCls" :pagination="getPagination">
|
||||
<template v-for="item in getData" :key="item.id">
|
||||
<a-list-item class="list-item" @click="handleTitleClick(item)" :style="{ cursor: isTitleClickable ? 'pointer' : '' }">
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="title">
|
||||
<a-typography-paragraph
|
||||
style="width: 100%; margin-bottom: 0 !important"
|
||||
:delete="!!item.titleDelete"
|
||||
:ellipsis="$props.titleRows && $props.titleRows > 0 ? { rows: $props.titleRows, tooltip: !!item.title } : false"
|
||||
:content="item.title"
|
||||
/>
|
||||
<div class="extra" v-if="item.extra">
|
||||
<a-tag class="tag" :color="item.color">
|
||||
{{ item.extra }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #avatar>
|
||||
<a-avatar v-if="item.avatar" class="avatar" :src="item.avatar" />
|
||||
<template v-else-if="item.priority">
|
||||
<a-avatar v-if="item.priority === PriorityTypes.L" class="avatar priority-L" title="一般消息">
|
||||
<template #icon>
|
||||
<Icon icon="entypo:info" />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<a-avatar v-if="item.priority === PriorityTypes.M" class="avatar priority-M" title="重要消息">
|
||||
<template #icon>
|
||||
<Icon icon="bi:exclamation-lg" />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<a-avatar v-if="item.priority === PriorityTypes.H" class="avatar priority-H" title="紧急消息">
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:warning-filled" />
|
||||
</template>
|
||||
</a-avatar>
|
||||
</template>
|
||||
<span v-else> {{ item.avatar }}</span>
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<div>
|
||||
<div class="description" v-if="item.description">
|
||||
<a-typography-paragraph
|
||||
style="width: 100%; margin-bottom: 0 !important"
|
||||
:ellipsis="$props.descRows && $props.descRows > 0 ? { rows: $props.descRows, tooltip: !!item.description } : false"
|
||||
:content="item.description"
|
||||
/>
|
||||
</div>
|
||||
<div class="datetime">
|
||||
<Time :value="item.datetime" :title="item.datetime" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref, watch, unref } from 'vue';
|
||||
import { PriorityTypes, ListItem } from './data';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
|
||||
import { Time } from '/@/components/Time';
|
||||
import { isNumber } from '/@/utils/is';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
[Avatar.name]: Avatar,
|
||||
[List.name]: List,
|
||||
[List.Item.name]: List.Item,
|
||||
AListItemMeta: List.Item.Meta,
|
||||
ATypographyParagraph: Typography.Paragraph,
|
||||
[Tag.name]: Tag,
|
||||
Time,
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array as PropType<ListItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
pageSize: {
|
||||
type: [Boolean, Number] as PropType<Boolean | Number>,
|
||||
default: 5,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
titleRows: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
descRows: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
onTitleClick: {
|
||||
type: Function as PropType<(Recordable) => void>,
|
||||
},
|
||||
},
|
||||
emits: ['update:currentPage'],
|
||||
setup(props, { emit }) {
|
||||
const { prefixCls } = useDesign('header-notify-list');
|
||||
const current = ref(props.currentPage || 1);
|
||||
const getData = computed(() => {
|
||||
const { pageSize, list } = props;
|
||||
if (pageSize === false) return [];
|
||||
let size = isNumber(pageSize) ? pageSize : 5;
|
||||
return list.slice(size * (unref(current) - 1), size * unref(current));
|
||||
});
|
||||
watch(
|
||||
() => props.currentPage,
|
||||
(v) => {
|
||||
current.value = v;
|
||||
}
|
||||
);
|
||||
const isTitleClickable = computed(() => !!props.onTitleClick);
|
||||
const getPagination = computed(() => {
|
||||
const { list, pageSize } = props;
|
||||
if (pageSize > 0 && list && list.length > pageSize) {
|
||||
return {
|
||||
total: list.length,
|
||||
pageSize,
|
||||
//size: 'small',
|
||||
current: unref(current),
|
||||
onChange(page) {
|
||||
current.value = page;
|
||||
emit('update:currentPage', page);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
function handleTitleClick(item: ListItem) {
|
||||
props.onTitleClick && props.onTitleClick(item);
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
getPagination,
|
||||
getData,
|
||||
handleTitleClick,
|
||||
isTitleClickable,
|
||||
PriorityTypes,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-header-notify-list';
|
||||
|
||||
.@{prefix-cls} {
|
||||
width: 340px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.ant-pagination-disabled) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
|
||||
.extra {
|
||||
float: right;
|
||||
margin-top: -1.5px;
|
||||
margin-right: 0;
|
||||
font-weight: normal;
|
||||
|
||||
.tag {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
.priority-L,
|
||||
.priority-M,
|
||||
.priority-H {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.priority-L {
|
||||
background-color: #7cd1ff;
|
||||
}
|
||||
|
||||
.priority-M {
|
||||
background-color: #ffa743;
|
||||
}
|
||||
|
||||
.priority-H {
|
||||
background-color: #f8766c;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,206 @@
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
avatar: string;
|
||||
// 通知的标题内容
|
||||
title: string;
|
||||
// 是否在标题上显示删除线
|
||||
titleDelete?: boolean;
|
||||
datetime: string;
|
||||
type: string;
|
||||
read?: boolean;
|
||||
description: string;
|
||||
clickClose?: boolean;
|
||||
extra?: string;
|
||||
color?: string;
|
||||
// 优先级
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
export enum PriorityTypes {
|
||||
// 低优先级,一般消息
|
||||
L = 'L',
|
||||
// 中优先级,重要消息
|
||||
M = 'M',
|
||||
// 高优先级,紧急消息
|
||||
H = 'H',
|
||||
}
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
name: string;
|
||||
list: ListItem[];
|
||||
unreadlist?: ListItem[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const tabListData: TabItem[] = [
|
||||
{
|
||||
key: '1',
|
||||
name: '通知',
|
||||
list: [
|
||||
{
|
||||
id: '000000001',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
|
||||
title: '你收到了 14 份新周报',
|
||||
description: '',
|
||||
datetime: '2017-08-09',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000002',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
|
||||
title: '你推荐的 曲妮妮 已通过第三轮面试',
|
||||
description: '',
|
||||
datetime: '2017-08-08',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000003',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
|
||||
title: '这种模板可以区分多种通知类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
// read: true,
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000004',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000005',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '标题可以设置自动显示省略号,本例中标题行数已设为1行,如果内容超过1行将自动截断并支持tooltip显示完整标题。',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000006',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000007',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000008',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000009',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
{
|
||||
id: '000000010',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
|
||||
title: '左侧图标用于区分不同的类型',
|
||||
description: '',
|
||||
datetime: '2017-08-07',
|
||||
type: '1',
|
||||
},
|
||||
],
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: '系统消息',
|
||||
list: [
|
||||
{
|
||||
id: '000000006',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '曲丽丽 评论了你',
|
||||
description: '描述信息描述信息描述信息',
|
||||
datetime: '2017-08-07',
|
||||
type: '2',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000007',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '朱偏右 回复了你',
|
||||
description: '这种模板用于提醒谁与你发生了互动',
|
||||
datetime: '2017-08-07',
|
||||
type: '2',
|
||||
clickClose: true,
|
||||
},
|
||||
{
|
||||
id: '000000008',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
|
||||
title: '标题',
|
||||
description:
|
||||
'请将鼠标移动到此处,以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2,超过2行的描述内容将被省略并且可以通过tooltip查看完整内容',
|
||||
datetime: '2017-08-07',
|
||||
type: '2',
|
||||
clickClose: true,
|
||||
},
|
||||
],
|
||||
count: 0,
|
||||
},
|
||||
// {
|
||||
// key: '3',
|
||||
// name: '待办',
|
||||
// list: [
|
||||
// {
|
||||
// id: '000000009',
|
||||
// avatar: '',
|
||||
// title: '任务名称',
|
||||
// description: '任务需要在 2017-01-12 20:00 前启动',
|
||||
// datetime: '',
|
||||
// extra: '未开始',
|
||||
// color: '',
|
||||
// type: '3',
|
||||
// },
|
||||
// {
|
||||
// id: '000000010',
|
||||
// avatar: '',
|
||||
// title: '第三方紧急代码变更',
|
||||
// description: '冠霖 需在 2017-01-07 前完成代码变更任务',
|
||||
// datetime: '',
|
||||
// extra: '马上到期',
|
||||
// color: 'red',
|
||||
// type: '3',
|
||||
// },
|
||||
// {
|
||||
// id: '000000011',
|
||||
// avatar: '',
|
||||
// title: '信息安全考试',
|
||||
// description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||
// datetime: '',
|
||||
// extra: '已耗时 8 天',
|
||||
// color: 'gold',
|
||||
// type: '3',
|
||||
// },
|
||||
// {
|
||||
// id: '000000012',
|
||||
// avatar: '',
|
||||
// title: 'ABCD 版本发布',
|
||||
// description: '指派竹尔于 2017-01-09 前完成更新并发布',
|
||||
// datetime: '',
|
||||
// extra: '进行中',
|
||||
// color: 'blue',
|
||||
// type: '3',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<Badge :count="count" :overflowCount="9" :offset="[-4, 10]" :numberStyle="numberStyle" @click="clickBadge">
|
||||
<BellOutlined />
|
||||
</Badge>
|
||||
|
||||
<DynamicNotice ref="dynamicNoticeRef" v-bind="dynamicNoticeProps" />
|
||||
<DetailModal @register="registerDetail" />
|
||||
|
||||
<sys-message-modal @register="registerMessageModal" @refresh="reloadCount"></sys-message-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, unref, reactive, onMounted, getCurrentInstance } from 'vue';
|
||||
import { Popover, Tabs, Badge } from 'ant-design-vue';
|
||||
import { BellOutlined } from '@ant-design/icons-vue';
|
||||
import { tabListData } from './data';
|
||||
import { listCementByUser, editCementSend } from './notify.api';
|
||||
import NoticeList from './NoticeList.vue';
|
||||
import DetailModal from '/@/views/monitor/mynews/DetailModal.vue';
|
||||
import DynamicNotice from '/@/views/monitor/mynews/DynamicNotice.vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
|
||||
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
import md5 from 'crypto-js/md5';
|
||||
|
||||
import SysMessageModal from '/@/views/system/message/components/SysMessageModal.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popover,
|
||||
BellOutlined,
|
||||
Tabs,
|
||||
TabPane: Tabs.TabPane,
|
||||
Badge,
|
||||
NoticeList,
|
||||
DetailModal,
|
||||
DynamicNotice,
|
||||
SysMessageModal,
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('header-notify');
|
||||
const instance: any = getCurrentInstance();
|
||||
const userStore = useUserStore();
|
||||
const glob = useGlobSetting();
|
||||
const dynamicNoticeProps = reactive({ path: '', formData: {} });
|
||||
const [registerDetail, detailModal] = useModal();
|
||||
const listData = ref(tabListData);
|
||||
const count = computed(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < listData.value.length; i++) {
|
||||
count += listData.value[i].count;
|
||||
}
|
||||
return count;
|
||||
});
|
||||
|
||||
const [registerMessageModal, { openModal: openMessageModal }] = useModal();
|
||||
function clickBadge(){
|
||||
//消息列表弹窗前去除角标
|
||||
for (let i = 0; i < listData.value.length; i++) {
|
||||
listData.value[i].count = 0;
|
||||
}
|
||||
openMessageModal(true, {})
|
||||
}
|
||||
|
||||
const popoverVisible = ref<boolean>(false);
|
||||
onMounted(() => {
|
||||
initWebSocket();
|
||||
});
|
||||
|
||||
function mapAnnouncement(item) {
|
||||
return {
|
||||
...item,
|
||||
title: item.titile,
|
||||
description: item.msgAbstract,
|
||||
datetime: item.sendTime,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取系统消息
|
||||
async function loadData() {
|
||||
try {
|
||||
let { anntMsgList, sysMsgList, anntMsgTotal, sysMsgTotal } = await listCementByUser({
|
||||
pageSize: 5,
|
||||
});
|
||||
listData.value[0].list = anntMsgList.map(mapAnnouncement);
|
||||
listData.value[1].list = sysMsgList.map(mapAnnouncement);
|
||||
listData.value[0].count = anntMsgTotal;
|
||||
listData.value[1].count = sysMsgTotal;
|
||||
} catch (e) {
|
||||
console.warn('系统消息通知异常:', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
function onNoticeClick(record) {
|
||||
try {
|
||||
editCementSend(record.id);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (record.openType === 'component') {
|
||||
dynamicNoticeProps.path = record.openPage;
|
||||
dynamicNoticeProps.formData = { id: record.busId };
|
||||
instance.refs.dynamicNoticeRef?.detail(record.openPage);
|
||||
} else {
|
||||
detailModal.openModal(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
});
|
||||
}
|
||||
popoverVisible.value = false;
|
||||
}
|
||||
|
||||
// 初始化 WebSocket
|
||||
function initWebSocket() {
|
||||
let token = getToken();
|
||||
//将登录token生成一个短的标识
|
||||
let wsClientId = md5(token);
|
||||
let userId = unref(userStore.getUserInfo).id + "_" + wsClientId;
|
||||
// WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
|
||||
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
|
||||
connectWebSocket(url);
|
||||
onWebSocket(onWebSocketMessage);
|
||||
}
|
||||
|
||||
function onWebSocketMessage(data) {
|
||||
if (data.cmd === 'topic' || data.cmd === 'user') {
|
||||
//update-begin-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
|
||||
//后台保存数据太慢 前端延迟刷新消息
|
||||
setTimeout(()=>{
|
||||
loadData();
|
||||
}, 1000)
|
||||
//update-end-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
|
||||
}
|
||||
}
|
||||
|
||||
// 清空消息
|
||||
function onEmptyNotify() {
|
||||
popoverVisible.value = false;
|
||||
readAllMsg({}, loadData);
|
||||
}
|
||||
async function reloadCount(id){
|
||||
try {
|
||||
await editCementSend(id);
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
listData,
|
||||
count,
|
||||
clickBadge,
|
||||
registerMessageModal,
|
||||
reloadCount,
|
||||
onNoticeClick,
|
||||
onEmptyNotify,
|
||||
numberStyle: {},
|
||||
popoverVisible,
|
||||
registerDetail,
|
||||
dynamicNoticeProps,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-header-notify';
|
||||
|
||||
.@{prefix-cls} {
|
||||
padding-top: 2px;
|
||||
|
||||
&__overlay {
|
||||
max-width: 340px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
padding: 12px 24px;
|
||||
transition: background-color 300ms;
|
||||
|
||||
}
|
||||
|
||||
.bottom-buttons {
|
||||
text-align: center;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
height: 42px;
|
||||
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
font-size: 18px;
|
||||
|
||||
.ant-badge-count {
|
||||
@badget-size: 16px;
|
||||
width: @badget-size;
|
||||
height: @badget-size;
|
||||
min-width: @badget-size;
|
||||
line-height: @badget-size;
|
||||
padding: 0;
|
||||
|
||||
.ant-scroll-number-only > p.ant-scroll-number-only-unit {
|
||||
font-size: 14px;
|
||||
height: @badget-size;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-badge-multiple-words {
|
||||
padding: 0 0 0 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容黑暗模式
|
||||
[data-theme='dark'] .@{prefix-cls} {
|
||||
&__overlay {
|
||||
.ant-list-item {
|
||||
&:hover {
|
||||
background-color: #111b26;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-buttons {
|
||||
border-top: 1px solid #303030;
|
||||
|
||||
.ant-btn {
|
||||
&:first-child {
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<Popover v-model:open="popoverVisible" title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
|
||||
<Badge :count="count" :overflowCount="9" :offset="[-4, 10]" :numberStyle="numberStyle">
|
||||
<BellOutlined />
|
||||
</Badge>
|
||||
<template #content>
|
||||
<Tabs>
|
||||
<template v-for="item in listData" :key="item.key">
|
||||
<TabPane>
|
||||
<template #tab>
|
||||
{{ item.name }}
|
||||
<span v-if="item.list.length !== 0">({{ item.count }})</span>
|
||||
</template>
|
||||
<!-- 绑定title-click事件的通知列表中标题是“可点击”的-->
|
||||
<NoticeList :list="item.list" @title-click="onNoticeClick" />
|
||||
</TabPane>
|
||||
</template>
|
||||
</Tabs>
|
||||
<a-row class="bottom-buttons">
|
||||
<a-col :span="count === 0 ? 0 : 12">
|
||||
<a-button @click="onEmptyNotify" type="dashed" block>清空消息</a-button>
|
||||
</a-col>
|
||||
<a-col :span="count === 0 ? 24 : 12">
|
||||
<a-button @click="popoverVisible = false" type="dashed" block>
|
||||
<router-link to="/monitor/mynews">查看更多</router-link>
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</Popover>
|
||||
<DynamicNotice ref="dynamicNoticeRef" v-bind="dynamicNoticeProps" />
|
||||
<DetailModal @register="registerDetail" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, unref, reactive, onMounted, getCurrentInstance, onUnmounted } from 'vue';
|
||||
import { Popover, Tabs, Badge } from 'ant-design-vue';
|
||||
import { BellOutlined } from '@ant-design/icons-vue';
|
||||
import { tabListData } from './data';
|
||||
import { listCementByUser, editCementSend } from './notify.api';
|
||||
import NoticeList from './NoticeList.vue';
|
||||
import DetailModal from '/@/views/monitor/mynews/DetailModal.vue';
|
||||
import DynamicNotice from '/@/views/monitor/mynews/DynamicNotice.vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { connectWebSocket, onWebSocket, result } from '/@/hooks/web/useWebSocket';
|
||||
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Popover,
|
||||
BellOutlined,
|
||||
Tabs,
|
||||
TabPane: Tabs.TabPane,
|
||||
Badge,
|
||||
NoticeList,
|
||||
DetailModal,
|
||||
DynamicNotice,
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('header-notify');
|
||||
const instance: any = getCurrentInstance();
|
||||
const userStore = useUserStore();
|
||||
const glob = useGlobSetting();
|
||||
const dynamicNoticeProps = reactive({ path: '', formData: {} });
|
||||
const [registerDetail, detailModal] = useModal();
|
||||
const popoverVisible = ref<boolean>(false);
|
||||
const listData = ref(tabListData);
|
||||
listData.value[0].list = [];
|
||||
listData.value[1].list = [];
|
||||
listData.value[0].count = 0;
|
||||
listData.value[1].count = 0;
|
||||
|
||||
onMounted(() => {
|
||||
initWebSocket();
|
||||
});
|
||||
|
||||
const count = computed(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < listData.value.length; i++) {
|
||||
count += listData.value[i].count;
|
||||
}
|
||||
return count;
|
||||
});
|
||||
|
||||
function mapAnnouncement(item) {
|
||||
return {
|
||||
...item,
|
||||
title: item.titile,
|
||||
description: item.msgAbstract,
|
||||
datetime: item.sendTime,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取系统消息
|
||||
async function loadData() {
|
||||
try {
|
||||
let { anntMsgList, sysMsgList, anntMsgTotal, sysMsgTotal } = await listCementByUser({
|
||||
pageSize: 5,
|
||||
});
|
||||
listData.value[0].list = anntMsgList.map(mapAnnouncement);
|
||||
listData.value[1].list = sysMsgList.map(mapAnnouncement);
|
||||
listData.value[0].count = anntMsgTotal;
|
||||
listData.value[1].count = sysMsgTotal;
|
||||
} catch (e) {
|
||||
console.warn('系统消息通知异常:', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
function onNoticeClick(record) {
|
||||
try {
|
||||
editCementSend(record.id);
|
||||
loadData();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (record.openType === 'component') {
|
||||
dynamicNoticeProps.path = record.openPage;
|
||||
dynamicNoticeProps.formData = { id: record.busId };
|
||||
instance.refs.dynamicNoticeRef?.detail(record.openPage);
|
||||
} else {
|
||||
detailModal.openModal(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
});
|
||||
}
|
||||
popoverVisible.value = false;
|
||||
}
|
||||
|
||||
// 初始化 WebSocket
|
||||
function initWebSocket() {
|
||||
let userId = unref(userStore.getUserInfo).id;
|
||||
let token = getToken();
|
||||
// WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
|
||||
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
|
||||
connectWebSocket(url);
|
||||
onWebSocket(onWebSocketMessage);
|
||||
}
|
||||
|
||||
function onWebSocketMessage(data) {
|
||||
console.log('---onWebSocketMessage---', data)
|
||||
if (data.cmd === 'topic' || data.cmd === 'user') {
|
||||
//update-begin-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
|
||||
//后台保存数据太慢 前端延迟刷新消息
|
||||
setTimeout(()=>{
|
||||
loadData();
|
||||
}, 1000)
|
||||
//update-end-author:taoyan date:2022-7-13 for: VUEN-1674【严重bug】系统通知,为什么必须刷新右上角才提示
|
||||
}
|
||||
}
|
||||
|
||||
// 清空消息
|
||||
function onEmptyNotify() {
|
||||
popoverVisible.value = false;
|
||||
readAllMsg({}, loadData);
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
listData,
|
||||
count,
|
||||
onNoticeClick,
|
||||
onEmptyNotify,
|
||||
numberStyle: {},
|
||||
popoverVisible,
|
||||
registerDetail,
|
||||
dynamicNoticeProps,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-header-notify';
|
||||
|
||||
.@{prefix-cls} {
|
||||
padding-top: 2px;
|
||||
|
||||
&__overlay {
|
||||
max-width: 340px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
padding: 12px 24px;
|
||||
transition: background-color 300ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-buttons {
|
||||
text-align: center;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
height: 42px;
|
||||
|
||||
.ant-btn {
|
||||
border: 0;
|
||||
height: 100%;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
font-size: 18px;
|
||||
|
||||
.ant-badge-count {
|
||||
@badget-size: 16px;
|
||||
width: @badget-size;
|
||||
height: @badget-size;
|
||||
min-width: @badget-size;
|
||||
line-height: @badget-size;
|
||||
padding: 0;
|
||||
|
||||
.ant-scroll-number-only > p.ant-scroll-number-only-unit {
|
||||
font-size: 14px;
|
||||
height: @badget-size;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-badge-multiple-words {
|
||||
padding: 0 0 0 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容黑暗模式
|
||||
[data-theme='dark'] .@{prefix-cls} {
|
||||
&__overlay {
|
||||
.ant-list-item {
|
||||
&:hover {
|
||||
background-color: #111b26;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-buttons {
|
||||
border-top: 1px solid #303030;
|
||||
|
||||
.ant-btn {
|
||||
&:first-child {
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,27 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
enum Api {
|
||||
listCementByUser = '/sys/annountCement/listByUser',
|
||||
getUnreadMessageCount = '/sys/annountCement/getUnreadMessageCount',
|
||||
editCementSend = '/sys/sysAnnouncementSend/editByAnntIdAndUserId',
|
||||
clearAllUnReadMessage = '/sys/annountCement/clearAllUnReadMessage',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统通知消息列表
|
||||
* @param params
|
||||
*/
|
||||
export const listCementByUser = (params?) => defHttp.get({ url: Api.listCementByUser, params });
|
||||
|
||||
/**
|
||||
* 获取用户近两个月未读消息数量
|
||||
* @param params
|
||||
*/
|
||||
export const getUnreadMessageCount = (params?) => defHttp.get({ url: Api.getUnreadMessageCount, params });
|
||||
|
||||
export const editCementSend = (anntId, params?) => defHttp.put({ url: Api.editCementSend, params: { anntId, ...params } });
|
||||
|
||||
/**
|
||||
* 清空全部未读消息
|
||||
*/
|
||||
export const clearAllUnReadMessage = () => defHttp.post({ url: Api.clearAllUnReadMessage },{ isTransformResponse: false });
|
||||
@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<BasicModal v-bind="config" :maxHeight="500" :title="currTitle" v-model:visible="visible" wrapClassName="loginSelectModal">
|
||||
<a-form ref="formRef" v-bind="layout" :colon="false" class="loginSelectForm">
|
||||
<a-form-item v-if="isMultiTenant" :validate-status="validate_status">
|
||||
<!--label内容-->
|
||||
<template #label>
|
||||
<a-tooltip placement="topLeft">
|
||||
<template #title>
|
||||
<span>您隶属于多租户,请选择当前所属租户</span>
|
||||
</template>
|
||||
<a-avatar style="background-color: #87d068" :size="30"> 租户 </a-avatar>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<!--部门下拉内容-->
|
||||
<a-select v-model:value="tenantSelected" placeholder="请选择登录部门" :class="{ 'valid-error': validate_status == 'error' }">
|
||||
<template #suffixIcon>
|
||||
<Icon icon="ant-design:gold-outline" />
|
||||
</template>
|
||||
<template v-for="tenant in tenantList" :key="tenant.id">
|
||||
<a-select-option :value="tenant.id">{{ tenant.name }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isMultiDepart" :validate-status="validate_status1">
|
||||
<!--label内容-->
|
||||
<template #label>
|
||||
<a-tooltip placement="topLeft">
|
||||
<template #title>
|
||||
<span>您隶属于多部门,请选择当前所在部门</span>
|
||||
</template>
|
||||
<a-avatar style="background-color: rgb(104, 208, 203)" :size="30"> 部门 </a-avatar>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<!--部门下拉内容-->
|
||||
<a-select v-model:value="departSelected" placeholder="请选择登录部门" :class="{ 'valid-error': validate_status1 == 'error' }">
|
||||
<template #suffixIcon>
|
||||
<Icon icon="ant-design:gold-outline" />
|
||||
</template>
|
||||
<template v-for="depart in departList" :key="depart.orgCode">
|
||||
<a-select-option :value="depart.orgCode">{{ depart.departName }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="close">关闭</a-button>
|
||||
<a-button @click="handleSubmit" type="primary">确认</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, unref } from 'vue';
|
||||
import { Avatar } from 'ant-design-vue';
|
||||
import { BasicModal } from '/@/components/Modal';
|
||||
import { getUserDeparts, selectDepart } from '/@/views/system/depart/depart.api';
|
||||
import { getUserTenants } from '/@/views/system/tenant/tenant.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { createMessage, notification } = useMessage();
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '部门选择' },
|
||||
closable: { type: Boolean, default: false },
|
||||
username: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const layout = {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const config = {
|
||||
maskClosable: false,
|
||||
closable: false,
|
||||
canFullscreen: false,
|
||||
width: '500px',
|
||||
minHeight: 20,
|
||||
maxHeight: 20,
|
||||
};
|
||||
const currTitle = ref('');
|
||||
|
||||
const isMultiTenant = ref(false);
|
||||
const currentTenantName = ref('');
|
||||
const tenantSelected = ref();
|
||||
const tenantList = ref([]);
|
||||
const validate_status = ref('');
|
||||
|
||||
const isMultiDepart = ref(false);
|
||||
const currentDepartName = ref('');
|
||||
const departSelected = ref('');
|
||||
const departList = ref([]);
|
||||
const validate_status1 = ref('');
|
||||
//弹窗显隐
|
||||
const visible = ref(false);
|
||||
/**
|
||||
* 弹窗打开前处理
|
||||
*/
|
||||
async function show() {
|
||||
//加载部门
|
||||
await loadDepartList();
|
||||
//加载租户
|
||||
await loadTenantList();
|
||||
//标题配置
|
||||
if (unref(isMultiTenant) && unref(isMultiDepart)) {
|
||||
currTitle.value = '切换租户和部门';
|
||||
} else if (unref(isMultiTenant)) {
|
||||
currTitle.value =
|
||||
unref(currentTenantName) && unref(currentTenantName).length > 0 ? `租户切换(当前租户 :${unref(currentTenantName)})` : props.title;
|
||||
} else if (unref(isMultiDepart)) {
|
||||
currTitle.value =
|
||||
unref(currentDepartName) && unref(currentDepartName).length > 0 ? `部门切换(当前部门 :${unref(currentDepartName)})` : props.title;
|
||||
}
|
||||
//model显隐
|
||||
if (unref(isMultiTenant) || unref(isMultiDepart)) {
|
||||
visible.value = true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
*加载部门信息
|
||||
*/
|
||||
async function loadDepartList() {
|
||||
const result = await getUserDeparts();
|
||||
if (!result.list || result.list.length == 0) {
|
||||
return;
|
||||
}
|
||||
let currentDepart = result.list.filter((item) => item.orgCode == result.orgCode);
|
||||
departList.value = result.list;
|
||||
departSelected.value = currentDepart && currentDepart.length > 0 ? result.orgCode : '';
|
||||
currentDepartName.value = currentDepart && currentDepart.length > 0 ? currentDepart[0].departName : '';
|
||||
isMultiDepart.value = true;
|
||||
}
|
||||
/**
|
||||
*加载租户信息
|
||||
*/
|
||||
async function loadTenantList() {
|
||||
const result = await getUserTenants();
|
||||
if (!result.list || result.list.length == 0) {
|
||||
return;
|
||||
}
|
||||
let tenantId = userStore.getTenant;
|
||||
let currentTenant = result.list.filter((item) => item.id == tenantId);
|
||||
currentTenantName.value = currentTenant && currentTenant.length > 0 ? currentTenant[0].name : '';
|
||||
tenantList.value = result.list;
|
||||
tenantSelected.value = tenantId;
|
||||
isMultiTenant.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交数据
|
||||
*/
|
||||
async function handleSubmit() {
|
||||
if (unref(isMultiTenant) && unref(tenantSelected)==null) {
|
||||
validate_status.value = 'error';
|
||||
return false;
|
||||
}
|
||||
if (unref(isMultiDepart) && !unref(departSelected)) {
|
||||
validate_status1.value = 'error';
|
||||
return false;
|
||||
}
|
||||
departResolve()
|
||||
.then(() => {
|
||||
if (unref(isMultiTenant)) {
|
||||
userStore.setTenant(unref(tenantSelected));
|
||||
}
|
||||
createMessage.success('切换成功');
|
||||
|
||||
//切换租户后要刷新首页
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('登录选择出现问题', e);
|
||||
})
|
||||
.finally(() => {
|
||||
if (unref(isMultiTenant)) {
|
||||
userStore.setTenant(unref(tenantSelected));
|
||||
}
|
||||
close();
|
||||
});
|
||||
}
|
||||
/**
|
||||
*切换选择部门
|
||||
*/
|
||||
function departResolve() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!unref(isMultiDepart)) {
|
||||
resolve();
|
||||
} else {
|
||||
const result = await selectDepart({
|
||||
username: userStore.getUserInfo.username,
|
||||
orgCode: unref(departSelected),
|
||||
loginTenantId: unref(tenantSelected),
|
||||
});
|
||||
if (result.userInfo) {
|
||||
const userInfo = result.userInfo;
|
||||
userStore.setUserInfo(userInfo);
|
||||
resolve();
|
||||
} else {
|
||||
requestFailed(result);
|
||||
userStore.logout();
|
||||
reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 请求失败处理
|
||||
*/
|
||||
function requestFailed(err) {
|
||||
notification.error({
|
||||
message: '登录失败',
|
||||
description: ((err.response || {}).data || {}).message || err.message || '请求出现错误,请稍后再试',
|
||||
duration: 4,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 关闭model
|
||||
*/
|
||||
function close() {
|
||||
departClear();
|
||||
}
|
||||
|
||||
/**
|
||||
*初始化数据
|
||||
*/
|
||||
function departClear() {
|
||||
currTitle.value = '';
|
||||
|
||||
isMultiTenant.value = false;
|
||||
currentTenantName.value = '';
|
||||
tenantSelected.value = '';
|
||||
tenantList.value = [];
|
||||
validate_status.value = '';
|
||||
|
||||
isMultiDepart.value = false;
|
||||
currentDepartName.value = '';
|
||||
departSelected.value = '';
|
||||
departList.value = [];
|
||||
validate_status1.value = '';
|
||||
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听username
|
||||
*/
|
||||
watch(
|
||||
() => props.username,
|
||||
(value) => {
|
||||
value && loadDepartList();
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.loginSelectForm {
|
||||
margin-bottom: -20px;
|
||||
}
|
||||
|
||||
.loginSelectModal {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.valid-error .ant-select-selection__placeholder {
|
||||
color: #f5222d;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<MenuItem :key="itemKey">
|
||||
<span class="flex items-center">
|
||||
<Icon :icon="icon" class="mr-1" />
|
||||
<span>{{ text }}</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Menu } from 'ant-design-vue';
|
||||
|
||||
import { computed, defineComponent, getCurrentInstance } from 'vue';
|
||||
|
||||
import Icon from '/@/components/Icon/index';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DropdownMenuItem',
|
||||
components: { MenuItem: Menu.Item, Icon },
|
||||
props: {
|
||||
key: propTypes.string,
|
||||
text: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
},
|
||||
setup(props) {
|
||||
const instance = getCurrentInstance();
|
||||
const itemKey = computed(() => props.key || instance?.vnode?.props?.key);
|
||||
return { itemKey };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="600px">
|
||||
<BasicForm @register="registerForm" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, unref } from 'vue';
|
||||
import { rules } from '/@/utils/helper/validator';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import BasicForm from '/@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '/@/components/Form/src/hooks/useForm';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useLocaleStore } from '/@/store/modules/locale';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const localeStore = useLocaleStore();
|
||||
const { t } = useI18n();
|
||||
// 声明Emits
|
||||
const emit = defineEmits(['register']);
|
||||
const $message = useMessage();
|
||||
const formRef = ref();
|
||||
const username = ref('');
|
||||
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
const title = ref(t('layout.changePassword.changePassword'));
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, validate, clearValidate }] = useForm({
|
||||
schemas: [
|
||||
{
|
||||
label: t('layout.changePassword.oldPassword'),
|
||||
field: 'oldpassword',
|
||||
component: 'InputPassword',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: t('layout.changePassword.newPassword'),
|
||||
field: 'password',
|
||||
component: 'StrengthMeter',
|
||||
componentProps: {
|
||||
placeholder: t('layout.changePassword.pleaseEnterNewPassword'),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: t('layout.changePassword.pleaseEnterNewPassword'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('layout.changePassword.confirmNewPassword'),
|
||||
field: 'confirmpassword',
|
||||
component: 'InputPassword',
|
||||
dynamicRules: ({ values }) => rules.confirmPassword(values, true),
|
||||
},
|
||||
],
|
||||
showActionButtonGroup: false,
|
||||
wrapperCol: null,
|
||||
labelWidth: localeStore.getLocale == 'zh_CN' ? 100 : 160,
|
||||
});
|
||||
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner();
|
||||
|
||||
//表单提交事件
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
//提交表单
|
||||
let params = Object.assign({ username: unref(username) }, values);
|
||||
defHttp.put({ url: '/sys/user/updatePassword', params }, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
$message.createMessage.success(res.message);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
} else {
|
||||
$message.createMessage.warning(res.message);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function show(name) {
|
||||
if (!name) {
|
||||
$message.createMessage.warning('当前系统无登录用户!');
|
||||
return;
|
||||
} else {
|
||||
username.value = name;
|
||||
await setModalProps({ visible: true });
|
||||
await resetFields();
|
||||
await clearValidate();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
title,
|
||||
show,
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
|
||||
<span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex">
|
||||
<img :class="`${prefixCls}__header`" :src="getAvatarUrl" />
|
||||
<span :class="`${prefixCls}__info hidden md:block`">
|
||||
<span :class="`${prefixCls}__name `" class="truncate">
|
||||
{{ getUserInfo.realname }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="ion:document-text-outline" v-if="getShowDoc" />
|
||||
<MenuDivider v-if="getShowDoc" />
|
||||
<MenuItem key="account" :text="t('layout.header.dropdownItemSwitchAccount')" icon="ant-design:setting-outlined" />
|
||||
<MenuItem key="password" :text="t('layout.header.dropdownItemSwitchPassword')" icon="ant-design:edit-outlined" />
|
||||
<MenuItem key="depart" :text="t('layout.header.dropdownItemSwitchDepart')" icon="ant-design:cluster-outlined" />
|
||||
<MenuItem key="cache" :text="t('layout.header.dropdownItemRefreshCache')" icon="ion:sync-outline" />
|
||||
<!-- <MenuItem
|
||||
v-if="getUseLockPage"
|
||||
key="lock"
|
||||
:text="t('layout.header.tooltipLock')"
|
||||
icon="ion:lock-closed-outline"
|
||||
/>-->
|
||||
<MenuItem key="logout" :text="t('layout.header.dropdownItemLoginOut')" icon="ion:power-outline" />
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<LockAction v-if="lockActionVisible" ref="lockActionRef" @register="register" />
|
||||
<DepartSelect ref="loginSelectRef" />
|
||||
<UpdatePassword v-if="passwordVisible" ref="updatePasswordRef" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// components
|
||||
import { Dropdown, Menu } from 'ant-design-vue';
|
||||
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
|
||||
import { SITE_URL } from '/@/settings/siteSetting';
|
||||
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { useMessage } from '/src/hooks/web/useMessage';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import headerImg from '/@/assets/images/header.jpg';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { openWindow } from '/@/utils';
|
||||
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
|
||||
import { DB_DICT_DATA_KEY } from '/src/enums/cacheEnum';
|
||||
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { getRefPromise } from '/@/utils/index';
|
||||
|
||||
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart';
|
||||
const { createMessage } = useMessage();
|
||||
export default defineComponent({
|
||||
name: 'UserDropdown',
|
||||
components: {
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
|
||||
MenuDivider: Menu.Divider,
|
||||
LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')),
|
||||
DepartSelect: createAsyncComponent(() => import('./DepartSelect.vue')),
|
||||
UpdatePassword: createAsyncComponent(() => import('./UpdatePassword.vue')),
|
||||
},
|
||||
props: {
|
||||
theme: propTypes.oneOf(['dark', 'light']),
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('header-user-dropdown');
|
||||
const { t } = useI18n();
|
||||
const { getShowDoc, getUseLockPage } = useHeaderSetting();
|
||||
const userStore = useUserStore();
|
||||
const go = useGo();
|
||||
const passwordVisible = ref(false);
|
||||
const lockActionVisible = ref(false);
|
||||
const lockActionRef = ref(null);
|
||||
|
||||
const getUserInfo = computed(() => {
|
||||
const { realname = '', avatar, desc } = userStore.getUserInfo || {};
|
||||
return { realname, avatar: avatar || headerImg, desc };
|
||||
});
|
||||
|
||||
const getAvatarUrl = computed(() => {
|
||||
let { avatar } = getUserInfo.value;
|
||||
if (avatar == headerImg) {
|
||||
return avatar;
|
||||
} else {
|
||||
return getFileAccessHttpUrl(avatar);
|
||||
}
|
||||
});
|
||||
|
||||
const [register, { openModal }] = useModal();
|
||||
/**
|
||||
* 多部门弹窗逻辑
|
||||
*/
|
||||
const loginSelectRef = ref();
|
||||
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
async function handleLock() {
|
||||
await getRefPromise(lockActionRef);
|
||||
openModal(true);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
// login out
|
||||
function handleLoginOut() {
|
||||
userStore.confirmLoginOut();
|
||||
}
|
||||
|
||||
// open doc
|
||||
function openDoc() {
|
||||
openWindow(SITE_URL);
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
async function clearCache() {
|
||||
const result = await refreshCache();
|
||||
if (result.success) {
|
||||
const res = await queryAllDictItems();
|
||||
removeAuthCache(DB_DICT_DATA_KEY);
|
||||
setAuthCache(DB_DICT_DATA_KEY, res.result);
|
||||
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
createMessage.success(t('layout.header.refreshCacheComplete'));
|
||||
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
} else {
|
||||
// update-begin--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
createMessage.error(t('layout.header.refreshCacheFailure'));
|
||||
// update-end--author:liaozhiyang---date:20240124---for:【QQYUN-7970】国际化
|
||||
}
|
||||
}
|
||||
// 切换部门
|
||||
function updateCurrentDepart() {
|
||||
loginSelectRef.value.show();
|
||||
}
|
||||
// 修改密码
|
||||
const updatePasswordRef = ref();
|
||||
// update-begin--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
async function updatePassword() {
|
||||
passwordVisible.value = true;
|
||||
await getRefPromise(updatePasswordRef);
|
||||
updatePasswordRef.value.show(userStore.getUserInfo.username);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230901---for:【QQYUN-6333】空路由问题—首次访问资源太大
|
||||
function handleMenuClick(e: { key: MenuEvent }) {
|
||||
switch (e.key) {
|
||||
case 'logout':
|
||||
handleLoginOut();
|
||||
break;
|
||||
case 'doc':
|
||||
openDoc();
|
||||
break;
|
||||
case 'lock':
|
||||
handleLock();
|
||||
break;
|
||||
case 'cache':
|
||||
clearCache();
|
||||
break;
|
||||
case 'depart':
|
||||
updateCurrentDepart();
|
||||
break;
|
||||
case 'password':
|
||||
updatePassword();
|
||||
break;
|
||||
case 'account':
|
||||
//update-begin---author:wangshuai ---date:20221125 for:进入用户设置页面------------
|
||||
go(`/system/usersetting`);
|
||||
//update-end---author:wangshuai ---date:20221125 for:进入用户设置页面--------------
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
t,
|
||||
getUserInfo,
|
||||
getAvatarUrl,
|
||||
handleMenuClick,
|
||||
getShowDoc,
|
||||
register,
|
||||
getUseLockPage,
|
||||
loginSelectRef,
|
||||
updatePasswordRef,
|
||||
passwordVisible,
|
||||
lockActionVisible,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-header-user-dropdown';
|
||||
|
||||
.@{prefix-cls} {
|
||||
height: @header-height;
|
||||
padding: 0 0 0 10px;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
&:hover {
|
||||
background-color: @header-dark-bg-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--light {
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__name {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
.@{prefix-cls}__desc {
|
||||
color: @header-light-desc-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-dropdown-overlay {
|
||||
// update-begin--author:liaozhiyang---date:20231226---for:【QQYUN-7512】顶部账号划过首次弹出时位置会变更一下
|
||||
width: 160px;
|
||||
// update-end--author:liaozhiyang---date:20231226---for:【QQYUN-7512】顶部账号划过首次弹出时位置会变更一下
|
||||
.ant-dropdown-menu-item {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
jeecgboot-vue3/src/layouts/default/header/index.less
Normal file
200
jeecgboot-vue3/src/layouts/default/header/index.less
Normal file
@ -0,0 +1,200 @@
|
||||
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
|
||||
@header-prefix-cls: ~'@{namespace}-layout-header';
|
||||
@breadcrumb-prefix-cls: ~'@{namespace}-layout-breadcrumb';
|
||||
@logo-prefix-cls: ~'@{namespace}-app-logo';
|
||||
|
||||
.@{header-prefix-cls} {
|
||||
display: flex;
|
||||
height: @header-height;
|
||||
padding: 0;
|
||||
margin-left: -1px;
|
||||
line-height: @header-height;
|
||||
color: @white;
|
||||
background-color: @white;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&--mobile {
|
||||
.@{breadcrumb-prefix-cls},
|
||||
.error-action,
|
||||
.notify-item,
|
||||
.lock-item,
|
||||
.fullscreen-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.@{logo-prefix-cls} {
|
||||
min-width: unset;
|
||||
padding-right: 0;
|
||||
|
||||
&__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.@{header-trigger-prefix-cls} {
|
||||
padding: 0 4px 0 8px !important;
|
||||
}
|
||||
|
||||
.@{header-prefix-cls}-action {
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&--fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: @layout-header-fixed-z-index;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-logo {
|
||||
height: @header-height;
|
||||
min-width: 192px;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
|
||||
img {
|
||||
width: @logo-width;
|
||||
height: @logo-width;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
.@{header-trigger-prefix-cls} {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 1px 10px 0 10px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
.anticon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&.light {
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
&:hover {
|
||||
background-color: @header-dark-bg-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-menu {
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: flex;
|
||||
min-width: 180px;
|
||||
// padding-right: 12px;
|
||||
align-items: center;
|
||||
|
||||
&__item {
|
||||
display: flex !important;
|
||||
height: @header-height;
|
||||
padding: 0 2px;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
.ant-badge {
|
||||
height: @header-height;
|
||||
line-height: @header-height;
|
||||
}
|
||||
|
||||
.ant-badge-dot {
|
||||
top: 10px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
span[role='img'] {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&--light {
|
||||
background-color: @white !important;
|
||||
border-bottom: 1px solid @header-light-bottom-border-color;
|
||||
border-left: 1px solid @header-light-bottom-border-color;
|
||||
|
||||
.@{header-prefix-cls}-logo {
|
||||
color: @text-color-base;
|
||||
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{header-prefix-cls}-action {
|
||||
&__item {
|
||||
color: @text-color-base;
|
||||
|
||||
.app-iconify {
|
||||
padding: 0 10px;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @header-light-bg-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon,
|
||||
span[role='img'] {
|
||||
color: @text-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background-color: @header-dark-bg-color !important;
|
||||
// border-bottom: 1px solid @border-color-base;
|
||||
border-left: 1px solid @border-color-base;
|
||||
|
||||
.@{header-prefix-cls}-logo {
|
||||
&:hover {
|
||||
background-color: @header-dark-bg-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.@{header-prefix-cls}-action {
|
||||
&__item {
|
||||
.app-iconify {
|
||||
padding: 0 10px;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.ant-badge {
|
||||
span {
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @header-dark-bg-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
260
jeecgboot-vue3/src/layouts/default/header/index.vue
Normal file
260
jeecgboot-vue3/src/layouts/default/header/index.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<Header :class="getHeaderClass">
|
||||
<!-- left start -->
|
||||
<div :class="`${prefixCls}-left`">
|
||||
<!-- logo -->
|
||||
<AppLogo v-if="getShowHeaderLogo || getIsMobile" :class="`${prefixCls}-logo`" :theme="getHeaderTheme" :style="getLogoWidth" />
|
||||
<LayoutTrigger
|
||||
v-if="(getShowContent && getShowHeaderTrigger && !getSplit && !getIsMixSidebar) || getIsMobile"
|
||||
:theme="getHeaderTheme"
|
||||
:sider="false"
|
||||
/>
|
||||
<LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" />
|
||||
<!-- 欢迎语 -->
|
||||
<span v-if="getShowContent && getShowBreadTitle && !getIsMobile" :class="[prefixCls, `${prefixCls}--${getHeaderTheme}`,'headerIntroductionClass']"> {{t('layout.header.welcomeIn')}} {{ title }} </span>
|
||||
</div>
|
||||
<!-- left end -->
|
||||
|
||||
<!-- menu start -->
|
||||
<div :class="`${prefixCls}-menu`" v-if="getShowTopMenu && !getIsMobile">
|
||||
<LayoutMenu :isHorizontal="true" :theme="getHeaderTheme" :splitType="getSplitType" :menuMode="getMenuMode" />
|
||||
</div>
|
||||
<!-- menu-end -->
|
||||
|
||||
<!-- action -->
|
||||
<div :class="`${prefixCls}-action`">
|
||||
<AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
|
||||
|
||||
<ErrorAction v-if="getUseErrorHandle" :class="`${prefixCls}-action__item error-action`" />
|
||||
|
||||
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
|
||||
|
||||
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
|
||||
|
||||
<LockScreen v-if="getUseLockPage" />
|
||||
|
||||
<AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
|
||||
|
||||
<UserDropDown :theme="getHeaderTheme" />
|
||||
|
||||
<SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
|
||||
<!-- ai助手 -->
|
||||
<!--<Aide></Aide>-->
|
||||
</div>
|
||||
</Header>
|
||||
<LoginSelect ref="loginSelectRef" @success="loginSelectOk"></LoginSelect>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, computed, ref, onMounted, toRaw } from 'vue';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
import { Layout } from 'ant-design-vue';
|
||||
import { AppLogo } from '/@/components/Application';
|
||||
import LayoutMenu from '../menu/index.vue';
|
||||
import LayoutTrigger from '../trigger/index.vue';
|
||||
|
||||
import { AppSearch } from '/@/components/Application';
|
||||
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
|
||||
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
|
||||
import { AppLocalePicker } from '/@/components/Application';
|
||||
|
||||
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { useLocale } from '/@/locales/useLocale';
|
||||
|
||||
import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import Aide from "@/views/dashboard/ai/components/aide/index.vue"
|
||||
const { t } = useI18n();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutHeader',
|
||||
components: {
|
||||
Header: Layout.Header,
|
||||
AppLogo,
|
||||
LayoutTrigger,
|
||||
LayoutBreadcrumb,
|
||||
LayoutMenu,
|
||||
UserDropDown,
|
||||
AppLocalePicker,
|
||||
FullScreen,
|
||||
Notify,
|
||||
AppSearch,
|
||||
ErrorAction,
|
||||
LockScreen,
|
||||
LoginSelect,
|
||||
SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
|
||||
loading: true,
|
||||
}),
|
||||
Aide
|
||||
},
|
||||
props: {
|
||||
fixed: propTypes.bool,
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('layout-header');
|
||||
const userStore = useUserStore();
|
||||
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
|
||||
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
|
||||
const { title } = useGlobSetting();
|
||||
|
||||
const {
|
||||
getHeaderTheme,
|
||||
getShowFullScreen,
|
||||
getShowNotice,
|
||||
getShowContent,
|
||||
getShowBread,
|
||||
getShowHeaderLogo,
|
||||
getShowHeader,
|
||||
getShowSearch,
|
||||
getUseLockPage,
|
||||
getShowBreadTitle,
|
||||
} = useHeaderSetting();
|
||||
|
||||
const { getShowLocalePicker } = useLocale();
|
||||
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const getHeaderClass = computed(() => {
|
||||
const theme = unref(getHeaderTheme);
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--fixed`]: props.fixed,
|
||||
[`${prefixCls}--mobile`]: unref(getIsMobile),
|
||||
[`${prefixCls}--${theme}`]: theme,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const getShowSetting = computed(() => {
|
||||
if (!unref(getShowSettingButton)) {
|
||||
return false;
|
||||
}
|
||||
const settingButtonPosition = unref(getSettingButtonPosition);
|
||||
|
||||
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
|
||||
return unref(getShowHeader);
|
||||
}
|
||||
return settingButtonPosition === SettingButtonPositionEnum.HEADER;
|
||||
});
|
||||
|
||||
const getLogoWidth = computed(() => {
|
||||
if (!unref(getIsMixMode) || unref(getIsMobile)) {
|
||||
return {};
|
||||
}
|
||||
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
|
||||
return { width: `${width}px` };
|
||||
});
|
||||
|
||||
const getSplitType = computed(() => {
|
||||
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
|
||||
});
|
||||
|
||||
const getMenuMode = computed(() => {
|
||||
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
|
||||
});
|
||||
|
||||
/**
|
||||
* 首页多租户部门弹窗逻辑
|
||||
*/
|
||||
const loginSelectRef = ref();
|
||||
|
||||
function showLoginSelect() {
|
||||
//update-begin---author:liusq Date:20220101 for:判断登录进来是否需要弹窗选择租户----
|
||||
//判断是否是登陆进来
|
||||
const loginInfo = toRaw(userStore.getLoginInfo) || {};
|
||||
if (!!loginInfo.isLogin) {
|
||||
loginSelectRef.value.show(loginInfo);
|
||||
}
|
||||
//update-end---author:liusq Date:20220101 for:判断登录进来是否需要弹窗选择租户----
|
||||
}
|
||||
|
||||
function loginSelectOk() {
|
||||
console.log('成功。。。。。');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
showLoginSelect();
|
||||
});
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
getHeaderClass,
|
||||
getShowHeaderLogo,
|
||||
getHeaderTheme,
|
||||
getShowHeaderTrigger,
|
||||
getIsMobile,
|
||||
getShowBreadTitle,
|
||||
getShowBread,
|
||||
getShowContent,
|
||||
getSplitType,
|
||||
getSplit,
|
||||
getMenuMode,
|
||||
getShowTopMenu,
|
||||
getShowLocalePicker,
|
||||
getShowFullScreen,
|
||||
getShowNotice,
|
||||
getUseErrorHandle,
|
||||
getLogoWidth,
|
||||
getIsMixSidebar,
|
||||
getShowSettingButton,
|
||||
getShowSetting,
|
||||
getShowSearch,
|
||||
getUseLockPage,
|
||||
loginSelectOk,
|
||||
loginSelectRef,
|
||||
title,
|
||||
t
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './index.less';
|
||||
//update-begin---author:scott ---date:2022-09-30 for:默认隐藏顶部菜单面包屑-----------
|
||||
//顶部欢迎语展示样式
|
||||
@prefix-cls: ~'@{namespace}-layout-header';
|
||||
|
||||
.ant-layout .@{prefix-cls} {
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
// update-begin--author:liaozhiyang---date:20240407---for:【QQYUN-8762】顶栏高度
|
||||
height: @header-height;
|
||||
// update-end--author:liaozhiyang---date:20240407---for:【QQYUN-8762】顶栏高度
|
||||
align-items: center;
|
||||
|
||||
.headerIntroductionClass {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
border-bottom: 0px;
|
||||
border-left: 0px;
|
||||
}
|
||||
|
||||
&--light {
|
||||
.headerIntroductionClass {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&--dark {
|
||||
.headerIntroductionClass {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
.anticon, .truncate {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
//update-end---author:scott ---date::2022-09-30 for:默认隐藏顶部菜单面包屑--------------
|
||||
}
|
||||
</style>
|
||||
92
jeecgboot-vue3/src/layouts/default/index.vue
Normal file
92
jeecgboot-vue3/src/layouts/default/index.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<Layout :class="prefixCls" v-bind="lockEvents">
|
||||
<LayoutFeatures />
|
||||
<LayoutHeader fixed v-if="getShowFullHeaderRef" />
|
||||
<Layout :class="[layoutClass]">
|
||||
<LayoutSideBar v-if="getShowSidebar || getIsMobile" />
|
||||
<Layout :class="`${prefixCls}-main`">
|
||||
<LayoutMultipleHeader />
|
||||
<LayoutContent />
|
||||
<LayoutFooter />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, unref, ref } from 'vue';
|
||||
import { Layout } from 'ant-design-vue';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
import LayoutHeader from './header/index.vue';
|
||||
import LayoutContent from './content/index.vue';
|
||||
import LayoutSideBar from './sider/index.vue';
|
||||
import LayoutMultipleHeader from './header/MultipleHeader.vue';
|
||||
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useLockPage } from '/@/hooks/web/useLockPage';
|
||||
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DefaultLayout',
|
||||
components: {
|
||||
LayoutFeatures: createAsyncComponent(() => import('/@/layouts/default/feature/index.vue')),
|
||||
LayoutFooter: createAsyncComponent(() => import('/@/layouts/default/footer/index.vue')),
|
||||
LayoutHeader,
|
||||
LayoutContent,
|
||||
LayoutSideBar,
|
||||
LayoutMultipleHeader,
|
||||
Layout,
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('default-layout');
|
||||
const { getIsMobile } = useAppInject();
|
||||
const { getShowFullHeaderRef } = useHeaderSetting();
|
||||
const { getShowSidebar, getIsMixSidebar, getShowMenu } = useMenuSetting();
|
||||
|
||||
// Create a lock screen monitor
|
||||
const lockEvents = useLockPage();
|
||||
|
||||
const layoutClass = computed(() => {
|
||||
let cls: string[] = ['ant-layout'];
|
||||
if (unref(getIsMixSidebar) || unref(getShowMenu)) {
|
||||
cls.push('ant-layout-has-sider');
|
||||
}
|
||||
return cls;
|
||||
});
|
||||
|
||||
return {
|
||||
getShowFullHeaderRef,
|
||||
getShowSidebar,
|
||||
prefixCls,
|
||||
getIsMobile,
|
||||
getIsMixSidebar,
|
||||
layoutClass,
|
||||
lockEvents,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-default-layout';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
background-color: @content-bg;
|
||||
flex-direction: column;
|
||||
|
||||
> .ant-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 100%;
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
jeecgboot-vue3/src/layouts/default/menu/index.vue
Normal file
196
jeecgboot-vue3/src/layouts/default/menu/index.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<script lang="tsx">
|
||||
import type { PropType, CSSProperties } from 'vue';
|
||||
|
||||
import { computed, defineComponent, unref, toRef } from 'vue';
|
||||
import { BasicMenu } from '/@/components/Menu';
|
||||
import { SimpleMenu } from '/@/components/SimpleMenu';
|
||||
import { AppLogo } from '/@/components/Application';
|
||||
|
||||
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { ScrollContainer } from '/@/components/Container';
|
||||
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { useSplitMenu } from './useLayoutMenu';
|
||||
import { openWindow } from '/@/utils';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { isUrl } from '/@/utils/is';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useLocaleStore } from '/@/store/modules/locale';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutMenu',
|
||||
props: {
|
||||
theme: propTypes.oneOf(['light', 'dark']),
|
||||
|
||||
splitType: {
|
||||
type: Number as PropType<MenuSplitTyeEnum>,
|
||||
default: MenuSplitTyeEnum.NONE,
|
||||
},
|
||||
|
||||
isHorizontal: propTypes.bool,
|
||||
// menu Mode
|
||||
menuMode: {
|
||||
type: [String] as PropType<Nullable<MenuModeEnum>>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const go = useGo();
|
||||
|
||||
const {
|
||||
getMenuMode,
|
||||
getMenuType,
|
||||
getMenuTheme,
|
||||
getCollapsed,
|
||||
getCollapsedShowTitle,
|
||||
getAccordion,
|
||||
getIsHorizontal,
|
||||
getIsSidebarType,
|
||||
getSplit,
|
||||
} = useMenuSetting();
|
||||
const { getShowLogo } = useRootSetting();
|
||||
|
||||
const { prefixCls } = useDesign('layout-menu');
|
||||
|
||||
const { menusRef } = useSplitMenu(toRef(props, 'splitType'));
|
||||
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const getComputedMenuMode = computed(() => (unref(getIsMobile) ? MenuModeEnum.INLINE : props.menuMode || unref(getMenuMode)));
|
||||
|
||||
const getComputedMenuTheme = computed(() => props.theme || unref(getMenuTheme));
|
||||
|
||||
const getIsShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
|
||||
|
||||
const getUseScroll = computed(() => {
|
||||
return (
|
||||
!unref(getIsHorizontal) &&
|
||||
(unref(getIsSidebarType) || props.splitType === MenuSplitTyeEnum.LEFT || props.splitType === MenuSplitTyeEnum.NONE)
|
||||
);
|
||||
});
|
||||
|
||||
const getWrapperStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: `calc(100% - ${unref(getIsShowLogo) ? '48px' : '0px'})`,
|
||||
};
|
||||
});
|
||||
|
||||
const getLogoClass = computed(() => {
|
||||
return [
|
||||
`${prefixCls}-logo`,
|
||||
unref(getComputedMenuTheme),
|
||||
{
|
||||
[`${prefixCls}--mobile`]: unref(getIsMobile),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const getCommonProps = computed(() => {
|
||||
const menus = unref(menusRef);
|
||||
return {
|
||||
menus,
|
||||
beforeClickFn: beforeMenuClickFn,
|
||||
items: menus,
|
||||
theme: unref(getComputedMenuTheme),
|
||||
accordion: unref(getAccordion),
|
||||
collapse: unref(getCollapsed),
|
||||
collapsedShowTitle: unref(getCollapsedShowTitle),
|
||||
onMenuClick: handleMenuClick,
|
||||
};
|
||||
});
|
||||
/**
|
||||
* click menu
|
||||
* @param menu
|
||||
*/
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
|
||||
const localeStore = useLocaleStore();
|
||||
function handleMenuClick(path: string, item) {
|
||||
if (item) {
|
||||
localeStore.setPathTitle(path, item.title || '');
|
||||
}
|
||||
go(path);
|
||||
}
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
|
||||
|
||||
/**
|
||||
* before click menu
|
||||
* @param menu
|
||||
*/
|
||||
async function beforeMenuClickFn(path: string) {
|
||||
if (!isUrl(path)) {
|
||||
return true;
|
||||
}
|
||||
openWindow(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!unref(getIsShowLogo) && !unref(getIsMobile)) return null;
|
||||
|
||||
return <AppLogo showTitle={!unref(getCollapsed)} class={unref(getLogoClass)} theme={unref(getComputedMenuTheme)} />;
|
||||
}
|
||||
|
||||
function renderMenu() {
|
||||
const { menus, ...menuProps } = unref(getCommonProps);
|
||||
// console.log(menus);
|
||||
if (!menus || !menus.length) return null;
|
||||
return !props.isHorizontal ? (
|
||||
<SimpleMenu {...menuProps} isSplitMenu={unref(getSplit)} items={menus} />
|
||||
) : (
|
||||
<BasicMenu
|
||||
{...(menuProps as any)}
|
||||
isHorizontal={props.isHorizontal}
|
||||
type={unref(getMenuType)}
|
||||
showLogo={unref(getIsShowLogo)}
|
||||
mode={unref(getComputedMenuMode as any)}
|
||||
items={menus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
{renderHeader()}
|
||||
{unref(getUseScroll) ? <ScrollContainer style={unref(getWrapperStyle)}>{() => renderMenu()}</ScrollContainer> : renderMenu()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
// update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5872】菜单优化,上下滚动条去掉
|
||||
.scroll-container :deep(.scrollbar__bar) {
|
||||
display: none;
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5872】菜单优化,上下滚动条去掉
|
||||
</style>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-menu';
|
||||
@logo-prefix-cls: ~'@{namespace}-app-logo';
|
||||
|
||||
.@{prefix-cls} {
|
||||
&-logo {
|
||||
height: @header-height;
|
||||
padding: 10px 4px 10px 10px;
|
||||
|
||||
img {
|
||||
width: @logo-width;
|
||||
height: @logo-width;
|
||||
}
|
||||
}
|
||||
|
||||
&--mobile {
|
||||
.@{logo-prefix-cls} {
|
||||
&__title {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
105
jeecgboot-vue3/src/layouts/default/menu/useLayoutMenu.ts
Normal file
105
jeecgboot-vue3/src/layouts/default/menu/useLayoutMenu.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import type { Menu } from '/@/router/types';
|
||||
import type { Ref } from 'vue';
|
||||
import { watch, unref, ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { getChildrenMenus, getCurrentParentPath, getMenus, getShallowMenus } from '/@/router/menus';
|
||||
import { usePermissionStore } from '/@/store/modules/permission';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
|
||||
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
// Menu array
|
||||
const menusRef = ref<Menu[]>([]);
|
||||
const { currentRoute } = useRouter();
|
||||
const { getIsMobile } = useAppInject();
|
||||
const permissionStore = usePermissionStore();
|
||||
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
|
||||
|
||||
const throttleHandleSplitLeftMenu = useThrottleFn(handleSplitLeftMenu, 50);
|
||||
|
||||
const splitNotLeft = computed(() => unref(splitType) !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontal));
|
||||
|
||||
const getSplitLeft = computed(() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT);
|
||||
|
||||
const getSpiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
|
||||
|
||||
const normalType = computed(() => {
|
||||
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => unref(currentRoute).path, () => unref(splitType)],
|
||||
async ([path]: [string, MenuSplitTyeEnum]) => {
|
||||
if (unref(splitNotLeft) || unref(getIsMobile)) return;
|
||||
|
||||
const { meta } = unref(currentRoute);
|
||||
const currentActiveMenu = meta.currentActiveMenu as string;
|
||||
let parentPath = await getCurrentParentPath(path);
|
||||
if (!parentPath) {
|
||||
parentPath = await getCurrentParentPath(currentActiveMenu);
|
||||
}
|
||||
parentPath && throttleHandleSplitLeftMenu(parentPath);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Menu changes
|
||||
watch(
|
||||
[() => permissionStore.getLastBuildMenuTime, () => permissionStore.getBackMenuList],
|
||||
() => {
|
||||
genMenus();
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// split Menu changes
|
||||
watch(
|
||||
() => getSplit.value,
|
||||
() => {
|
||||
if (unref(splitNotLeft)) return;
|
||||
genMenus();
|
||||
}
|
||||
);
|
||||
|
||||
// Handle left menu split
|
||||
async function handleSplitLeftMenu(parentPath: string) {
|
||||
if (unref(getSplitLeft) || unref(getIsMobile)) return;
|
||||
|
||||
// spilt mode left
|
||||
const children = await getChildrenMenus(parentPath);
|
||||
|
||||
if (!children || !children.length) {
|
||||
setMenuSetting({ hidden: true });
|
||||
menusRef.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
setMenuSetting({ hidden: false });
|
||||
menusRef.value = children;
|
||||
}
|
||||
|
||||
// get menus
|
||||
async function genMenus() {
|
||||
// normal mode
|
||||
if (unref(normalType) || unref(getIsMobile)) {
|
||||
menusRef.value = await getMenus();
|
||||
return;
|
||||
}
|
||||
|
||||
// split-top
|
||||
if (unref(getSpiltTop)) {
|
||||
const shallowMenus = await getShallowMenus();
|
||||
|
||||
menusRef.value = shallowMenus;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return { menusRef };
|
||||
}
|
||||
364
jeecgboot-vue3/src/layouts/default/setting/SettingDrawer.tsx
Normal file
364
jeecgboot-vue3/src/layouts/default/setting/SettingDrawer.tsx
Normal file
@ -0,0 +1,364 @@
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { BasicDrawer } from '/@/components/Drawer/index';
|
||||
import { Divider } from 'ant-design-vue';
|
||||
import { TypePicker, ThemeColorPicker, SettingFooter, SwitchItem, SelectItem, InputNumberItem } from './components';
|
||||
|
||||
import { AppDarkModeToggle } from '/@/components/Application';
|
||||
|
||||
import { MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
import { layoutHandler } from './handler';
|
||||
|
||||
import {
|
||||
HandlerEnum,
|
||||
contentModeOptions,
|
||||
topMenuAlignOptions,
|
||||
getMenuTriggerOptions,
|
||||
routerTransitionOptions,
|
||||
menuTypeList,
|
||||
mixSidebarTriggerOptions,
|
||||
tabsThemeOptions,
|
||||
} from './enum';
|
||||
|
||||
import { HEADER_PRESET_BG_COLOR_LIST, SIDE_BAR_BG_COLOR_LIST, APP_PRESET_COLOR_LIST } from '/@/settings/designSetting';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingDrawer',
|
||||
setup(_, { attrs }) {
|
||||
const {
|
||||
getContentMode,
|
||||
getShowFooter,
|
||||
getShowBreadCrumb,
|
||||
getShowBreadCrumbIcon,
|
||||
getShowLogo,
|
||||
getFullContent,
|
||||
getColorWeak,
|
||||
getGrayMode,
|
||||
getLockTime,
|
||||
getShowDarkModeToggle,
|
||||
getThemeColor,
|
||||
} = useRootSetting();
|
||||
|
||||
const { getOpenPageLoading, getBasicTransition, getEnableTransition, getOpenNProgress } = useTransitionSetting();
|
||||
|
||||
const {
|
||||
getIsHorizontal,
|
||||
getShowMenu,
|
||||
getMenuType,
|
||||
getTrigger,
|
||||
getCollapsedShowTitle,
|
||||
getMenuFixed,
|
||||
getCollapsed,
|
||||
getCanDrag,
|
||||
getTopMenuAlign,
|
||||
getAccordion,
|
||||
getMenuWidth,
|
||||
getMenuBgColor,
|
||||
getIsTopMenu,
|
||||
getSplit,
|
||||
getIsMixSidebar,
|
||||
getCloseMixSidebarOnChange,
|
||||
getMixSideTrigger,
|
||||
getMixSideFixed,
|
||||
} = useMenuSetting();
|
||||
|
||||
const { getShowHeader, getFixed: getHeaderFixed, getHeaderBgColor, getShowSearch } = useHeaderSetting();
|
||||
|
||||
const { getShowMultipleTab, getShowQuick, getShowRedo, getShowFold, getTabsTheme } = useMultipleTabSetting();
|
||||
|
||||
const getShowMenuRef = computed(() => {
|
||||
return unref(getShowMenu) && !unref(getIsHorizontal);
|
||||
});
|
||||
|
||||
const isDev= import.meta.env.DEV
|
||||
|
||||
function renderSidebar() {
|
||||
return (
|
||||
<>
|
||||
<TypePicker
|
||||
menuTypeList={menuTypeList}
|
||||
handler={(item: typeof menuTypeList[0]) => {
|
||||
layoutHandler(HandlerEnum.CHANGE_LAYOUT, {
|
||||
mode: item.mode,
|
||||
type: item.type,
|
||||
split: unref(getIsHorizontal) ? false : undefined,
|
||||
});
|
||||
}}
|
||||
def={unref(getMenuType)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderHeaderTheme() {
|
||||
return <ThemeColorPicker colorList={HEADER_PRESET_BG_COLOR_LIST} def={unref(getHeaderBgColor)} event={HandlerEnum.HEADER_THEME} />;
|
||||
}
|
||||
|
||||
function renderSiderTheme() {
|
||||
return <ThemeColorPicker colorList={SIDE_BAR_BG_COLOR_LIST} def={unref(getMenuBgColor)} event={HandlerEnum.MENU_THEME} />;
|
||||
}
|
||||
|
||||
function renderMainTheme() {
|
||||
return <ThemeColorPicker colorList={APP_PRESET_COLOR_LIST} def={unref(getThemeColor)} event={HandlerEnum.CHANGE_THEME_COLOR} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:
|
||||
*/
|
||||
function renderFeatures() {
|
||||
let triggerDef = unref(getTrigger);
|
||||
|
||||
const triggerOptions = getMenuTriggerOptions(unref(getSplit));
|
||||
const some = triggerOptions.some((item) => item.value === triggerDef);
|
||||
if (!some) {
|
||||
triggerDef = TriggerEnum.FOOTER;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SwitchItem
|
||||
title={t('layout.setting.splitMenu')}
|
||||
event={HandlerEnum.MENU_SPLIT}
|
||||
def={unref(getSplit)}
|
||||
disabled={!unref(getShowMenuRef) || unref(getMenuType) !== MenuTypeEnum.MIX}
|
||||
/>
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.mixSidebarFixed')}*/}
|
||||
{/* event={HandlerEnum.MENU_FIXED_MIX_SIDEBAR}*/}
|
||||
{/* def={unref(getMixSideFixed)}*/}
|
||||
{/* disabled={!unref(getIsMixSidebar)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.closeMixSidebarOnChange')}*/}
|
||||
{/* event={HandlerEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE}*/}
|
||||
{/* def={unref(getCloseMixSidebarOnChange)}*/}
|
||||
{/* disabled={!unref(getIsMixSidebar)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.menuCollapse')}*/}
|
||||
{/* event={HandlerEnum.MENU_COLLAPSED}*/}
|
||||
{/* def={unref(getCollapsed)}*/}
|
||||
{/* disabled={!unref(getShowMenuRef)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.menuSearch')}*/}
|
||||
{/* event={HandlerEnum.HEADER_SEARCH}*/}
|
||||
{/* def={unref(getShowSearch)}*/}
|
||||
{/* disabled={!unref(getShowHeader)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.menuAccordion')}*/}
|
||||
{/* event={HandlerEnum.MENU_ACCORDION}*/}
|
||||
{/* def={unref(getAccordion)}*/}
|
||||
{/* disabled={!unref(getShowMenuRef)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.fixedHeader')}*/}
|
||||
{/* event={HandlerEnum.HEADER_FIXED}*/}
|
||||
{/* def={unref(getHeaderFixed)}*/}
|
||||
{/* disabled={!unref(getShowHeader)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.fixedSideBar')}*/}
|
||||
{/* event={HandlerEnum.MENU_FIXED}*/}
|
||||
{/* def={unref(getMenuFixed)}*/}
|
||||
{/* disabled={!unref(getShowMenuRef) || unref(getIsMixSidebar)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SelectItem*/}
|
||||
{/* title={t('layout.setting.mixSidebarTrigger')}*/}
|
||||
{/* event={HandlerEnum.MENU_TRIGGER_MIX_SIDEBAR}*/}
|
||||
{/* def={unref(getMixSideTrigger)}*/}
|
||||
{/* options={mixSidebarTriggerOptions}*/}
|
||||
{/* disabled={!unref(getIsMixSidebar)}*/}
|
||||
{/*/>*/}
|
||||
<SelectItem title={t('layout.setting.tabsTheme')} event={HandlerEnum.TABS_THEME} def={unref(getTabsTheme)} options={tabsThemeOptions} />
|
||||
<SelectItem
|
||||
title={t('layout.setting.topMenuLayout')}
|
||||
event={HandlerEnum.MENU_TOP_ALIGN}
|
||||
def={unref(getTopMenuAlign)}
|
||||
options={topMenuAlignOptions}
|
||||
disabled={!unref(getShowHeader) || unref(getSplit) || (!unref(getIsTopMenu) && !unref(getSplit)) || unref(getIsMixSidebar)}
|
||||
/>
|
||||
<SelectItem
|
||||
title={t('layout.setting.menuCollapseButton')}
|
||||
event={HandlerEnum.MENU_TRIGGER}
|
||||
def={triggerDef}
|
||||
options={triggerOptions}
|
||||
disabled={!unref(getShowMenuRef) || unref(getIsMixSidebar)}
|
||||
/>
|
||||
{
|
||||
isDev && <SelectItem
|
||||
title={t('layout.setting.contentMode')}
|
||||
event={HandlerEnum.CONTENT_MODE}
|
||||
def={unref(getContentMode)}
|
||||
options={contentModeOptions}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isDev && <InputNumberItem
|
||||
title={t('layout.setting.autoScreenLock')}
|
||||
min={0}
|
||||
event={HandlerEnum.LOCK_TIME}
|
||||
defaultValue={unref(getLockTime)}
|
||||
formatter={(value: string) => {
|
||||
return parseInt(value) === 0 ? `0(${t('layout.setting.notAutoScreenLock')})` : `${value}${t('layout.setting.minute')}`;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isDev && <InputNumberItem
|
||||
title={t('layout.setting.expandedMenuWidth')}
|
||||
max={600}
|
||||
min={100}
|
||||
step={10}
|
||||
event={HandlerEnum.MENU_WIDTH}
|
||||
disabled={!unref(getShowMenuRef)}
|
||||
defaultValue={unref(getMenuWidth)}
|
||||
formatter={(value: string) => `${parseInt(value)}px`}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
isDev && <SwitchItem
|
||||
title={t('layout.setting.menuDrag')}
|
||||
event={HandlerEnum.MENU_HAS_DRAG}
|
||||
def={unref(getCanDrag)}
|
||||
disabled={!unref(getShowMenuRef)}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isDev && <SwitchItem
|
||||
title={t('layout.setting.collapseMenuDisplayName')}
|
||||
event={HandlerEnum.MENU_COLLAPSED_SHOW_TITLE}
|
||||
def={unref(getCollapsedShowTitle)}
|
||||
disabled={!unref(getShowMenuRef) || !unref(getCollapsed) || unref(getIsMixSidebar)}
|
||||
/>
|
||||
}
|
||||
<SwitchItem title={t('layout.setting.tabs')} event={HandlerEnum.TABS_SHOW} def={unref(getShowMultipleTab)} />
|
||||
<SwitchItem
|
||||
title={t('layout.setting.breadcrumb')}
|
||||
event={HandlerEnum.SHOW_BREADCRUMB}
|
||||
def={unref(getShowBreadCrumb)}
|
||||
disabled={!unref(getShowHeader)}
|
||||
/>
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.breadcrumbIcon')}*/}
|
||||
{/* event={HandlerEnum.SHOW_BREADCRUMB_ICON}*/}
|
||||
{/* def={unref(getShowBreadCrumbIcon)}*/}
|
||||
{/* disabled={!unref(getShowHeader)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.tabsRedoBtn')}*/}
|
||||
{/* event={HandlerEnum.TABS_SHOW_REDO}*/}
|
||||
{/* def={unref(getShowRedo)}*/}
|
||||
{/* disabled={!unref(getShowMultipleTab)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.tabsQuickBtn')}*/}
|
||||
{/* event={HandlerEnum.TABS_SHOW_QUICK}*/}
|
||||
{/* def={unref(getShowQuick)}*/}
|
||||
{/* disabled={!unref(getShowMultipleTab)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.tabsFoldBtn')}*/}
|
||||
{/* event={HandlerEnum.TABS_SHOW_FOLD}*/}
|
||||
{/* def={unref(getShowFold)}*/}
|
||||
{/* disabled={!unref(getShowMultipleTab)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.sidebar')}*/}
|
||||
{/* event={HandlerEnum.MENU_SHOW_SIDEBAR}*/}
|
||||
{/* def={unref(getShowMenu)}*/}
|
||||
{/* disabled={unref(getIsHorizontal)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.header')}*/}
|
||||
{/* event={HandlerEnum.HEADER_SHOW}*/}
|
||||
{/* def={unref(getShowHeader)}*/}
|
||||
{/*/>*/}
|
||||
{/*<SwitchItem*/}
|
||||
{/* title="Logo"*/}
|
||||
{/* event={HandlerEnum.SHOW_LOGO}*/}
|
||||
{/* def={unref(getShowLogo)}*/}
|
||||
{/* disabled={unref(getIsMixSidebar)}*/}
|
||||
{/*/>*/}
|
||||
<SwitchItem title={t('layout.setting.footer')} event={HandlerEnum.SHOW_FOOTER} def={unref(getShowFooter)} />
|
||||
{/*<SwitchItem*/}
|
||||
{/* title={t('layout.setting.fullContent')}*/}
|
||||
{/* event={HandlerEnum.FULL_CONTENT}*/}
|
||||
{/* def={unref(getFullContent)}*/}
|
||||
{/*/>*/}
|
||||
|
||||
<SwitchItem title={t('layout.setting.grayMode')} event={HandlerEnum.GRAY_MODE} def={unref(getGrayMode)} />
|
||||
|
||||
<SwitchItem title={t('layout.setting.colorWeak')} event={HandlerEnum.COLOR_WEAK} def={unref(getColorWeak)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTransition() {
|
||||
return (
|
||||
<>
|
||||
<SwitchItem title={t('layout.setting.progress')} event={HandlerEnum.OPEN_PROGRESS} def={unref(getOpenNProgress)} />
|
||||
<SwitchItem title={t('layout.setting.switchLoading')} event={HandlerEnum.OPEN_PAGE_LOADING} def={unref(getOpenPageLoading)} />
|
||||
|
||||
<SwitchItem title={t('layout.setting.switchAnimation')} event={HandlerEnum.OPEN_ROUTE_TRANSITION} def={unref(getEnableTransition)} />
|
||||
|
||||
<SelectItem
|
||||
title={t('layout.setting.animationType')}
|
||||
event={HandlerEnum.ROUTER_TRANSITION}
|
||||
def={unref(getBasicTransition)}
|
||||
options={routerTransitionOptions}
|
||||
disabled={!unref(getEnableTransition)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return () => (
|
||||
<BasicDrawer {...attrs} title={t('layout.setting.drawerTitle')} width={330} class="setting-drawer">
|
||||
{unref(getShowDarkModeToggle) && <Divider>{() => t('layout.setting.darkMode')}</Divider>}
|
||||
{unref(getShowDarkModeToggle) && <AppDarkModeToggle class="mx-auto" />}
|
||||
<Divider>{() => t('layout.setting.navMode')}</Divider>
|
||||
{renderSidebar()}
|
||||
<Divider>{() => t('layout.setting.sysTheme')}</Divider>
|
||||
{renderMainTheme()}
|
||||
<Divider>{() => t('layout.setting.headerTheme')}</Divider>
|
||||
{renderHeaderTheme()}
|
||||
<Divider>{() => t('layout.setting.sidebarTheme')}</Divider>
|
||||
{renderSiderTheme()}
|
||||
<Divider>{() => t('layout.setting.interfaceFunction')}</Divider>
|
||||
{renderFeatures()}
|
||||
{/*<Divider>{() => t('layout.setting.interfaceDisplay')}</Divider>*/}
|
||||
{renderContent()}
|
||||
{/*<Divider>{() => t('layout.setting.animation')}</Divider>*/}
|
||||
{/*{renderTransition()}*/}
|
||||
<Divider />
|
||||
<SettingFooter />
|
||||
</BasicDrawer>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<span> {{ title }}</span>
|
||||
<InputNumber v-bind="$attrs" size="small" :class="`${prefixCls}-input-number`" @change="handleChange" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
import { InputNumber } from 'ant-design-vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { baseHandler } from '../handler';
|
||||
import { HandlerEnum } from '../enum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InputNumberItem',
|
||||
components: { InputNumber },
|
||||
props: {
|
||||
event: {
|
||||
type: Number as PropType<HandlerEnum>,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('setting-input-number-item');
|
||||
|
||||
function handleChange(e) {
|
||||
props.event && baseHandler(props.event, e);
|
||||
}
|
||||
return {
|
||||
prefixCls,
|
||||
handleChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting-input-number-item';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
|
||||
&-input-number {
|
||||
width: 126px;
|
||||
}
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<span> {{ title }}</span>
|
||||
<Select v-bind="getBindValue" :class="`${prefixCls}-select`" @change="handleChange" :disabled="disabled" size="small" :options="options" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
|
||||
import { Select } from 'ant-design-vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { baseHandler } from '../handler';
|
||||
import { HandlerEnum } from '../enum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectItem',
|
||||
components: { Select },
|
||||
props: {
|
||||
event: {
|
||||
type: Number as PropType<HandlerEnum>,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
def: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
},
|
||||
initValue: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<LabelValueOptions>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('setting-select-item');
|
||||
const getBindValue = computed(() => {
|
||||
return props.def ? { value: props.def, defaultValue: props.initValue || props.def } : {};
|
||||
});
|
||||
|
||||
function handleChange(e: ChangeEvent) {
|
||||
props.event && baseHandler(props.event, e);
|
||||
}
|
||||
return {
|
||||
prefixCls,
|
||||
handleChange,
|
||||
getBindValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting-select-item';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
|
||||
&-select {
|
||||
width: 126px;
|
||||
}
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<a-button v-if="isDev" type="primary" block @click="handleCopy">
|
||||
<CopyOutlined class="mr-2" />
|
||||
{{ t('layout.setting.copyBtn') }}
|
||||
</a-button>
|
||||
|
||||
<a-button color="warning" block @click="handleResetSetting" class="my-3">
|
||||
<RedoOutlined class="mr-2" />
|
||||
{{ t('common.resetText') }}
|
||||
</a-button>
|
||||
|
||||
<a-button color="error" block @click="handleClearAndRedo">
|
||||
<RedoOutlined class="mr-2" />
|
||||
{{ t('layout.setting.clearBtn') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref } from 'vue';
|
||||
|
||||
import { CopyOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { useAppStore } from '/@/store/modules/app';
|
||||
import { usePermissionStore } from '/@/store/modules/permission';
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
|
||||
|
||||
import { updateColorWeak } from '/@/logics/theme/updateColorWeak';
|
||||
import { updateGrayMode } from '/@/logics/theme/updateGrayMode';
|
||||
import defaultSetting from '/@/settings/projectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingFooter',
|
||||
components: { CopyOutlined, RedoOutlined },
|
||||
setup() {
|
||||
const permissionStore = usePermissionStore();
|
||||
const { prefixCls } = useDesign('setting-footer');
|
||||
const { t } = useI18n();
|
||||
const { createSuccessModal, createMessage } = useMessage();
|
||||
const tabStore = useMultipleTabStore();
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
function handleCopy() {
|
||||
const { isSuccessRef } = useCopyToClipboard(JSON.stringify(unref(appStore.getProjectConfig), null, 2));
|
||||
unref(isSuccessRef) &&
|
||||
createSuccessModal({
|
||||
title: t('layout.setting.operatingTitle'),
|
||||
content: t('layout.setting.operatingContent'),
|
||||
});
|
||||
}
|
||||
function handleResetSetting() {
|
||||
try {
|
||||
appStore.setProjectConfig(defaultSetting);
|
||||
const { colorWeak, grayMode } = defaultSetting;
|
||||
// updateTheme(themeColor);
|
||||
updateColorWeak(colorWeak);
|
||||
updateGrayMode(grayMode);
|
||||
createMessage.success(t('layout.setting.resetSuccess'));
|
||||
} catch (error) {
|
||||
createMessage.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClearAndRedo() {
|
||||
localStorage.clear();
|
||||
appStore.resetAllState();
|
||||
permissionStore.resetState();
|
||||
tabStore.resetState();
|
||||
userStore.resetState();
|
||||
location.reload();
|
||||
}
|
||||
const isDev = import.meta.env.DEV;
|
||||
return {
|
||||
prefixCls,
|
||||
t,
|
||||
handleCopy,
|
||||
handleResetSetting,
|
||||
handleClearAndRedo,
|
||||
isDev,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting-footer';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<span> {{ title }}</span>
|
||||
<Switch
|
||||
v-bind="getBindValue"
|
||||
@change="handleChange"
|
||||
:disabled="disabled"
|
||||
:checkedChildren="t('layout.setting.on')"
|
||||
:unCheckedChildren="t('layout.setting.off')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
|
||||
import { Switch } from 'ant-design-vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { baseHandler } from '../handler';
|
||||
import { HandlerEnum } from '../enum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SwitchItem',
|
||||
components: { Switch },
|
||||
props: {
|
||||
event: {
|
||||
type: Number as PropType<HandlerEnum>,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
def: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('setting-switch-item');
|
||||
const { t } = useI18n();
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
return props.def ? { checked: props.def } : {};
|
||||
});
|
||||
function handleChange(e: ChangeEvent) {
|
||||
props.event && baseHandler(props.event, e);
|
||||
}
|
||||
return {
|
||||
prefixCls,
|
||||
t,
|
||||
handleChange,
|
||||
getBindValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting-switch-item';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 16px 0;
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<template v-for="color in colorList || []" :key="color">
|
||||
<span
|
||||
@click=" !isDisabledColor && handleClick(color)"
|
||||
:class="[
|
||||
`${prefixCls}__item`,
|
||||
{
|
||||
[`${prefixCls}__item--active`]: def === color,
|
||||
[`${prefixCls}__item--black`]: color == '#ffffff',
|
||||
disabledColor: isDisabledColor,
|
||||
},
|
||||
]"
|
||||
:style="{ background: color }"
|
||||
>
|
||||
<CheckOutlined />
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, watch, ref } from 'vue';
|
||||
import { CheckOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import { baseHandler } from '../handler';
|
||||
import { HandlerEnum } from '../enum';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { ThemeEnum } from '/@/enums/appEnum';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ThemeColorPicker',
|
||||
components: { CheckOutlined },
|
||||
props: {
|
||||
colorList: {
|
||||
type: Array as PropType<string[]>,
|
||||
defualt: [],
|
||||
},
|
||||
event: {
|
||||
type: Number as PropType<HandlerEnum>,
|
||||
},
|
||||
def: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('setting-theme-picker');
|
||||
const { getDarkMode } = useRootSetting();
|
||||
const isDisabledColor = ref(false);
|
||||
|
||||
function handleClick(color: string) {
|
||||
props.event && baseHandler(props.event, color);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8927】暗黑主题下不允许切换顶栏主题和菜单主题
|
||||
watch(
|
||||
() => getDarkMode.value,
|
||||
(newValue) => {
|
||||
isDisabledColor.value = props.event === 1 ? false : newValue === ThemeEnum.DARK;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8927】暗黑主题下不允许切换顶栏主题和菜单主题
|
||||
return {
|
||||
prefixCls,
|
||||
handleClick,
|
||||
isDisabledColor
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-setting-theme-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0;
|
||||
justify-content: space-around;
|
||||
// update-begin--author:liaozhiyang---date:20231220---for:【QQYUN-7677】antd4兼容改造,勾选垂直居中
|
||||
line-height: 1.3;
|
||||
// update-end--author:liaozhiyang---date:20231220---for:【QQYUN-7677】antd4兼容改造,勾选垂直居中
|
||||
&__item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 2px;
|
||||
&.disabledColor {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--active {
|
||||
svg {
|
||||
display: inline-block;
|
||||
margin: 0 0 3px 3px;
|
||||
font-size: 12px;
|
||||
fill: @white !important;
|
||||
}
|
||||
}
|
||||
&--black {
|
||||
svg {
|
||||
fill: #000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div :class="prefixCls">
|
||||
<template v-for="item in menuTypeList || []" :key="item.title">
|
||||
<Tooltip :title="item.title" placement="bottom">
|
||||
<div
|
||||
@click="handler(item)"
|
||||
:class="[
|
||||
`${prefixCls}__item`,
|
||||
`${prefixCls}__item--${item.type}`,
|
||||
{
|
||||
[`${prefixCls}__item--active`]: def === item.type,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="mix-sidebar"></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import { menuTypeList } from '../enum';
|
||||
export default defineComponent({
|
||||
name: 'MenuTypePicker',
|
||||
components: { Tooltip },
|
||||
props: {
|
||||
menuTypeList: {
|
||||
type: Array as PropType<typeof menuTypeList>,
|
||||
defualt: () => [],
|
||||
},
|
||||
handler: {
|
||||
type: Function as PropType<Fn>,
|
||||
default: () => ({}),
|
||||
},
|
||||
def: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('setting-menu-type-picker');
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-setting-menu-type-picker';
|
||||
|
||||
.@{prefix-cls} {
|
||||
display: flex;
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 48px;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2.5px 0 rgba(0, 0, 0, 0.18);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&--sidebar,
|
||||
&--light {
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--mix {
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #273352;
|
||||
}
|
||||
}
|
||||
|
||||
&--top-menu {
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #273352;
|
||||
}
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background-color: #273352;
|
||||
}
|
||||
|
||||
&--mix-sidebar {
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: #273352;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.mix-sidebar {
|
||||
position: absolute;
|
||||
left: 25%;
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
padding: 12px;
|
||||
border: 2px solid @primary-color;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,8 @@
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
export const TypePicker = createAsyncComponent(() => import('./TypePicker.vue'));
|
||||
export const ThemeColorPicker = createAsyncComponent(() => import('./ThemeColorPicker.vue'));
|
||||
export const SettingFooter = createAsyncComponent(() => import('./SettingFooter.vue'));
|
||||
export const SwitchItem = createAsyncComponent(() => import('./SwitchItem.vue'));
|
||||
export const SelectItem = createAsyncComponent(() => import('./SelectItem.vue'));
|
||||
export const InputNumberItem = createAsyncComponent(() => import('./InputNumberItem.vue'));
|
||||
167
jeecgboot-vue3/src/layouts/default/setting/enum.ts
Normal file
167
jeecgboot-vue3/src/layouts/default/setting/enum.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { TabsThemeEnum, ContentEnum, RouterTransitionEnum } from '/@/enums/appEnum';
|
||||
import { MenuModeEnum, MenuTypeEnum, TopMenuAlignEnum, TriggerEnum, MixSidebarTriggerEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
export enum HandlerEnum {
|
||||
CHANGE_LAYOUT,
|
||||
CHANGE_THEME_COLOR,
|
||||
CHANGE_THEME,
|
||||
// menu
|
||||
MENU_HAS_DRAG,
|
||||
MENU_ACCORDION,
|
||||
MENU_TRIGGER,
|
||||
MENU_TOP_ALIGN,
|
||||
MENU_COLLAPSED,
|
||||
MENU_COLLAPSED_SHOW_TITLE,
|
||||
MENU_WIDTH,
|
||||
MENU_SHOW_SIDEBAR,
|
||||
MENU_THEME,
|
||||
MENU_SPLIT,
|
||||
MENU_FIXED,
|
||||
MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE,
|
||||
MENU_TRIGGER_MIX_SIDEBAR,
|
||||
MENU_FIXED_MIX_SIDEBAR,
|
||||
|
||||
// header
|
||||
HEADER_SHOW,
|
||||
HEADER_THEME,
|
||||
HEADER_FIXED,
|
||||
|
||||
HEADER_SEARCH,
|
||||
|
||||
TABS_SHOW_QUICK,
|
||||
TABS_SHOW_REDO,
|
||||
TABS_SHOW,
|
||||
TABS_SHOW_FOLD,
|
||||
TABS_THEME,
|
||||
|
||||
LOCK_TIME,
|
||||
FULL_CONTENT,
|
||||
CONTENT_MODE,
|
||||
SHOW_BREADCRUMB,
|
||||
SHOW_BREADCRUMB_ICON,
|
||||
GRAY_MODE,
|
||||
COLOR_WEAK,
|
||||
SHOW_LOGO,
|
||||
SHOW_FOOTER,
|
||||
|
||||
ROUTER_TRANSITION,
|
||||
OPEN_PROGRESS,
|
||||
OPEN_PAGE_LOADING,
|
||||
OPEN_ROUTE_TRANSITION,
|
||||
}
|
||||
|
||||
// 标签页样式
|
||||
export const tabsThemeOptions = [
|
||||
{
|
||||
value: TabsThemeEnum.SMOOTH,
|
||||
label: t('layout.setting.tabsThemeSmooth'),
|
||||
},
|
||||
{
|
||||
value: TabsThemeEnum.CARD,
|
||||
label: t('layout.setting.tabsThemeCard'),
|
||||
},
|
||||
{
|
||||
value: TabsThemeEnum.SIMPLE,
|
||||
label: t('layout.setting.tabsThemeSimple'),
|
||||
},
|
||||
];
|
||||
|
||||
export const contentModeOptions = [
|
||||
{
|
||||
value: ContentEnum.FULL,
|
||||
label: t('layout.setting.contentModeFull'),
|
||||
},
|
||||
{
|
||||
value: ContentEnum.FIXED,
|
||||
label: t('layout.setting.contentModeFixed'),
|
||||
},
|
||||
];
|
||||
|
||||
export const topMenuAlignOptions = [
|
||||
{
|
||||
value: TopMenuAlignEnum.CENTER,
|
||||
label: t('layout.setting.topMenuAlignRight'),
|
||||
},
|
||||
{
|
||||
value: TopMenuAlignEnum.START,
|
||||
label: t('layout.setting.topMenuAlignLeft'),
|
||||
},
|
||||
{
|
||||
value: TopMenuAlignEnum.END,
|
||||
label: t('layout.setting.topMenuAlignCenter'),
|
||||
},
|
||||
];
|
||||
|
||||
export const getMenuTriggerOptions = (hideTop: boolean) => {
|
||||
return [
|
||||
{
|
||||
value: TriggerEnum.NONE,
|
||||
label: t('layout.setting.menuTriggerNone'),
|
||||
},
|
||||
{
|
||||
value: TriggerEnum.FOOTER,
|
||||
label: t('layout.setting.menuTriggerBottom'),
|
||||
},
|
||||
...(hideTop
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: TriggerEnum.HEADER,
|
||||
label: t('layout.setting.menuTriggerTop'),
|
||||
},
|
||||
]),
|
||||
];
|
||||
};
|
||||
|
||||
export const routerTransitionOptions = [
|
||||
RouterTransitionEnum.ZOOM_FADE,
|
||||
RouterTransitionEnum.FADE,
|
||||
RouterTransitionEnum.ZOOM_OUT,
|
||||
RouterTransitionEnum.FADE_SIDE,
|
||||
RouterTransitionEnum.FADE_BOTTOM,
|
||||
RouterTransitionEnum.FADE_SCALE,
|
||||
].map((item) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
|
||||
export const menuTypeList = [
|
||||
{
|
||||
title: t('layout.setting.menuTypeSidebar'),
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: MenuTypeEnum.SIDEBAR,
|
||||
},
|
||||
{
|
||||
title: t('layout.setting.menuTypeMix'),
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: MenuTypeEnum.MIX,
|
||||
},
|
||||
|
||||
{
|
||||
title: t('layout.setting.menuTypeTopMenu'),
|
||||
mode: MenuModeEnum.HORIZONTAL,
|
||||
type: MenuTypeEnum.TOP_MENU,
|
||||
},
|
||||
{
|
||||
title: t('layout.setting.menuTypeMixSidebar'),
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: MenuTypeEnum.MIX_SIDEBAR,
|
||||
},
|
||||
];
|
||||
|
||||
export const mixSidebarTriggerOptions = [
|
||||
{
|
||||
value: MixSidebarTriggerEnum.HOVER,
|
||||
label: t('layout.setting.triggerHover'),
|
||||
},
|
||||
{
|
||||
value: MixSidebarTriggerEnum.CLICK,
|
||||
label: t('layout.setting.triggerClick'),
|
||||
},
|
||||
];
|
||||
238
jeecgboot-vue3/src/layouts/default/setting/handler.ts
Normal file
238
jeecgboot-vue3/src/layouts/default/setting/handler.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import { HandlerEnum, tabsThemeOptions} from './enum';
|
||||
import { updateHeaderBgColor, updateSidebarBgColor } from '/@/logics/theme/updateBackground';
|
||||
import { updateColorWeak } from '/@/logics/theme/updateColorWeak';
|
||||
import { updateGrayMode } from '/@/logics/theme/updateGrayMode';
|
||||
|
||||
import { useAppStore } from '/@/store/modules/app';
|
||||
import { ProjectConfig } from '/#/config';
|
||||
import { changeTheme } from '/@/logics/theme';
|
||||
import { updateDarkTheme } from '/@/logics/theme/dark';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
|
||||
import { HEADER_PRESET_BG_COLOR_LIST, APP_PRESET_COLOR_LIST, SIDE_BAR_BG_COLOR_LIST } from '/@/settings/designSetting';
|
||||
import { isObject } from '/@/utils/is';
|
||||
import { ThemeEnum } from '/@/enums/appEnum';
|
||||
import { APP__THEME__COLOR } from '/@/enums/cacheEnum';
|
||||
|
||||
/**
|
||||
* 2024-04-07
|
||||
* liaozhiyang
|
||||
* 切换导航栏模式都走这个方法,每个模式都会有固定的顶部和菜单颜色搭配。暗黑模式则不走固定搭配
|
||||
* */
|
||||
export function layoutHandler(event: HandlerEnum, value: any) {
|
||||
const isHTopMenu = isObject(value) && value.type == MenuTypeEnum.TOP_MENU && value.mode == MenuModeEnum.HORIZONTAL;
|
||||
const isMixMenu = isObject(value) && value.type == MenuTypeEnum.MIX && value.mode == MenuModeEnum.INLINE;
|
||||
const isMixSidebarMenu = isObject(value) && value.type == MenuTypeEnum.MIX_SIDEBAR && value.mode == MenuModeEnum.INLINE;
|
||||
const appStore = useAppStore();
|
||||
const darkMode = appStore.getDarkMode === ThemeEnum.DARK;
|
||||
if (isHTopMenu) {
|
||||
baseHandler(event, value);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[2]);
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[2]);
|
||||
if (darkMode) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
} else if (isMixMenu) {
|
||||
baseHandler(event, value);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[4]);
|
||||
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[3]);
|
||||
if (darkMode) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[1]);
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
} else if (isMixSidebarMenu) {
|
||||
baseHandler(event, value);
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[1]);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[0]);
|
||||
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[0]);
|
||||
if (darkMode) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
} else {
|
||||
baseHandler(event, value);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[4]);
|
||||
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[7]);
|
||||
if (darkMode) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[1]);
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
}
|
||||
}
|
||||
|
||||
export function baseHandler(event: HandlerEnum, value: any) {
|
||||
const appStore = useAppStore();
|
||||
const config = handler(event, value);
|
||||
appStore.setProjectConfig(config);
|
||||
if (event === HandlerEnum.CHANGE_THEME) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
}
|
||||
|
||||
export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConfig> {
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { getThemeColor, getDarkMode } = useRootSetting();
|
||||
switch (event) {
|
||||
case HandlerEnum.CHANGE_LAYOUT:
|
||||
const { mode, type, split } = value;
|
||||
const splitOpt = split === undefined ? { split } : {};
|
||||
|
||||
return {
|
||||
menuSetting: {
|
||||
mode,
|
||||
type,
|
||||
collapsed: false,
|
||||
show: true,
|
||||
hidden: false,
|
||||
...splitOpt,
|
||||
},
|
||||
};
|
||||
|
||||
case HandlerEnum.CHANGE_THEME_COLOR:
|
||||
if (getThemeColor.value === value) {
|
||||
return {};
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8925】系统主题颜色(供页面加载使用)
|
||||
localStorage.setItem(APP__THEME__COLOR, value);
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8925】系统主题颜色(供页面加载使用)
|
||||
changeTheme(value);
|
||||
|
||||
return { themeColor: value };
|
||||
|
||||
case HandlerEnum.CHANGE_THEME:
|
||||
if (getDarkMode.value === value) {
|
||||
return {};
|
||||
}
|
||||
updateDarkTheme(value);
|
||||
|
||||
return {};
|
||||
|
||||
case HandlerEnum.MENU_HAS_DRAG:
|
||||
return { menuSetting: { canDrag: value } };
|
||||
|
||||
case HandlerEnum.MENU_ACCORDION:
|
||||
return { menuSetting: { accordion: value } };
|
||||
|
||||
case HandlerEnum.MENU_TRIGGER:
|
||||
return { menuSetting: { trigger: value } };
|
||||
|
||||
case HandlerEnum.MENU_TOP_ALIGN:
|
||||
return { menuSetting: { topMenuAlign: value } };
|
||||
|
||||
case HandlerEnum.MENU_COLLAPSED:
|
||||
return { menuSetting: { collapsed: value } };
|
||||
|
||||
case HandlerEnum.MENU_WIDTH:
|
||||
return { menuSetting: { menuWidth: value } };
|
||||
|
||||
case HandlerEnum.MENU_SHOW_SIDEBAR:
|
||||
return { menuSetting: { show: value } };
|
||||
|
||||
case HandlerEnum.MENU_COLLAPSED_SHOW_TITLE:
|
||||
return { menuSetting: { collapsedShowTitle: value } };
|
||||
|
||||
case HandlerEnum.MENU_THEME:
|
||||
updateSidebarBgColor(value);
|
||||
return { menuSetting: { bgColor: value } };
|
||||
|
||||
case HandlerEnum.MENU_SPLIT:
|
||||
return { menuSetting: { split: value } };
|
||||
|
||||
case HandlerEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE:
|
||||
return { menuSetting: { closeMixSidebarOnChange: value } };
|
||||
|
||||
case HandlerEnum.MENU_FIXED:
|
||||
return { menuSetting: { fixed: value } };
|
||||
|
||||
case HandlerEnum.MENU_TRIGGER_MIX_SIDEBAR:
|
||||
return { menuSetting: { mixSideTrigger: value } };
|
||||
|
||||
case HandlerEnum.MENU_FIXED_MIX_SIDEBAR:
|
||||
return { menuSetting: { mixSideFixed: value } };
|
||||
|
||||
// ============transition==================
|
||||
case HandlerEnum.OPEN_PAGE_LOADING:
|
||||
appStore.setPageLoading(false);
|
||||
return { transitionSetting: { openPageLoading: value } };
|
||||
|
||||
case HandlerEnum.ROUTER_TRANSITION:
|
||||
return { transitionSetting: { basicTransition: value } };
|
||||
|
||||
case HandlerEnum.OPEN_ROUTE_TRANSITION:
|
||||
return { transitionSetting: { enable: value } };
|
||||
|
||||
case HandlerEnum.OPEN_PROGRESS:
|
||||
return { transitionSetting: { openNProgress: value } };
|
||||
// ============root==================
|
||||
|
||||
case HandlerEnum.LOCK_TIME:
|
||||
return { lockTime: value };
|
||||
|
||||
case HandlerEnum.FULL_CONTENT:
|
||||
return { fullContent: value };
|
||||
|
||||
case HandlerEnum.CONTENT_MODE:
|
||||
return { contentMode: value };
|
||||
|
||||
case HandlerEnum.SHOW_BREADCRUMB:
|
||||
return { showBreadCrumb: value };
|
||||
|
||||
case HandlerEnum.SHOW_BREADCRUMB_ICON:
|
||||
return { showBreadCrumbIcon: value };
|
||||
|
||||
case HandlerEnum.GRAY_MODE:
|
||||
updateGrayMode(value);
|
||||
return { grayMode: value };
|
||||
|
||||
case HandlerEnum.SHOW_FOOTER:
|
||||
return { showFooter: value };
|
||||
|
||||
case HandlerEnum.COLOR_WEAK:
|
||||
updateColorWeak(value);
|
||||
return { colorWeak: value };
|
||||
|
||||
case HandlerEnum.SHOW_LOGO:
|
||||
return { showLogo: value };
|
||||
|
||||
// ============tabs==================
|
||||
case HandlerEnum.TABS_SHOW_QUICK:
|
||||
return { multiTabsSetting: { showQuick: value } };
|
||||
|
||||
case HandlerEnum.TABS_SHOW:
|
||||
return { multiTabsSetting: { show: value } };
|
||||
|
||||
case HandlerEnum.TABS_SHOW_REDO:
|
||||
return { multiTabsSetting: { showRedo: value } };
|
||||
|
||||
case HandlerEnum.TABS_SHOW_FOLD:
|
||||
return { multiTabsSetting: { showFold: value } };
|
||||
|
||||
case HandlerEnum.TABS_THEME:
|
||||
return { multiTabsSetting: { theme: value } };
|
||||
|
||||
// ============header==================
|
||||
case HandlerEnum.HEADER_THEME:
|
||||
updateHeaderBgColor(value);
|
||||
return { headerSetting: { bgColor: value } };
|
||||
|
||||
case HandlerEnum.HEADER_SEARCH:
|
||||
return { headerSetting: { showSearch: value } };
|
||||
|
||||
case HandlerEnum.HEADER_FIXED:
|
||||
return { headerSetting: { fixed: value } };
|
||||
|
||||
case HandlerEnum.HEADER_SHOW:
|
||||
return { headerSetting: { show: value } };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
26
jeecgboot-vue3/src/layouts/default/setting/index.vue
Normal file
26
jeecgboot-vue3/src/layouts/default/setting/index.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div @click="openDrawer(true)">
|
||||
<Icon icon="ion:settings-outline" />
|
||||
<SettingDrawer @register="register" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import SettingDrawer from './SettingDrawer';
|
||||
import Icon from '/@/components/Icon';
|
||||
|
||||
import { useDrawer } from '/@/components/Drawer';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingButton',
|
||||
components: { SettingDrawer, Icon },
|
||||
setup() {
|
||||
const [register, { openDrawer }] = useDrawer();
|
||||
|
||||
return {
|
||||
register,
|
||||
openDrawer,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
66
jeecgboot-vue3/src/layouts/default/sider/DragBar.vue
Normal file
66
jeecgboot-vue3/src/layouts/default/sider/DragBar.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div :class="getClass" :style="getDragBarStyle"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DargBar',
|
||||
props: {
|
||||
mobile: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const { getMiniWidthNumber, getCollapsed, getCanDrag } = useMenuSetting();
|
||||
|
||||
const { prefixCls } = useDesign('darg-bar');
|
||||
const getDragBarStyle = computed(() => {
|
||||
if (unref(getCollapsed)) {
|
||||
return { left: `${unref(getMiniWidthNumber)}px` };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--hide`]: !unref(getCanDrag) || props.mobile,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
getDragBarStyle,
|
||||
getClass,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-darg-bar';
|
||||
|
||||
.@{prefix-cls} {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2px;
|
||||
z-index: @side-drag-z-index;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
|
||||
&--hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @primary-color;
|
||||
box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
172
jeecgboot-vue3/src/layouts/default/sider/LayoutSider.vue
Normal file
172
jeecgboot-vue3/src/layouts/default/sider/LayoutSider.vue
Normal file
@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div v-if="getMenuFixed && !getIsMobile" :style="getHiddenDomStyle" v-show="showClassSideBarRef"></div>
|
||||
<Sider
|
||||
v-show="showClassSideBarRef"
|
||||
ref="sideRef"
|
||||
breakpoint="lg"
|
||||
collapsible
|
||||
:class="getSiderClass"
|
||||
:width="getMenuWidth"
|
||||
:collapsed="getCollapsed"
|
||||
:collapsedWidth="getCollapsedWidth"
|
||||
:theme="getMenuTheme"
|
||||
@breakpoint="onBreakpointChange"
|
||||
:trigger="getTrigger"
|
||||
v-bind="getTriggerAttr"
|
||||
>
|
||||
<template #trigger v-if="getShowTrigger">
|
||||
<LayoutTrigger />
|
||||
</template>
|
||||
<LayoutMenu :theme="getMenuTheme" :menuMode="getMode" :splitType="getSplitType" />
|
||||
<DragBar ref="dragBarRef" />
|
||||
</Sider>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, unref, CSSProperties, h } from 'vue';
|
||||
|
||||
import { Layout } from 'ant-design-vue';
|
||||
import LayoutMenu from '../menu/index.vue';
|
||||
import LayoutTrigger from '/@/layouts/default/trigger/index.vue';
|
||||
|
||||
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useTrigger, useDragLine, useSiderEvent } from './useLayoutSider';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
import DragBar from './DragBar.vue';
|
||||
export default defineComponent({
|
||||
name: 'LayoutSideBar',
|
||||
components: { Sider: Layout.Sider, LayoutMenu, DragBar, LayoutTrigger },
|
||||
setup() {
|
||||
const dragBarRef = ref<ElRef>(null);
|
||||
const sideRef = ref<ElRef>(null);
|
||||
|
||||
const { getCollapsed, getMenuWidth, getSplit, getMenuTheme, getRealWidth, getMenuHidden, getMenuFixed, getIsMixMode, toggleCollapsed } =
|
||||
useMenuSetting();
|
||||
|
||||
const { prefixCls } = useDesign('layout-sideBar');
|
||||
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const { getTriggerAttr, getShowTrigger } = useTrigger(getIsMobile);
|
||||
|
||||
useDragLine(sideRef, dragBarRef);
|
||||
|
||||
const { getCollapsedWidth, onBreakpointChange } = useSiderEvent();
|
||||
|
||||
const getMode = computed(() => {
|
||||
return unref(getSplit) ? MenuModeEnum.INLINE : null;
|
||||
});
|
||||
|
||||
const getSplitType = computed(() => {
|
||||
return unref(getSplit) ? MenuSplitTyeEnum.LEFT : MenuSplitTyeEnum.NONE;
|
||||
});
|
||||
|
||||
const showClassSideBarRef = computed(() => {
|
||||
return unref(getSplit) ? !unref(getMenuHidden) : true;
|
||||
});
|
||||
|
||||
const getSiderClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--fixed`]: unref(getMenuFixed),
|
||||
[`${prefixCls}--mix`]: unref(getIsMixMode) && !unref(getIsMobile),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const getHiddenDomStyle = computed((): CSSProperties => {
|
||||
const width = `${unref(getRealWidth)}px`;
|
||||
return {
|
||||
width: width,
|
||||
overflow: 'hidden',
|
||||
flex: `0 0 ${width}`,
|
||||
maxWidth: width,
|
||||
minWidth: width,
|
||||
transition: 'all 0.2s',
|
||||
};
|
||||
});
|
||||
|
||||
// 在此处使用计算量可能会导致sider异常
|
||||
// andv 更新后,如果trigger插槽可用,则此处代码可废弃
|
||||
const getTrigger = h(LayoutTrigger);
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
sideRef,
|
||||
dragBarRef,
|
||||
getIsMobile,
|
||||
getHiddenDomStyle,
|
||||
getSiderClass,
|
||||
getTrigger,
|
||||
getTriggerAttr,
|
||||
getCollapsedWidth,
|
||||
getMenuFixed,
|
||||
showClassSideBarRef,
|
||||
getMenuWidth,
|
||||
getCollapsed,
|
||||
getMenuTheme,
|
||||
onBreakpointChange,
|
||||
getMode,
|
||||
getSplitType,
|
||||
getShowTrigger,
|
||||
toggleCollapsed,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-sideBar';
|
||||
|
||||
.@{prefix-cls} {
|
||||
z-index: @layout-sider-fixed-z-index;
|
||||
|
||||
&--fixed {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--mix {
|
||||
top: @header-height;
|
||||
height: calc(100% - @header-height);
|
||||
}
|
||||
|
||||
&.ant-layout-sider-dark {
|
||||
background-color: @sider-dark-bg-color;
|
||||
|
||||
.ant-layout-sider-trigger {
|
||||
color: darken(@white, 25%);
|
||||
background-color: @trigger-dark-bg-color;
|
||||
|
||||
&:hover {
|
||||
color: @white;
|
||||
background-color: @trigger-dark-hover-bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-layout-sider-dark) {
|
||||
// box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
|
||||
|
||||
.ant-layout-sider-trigger {
|
||||
color: @text-color-base;
|
||||
border-top: 1px solid @border-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-sider-zero-width-trigger {
|
||||
top: 40%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
& .ant-layout-sider-trigger {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
594
jeecgboot-vue3/src/layouts/default/sider/MixSider.vue
Normal file
594
jeecgboot-vue3/src/layouts/default/sider/MixSider.vue
Normal file
@ -0,0 +1,594 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-dom`" :style="getDomStyle"></div>
|
||||
<div
|
||||
v-click-outside="handleClickOutside"
|
||||
:style="getWrapStyle"
|
||||
:class="[
|
||||
prefixCls,
|
||||
getMenuTheme,
|
||||
{
|
||||
open: openMenu,
|
||||
mini: getCollapsed,
|
||||
bright: isThemeBright,
|
||||
},
|
||||
]"
|
||||
v-bind="getMenuEvents"
|
||||
>
|
||||
<AppLogo :showTitle="false" :class="`${prefixCls}-logo`" />
|
||||
|
||||
<LayoutTrigger :class="`${prefixCls}-trigger`" />
|
||||
|
||||
<ScrollContainer>
|
||||
<ul :class="`${prefixCls}-module`">
|
||||
<li
|
||||
:class="[
|
||||
`${prefixCls}-module__item `,
|
||||
{
|
||||
[`${prefixCls}-module__item--active`]: item.path === activePath,
|
||||
},
|
||||
]"
|
||||
v-bind="getItemEvents(item)"
|
||||
v-for="item in menuModules"
|
||||
:key="item.path"
|
||||
>
|
||||
<SimpleMenuTag :item="item" collapseParent dot />
|
||||
<Icon :class="`${prefixCls}-module__icon`" :size="getCollapsed ? 16 : 20" :icon="item.icon || (item.meta && item.meta.icon)" />
|
||||
<p :class="`${prefixCls}-module__name`">
|
||||
{{ t(item.name) }}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</ScrollContainer>
|
||||
|
||||
<div :class="`${prefixCls}-menu-list`" ref="sideRef" :style="getMenuStyle">
|
||||
<div
|
||||
v-show="openMenu"
|
||||
:class="[
|
||||
`${prefixCls}-menu-list__title`,
|
||||
{
|
||||
show: openMenu,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<span class="text"> {{ shortTitle }}</span>
|
||||
<Icon :size="16" :icon="getMixSideFixed ? 'ri:pushpin-2-fill' : 'ri:pushpin-2-line'" class="pushpin" @click="handleFixedMenu" />
|
||||
</div>
|
||||
<ScrollContainer :class="`${prefixCls}-menu-list__content`">
|
||||
<SimpleMenu :items="childrenMenus" :theme="getMenuTheme" mixSider @menuClick="handleMenuClick" />
|
||||
</ScrollContainer>
|
||||
<div v-show="getShowDragBar && openMenu" :class="`${prefixCls}-drag-bar`" ref="dragBarRef"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { Menu } from '/@/router/types';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed, defineComponent, onMounted, ref, unref, watch} from 'vue';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { ScrollContainer } from '/@/components/Container';
|
||||
import { SimpleMenu, SimpleMenuTag } from '/@/components/SimpleMenu';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
import { AppLogo } from '/@/components/Application';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useDragLine } from './useLayoutSider';
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
|
||||
import clickOutside from '/@/directives/clickOutside';
|
||||
import { getChildrenMenus, getCurrentParentPath, getShallowMenus } from '/@/router/menus';
|
||||
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
|
||||
import LayoutTrigger from '../trigger/index.vue';
|
||||
import { useAppStore } from '/@/store/modules/app';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutMixSider',
|
||||
components: {
|
||||
ScrollContainer,
|
||||
AppLogo,
|
||||
SimpleMenu,
|
||||
Icon,
|
||||
LayoutTrigger,
|
||||
SimpleMenuTag,
|
||||
},
|
||||
directives: {
|
||||
clickOutside,
|
||||
},
|
||||
setup() {
|
||||
let menuModules = ref<Menu[]>([]);
|
||||
const activePath = ref('');
|
||||
const childrenMenus = ref<Menu[]>([]);
|
||||
const openMenu = ref(false);
|
||||
const dragBarRef = ref<ElRef>(null);
|
||||
const sideRef = ref<ElRef>(null);
|
||||
const currentRoute = ref<Nullable<RouteLocationNormalized>>(null);
|
||||
const appStore = useAppStore();
|
||||
const isThemeBright = ref(false);
|
||||
|
||||
const { prefixCls } = useDesign('layout-mix-sider');
|
||||
const go = useGo();
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
getMenuWidth,
|
||||
getCanDrag,
|
||||
getCloseMixSidebarOnChange,
|
||||
getMenuTheme,
|
||||
getMixSideTrigger,
|
||||
getRealWidth,
|
||||
getMixSideFixed,
|
||||
mixSideHasChildren,
|
||||
setMenuSetting,
|
||||
getIsMixSidebar,
|
||||
getCollapsed,
|
||||
} = useMenuSetting();
|
||||
|
||||
const { shortTitle } = useGlobSetting();
|
||||
|
||||
useDragLine(sideRef, dragBarRef, true);
|
||||
|
||||
const getMenuStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
width: unref(openMenu) ? `${unref(getMenuWidth) - 60}px` : 0,
|
||||
left: `${unref(getMixSideWidth)}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const getIsFixed = computed(() => {
|
||||
/* eslint-disable-next-line */
|
||||
mixSideHasChildren.value = unref(childrenMenus).length > 0;
|
||||
const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
|
||||
if (isFixed) {
|
||||
/* eslint-disable-next-line */
|
||||
openMenu.value = true;
|
||||
}
|
||||
return isFixed;
|
||||
});
|
||||
|
||||
const getMixSideWidth = computed(() => {
|
||||
return unref(getCollapsed) ? SIDE_BAR_MINI_WIDTH : SIDE_BAR_SHOW_TIT_MINI_WIDTH;
|
||||
});
|
||||
|
||||
const getDomStyle = computed((): CSSProperties => {
|
||||
const fixedWidth = unref(getIsFixed) ? unref(getRealWidth) : 0;
|
||||
const width = `${unref(getMixSideWidth) + fixedWidth}px`;
|
||||
return getWrapCommonStyle(width);
|
||||
});
|
||||
|
||||
const getWrapStyle = computed((): CSSProperties => {
|
||||
const width = `${unref(getMixSideWidth)}px`;
|
||||
return getWrapCommonStyle(width);
|
||||
});
|
||||
|
||||
const getMenuEvents = computed(() => {
|
||||
return !unref(getMixSideFixed)
|
||||
? {
|
||||
onMouseleave: () => {
|
||||
setActive(true);
|
||||
closeMenu();
|
||||
},
|
||||
}
|
||||
: {};
|
||||
});
|
||||
|
||||
const getShowDragBar = computed(() => unref(getCanDrag));
|
||||
|
||||
onMounted(async () => {
|
||||
menuModules.value = await getShallowMenus();
|
||||
});
|
||||
|
||||
listenerRouteChange((route) => {
|
||||
currentRoute.value = route;
|
||||
setActive(true);
|
||||
if (unref(getCloseMixSidebarOnChange)) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
function getWrapCommonStyle(width: string): CSSProperties {
|
||||
return {
|
||||
width,
|
||||
maxWidth: width,
|
||||
minWidth: width,
|
||||
flex: `0 0 ${width}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Process module menu click
|
||||
async function handleModuleClick(path: string, hover = false) {
|
||||
const children = await getChildrenMenus(path);
|
||||
if (unref(activePath) === path) {
|
||||
if (!hover) {
|
||||
if (!unref(openMenu)) {
|
||||
openMenu.value = true;
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
} else {
|
||||
if (!unref(openMenu)) {
|
||||
openMenu.value = true;
|
||||
}
|
||||
}
|
||||
if (!unref(openMenu)) {
|
||||
setActive();
|
||||
}
|
||||
} else {
|
||||
openMenu.value = true;
|
||||
activePath.value = path;
|
||||
}
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
if (!hover) go(path);
|
||||
childrenMenus.value = [];
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
childrenMenus.value = children;
|
||||
}
|
||||
|
||||
// Set the currently active menu and submenu
|
||||
async function setActive(setChildren = false) {
|
||||
const path = currentRoute.value?.path;
|
||||
if (!path) return;
|
||||
activePath.value = await getCurrentParentPath(path);
|
||||
// hanldeModuleClick(parentPath);
|
||||
if (unref(getIsMixSidebar)) {
|
||||
const activeMenu = unref(menuModules).find((item) => item.path === unref(activePath));
|
||||
const p = activeMenu?.path;
|
||||
if (p) {
|
||||
const children = await getChildrenMenus(p);
|
||||
if (setChildren) {
|
||||
childrenMenus.value = children;
|
||||
|
||||
if (unref(getMixSideFixed)) {
|
||||
openMenu.value = children.length > 0;
|
||||
}
|
||||
}
|
||||
if (children.length === 0) {
|
||||
childrenMenus.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuClick(path: string) {
|
||||
go(path);
|
||||
}
|
||||
|
||||
function handleClickOutside() {
|
||||
setActive(true);
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
function getItemEvents(item: Menu) {
|
||||
if (unref(getMixSideTrigger) === 'hover') {
|
||||
return {
|
||||
onMouseenter: () => handleModuleClick(item.path, true),
|
||||
onClick: async () => {
|
||||
const children = await getChildrenMenus(item.path);
|
||||
if (item.path && (!children || children.length === 0)) go(item.path);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
onClick: () => handleModuleClick(item.path),
|
||||
};
|
||||
}
|
||||
|
||||
function handleFixedMenu() {
|
||||
setMenuSetting({
|
||||
mixSideFixed: !unref(getIsFixed),
|
||||
});
|
||||
}
|
||||
|
||||
// Close menu
|
||||
function closeMenu() {
|
||||
if (!unref(getIsFixed)) {
|
||||
openMenu.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边折叠导航模式区分彩色模式
|
||||
watch(
|
||||
() => appStore.getProjectConfig.menuSetting,
|
||||
(menuSetting) => {
|
||||
isThemeBright.value = !!menuSetting?.isThemeBright;
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边折叠导航模式区分彩色模式
|
||||
|
||||
return {
|
||||
t,
|
||||
prefixCls,
|
||||
menuModules,
|
||||
handleModuleClick: handleModuleClick,
|
||||
activePath,
|
||||
childrenMenus: childrenMenus,
|
||||
getShowDragBar,
|
||||
handleMenuClick,
|
||||
getMenuStyle,
|
||||
handleClickOutside,
|
||||
sideRef,
|
||||
dragBarRef,
|
||||
shortTitle,
|
||||
openMenu,
|
||||
getMenuTheme,
|
||||
getItemEvents,
|
||||
getMenuEvents,
|
||||
getDomStyle,
|
||||
handleFixedMenu,
|
||||
getMixSideFixed,
|
||||
getWrapStyle,
|
||||
getCollapsed,
|
||||
isThemeBright,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-mix-sider';
|
||||
@width: 80px;
|
||||
.@{prefix-cls} {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: @layout-mix-sider-fixed-z-index;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: @sider-dark-bg-color;
|
||||
transition: all 0.2s ease 0s;
|
||||
|
||||
&-dom {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease 0s;
|
||||
}
|
||||
|
||||
&-logo {
|
||||
display: flex;
|
||||
height: @header-height;
|
||||
padding-left: 0 !important;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: @logo-width;
|
||||
height: @logo-width;
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
.@{prefix-cls}-logo {
|
||||
border-bottom: 1px solid rgb(238, 238, 238);
|
||||
}
|
||||
|
||||
&.open {
|
||||
> .scrollbar {
|
||||
border-right: 1px solid rgb(238, 238, 238);
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-module {
|
||||
&__item {
|
||||
font-weight: normal;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
|
||||
&--active {
|
||||
color: @primary-color;
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
.@{prefix-cls}-menu-list {
|
||||
&__content {
|
||||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&__title {
|
||||
.pushpin {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@border-color: @sider-dark-lighten-bg-color;
|
||||
|
||||
&.dark {
|
||||
&.open {
|
||||
.@{prefix-cls}-logo {
|
||||
// border-bottom: 1px solid @border-color;
|
||||
}
|
||||
|
||||
> .scrollbar {
|
||||
border-right: 1px solid @border-color;
|
||||
}
|
||||
}
|
||||
.@{prefix-cls}-menu-list {
|
||||
background-color: @sider-dark-bg-color;
|
||||
|
||||
&__title {
|
||||
color: @white;
|
||||
border-bottom: none;
|
||||
border-bottom: 1px solid @border-color;
|
||||
}
|
||||
}
|
||||
// 侧边折叠导航彩色模式文字颜色
|
||||
&.bright {
|
||||
.@{prefix-cls}-module {
|
||||
&__item {
|
||||
font-weight: normal;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .scrollbar {
|
||||
height: calc(100% - @header-height - 38px);
|
||||
}
|
||||
|
||||
&.mini &-module {
|
||||
&__name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-module {
|
||||
position: relative;
|
||||
padding-top: 1px;
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
padding: 12px 0;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: @white;
|
||||
}
|
||||
// &:hover,
|
||||
&--active {
|
||||
font-weight: 700;
|
||||
color: @white;
|
||||
background-color: @sider-dark-darken-bg-color;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: @primary-color;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 8px;
|
||||
font-size: 24px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
&-trigger {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: @trigger-dark-bg-color;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
&.light &-trigger {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
&-menu-list {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 200px;
|
||||
height: calc(100%);
|
||||
background-color: #fff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
height: @header-height;
|
||||
// margin-left: -6px;
|
||||
font-size: 18px;
|
||||
color: @primary-color;
|
||||
border-bottom: 1px solid rgb(238, 238, 238);
|
||||
opacity: 0;
|
||||
transition: unset;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
text-align: center;
|
||||
.text {flex: 1;}
|
||||
|
||||
&.show {
|
||||
min-width: 130px;
|
||||
opacity: 1;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.pushpin {
|
||||
margin-right: 6px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{namespace}-simple-menu-sub-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
height: calc(100% - @header-height) !important;
|
||||
|
||||
.scrollbar__wrap {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.scrollbar__bar.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-menu-inline,
|
||||
.ant-menu-vertical,
|
||||
.ant-menu-vertical-left {
|
||||
border-right: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-drag-bar {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: -1px;
|
||||
width: 1px;
|
||||
height: calc(100% - 50px);
|
||||
cursor: ew-resize;
|
||||
background-color: #f8f8f9;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
jeecgboot-vue3/src/layouts/default/sider/index.vue
Normal file
57
jeecgboot-vue3/src/layouts/default/sider/index.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<Drawer
|
||||
v-if="getIsMobile"
|
||||
placement="left"
|
||||
:class="prefixCls"
|
||||
:width="getMenuWidth"
|
||||
:getContainer="null"
|
||||
:open="!getCollapsed"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Sider />
|
||||
</Drawer>
|
||||
<MixSider v-else-if="getIsMixSidebar" />
|
||||
<Sider v-else />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import Sider from './LayoutSider.vue';
|
||||
import MixSider from './MixSider.vue';
|
||||
import { Drawer } from 'ant-design-vue';
|
||||
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
export default defineComponent({
|
||||
name: 'SiderWrapper',
|
||||
components: { Sider, Drawer, MixSider },
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('layout-sider-wrapper');
|
||||
const { getIsMobile } = useAppInject();
|
||||
const { setMenuSetting, getCollapsed, getMenuWidth, getIsMixSidebar } = useMenuSetting();
|
||||
|
||||
function handleClose() {
|
||||
setMenuSetting({
|
||||
collapsed: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { prefixCls, getIsMobile, getCollapsed, handleClose, getMenuWidth, getIsMixSidebar };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-layout-sider-wrapper';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.ant-drawer-body {
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ant-drawer-header-no-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
jeecgboot-vue3/src/layouts/default/sider/useLayoutSider.ts
Normal file
133
jeecgboot-vue3/src/layouts/default/sider/useLayoutSider.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, unref, onMounted, nextTick, ref } from 'vue';
|
||||
|
||||
import { TriggerEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* Handle related operations of menu events
|
||||
*/
|
||||
export function useSiderEvent() {
|
||||
const brokenRef = ref(false);
|
||||
|
||||
const { getMiniWidthNumber } = useMenuSetting();
|
||||
|
||||
const getCollapsedWidth = computed(() => {
|
||||
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
|
||||
});
|
||||
|
||||
function onBreakpointChange(broken: boolean) {
|
||||
brokenRef.value = broken;
|
||||
}
|
||||
|
||||
return { getCollapsedWidth, onBreakpointChange };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle related operations of menu folding
|
||||
*/
|
||||
export function useTrigger(getIsMobile: Ref<boolean>) {
|
||||
const { getTrigger, getSplit } = useMenuSetting();
|
||||
|
||||
const getShowTrigger = computed(() => {
|
||||
const trigger = unref(getTrigger);
|
||||
|
||||
return trigger !== TriggerEnum.NONE && !unref(getIsMobile) && (trigger === TriggerEnum.FOOTER || unref(getSplit));
|
||||
});
|
||||
|
||||
const getTriggerAttr = computed(() => {
|
||||
if (unref(getShowTrigger)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
trigger: null,
|
||||
};
|
||||
});
|
||||
|
||||
return { getTriggerAttr, getShowTrigger };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle menu drag and drop related operations
|
||||
* @param siderRef
|
||||
* @param dragBarRef
|
||||
*/
|
||||
export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>, mix = false) {
|
||||
const { getMiniWidthNumber, getCollapsed, setMenuSetting } = useMenuSetting();
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const exec = useDebounceFn(changeWrapWidth, 80);
|
||||
exec();
|
||||
});
|
||||
});
|
||||
|
||||
function getEl(elRef: Ref<ElRef | ComponentRef>): any {
|
||||
const el = unref(elRef);
|
||||
if (!el) return null;
|
||||
if (Reflect.has(el, '$el')) {
|
||||
return (unref(elRef) as ComponentRef)?.$el;
|
||||
}
|
||||
return unref(elRef);
|
||||
}
|
||||
|
||||
function handleMouseMove(ele: HTMLElement, wrap: HTMLElement, clientX: number) {
|
||||
document.onmousemove = function (innerE) {
|
||||
let iT = (ele as any).left + (innerE.clientX - clientX);
|
||||
innerE = innerE || window.event;
|
||||
const maxT = 800;
|
||||
const minT = unref(getMiniWidthNumber);
|
||||
iT < 0 && (iT = 0);
|
||||
iT > maxT && (iT = maxT);
|
||||
iT < minT && (iT = minT);
|
||||
ele.style.left = wrap.style.width = iT + 'px';
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// Drag and drop in the menu area-release the mouse
|
||||
function removeMouseup(ele: any) {
|
||||
const wrap = getEl(siderRef);
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = null;
|
||||
document.onmouseup = null;
|
||||
wrap.style.transition = 'width 0.2s';
|
||||
const width = parseInt(wrap.style.width);
|
||||
|
||||
if (!mix) {
|
||||
const miniWidth = unref(getMiniWidthNumber);
|
||||
if (!unref(getCollapsed)) {
|
||||
width > miniWidth + 20 ? setMenuSetting({ menuWidth: width }) : setMenuSetting({ collapsed: true });
|
||||
} else {
|
||||
width > miniWidth && setMenuSetting({ collapsed: false, menuWidth: width });
|
||||
}
|
||||
} else {
|
||||
setMenuSetting({ menuWidth: width });
|
||||
}
|
||||
|
||||
ele.releaseCapture?.();
|
||||
};
|
||||
}
|
||||
|
||||
function changeWrapWidth() {
|
||||
const ele = getEl(dragBarRef);
|
||||
if (!ele) return;
|
||||
const wrap = getEl(siderRef);
|
||||
if (!wrap) return;
|
||||
|
||||
ele.onmousedown = (e: any) => {
|
||||
wrap.style.transition = 'unset';
|
||||
const clientX = e?.clientX;
|
||||
ele.left = ele.offsetLeft;
|
||||
handleMouseMove(ele, wrap, clientX);
|
||||
removeMouseup(ele);
|
||||
ele.setCapture?.();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<span :class="`${prefixCls}__extra-fold`" @click="handleFold">
|
||||
<Icon :icon="getIcon" />
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, unref, computed } from 'vue';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { triggerWindowResize } from '/@/utils/event';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FoldButton',
|
||||
components: { Icon },
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('multiple-tabs-content');
|
||||
const { getShowMenu, setMenuSetting } = useMenuSetting();
|
||||
const { getShowHeader, setHeaderSetting } = useHeaderSetting();
|
||||
|
||||
const getIsUnFold = computed(() => !unref(getShowMenu) && !unref(getShowHeader));
|
||||
|
||||
const getIcon = computed(() => (unref(getIsUnFold) ? 'codicon:screen-normal' : 'codicon:screen-full'));
|
||||
|
||||
function handleFold() {
|
||||
const isUnFold = unref(getIsUnFold);
|
||||
setMenuSetting({
|
||||
show: isUnFold,
|
||||
hidden: !isUnFold,
|
||||
});
|
||||
setHeaderSetting({ show: isUnFold });
|
||||
triggerWindowResize();
|
||||
}
|
||||
|
||||
return { prefixCls, getIcon, handleFold };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menuEvent="handleMenuEvent" :overlayClassName="prefixCls">
|
||||
<div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="getIsTabs">
|
||||
<!-- updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式:增加前缀图标 -->
|
||||
<!-- <span v-if="showPrefixIcon" :class="`${prefixCls}__prefix-icon`" @click="handleContext">
|
||||
<Icon :icon="prefixIconType" :size="14" />
|
||||
</span> -->
|
||||
<span class="ml-1">{{ getTitle }}</span>
|
||||
</div>
|
||||
<span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
|
||||
<Icon icon="ion:chevron-down" />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
import { defineComponent, computed, unref } from 'vue';
|
||||
import { Dropdown } from '/@/components/Dropdown/index';
|
||||
import { Icon } from '/@/components/Icon';
|
||||
|
||||
import { TabContentProps } from '../types';
|
||||
|
||||
import { TabsThemeEnum } from '/@/enums/appEnum';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useTabDropdown } from '../useTabDropdown';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
import { useLocaleStore } from '/@/store/modules/locale';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabContent',
|
||||
components: { Dropdown, Icon },
|
||||
props: {
|
||||
tabItem: {
|
||||
type: Object as PropType<RouteLocationNormalized>,
|
||||
default: null,
|
||||
},
|
||||
isExtra: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const { prefixCls } = useDesign('multiple-tabs-content');
|
||||
const { t } = useI18n();
|
||||
|
||||
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
|
||||
const localeStore = useLocaleStore();
|
||||
const getTitle = computed(() => {
|
||||
const { tabItem: { meta, fullPath } = {} } = props;
|
||||
let title = localeStore.getPathTitle(fullPath);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
return meta && t(meta.title as string);
|
||||
});
|
||||
//update-end-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
|
||||
|
||||
const getIsTabs = computed(() => !props.isExtra);
|
||||
|
||||
// updateBy:sunjianlei---updateDate:2021-09-03---修改tab切换栏样式:前缀图标类型
|
||||
const prefixIconType = computed(() => {
|
||||
if (props.tabItem.meta.icon) {
|
||||
return props.tabItem.meta.icon;
|
||||
} else if (props.tabItem.path === '/dashboard/analysis') {
|
||||
// 当是首页时返回 home 图标 TODO 此处可能需要动态判断首页路径
|
||||
return 'ant-design:home-outlined';
|
||||
} else {
|
||||
return 'ant-design:code';
|
||||
}
|
||||
});
|
||||
|
||||
const getTrigger = computed((): ('contextmenu' | 'click' | 'hover')[] => (unref(getIsTabs) ? ['contextmenu'] : ['click']));
|
||||
|
||||
const { getDropMenuList, handleMenuEvent, handleContextMenu } = useTabDropdown(props as TabContentProps, getIsTabs);
|
||||
|
||||
function handleContext(e) {
|
||||
props.tabItem && handleContextMenu(props.tabItem)(e);
|
||||
}
|
||||
|
||||
const { getTabsTheme } = useMultipleTabSetting();
|
||||
// 是否显示图标
|
||||
const showPrefixIcon = computed(() => unref(getTabsTheme) === TabsThemeEnum.SMOOTH);
|
||||
|
||||
return {
|
||||
prefixCls,
|
||||
getDropMenuList,
|
||||
handleMenuEvent,
|
||||
handleContext,
|
||||
getTrigger,
|
||||
getIsTabs,
|
||||
getTitle,
|
||||
prefixIconType,
|
||||
showPrefixIcon,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@prefix-cls: ~'@{namespace}-multiple-tabs-content';
|
||||
.@{prefix-cls} {
|
||||
.ant-dropdown-menu-item {
|
||||
.ant-dropdown-menu-title-content {
|
||||
.anticon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
span:not(.anticon) {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<span :class="`${prefixCls}__extra-redo`" @click="handleRedo">
|
||||
<SvgIcon name="reload-01"></SvgIcon>
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { SvgIcon } from '/@/components/Icon/index';
|
||||
export default defineComponent({
|
||||
name: 'TabRedo',
|
||||
components: { SvgIcon },
|
||||
|
||||
setup() {
|
||||
const loading = ref(false);
|
||||
|
||||
const { prefixCls } = useDesign('multiple-tabs-content');
|
||||
const { refreshPage } = useTabs();
|
||||
|
||||
async function handleRedo() {
|
||||
loading.value = true;
|
||||
await refreshPage();
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
// Animation execution time
|
||||
}, 1200);
|
||||
}
|
||||
return { prefixCls, handleRedo, loading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
226
jeecgboot-vue3/src/layouts/default/tabs/index.less
Normal file
226
jeecgboot-vue3/src/layouts/default/tabs/index.less
Normal file
@ -0,0 +1,226 @@
|
||||
@prefix-cls: ~'@{namespace}-multiple-tabs';
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls} {
|
||||
.ant-tabs-tab {
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.@{prefix-cls} {
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls} {
|
||||
z-index: 10;
|
||||
height: @multiple-height + 2;
|
||||
line-height: @multiple-height + 2;
|
||||
background-color: @component-background;
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
box-shadow: 0 4px 4px rgb(0 21 41 / 8%);
|
||||
|
||||
.ant-tabs-small {
|
||||
height: calc(@multiple-height + 4px);
|
||||
}
|
||||
|
||||
.ant-tabs.ant-tabs-card {
|
||||
padding-left: 0px;
|
||||
|
||||
.ant-tabs-nav {
|
||||
height: calc(@multiple-height);
|
||||
margin: 0;
|
||||
background-color: @component-background;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
height: @multiple-height;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
height: calc(@multiple-height - 4px);
|
||||
padding-right: 12px;
|
||||
line-height: calc(@multiple-height - 4px);
|
||||
color: @text-color-base;
|
||||
background-color: @component-background;
|
||||
transition: none;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @text-color-base;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 4px;
|
||||
|
||||
.anticon-close {
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
width: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: @text-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
color: @white !important;
|
||||
background: @primary-color;
|
||||
border: 1px solid transparent;
|
||||
transition: none;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @white;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.7em;
|
||||
fill: @white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav > div:nth-child(1) {
|
||||
padding: 0 6px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
margin-right: 6px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
font-size: 12px;
|
||||
|
||||
svg {
|
||||
width: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-extra-content {
|
||||
margin-top: 2px;
|
||||
line-height: @multiple-height !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&--hide-close {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
&__extra-quick,
|
||||
&__extra-redo,
|
||||
&__extra-fold {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: @multiple-height;
|
||||
line-height: @multiple-height;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid @border-color-base;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
span[role='img'] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__extra-redo {
|
||||
span[role='img'] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: @multiple-height - 2;
|
||||
padding-left: 0;
|
||||
margin-left: -10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-dropdown-menu {
|
||||
&-title-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.@{prefix-cls} {
|
||||
&-content__info {
|
||||
width: auto;
|
||||
margin-left: 0;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item-remove {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multiple-tabs__dropdown {
|
||||
.ant-dropdown-content {
|
||||
width: 172px;
|
||||
}
|
||||
}
|
||||
177
jeecgboot-vue3/src/layouts/default/tabs/index.vue
Normal file
177
jeecgboot-vue3/src/layouts/default/tabs/index.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div :class="getWrapClass">
|
||||
<Tabs
|
||||
type="editable-card"
|
||||
size="small"
|
||||
:animated="false"
|
||||
:hideAdd="true"
|
||||
:tabBarGutter="3"
|
||||
:activeKey="activeKeyRef"
|
||||
@change="handleChange"
|
||||
@edit="handleEdit"
|
||||
>
|
||||
<template v-for="item in getTabsState" :key="item.query ? item.fullPath : item.path">
|
||||
<TabPane :closable="!(item && item.meta && item.meta.affix)">
|
||||
<template #tab>
|
||||
<TabContent :tabItem="item" />
|
||||
</template>
|
||||
</TabPane>
|
||||
</template>
|
||||
|
||||
<template #rightExtra v-if="getShowRedo || getShowQuick">
|
||||
<TabRedo v-if="getShowRedo" />
|
||||
<!-- <TabContent isExtra :tabItem="$route" v-if="getShowQuick" /> -->
|
||||
<!-- 列表页全屏
|
||||
<FoldButton v-if="getShowFold" />-->
|
||||
<!-- <FullscreenOutlined /> -->
|
||||
<router-link to="/ai" class="ai-icon">
|
||||
<a-tooltip title="AI助手" placement="left">
|
||||
<svg t="1706259688149" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2056" width="17" height="17">
|
||||
<path d="M826.368 325.632c0-7.168 2.048-10.24 10.24-10.24h123.904c7.168 0 10.24 2.048 10.24 10.24v621.568c0 7.168-2.048 10.24-10.24 10.24h-122.88c-8.192 0-10.24-4.096-10.24-10.24l-1.024-621.568z m-8.192-178.176c0-50.176 35.84-79.872 79.872-79.872 48.128 0 79.872 32.768 79.872 79.872 0 52.224-33.792 79.872-81.92 79.872-46.08 1.024-77.824-27.648-77.824-79.872zM462.848 584.704C441.344 497.664 389.12 307.2 368.64 215.04h-2.048c-16.384 92.16-58.368 247.808-92.16 369.664h188.416zM243.712 712.704l-62.464 236.544c-2.048 7.168-4.096 8.192-12.288 8.192H54.272c-8.192 0-10.24-2.048-8.192-12.288l224.256-783.36c4.096-13.312 7.168-26.624 8.192-65.536 0-6.144 2.048-8.192 7.168-8.192H450.56c6.144 0 8.192 2.048 10.24 8.192l250.88 849.92c2.048 7.168 0 10.24-7.168 10.24H573.44c-7.168 0-10.24-2.048-12.288-7.168l-65.536-236.544c1.024 1.024-251.904 0-251.904 0z" fill="#333333" p-id="19816"></path>
|
||||
</svg>
|
||||
</a-tooltip>
|
||||
</router-link>
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { RouteLocationNormalized, RouteMeta } from 'vue-router';
|
||||
|
||||
import { defineComponent, computed, unref, ref } from 'vue';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
import TabContent from './components/TabContent.vue';
|
||||
import FoldButton from './components/FoldButton.vue';
|
||||
import TabRedo from './components/TabRedo.vue';
|
||||
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
|
||||
import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
|
||||
import { REDIRECT_NAME } from '/@/router/constant';
|
||||
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import Aide from "/@/views/dashboard/ai/components/aide/index.vue"
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MultipleTabs',
|
||||
components: {
|
||||
TabRedo,
|
||||
FoldButton,
|
||||
Tabs,
|
||||
TabPane: Tabs.TabPane,
|
||||
TabContent,
|
||||
Aide,
|
||||
},
|
||||
setup() {
|
||||
const affixTextList = initAffixTabs();
|
||||
const activeKeyRef = ref('');
|
||||
|
||||
useTabsDrag(affixTextList);
|
||||
const tabStore = useMultipleTabStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const { prefixCls } = useDesign('multiple-tabs');
|
||||
const go = useGo();
|
||||
const { getShowQuick, getShowRedo, getShowFold, getTabsTheme } = useMultipleTabSetting();
|
||||
|
||||
const getTabsState = computed(() => {
|
||||
return tabStore.getTabList.filter((item) => !item.meta?.hideTab);
|
||||
});
|
||||
|
||||
const unClose = computed(() => unref(getTabsState).length === 1);
|
||||
|
||||
const getWrapClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--hide-close`]: unref(unClose),
|
||||
},
|
||||
`${prefixCls}--theme-${unref(getTabsTheme)}`,
|
||||
];
|
||||
});
|
||||
|
||||
listenerRouteChange((route) => {
|
||||
const { name } = route;
|
||||
if (name === REDIRECT_NAME || !route || !userStore.getToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { path, fullPath, meta = {} } = route;
|
||||
const { currentActiveMenu, hideTab } = meta as RouteMeta;
|
||||
const isHide = !hideTab ? null : currentActiveMenu;
|
||||
const p = isHide || fullPath || path;
|
||||
if (activeKeyRef.value !== p) {
|
||||
activeKeyRef.value = p as string;
|
||||
}
|
||||
|
||||
if (isHide) {
|
||||
const findParentRoute = router.getRoutes().find((item) => item.path === currentActiveMenu);
|
||||
|
||||
findParentRoute && tabStore.addTab(findParentRoute as unknown as RouteLocationNormalized);
|
||||
} else {
|
||||
tabStore.addTab(unref(route));
|
||||
}
|
||||
});
|
||||
|
||||
function handleChange(activeKey: any) {
|
||||
activeKeyRef.value = activeKey;
|
||||
go(activeKey, false);
|
||||
}
|
||||
|
||||
// Close the current tab
|
||||
function handleEdit(targetKey: string) {
|
||||
// Added operation to hide, currently only use delete operation
|
||||
if (unref(unClose)) {
|
||||
return;
|
||||
}
|
||||
|
||||
tabStore.closeTabByKey(targetKey, router);
|
||||
}
|
||||
return {
|
||||
prefixCls,
|
||||
unClose,
|
||||
getWrapClass,
|
||||
handleEdit,
|
||||
handleChange,
|
||||
activeKeyRef,
|
||||
getTabsState,
|
||||
getShowQuick,
|
||||
getShowRedo,
|
||||
getShowFold,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './index.less';
|
||||
@import './tabs.theme.card.less';
|
||||
@import './tabs.theme.smooth.less';
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
@prefix-cls: ~'@{namespace}-multiple-tabs';
|
||||
.@{prefix-cls} {
|
||||
:deep(.anticon) {
|
||||
display: inline-block;
|
||||
}
|
||||
.ai-icon{
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
width: 36px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
color: @text-color;
|
||||
text-align: center;
|
||||
border-left: 1px solid @border-color-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
236
jeecgboot-vue3/src/layouts/default/tabs/tabs.theme.card.less
Normal file
236
jeecgboot-vue3/src/layouts/default/tabs/tabs.theme.card.less
Normal file
@ -0,0 +1,236 @@
|
||||
// tabs卡片样式
|
||||
@prefix-cls-theme-card: ~'@{prefix-cls}.@{prefix-cls}--theme-card';
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls-theme-card} {
|
||||
.ant-tabs-tab {
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.@{prefix-cls-theme-card} {
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls-theme-card} {
|
||||
@tabHeight: calc(@multiple-card-height - 10px);
|
||||
|
||||
z-index: 10;
|
||||
height: @multiple-card-height;
|
||||
line-height: @multiple-card-height;
|
||||
background-color: @component-background;
|
||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
||||
|
||||
.ant-tabs-small {
|
||||
height: @multiple-card-height;
|
||||
}
|
||||
|
||||
.ant-tabs.ant-tabs-card {
|
||||
.ant-tabs-nav {
|
||||
height: @multiple-card-height;
|
||||
margin: 0;
|
||||
background-color: @component-background;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-left: 10px;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
height: @tabHeight;
|
||||
margin-top: 4px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
height: @tabHeight;
|
||||
line-height: @tabHeight;
|
||||
color: @text-color-base;
|
||||
background-color: @component-background;
|
||||
padding: 0 20px 0 30px;
|
||||
margin: 0 10px 0 0 !important;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @text-color-call-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//padding: 0 36px 0 30px;
|
||||
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
//update-begin---author:scott ---date:2023-08-28 for:【QQYUN-6374】UnoCSS替代windicss导致应用样式问题--
|
||||
/* top: 5px;*/
|
||||
//update-end---author:scott ---date::2023-08-28 for:【QQYUN-6374】UnoCSS替代windicss导致应用样式问题--
|
||||
left: 4px;
|
||||
|
||||
.anticon-close {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
top: 0;
|
||||
left: 6px;
|
||||
vertical-align: middle;
|
||||
line-height: 10px;
|
||||
overflow: hidden;
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: @text-color-base;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
border: none !important;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color !important;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
position: relative;
|
||||
color: @primary-color !important;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 1px solid @primary-color !important;
|
||||
font-weight: inherit;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 0;
|
||||
|
||||
svg {
|
||||
width: 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: inherit;
|
||||
fill: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav > div:nth-child(1) {
|
||||
padding: 0 6px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
font-size: 12px;
|
||||
|
||||
svg {
|
||||
width: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-extra-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
line-height: @multiple-card-height !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.@{prefix-cls}--hide-close {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-content {
|
||||
&__extra-quick,
|
||||
&__extra-redo,
|
||||
&__extra-fold {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: @multiple-card-height;
|
||||
line-height: @multiple-card-height;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid @border-color-base;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
span[role='img'] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__extra-redo {
|
||||
span[role='img'] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: @tabHeight;
|
||||
padding-left: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// tab 前缀图标样式
|
||||
&__prefix-icon {
|
||||
& .app-iconify.anticon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
jeecgboot-vue3/src/layouts/default/tabs/tabs.theme.smooth.less
Normal file
233
jeecgboot-vue3/src/layouts/default/tabs/tabs.theme.smooth.less
Normal file
@ -0,0 +1,233 @@
|
||||
// tabs圆滑样式
|
||||
@prefix-cls-theme-smooth: ~'@{prefix-cls}.@{prefix-cls}--theme-smooth';
|
||||
|
||||
html[data-theme='dark'] {
|
||||
.@{prefix-cls-theme-smooth} {
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.@{prefix-cls-theme-smooth} {
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls-theme-smooth} {
|
||||
@tabHeight: calc(@multiple-smooth-height - 12px);
|
||||
z-index: 10;
|
||||
height: @multiple-smooth-height;
|
||||
line-height: @multiple-smooth-height;
|
||||
background-color: @component-background;
|
||||
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
||||
|
||||
.ant-tabs-small {
|
||||
height: @multiple-smooth-height;
|
||||
}
|
||||
|
||||
.ant-tabs.ant-tabs-card {
|
||||
.ant-tabs-nav {
|
||||
height: @multiple-smooth-height;
|
||||
margin: 0;
|
||||
background-color: @component-background;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
padding-left: 10px;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
height: @tabHeight;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
height: @tabHeight;
|
||||
line-height: @tabHeight;
|
||||
color: @text-color-base;
|
||||
background-color: @component-background;
|
||||
transition: padding 0.3s;
|
||||
padding: 0 20px 0 26px;
|
||||
margin: 0 -14px 0 0 !important;
|
||||
mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
|
||||
mask-size: 100% 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
padding: 0 20px 0 26px;
|
||||
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
top: -1px;
|
||||
left: 8px;
|
||||
|
||||
.anticon-close {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
vertical-align: middle;
|
||||
line-height: 10px;
|
||||
overflow: hidden;
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: @text-color-base;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding: 0 30px 0 30px !important;
|
||||
}
|
||||
span{font-weight: 200;}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
&:hover {
|
||||
color: inherit;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active {
|
||||
position: relative;
|
||||
padding: 0 20px 0 26px;
|
||||
color: @primary-color !important;
|
||||
background: #f5f5f5;
|
||||
border: 0;
|
||||
z-index: 3;
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 1;
|
||||
|
||||
svg {
|
||||
width: 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: inherit;
|
||||
fill: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav > div:nth-child(1) {
|
||||
padding: 0 6px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
margin-right: -15px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
||||
.anticon-close {
|
||||
font-size: 12px;
|
||||
|
||||
svg {
|
||||
width: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-extra-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
line-height: @multiple-smooth-height !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-trigger {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.@{prefix-cls}--hide-close {
|
||||
.ant-tabs-tab-remove .anticon-close {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.@{prefix-cls}-content {
|
||||
&__extra-quick,
|
||||
&__extra-redo,
|
||||
&__extra-fold {
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: @multiple-smooth-height;
|
||||
line-height: @multiple-smooth-height;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid @border-color-base;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
span[role='img'] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__extra-redo {
|
||||
span[role='img'] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: @tabHeight;
|
||||
line-height: 32px;
|
||||
padding-left: 0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// tab 前缀图标样式
|
||||
&__prefix-icon {
|
||||
& .app-iconify.anticon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
jeecgboot-vue3/src/layouts/default/tabs/types.ts
Normal file
25
jeecgboot-vue3/src/layouts/default/tabs/types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { DropMenu } from '/@/components/Dropdown/index';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export enum TabContentEnum {
|
||||
TAB_TYPE,
|
||||
EXTRA_TYPE,
|
||||
}
|
||||
|
||||
export type { DropMenu };
|
||||
|
||||
export interface TabContentProps {
|
||||
tabItem: RouteLocationNormalized;
|
||||
type?: TabContentEnum;
|
||||
trigger?: ('click' | 'hover' | 'contextmenu')[];
|
||||
}
|
||||
|
||||
export enum MenuEventEnum {
|
||||
REFRESH_PAGE,
|
||||
CLOSE_CURRENT,
|
||||
CLOSE_LEFT,
|
||||
CLOSE_RIGHT,
|
||||
CLOSE_OTHER,
|
||||
CLOSE_ALL,
|
||||
SCALE,
|
||||
}
|
||||
78
jeecgboot-vue3/src/layouts/default/tabs/useMultipleTabs.ts
Normal file
78
jeecgboot-vue3/src/layouts/default/tabs/useMultipleTabs.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { toRaw, ref, nextTick } from 'vue';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { useSortable } from '/@/hooks/web/useSortable';
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { isNullAndUnDef } from '/@/utils/is';
|
||||
import projectSetting from '/@/settings/projectSetting';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export function initAffixTabs(): string[] {
|
||||
const affixList = ref<RouteLocationNormalized[]>([]);
|
||||
|
||||
const tabStore = useMultipleTabStore();
|
||||
const router = useRouter();
|
||||
/**
|
||||
* @description: Filter all fixed routes
|
||||
*/
|
||||
function filterAffixTabs(routes: RouteLocationNormalized[]) {
|
||||
const tabs: RouteLocationNormalized[] = [];
|
||||
routes &&
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
tabs.push(toRaw(route));
|
||||
}
|
||||
});
|
||||
return tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Set fixed tabs
|
||||
*/
|
||||
function addAffixTabs(): void {
|
||||
const affixTabs = filterAffixTabs(router.getRoutes() as unknown as RouteLocationNormalized[]);
|
||||
affixList.value = affixTabs;
|
||||
for (const tab of affixTabs) {
|
||||
tabStore.addTab({
|
||||
meta: tab.meta,
|
||||
name: tab.name,
|
||||
path: tab.path,
|
||||
} as unknown as RouteLocationNormalized);
|
||||
}
|
||||
}
|
||||
|
||||
let isAddAffix = false;
|
||||
|
||||
if (!isAddAffix) {
|
||||
addAffixTabs();
|
||||
isAddAffix = true;
|
||||
}
|
||||
return affixList.value.map((item) => item.meta?.title).filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
export function useTabsDrag(affixTextList: string[]) {
|
||||
const tabStore = useMultipleTabStore();
|
||||
const { multiTabsSetting } = projectSetting;
|
||||
const { prefixCls } = useDesign('multiple-tabs');
|
||||
nextTick(() => {
|
||||
if (!multiTabsSetting.canDrag) return;
|
||||
const el = document.querySelectorAll(`.${prefixCls} .ant-tabs-nav > div`)?.[0] as HTMLElement;
|
||||
const { initSortable } = useSortable(el, {
|
||||
filter: (e: ChangeEvent) => {
|
||||
const text = e?.target?.innerText;
|
||||
if (!text) return false;
|
||||
return affixTextList.includes(text);
|
||||
},
|
||||
onEnd: (evt) => {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
|
||||
if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
tabStore.sortTabs(oldIndex, newIndex);
|
||||
},
|
||||
});
|
||||
initSortable();
|
||||
});
|
||||
}
|
||||
169
jeecgboot-vue3/src/layouts/default/tabs/useTabDropdown.ts
Normal file
169
jeecgboot-vue3/src/layouts/default/tabs/useTabDropdown.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import type { TabContentProps } from './types';
|
||||
import type { DropMenu } from '/@/components/Dropdown';
|
||||
import type { ComputedRef } from 'vue';
|
||||
|
||||
import { computed, unref, reactive } from 'vue';
|
||||
import { MenuEventEnum } from './types';
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { RouteLocationNormalized, useRouter } from 'vue-router';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
|
||||
export function useTabDropdown(tabContentProps: TabContentProps, getIsTabs: ComputedRef<boolean>) {
|
||||
const state = reactive({
|
||||
current: null as Nullable<RouteLocationNormalized>,
|
||||
currentIndex: 0,
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const tabStore = useMultipleTabStore();
|
||||
const { currentRoute } = useRouter();
|
||||
const { refreshPage, closeAll, close, closeLeft, closeOther, closeRight } = useTabs();
|
||||
|
||||
const getTargetTab = computed((): RouteLocationNormalized => {
|
||||
return unref(getIsTabs) ? tabContentProps.tabItem : unref(currentRoute);
|
||||
});
|
||||
|
||||
/**
|
||||
* @description: drop-down list
|
||||
*/
|
||||
const getDropMenuList = computed(() => {
|
||||
if (!unref(getTargetTab)) {
|
||||
return;
|
||||
}
|
||||
const { meta } = unref(getTargetTab);
|
||||
const { path } = unref(currentRoute);
|
||||
|
||||
// Refresh button
|
||||
const curItem = state.current;
|
||||
|
||||
const isCurItem = curItem ? curItem.path === path : false;
|
||||
const index = state.currentIndex;
|
||||
const refreshDisabled = !isCurItem;
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
// Close left
|
||||
const closeLeftDisabled = () => {
|
||||
if (index === 0) {
|
||||
return true;
|
||||
} else {
|
||||
// 【TV360X-1039】当只有首页和另一个tab页时关闭左侧禁用
|
||||
const validTabList = tabStore.getTabList.filter((item) => !item?.meta?.affix);
|
||||
return validTabList[0].path === state.current?.path;
|
||||
}
|
||||
};
|
||||
// Close other
|
||||
const closeOtherDisabled = () => {
|
||||
if (tabStore.getTabList.length === 1) {
|
||||
return true;
|
||||
} else {
|
||||
// 【TV360X-1039】当只有首页和另一个tab页时关闭其它禁用
|
||||
const validTabList = tabStore.getTabList.filter((item) => !item?.meta?.affix);
|
||||
return validTabList.length == 1;
|
||||
}
|
||||
};
|
||||
|
||||
// Close right
|
||||
const closeRightDisabled = index === tabStore.getTabList.length - 1 && tabStore.getLastDragEndIndex >= 0;
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
const dropMenuList: DropMenu[] = [
|
||||
{
|
||||
icon: 'jam:refresh-reverse',
|
||||
event: MenuEventEnum.REFRESH_PAGE,
|
||||
text: t('layout.multipleTab.reload'),
|
||||
disabled: refreshDisabled,
|
||||
divider: true,
|
||||
},
|
||||
// {
|
||||
// icon: 'ic:twotone-close',
|
||||
// event: MenuEventEnum.CLOSE_CURRENT,
|
||||
// text: t('layout.multipleTab.close'),
|
||||
// disabled: !!meta?.affix || disabled,
|
||||
// divider: true,
|
||||
// },
|
||||
{
|
||||
icon: 'mdi:arrow-left',
|
||||
event: MenuEventEnum.CLOSE_LEFT,
|
||||
text: t('layout.multipleTab.closeLeft'),
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
disabled: closeLeftDisabled(),
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
divider: false,
|
||||
},
|
||||
{
|
||||
icon: 'mdi:arrow-right',
|
||||
event: MenuEventEnum.CLOSE_RIGHT,
|
||||
text: t('layout.multipleTab.closeRight'),
|
||||
disabled: closeRightDisabled,
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
icon: 'material-symbols:arrows-outward',
|
||||
event: MenuEventEnum.CLOSE_OTHER,
|
||||
text: t('layout.multipleTab.closeOther'),
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
disabled: closeOtherDisabled(),
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
},
|
||||
// {
|
||||
// icon: 'clarity:minus-line',
|
||||
// event: MenuEventEnum.CLOSE_ALL,
|
||||
// text: t('layout.multipleTab.closeAll'),
|
||||
// disabled: disabled,
|
||||
// },
|
||||
];
|
||||
|
||||
return dropMenuList;
|
||||
});
|
||||
|
||||
function handleContextMenu(tabItem: RouteLocationNormalized) {
|
||||
return (e: Event) => {
|
||||
if (!tabItem) {
|
||||
return;
|
||||
}
|
||||
e?.preventDefault();
|
||||
const index = tabStore.getTabList.findIndex((tab) => tab.path === tabItem.path);
|
||||
state.current = tabItem;
|
||||
state.currentIndex = index;
|
||||
};
|
||||
}
|
||||
|
||||
// Handle right click event
|
||||
function handleMenuEvent(menu: DropMenu): void {
|
||||
const { event } = menu;
|
||||
switch (event) {
|
||||
case MenuEventEnum.REFRESH_PAGE:
|
||||
// refresh page
|
||||
refreshPage();
|
||||
break;
|
||||
// Close current
|
||||
case MenuEventEnum.CLOSE_CURRENT:
|
||||
close(tabContentProps.tabItem);
|
||||
break;
|
||||
// Close left
|
||||
case MenuEventEnum.CLOSE_LEFT:
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
closeLeft(state.current);
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
break;
|
||||
// Close right
|
||||
case MenuEventEnum.CLOSE_RIGHT:
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
closeRight(state.current);
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
break;
|
||||
// Close other
|
||||
case MenuEventEnum.CLOSE_OTHER:
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
closeOther(state.current);
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
break;
|
||||
// Close all
|
||||
case MenuEventEnum.CLOSE_ALL:
|
||||
// update-begin--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
closeAll(state.current);
|
||||
// update-end--author:liaozhiyang---date:20240605---for:【TV360X-732】非当前页右键关闭左侧、关闭右侧、关闭其它功能正常使用
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { getDropMenuList, handleMenuEvent, handleContextMenu };
|
||||
}
|
||||
23
jeecgboot-vue3/src/layouts/default/trigger/HeaderTrigger.vue
Normal file
23
jeecgboot-vue3/src/layouts/default/trigger/HeaderTrigger.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<span :class="[prefixCls, theme]" @click="toggleCollapsed"> <MenuUnfoldOutlined v-if="getCollapsed" /> <MenuFoldOutlined v-else /> </span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HeaderTrigger',
|
||||
components: { MenuUnfoldOutlined, MenuFoldOutlined },
|
||||
props: {
|
||||
theme: propTypes.oneOf(['light', 'dark']),
|
||||
},
|
||||
setup() {
|
||||
const { getCollapsed, toggleCollapsed } = useMenuSetting();
|
||||
const { prefixCls } = useDesign('layout-header-trigger');
|
||||
return { getCollapsed, toggleCollapsed, prefixCls };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
21
jeecgboot-vue3/src/layouts/default/trigger/SiderTrigger.vue
Normal file
21
jeecgboot-vue3/src/layouts/default/trigger/SiderTrigger.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div @click.stop="toggleCollapsed">
|
||||
<DoubleRightOutlined v-if="getCollapsed" />
|
||||
<DoubleLeftOutlined v-else />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SiderTrigger',
|
||||
components: { DoubleRightOutlined, DoubleLeftOutlined },
|
||||
setup() {
|
||||
const { getCollapsed, toggleCollapsed } = useMenuSetting();
|
||||
|
||||
return { getCollapsed, toggleCollapsed };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
22
jeecgboot-vue3/src/layouts/default/trigger/index.vue
Normal file
22
jeecgboot-vue3/src/layouts/default/trigger/index.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<SiderTrigger v-if="sider" />
|
||||
<HeaderTrigger v-else :theme="theme" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import HeaderTrigger from './HeaderTrigger.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutTrigger',
|
||||
components: {
|
||||
SiderTrigger: createAsyncComponent(() => import('./SiderTrigger.vue')),
|
||||
HeaderTrigger: HeaderTrigger,
|
||||
},
|
||||
props: {
|
||||
sider: propTypes.bool.def(true),
|
||||
theme: propTypes.oneOf(['light', 'dark']),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user