initial commit

This commit is contained in:
陈文彬
2020-09-28 20:19:10 +08:00
commit 2f6253cfb6
436 changed files with 26843 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import type { AppRouteRecordRaw } from '/@/router/types';
import type { RouteLocationMatched } from 'vue-router';
import { defineComponent, TransitionGroup, unref, watch, ref } from 'vue';
import Breadcrumb from '/@/components/Breadcrumb/Breadcrumb.vue';
import BreadcrumbItem from '/@/components/Breadcrumb/BreadcrumbItem.vue';
import { useRouter } from 'vue-router';
import router from '/@/router';
import { PageEnum } from '/@/enums/pageEnum';
import { isBoolean } from '/@/utils/is';
import { compile } from 'path-to-regexp';
export default defineComponent({
name: 'BasicBreadcrumb',
setup() {
const itemList = ref<AppRouteRecordRaw[]>([]);
const { currentRoute, push } = useRouter();
function getBreadcrumb() {
const { matched } = unref(currentRoute);
const matchedList = matched.filter((item) => item.meta && item.meta.title).slice(1);
const firstItem = matchedList[0];
const ret = getHomeRoute(firstItem);
if (!isBoolean(ret)) {
matchedList.unshift(ret);
}
itemList.value = ((matchedList as any) as AppRouteRecordRaw[]).filter(
(item) => item.meta && item.meta.title && !item.meta.hideBreadcrumb
);
}
function getHomeRoute(firstItem: RouteLocationMatched) {
if (!firstItem || !firstItem.name) return false;
const routes = router.getRoutes();
const homeRoute = routes.find((item) => item.path === PageEnum.BASE_HOME);
if (!homeRoute) return false;
if (homeRoute.name === firstItem.name) return false;
return homeRoute;
}
function pathCompile(path: string) {
const { params } = unref(currentRoute);
const toPath = compile(path);
return toPath(params);
}
function handleItemClick(item: AppRouteRecordRaw) {
const { redirect, path, meta } = item;
if (meta.disabledRedirect) return;
if (redirect) {
push(redirect as string);
return;
}
return push(pathCompile(path));
}
watch(
() => currentRoute.value,
() => {
if (unref(currentRoute).name === 'Redirect') return;
getBreadcrumb();
},
{ immediate: true }
);
return () => (
<>
<Breadcrumb class="layout-breadcrumb">
{() => (
<>
<TransitionGroup name="breadcrumb">
{() => {
return unref(itemList).map((item) => {
const isLink = !!item.redirect && !item.meta.disabledRedirect;
return (
<BreadcrumbItem
key={item.path}
isLink={isLink}
onClick={handleItemClick.bind(null, item)}
>
{() => item.meta.title}
</BreadcrumbItem>
);
});
}}
</TransitionGroup>
</>
)}
</Breadcrumb>
</>
);
},
});

View File

@@ -0,0 +1,25 @@
import { defineComponent } from 'vue';
import { Layout } from 'ant-design-vue';
// hooks
import { ContentEnum } from '/@/enums/appEnum';
import { appStore } from '/@/store/modules/app';
// import { RouterView } from 'vue-router';
import PageLayout from '/@/layouts/page/index';
export default defineComponent({
name: 'DefaultLayoutContent',
setup() {
return () => {
const { getProjectConfig } = appStore;
const { contentMode } = getProjectConfig;
const wrapClass = contentMode === ContentEnum.FULL ? 'full' : 'fixed';
return (
<Layout.Content class={`layout-content ${wrapClass} `}>
{{
default: () => <PageLayout />,
}}
</Layout.Content>
);
};
},
});

View File

@@ -0,0 +1,154 @@
import { defineComponent, unref, computed } from 'vue';
import { Layout, Tooltip } from 'ant-design-vue';
import Logo from '/@/layouts/Logo.vue';
import UserDropdown from './UserDropdown';
import LayoutMenu from './LayoutMenu';
import { appStore } from '/@/store/modules/app';
import { MenuModeEnum, MenuSplitTyeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import LayoutBreadcrumb from './LayoutBreadcrumb';
import {
RedoOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
GithubFilled,
LockOutlined,
} from '@ant-design/icons-vue';
import { useFullscreen } from '/@/hooks/web/useFullScreen';
import { useTabs } from '/@/hooks/web/useTabs';
import { GITHUB_URL } from '/@/settings/siteSetting';
import LockAction from './actions/LockActionItem';
import { useModal } from '/@/components/Modal/index';
export default defineComponent({
name: 'DefaultLayoutHeader',
setup() {
const { refreshPage } = useTabs();
const [register, { openModal }] = useModal();
const { toggleFullscreen, isFullscreenRef } = useFullscreen();
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
function goToGithub() {
window.open(GITHUB_URL, '__blank');
}
const headerClass = computed(() => {
const theme = unref(getProjectConfigRef).headerSetting.theme;
return theme ? `layout-header__header--${theme}` : '';
});
/**
* @description: 锁定屏幕
*/
function handleLockPage() {
openModal(true);
}
return () => {
const getProjectConfig = unref(getProjectConfigRef);
const {
// useErrorHandle,
showLogo,
headerSetting: { theme: headerTheme, showRedo, showGithub, showFullScreen },
menuSetting: { mode, type: menuType, split: splitMenu, topMenuAlign },
showBreadCrumb,
} = getProjectConfig;
const isSidebarType = menuType === MenuTypeEnum.SIDEBAR;
return (
<Layout.Header
class={[
'layout-header',
'bg-white flex p-0 px-4 justify-items-center',
unref(headerClass),
]}
>
{() => (
<>
<div class="flex-grow flex justify-center items-center">
{showLogo && !isSidebarType && <Logo class={`layout-header__logo`} />}
{mode !== MenuModeEnum.HORIZONTAL && showBreadCrumb && !splitMenu && (
<LayoutBreadcrumb class="flex-grow " />
)}
{(mode === MenuModeEnum.HORIZONTAL || splitMenu) && (
<div class={[`layout-header__menu flex-grow `, `justify-${topMenuAlign}`]}>
<LayoutMenu
theme={headerTheme}
splitType={splitMenu ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE}
menuMode={splitMenu ? MenuModeEnum.HORIZONTAL : null}
showSearch={false}
/>
</div>
)}
</div>
<div class={`layout-header__action`}>
{showGithub && (
// @ts-ignore
<Tooltip>
{{
title: () => 'github',
default: () => (
<div class={`layout-header__action-item`} onClick={goToGithub}>
<GithubFilled class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{showGithub && (
// @ts-ignore
<Tooltip>
{{
title: () => '锁定屏幕',
default: () => (
<div class={`layout-header__action-item`} onClick={handleLockPage}>
<LockOutlined class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{showRedo && (
// @ts-ignore
<Tooltip>
{{
title: () => '刷新',
default: () => (
<div class={`layout-header__action-item`} onClick={refreshPage}>
<RedoOutlined class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{showFullScreen && (
// @ts-ignore
<Tooltip>
{{
title: () => (unref(isFullscreenRef) ? '退出全屏' : '全屏'),
default: () => {
const Icon: any = !unref(isFullscreenRef) ? (
<FullscreenOutlined />
) : (
<FullscreenExitOutlined />
);
return (
<div class={`layout-header__action-item`} onClick={toggleFullscreen}>
<Icon class={`layout-header__action-icon`} />
</div>
);
},
}}
</Tooltip>
)}
<UserDropdown class={`layout-header__user-dropdown`} />
</div>
<LockAction onRegister={register} />
</>
)}
</Layout.Header>
);
};
},
});

View File

@@ -0,0 +1,215 @@
import type { PropType } from 'vue';
import type { Menu } from '/@/router/types';
import { computed, defineComponent, unref, ref, onMounted, watch } from 'vue';
import { BasicMenu } from '/@/components/Menu/index';
import Logo from '/@/layouts/Logo.vue';
import { PageEnum } from '/@/enums/pageEnum';
import { MenuModeEnum, MenuSplitTyeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
// store
import { appStore } from '/@/store/modules/app';
import { menuStore } from '/@/store/modules/menu';
import {
getMenus,
getFlatMenus,
getShallowMenus,
getChildrenMenus,
getFlatChildrenMenus,
getCurrentParentPath,
} from '/@/router/menus/index';
import { useGo } from '/@/hooks/web/usePage';
import { useRouter } from 'vue-router';
import { useThrottle } from '/@/hooks/core/useThrottle';
import { permissionStore } from '/@/store/modules/permission';
// import
export default defineComponent({
name: 'DefaultLayoutMenu',
props: {
theme: {
type: String as PropType<string>,
default: '',
},
splitType: {
type: Number as PropType<MenuSplitTyeEnum>,
default: MenuSplitTyeEnum.NONE,
},
parentMenuPath: {
type: String as PropType<string>,
default: '',
},
showSearch: {
type: Boolean as PropType<boolean>,
default: true,
},
menuMode: {
type: [String] as PropType<MenuModeEnum | null>,
default: '',
},
},
setup(props) {
const menusRef = ref<Menu[]>([]);
const flatMenusRef = ref<Menu[]>([]);
const { currentRoute } = useRouter();
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
const getIsHorizontalRef = computed(() => {
return unref(getProjectConfigRef).menuSetting.mode === MenuModeEnum.HORIZONTAL;
});
const go = useGo();
onMounted(() => {
genMenus();
});
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
// watch(
// () => menuStore.getCurrentTopSplitMenuPathState,
// async (parentPath: string) => {
// throttleHandleSplitLeftMenu(parentPath);
// }
// );
watch(
[() => unref(currentRoute).path, () => props.splitType],
async ([path, splitType]: [string, MenuSplitTyeEnum]) => {
if (splitType !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontalRef)) return;
const parentPath = await getCurrentParentPath(path);
parentPath && throttleHandleSplitLeftMenu(parentPath);
},
{
immediate: true,
}
);
watch(
[() => permissionStore.getLastBuildMenuTimeState, permissionStore.getBackMenuListState],
() => {
genMenus();
}
);
watch([() => appStore.getProjectConfig.menuSetting.split], () => {
if (props.splitType !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontalRef)) return;
genMenus();
});
async function handleSplitLeftMenu(parentPath: string) {
const isSplitMenu = unref(getProjectConfigRef).menuSetting.split;
if (!isSplitMenu) return;
const { splitType } = props;
// 菜单分割模式-left
if (splitType === MenuSplitTyeEnum.LEFT) {
const children = await getChildrenMenus(parentPath);
if (!children) return;
const flatChildren = await getFlatChildrenMenus(children);
flatMenusRef.value = flatChildren;
menusRef.value = children;
}
}
async function genMenus() {
const isSplitMenu = unref(getProjectConfigRef).menuSetting.split;
// 普通模式
const { splitType } = props;
if (splitType === MenuSplitTyeEnum.NONE || !isSplitMenu) {
flatMenusRef.value = await getFlatMenus();
menusRef.value = await getMenus();
return;
}
// 菜单分割模式-top
if (splitType === MenuSplitTyeEnum.TOP) {
const parentPath = await getCurrentParentPath(unref(currentRoute).path);
menuStore.commitCurrentTopSplitMenuPathState(parentPath);
const shallowMenus = await getShallowMenus();
flatMenusRef.value = shallowMenus;
menusRef.value = shallowMenus;
return;
}
}
function handleMenuClick(menu: Menu) {
const { path } = menu;
if (path) {
const { splitType } = props;
// 菜单分割模式-top
if (splitType === MenuSplitTyeEnum.TOP) {
menuStore.commitCurrentTopSplitMenuPathState(path);
}
go(path as PageEnum);
}
}
async function beforeMenuClickFn(menu: Menu) {
const { meta: { externalLink } = {} } = menu;
if (externalLink) {
window.open(externalLink, '_blank');
return false;
}
return true;
}
function handleClickSearchInput() {
if (menuStore.getCollapsedState) {
menuStore.commitCollapsedState(false);
}
}
const showSearchRef = computed(() => {
const { showSearch, type, mode } = unref(getProjectConfigRef).menuSetting;
return (
showSearch &&
props.showSearch &&
!(type === MenuTypeEnum.MIX && mode === MenuModeEnum.HORIZONTAL)
);
});
return () => {
const {
showLogo,
menuSetting: { type: menuType, mode, theme, collapsed },
} = unref(getProjectConfigRef);
const isSidebarType = menuType === MenuTypeEnum.SIDEBAR;
const isShowLogo = showLogo && isSidebarType;
const themeData = props.theme || theme;
return (
<BasicMenu
beforeClickFn={beforeMenuClickFn}
onMenuClick={handleMenuClick}
type={menuType}
mode={props.menuMode || mode}
class="layout-menu"
theme={themeData}
showLogo={isShowLogo}
search={unref(showSearchRef)}
items={unref(menusRef)}
flatItems={unref(flatMenusRef)}
onClickSearchInput={handleClickSearchInput}
appendClass={props.splitType === MenuSplitTyeEnum.TOP}
>
{{
header: () =>
isShowLogo && (
<Logo
showTitle={!collapsed}
class={[`layout-menu__logo`, collapsed ? 'justify-center' : '', themeData]}
/>
),
}}
</BasicMenu>
);
};
},
});

View File

@@ -0,0 +1,193 @@
import { computed, defineComponent, nextTick, onMounted, ref, unref } from 'vue';
import { Layout } from 'ant-design-vue';
import SideBarTrigger from './SideBarTrigger';
import { menuStore } from '/@/store/modules/menu';
import darkMiniIMg from '/@/assets/images/sidebar/dark-mini.png';
import lightMiniImg from '/@/assets/images/sidebar/light-mini.png';
import darkImg from '/@/assets/images/sidebar/dark.png';
import lightImg from '/@/assets/images/sidebar/light.png';
import { appStore } from '/@/store/modules/app';
import { MenuModeEnum, MenuSplitTyeEnum, MenuThemeEnum } from '/@/enums/menuEnum';
import { useDebounce } from '/@/hooks/core/useDebounce';
import LayoutMenu from './LayoutMenu';
export default defineComponent({
name: 'DefaultLayoutSideBar',
setup() {
const initRef = ref(false);
const brokenRef = ref(false);
const collapseRef = ref(true);
const dragBarRef = ref<Nullable<HTMLDivElement>>(null);
const sideRef = ref<any>(null);
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
// 根据展开状态设置背景图片
const getStyle = computed((): any => {
const collapse = unref(collapseRef);
const theme = unref(getProjectConfigRef).menuSetting.theme;
let bg = '';
if (theme === MenuThemeEnum.DARK) {
bg = collapse ? darkMiniIMg : darkImg;
}
if (theme === MenuThemeEnum.LIGHT) {
bg = collapse ? lightMiniImg : lightImg;
}
return {
'background-image': `url(${bg})`,
};
});
function onCollapseChange(val: boolean) {
if (initRef.value) {
collapseRef.value = val;
menuStore.commitCollapsedState(val);
} else {
const collapsed = appStore.getProjectConfig.menuSetting.collapsed;
!collapsed && menuStore.commitCollapsedState(val);
}
initRef.value = true;
}
// 菜单区域拖拽 - 鼠标移动
function handleMouseMove(ele: any, wrap: any, clientX: number) {
document.onmousemove = function (innerE) {
let iT = ele.left + ((innerE || event).clientX - clientX);
innerE = innerE || window.event;
// let tarnameb = innerE.target || innerE.srcElement;
const maxT = 600;
const minT = 80;
iT < 0 && (iT = 0);
iT > maxT && (iT = maxT);
iT < minT && (iT = minT);
ele.style.left = wrap.style.width = iT + 'px';
return false;
};
}
// 菜单区域拖拽 - 鼠标松开
function removeMouseup(ele: any) {
const wrap = unref(sideRef).$el;
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
const width = parseInt(wrap.style.width);
menuStore.commitDragStartState(false);
if (!menuStore.getCollapsedState) {
if (width > 100) {
setMenuWidth(width);
} else {
menuStore.commitCollapsedState(true);
}
} else {
if (width > 80) {
setMenuWidth(width);
menuStore.commitCollapsedState(false);
}
}
ele.releaseCapture && ele.releaseCapture();
};
}
function setMenuWidth(width: number) {
appStore.commitProjectConfigState({
menuSetting: {
menuWidth: width,
},
});
}
function changeWrapWidth() {
const ele = unref(dragBarRef) as any;
const side = unref(sideRef);
const wrap = (side || {}).$el;
// const eleWidth = 6;
ele &&
(ele.onmousedown = (e: any) => {
menuStore.commitDragStartState(true);
wrap.style.transition = 'unset';
const clientX = (e || event).clientX;
ele.left = ele.offsetLeft;
handleMouseMove(ele, wrap, clientX);
removeMouseup(ele);
ele.setCapture && ele.setCapture();
return false;
});
}
function handleBreakpoint(broken: boolean) {
brokenRef.value = broken;
}
onMounted(() => {
nextTick(() => {
const [exec] = useDebounce(changeWrapWidth, 20);
exec();
});
});
const getDragBarStyle = computed(() => {
if (menuStore.getCollapsedState) {
return { left: '80px' };
}
return {};
});
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : 80;
});
function renderDragLine() {
const { menuSetting: { hasDrag = true } = {} } = unref(getProjectConfigRef);
return (
<div
class={[`layout-sidebar__dargbar`, !hasDrag ? 'hide' : '']}
style={unref(getDragBarStyle)}
ref={dragBarRef}
/>
);
}
return () => {
const {
menuSetting: { theme, split: splitMenu },
} = unref(getProjectConfigRef);
const { getCollapsedState, getMenuWidthState } = menuStore;
return (
<Layout.Sider
onCollapse={onCollapseChange}
breakpoint="md"
width={getMenuWidthState}
collapsed={getCollapsedState}
collapsible
collapsedWidth={unref(getCollapsedWidth)}
theme={theme}
class="layout-sidebar"
ref={sideRef}
onBreakpoint={handleBreakpoint}
style={unref(getStyle)}
>
{{
trigger: () => <SideBarTrigger />,
default: () => (
<>
<LayoutMenu
theme={theme}
menuMode={splitMenu ? MenuModeEnum.INLINE : null}
splitType={splitMenu ? MenuSplitTyeEnum.LEFT : MenuSplitTyeEnum.NONE}
/>
{renderDragLine()}
</>
),
}}
</Layout.Sider>
);
};
},
});

View File

@@ -0,0 +1,12 @@
import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons-vue';
import { defineComponent } from 'vue';
// store
import { menuStore } from '/@/store/modules/menu';
export default defineComponent({
name: 'SideBarTrigger',
setup() {
return () => (menuStore.getCollapsedState ? <DoubleRightOutlined /> : <DoubleLeftOutlined />);
},
});

View File

@@ -0,0 +1,103 @@
// components
import { Dropdown, Menu, Divider } from 'ant-design-vue';
import { defineComponent, computed, unref } from 'vue';
// res
import headerImg from '/@/assets/images/header.jpg';
import Icon from '/@/components/Icon/index';
import { userStore } from '/@/store/modules/user';
import { DOC_URL } from '/@/settings/siteSetting';
import { appStore } from '/@/store/modules/app';
const prefixCls = 'user-dropdown';
export default defineComponent({
name: 'UserDropdown',
setup() {
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
/**
* @description: 退出登录
*/
function handleLoginOut() {
userStore.confirmLoginOut();
}
// 打开文档
function openDoc() {
window.open(DOC_URL, '__blank');
}
function handleMenuClick(e: any) {
if (e.key === 'loginOut') {
handleLoginOut();
}
if (e.key === 'doc') {
openDoc();
}
}
const getUserInfo = computed(() => {
const { realName = '', desc } = userStore.getUserInfoState || {};
return { realName, desc };
});
return () => {
const { realName } = unref(getUserInfo);
const {
headerSetting: { showDoc },
} = unref(getProjectConfigRef);
return (
<Dropdown placement="bottomLeft">
{{
default: () => (
<>
<section class={prefixCls}>
<img class={`${prefixCls}__header`} src={headerImg} />
<section class={`${prefixCls}__info`}>
<section class={`${prefixCls}__name`}>{realName}</section>
</section>
</section>
</>
),
overlay: () => (
<Menu slot="overlay" onClick={handleMenuClick}>
{() => (
<>
{showDoc && (
<Menu.Item key="doc">
{() => (
<>
<span class="flex items-center">
<Icon icon="gg:loadbar-doc" class="mr-1" />
<span></span>
</span>
</>
)}
</Menu.Item>
)}
{showDoc && <Divider />}
<Menu.Item key="loginOut">
{() => (
<>
<span class="flex items-center">
<Icon icon="ant-design:poweroff-outlined" class="mr-1" />
<span>退</span>
</span>
</>
)}
</Menu.Item>
</>
)}
</Menu>
),
}}
</Dropdown>
);
};
},
});

View File

@@ -0,0 +1,31 @@
.lock-modal {
&__entry {
position: relative;
width: 500px;
height: 240px;
padding: 80px 30px 0 30px;
background: #fff;
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;
}
}

View File

@@ -0,0 +1,82 @@
// 组件相关
import { defineComponent } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal/index';
// hook
import { BasicForm, useForm } from '/@/components/Form/index';
import headerImg from '/@/assets/images/header.jpg';
import { appStore } from '/@/store/modules/app';
import { userStore } from '/@/store/modules/user';
import Button from '/@/components/Button/index.vue';
import './LockActionItem.less';
const prefixCls = 'lock-modal';
export default defineComponent({
name: 'LockModal',
setup(_, { attrs }) {
const [register, { setModalProps }] = useModalInner();
// 样式前缀
const [registerForm, { validateFields, resetFields }] = useForm({
// 隐藏按钮
showActionButtonGroup: false,
// 表单项
schemas: [
{
field: 'password',
label: '锁屏密码',
component: 'InputPassword',
componentProps: {
placeholder: '请输入锁屏密码',
},
rules: [{ required: true }],
},
],
});
/**
* @description: lock
*/
async function lock(valid = true) {
let password: string | undefined = '';
try {
const values = (await validateFields()) as any;
password = values.password;
if (!valid) {
password = undefined;
}
setModalProps({
visible: false,
});
appStore.commitLockInfoState({
isLock: true,
pwd: password,
});
resetFields();
} catch (error) {}
}
// 账号密码登录
return () => (
<BasicModal footer={null} title="锁定屏幕" {...attrs} class={prefixCls} onRegister={register}>
{() => (
<div class={`${prefixCls}__entry`}>
<div class={`${prefixCls}__header`}>
<img src={headerImg} class={`${prefixCls}__header-img`} />
<p class={`${prefixCls}__header-name`}>{userStore.getUserInfoState.realName}</p>
</div>
<BasicForm onRegister={registerForm} />
<div class={`${prefixCls}__footer`}>
<Button type="primary" block class="mt-2" onClick={lock}>
{() => '锁屏'}
</Button>
<Button block class="mt-2" onClick={lock.bind(null, false)}>
{() => ' 不设置密码锁屏'}
</Button>
</div>
</div>
)}
</BasicModal>
);
},
});

View File

@@ -0,0 +1,401 @@
@import (reference) '../../design/index.less';
.default-layout {
&__content {
position: relative;
&.fixed {
overflow: hidden;
}
}
&__loading {
position: absolute;
z-index: @page-loading-z-index;
}
&__main {
position: relative;
height: 100%;
// overflow: hidden;
// overflow: auto;
&.fixed {
overflow: auto;
}
&.fixed.lock {
overflow: hidden;
}
}
.layout-content {
position: relative;
// height: 100%;
&.fixed {
width: 1200px;
margin: 0 auto;
}
}
.layout-menu {
&__logo {
height: @header-height;
padding: 10px;
img {
width: @logo-width;
height: @logo-width;
}
&.light {
.logo-title {
color: @text-color-base;
}
}
&.dark {
.logo-title {
color: @white;
}
}
}
}
.layout-sidebar {
background-size: 100% 100%;
.ant-layout-sider-zero-width-trigger {
top: 40%;
z-index: 10;
}
&__dargbar {
position: absolute;
top: 0;
right: -2px;
z-index: @sider-drag-z-index;
width: 2px;
height: 100%;
cursor: col-resize;
border-top: none;
border-bottom: none;
&.hide {
display: none;
}
&:hover {
background: @primary-color;
box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
}
}
}
.setting-button {
top: 45%;
right: 0;
border-radius: 10px 0 0 10px;
.svg {
width: 2em;
}
}
&__tabs {
z-index: 10;
height: @multiple-height;
padding: 0;
line-height: @multiple-height;
background: @border-color-shallow-light;
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.08);
}
}
.setting-drawer {
.ant-drawer-body {
padding-top: 0;
background: @white;
}
&__footer {
display: flex;
flex-direction: column;
align-items: center;
}
&__cell-item {
display: flex;
justify-content: space-between;
margin: 16px 0;
}
&__theme-item {
display: flex;
flex-wrap: wrap;
margin: 16px 0;
span {
display: inline-block;
width: 20px;
height: 20px;
margin-top: 10px;
margin-right: 10px;
cursor: pointer;
border-radius: 4px;
svg {
display: none;
}
&.active {
svg {
display: inline-block;
margin-left: 4px;
font-size: 0.8em;
fill: @white;
}
}
}
}
&__siderbar {
display: flex;
> div {
position: relative;
.check-icon {
position: absolute;
top: 40%;
left: 40%;
display: none;
color: @primary-color;
&.active {
display: inline-block;
}
}
}
img {
margin-right: 10px;
cursor: pointer;
}
}
}
.layout-header {
display: flex;
height: @header-height;
padding: 0 20px 0 0;
color: @white;
align-items: center;
justify-content: space-between;
&__header--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
.layout-header__menu {
height: calc(@header-height - 1px);
.ant-menu-submenu {
height: @header-height;
line-height: @header-height;
}
}
.layout-header__logo {
height: @header-height;
color: @text-color-base;
img {
width: @logo-width;
height: @logo-width;
margin-right: 6px;
}
&:hover {
background: @header-light-bg-hover-color;
}
}
.layout-header__action {
&-item {
&:hover {
background: @header-light-bg-hover-color;
}
}
&-icon {
color: @text-color-base;
}
}
.layout-header__user-dropdown {
&:hover {
background: @header-light-bg-hover-color;
}
}
.user-dropdown {
&__name {
color: @text-color-base;
}
&__desc {
color: @header-light-desc-color;
}
}
}
&__header--dark {
background: @header-dark-bg-color;
.layout-header__action {
&-item {
&:hover {
background: @header-dark-bg-hover-color;
}
}
}
.layout-header__logo {
height: @header-height;
img {
width: @logo-width;
height: @logo-width;
margin-right: 10px;
}
&:hover {
background: @header-dark-bg-hover-color;
}
}
.layout-header__user-dropdown {
&:hover {
background: @header-dark-bg-hover-color;
}
}
.breadcrumb {
&__item:last-child .breadcrumb__inner,
&__item:last-child &__inner a,
&__item:last-child &__inner a:hover,
&__item:last-child &__inner:hover {
font-weight: 400;
color: rgba(255, 255, 255, 0.6);
cursor: text;
}
&__inner,
&__separator {
color: @white;
}
}
}
&-lm {
display: flex;
}
&__logo {
padding: 0 10px;
}
&__bread {
flex: 1;
display: none;
}
&__action {
display: flex;
align-items: center;
&-item {
display: flex;
align-items: center;
height: @header-height;
font-size: 1.3em;
cursor: pointer;
}
&-icon {
padding: 0 12px;
}
}
&__menu {
display: flex;
margin-left: 20px;
overflow: hidden;
align-items: center;
}
&__user-dropdown {
height: 52px;
padding: 0 0 0 10px;
}
}
.user-dropdown {
display: flex;
height: 100%;
cursor: pointer;
align-items: center;
img {
width: @logo-width;
height: @logo-width;
margin-right: 24px;
}
&__header {
border-radius: 50%;
}
&__divider {
width: 1px;
height: 30px;
margin-right: 20px;
background: #c6d9ee;
}
&__exit {
margin-top: -40px;
font-size: 12px;
color: #c6d9ee;
text-align: center;
> section {
height: 20px;
}
}
&__info {
display: flex;
margin-right: 12px;
flex-direction: column;
> section {
line-height: 1.8;
}
}
&__name {
font-size: 14px;
}
&__desc {
font-size: 12px;
.text-truncate();
}
}
.layout-breadcrumb {
padding: 0 16px;
}

View File

@@ -0,0 +1,126 @@
import { defineComponent, unref, onMounted, computed } from 'vue';
import { Layout, BackTop } from 'ant-design-vue';
import LayoutHeader from './LayoutHeader';
import { appStore } from '/@/store/modules/app';
import LayoutContent from './LayoutContent';
import LayoutSideBar from './LayoutSideBar';
import SettingBtn from './setting/index.vue';
import MultipleTabs from './multitabs/index';
import { FullLoading } from '/@/components/Loading/index';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { useFullContent } from '/@/hooks/web/useFullContent';
import LockPage from '/@/views/sys/lock/index.vue';
import './index.less';
import { userStore } from '/@/store/modules/user';
export default defineComponent({
name: 'DefaultLayout',
setup() {
// 获取项目配置
const { getFullContent } = useFullContent();
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
const getLockMainScrollStateRef = computed(() => {
return appStore.getLockMainScrollState;
});
const showHeaderRef = computed(() => {
const {
headerSetting: { show },
} = unref(getProjectConfigRef);
return show;
});
const isShowMixHeaderRef = computed(() => {
const {
menuSetting: { type },
} = unref(getProjectConfigRef);
return type !== MenuTypeEnum.SIDEBAR && unref(showHeaderRef);
});
const showSideBarRef = computed(() => {
const {
menuSetting: { show, mode },
} = unref(getProjectConfigRef);
return show && mode !== MenuModeEnum.HORIZONTAL && !unref(getFullContent);
});
// const { currentRoute } = useRouter();
onMounted(() => {
// Each refresh will request the latest user information, if you dont need it, you can delete it
userStore.getUserInfoAction({ userId: userStore.getUserInfoState.userId });
});
// Get project configuration
// const { getFullContent } = useFullContent(currentRoute);
function getTarget(): any {
const {
headerSetting: { fixed },
} = unref(getProjectConfigRef);
return document.querySelector(`.default-layout__${fixed ? 'main' : 'content'}`);
}
return () => {
const { getPageLoading, getLockInfo } = appStore;
const {
openPageLoading,
useOpenBackTop,
showSettingButton,
multiTabsSetting: { show: showTabs },
headerSetting: { fixed },
} = unref(getProjectConfigRef);
// const fixedHeaderCls = fixed ? ('fixed' + getLockMainScrollState ? ' lock' : '') : '';
const fixedHeaderCls = fixed
? 'fixed' + (unref(getLockMainScrollStateRef) ? ' lock' : '')
: '';
const { isLock } = getLockInfo;
return (
<Layout class="default-layout relative">
{() => (
<>
{isLock && <LockPage />}
{!unref(getFullContent) && unref(isShowMixHeaderRef) && unref(showHeaderRef) && (
<LayoutHeader />
)}
{showSettingButton && <SettingBtn />}
<Layout>
{() => (
<>
{unref(showSideBarRef) && <LayoutSideBar />}
<Layout class={[`default-layout__content`, fixedHeaderCls]}>
{() => (
<>
{!unref(getFullContent) &&
!unref(isShowMixHeaderRef) &&
unref(showHeaderRef) && <LayoutHeader />}
{showTabs && !unref(getFullContent) && (
<Layout.Header class={`default-layout__tabs`}>
{() => <MultipleTabs />}
</Layout.Header>
)}
{useOpenBackTop && <BackTop target={getTarget} />}
<div class={[`default-layout__main`, fixedHeaderCls]}>
{openPageLoading && (
<FullLoading
class={[`default-layout__loading`, !getPageLoading && 'hidden']}
/>
)}
<LayoutContent />
</div>
</>
)}
</Layout>
</>
)}
</Layout>
</>
)}
</Layout>
);
};
},
});

View File

@@ -0,0 +1,108 @@
import { TabItem, tabStore } from '/@/store/modules/tab';
import type { PropType } from 'vue';
import { getScaleAction, TabContentProps } from './tab.data';
import { defineComponent, unref, computed } from 'vue';
import { Dropdown } from '/@/components/Dropdown/index';
import Icon from '/@/components/Icon/index';
import { DoubleRightOutlined } from '@ant-design/icons-vue';
import { appStore } from '/@/store/modules/app';
import { TabContentEnum } from './tab.data';
import { useTabDropdown } from './useTabDropdown';
export default defineComponent({
name: 'TabContent',
props: {
tabItem: {
type: Object as PropType<TabItem>,
default: null,
},
type: {
type: Number as PropType<number>,
default: TabContentEnum.TAB_TYPE,
},
trigger: {
type: Array as PropType<string[]>,
default: () => {
return ['contextmenu'];
},
},
},
setup(props) {
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
const getIsScaleRef = computed(() => {
const {
menuSetting: { show: showMenu },
headerSetting: { show: showHeader },
} = unref(getProjectConfigRef);
return !showMenu && !showHeader;
});
function handleContextMenu(e: Event) {
if (!props.tabItem) return;
const tableItem = props.tabItem;
e.preventDefault();
const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);
tabStore.commitCurrentContextMenuIndexState(index);
tabStore.commitCurrentContextMenuState(props.tabItem);
}
/**
* @description: 渲染图标
*/
function renderIcon() {
const { tabItem } = props;
if (!tabItem) return;
const icon = tabItem.meta && tabItem.meta.icon;
if (!icon || !unref(getProjectConfigRef).multiTabsSetting.showIcon) return null;
return <Icon icon={icon} class="align-middle mb-1" />;
}
function renderTabContent() {
const { tabItem: { meta } = {} } = props;
return (
<div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>
{renderIcon()}
<span class="ml-1">{meta && meta.title}</span>
</div>
);
}
function renderExtraContent() {
return (
<span class={`multiple-tabs-content__extra `}>
<DoubleRightOutlined />
</span>
);
}
const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps);
return () => {
const { trigger, type } = props;
const {
multiTabsSetting: { showQuick },
} = unref(getProjectConfigRef);
const isTab = !showQuick ? true : type === TabContentEnum.TAB_TYPE;
const scaleAction = getScaleAction(
unref(getIsScaleRef) ? '缩小' : '放大',
unref(getIsScaleRef)
);
const dropMenuList = unref(getDropMenuList) || [];
return (
<Dropdown
dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList}
trigger={isTab ? trigger : ['hover']}
onMenuEvent={handleMenuEvent}
>
{() => (isTab ? renderTabContent() : renderExtraContent())}
</Dropdown>
);
};
},
});

View File

@@ -0,0 +1,135 @@
@import (reference) '../../../design/index.less';
.multiple-tabs {
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.ant-tabs-small {
height: @multiple-height;
}
.ant-tabs.ant-tabs-card {
.ant-tabs-card-bar {
height: @multiple-height;
margin: 0;
background: @white;
border: 0;
box-shadow: 0 4px 26px 1px rgba(0, 0, 0, 0.08);
.ant-tabs-nav-container {
height: @multiple-height;
padding-top: 2px;
}
.ant-tabs-tab {
height: calc(@multiple-height - 2px);
font-size: 14px;
line-height: calc(@multiple-height - 2px);
color: @text-color-call-out;
background: @white;
border: 1px solid @border-color-shallow-dark;
border-radius: 4px 4px 0 0;
transition: none;
.ant-tabs-close-x {
color: inherit;
}
svg {
fill: @text-color-base;
}
&::before {
position: absolute;
top: -2px;
right: 0;
left: 0;
height: 4px;
background-color: @primary-color;
border-radius: 16px 6px 0 0;
content: '';
transform: scaleX(0);
transform-origin: bottom right;
}
&:hover::before {
transform: scaleX(1);
transition: transform 0.4s ease;
transform-origin: bottom left;
}
}
.ant-tabs-tab-active {
height: calc(@multiple-height - 3px);
color: @white;
// background: @primary-color;
background: linear-gradient(
118deg,
rgba(@primary-color, 0.8),
rgba(@primary-color, 1)
) !important;
border: 0;
box-shadow: 0 0 6px 1px rgba(@primary-color, 0.4);
&::before {
display: none;
}
svg {
fill: @white;
}
}
}
.ant-tabs-nav > div:nth-child(1) {
padding: 0 10px;
}
.ant-tabs-tab-prev,
.ant-tabs-tab-next {
color: @border-color-dark;
background: @white;
}
}
.ant-tabs-tab:not(.ant-tabs-tab-active) {
.anticon-close {
font-size: 12px;
svg {
width: 0.8em;
}
}
&:hover {
.anticon-close {
color: @white;
}
}
}
}
.multiple-tabs-content {
&__extra {
display: inline-block;
width: @multiple-height;
height: @multiple-height;
line-height: @multiple-height;
color: @primary-color;
text-align: center;
cursor: pointer;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
span[role='img'] {
transform: rotate(90deg);
}
}
&__content {
display: inline-block;
width: 100%;
padding-left: 10px;
margin-left: -10px;
cursor: pointer;
user-select: none;
}
}

View File

@@ -0,0 +1,131 @@
import type { TabContentProps } from './tab.data';
import type { TabItem } from '/@/store/modules/tab';
import type { AppRouteRecordRaw } from '/@/router/types';
import { defineComponent, watch, computed, ref, unref } from 'vue';
import { Tabs } from 'ant-design-vue';
import TabContent from './TabContent';
import { useGo } from '/@/hooks/web/usePage';
import { TabContentEnum } from './tab.data';
import { useRouter } from 'vue-router';
import './index.less';
import { tabStore } from '/@/store/modules/tab';
import { closeTab } from './useTabDropdown';
import router from '/@/router';
export default defineComponent({
name: 'MultiTabs',
setup() {
let isAddAffix = false;
const go = useGo();
const { currentRoute } = useRouter();
// 当前激活tab
const activeKeyRef = ref<string>('');
// 当前tab列表
const getTabsState = computed(() => {
return tabStore.getTabsState;
});
watch(
() => unref(currentRoute).path,
(path) => {
if (!isAddAffix) {
addAffixTabs();
isAddAffix = true;
}
activeKeyRef.value = path;
tabStore.commitAddTab((unref(currentRoute) as unknown) as AppRouteRecordRaw);
},
{
immediate: true,
}
);
/**
* @description: 过滤所有固定路由
*/
function filterAffixTabs(routes: AppRouteRecordRaw[]) {
const tabs: TabItem[] = [];
routes &&
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
tabs.push({
path: route.path,
name: route.name,
meta: { ...route.meta },
});
}
});
return tabs;
}
/**
* @description: 设置固定tabs
*/
function addAffixTabs(): void {
const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]);
for (const tab of affixTabs) {
tabStore.commitAddTab(tab);
}
}
// tab切换
function handleChange(activeKey: any) {
activeKeyRef.value = activeKey;
go(activeKey, false);
}
// 关闭当前ab
function handleEdit(targetKey: string) {
// 新增操作隐藏,目前只使用删除操作
const index = unref(getTabsState).findIndex((item) => item.path === targetKey);
index !== -1 && closeTab(unref(getTabsState)[index]);
}
function renderQuick() {
const tabContentProps: TabContentProps = {
tabItem: (currentRoute as unknown) as AppRouteRecordRaw,
type: TabContentEnum.EXTRA_TYPE,
trigger: ['click', 'contextmenu'],
};
return (
<span>
<TabContent {...tabContentProps} />
</span>
);
}
function renderTabs() {
return unref(getTabsState).map((item: TabItem) => {
return (
<Tabs.TabPane key={item.path} closable={!(item && item.meta && item.meta.affix)}>
{{
tab: () => <TabContent tabItem={item} />,
}}
</Tabs.TabPane>
);
});
}
return () => {
return (
<div class="multiple-tabs">
<Tabs
type="editable-card"
size="small"
hideAdd={true}
tabBarGutter={2}
activeKey={unref(activeKeyRef)}
onChange={handleChange}
onEdit={handleEdit}
>
{{
default: () => renderTabs(),
tabBarExtraContent: () => renderQuick(),
}}
</Tabs>
</div>
);
};
},
});

View File

@@ -0,0 +1,84 @@
import { DropMenu } from '/@/components/Dropdown/index';
import { AppRouteRecordRaw } from '/@/router/types';
import type { TabItem } from '/@/store/modules/tab';
export enum TabContentEnum {
TAB_TYPE,
EXTRA_TYPE,
}
export interface TabContentProps {
tabItem: TabItem | AppRouteRecordRaw;
type?: TabContentEnum;
trigger?: Array<'click' | 'hover' | 'contextmenu'>;
}
/**
* @description: 右键:下拉菜单文字
*/
export enum MenuEventEnum {
// 刷新
REFRESH_PAGE,
// 关闭当前
CLOSE_CURRENT,
// 关闭左侧
CLOSE_LEFT,
// 关闭右侧
CLOSE_RIGHT,
// 关闭其他
CLOSE_OTHER,
// 关闭所有
CLOSE_ALL,
// 放大
SCALE,
}
export function getActions() {
const REFRESH_PAGE: DropMenu = {
icon: 'ant-design:reload-outlined',
event: MenuEventEnum.REFRESH_PAGE,
text: '刷新',
disabled: false,
};
const CLOSE_CURRENT: DropMenu = {
icon: 'ant-design:close-outlined',
event: MenuEventEnum.CLOSE_CURRENT,
text: '关闭',
disabled: false,
divider: true,
};
const CLOSE_LEFT: DropMenu = {
icon: 'ant-design:pic-left-outlined',
event: MenuEventEnum.CLOSE_LEFT,
text: '关闭左侧',
disabled: false,
divider: false,
};
const CLOSE_RIGHT: DropMenu = {
icon: 'ant-design:pic-right-outlined',
event: MenuEventEnum.CLOSE_RIGHT,
text: '关闭右侧',
disabled: false,
divider: true,
};
const CLOSE_OTHER: DropMenu = {
icon: 'ant-design:pic-center-outlined',
event: MenuEventEnum.CLOSE_OTHER,
text: '关闭其他',
disabled: false,
};
const CLOSE_ALL: DropMenu = {
icon: 'ant-design:line-outlined',
event: MenuEventEnum.CLOSE_ALL,
text: '关闭全部',
disabled: false,
};
return [REFRESH_PAGE, CLOSE_CURRENT, CLOSE_LEFT, CLOSE_RIGHT, CLOSE_OTHER, CLOSE_ALL];
}
export function getScaleAction(text: string, isZoom = false) {
return {
icon: isZoom ? 'codicon:screen-normal' : 'codicon:screen-full',
event: MenuEventEnum.SCALE,
text: text,
disabled: false,
};
}

View File

@@ -0,0 +1,227 @@
import type { AppRouteRecordRaw } from '/@/router/types';
import type { TabContentProps } from './tab.data';
import type { Ref } from 'vue';
import type { TabItem } from '/@/store/modules/tab';
import type { DropMenu } from '/@/components/Dropdown';
import { computed, unref } from 'vue';
import { TabContentEnum, MenuEventEnum, getActions } from './tab.data';
import { tabStore } from '/@/store/modules/tab';
import { appStore } from '/@/store/modules/app';
import { PageEnum } from '/@/enums/pageEnum';
import { useGo, useRedo } from '/@/hooks/web/usePage';
import router from '/@/router';
import { useTabs, isInitUseTab } from '/@/hooks/web/useTabs';
const { initTabFn } = useTabs();
/**
* @description: 右键下拉
*/
export function useTabDropdown(tabContentProps: TabContentProps) {
const { currentRoute } = router;
const redo = useRedo();
const go = useGo();
const isTabsRef = computed(() => tabContentProps.type === TabContentEnum.TAB_TYPE);
const getCurrentTab: Ref<TabItem | AppRouteRecordRaw> = computed(() => {
return unref(isTabsRef)
? tabContentProps.tabItem
: ((unref(currentRoute) as any) as AppRouteRecordRaw);
});
// 当前tab列表
const getTabsState = computed(() => {
return tabStore.getTabsState;
});
/**
* @description: 下拉列表
*/
const getDropMenuList = computed(() => {
const dropMenuList = getActions();
// 重置为初始状态
for (const item of dropMenuList) {
item.disabled = false;
}
// 没有tab
if (!unref(getTabsState) || unref(getTabsState).length <= 0) {
return dropMenuList;
} else if (unref(getTabsState).length === 1) {
// 只有一个tab
for (const item of dropMenuList) {
if (item.event !== MenuEventEnum.REFRESH_PAGE) {
item.disabled = true;
}
}
return dropMenuList;
}
if (!unref(getCurrentTab)) {
return;
}
const { meta, path } = unref(getCurrentTab);
// console.log(unref(getCurrentTab));
// 刷新按钮
const curItem = tabStore.getCurrentContextMenuState;
const index = tabStore.getCurrentContextMenuIndexState;
const refreshDisabled = curItem ? curItem.path !== path : true;
// 关闭左侧
const closeLeftDisabled = index === 0;
// 关闭右侧
const closeRightDisabled = index === unref(getTabsState).length - 1;
// 当前为固定tab
dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false;
if (meta && meta.affix) {
dropMenuList[1].disabled = true;
}
dropMenuList[2].disabled = closeLeftDisabled;
dropMenuList[3].disabled = closeRightDisabled;
return dropMenuList;
});
/**
* @description: 关闭所有页面时,跳转页面
*/
function gotoPage() {
const len = unref(getTabsState).length;
const { path } = unref(currentRoute);
let toPath: PageEnum | string = PageEnum.BASE_HOME;
if (len > 0) {
toPath = unref(getTabsState)[len - 1].path;
}
// 跳到当前页面报错
path !== toPath && go(toPath as PageEnum, true);
}
function isGotoPage(currentTab?: TabItem) {
const { path } = unref(currentRoute);
const currentPath = (currentTab || unref(getCurrentTab)).path;
// 不是当前tab关闭左侧/右侧时,需跳转页面
if (path !== currentPath) {
go(currentPath as PageEnum, true);
}
}
function refreshPage(tabItem?: TabItem) {
try {
tabStore.commitCloseTabKeepAlive(tabItem || unref(getCurrentTab));
} catch (error) {}
redo();
}
function closeAll() {
tabStore.commitCloseAllTab();
gotoPage();
}
function closeLeft(tabItem?: TabItem) {
tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab));
isGotoPage(tabItem);
}
function closeRight(tabItem?: TabItem) {
tabStore.closeRightTabAction(tabItem || unref(getCurrentTab));
isGotoPage(tabItem);
}
function closeOther(tabItem?: TabItem) {
tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab));
isGotoPage(tabItem);
}
function closeCurrent(tabItem?: TabItem) {
closeTab(unref(tabItem || unref(getCurrentTab)));
}
function scaleScreen() {
const {
headerSetting: { show: showHeader },
menuSetting: { show: showMenu },
} = appStore.getProjectConfig;
const isScale = !showHeader && !showMenu;
appStore.commitProjectConfigState({
headerSetting: { show: isScale },
menuSetting: { show: isScale },
});
}
if (!isInitUseTab) {
initTabFn({
refreshPageFn: refreshPage,
closeAllFn: closeAll,
closeCurrentFn: closeCurrent,
closeLeftFn: closeLeft,
closeOtherFn: closeOther,
closeRightFn: closeRight,
});
}
// 处理右键事件
function handleMenuEvent(menu: DropMenu): void {
const { event } = menu;
switch (event) {
case MenuEventEnum.SCALE:
scaleScreen();
break;
case MenuEventEnum.REFRESH_PAGE:
// 刷新页面
refreshPage();
break;
// 关闭当前
case MenuEventEnum.CLOSE_CURRENT:
closeCurrent();
break;
// 关闭左侧
case MenuEventEnum.CLOSE_LEFT:
closeLeft();
break;
// 关闭右侧
case MenuEventEnum.CLOSE_RIGHT:
closeRight();
break;
// 关闭其他
case MenuEventEnum.CLOSE_OTHER:
closeOther();
break;
// 关闭其他
case MenuEventEnum.CLOSE_ALL:
closeAll();
break;
default:
break;
}
}
return { getDropMenuList, handleMenuEvent };
}
export function closeTab(closedTab: TabItem) {
const { currentRoute, replace } = router;
// 当前tab列表
const getTabsState = computed(() => {
return tabStore.getTabsState;
});
const { path } = unref(currentRoute);
if (path !== closedTab.path) {
// 关闭的不是激活tab
tabStore.commitCloseTab(closedTab);
return;
}
// 关闭的为激活atb
let toPath: PageEnum | string;
const index = unref(getTabsState).findIndex((item) => item.path === path);
// 如果当前为最左边tab
if (index === 0) {
// 只有一个tab则跳转至首页否则跳转至右tab
if (unref(getTabsState).length === 1) {
toPath = PageEnum.BASE_HOME;
} else {
// 跳转至右边tab
toPath = unref(getTabsState)[index + 1].path;
}
} else {
// 跳转至左边tab
toPath = unref(getTabsState)[index - 1].path;
}
const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw;
tabStore.commitCloseTab(route);
replace(toPath);
}

View File

@@ -0,0 +1,670 @@
import { defineComponent, computed, unref, ref } from 'vue';
import { BasicDrawer } from '/@/components/Drawer/index';
import { Divider, Switch, Tooltip, InputNumber, Select } from 'ant-design-vue';
import Button from '/@/components/Button/index.vue';
import { MenuModeEnum, MenuTypeEnum, MenuThemeEnum, TopMenuAlignEnum } from '/@/enums/menuEnum';
import { ContentEnum, RouterTransitionEnum } from '/@/enums/appEnum';
import { CopyOutlined, RedoOutlined, CheckOutlined } from '@ant-design/icons-vue';
import { appStore } from '/@/store/modules/app';
import { userStore } from '/@/store/modules/user';
import { ProjectConfig } from '/@/types/config';
import { useMessage } from '/@/hooks/web/useMessage';
import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
import defaultSetting from '/@/settings/projectSetting';
import mixImg from '/@/assets/images/layout/menu-mix.svg';
import sidebarImg from '/@/assets/images/layout/menu-sidebar.svg';
import menuTopImg from '/@/assets/images/layout/menu-top.svg';
import { updateColorWeak, updateGrayMode } from '/@/setup/theme';
const themeOptions = [
{
value: MenuThemeEnum.LIGHT,
label: '亮色',
key: MenuThemeEnum.LIGHT,
},
{
value: MenuThemeEnum.DARK,
label: '暗色',
key: MenuThemeEnum.DARK,
},
];
const contentModeOptions = [
{
value: ContentEnum.FULL,
label: '流式',
key: ContentEnum.FULL,
},
{
value: ContentEnum.FIXED,
label: '定宽',
key: ContentEnum.FIXED,
},
];
const topMenuAlignOptions = [
{
value: TopMenuAlignEnum.CENTER,
label: '居中',
key: TopMenuAlignEnum.CENTER,
},
{
value: TopMenuAlignEnum.START,
label: '居左',
key: TopMenuAlignEnum.START,
},
{
value: TopMenuAlignEnum.END,
label: '居右',
key: TopMenuAlignEnum.END,
},
];
const routerTransitionOptions = [
RouterTransitionEnum.ZOOM_FADE,
RouterTransitionEnum.FADE,
RouterTransitionEnum.ZOOM_OUT,
RouterTransitionEnum.SIDE_FADE,
RouterTransitionEnum.FADE_BOTTOM,
].map((item) => {
return {
label: item,
value: item,
key: item,
};
});
interface SwitchOptions {
config?: DeepPartial<ProjectConfig>;
def?: any;
disabled?: boolean;
handler?: Fn;
}
interface SelectConfig {
options?: SelectOptions;
def?: any;
disabled?: boolean;
handler?: Fn;
}
export default defineComponent({
name: 'SettingDrawer',
setup(_, { attrs }) {
const { createSuccessModal, createMessage } = useMessage();
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
const getIsHorizontalRef = computed(() => {
return unref(getProjectConfigRef).menuSetting.mode === MenuModeEnum.HORIZONTAL;
});
const getShowHeaderRef = computed(() => {
return unref(getProjectConfigRef).headerSetting.show;
});
const getShowMenuRef = computed(() => {
return unref(getProjectConfigRef).menuSetting.show && !unref(getIsHorizontalRef);
});
const getShowTabsRef = computed(() => {
return unref(getProjectConfigRef).multiTabsSetting.show;
});
function handleCopy() {
const { isSuccessRef } = useCopyToClipboard(
JSON.stringify(unref(getProjectConfigRef), null, 2)
);
unref(isSuccessRef) &&
createSuccessModal({
title: '操作成功',
content: '复制成功,请到 src/settings/projectSetting.ts 中修改配置!',
});
}
function renderSidebar() {
const {
headerSetting: { theme: headerTheme },
menuSetting: { type, theme: menuTheme, split },
} = unref(getProjectConfigRef);
const typeList = ref([
{
title: '左侧菜单模式',
mode: MenuModeEnum.INLINE,
type: MenuTypeEnum.SIDEBAR,
src: sidebarImg,
},
{
title: '混合模式',
mode: MenuModeEnum.INLINE,
type: MenuTypeEnum.MIX,
src: mixImg,
},
{
title: '顶部菜单模式',
mode: MenuModeEnum.HORIZONTAL,
type: MenuTypeEnum.TOP_MENU,
src: menuTopImg,
},
]);
return [
<div class={`setting-drawer__siderbar`}>
{unref(typeList).map((item) => {
const { title, type: ItemType, mode, src } = item;
return (
<Tooltip title={title} placement="bottom" key={title}>
{{
default: () => (
<div
onClick={baseHandler.bind(null, 'layout', {
mode: mode,
type: ItemType,
split: unref(getIsHorizontalRef) ? false : undefined,
})}
>
<CheckOutlined class={['check-icon', type === ItemType ? 'active' : '']} />
<img src={src} />
</div>
),
}}
</Tooltip>
);
})}
</div>,
renderSwitchItem('分割菜单', {
handler: (e) => {
baseHandler('splitMenu', e);
},
def: split,
disabled: !unref(getShowMenuRef),
}),
renderSelectItem('顶栏主题', {
handler: (e) => {
baseHandler('headerMenu', e);
},
def: headerTheme,
options: themeOptions,
disabled: !unref(getShowHeaderRef),
}),
renderSelectItem('菜单主题', {
handler: (e) => {
baseHandler('menuTheme', e);
},
def: menuTheme,
options: themeOptions,
disabled: !unref(getShowMenuRef),
}),
];
}
/**
* @description:
*/
function renderFeatures() {
const {
contentMode,
headerSetting: { fixed },
menuSetting: { hasDrag, collapsed, showSearch, menuWidth, topMenuAlign } = {},
} = appStore.getProjectConfig;
return [
renderSwitchItem('侧边菜单拖拽', {
handler: (e) => {
baseHandler('hasDrag', e);
},
def: hasDrag,
disabled: !unref(getShowMenuRef),
}),
renderSwitchItem('侧边菜单搜索', {
handler: (e) => {
baseHandler('showSearch', e);
},
def: showSearch,
disabled: !unref(getShowMenuRef),
}),
renderSwitchItem('折叠菜单', {
handler: (e) => {
baseHandler('collapsed', e);
},
def: collapsed,
disabled: !unref(getShowMenuRef),
}),
renderSwitchItem('固定header', {
handler: (e) => {
baseHandler('headerFixed', e);
},
def: fixed,
disabled: !unref(getShowHeaderRef),
}),
renderSelectItem('顶部菜单布局', {
handler: (e) => {
baseHandler('topMenuAlign', e);
},
def: topMenuAlign,
options: topMenuAlignOptions,
disabled: !unref(getShowHeaderRef),
}),
renderSelectItem('内容区域宽度', {
handler: (e) => {
baseHandler('contentMode', e);
},
def: contentMode,
options: contentModeOptions,
}),
<div class={`setting-drawer__cell-item`}>
<span></span>
<InputNumber
style="width:120px"
size="small"
min={0}
onChange={(e) => {
baseHandler('menuWidth', e);
}}
defaultValue={appStore.getProjectConfig.lockTime}
formatter={(value: string) => {
if (parseInt(value) === 0) {
return '0(不自动锁屏)';
}
return `${value}分钟`;
}}
/>
</div>,
<div class={`setting-drawer__cell-item`}>
<span></span>
<InputNumber
style="width:120px"
size="small"
max={600}
min={100}
step={10}
disabled={!unref(getShowMenuRef)}
defaultValue={menuWidth}
formatter={(value: string) => `${parseInt(value)}px`}
onChange={(e) => {
baseHandler('menuWidth', e);
}}
/>
</div>,
];
}
function renderTransition() {
const { routerTransition, openRouterTransition, openPageLoading } = appStore.getProjectConfig;
return (
<>
{renderSwitchItem('页面切换loading', {
handler: (e) => {
baseHandler('openPageLoading', e);
},
def: openPageLoading,
})}
{renderSwitchItem('切换动画', {
handler: (e) => {
baseHandler('openRouterTransition', e);
},
def: openRouterTransition,
})}
{renderSelectItem('路由动画', {
handler: (e) => {
baseHandler('routerTransition', e);
},
def: routerTransition,
options: routerTransitionOptions,
disabled: !openRouterTransition,
})}
</>
);
}
function renderContent() {
const {
grayMode,
colorWeak,
fullContent,
showLogo,
headerSetting: { show: showHeader },
menuSetting: { show: showMenu },
multiTabsSetting: { show: showMultiple, showQuick, showIcon: showTabIcon },
showBreadCrumb,
} = unref(getProjectConfigRef);
return [
renderSwitchItem('面包屑', {
handler: (e) => {
baseHandler('showBreadCrumb', e);
},
def: showBreadCrumb,
disabled: !unref(getShowHeaderRef),
}),
renderSwitchItem('标签页', {
handler: (e) => {
baseHandler('showMultiple', e);
},
def: showMultiple,
}),
renderSwitchItem('标签页快捷按钮', {
handler: (e) => {
baseHandler('showQuick', e);
},
def: showQuick,
disabled: !unref(getShowTabsRef),
}),
renderSwitchItem('标签页图标', {
handler: (e) => {
baseHandler('showTabIcon', e);
},
def: showTabIcon,
disabled: !unref(getShowTabsRef),
}),
renderSwitchItem('左侧菜单', {
handler: (e) => {
baseHandler('showSidebar', e);
},
def: showMenu,
disabled: unref(getIsHorizontalRef),
}),
renderSwitchItem('顶栏', {
handler: (e) => {
baseHandler('showHeader', e);
},
def: showHeader,
}),
renderSwitchItem('Logo', {
handler: (e) => {
baseHandler('showLogo', e);
},
def: showLogo,
}),
renderSwitchItem('全屏内容', {
handler: (e) => {
baseHandler('fullContent', e);
},
def: fullContent,
}),
renderSwitchItem('灰色模式', {
handler: (e) => {
baseHandler('grayMode', e);
},
def: grayMode,
}),
renderSwitchItem('色弱模式', {
handler: (e) => {
baseHandler('colorWeak', e);
},
def: colorWeak,
}),
];
}
function baseHandler(event: string, value: any) {
let config: DeepPartial<ProjectConfig> = {};
if (event === 'layout') {
const { mode, type, split } = value;
const splitOpt = split === undefined ? { split } : {};
config = {
menuSetting: {
mode,
type,
...splitOpt,
},
};
}
if (event === 'hasDrag') {
config = {
menuSetting: {
hasDrag: value,
},
};
}
if (event === 'openPageLoading') {
config = {
openPageLoading: value,
};
}
if (event === 'topMenuAlign') {
config = {
menuSetting: {
topMenuAlign: value,
},
};
}
if (event === 'showBreadCrumb') {
config = {
showBreadCrumb: value,
};
}
if (event === 'collapsed') {
config = {
menuSetting: {
collapsed: value,
},
};
}
if (event === 'menuWidth') {
config = {
menuSetting: {
menuWidth: value,
},
};
}
if (event === 'menuWidth') {
config = {
lockTime: value,
};
}
if (event === 'showQuick') {
config = {
multiTabsSetting: {
showQuick: value,
},
};
}
if (event === 'showTabIcon') {
config = {
multiTabsSetting: {
showIcon: value,
},
};
}
if (event === 'contentMode') {
config = {
contentMode: value,
};
}
if (event === 'menuTheme') {
config = {
menuSetting: {
theme: value,
},
};
}
if (event === 'splitMenu') {
config = {
menuSetting: {
split: value,
},
};
}
if (event === 'showMultiple') {
config = {
multiTabsSetting: {
show: value,
},
};
}
if (event === 'headerMenu') {
config = {
headerSetting: {
theme: value,
},
};
}
if (event === 'grayMode') {
config = {
grayMode: value,
};
updateGrayMode(value);
}
if (event === 'colorWeak') {
config = {
colorWeak: value,
};
updateColorWeak(value);
}
if (event === 'showLogo') {
config = {
showLogo: value,
};
}
if (event === 'showSearch') {
config = {
menuSetting: {
showSearch: value,
},
};
}
if (event === 'showSidebar') {
config = {
menuSetting: {
show: value,
},
};
}
if (event === 'openRouterTransition') {
config = {
openRouterTransition: value,
};
}
if (event === 'routerTransition') {
config = {
routerTransition: value,
};
}
if (event === 'headerFixed') {
config = {
headerSetting: {
fixed: value,
},
};
}
if (event === 'fullContent') {
config = {
fullContent: value,
};
}
if (event === 'showHeader') {
config = {
headerSetting: {
show: value,
},
};
}
appStore.commitProjectConfigState(config);
}
function handleResetSetting() {
try {
appStore.commitProjectConfigState(defaultSetting);
const { colorWeak, grayMode } = defaultSetting;
// updateTheme(themeColor);
updateColorWeak(colorWeak);
updateGrayMode(grayMode);
createMessage.success('重置成功!');
} catch (error) {
createMessage.error(error);
}
}
function handleClearAndRedo() {
localStorage.clear();
userStore.resumeAllState();
location.reload();
}
function renderSelectItem(text: string, config?: SelectConfig) {
const { handler, def, disabled = false, options } = config || {};
const opt = def ? { value: def, defaultValue: def } : {};
return (
<div class={`setting-drawer__cell-item`}>
<span>{text}</span>
{/* @ts-ignore */}
<Select
{...opt}
disabled={disabled}
size="small"
style={{ width: '120px' }}
onChange={(e) => {
handler && handler(e);
}}
options={options}
/>
</div>
);
}
function renderSwitchItem(text: string, options?: SwitchOptions) {
const { handler, def, disabled = false } = options || {};
const opt = def ? { checked: def } : {};
return (
<div class={`setting-drawer__cell-item`}>
<span>{text}</span>
<Switch
{...opt}
disabled={disabled}
onChange={(e) => {
handler && handler(e);
}}
checkedChildren="开"
unCheckedChildren="关"
/>
</div>
);
}
return () => (
<BasicDrawer {...attrs} title="项目配置" width={300} wrapClassName="setting-drawer">
{{
default: () => (
<>
<Divider>{() => '导航栏模式'}</Divider>
{renderSidebar()}
<Divider>{() => '界面功能'}</Divider>
{renderFeatures()}
<Divider>{() => '界面显示'}</Divider>
{renderContent()}
<Divider>{() => '切换动画'}</Divider>
{renderTransition()}
<Divider />
<div class="setting-drawer__footer">
<Button type="primary" block onClick={handleCopy}>
{() => (
<>
<CopyOutlined class="mr-2" />
</>
)}
</Button>
<Button block class="mt-2" onClick={handleResetSetting} color="warning">
{() => (
<>
<RedoOutlined class="mr-2" />
</>
)}
</Button>
<Button block class="mt-2" onClick={handleClearAndRedo} color="error">
{() => (
<>
<RedoOutlined class="mr-2" />
</>
)}
</Button>
</div>
</>
),
}}
</BasicDrawer>
);
},
});

View File

@@ -0,0 +1,28 @@
<template>
<div
@click="openDrawer"
class="setting-button bg-primary flex justify-center items-center text-white p-4 absolute z-10 cursor-pointer"
>
<SettingOutlined :spin="true" />
<SettingDrawer @register="register" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { SettingOutlined } from '@ant-design/icons-vue';
import SettingDrawer from './SettingDrawer';
import { useDrawer } from '/@/components/Drawer';
//
export default defineComponent({
name: 'SettingBtn',
components: { SettingOutlined, SettingDrawer },
setup() {
const [register, { openDrawer }] = useDrawer();
return {
register,
openDrawer,
};
},
});
</script>