新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
<template>
<div :class="[prefixCls, getLayoutContentMode]" v-loading="getOpenPageLoading && getPageLoading">
<PageLayout />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import PageLayout from '@jeesite/core/layouts/page/index.vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useTransitionSetting } from '@jeesite/core/hooks/setting/useTransitionSetting';
import { useContentViewHeight } from './useContentViewHeight';
export default defineComponent({
name: 'LayoutContent',
components: { PageLayout },
setup() {
const { prefixCls } = useDesign('layout-content');
const { getOpenPageLoading } = useTransitionSetting();
const { getLayoutContentMode, getPageLoading } = useRootSetting();
useContentViewHeight();
return {
prefixCls,
getOpenPageLoading,
getLayoutContentMode,
getPageLoading,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-layout-content';
.@{prefix-cls} {
position: relative;
flex: 1 1 auto;
min-height: 0;
padding: 12px 12px 0;
background-color: @content-bg;
&.fixed {
width: 1200px;
margin: 0 auto;
}
&-loading {
position: absolute;
top: 200px;
z-index: @page-loading-z-index;
}
}
html[data-theme='dark'] {
.@{prefix-cls} {
background: #000;
}
}
</style>

View File

@@ -0,0 +1,17 @@
import type { InjectionKey, ComputedRef } from 'vue';
import { createContext, useContext } from '@jeesite/core/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);
}

View File

@@ -0,0 +1,42 @@
import { ref, computed, unref } from 'vue';
import { createPageContext } from '@jeesite/core/hooks/component/usePageContext';
import { useWindowSizeFn } from '@jeesite/core/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,
});
}

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { defineComponent, computed, unref } from 'vue';
import { FloatButton } from 'ant-design-vue';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useHeaderSetting } from '@jeesite/core/hooks/setting/useHeaderSetting';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useUserStoreWithOut } from '@jeesite/core/store/modules/user';
import { SettingButtonPositionEnum } from '@jeesite/core/enums/appEnum';
import { createAsyncComponent } from '@jeesite/core/utils/factory/createAsyncComponent';
import SessionTimeoutLogin from '@jeesite/core/layouts/views/login/SessionTimeoutLogin.vue';
import { useFullContent } from '@jeesite/core/hooks/web/useFullContent';
export default defineComponent({
name: 'LayoutFeatures',
components: {
ABackTop: FloatButton.BackTop,
LayoutLockPage: createAsyncComponent(() => import('@jeesite/core/layouts/views/lock/index.vue')),
SettingDrawer: createAsyncComponent(() => import('@jeesite/core/layouts/default/setting/index.vue')),
SessionTimeoutLogin,
},
setup() {
const { getUseOpenBackTop, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
const userStore = useUserStoreWithOut();
const { prefixCls } = useDesign('setting-drawer-fearure');
const { getShowHeader } = useHeaderSetting();
const { getFullContent } = useFullContent();
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 />
<ABackTop v-if="getUseOpenBackTop" :target="getTarget" />
<SettingDrawer v-if="getIsFixedSettingDrawer" :class="prefixCls" />
<SessionTimeoutLogin v-if="getIsSessionTimeout" />
</template>
<style lang="less">
@prefix-cls: ~'jeesite-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>

View File

@@ -0,0 +1,91 @@
<template>
<Footer :class="prefixCls" v-if="getShowLayoutFooter" ref="footerRef">
<div :class="`${prefixCls}__links`">
<a @click="openWindow(SITE_URL)">{{ t('layout.footer.onlinePreview') }}</a>
<Icon icon="i-ant-design:github-filled" @click="openWindow(GITHUB_URL)" :class="`${prefixCls}__github`" />
<a @click="openWindow(DOC_URL)">{{ t('layout.footer.onlineDocument') }}</a>
</div>
<div>Copyright &copy;2021 <a href="https://jeesite.com" target="_blank">JeeSite</a></div>
</Footer>
</template>
<script lang="ts">
import { computed, defineComponent, unref, ref } from 'vue';
import { Layout } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { DOC_URL, GITHUB_URL, SITE_URL } from '@jeesite/core/settings/siteSetting';
import { openWindow } from '@jeesite/core/utils';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useRouter } from 'vue-router';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useLayoutHeight } from '../content/useContentViewHeight';
export default defineComponent({
name: 'LayoutFooter',
components: { Footer: Layout.Footer, Icon },
setup() {
const { t } = useI18n();
const { getShowFooter } = useRootSetting();
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-footer');
const footerRef = ref<ComponentRef>(null);
const { setFooterHeight } = useLayoutHeight();
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;
});
return {
getShowLayoutFooter,
prefixCls,
t,
DOC_URL,
GITHUB_URL,
SITE_URL,
openWindow,
footerRef,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-layout-footer';
.@{prefix-cls} {
text-align: center;
opacity: 0.7;
a {
color: @text-color-base !important;
&:hover {
opacity: 1;
}
}
&__links {
margin-bottom: 8px;
}
&__github {
margin: 0 30px;
cursor: pointer;
&:hover {
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<div :style="getPlaceholderDomStyle" v-if="getIsShowPlaceholderDom"></div>
<div :style="getWrapStyle" :class="getClass">
<LayoutHeader v-if="getShowInsetHeaderRef" />
<MultipleTabs v-if="getShowTabs" v-show="getShowTabs2" />
</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 '@jeesite/core/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useFullContent } from '@jeesite/core/hooks/web/useFullContent';
import { useMultipleTabSetting } from '@jeesite/core/hooks/setting/useMultipleTabSetting';
import { useAppInject } from '@jeesite/core/hooks/web/useAppInject';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useLayoutHeight } from '../content/useContentViewHeight';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
const HEADER_HEIGHT = 48;
const TABS_HEIGHT = 32;
const TABS_HEIGHT_LARGE = 37;
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, getTabsStyle } = useMultipleTabSetting();
const tabStore = useMultipleTabStore();
const getShowTabs = computed(() => {
return unref(getShowMultipleTab) && !unref(getFullContent);
});
const getShowTabs2 = computed(() => {
return tabStore.getTabList.length > 1;
});
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);
});
const getPlaceholderDomStyle = computed((): CSSProperties => {
let height = 0;
if ((unref(getShowFullHeaderRef) || !unref(getSplit)) && unref(getShowHeader) && !unref(getFullContent)) {
height += HEADER_HEIGHT;
}
if (unref(getShowMultipleTab) && !unref(getFullContent) && unref(getShowTabs2)) {
if (unref(getTabsStyle) == '3') {
height += TABS_HEIGHT_LARGE;
} else {
height += TABS_HEIGHT;
}
}
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,
getShowTabs2,
getShowInsetHeaderRef,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-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>

View File

@@ -0,0 +1,226 @@
<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)">
{{ 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 '@jeesite/core/router/types';
import { defineComponent, ref, watchEffect } from 'vue';
import { Breadcrumb } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useGo } from '@jeesite/core/hooks/web/usePage';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { isString } from '@jeesite/core/utils/is';
import { filter } from '@jeesite/core/utils/helper/treeHelper';
import { getMenus } from '@jeesite/core/router/menus';
import { REDIRECT_NAME } from '@jeesite/core/router/constant';
import { getAllParentPath } from '@jeesite/core/router/helper/menuHelper';
export default defineComponent({
name: 'LayoutBreadcrumb',
components: { Icon, [Breadcrumb.name as string]: 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, hideMenu } = meta;
if (!title || hideBreadcrumb || hideMenu) {
return false;
}
return true;
}).filter((item) => !item.meta?.hideBreadcrumb || !item.meta?.hideMenu);
}
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 };
},
} as any);
</script>
<style lang="less">
@prefix-cls: ~'jeesite-layout-breadcrumb';
.@{prefix-cls} {
display: flex;
padding: 0 8px;
align-items: center;
.ant-breadcrumb-overlay-link {
.anticon {
margin-right: 4px;
margin-bottom: 2px;
}
}
&--light .ant-breadcrumb {
.ant-breadcrumb-overlay-link {
color: @breadcrumb-item-normal-color;
&:hover a {
color: @primary-color;
}
a {
color: rgb(0 0 0 / 65%);
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
}
.ant-breadcrumb-link {
a,
span {
color: rgb(0 0 0 / 65%);
}
}
}
&--dark .ant-breadcrumb {
.ant-breadcrumb-overlay-link {
color: rgb(255 255 255 / 60%);
&:hover a {
color: @white;
}
a {
color: rgb(255 255 255 / 80%);
&:hover {
color: @white;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgb(255 255 255 / 80%);
}
.ant-breadcrumb-link {
a,
span {
color: rgb(255 255 255 / 80%);
}
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<Tooltip
:title="t('layout.header.tooltipErrorLog')"
placement="bottom"
:mouseEnterDelay="0.5"
@click="handleToErrorList"
>
<Badge :count="getCount" :offset="[-6, 11]" :overflowCount="99">
<Icon icon="i-ion:bug-outline" />
</Badge>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { Tooltip, Badge } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useErrorLogStore } from '@jeesite/core/store/modules/errorLog';
import { PageEnum } from '@jeesite/core/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>

View File

@@ -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 '@jeesite/core/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>

View File

@@ -0,0 +1,71 @@
<template>
<Tooltip :title="t('在线用户')" placement="bottom" :mouseEnterDelay="0.5" @click="handleToOnlineList">
<Badge :count="count" :offset="[-6, 11]" :overflowCount="99" :number-style="{ backgroundColor: '#00a65a' }">
<Icon icon="i-simple-line-icons:people" />
</Badge>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { Tooltip, Badge } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { usePermission } from '@jeesite/core/hooks/web/usePermission';
import { onlineCount } from '@jeesite/core/api/sys/online';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'JeeSiteOnlineCount',
components: { Icon, Tooltip, Badge },
setup() {
const { t } = useI18n();
const { push } = useRouter();
const { hasPermission } = usePermission();
const { createConfirm } = useMessage();
const count = ref<number>(0);
async function refreshOnlineCount() {
const data = await onlineCount();
if (data && data.message) {
if (data.result == 'false' || data.result == 'login') {
if ((window as any).rocInt) clearInterval((window as any).rocInt);
if ((window as any).ppmInt) clearInterval((window as any).ppmInt);
}
createConfirm({
title: t('sys.api.errorTip'),
content: data.message,
iconType: 'info',
onOk() {
location.reload();
},
});
return;
}
let num = Number(data || 0);
count.value = num !== num ? 0 : num;
}
onMounted(async () => {
await refreshOnlineCount(); // 先执行一次
(window as any).rocInt = setInterval(refreshOnlineCount, 180000); // 3分钟执行一次
});
function handleToOnlineList() {
if (hasPermission('sys:online:view')) {
push('/sys/online/list');
}
}
return {
t,
count,
handleToOnlineList,
};
},
});
</script>

View File

@@ -0,0 +1,17 @@
import { createAsyncComponent } from '@jeesite/core/utils/factory/createAsyncComponent';
import FullScreen from './FullScreen.vue';
import UserDropDown from './user-dropdown/index.vue';
export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.vue'));
export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
export const OnlineCount = createAsyncComponent(() => import('./OnlineCount.vue'));
export const SettingDrawer = createAsyncComponent(() => import('@jeesite/core/layouts/default/setting/index.vue'), {
loading: true,
});
export { FullScreen, UserDropDown };

View File

@@ -0,0 +1,125 @@
<template>
<BasicModal
:footer="null"
:title="t('layout.header.lockScreen')"
v-bind="$attrs"
:class="prefixCls"
@register="register"
>
<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 '@jeesite/core/hooks/web/useI18n';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { BasicModal, useModalInner } from '@jeesite/core/components/Modal';
import { BasicForm, useForm } from '@jeesite/core/components/Form';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { useLockStore } from '@jeesite/core/store/modules/lock';
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?.userName);
const [register, { closeModal }] = useModalInner();
const [registerForm, { validateFields, resetFields }] = useForm({
showActionButtonGroup: false,
labelWidth: 100,
schemas: [
{
field: 'password',
label: t('layout.header.lockScreenPassword'),
component: 'InputPassword',
required: true,
},
],
baseColProps: { md: 23, lg: 23 },
});
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 { avatarUrl } = userStore.getUserInfo;
return avatarUrl;
});
return {
t,
prefixCls,
getRealName,
register,
registerForm,
handleLock,
avatar,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-header-lock-modal';
.@{prefix-cls} {
&__entry {
position: relative;
//height: 240px;
padding: 130px 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>

View File

@@ -0,0 +1,210 @@
<template>
<a-list :class="prefixCls" bordered :pagination="getPagination">
<template v-for="item in getData" :key="item.id">
<a-list-item class="list-item" v-if="!item.titleDelete">
<a-list-item-meta @click="handleTitleClick(item)">
<template #title>
<div class="title">
<a-typography-paragraph
style="width: 100%; margin-bottom: 0 !important"
:style="{ cursor: isTitleClickable ? 'pointer' : '' }"
: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 && item.avatar.indexOf('://') != -1" class="avatar" :src="item.avatar" />
<a-avatar v-else-if="item.avatar && item.avatar.indexOf(':') != -1" class="avatar avatar-icon">
<Icon :icon="item.avatar" />
</a-avatar>
<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">
{{ 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 { ListItem } from './data';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { List, Avatar, Tag, Typography } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { isNumber } from '@jeesite/core/utils/is';
export default defineComponent({
components: {
[Avatar.name as string]: Avatar,
[List.name as string]: List,
[List.Item.name as string]: List.Item,
AListItemMeta: List.Item.Meta,
ATypographyParagraph: Typography.Paragraph,
[Tag.name as string]: Tag,
Icon,
},
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: 2,
},
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<ListItem[]>(() => {
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 as number) > 0 && list && list.length > (pageSize as number)) {
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 };
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-header-notify-list';
.ant-list.@{prefix-cls} {
&::-webkit-scrollbar {
display: none;
}
::v-deep(.ant-pagination-disabled) {
display: inline-block !important;
}
::v-deep(.ant-list-pagination) {
margin: 12px 18px !important;
}
.ant-list-item.list-item {
padding: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: rgb(0 0 0 / 3%);
}
.ant-list-item-meta {
&-title {
margin-top: 3px;
}
}
.title {
margin-bottom: 3px;
font-weight: normal;
.extra {
float: right;
margin-top: -22px;
margin-right: 0;
font-weight: normal;
.tag {
margin-right: 0;
}
}
}
.avatar {
margin: 8px 0 0 8px;
}
.avatar-icon {
background-color: @primary-color;
}
.description {
font-size: 12px;
line-height: 18px;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
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;
}
export interface TabItem {
key: string;
name: string;
count?: number;
btnHref?: string;
btnText?: string;
list: ListItem[];
unreadlist?: ListItem[];
}
export const tabListData: TabItem[] = [
{
key: '1',
name: '通知',
list: [
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 10 份新周报',
description: '',
datetime: '2022-08-09',
type: '1',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的果汁已通过第三轮面试',
description: '',
datetime: '2022-08-08',
type: '1',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
description: '',
datetime: '2022-08-07',
// read: true,
type: '1',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title:
'标题可以设置自动显示省略号本例中标题行数已设为1行如果内容超过1行将自动截断并支持tooltip显示完整标题。',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000009',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
{
id: '000000010',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
description: '',
datetime: '2022-08-07',
type: '1',
},
],
},
{
key: '2',
name: '消息',
list: [
{
id: '000000006',
avatar: 'ant-design:message-outlined',
title: '彩虹 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2022-08-07',
type: '2',
clickClose: true,
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '果汁 回复了你',
description: '这种模板用于提醒谁与你发生了互动',
datetime: '2022-08-07',
type: '2',
clickClose: true,
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description:
'请将鼠标移动到此处以便测试超长的消息在此处将如何处理。本例中设置的描述最大行数为2超过2行的描述内容将被省略并且可以通过tooltip查看完整内容',
datetime: '2022-08-07',
type: '2',
clickClose: true,
},
],
},
{
key: '3',
name: '待办',
list: [
{
id: '000000009',
avatar: '',
title: '任务名称',
description: '任务需要在 2022-01-12 20:00 前启动',
datetime: '',
extra: '未开始',
color: '',
type: '3',
},
{
id: '000000010',
avatar: '',
title: '第三方紧急代码变更',
description: '彩虹 需在 2022-01-07 前完成代码变更任务',
datetime: '',
extra: '马上到期',
color: 'red',
type: '3',
},
{
id: '000000011',
avatar: '',
title: '信息安全考试',
description: '指派竹尔于 2022-01-09 前完成更新并发布',
datetime: '',
extra: '已耗时 8 天',
color: 'gold',
type: '3',
},
{
id: '000000012',
avatar: '',
title: 'ABCD 版本发布',
description: '指派竹尔于 2022-01-09 前完成更新并发布',
datetime: '',
extra: '进行中',
color: 'blue',
type: '3',
},
],
},
];

View File

@@ -0,0 +1,95 @@
<template>
<div :class="prefixCls">
<Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
<Badge :count="count" dot :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.list.length }})</span>
</template>
<!-- 绑定title-click事件的通知列表中标题是可点击-->
<NoticeList :list="item.list" v-if="item.key === '1'" @title-click="onNoticeClick" />
<NoticeList :list="item.list" v-else />
</TabPane>
</template>
</Tabs>
</template>
</Popover>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue';
import { Popover, Tabs, Badge } from 'ant-design-vue';
import { BellOutlined } from '@ant-design/icons-vue';
import { tabListData, ListItem } from './data';
import NoticeList from './NoticeList.vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
export default defineComponent({
components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
setup() {
const { prefixCls } = useDesign('header-notify');
const { createMessage } = useMessage();
const listData = ref(tabListData);
const count = computed(() => {
let count = 0;
for (let i = 0; i < tabListData.length; i++) {
count += tabListData[i].list.length;
}
return count;
});
function onNoticeClick(record: ListItem) {
createMessage.success('你点击了通知ID=' + record.id);
// 可以直接将其标记为已读(为标题添加删除线),此处演示的代码会切换删除线状态
record.titleDelete = !record.titleDelete;
}
return {
prefixCls,
listData,
count,
onNoticeClick,
numberStyle: {},
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-header-notify';
.@{prefix-cls} {
padding-top: 2px;
&__overlay {
max-width: 360px;
.ant-popover-content {
width: 300px;
}
}
.ant-tabs-content {
width: 300px;
}
.ant-badge {
font-size: 18px;
.ant-badge-multiple-words {
padding: 0 4px;
}
svg {
width: 0.9em;
}
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<MenuItem :key="itemKey">
<span class="flex items-center">
<Icon v-if="icon" :icon="icon" class="mr-1" />
<span>{{ text }}</span>
<slot name="menuItemAfter"></slot>
</span>
</MenuItem>
</template>
<script lang="ts">
import { Menu } from 'ant-design-vue';
import { computed, defineComponent, getCurrentInstance } from 'vue';
import Icon from '@jeesite/core/components/Icon';
import { propTypes } from '@jeesite/core/utils/propTypes';
export default defineComponent({
name: 'DropdownMenuItem',
components: { MenuItem: Menu.Item, Icon },
props: {
value: propTypes.string,
text: propTypes.string,
icon: propTypes.string,
},
setup(props) {
const instance = getCurrentInstance();
const itemKey = computed(() => props.value || instance?.vnode?.props?.value);
return { itemKey };
},
});
</script>

View File

@@ -0,0 +1,423 @@
<template>
<div v-if="props.sidebar" :class="`${prefixCls}-sidebar md:hidden lg:block think gem`">
<span :class="[prefixCls, `${prefixCls}--${props.theme}`]" class="flex">
<img :class="`${prefixCls}__header`" :src="getUserInfo.avatarUrl" />
<span :class="`${prefixCls}__info`">
<span :class="`${prefixCls}__name`" class="truncate">
{{ getUserInfo.userName }}
</span>
<span :class="`${prefixCls}__btns`" class="block">
<a class="online"><Icon icon="i-fa:circle" /> {{ t('layout.header.sidebarOnline') }}</a>
<a class="logout" @click="handleLoginOut">
<Icon icon="i-fa:sign-out" /> {{ t('layout.header.sidebarLogout') }}
</a>
</span>
</span>
</span>
</div>
<Dropdown v-else placement="bottom" :overlayClassName="`${prefixCls}-dropdown-overlay`">
<span :class="[prefixCls, `${prefixCls}--${props.theme}`]" class="flex">
<img :class="`${prefixCls}__header`" :src="getUserInfo.avatarUrl" />
<span :class="`${prefixCls}__info md:hidden lg:block`">
<span :class="`${prefixCls}__name`" class="truncate">
{{ getUserInfo.userName }}
</span>
</span>
</span>
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem value="accountCenter" :text="t('sys.account.center')" icon="i-ion:person-outline" />
<MenuItem value="modifyPwd" :text="t('sys.account.modifyPwd')" icon="i-ant-design:key-outlined" />
<MenuDivider />
<MenuItem
value="doc"
:text="t('layout.header.dropdownItemDoc')"
icon="i-ion:document-text-outline"
v-if="getShowDoc"
/>
<MenuDivider v-if="getShowDoc" />
<MenuItem
v-if="getUseLockPage"
value="lock"
:text="t('layout.header.tooltipLock')"
icon="i-ion:lock-closed-outline"
/>
<MenuItem value="logout" :text="t('layout.header.dropdownItemLoginOut')" icon="i-ion:power-outline" />
<MenuDivider v-if="sysListRef.length > 0" />
<MenuItem v-if="sysListRef.length > 0" :class="`${prefixCls}-menu-subtitle`" :text="t('系统切换:')" />
<MenuItem
v-for="item in sysListRef"
:key="item.value"
:value="'sysCode-' + item.value"
:text="item.name"
:icon="sysCodeRef == item.value ? 'i-ant-design:check-outlined' : 'i-radix-icons:dot'"
/>
<template v-if="getUserInfo.postList.length > 0">
<MenuDivider />
<MenuItem :class="`${prefixCls}-menu-subtitle`" :text="t('选择岗位:')">
<template #menuItemAfter>
<Icon
v-if="postCodeRef"
icon="i-ant-design:close-circle-outlined"
class="ml-1"
@click="handleMenuClick({ key: 'postCode-' })"
:title="t('取消设置')"
/>
</template>
</MenuItem>
<MenuItem
v-for="item in getUserInfo.postList"
:key="item.postCode"
:value="'postCode-' + item.postCode"
:text="item.postName"
:icon="postCodeRef == item.postCode ? 'i-ant-design:check-outlined' : 'i-radix-icons:dot'"
/>
</template>
<template v-else-if="getUserInfo.roleList.length > 0">
<MenuDivider />
<MenuItem :class="`${prefixCls}-menu-subtitle`" :text="t('选择身份:')">
<template #menuItemAfter>
<Icon
v-if="roleCodeRef"
icon="i-ant-design:close-circle-outlined"
class="ml-1"
@click="handleMenuClick({ key: 'roleCode-' })"
:title="t('取消设置')"
/>
</template>
</MenuItem>
<MenuItem
v-for="item in getUserInfo.roleList"
:key="item.roleCode"
:value="'roleCode-' + item.roleCode"
:text="item.roleName"
:icon="roleCodeRef == item.roleCode ? 'i-ant-design:check-outlined' : 'i-radix-icons:dot'"
/>
</template>
</Menu>
</template>
</Dropdown>
<LockAction v-if="!props.sidebar" @register="registerModal" />
</template>
<script lang="ts">
import { defineComponent, computed, ref, onMounted } from 'vue';
import { Dropdown, Menu } from 'ant-design-vue';
import { DOC_URL } from '@jeesite/core/settings/siteSetting';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { useHeaderSetting } from '@jeesite/core/hooks/setting/useHeaderSetting';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useModal } from '@jeesite/core/components/Modal';
import { useGo } from '@jeesite/core/hooks/web/usePage';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { openWindow } from '@jeesite/core/utils';
import { useDict } from '@jeesite/core/components/Dict';
import { switchSys, switchRole, switchPost } from '@jeesite/core/api/sys/login';
import { PageEnum } from '@jeesite/core/enums/pageEnum';
import { Icon } from '@jeesite/core/components/Icon';
import MenuItem from './DropMenuItem.vue';
import LockAction from '../lock/LockModal.vue';
import { publicPath } from '@jeesite/core/utils/env';
type MenuEvent = 'accountCenter' | 'modifyPwd' | 'logout' | 'doc' | 'lock' | 'roleCode-';
const props = {
theme: propTypes.oneOf(['dark', 'light']),
sidebar: propTypes.bool.def(false),
};
export default defineComponent({
name: 'UserDropdown',
components: {
Dropdown,
Menu,
MenuItem,
MenuDivider: Menu.Divider,
LockAction,
Icon,
},
props,
setup(props: any) {
const { prefixCls } = useDesign('header-user-dropdown');
const { t } = useI18n();
const { getShowDoc, getUseLockPage } = useHeaderSetting();
const userStore = useUserStore();
const go = useGo();
const sysCodeRef = ref<string>('default');
const sysListRef = ref<Recordable[]>([]);
const roleCodeRef = ref<string>('');
const postCodeRef = ref<string>('');
const getUserInfo = computed(() => {
const { userName = '', avatarUrl, remarks, roleList, postList } = userStore.getUserInfo || {};
return {
userName,
avatarUrl,
remarks,
roleList: (roleList || []).filter((e) => e.isShow == '1'),
postList: postList || [],
};
});
if (!props.sidebar) {
onMounted(async () => {
sysCodeRef.value = userStore.getPageCacheByKey('sysCode', 'default');
roleCodeRef.value = userStore.getPageCacheByKey('roleCode', '');
postCodeRef.value = userStore.getPageCacheByKey('postCode', '');
const sysList = await useDict().initGetDictList('sys_menu_sys_code');
if (sysList.length > 1) {
var sysCodes: string[] = [];
for (let role of getUserInfo.value.roleList) {
if (role.sysCodes) {
for (let code of role.sysCodes.split(',')) {
if (code != '') sysCodes.push(code);
}
}
}
sysListRef.value = sysCodes.length === 0 ? sysList : sysList.filter((e) => sysCodes.includes(e.value));
}
});
}
const [registerModal, { openModal }] = useModal();
function handleLoginOut() {
userStore.confirmLoginOut();
}
function handleAccountCenter() {
go('/account/center');
}
function handleModifyPwd() {
go('/account/modPwd');
}
function handleOpenDoc() {
openWindow(DOC_URL);
}
function handleLock() {
openModal(true);
}
async function handleMenuClick(e: { key: MenuEvent } | any) {
switch (e.key) {
case 'accountCenter':
handleAccountCenter();
break;
case 'modifyPwd':
handleModifyPwd();
break;
case 'logout':
handleLoginOut();
break;
case 'doc':
handleOpenDoc();
break;
case 'lock':
handleLock();
break;
default:
const sysCodePrefix = 'sysCode-';
if (String(e.key).startsWith(sysCodePrefix)) {
const sysCode = String(e.key).substring(sysCodePrefix.length);
await switchSys(sysCode);
await userStore.getUserInfoAction();
location.href = publicPath + PageEnum.BASE_HOME;
}
const roleCodePrefix = 'roleCode-';
if (String(e.key).startsWith(roleCodePrefix)) {
const roleCode = String(e.key).substring(roleCodePrefix.length);
await switchRole(roleCode);
await userStore.getUserInfoAction();
location.href = publicPath + PageEnum.BASE_HOME;
}
const postCodePrefix = 'postCode-';
if (String(e.key).startsWith(postCodePrefix)) {
const postCode = String(e.key).substring(postCodePrefix.length);
await switchPost(postCode);
await userStore.getUserInfoAction();
location.href = publicPath + PageEnum.BASE_HOME;
}
break;
}
}
return {
prefixCls,
t,
getUserInfo,
handleMenuClick,
getShowDoc,
registerModal,
getUseLockPage,
handleLoginOut,
sysCodeRef,
sysListRef,
roleCodeRef,
postCodeRef,
props,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-header-user-dropdown';
@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7);
.@{prefix-cls} {
height: @header-height;
padding: 0 10px !important;
overflow: hidden;
font-size: 12px;
cursor: pointer;
align-items: center;
img {
width: 24px;
height: 24px;
margin-right: 10px;
background: #eee;
}
&__header {
border-radius: 50%;
}
&__name {
font-size: 14px;
}
&--dark {
&:hover {
background-color: @header-dark-bg-hover-color;
}
.@{prefix-cls}__info {
color: @menu-dark-subsidiary-color;
}
.@{prefix-cls}__desc {
color: @menu-dark-subsidiary-color;
}
}
&--light {
&:hover {
background-color: @header-light-bg-hover-color;
}
.@{prefix-cls}__info {
color: @text-color-base;
}
.@{prefix-cls}__desc {
color: @header-light-desc-color;
}
}
&-dropdown-overlay {
.ant-dropdown-menu-item {
min-width: 115px;
}
}
&-menu-subtitle {
line-height: 13px;
span {
font-weight: bold;
opacity: 0.7;
svg {
padding-top: 3px;
}
}
}
&-sidebar {
.@{prefix-cls} {
height: auto;
cursor: default;
padding: 8px 10px 10px !important;
img {
width: 45px;
height: 45px;
transition: all 0.1s;
}
&__name {
font-weight: bold;
}
&__btns {
padding-top: 3px;
font-size: 11px;
white-space: nowrap;
.anticon {
padding-right: 2px;
}
.online {
padding-right: 9px;
.anticon {
color: #3c763d;
}
}
.logout {
.anticon {
color: #a94442;
}
}
}
&--dark {
color: @menu-dark-subsidiary-color;
a {
color: @menu-dark-subsidiary-color;
}
&:hover {
background-color: transparent;
}
}
&--light {
color: @text-color-base;
a {
color: @text-color-base;
}
&:hover {
background-color: transparent;
}
}
}
}
}
.ant-layout-sider-collapsed {
.@{prefix-cls} {
padding: 10px 0;
justify-content: center;
img {
width: 25px;
height: 25px;
margin: 0;
}
&__info {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,242 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author Vben、ThinkGem
*/
@header-trigger-prefix-cls: ~'jeesite-layout-header-trigger';
@header-prefix-cls: ~'jeesite-layout-header';
@breadcrumb-prefix-cls: ~'jeesite-layout-breadcrumb';
@logo-prefix-cls: ~'jeesite-app-logo';
.ant-layout .ant-layout-header.@{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;
}
@media (max-width: @screen-md) {
.ant-layout .ant-layout-header.@{header-prefix-cls} {
overflow: auto hidden;
}
}
.@{header-prefix-cls} {
&--mobile {
.@{breadcrumb-prefix-cls},
.error-action,
.notify-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;
font-weight: bold;
justify-content: center;
img {
width: @logo-width;
height: @logo-width;
margin-right: 2px;
}
}
&-left {
display: flex;
height: 100%;
align-items: center;
flex-shrink: 0;
.@{header-trigger-prefix-cls} {
display: flex;
height: 100%;
padding: 1px 10px 0;
cursor: pointer;
align-items: center;
.anticon {
font-size: 16px;
}
&.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;
// overflow-x: hidden;
// position: relative;
}
&-action {
display: flex;
min-width: 180px;
// padding-right: 12px;
align-items: center;
flex-shrink: 0;
> div,
> span {
display: flex !important;
padding: 0 2px;
height: calc(@header-height - 10px);
line-height: calc(@header-height - 10px);
border-radius: 6px;
font-size: 1.2em;
cursor: pointer;
align-items: center;
&.ant-badge {
.ant-badge-dot {
top: 10px;
right: 2px;
}
.ant-badge-count {
min-width: 14px;
height: 14px;
padding: 0 4px;
box-shadow: 0 0 0 1px #d1d1d1;
font-size: 9px;
line-height: 15px;
}
}
}
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 {
> div,
> span {
color: @text-color-base;
.jeesite-icon {
padding: 0 10px;
font-size: 20px !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 {
> div,
> span {
.jeesite-icon {
padding: 0 10px;
font-size: 20px !important;
color: @white;
}
.ant-badge {
span {
color: @white;
}
}
&:hover {
background-color: @header-dark-bg-hover-color;
}
}
}
}
}
html[data-theme='dark'] {
.@{header-prefix-cls} {
&--dark {
.@{header-prefix-cls}-action {
> div,
> span {
&:hover {
background-color: #262626;
}
}
}
}
}
}

View File

@@ -0,0 +1,197 @@
<template>
<ALayoutHeader :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" />
</div>
<!-- left end -->
<!-- menu start -->
<div :class="`${prefixCls}-menu`" v-if="getIsInitMenu && getShowTopMenu && !getIsMobile">
<LayoutMenu :isHorizontal="true" :theme="getHeaderTheme" :splitType="getSplitType" :menuMode="getMenuMode" />
</div>
<!-- menu-end -->
<!-- action -->
<div :class="`${prefixCls}-action`">
<AppSearch v-if="getShowSearch" class="switch-corp" />
<OnlineCount class="online-count" />
<Notify v-if="getShowNotice" class="notify-item" />
<ErrorAction v-if="getUseErrorHandle" class="error-action" />
<FullScreen v-if="getShowFullScreen" class="fullscreen-item" />
<UserDropDown :theme="getHeaderTheme" />
<SettingDrawer v-if="getShowSetting" />
</div>
</ALayoutHeader>
</template>
<script lang="ts">
import { defineComponent, ref, unref, computed } from 'vue';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { Layout } from 'ant-design-vue';
import { AppLogo } from '@jeesite/core/components/Application';
import { AppSearch } from '@jeesite/core/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '@jeesite/core/enums/menuEnum';
import { SettingButtonPositionEnum } from '@jeesite/core/enums/appEnum';
import { useHeaderSetting } from '@jeesite/core/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useAppInject } from '@jeesite/core/hooks/web/useAppInject';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { usePermission } from '@jeesite/core/hooks/web/usePermission';
import { useLocale } from '@jeesite/core/locales/useLocale';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { onMountedOrActivated } from '@jeesite/core/hooks/core/onMountedOrActivated';
import LayoutMenu from '../menu/index.vue';
import LayoutTrigger from '../trigger/index.vue';
import {
UserDropDown,
LayoutBreadcrumb,
FullScreen,
Notify,
ErrorAction,
OnlineCount,
SettingDrawer,
} from './components';
export default defineComponent({
name: 'LayoutHeader',
components: {
ALayoutHeader: Layout.Header,
AppLogo,
LayoutTrigger,
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
FullScreen,
Notify,
AppSearch,
ErrorAction,
OnlineCount,
SettingDrawer,
},
props: {
fixed: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('layout-header');
// 增加延迟修复Safari下首次加载顶部菜单重叠问题。
const getIsInitMenu = ref<boolean>(false);
onMountedOrActivated(() => {
setTimeout(() => {
getIsInitMenu.value = true;
}, 100);
});
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } =
useMenuSetting();
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
const {
getHeaderTheme,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
getShowHeader,
getShowSearch,
} = 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 getUseCorpModel = computed(() => {
const userStore = useUserStore();
const { hasPermission } = usePermission();
return userStore.getPageCacheByKey('useCorpModel', false) && hasPermission('sys:corpAdmin:edit');
});
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 { minWidth: `${width}px` };
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
});
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
return {
prefixCls,
getHeaderClass,
getShowHeaderLogo,
getHeaderTheme,
getShowHeaderTrigger,
getIsMobile,
getShowBread,
getShowContent,
getSplitType,
getSplit,
getMenuMode,
getIsInitMenu,
getShowTopMenu,
getShowLocalePicker,
getShowFullScreen,
getShowNotice,
getUseErrorHandle,
getLogoWidth,
getIsMixSidebar,
getShowSettingButton,
getShowSetting,
getShowSearch,
getUseCorpModel,
};
},
});
</script>
<style lang="less">
@import './index.less';
</style>

View File

@@ -0,0 +1,95 @@
<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 } from 'vue';
import { Layout } from 'ant-design-vue';
import { createAsyncComponent } from '@jeesite/core/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 '@jeesite/core/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useLockPage } from '@jeesite/core/hooks/web/useLockPage';
import { useAppInject } from '@jeesite/core/hooks/web/useAppInject';
import { switchSkin } from '@jeesite/core/api/sys/login';
export default defineComponent({
name: 'DefaultLayout',
components: {
LayoutFeatures: createAsyncComponent(() => import('@jeesite/core/layouts/default/feature/index.vue')),
LayoutFooter: createAsyncComponent(() => import('@jeesite/core/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();
switchSkin();
// 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: ~'jeesite-default-layout';
.ant-layout.@{prefix-cls} {
display: flex;
width: 100%;
min-height: 100%;
flex-direction: column;
background-color: @content-bg;
// .ant-layout {
// min-height: 100%;
// background-color: @content-bg;
// }
&-main {
width: 100%;
margin-left: 1px;
}
}
</style>

View File

@@ -0,0 +1,218 @@
<script lang="tsx">
import type { PropType, CSSProperties } from 'vue';
import { computed, defineComponent, unref, toRef } from 'vue';
import { BasicMenu } from '@jeesite/core/components/Menu';
import { SimpleMenu } from '@jeesite/core/components/SimpleMenu';
import { AppLogo } from '@jeesite/core/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum, MenuTypeEnum } from '@jeesite/core/enums/menuEnum';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { ScrollContainer } from '@jeesite/core/components/Container';
import { useGo } from '@jeesite/core/hooks/web/usePage';
import { useSplitMenu } from './useLayoutMenu';
import { openWindow } from '@jeesite/core/utils';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { isUrl } from '@jeesite/core/utils/is';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useAppInject } from '@jeesite/core/hooks/web/useAppInject';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { UserDropDown } from '../header/components';
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
*/
function handleMenuClick(path: string, item: any) {
if (item.target === '_blank') {
window.open(path);
} else {
// const url = String(item.url);
// const paramIdx = url.indexOf('?');
// if (paramIdx != -1 && !item.meta.frameSrc) {
// const params = url.substring(paramIdx);
// go(item.path + params);
// } else {
go(path);
// }
}
}
/**
* 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 renderUserInfo() {
if (unref(getMenuType) === MenuTypeEnum.SIDEBAR) return null;
return <UserDropDown theme={unref(getMenuTheme)} sidebar={true} />;
}
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}
v-slots={{
menuBefore: () => renderUserInfo(),
}}
/>
) : (
<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">
@prefix-cls: ~'jeesite-layout-menu';
@logo-prefix-cls: ~'jeesite-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>

View File

@@ -0,0 +1,112 @@
import type { Menu } from '@jeesite/core/router/types';
import type { Ref } from 'vue';
import { watch, unref, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { MenuSplitTyeEnum } from '@jeesite/core/enums/menuEnum';
import { useThrottleFn } from '@vueuse/core';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { getChildrenMenus, getCurrentParentPath, getMenus, getShallowMenus } from '@jeesite/core/router/menus';
import { usePermissionStore } from '@jeesite/core/store/modules/permission';
import { useAppInject } from '@jeesite/core/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 getSplitNotLeft = computed(() => unref(splitType) !== MenuSplitTyeEnum.LEFT && unref(getSplit));
const getSplitLeft = computed(() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT);
const getSpiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
const getNormalType = computed(() => {
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
});
watch(
[() => unref(currentRoute).path, () => unref(splitType)],
async ([path]: [string, MenuSplitTyeEnum]) => {
if (unref(getSplitNotLeft) || unref(getIsMobile)) return;
const { meta } = unref(currentRoute);
const currentPath = (meta.currentActiveMenu as string) || path;
let parentPath: string | null = await getCurrentParentPath(currentPath);
// if (parentPath) {
// sessionStorage.setItem('temp-parent-path', parentPath);
// } else {
// parentPath = sessionStorage.getItem('temp-parent-path');
// }
if (!parentPath) {
const menus = await getMenus();
parentPath = menus[0] && menus[0].path;
}
// console.log('parentPath', parentPath, path, currentActiveMenu);
parentPath && throttleHandleSplitLeftMenu(parentPath);
},
{
immediate: true,
},
);
// Menu changes
watch(
[() => permissionStore.getLastBuildMenuTime, () => permissionStore.getBackMenuList],
() => {
genMenus();
},
{
immediate: true,
},
);
// split Menu changes
watch(
() => getSplit.value,
() => {
if (unref(getSplitNotLeft)) 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(getNormalType) || unref(getIsMobile)) {
menusRef.value = await getMenus();
return;
}
// split-top
if (unref(getSpiltTop)) {
const shallowMenus = await getShallowMenus();
menusRef.value = shallowMenus;
return;
}
}
return { menusRef };
}

View File

@@ -0,0 +1,308 @@
import { defineComponent, computed, unref } from 'vue';
import { BasicDrawer } from '@jeesite/core/components/Drawer';
import { Divider } from 'ant-design-vue';
import { TypePicker, ThemeColorPicker, SettingFooter, SwitchItem, SelectItem, InputNumberItem } from './components';
import { AppDarkModeToggle } from '@jeesite/core/components/Application';
import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '@jeesite/core/enums/menuEnum';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useHeaderSetting } from '@jeesite/core/hooks/setting/useHeaderSetting';
import { useMultipleTabSetting } from '@jeesite/core/hooks/setting/useMultipleTabSetting';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { baseHandler } from './handler';
import {
HandlerEnum,
topMenuAlignOptions,
getMenuTriggerOptions,
menuTypeList,
mixSidebarTriggerOptions,
} from './enum';
import {
HEADER_PRESET_BG_COLOR_LIST,
SIDE_BAR_BG_COLOR_LIST,
APP_PRESET_COLOR_LIST,
} from '@jeesite/core/settings/designSetting';
const { t } = useI18n();
export default defineComponent({
name: 'SettingDrawer',
setup(_, { attrs }) {
const {
getShowBreadCrumb,
getShowBreadCrumbIcon,
getColorWeak,
getGrayMode,
getLockTime,
getShowDarkModeToggle,
getThemeColor,
} = useRootSetting();
const {
getIsHorizontal,
getShowMenu,
getMenuType,
getTrigger,
getCollapsedShowTitle,
getCollapsed,
getTopMenuAlign,
getAccordion,
getMenuWidth,
getMenuBgColor,
getIsTopMenu,
getSplit,
getIsMixSidebar,
getCloseMixSidebarOnChange,
getMixSideTrigger,
getMixSideFixed,
} = useMenuSetting();
const { getShowHeader, getHeaderBgColor, getShowSearch } = useHeaderSetting();
const { getShowMultipleTab, getShowQuick, getShowRedo, getShowFold } = useMultipleTabSetting();
const getShowMenuRef = computed(() => {
return unref(getShowMenu) && !unref(getIsHorizontal);
});
function renderSidebar() {
return (
<>
<TypePicker
menuTypeList={menuTypeList}
handler={(item: (typeof menuTypeList)[0]) => {
baseHandler(HandlerEnum.CHANGE_LAYOUT, {
mode: item.mode,
type: item.type,
// split: unref(getIsHorizontal) ? false : undefined,
split: item.mode === MenuModeEnum.INLINE && item.type === MenuTypeEnum.MIX,
});
}}
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.collapseMenuDisplayName')}
event={HandlerEnum.MENU_COLLAPSED_SHOW_TITLE}
def={unref(getCollapsedShowTitle)}
disabled={!unref(getShowMenuRef) || !unref(getCollapsed) || 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.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)}
/>
<InputNumberItem
title={t('layout.setting.autoScreenLock')}
min={0}
max={99999}
event={HandlerEnum.LOCK_TIME}
defaultValue={unref(getLockTime)}
formatter={(value: string) => {
return parseInt(value) === 0
? `0(${t('layout.setting.notAutoScreenLock')})`
: `${value}${t('layout.setting.minute')}`;
}}
/>
</>
);
}
function renderContent() {
return (
<>
<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`}
/>
<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.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.grayMode')} event={HandlerEnum.GRAY_MODE} def={unref(getGrayMode)} />
<SwitchItem title={t('layout.setting.colorWeak')} event={HandlerEnum.COLOR_WEAK} def={unref(getColorWeak)} />
</>
);
}
return () => (
<BasicDrawer
{...attrs}
title={t('layout.setting.drawerTitle')}
width={330}
mask={true}
maskStyle={{ animation: 'none', opacity: 0 }}
>
{/* {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.headerTheme')}</Divider>
{renderHeaderTheme()}
<Divider>{() => t('layout.setting.sysTheme')}</Divider>
{renderMainTheme()}
<Divider>{() => t('layout.setting.sidebarTheme')}</Divider>
{renderSiderTheme()}
<Divider>{() => t('layout.setting.interfaceFunction')}</Divider>
{renderFeatures()}
<Divider>{() => t('layout.setting.interfaceDisplay')}</Divider>
{renderContent()}
<Divider />
<SettingFooter />
</BasicDrawer>
);
},
});

View File

@@ -0,0 +1,52 @@
<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 '@jeesite/core/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">
@prefix-cls: ~'jeesite-setting-input-number-item';
.@{prefix-cls} {
display: flex;
justify-content: space-between;
margin: 16px 0;
color: @text-color-base;
&-input-number {
width: 126px !important;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<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 '@jeesite/core/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: any) {
props.event && baseHandler(props.event, e);
}
return {
prefixCls,
handleChange,
getBindValue,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-setting-select-item';
.@{prefix-cls} {
display: flex;
justify-content: space-between;
margin: 16px 0;
color: @text-color-base;
&-select {
width: 126px;
}
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<div :class="prefixCls">
<a-button type="primary" block @click="handleCopy">
<CopyOutlined class="mr-2" />
{{ t('layout.setting.copyBtn') }}
</a-button>
<!-- <a-button color="warning" block @click="handleResetSetting" class="mt-3">
<RedoOutlined class="mr-2" />
{{ t('common.resetText') }}
</a-button> -->
<a-button color="error" block @click="handleClearAndRedo" class="mb-2 mt-3">
<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 '@jeesite/core/store/modules/app';
// import { usePermissionStore } from '@jeesite/core/store/modules/permission';
// import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
// import { useUserStore } from '@jeesite/core/store/modules/user';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { useCopyToClipboard } from '@jeesite/core/hooks/web/useCopyToClipboard';
import { updateColorWeak } from '@jeesite/core/logics/theme/updateColorWeak';
import { updateGrayMode } from '@jeesite/core/logics/theme/updateGrayMode';
import { Persistent } from '@jeesite/core/utils/cache/persistent';
import defaultSetting from '@jeesite/core/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: any) {
createMessage.error(error);
}
location.reload();
}
function handleClearAndRedo() {
Persistent.clearAll(true);
// appStore.resetAllState();
// tabStore.resetState();
// permissionStore.resetState();
// userStore.resetState();
location.reload();
}
return {
prefixCls,
t,
handleCopy,
handleResetSetting,
handleClearAndRedo,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-setting-footer';
.@{prefix-cls} {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div :class="prefixCls">
<span>
{{ title }}
<BasicHelp v-if="helpMessage" placement="top" :text="helpMessage" />
</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 '@jeesite/core/hooks/web/useDesign';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { BasicHelp } from '@jeesite/core/components/Basic';
import { baseHandler } from '../handler';
import { HandlerEnum } from '../enum';
export default defineComponent({
name: 'SwitchItem',
components: { Switch, BasicHelp },
props: {
event: {
type: Number as PropType<HandlerEnum>,
},
disabled: {
type: Boolean,
},
title: {
type: String,
},
helpMessage: {
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: any) {
props.event && baseHandler(props.event, e);
}
return {
prefixCls,
t,
handleChange,
getBindValue,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-setting-switch-item';
.@{prefix-cls} {
display: flex;
justify-content: space-between;
margin: 16px 0;
color: @text-color-base;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div :class="prefixCls">
<template v-for="color in colorList || []" :key="color">
<span
@click="handleClick(color)"
:class="[
`${prefixCls}__item`,
{
[`${prefixCls}__item--active`]: def === color,
},
]"
:style="{ background: color }"
>
<CheckOutlined />
</span>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { CheckOutlined } from '@ant-design/icons-vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { baseHandler } from '../handler';
import { HandlerEnum } from '../enum';
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');
function handleClick(color: string) {
props.event && baseHandler(props.event, color);
}
return {
prefixCls,
handleClick,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-setting-theme-picker';
.@{prefix-cls} {
display: flex;
flex-wrap: wrap;
margin: 16px 0;
justify-content: space-around;
color: @text-color-base;
&__item {
width: 20px;
height: 20px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 2px;
svg {
display: none;
}
&--active {
border: 1px solid lighten(@primary-color, 10%);
.anticon {
vertical-align: 1px;
}
svg {
display: inline-block;
margin: 0 0 3px 3px;
font-size: 12px;
fill: #ddd !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<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 '@jeesite/core/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">
@prefix-cls: ~'jeesite-setting-menu-type-picker';
.@{prefix-cls} {
display: flex;
justify-content: space-evenly;
&__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 rgb(0 0 0 / 18%);
color: @text-color-base;
&::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>

View File

@@ -0,0 +1,8 @@
import { createAsyncComponent } from '@jeesite/core/utils/factory/createAsyncComponent';
export const TypePicker = createAsyncComponent(() => import('./TypePicker.vue'), { loading: true });
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'));

View File

@@ -0,0 +1,155 @@
import { ContentEnum, RouterTransitionEnum } from '@jeesite/core/enums/appEnum';
import {
MenuModeEnum,
MenuTypeEnum,
TopMenuAlignEnum,
TriggerEnum,
MixSidebarTriggerEnum,
} from '@jeesite/core/enums/menuEnum';
import { useI18n } from '@jeesite/core/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,
TABS_SHOW_QUICK,
TABS_SHOW_REDO,
TABS_SHOW_FOLD,
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 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.menuTypeMix'),
mode: MenuModeEnum.INLINE,
type: MenuTypeEnum.MIX,
},
{
title: t('layout.setting.menuTypeTopMenu'),
mode: MenuModeEnum.HORIZONTAL,
type: MenuTypeEnum.TOP_MENU,
},
{
title: t('layout.setting.menuTypeSidebar'),
mode: MenuModeEnum.INLINE,
type: MenuTypeEnum.SIDEBAR,
},
{
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'),
},
];

View File

@@ -0,0 +1,208 @@
import { HandlerEnum } from './enum';
import { updateHeaderBgColor, updateSidebarBgColor } from '@jeesite/core/logics/theme/updateBackground';
import { updateColorWeak } from '@jeesite/core/logics/theme/updateColorWeak';
import { updateGrayMode } from '@jeesite/core/logics/theme/updateGrayMode';
import { useAppStore } from '@jeesite/core/store/modules/app';
import { ProjectConfig } from '@jeesite/types/config';
import { changeTheme } from '@jeesite/core/logics/theme';
import { updateDarkTheme } from '@jeesite/core/logics/theme/dark';
import { useRootSetting } from '@jeesite/core/hooks/setting/useRootSetting';
import { MenuTypeEnum } from '@jeesite/core/enums/menuEnum';
import {
APP_PRESET_COLOR_LIST,
HEADER_PRESET_BG_COLOR_LIST,
SIDE_BAR_BG_COLOR_LIST,
} from '@jeesite/core/settings/designSetting';
import { ThemeEnum } from '@jeesite/core/enums/appEnum';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
export function baseHandler(event: HandlerEnum, value: any) {
const { getDarkMode } = useRootSetting();
if (
getDarkMode.value === ThemeEnum.DARK &&
(event === HandlerEnum.MENU_THEME || event === HandlerEnum.HEADER_THEME || event === HandlerEnum.CHANGE_THEME_COLOR)
) {
const { showMessage } = useMessage();
const { t } = useI18n();
showMessage(t('黑暗模式下不允许更改配色'));
return;
}
const appStore = useAppStore();
const config = handler(event, value);
appStore.setProjectConfig(config);
if (event === HandlerEnum.CHANGE_LAYOUT) {
if (value.type === MenuTypeEnum.MIX || value.type === MenuTypeEnum.TOP_MENU) {
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[0]);
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[0]);
} else if (value.type === MenuTypeEnum.SIDEBAR) {
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[1]);
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[3]);
} else if (value.type === MenuTypeEnum.MIX_SIDEBAR) {
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[0]);
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[3]);
}
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[0]);
}
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,
split,
},
headerSetting: { show: true },
};
case HandlerEnum.CHANGE_THEME_COLOR:
if (getThemeColor.value === value) {
return {};
}
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:
return { multiTabsSetting: { show: value != '0', style: value } };
case HandlerEnum.TABS_SHOW_QUICK:
return { multiTabsSetting: { showQuick: value } };
case HandlerEnum.TABS_SHOW_REDO:
return { multiTabsSetting: { showRedo: value } };
case HandlerEnum.TABS_SHOW_FOLD:
return { multiTabsSetting: { showFold: 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 {};
}
}

View File

@@ -0,0 +1,26 @@
<template>
<div @click="openDrawer(true)">
<Icon icon="i-ion:settings-outline" />
<SettingDrawer @register="register" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import SettingDrawer from './SettingDrawer';
import { Icon } from '@jeesite/core/components/Icon';
import { useDrawer } from '@jeesite/core/components/Drawer';
export default defineComponent({
name: 'SettingButton',
components: { SettingDrawer, Icon },
setup() {
const [register, { openDrawer }] = useDrawer();
return {
register,
openDrawer,
};
},
});
</script>

View 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 '@jeesite/core/hooks/web/useDesign';
import { useMenuSetting } from '@jeesite/core/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">
@prefix-cls: ~'jeesite-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 rgb(28 36 56 / 15%);
}
}
</style>

View File

@@ -0,0 +1,186 @@
<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 '@jeesite/core/layouts/default/trigger/index.vue';
import { MenuModeEnum, MenuSplitTyeEnum } from '@jeesite/core/enums/menuEnum';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useTrigger, useDragLine, useSiderEvent } from './useLayoutSider';
import { useAppInject } from '@jeesite/core/hooks/web/useAppInject';
import { useDesign } from '@jeesite/core/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: ~'jeesite-layout-sideBar';
.ant-layout .ant-layout-sider.@{prefix-cls} {
z-index: @layout-sider-fixed-z-index;
&--fixed {
position: fixed;
top: 0;
left: 0;
height: 100%;
}
&--mix {
top: @header-height;
height: calc(100% - @header-height);
}
// &.ant-layout-sider-light {
// // border-right: 1px solid @border-color-base;
// box-shadow: 1px 0 0 0 @header-light-bottom-border-color;
// }
&.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 @header-light-bottom-border-color;
}
}
.ant-layout-sider-zero-width-trigger {
top: 40%;
z-index: 10;
}
& .ant-layout-sider-trigger {
height: 36px;
line-height: 36px;
}
}
</style>

View File

@@ -0,0 +1,572 @@
<template>
<div :class="`${prefixCls}-dom`" :style="getDomStyle"></div>
<div
v-click-outside="handleClickOutside"
:style="getWrapStyle"
:class="[
prefixCls,
getMenuTheme,
{
open: openMenu,
mini: getCollapsed,
},
]"
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" dot collapseparent />
<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"> {{ title }}</span>
<Icon
:size="16"
:icon="getMixSideFixed ? 'i-ri:pushpin-2-fill' : 'i-ri:pushpin-2-line'"
class="pushpin"
@click="handleFixedMenu"
/>
</div>
<ScrollContainer :class="`${prefixCls}-menu-list__content`">
<SimpleMenu :items="childrenMenus" :theme="getMenuTheme" mixSider @menu-click="handleMenuClick" />
</ScrollContainer>
<div v-show="getShowDragBar && openMenu" :class="`${prefixCls}-drag-bar`" ref="dragBarRef"></div>
</div>
</div>
</template>
<script lang="ts">
import type { Menu } from '@jeesite/core/router/types';
import type { CSSProperties } from 'vue';
import { computed, defineComponent, onMounted, ref, unref } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { ScrollContainer } from '@jeesite/core/components/Container';
import { SimpleMenu, SimpleMenuTag } from '@jeesite/core/components/SimpleMenu';
import { Icon } from '@jeesite/core/components/Icon';
import { AppLogo } from '@jeesite/core/components/Application';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useDragLine } from './useLayoutSider';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useGo } from '@jeesite/core/hooks/web/usePage';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '@jeesite/core/enums/appEnum';
import clickOutside from '@jeesite/core/directives/clickOutside';
import { getChildrenMenus, getCurrentParentPath, getShallowMenus } from '@jeesite/core/router/menus';
import { listenerRouteChange } from '@jeesite/core/logics/mitt/routeChange';
import LayoutTrigger from '../trigger/index.vue';
import { omit } from 'lodash-es';
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 { 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 { title } = useGlobSetting();
useDragLine(sideRef, dragBarRef, true);
const getMenuStyle = computed((): CSSProperties => {
return {
width: unref(openMenu) ? `${unref(getMenuWidth)}px` : 0,
left: `${unref(getMixSideWidth)}px`,
};
});
const getIsFixed = computed(() => {
mixSideHasChildren.value = unref(childrenMenus).length > 0;
const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
if (isFixed) {
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, item: any, 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) handleMenuClick(path, item);
childrenMenus.value = [];
closeMenu();
return;
}
childrenMenus.value = children;
}
// Set the currently active menu and submenu
async function setActive(setChildren = false) {
// const path = currentRoute.value?.path;
const currRoute = unref(currentRoute);
const path = (currRoute?.meta?.currentActiveMenu as string) || currRoute?.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, item: any) {
if (item.target === '_blank') {
window.open(path);
} else {
go(path);
}
}
function handleClickOutside() {
setActive(true);
closeMenu();
}
function getItemEvents(item: Menu) {
const getItem = omit(item, 'children', 'icon', 'title', 'color', 'extend');
if (unref(getMixSideTrigger) === 'hover') {
return {
onMouseenter: () => handleModuleClick(item.path, getItem, true),
onClick: async () => {
const children = await getChildrenMenus(item.path);
if (item.path && (!children || children.length === 0)) handleMenuClick(item.path, getItem);
},
};
}
return {
onClick: () => handleModuleClick(item.path, getItem),
};
}
function handleFixedMenu() {
setMenuSetting({
mixSideFixed: !unref(getIsFixed),
});
}
// Close menu
function closeMenu() {
if (!unref(getIsFixed)) {
openMenu.value = false;
}
}
return {
t,
prefixCls,
menuModules,
handleModuleClick: handleModuleClick,
activePath,
childrenMenus: childrenMenus,
getShowDragBar,
handleMenuClick,
getMenuStyle,
handleClickOutside,
sideRef,
dragBarRef,
title,
openMenu,
getMenuTheme,
getItemEvents,
getMenuEvents,
getDomStyle,
handleFixedMenu,
getMixSideFixed,
getWrapStyle,
getCollapsed,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-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;
padding: 0 5px 5px; // 2
&-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 {
padding: 0;
width: @logo-width;
height: @logo-width;
}
}
&.light .@{prefix-cls}-logo {
border-bottom: 1px solid rgb(238 238 238);
}
> .scrollbar {
height: calc(100% - @header-height - 38px);
}
&.mini &-module {
&__name {
display: none;
}
&__icon {
margin-bottom: 0;
}
}
&-module {
position: relative;
padding-top: 3px;
&__item {
position: relative;
padding: 9px 0;
margin: 0 3px;
color: rgb(255 255 255 / 65%);
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
color: @white;
}
// &:hover,
&--active {
border-radius: 6px; // 2
// &::before { // 1
// 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;
}
}
&.light &-module {
&__item {
font-weight: normal;
color: rgb(0 0 0 / 65%);
&--active {
color: @primary-color;
background-color: fade(@primary-color, 8) !important;
}
}
}
&.dark &-module {
&__item {
&--active {
color: @white;
background-color: fade(@primary-color, 25) !important;
}
}
}
&-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 {
text-wrap: nowrap;
overflow: hidden;
}
&.show {
min-width: 130px;
opacity: 1;
transition: all 0.5s ease;
}
.pushpin {
margin-right: 6px;
color: rgb(255 255 255 / 65%);
cursor: pointer;
&:hover {
color: #fff;
}
}
}
&__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;
}
}
}
&.light &-menu-list {
&__content {
box-shadow: -1px 1px 2px 0 rgb(0 0 0 / 5%);
}
&__title {
.pushpin {
color: rgb(0 0 0 / 35%);
&:hover {
color: rgb(0 0 0 / 85%);
}
}
}
}
&.dark &-menu-list {
background-color: @sider-dark-bg-color;
&__title {
color: @white;
border-bottom: none;
border-bottom: 1px solid @sider-dark-lighten-bg-color;
}
}
&-trigger {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
font-size: 14px;
color: rgb(255 255 255 / 65%);
text-align: center;
cursor: pointer;
background-color: @trigger-dark-bg-color;
height: 36px;
line-height: 36px;
}
&.light &-trigger {
color: rgb(0 0 0 / 65%);
background-color: #fff;
border-top: 1px solid #eee;
}
&-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 rgb(28 36 56 / 15%);
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<Drawer
v-if="getIsMobile"
placement="left"
:class="prefixCls"
:width="getMenuWidth"
:getContainer="false"
: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 '@jeesite/core/hooks/web/useAppInject';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { useDesign } from '@jeesite/core/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: ~'jeesite-layout-sider-wrapper';
.@{prefix-cls} {
.ant-drawer-body {
height: 100vh;
padding: 0;
}
.ant-drawer-header-no-title {
display: none;
}
}
</style>

View File

@@ -0,0 +1,133 @@
import type { Ref } from 'vue';
import { computed, unref, onMounted, nextTick, ref } from 'vue';
import { TriggerEnum } from '@jeesite/core/enums/menuEnum';
import { useMenuSetting } from '@jeesite/core/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 {};
}

View File

@@ -0,0 +1,49 @@
<template>
<span :class="`${prefixCls}__extra-fold`" @click="handleToggle">
<Icon :icon="getIcon" />
</span>
</template>
<script lang="ts">
import { defineComponent, unref, computed, onMounted } from 'vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useHeaderSetting } from '@jeesite/core/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '@jeesite/core/hooks/setting/useMenuSetting';
import { triggerResize } from '@jeesite/core/utils/event';
export default defineComponent({
name: 'FoldButton',
components: { Icon },
setup() {
const { prefixCls } = useDesign('multiple-tabs-content');
const { getShowMenu, getShowSidebar, setMenuSetting } = useMenuSetting();
const { getShowHeader, setHeaderSetting } = useHeaderSetting();
const getIsUnFold = computed(() => !unref(getShowMenu) && !unref(getShowHeader) && !unref(getShowSidebar));
const getIcon = computed(() =>
unref(getIsUnFold) ? 'i-ant-design:fullscreen-exit-outlined' : 'i-ant-design:fullscreen-outlined',
);
function handleFold(isUnFold: boolean) {
setMenuSetting({
show: isUnFold,
hidden: !isUnFold,
});
setHeaderSetting({ show: isUnFold });
triggerResize();
}
function handleToggle() {
handleFold(unref(getIsUnFold));
}
onMounted(() => {
handleFold(true);
});
return { prefixCls, getIcon, handleToggle };
},
});
</script>

View File

@@ -0,0 +1,76 @@
<template>
<Dropdown :dropMenuList="getDropMenuList" :trigger="getTrigger" @menu-event="handleMenuEvent">
<div :class="`${prefixCls}__info`" @contextmenu="handleContext" v-if="getIsTabs">
<span class="ml-1"><Icon v-if="getIcon" :icon="getIcon" />{{ getTitle }}</span>
</div>
<span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
<Icon icon="i-ant-design:down-outlined" />
</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 '@jeesite/core/components/Dropdown';
import { Icon } from '@jeesite/core/components/Icon';
import { TabContentProps } from '../types';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useTabDropdown } from '../useTabDropdown';
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();
const getIcon = computed(() => {
const { tabItem: { meta } = {} } = props;
return meta && t(meta.tabIcon as string);
});
const getTitle = computed(() => {
const { tabItem: { meta } = {} } = props;
return meta && t(meta.title as string);
});
const getIsTabs = computed(() => !props.isExtra);
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);
}
return {
prefixCls,
getDropMenuList,
handleMenuEvent,
handleContext,
getTrigger,
getIsTabs,
getIcon,
getTitle,
};
},
});
</script>

View File

@@ -0,0 +1,33 @@
<template>
<span :class="`${prefixCls}__extra-redo`" @click="handleRedo">
<Icon icon="i-ant-design:redo-outlined" :spin="loading" />
</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useTabs } from '@jeesite/core/hooks/web/useTabs';
import { Icon } from '@jeesite/core/components/Icon';
export default defineComponent({
name: 'TabRedo',
components: { Icon },
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>

View File

@@ -0,0 +1,148 @@
<template>
<div :class="getWrapClass">
<Tabs
type="editable-card"
size="small"
:animated="false"
:hideAdd="true"
:tabBarGutter="3"
:activeKey="activeKeyRef"
@change="handleChange"
@edit="(e) => handleEdit(`${e}`)"
>
<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 || getShowFold">
<TabRedo v-if="getShowRedo" />
<TabContent isExtra :tabItem="route" v-if="getShowQuick" />
<FoldButton v-if="getShowFold" />
</template>
</Tabs>
</div>
</template>
<script lang="ts">
import { RouteLocationNormalized, RouteMeta, useRoute } 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 '@jeesite/core/hooks/web/usePage';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
import { useUserStore } from '@jeesite/core/store/modules/user';
import { initAffixTabs, useTabsDrag } from './useMultipleTabs';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useMultipleTabSetting } from '@jeesite/core/hooks/setting/useMultipleTabSetting';
import { REDIRECT_NAME } from '@jeesite/core/router/constant';
import { listenerRouteChange } from '@jeesite/core/logics/mitt/routeChange';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'MultipleTabs',
components: {
TabRedo,
FoldButton,
Tabs,
TabPane: Tabs.TabPane,
TabContent,
},
setup() {
const route = useRoute();
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 { getTabsStyle, getShowQuick, getShowRedo, getShowFold } = 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}-${unref(getTabsStyle)}`,
{
[`${prefixCls}-hide-close`]: unref(unClose),
},
];
});
listenerRouteChange((route) => {
const { name } = route;
// if (name === REDIRECT_NAME || !route || !userStore.getToken) {
if (name === REDIRECT_NAME || !route || userStore.getSessionTimeout) {
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 {
route,
prefixCls,
unClose,
getWrapClass,
handleEdit,
handleChange,
activeKeyRef,
getTabsState,
getShowQuick,
getShowRedo,
getShowFold,
};
},
});
</script>
<style lang="less">
@import './index3.less';
</style>

View File

@@ -0,0 +1,252 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author Vben、ThinkGem
*/
@prefix-cls-3: ~'jeesite-multiple-tabs-3';
@multiple-height-large: 40px; // TABS_HEIGHT_LARGE
.@{prefix-cls-3} {
z-index: 10;
height: @multiple-height-large + 2;
line-height: @multiple-height-large + 2;
.ant-tabs.ant-tabs-card {
.ant-tabs-nav {
height: @multiple-height-large;
background-color: @content-bg;
margin: 0;
border: 0;
box-shadow: none;
&::before {
border-bottom: 0;
}
.ant-tabs-tab {
height: calc(@multiple-height-large - 15px);
line-height: calc(@multiple-height-large - 13px);
color: @text-color-base;
background-color: @component-background;
transition: none;
border-radius: 4px !important;
margin: 12px 0 0;
padding-left: 10px;
padding-right: 11px;
border: 0 !important;
.ant-tabs-tab-btn {
transition: none;
}
&:hover,
.ant-tabs-tab-btn:hover {
color: @text-color-base;
}
.anticon {
opacity: 0.8;
font-size: 16px;
text-align: center;
margin-right: 5px;
svg {
fill: @text-color-base;
}
}
.ant-tabs-tab-remove {
width: 12px;
height: 23px;
color: inherit;
transition: none;
padding: 0;
.anticon {
svg {
width: 0.6em;
}
&:hover svg {
width: 0.7em;
}
}
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
&:hover {
color: @primary-color;
}
}
.ant-tabs-tab-active {
position: relative;
// padding-left: 18px;
color: @white !important;
// color: @primary-color !important;
background: fade(@primary-color, 90);
// border-color: fade(@primary-color, 25);
// height: calc(@multiple-height-large - 2px);
border: 0;
transition: none;
text-shadow: none;
// span {
// color: @white !important;
// }
.ant-tabs-tab-btn {
color: @white !important;
text-shadow: none;
}
.ant-tabs-tab-remove {
opacity: 1;
svg {
fill: @white;
}
}
}
}
.ant-tabs-nav > div:nth-child(1) {
margin-left: 13px;
.ant-tabs-tab {
margin-right: 6px !important;
&:nth-last-child(2) {
margin-right: 20px !important;
}
}
}
}
.ant-tabs-extra-content {
margin-top: 2px;
margin-right: 10px;
line-height: @multiple-height-large !important;
}
.ant-dropdown-trigger {
display: inline-flex;
}
&.jeesite-multiple-tabs-hide-close {
.ant-tabs-tab-remove {
opacity: 0 !important;
}
}
.jeesite-multiple-tabs-content {
&__info {
display: inline-block;
width: 100%;
cursor: pointer;
user-select: none;
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__extra-quick,
&__extra-redo,
&__extra-fold {
display: inline-block;
width: 30px;
// height: @multiple-height-large;
// line-height: @multiple-height-large;
padding-top: 7px;
color: @text-color-secondary;
text-align: center;
cursor: pointer;
// border-left: 1px solid @header-light-bottom-border-color;
&:hover {
color: @text-color-base;
}
span[role='img'] {
transform: rotate(90deg);
}
}
}
.ant-tabs .ant-tabs-nav {
.ant-tabs-nav-more {
padding-top: 12px;
}
}
}
.ant-tabs-dropdown-menu {
padding: 5px 0;
max-height: 300px;
&-title-content {
display: flex;
align-items: center;
padding-left: 8px;
.@{prefix-cls-3} {
&-content__info {
width: auto;
margin-left: 0;
line-height: 28px;
}
}
.anticon:not(.anticon-close) {
margin-left: -3px;
margin-right: 3px;
}
}
&-item-remove {
margin-left: auto !important;
}
}
html[data-theme='dark'] {
.@{prefix-cls-3} {
.ant-tabs.ant-tabs-card {
.ant-tabs-nav {
background: #000;
&::before {
border-bottom: 0;
}
.ant-tabs-tab {
color: #aaa !important;
background: #151515;
svg {
fill: #aaa !important;
}
&:hover,
.ant-tabs-tab-btn:hover {
color: #ddd !important;
}
}
.ant-tabs-tab-active {
background: fade(#2a50ed, 85) !important;
svg {
fill: #fff !important;
}
.ant-tabs-tab-btn,
.ant-tabs-tab-btn:hover {
color: #fff !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
import type { DropMenu } from '@jeesite/core/components/Dropdown';
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,
}

View File

@@ -0,0 +1,78 @@
import { toRaw, ref, nextTick } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { useSortable } from '@jeesite/core/hooks/web/useSortable';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
import { isNullAndUnDef } from '@jeesite/core/utils/is';
import projectSetting from '@jeesite/core/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-list`)?.[0] as HTMLElement;
const { initSortable } = useSortable(el, {
filter: (e: Event) => {
const text = (e as ChangeEvent)?.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();
});
}

View File

@@ -0,0 +1,140 @@
import type { TabContentProps } from './types';
import type { DropMenu } from '@jeesite/core/components/Dropdown';
import type { ComputedRef } from 'vue';
import { computed, unref, reactive } from 'vue';
import { MenuEventEnum } from './types';
import { useMultipleTabStore } from '@jeesite/core/store/modules/multipleTab';
import { RouteLocationNormalized, useRouter } from 'vue-router';
import { useTabs } from '@jeesite/core/hooks/web/useTabs';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useGo } from '@jeesite/core/hooks/web/usePage';
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);
});
const go = useGo();
/**
* @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 index = state.currentIndex;
const refreshDisabled = curItem ? curItem.path !== path : true;
// Close left
const closeLeftDisabled = index === 0;
const disabled = tabStore.getTabList.length === 1;
// Close right
const closeRightDisabled = index === tabStore.getTabList.length - 1 && tabStore.getLastDragEndIndex >= 0;
const dropMenuList: DropMenu[] = [
{
icon: 'i-ant-design:reload-outlined',
event: MenuEventEnum.REFRESH_PAGE,
text: t('layout.multipleTab.reload'),
disabled: refreshDisabled,
},
{
icon: 'i-ant-design:close-outlined',
event: MenuEventEnum.CLOSE_CURRENT,
text: t('layout.multipleTab.close'),
disabled: !!meta?.affix || disabled,
divider: true,
},
{
icon: 'i-ant-design:arrow-left-outlined',
event: MenuEventEnum.CLOSE_LEFT,
text: t('layout.multipleTab.closeLeft'),
disabled: closeLeftDisabled,
divider: false,
},
{
icon: 'i-ant-design:arrow-right-outlined',
event: MenuEventEnum.CLOSE_RIGHT,
text: t('layout.multipleTab.closeRight'),
disabled: closeRightDisabled,
divider: true,
},
{
icon: 'i-ant-design:pic-center-outlined',
event: MenuEventEnum.CLOSE_OTHER,
text: t('layout.multipleTab.closeOther'),
disabled: disabled,
},
{
icon: 'i-ant-design:line-outlined',
event: MenuEventEnum.CLOSE_ALL,
text: t('layout.multipleTab.closeAll'),
disabled: disabled,
},
];
return dropMenuList;
});
function handleContextMenu(tabItem: RouteLocationNormalized) {
return async (e: Event) => {
if (!tabItem) {
return;
}
e?.preventDefault();
const index = tabStore.getTabList.findIndex((tab) => tab.path === tabItem.path);
state.current = tabItem;
state.currentIndex = index;
await go(tabItem); // 右键激活页签
};
}
// 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:
closeLeft();
break;
// Close right
case MenuEventEnum.CLOSE_RIGHT:
closeRight();
break;
// Close other
case MenuEventEnum.CLOSE_OTHER:
closeOther();
break;
// Close all
case MenuEventEnum.CLOSE_ALL:
closeAll();
break;
}
}
return { getDropMenuList, handleMenuEvent, handleContextMenu };
}

View File

@@ -0,0 +1,25 @@
<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 '@jeesite/core/hooks/setting/useMenuSetting';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/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>

View 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 '@jeesite/core/hooks/setting/useMenuSetting';
export default defineComponent({
name: 'SiderTrigger',
components: { DoubleRightOutlined, DoubleLeftOutlined },
setup() {
const { getCollapsed, toggleCollapsed } = useMenuSetting();
return { getCollapsed, toggleCollapsed };
},
});
</script>

View 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 '@jeesite/core/utils/factory/createAsyncComponent';
import { propTypes } from '@jeesite/core/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>