wip: refactor layout

This commit is contained in:
vben 2020-11-23 23:24:13 +08:00
parent 234c1d1fae
commit ba068ba1df
79 changed files with 1393 additions and 1196 deletions

View File

@ -23,7 +23,7 @@ module.exports = {
'@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off', 'vue/custom-event-name-casing': 'off',
'no-use-before-define': 'off', 'no-use-before-define': 'off',
// 'no-use-before-define': [ // 'no-setting-before-define': [
// 'error', // 'error',
// { // {
// functions: false, // functions: false,
@ -31,7 +31,7 @@ module.exports = {
// }, // },
// ], // ],
'@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off',
// '@typescript-eslint/no-use-before-define': [ // '@typescript-eslint/no-setting-before-define': [
// 'error', // 'error',
// { // {
// functions: false, // functions: false,

View File

@ -1,7 +1,8 @@
import AppLocalPicker from './src/AppLocalPicker.vue'; import AppLocalPicker from './src/AppLocalPicker.vue';
import AppFooterToolbar from './src/AppFooterToolbar.vue'; import AppPageFooter from './src/AppPageFooter.vue';
import AppLogo from './src/AppLogo.vue';
import { withInstall } from '../util'; import { withInstall } from '../util';
export { AppLocalPicker, AppFooterToolbar }; export { AppLocalPicker, AppPageFooter, AppLogo };
export default withInstall(AppLocalPicker, AppFooterToolbar); export default withInstall(AppLocalPicker, AppPageFooter, AppLogo);

View File

@ -1,60 +0,0 @@
<template>
<div class="app-footer" :style="{ width: getWidth }">
<div class="app-footer__left">
<slot name="left" />
</div>
<div class="app-footer__right">
<slot name="right" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, unref } from 'vue';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
import { appStore } from '/@/store/modules/app';
import { menuStore } from '/@/store/modules/menu';
export default defineComponent({
name: 'AppFooterToolbar',
setup() {
const getMiniWidth = computed(() => {
const {
menuSetting: { collapsedShowTitle },
} = appStore.getProjectConfig;
return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH;
});
const getWidth = computed(() => {
const { getCollapsedState, getMenuWidthState } = menuStore;
const width = getCollapsedState ? unref(getMiniWidth) : getMenuWidthState;
return `calc(100% - ${width}px)`;
});
return { getWidth };
},
});
</script>
<style lang="less" scoped>
.app-footer {
position: fixed;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
width: 100%;
align-items: center;
padding: 0 24px;
line-height: 44px;
background: #fff;
border-top: 1px solid #f0f0f0;
box-shadow: 0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),
0 -12px 48px 16px rgba(0, 0, 0, 0.03);
transition: width 0.3s;
&__left {
flex: 1 1;
}
}
</style>

View File

@ -5,29 +5,44 @@
:selectedKeys="selectedKeys" :selectedKeys="selectedKeys"
@menuEvent="handleMenuEvent" @menuEvent="handleMenuEvent"
> >
<GlobalOutlined class="app-locale" /> <span class="app-local-picker">
<GlobalOutlined class="app-local-picker__icon" />
<span v-if="showText">{{ getLangText }}</span>
</span>
</Dropdown> </Dropdown>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watchEffect, unref } from 'vue'; import { defineComponent, ref, watchEffect, unref, computed } from 'vue';
import { Dropdown, DropMenu } from '/@/components/Dropdown'; import { Dropdown, DropMenu } from '/@/components/Dropdown';
import { GlobalOutlined } from '@ant-design/icons-vue'; import { GlobalOutlined } from '@ant-design/icons-vue';
import { useLocale } from '/@/hooks/web/useLocale'; import { useLocale } from '/@/hooks/web/useLocale';
import { useLocaleSetting } from '/@/settings/use/useLocaleSetting'; import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting';
import { LocaleType } from '/@/locales/types'; import { LocaleType } from '/@/locales/types';
export default defineComponent({ export default defineComponent({
name: 'AppLocalPicker', name: 'AppLocalPicker',
components: { GlobalOutlined, Dropdown }, components: { GlobalOutlined, Dropdown },
props: {
showText: {
type: Boolean,
default: true,
},
},
setup() { setup() {
const { localeList } = useLocaleSetting(); const { localeList } = useLocaleSetting();
const selectedKeys = ref<string[]>([]); const selectedKeys = ref<string[]>([]);
const { changeLocale, getLang } = useLocale(); const { changeLocale, getLang } = useLocale();
const getLangText = computed(() => {
const key = selectedKeys.value[0];
if (!key) return '';
return localeList.find((item) => item.event === key)?.text;
});
watchEffect(() => { watchEffect(() => {
selectedKeys.value = [unref(getLang)]; selectedKeys.value = [unref(getLang)];
}); });
@ -41,13 +56,19 @@
toggleLocale(menu.event as string); toggleLocale(menu.event as string);
} }
return { localeList, handleMenuEvent, selectedKeys }; return { localeList, handleMenuEvent, selectedKeys, getLangText };
}, },
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.app-locale { .app-local-picker {
display: flex;
align-items: center;
cursor: pointer; cursor: pointer;
&__icon {
margin-right: 4px;
}
} }
</style> </style>

View File

@ -0,0 +1,72 @@
<template>
<div class="app-logo anticon" :class="theme" @click="handleGoHome">
<img src="/@/assets/images/logo.png" />
<div class="app-logo__title ml-2 ellipsis">{{ globSetting.title }}</div>
</div>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { useGlobSetting } from '/@/hooks/setting';
import { useGo } from '/@/hooks/web/usePage';
import { PageEnum } from '/@/enums/pageEnum';
export default defineComponent({
name: 'AppLogo',
props: {
/**
* The theme of the current parent component
*/
theme: {
type: String as PropType<string>,
},
},
setup() {
const globSetting = useGlobSetting();
const go = useGo();
function handleGoHome(): void {
go(PageEnum.BASE_HOME);
}
return {
handleGoHome,
globSetting,
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../design/index.less';
.app-logo {
display: flex;
align-items: center;
padding-left: 16px;
cursor: pointer;
&.light {
border-bottom: 1px solid @border-color-base;
}
&.light &__title {
color: @primary-color;
}
&.dark &__title {
color: @white;
}
&__title {
font-size: 18px;
font-weight: 700;
opacity: 0;
transition: all 0.5s;
.respond-to(medium,{
opacity: 1;
});
}
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<div class="app-footer" :style="{ width: getCalcContentWidth }">
<div class="app-footer__left">
<slot name="left" />
</div>
<div class="app-footer__right">
<slot name="right" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
export default defineComponent({
name: 'AppFooterToolbar',
setup() {
const { getCalcContentWidth } = useMenuSetting();
return { getCalcContentWidth };
},
});
</script>
<style lang="less" scoped>
.app-footer {
position: fixed;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
width: 100%;
align-items: center;
padding: 0 24px;
line-height: 44px;
background: #fff;
border-top: 1px solid #f0f0f0;
box-shadow: 0 -6px 16px -8px rgba(0, 0, 0, 0.08), 0 -9px 28px 0 rgba(0, 0, 0, 0.05),
0 -12px 48px 16px rgba(0, 0, 0, 0.03);
transition: width 0.4s;
&__left {
flex: 1 1;
}
}
</style>

View File

@ -1,16 +1,15 @@
<!-- <!--
* @Author: Vben Access control component for fine-grained access control.
* @Description:Access control component for fine-grained access control.
--> -->
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent, computed, unref } from 'vue'; import { defineComponent, unref } from 'vue';
import { PermissionModeEnum } from '/@/enums/appEnum'; import { PermissionModeEnum } from '/@/enums/appEnum';
import { RoleEnum } from '/@/enums/roleEnum'; import { RoleEnum } from '/@/enums/roleEnum';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { usePermission } from '/@/hooks/web/usePermission'; import { usePermission } from '/@/hooks/web/usePermission';
import { appStore } from '/@/store/modules/app';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { getSlot } from '/@/utils/helper/tsxHelper';
@ -29,9 +28,8 @@
}, },
}, },
setup(props, { slots }) { setup(props, { slots }) {
const getModeRef = computed(() => { const { getPermissionMode } = useRootSetting();
return appStore.getProjectConfig.permissionMode; const { hasPermission } = usePermission();
});
/** /**
* Render role button * Render role button
@ -41,7 +39,6 @@
if (!value) { if (!value) {
return getSlot(slots); return getSlot(slots);
} }
const { hasPermission } = usePermission();
return hasPermission(value) ? getSlot(slots) : null; return hasPermission(value) ? getSlot(slots) : null;
} }
@ -52,12 +49,11 @@
if (!value) { if (!value) {
return getSlot(slots); return getSlot(slots);
} }
const { hasPermission } = usePermission();
return hasPermission(value) ? getSlot(slots) : null; return hasPermission(value) ? getSlot(slots) : null;
} }
return () => { return () => {
const mode = unref(getModeRef); const mode = unref(getPermissionMode);
// Role-based value control // Role-based value control
if (mode === PermissionModeEnum.ROLE) { if (mode === PermissionModeEnum.ROLE) {
return renderRoleAuth(); return renderRoleAuth();

View File

@ -33,7 +33,7 @@ export default defineComponent({
}); });
/** /**
* @description: Whether to use title * @description: Whether to setting title
*/ */
const useWrapper = computed(() => { const useWrapper = computed(() => {
return !!unref(getMergeProps).title; return !!unref(getMergeProps).title;

View File

@ -15,7 +15,7 @@ export default defineComponent({
const getMenuList = computed(() => props.dropMenuList); const getMenuList = computed(() => props.dropMenuList);
function handleClickMenu({ key }: any) { function handleClickMenu({ key }: any) {
const menu = unref(getMenuList).find((item) => item.event === key); const menu = unref(getMenuList).find((item) => `${item.event}` === `${key}`);
emit('menuEvent', menu); emit('menuEvent', menu);
} }

View File

@ -104,7 +104,7 @@
*/ */
function handleInputClick(e: Event) { function handleInputClick(e: Event) {
const files = e && (e.target as HTMLInputElement).files; const files = e && (e.target as HTMLInputElement).files;
const rawFile = files && files[0]; // only use files[0] const rawFile = files && files[0]; // only setting files[0]
if (!rawFile) return; if (!rawFile) return;
upload(rawFile); upload(rawFile);
} }

View File

@ -2,7 +2,7 @@ import { Component } from 'vue';
import type { ComponentType } from './types/index'; import type { ComponentType } from './types/index';
/** /**
* Component list, register here to use it in the form * Component list, register here to setting it in the form
*/ */
import { import {
Input, Input,

View File

@ -31,7 +31,7 @@ export function useItemLabelWidth(schemaItemRef: Ref<FormSchema>, propsRef: Ref<
wrapperCol: globWrapperCol, wrapperCol: globWrapperCol,
} = unref(propsRef) as any; } = unref(propsRef) as any;
// If labelWidth is set globally, all items use // If labelWidth is set globally, all items setting
if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) { if ((!globalLabelWidth && !labelWidth && !globalLabelCol) || disabledLabelWidth) {
return { labelCol, wrapperCol }; return { labelCol, wrapperCol };
} }

View File

@ -68,7 +68,7 @@ export interface FormItem {
*/ */
labelAlign?: 'left' | 'right'; labelAlign?: 'left' | 'right';
/** /**
* a key of model. In the use of validate and resetFields method, the attribute is required * a key of model. In the setting of validate and resetFields method, the attribute is required
*/ */
name?: NamePath; name?: NamePath;
/** /**
@ -76,7 +76,7 @@ export interface FormItem {
*/ */
rules?: object | object[]; rules?: object | object[];
/** /**
* Whether to automatically associate form fields. In most cases, you can use automatic association. * Whether to automatically associate form fields. In most cases, you can setting automatic association.
* If the conditions for automatic association are not met, you can manually associate them. See the notes below. * If the conditions for automatic association are not met, you can manually associate them. See the notes below.
*/ */
autoLink?: boolean; autoLink?: boolean;

View File

@ -1 +1,5 @@
export { default as BasicMenu } from './src/BasicMenu'; import BasicMenu from './src/BasicMenu';
import { withInstall } from '../util';
export default withInstall(BasicMenu);
export { BasicMenu };

View File

@ -1,3 +1,5 @@
import './index.less';
import type { MenuState } from './types'; import type { MenuState } from './types';
import type { Menu as MenuType } from '/@/router/types'; import type { Menu as MenuType } from '/@/router/types';
@ -9,11 +11,10 @@ import MenuContent from './MenuContent';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { ThemeEnum } from '/@/enums/appEnum'; import { ThemeEnum } from '/@/enums/appEnum';
import { menuStore } from '/@/store/modules/menu';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import { useSearchInput } from './useSearchInput'; import { useSearchInput } from './hooks/useSearchInput';
import { useOpenKeys } from './useOpenKeys'; import { useOpenKeys } from './hooks/useOpenKeys';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { isFunction } from '/@/utils/is'; import { isFunction } from '/@/utils/is';
@ -23,7 +24,7 @@ import { menuHasChildren } from './helper';
import { getCurrentParentPath } from '/@/router/menus'; import { getCurrentParentPath } from '/@/router/menus';
import { basicProps } from './props'; import { basicProps } from './props';
import './index.less'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
export default defineComponent({ export default defineComponent({
name: 'BasicMenu', name: 'BasicMenu',
props: basicProps, props: basicProps,
@ -39,6 +40,8 @@ export default defineComponent({
selectedKeys: [], selectedKeys: [],
collapsedOpenKeys: [], collapsedOpenKeys: [],
}); });
const { getCollapsed } = useMenuSetting();
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
const { items, flatItems, isAppMenu, mode, accordion } = toRefs(props); const { items, flatItems, isAppMenu, mode, accordion } = toRefs(props);
@ -61,7 +64,7 @@ export default defineComponent({
const getOpenKeys = computed(() => { const getOpenKeys = computed(() => {
if (props.isAppMenu) { if (props.isAppMenu) {
return menuStore.getCollapsedState ? menuState.collapsedOpenKeys : menuState.openKeys; return unref(getCollapsed) ? menuState.collapsedOpenKeys : menuState.openKeys;
} }
return menuState.openKeys; return menuState.openKeys;
}); });
@ -95,20 +98,20 @@ export default defineComponent({
cls.push('basic-menu__sidebar-hor'); cls.push('basic-menu__sidebar-hor');
} }
if (!props.isTop && props.isAppMenu && appStore.getProjectConfig.menuSetting.split) { if (!props.isHorizontal && props.isAppMenu && appStore.getProjectConfig.menuSetting.split) {
cls.push('basic-menu__second'); cls.push('basic-menu__second');
} }
return cls; return cls;
}); });
const showTitle = computed(() => props.collapsedShowTitle && menuStore.getCollapsedState); const showTitle = computed(() => props.collapsedShowTitle && unref(getCollapsed));
watch( watch(
() => currentRoute.value.name, () => currentRoute.value.name,
(name: string) => { (name: string) => {
if (name === 'Redirect') return; if (name === 'Redirect') return;
handleMenuChange(); handleMenuChange();
props.isTop && appStore.getProjectConfig.menuSetting.split && getParentPath(); props.isHorizontal && appStore.getProjectConfig.menuSetting.split && getParentPath();
} }
); );
@ -180,7 +183,7 @@ export default defineComponent({
<MenuContent <MenuContent
item={menu} item={menu}
level={index} level={index}
isTop={props.isTop} isHorizontal={props.isHorizontal}
showTitle={unref(showTitle)} showTitle={unref(showTitle)}
searchValue={menuState.searchValue} searchValue={menuState.searchValue}
/>, />,
@ -196,7 +199,7 @@ export default defineComponent({
showTitle={unref(showTitle)} showTitle={unref(showTitle)}
item={menu} item={menu}
level={index} level={index}
isTop={props.isTop} isHorizontal={props.isHorizontal}
searchValue={menuState.searchValue} searchValue={menuState.searchValue}
/>, />,
], ],
@ -214,7 +217,7 @@ export default defineComponent({
const inlineCollapsedObj = isInline const inlineCollapsedObj = isInline
? props.isAppMenu ? props.isAppMenu
? { ? {
inlineCollapsed: menuStore.getCollapsedState, inlineCollapsed: unref(getCollapsed),
} }
: { inlineCollapsed: props.inlineCollapsed } : { inlineCollapsed: props.inlineCollapsed }
: {}; : {};
@ -246,7 +249,6 @@ export default defineComponent({
}); });
return () => { return () => {
const { getCollapsedState } = menuStore;
const { mode } = props; const { mode } = props;
return mode === MenuModeEnum.HORIZONTAL ? ( return mode === MenuModeEnum.HORIZONTAL ? (
renderMenu() renderMenu()
@ -258,7 +260,7 @@ export default defineComponent({
theme={props.theme as ThemeEnum} theme={props.theme as ThemeEnum}
onChange={handleInputChange} onChange={handleInputChange}
onClick={handleInputClick} onClick={handleInputClick}
collapsed={getCollapsedState} collapsed={unref(getCollapsed)}
/> />
<section style={unref(getMenuWrapStyle)} class="basic-menu__content"> <section style={unref(getMenuWrapStyle)} class="basic-menu__content">
{renderMenu()} {renderMenu()}

View File

@ -26,7 +26,7 @@ export default defineComponent({
type: Number as PropType<number>, type: Number as PropType<number>,
default: 0, default: 0,
}, },
isTop: { isHorizontal: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: true,
}, },
@ -40,8 +40,8 @@ export default defineComponent({
} }
function renderTag() { function renderTag() {
const { item, showTitle, isTop } = props; const { item, showTitle, isHorizontal } = props;
if (!item || showTitle || isTop) return null; if (!item || showTitle || isHorizontal) return null;
const { tag } = item; const { tag } = item;
if (!tag) return null; if (!tag) return null;

View File

@ -1,12 +1,12 @@
import { MenuModeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum } from '/@/enums/menuEnum';
import type { Menu as MenuType } from '/@/router/types'; import type { Menu as MenuType } from '/@/router/types';
import type { MenuState } from './types'; import type { MenuState } from '../types';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { unref } from 'vue'; import { unref } from 'vue';
import { menuStore } from '/@/store/modules/menu';
import { getAllParentPath } from '/@/utils/helper/menuHelper'; import { getAllParentPath } from '/@/utils/helper/menuHelper';
import { es6Unique } from '/@/utils'; import { es6Unique } from '/@/utils';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
export function useOpenKeys( export function useOpenKeys(
menuState: MenuState, menuState: MenuState,
@ -16,6 +16,7 @@ export function useOpenKeys(
mode: Ref<MenuModeEnum>, mode: Ref<MenuModeEnum>,
accordion: Ref<boolean> accordion: Ref<boolean>
) { ) {
const { getCollapsed } = useMenuSetting();
/** /**
* @description: * @description:
*/ */
@ -49,7 +50,7 @@ export function useOpenKeys(
rootSubMenuKeys.push(path); rootSubMenuKeys.push(path);
} }
} }
if (!menuStore.getCollapsedState || !unref(isAppMenu)) { if (!unref(getCollapsed) || !unref(isAppMenu)) {
const latestOpenKey = openKeys.find((key) => menuState.openKeys.indexOf(key) === -1); const latestOpenKey = openKeys.find((key) => menuState.openKeys.indexOf(key) === -1);
if (rootSubMenuKeys.indexOf(latestOpenKey as string) === -1) { if (rootSubMenuKeys.indexOf(latestOpenKey as string) === -1) {
menuState.openKeys = openKeys; menuState.openKeys = openKeys;

View File

@ -1,5 +1,5 @@
import type { Menu as MenuType } from '/@/router/types'; import type { Menu as MenuType } from '/@/router/types';
import type { MenuState } from './types'; import type { MenuState } from '../types';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import { isString } from '/@/utils/is'; import { isString } from '/@/utils/is';

View File

@ -8,6 +8,10 @@ export const basicProps = {
type: Array as PropType<Menu[]>, type: Array as PropType<Menu[]>,
default: () => [], default: () => [],
}, },
flatItems: {
type: Array as PropType<Menu[]>,
default: () => [],
},
appendClass: { appendClass: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
@ -16,10 +20,6 @@ export const basicProps = {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
}, },
flatItems: {
type: Array as PropType<Menu[]>,
default: () => [],
},
// 是否显示搜索框 // 是否显示搜索框
search: { search: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
@ -55,7 +55,7 @@ export const basicProps = {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: true,
}, },
isTop: { isHorizontal: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
}, },

View File

@ -35,7 +35,7 @@ export const basicProps = Object.assign({}, modalProps, {
}, },
// Warm reminder message // Warm reminder message
helpMessage: [String, Array] as PropType<string | string[]>, helpMessage: [String, Array] as PropType<string | string[]>,
// Whether to use wrapper // Whether to setting wrapper
useWrapper: { useWrapper: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: true,

View File

@ -190,7 +190,7 @@ export interface ColumnProps<T> {
onFilterDropdownVisibleChange?: (visible: boolean) => void; onFilterDropdownVisibleChange?: (visible: boolean) => void;
/** /**
* When using columns, you can use this property to configure the properties that support the slot, * When using columns, you can setting this property to configure the properties that support the slot,
* such as slots: { filterIcon: 'XXX'} * such as slots: { filterIcon: 'XXX'}
* @type object * @type object
*/ */

View File

@ -86,7 +86,7 @@ export interface PaginationProps {
size?: string; size?: string;
/** /**
* whether to use simple mode * whether to setting simple mode
* @type boolean * @type boolean
*/ */
simple?: boolean; simple?: boolean;

View File

@ -1,4 +1,4 @@
// Any plugins you want to use has to be imported // Any plugins you want to setting has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/ // Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/ // Custom builds see https://www.tinymce.com/download/custom-builds/
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration // colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration

View File

@ -31,6 +31,7 @@ import {
PageHeader, PageHeader,
Result, Result,
Empty, Empty,
Avatar,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { getApp } from '/@/setup/App'; import { getApp } from '/@/setup/App';
@ -76,5 +77,6 @@ export function registerGlobComp() {
.use(PageHeader) .use(PageHeader)
.use(Result) .use(Result)
.use(Empty) .use(Empty)
.use(Avatar)
.use(Tabs); .use(Tabs);
} }

3
src/components/types.ts Normal file
View File

@ -0,0 +1,3 @@
import { defineComponent } from 'vue';
export type Component = ReturnType<typeof defineComponent>;

View File

@ -16,4 +16,4 @@
@page-loading-z-index: 10000; @page-loading-z-index: 10000;
// left-menu // left-menu
@app-menu-item-height: 44px; @app-menu-item-height: 42px;

View File

@ -5,4 +5,6 @@ export enum PageEnum {
BASE_HOME = '/dashboard', BASE_HOME = '/dashboard',
// error page path // error page path
ERROR_PAGE = '/exception', ERROR_PAGE = '/exception',
// error log page path
ERROR_LOG_PAGE = '/exception/error-log',
} }

View File

@ -0,0 +1,20 @@
import { customRef } from 'vue';
export function useDebouncedRef<T = any>(value: T, delay = 100) {
let timeout: TimeoutHandle;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue: T) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
}

View File

@ -0,0 +1,67 @@
import type { HeaderSetting } from '/@/types/config';
import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { MenuModeEnum } from '/@/enums/menuEnum';
export function useHeaderSetting() {
const { getShow: getShowMultipleTab } = useMultipleTabSetting();
const { getMode, getSplit, getShowHeaderTrigger, getIsSidebarType } = useMenuSetting();
const { getShowBreadCrumb, getShowLogo } = useRootSetting();
// Get header configuration
const getHeaderSetting = computed(() => appStore.getProjectConfig.headerSetting);
const getShowDoc = computed(() => unref(getHeaderSetting).showDoc);
const getTheme = computed(() => unref(getHeaderSetting).theme);
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
const getShowFullScreen = computed(() => unref(getHeaderSetting).showFullScreen);
const getShowNotice = computed(() => unref(getHeaderSetting).showNotice);
const getShowBread = computed(() => {
return (
unref(getMode) !== MenuModeEnum.HORIZONTAL && unref(getShowBreadCrumb) && !unref(getSplit)
);
});
const getShowHeaderLogo = computed(() => {
return unref(getShowLogo) && !unref(getIsSidebarType);
});
const getShowContent = computed(() => {
return unref(getShowBread) || unref(getShowHeaderTrigger);
});
// Set header configuration
function setHeaderSetting(headerSetting: Partial<HeaderSetting>): void {
appStore.commitProjectConfigState({ headerSetting });
}
return {
setHeaderSetting,
getHeaderSetting,
getShowDoc,
getTheme,
getShowRedo,
getUseLockPage,
getShowFullScreen,
getShowNotice,
getShowBread,
getShowContent,
getShowHeaderLogo,
};
}

View File

@ -1,6 +1,6 @@
import type { LocaleSetting } from '/@/types/config'; import type { LocaleSetting } from '/@/types/config';
import { computed } from 'vue'; import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import getProjectSetting from '/@/settings/projectSetting'; import getProjectSetting from '/@/settings/projectSetting';
@ -8,24 +8,16 @@ import { localeList } from '/@/locales';
export function useLocaleSetting() { export function useLocaleSetting() {
// Get locale configuration // Get locale configuration
const getLocale = computed(() => { const getLocale = computed(() => appStore.getProjectConfig.locale || getProjectSetting.locale);
return appStore.getProjectConfig.locale || getProjectSetting.locale;
});
// get current language // get current language
const getLang = computed(() => { const getLang = computed(() => unref(getLocale).lang);
return getLocale.value.lang;
});
// get Available Locales // get Available Locales
const getAvailableLocales = computed((): string[] => { const getAvailableLocales = computed((): string[] => unref(getLocale).availableLocales);
return getLocale.value.availableLocales;
});
// get Fallback Locales // get Fallback Locales
const getFallbackLocale = computed((): string => { const getFallbackLocale = computed((): string => unref(getLocale).fallback);
return getLocale.value.fallback;
});
// Set locale configuration // Set locale configuration
function setLocale(locale: Partial<LocaleSetting>): void { function setLocale(locale: Partial<LocaleSetting>): void {

View File

@ -0,0 +1,120 @@
import type { MenuSetting } from '/@/types/config';
import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
import { MenuModeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum';
export function useMenuSetting() {
// Get menu configuration
const getMenuSetting = computed(() => appStore.getProjectConfig.menuSetting);
const getMiniWidth = computed(() => unref(getMenuSetting).menuWidth);
const getCollapsed = computed(() => unref(getMenuSetting).collapsed);
const getType = computed(() => unref(getMenuSetting).type);
const getMode = computed(() => unref(getMenuSetting).mode);
const getShow = computed(() => unref(getMenuSetting).show);
const getMenuWidth = computed(() => unref(getMenuSetting).menuWidth);
const getTrigger = computed(() => unref(getMenuSetting).trigger);
const getTheme = computed(() => unref(getMenuSetting).theme);
const getSplit = computed(() => unref(getMenuSetting).split);
const getHasDrag = computed(() => unref(getMenuSetting).hasDrag);
const getAccordion = computed(() => unref(getMenuSetting).accordion);
const getCollapsedShowTitle = computed(() => unref(getMenuSetting).collapsedShowTitle);
const getCollapsedShowSearch = computed(() => unref(getMenuSetting).collapsedShowSearch);
const getTopMenuAlign = computed(() => unref(getMenuSetting).topMenuAlign);
const getIsSidebarType = computed(() => unref(getType) === MenuTypeEnum.SIDEBAR);
const getShowTopMenu = computed(() => {
return unref(getMode) === MenuModeEnum.HORIZONTAL || unref(getSplit);
});
const getShowHeaderTrigger = computed(() => {
if (
unref(getType) === MenuTypeEnum.TOP_MENU ||
!unref(getShow) ||
!unref(getMenuSetting).hidden
) {
return false;
}
return unref(getTrigger) === TriggerEnum.HEADER;
});
const getShowSearch = computed(() => {
return (
unref(getMenuSetting).showSearch &&
!(unref(getType) === MenuTypeEnum.MIX && unref(getMode) === MenuModeEnum.HORIZONTAL)
);
});
const getIsHorizontal = computed(() => {
return unref(getMode) === MenuModeEnum.HORIZONTAL;
});
const getMiniWidthNumber = computed(() => {
const { collapsedShowTitle } = unref(getMenuSetting);
return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH;
});
const getCalcContentWidth = computed(() => {
const width = unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMiniWidth);
return `calc(100% - ${width}px)`;
});
// Set menu configuration
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
appStore.commitProjectConfigState({ menuSetting });
}
function toggleCollapsed() {
setMenuSetting({
collapsed: !unref(getCollapsed),
});
}
return {
setMenuSetting,
toggleCollapsed,
getMenuSetting,
getMiniWidth,
getType,
getMode,
getShow,
getCollapsed,
getMiniWidthNumber,
getCalcContentWidth,
getMenuWidth,
getTrigger,
getSplit,
getTheme,
getHasDrag,
getIsHorizontal,
getShowSearch,
getCollapsedShowTitle,
getCollapsedShowSearch,
getIsSidebarType,
getAccordion,
getShowTopMenu,
getShowHeaderTrigger,
getTopMenuAlign,
};
}

View File

@ -0,0 +1,25 @@
import type { MultiTabsSetting } from '/@/types/config';
import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
export function useMultipleTabSetting() {
const getMultipleTabSetting = computed(() => appStore.getProjectConfig.multiTabsSetting);
const getMax = computed(() => unref(getMultipleTabSetting).max);
const getShow = computed(() => unref(getMultipleTabSetting).show);
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
appStore.commitProjectConfigState({ multiTabsSetting });
}
return {
setMultipleTabSetting,
getMultipleTabSetting,
getMax,
getShow,
};
}

View File

@ -0,0 +1,53 @@
import type { ProjectConfig } from '/@/types/config';
import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
type RootSetting = Omit<
ProjectConfig,
'locale' | 'headerSetting' | 'menuSetting' | 'multiTabsSetting'
>;
export function useRootSetting() {
const getRootSetting = computed((): RootSetting => appStore.getProjectConfig);
const getOpenPageLoading = computed(() => unref(getRootSetting).openPageLoading);
const getOpenRouterTransition = computed(() => unref(getRootSetting).openRouterTransition);
const getOpenKeepAlive = computed(() => unref(getRootSetting).openKeepAlive);
const getRouterTransition = computed(() => unref(getRootSetting).routerTransition);
const getCanEmbedIFramePage = computed(() => unref(getRootSetting).canEmbedIFramePage);
const getPermissionMode = computed(() => unref(getRootSetting).permissionMode);
const getShowLogo = computed(() => unref(getRootSetting).showLogo);
const getUseErrorHandle = computed(() => unref(getRootSetting).useErrorHandle);
const getShowBreadCrumb = computed(() => unref(getRootSetting).showBreadCrumb);
const getShowBreadCrumbIcon = computed(() => unref(getRootSetting).showBreadCrumbIcon);
function setRootSetting(setting: RootSetting) {
appStore.commitProjectConfigState(setting);
}
return {
setRootSetting,
getRootSetting,
getOpenPageLoading,
getOpenRouterTransition,
getOpenKeepAlive,
getRouterTransition,
getCanEmbedIFramePage,
getPermissionMode,
getShowLogo,
getUseErrorHandle,
getShowBreadCrumb,
getShowBreadCrumbIcon,
};
}

View File

@ -7,7 +7,7 @@ import { unref, ref } from 'vue';
import { getI18n } from '/@/setup/i18n'; import { getI18n } from '/@/setup/i18n';
import { useLocaleSetting } from '/@/settings/use/useLocaleSetting'; import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting';
import moment from 'moment'; import moment from 'moment';
@ -67,7 +67,7 @@ export function useLocale() {
} }
/** /**
* For non-setup use * For non-setup setting
*/ */
export function useExternalI18n() { export function useExternalI18n() {
return getI18n().global; return getI18n().global;

View File

@ -20,6 +20,7 @@ function handleError(e: Error) {
export function useGo() { export function useGo() {
const { push, replace } = useRouter(); const { push, replace } = useRouter();
function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) { function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
if (!opt) return;
if (isString(opt)) { if (isString(opt)) {
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError); isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError);
} else { } else {

View File

@ -1,20 +0,0 @@
import { computed } from 'vue';
import { appStore } from '/@/store/modules/app';
export function useSideBar() {
const currentCollapsedRef = computed(() => {
return appStore.getProjectConfig.menuSetting.collapsed;
});
const changeCollapsed = (collapsed: boolean) => {
appStore.commitProjectConfigState({
menuSetting: {
collapsed: collapsed,
},
});
};
return {
openSider: changeCollapsed(false),
closeSider: changeCollapsed(true),
currentCollapsedRef,
};
}

View File

@ -1,214 +0,0 @@
import { computed, defineComponent, nextTick, onMounted, ref, unref } from 'vue';
import { Layout } from 'ant-design-vue';
import LayoutTrigger from './LayoutTrigger';
import LayoutMenu from '/@/layouts/default/menu/LayoutMenu';
import { menuStore } from '/@/store/modules/menu';
import { appStore } from '/@/store/modules/app';
import { MenuModeEnum, MenuSplitTyeEnum, TriggerEnum } from '/@/enums/menuEnum';
import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
import { useDebounce } from '/@/hooks/core/useDebounce';
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 getMiniWidth = computed(() => {
const {
menuSetting: { collapsedShowTitle },
} = unref(getProjectConfigRef);
return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH;
});
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;
}
// Menu area drag and drop-mouse movement
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 = unref(getMiniWidth);
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 > unref(getMiniWidth) + 20) {
setMenuWidth(width);
} else {
menuStore.commitCollapsedState(true);
}
} else {
if (width > unref(getMiniWidth)) {
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;
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;
}
const getDragBarStyle = computed(() => {
if (menuStore.getCollapsedState) {
return { left: `${unref(getMiniWidth)}px` };
}
return {};
});
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidth);
});
const showTrigger = computed(() => {
const {
menuSetting: { trigger },
} = unref(getProjectConfigRef);
return trigger !== TriggerEnum.NONE && trigger === TriggerEnum.FOOTER;
});
onMounted(() => {
nextTick(() => {
const [exec] = useDebounce(changeWrapWidth, 20);
exec();
});
});
function handleSiderClick(e: ChangeEvent) {
if (!e || !e.target || e.target.className !== 'basic-menu__content') return;
const { collapsed, show } = appStore.getProjectConfig.menuSetting;
if (!collapsed || !show) return;
appStore.commitProjectConfigState({
menuSetting: {
collapsed: false,
},
});
}
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;
const triggerDom = unref(showTrigger)
? {
trigger: () => <LayoutTrigger />,
}
: {};
const triggerAttr = unref(showTrigger)
? {}
: {
trigger: null,
};
return (
<Layout.Sider
onClick={handleSiderClick}
onCollapse={onCollapseChange}
breakpoint="md"
width={getMenuWidthState}
collapsed={getCollapsedState}
collapsible
collapsedWidth={unref(getCollapsedWidth)}
theme={theme}
class="layout-sidebar"
ref={sideRef}
onBreakpoint={handleBreakpoint}
{...triggerAttr}
>
{{
...triggerDom,
default: () => (
<>
<LayoutMenu
theme={theme}
menuMode={splitMenu ? MenuModeEnum.INLINE : null}
splitType={splitMenu ? MenuSplitTyeEnum.LEFT : MenuSplitTyeEnum.NONE}
/>
{renderDragLine()}
</>
),
}}
</Layout.Sider>
);
};
},
});

View File

@ -1,41 +1,39 @@
import type { PropType } from 'vue';
import { defineComponent, unref } from 'vue';
import { import {
DoubleRightOutlined, DoubleRightOutlined,
DoubleLeftOutlined, DoubleLeftOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
MenuFoldOutlined, MenuFoldOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { defineComponent } from 'vue';
// store import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { menuStore } from '/@/store/modules/menu';
export default defineComponent({ export default defineComponent({
name: 'LayoutTrigger', name: 'LayoutTrigger',
props: { props: {
sider: { sider: {
type: Boolean, type: Boolean as PropType<boolean>,
default: true, default: true,
}, },
theme: { theme: {
type: String, type: String as PropType<string>,
}, },
}, },
setup(props) { setup(props) {
function toggleMenu() { const { toggleCollapsed, getCollapsed } = useMenuSetting();
menuStore.commitCollapsedState(!menuStore.getCollapsedState);
}
return () => { return () => {
const siderTrigger = menuStore.getCollapsedState ? ( const siderTrigger = unref(getCollapsed) ? <DoubleRightOutlined /> : <DoubleLeftOutlined />;
<DoubleRightOutlined />
) : ( if (props.sider) {
<DoubleLeftOutlined /> return siderTrigger;
); }
if (props.sider) return siderTrigger;
return ( return (
<span class={['layout-trigger', props.theme]} onClick={toggleMenu}> <span class={['layout-trigger', props.theme]} onClick={toggleCollapsed}>
{menuStore.getCollapsedState ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />} {unref(getCollapsed) ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span> </span>
); );
}; };

View File

@ -4,14 +4,17 @@ import type { PropType } from 'vue';
import { defineComponent, TransitionGroup, unref, watch, ref } from 'vue'; import { defineComponent, TransitionGroup, unref, watch, ref } from 'vue';
import Breadcrumb from '/@/components/Breadcrumb/Breadcrumb.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';
import Icon from '/@/components/Icon'; import Icon from '/@/components/Icon';
import BreadcrumbItem from '/@/components/Breadcrumb/BreadcrumbItem.vue';
import { useRouter } from 'vue-router';
import { isBoolean } from '/@/utils/is';
import { compile } from 'path-to-regexp';
import router from '/@/router';
import { PageEnum } from '/@/enums/pageEnum';
export default defineComponent({ export default defineComponent({
name: 'BasicBreadcrumb', name: 'BasicBreadcrumb',
@ -40,7 +43,6 @@ export default defineComponent({
const matchedList = matched.filter((item) => item.meta && item.meta.title).slice(1); const matchedList = matched.filter((item) => item.meta && item.meta.title).slice(1);
const firstItem = matchedList[0]; const firstItem = matchedList[0];
const ret = getHomeRoute(firstItem); const ret = getHomeRoute(firstItem);
if (!isBoolean(ret)) { if (!isBoolean(ret)) {
matchedList.unshift(ret); matchedList.unshift(ret);
} }
@ -74,42 +76,51 @@ export default defineComponent({
return push(pathCompile(path)); return push(pathCompile(path));
} }
function renderItemContent(item: AppRouteRecordRaw) {
return (
<>
{props.showIcon && item.meta.icon && item.meta.icon.trim() !== '' && (
<Icon
icon={item.meta.icon}
class="icon mr-1 "
style={{
marginBottom: '2px',
}}
/>
)}
{item.meta.title}
</>
);
}
function renderBreadcrumbItemList() {
return unref(itemList).map((item) => {
const isLink =
(!!item.redirect && !item.meta.disabledRedirect) ||
!item.children ||
item.children.length === 0;
return (
<BreadcrumbItem
key={item.path}
isLink={isLink}
onClick={handleItemClick.bind(null, item)}
>
{() => renderItemContent(item as AppRouteRecordRaw)}
</BreadcrumbItem>
);
});
}
function renderBreadcrumbDefault() {
return (
<TransitionGroup name="breadcrumb">{() => renderBreadcrumbItemList()}</TransitionGroup>
);
}
return () => ( return () => (
<Breadcrumb class={['layout-breadcrumb', unref(itemList).length === 0 ? 'hidden' : '']}> <Breadcrumb class={['layout-breadcrumb', unref(itemList).length === 0 ? 'hidden' : '']}>
{() => ( {() => renderBreadcrumbDefault()}
<TransitionGroup name="breadcrumb">
{() => {
return unref(itemList).map((item) => {
const isLink =
(!!item.redirect && !item.meta.disabledRedirect) ||
!item.children ||
item.children.length === 0;
return (
<BreadcrumbItem
key={item.path}
isLink={isLink}
onClick={handleItemClick.bind(null, item)}
>
{() => (
<>
{props.showIcon && item.meta.icon && item.meta.icon.trim() !== '' && (
<Icon
icon={item.meta.icon}
class="icon mr-1 "
style={{
marginBottom: '2px',
}}
/>
)}
{item.meta.title}
</>
)}
</BreadcrumbItem>
);
});
}}
</TransitionGroup>
)}
</Breadcrumb> </Breadcrumb>
); );
}, },

View File

@ -1,7 +1,9 @@
import './index.less';
import { defineComponent, unref, computed, ref } from 'vue'; import { defineComponent, unref, computed, ref } from 'vue';
import { Layout, Tooltip, Badge } from 'ant-design-vue'; import { Layout, Tooltip, Badge } from 'ant-design-vue';
import Logo from '/@/layouts/logo/index.vue'; import { AppLogo } from '/@/components/Application';
import UserDropdown from './UserDropdown'; import UserDropdown from './UserDropdown';
import LayoutMenu from '/@/layouts/default/menu/LayoutMenu'; import LayoutMenu from '/@/layouts/default/menu/LayoutMenu';
import LayoutBreadcrumb from './LayoutBreadcrumb'; import LayoutBreadcrumb from './LayoutBreadcrumb';
@ -12,50 +14,57 @@ import {
RedoOutlined, RedoOutlined,
FullscreenExitOutlined, FullscreenExitOutlined,
FullscreenOutlined, FullscreenOutlined,
GithubFilled,
LockOutlined, LockOutlined,
BugOutlined, BugOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import { useFullscreen } from '/@/hooks/web/useFullScreen'; import { useFullscreen } from '/@/hooks/web/useFullScreen';
import { useTabs } from '/@/hooks/web/useTabs'; import { useTabs } from '/@/hooks/web/useTabs';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn'; import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useRouter } from 'vue-router'; import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useModal } from '/@/components/Modal'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useRouter } from 'vue-router';
import { appStore } from '/@/store/modules/app';
import { errorStore } from '/@/store/modules/error'; import { errorStore } from '/@/store/modules/error';
import { MenuModeEnum, MenuSplitTyeEnum, MenuTypeEnum, TriggerEnum } from '/@/enums/menuEnum'; import { PageEnum } from '/@/enums/pageEnum';
import { GITHUB_URL } from '/@/settings/siteSetting'; import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { Component } from '/@/components/types';
import './index.less';
export default defineComponent({ export default defineComponent({
name: 'DefaultLayoutHeader', name: 'LayoutHeader',
setup() { setup() {
const widthRef = ref(200);
let logoEl: Element | null; let logoEl: Element | null;
const widthRef = ref(200);
const { refreshPage } = useTabs(); const { refreshPage } = useTabs();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getTopMenuAlign } = useMenuSetting();
const { getUseErrorHandle, getShowBreadCrumbIcon } = useRootSetting();
const {
getTheme,
getShowRedo,
getUseLockPage,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
} = useHeaderSetting();
const { push } = useRouter(); const { push } = useRouter();
const [register, { openModal }] = useModal(); const [register, { openModal }] = useModal();
const { toggleFullscreen, isFullscreenRef } = useFullscreen(); const { toggleFullscreen, isFullscreenRef } = useFullscreen();
const getProjectConfigRef = computed(() => {
return appStore.getProjectConfig;
});
const showTopMenu = computed(() => {
const getProjectConfig = unref(getProjectConfigRef);
const {
menuSetting: { mode, split: splitMenu },
} = getProjectConfig;
return mode === MenuModeEnum.HORIZONTAL || splitMenu;
});
useWindowSizeFn( useWindowSizeFn(
() => { () => {
if (!unref(showTopMenu)) return; if (!unref(getShowTopMenu)) return;
let width = 0; let width = 0;
if (!logoEl) { if (!logoEl) {
logoEl = document.querySelector('.layout-header__logo'); logoEl = document.querySelector('.layout-header__logo');
@ -69,24 +78,23 @@ export default defineComponent({
{ immediate: true } { immediate: true }
); );
function goToGithub() {
window.open(GITHUB_URL, '__blank');
}
const headerClass = computed(() => { const headerClass = computed(() => {
const theme = unref(getProjectConfigRef).headerSetting.theme; const theme = unref(getTheme);
return theme ? `layout-header__header--${theme}` : ''; return theme ? `layout-header__header--${theme}` : '';
}); });
const showHeaderTrigger = computed(() => { const getSplitType = computed(() => {
const { show, trigger, hidden, type } = unref(getProjectConfigRef).menuSetting; return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
if (type === MenuTypeEnum.TOP_MENU || !show || !hidden) return false; });
return trigger === TriggerEnum.HEADER;
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
}); });
function handleToErrorList() { function handleToErrorList() {
errorStore.commitErrorListCountState(0); push(PageEnum.ERROR_LOG_PAGE).then(() => {
push('/exception/error-log'); errorStore.commitErrorListCountState(0);
});
} }
/** /**
@ -96,162 +104,129 @@ export default defineComponent({
openModal(true); openModal(true);
} }
return () => { function renderHeaderContent() {
const getProjectConfig = unref(getProjectConfigRef);
const {
useErrorHandle,
showLogo,
multiTabsSetting: { show: showTab },
headerSetting: {
theme: headerTheme,
useLockPage,
showRedo,
showGithub,
showFullScreen,
showNotice,
},
menuSetting: { mode, type: menuType, split: splitMenu, topMenuAlign },
showBreadCrumb,
showBreadCrumbIcon,
} = getProjectConfig;
const isSidebarType = menuType === MenuTypeEnum.SIDEBAR;
const width = unref(widthRef); const width = unref(widthRef);
return (
<div class="layout-header__content ">
{unref(getShowHeaderLogo) && (
<AppLogo class={`layout-header__logo`} theme={unref(getTheme)} />
)}
const showLeft = {unref(getShowContent) && (
(mode !== MenuModeEnum.HORIZONTAL && showBreadCrumb && !splitMenu) || <div class="layout-header__left">
unref(showHeaderTrigger); {unref(getShowHeaderTrigger) && (
<LayoutTrigger theme={unref(getTheme)} sider={false} />
)}
{unref(getShowBread) && <LayoutBreadcrumb showIcon={unref(getShowBreadCrumbIcon)} />}
</div>
)}
{unref(getShowTopMenu) && (
<div class={[`layout-header__menu `]} style={{ width: `calc(100% - ${width}px)` }}>
<LayoutMenu
isHorizontal={true}
class={`justify-${unref(getTopMenuAlign)}`}
theme={unref(getTheme)}
splitType={unref(getSplitType)}
menuMode={unref(getMenuMode)}
showSearch={false}
/>
</div>
)}
</div>
);
}
function renderActionDefault(Comp: Component | any, event: Fn) {
return (
<div class={`layout-header__action-item`} onClick={event}>
<Comp class={`layout-header__action-icon`} />
</div>
);
}
function renderAction() {
return (
<div class={`layout-header__action`}>
{unref(getUseErrorHandle) && (
<Tooltip>
{{
title: () => '错误日志',
default: () => (
<Badge
count={errorStore.getErrorListCountState}
offset={[0, 10]}
dot
overflowCount={99}
>
{() => renderActionDefault(BugOutlined, handleToErrorList)}
</Badge>
),
}}
</Tooltip>
)}
{unref(getUseLockPage) && (
<Tooltip>
{{
title: () => '锁定屏幕',
default: () => renderActionDefault(LockOutlined, handleLockPage),
}}
</Tooltip>
)}
{unref(getShowNotice) && (
<Tooltip>
{{
title: () => '消息通知',
default: () => <NoticeAction />,
}}
</Tooltip>
)}
{unref(getShowRedo) && (
<Tooltip>
{{
title: () => '刷新',
default: () => renderActionDefault(RedoOutlined, refreshPage),
}}
</Tooltip>
)}
{unref(getShowFullScreen) && (
<Tooltip>
{{
title: () => (unref(isFullscreenRef) ? '退出全屏' : '全屏'),
default: () => {
const Icon = !unref(isFullscreenRef) ? (
<FullscreenOutlined />
) : (
<FullscreenExitOutlined />
);
return renderActionDefault(Icon, toggleFullscreen);
},
}}
</Tooltip>
)}
<UserDropdown class={`layout-header__user-dropdown`} />
</div>
);
}
function renderHeaderDefault() {
return (
<>
{renderHeaderContent()}
{renderAction()}
<LockAction onRegister={register} />
</>
);
}
return () => {
return ( return (
<Layout.Header class={['layout-header', 'flex p-0 px-4 ', unref(headerClass)]}> <Layout.Header class={['layout-header', 'flex p-0 px-4 ', unref(headerClass)]}>
{() => ( {() => renderHeaderDefault()}
<>
<div class="layout-header__content ">
{showLogo && !isSidebarType && (
<Logo class={`layout-header__logo`} theme={headerTheme} />
)}
{showLeft && (
<div class="layout-header__left">
{unref(showHeaderTrigger) && (
<LayoutTrigger theme={headerTheme} sider={false} />
)}
{mode !== MenuModeEnum.HORIZONTAL && showBreadCrumb && !splitMenu && (
<LayoutBreadcrumb showIcon={showBreadCrumbIcon} />
)}
</div>
)}
{unref(showTopMenu) && (
<div
class={[`layout-header__menu `]}
style={{ width: `calc(100% - ${unref(width)}px)` }}
>
<LayoutMenu
isTop={true}
class={`justify-${topMenuAlign}`}
theme={headerTheme}
splitType={splitMenu ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE}
menuMode={splitMenu ? MenuModeEnum.HORIZONTAL : null}
showSearch={false}
/>
</div>
)}
</div>
<div class={`layout-header__action`}>
{useErrorHandle && (
<Tooltip>
{{
title: () => '错误日志',
default: () => (
<Badge
count={errorStore.getErrorListCountState}
offset={[0, 10]}
dot
overflowCount={99}
>
{() => (
<div class={`layout-header__action-item`} onClick={handleToErrorList}>
<BugOutlined class={`layout-header__action-icon`} />
</div>
)}
</Badge>
),
}}
</Tooltip>
)}
{showGithub && (
<Tooltip>
{{
title: () => 'github',
default: () => (
<div class={`layout-header__action-item`} onClick={goToGithub}>
<GithubFilled class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{useLockPage && (
<Tooltip>
{{
title: () => '锁定屏幕',
default: () => (
<div class={`layout-header__action-item`} onClick={handleLockPage}>
<LockOutlined class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{showNotice && (
<div>
<Tooltip>
{{
title: () => '消息通知',
default: () => <NoticeAction />,
}}
</Tooltip>
</div>
)}
{showRedo && showTab && (
<Tooltip>
{{
title: () => '刷新',
default: () => (
<div class={`layout-header__action-item`} onClick={refreshPage}>
<RedoOutlined class={`layout-header__action-icon`} />
</div>
),
}}
</Tooltip>
)}
{showFullScreen && (
<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> </Layout.Header>
); );
}; };

View File

@ -1,7 +1,6 @@
.lock-modal { .lock-modal {
&__entry { &__entry {
position: relative; position: relative;
// width: 500px;
height: 240px; height: 240px;
padding: 130px 30px 60px 30px; padding: 130px 30px 60px 30px;
background: #fff; background: #fff;

View File

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

View File

@ -11,15 +11,23 @@ import Icon from '/@/components/Icon/index';
import { userStore } from '/@/store/modules/user'; import { userStore } from '/@/store/modules/user';
import { DOC_URL } from '/@/settings/siteSetting'; import { DOC_URL } from '/@/settings/siteSetting';
import { appStore } from '/@/store/modules/app';
import { openWindow } from '/@/utils';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
interface RenderItemParams {
icon: string;
text: string;
key: string;
}
const prefixCls = 'user-dropdown'; const prefixCls = 'user-dropdown';
export default defineComponent({ export default defineComponent({
name: 'UserDropdown', name: 'UserDropdown',
setup() { setup() {
const getProjectConfigRef = computed(() => { const { getShowDoc } = useHeaderSetting();
return appStore.getProjectConfig;
});
const getUserInfo = computed(() => { const getUserInfo = computed(() => {
const { realName = '', desc } = userStore.getUserInfoState || {}; const { realName = '', desc } = userStore.getUserInfoState || {};
@ -33,7 +41,7 @@ export default defineComponent({
// open doc // open doc
function openDoc() { function openDoc() {
window.open(DOC_URL, '__blank'); openWindow(DOC_URL);
} }
function handleMenuClick(e: any) { function handleMenuClick(e: any) {
@ -44,7 +52,7 @@ export default defineComponent({
} }
} }
function renderItem({ icon, text, key }: { icon: string; text: string; key: string }) { function renderItem({ icon, text, key }: RenderItemParams) {
return ( return (
<Menu.Item key={key}> <Menu.Item key={key}>
{() => ( {() => (
@ -57,37 +65,43 @@ export default defineComponent({
); );
} }
return () => { function renderSlotsDefault() {
const { realName } = unref(getUserInfo); const { realName } = unref(getUserInfo);
const { return (
headerSetting: { showDoc }, <section class={prefixCls}>
} = unref(getProjectConfigRef); <img class={`${prefixCls}__header`} src={headerImg} />
<section class={`${prefixCls}__info`}>
<section class={`${prefixCls}__name`}>{realName}</section>
</section>
</section>
);
}
function renderSlotOverlay() {
const showDoc = unref(getShowDoc);
return (
<Menu onClick={handleMenuClick}>
{() => (
<>
{showDoc && renderItem({ key: 'doc', text: '文档', icon: 'gg:loadbar-doc' })}
{showDoc && <Divider />}
{renderItem({
key: 'loginOut',
text: '退出系统',
icon: 'ant-design:poweroff-outlined',
})}
</>
)}
</Menu>
);
}
return () => {
return ( return (
<Dropdown placement="bottomLeft"> <Dropdown placement="bottomLeft">
{{ {{
default: () => ( default: () => renderSlotsDefault(),
<section class={prefixCls}> overlay: () => renderSlotOverlay(),
<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 && renderItem({ key: 'doc', text: '文档', icon: 'gg:loadbar-doc' })}
{showDoc && <Divider />}
{renderItem({
key: 'loginOut',
text: '退出系统',
icon: 'ant-design:poweroff-outlined',
})}
</>
)}
</Menu>
),
}} }}
</Dropdown> </Dropdown>
); );

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="layout-header__action-item notify-action"> <div class="layout-header__action-item notify-action">
<Popover title="" trigger="click"> <Popover title="" trigger="click" overlayClassName="layout-header__notify-action">
<Badge :count="count" dot :numberStyle="numberStyle"> <Badge :count="count" dot :numberStyle="numberStyle">
<BellOutlined class="layout-header__action-icon" /> <BellOutlined class="layout-header__action-icon" />
</Badge> </Badge>
@ -31,6 +31,7 @@
components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList }, components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
setup() { setup() {
let count = 0; let count = 0;
for (let i = 0; i < tabListData.length; i++) { for (let i = 0; i < tabListData.length; i++) {
count += tabListData[i].list.length; count += tabListData[i].list.length;
} }
@ -44,6 +45,10 @@
}); });
</script> </script>
<style lang="less"> <style lang="less">
.layout-header__notify-action {
max-width: 360px;
}
.notify-action { .notify-action {
padding-top: 2px; padding-top: 2px;
@ -56,7 +61,6 @@
.ant-badge-multiple-words { .ant-badge-multiple-words {
padding: 0 4px; padding: 0 4px;
// transform: translate(26%, -40%);
} }
svg { svg {

View File

@ -1,36 +1,37 @@
<template> <template>
<List class="list"> <a-list class="list">
<template v-for="item in list" :key="item.id"> <template v-for="item in list" :key="item.id">
<ListItem class="list__item"> <a-list-item class="list-item">
<ListItemMeta> <a-list-item-meta>
<template #title> <template #title>
<div class="title"> <div class="title">
{{ item.title }} {{ item.title }}
<div class="extra" v-if="item.extra"> <div class="extra" v-if="item.extra">
<Tag class="tag" :color="item.color"> <a-tag class="tag" :color="item.color">
{{ item.extra }} {{ item.extra }}
</Tag> </a-tag>
</div> </div>
</div> </div>
</template> </template>
<template #avatar> <template #avatar>
<Avatar v-if="item.avatar" class="avatar" :src="item.avatar" /> <a-avatar v-if="item.avatar" class="avatar" :src="item.avatar" />
<span v-else> {{ item.avatar }}</span> <span v-else> {{ item.avatar }}</span>
</template> </template>
<template #description> <template #description>
<div> <div>
<div class="description">{{ item.description }}</div> <div class="description">{{ item.description }}</div>
<div class="datetime">{{ item.datetime }}</div> <div class="datetime">{{ item.datetime }}</div>
</div> </div>
</template> </template>
</ListItemMeta> </a-list-item-meta>
</ListItem> </a-list-item>
</template> </template>
</List> </a-list>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { List, Avatar, Tag } from 'ant-design-vue';
import { ListItem } from './data'; import { ListItem } from './data';
export default defineComponent({ export default defineComponent({
@ -40,19 +41,6 @@
default: () => [], default: () => [],
}, },
}, },
components: {
List,
ListItem: List.Item,
ListItemMeta: List.Item.Meta,
Avatar,
Tag,
},
setup(props) {
const { list = [] } = props;
return {
list,
};
},
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -61,7 +49,7 @@
display: none; display: none;
} }
&__item { &-item {
padding: 6px; padding: 6px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;

View File

@ -56,14 +56,6 @@ export const tabListData: TabItem[] = [
datetime: '2017-08-07', datetime: '2017-08-07',
type: '1', type: '1',
}, },
// {
// id: '000000005',
// avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
// title: '内容不要超过两行字,超出时自动截断',
// description: '',
// datetime: '2017-08-07',
// type: '1',
// },
], ],
}, },
{ {

View File

@ -36,47 +36,4 @@
margin: 0 auto; margin: 0 auto;
} }
} }
.layout-sidebar {
background-size: 100% 100%;
&.ant-layout-sider-dark {
background: @sider-dark-bg-color;
}
&:not(.ant-layout-sider-dark) {
border-right: 1px solid @border-color-light;
}
.ant-layout-sider-zero-width-trigger {
top: 40%;
z-index: 10;
}
&__dargbar {
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: @primary-color;
box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
}
}
}
}
.ant-layout-sider-trigger {
height: 36px;
line-height: 36px;
} }

View File

@ -4,7 +4,7 @@ import LayoutHeader from './header/LayoutHeader';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import LayoutContent from './LayoutContent'; import LayoutContent from './LayoutContent';
import LayoutSideBar from './LayoutSideBar'; import LayoutSideBar from './sider/LayoutSideBar';
import SettingBtn from './setting/index.vue'; import SettingBtn from './setting/index.vue';
import MultipleTabs from './multitabs/index'; import MultipleTabs from './multitabs/index';
@ -36,7 +36,7 @@ export default defineComponent({
return show; return show;
}); });
const isShowMixHeaderRef = computed(() => { const showMixHeaderRef = computed(() => {
const { const {
menuSetting: { type }, menuSetting: { type },
} = unref(getProjectConfigRef); } = unref(getProjectConfigRef);
@ -57,11 +57,11 @@ export default defineComponent({
}); });
const showFullHeaderRef = computed(() => { const showFullHeaderRef = computed(() => {
return !unref(getFullContent) && unref(isShowMixHeaderRef) && unref(showHeaderRef); return !unref(getFullContent) && unref(showMixHeaderRef) && unref(showHeaderRef);
}); });
const showInsetHeaderRef = computed(() => { const showInsetHeaderRef = computed(() => {
return !unref(getFullContent) && !unref(isShowMixHeaderRef) && unref(showHeaderRef); return !unref(getFullContent) && !unref(showMixHeaderRef) && unref(showHeaderRef);
}); });
const fixedHeaderClsRef = computed(() => { const fixedHeaderClsRef = computed(() => {

View File

@ -1,29 +1,21 @@
import type { PropType } from 'vue'; import './index.less';
import { PropType, toRef } from 'vue';
import type { Menu } from '/@/router/types'; import type { Menu } from '/@/router/types';
import { computed, defineComponent, unref, ref, onMounted, watch } from 'vue'; import { computed, defineComponent, unref } from 'vue';
import { BasicMenu } from '/@/components/Menu/index'; import { BasicMenu } from '/@/components/Menu/index';
import Logo from '/@/layouts/logo/index.vue'; import { AppLogo } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
// store import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { appStore } from '/@/store/modules/app'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { menuStore } from '/@/store/modules/menu';
import { import { useGo } from '/@/hooks/web/usePage';
getMenus, import { useSplitMenu } from './useLayoutMenu';
getFlatMenus, import { openWindow } from '/@/utils';
getShallowMenus,
getChildrenMenus,
getFlatChildrenMenus,
getCurrentParentPath,
} from '/@/router/menus/index';
import { useRouter } from 'vue-router';
import { useThrottle } from '/@/hooks/core/useThrottle';
import { permissionStore } from '/@/store/modules/permission';
import './index.less';
export default defineComponent({ export default defineComponent({
name: 'DefaultLayoutMenu', name: 'DefaultLayoutMenu',
props: { props: {
@ -43,7 +35,7 @@ export default defineComponent({
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: true, default: true,
}, },
isTop: { isHorizontal: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
default: false, default: false,
}, },
@ -53,190 +45,99 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
// Menu array const go = useGo();
const menusRef = ref<Menu[]>([]);
// flat menu array
const flatMenusRef = ref<Menu[]>([]);
const { currentRoute, push } = useRouter();
// get app config const {
const getProjectConfigRef = computed(() => { setMenuSetting,
return appStore.getProjectConfig; getShowSearch,
getMode,
getType,
getCollapsedShowTitle,
getCollapsedShowSearch,
getIsSidebarType,
getTheme,
getCollapsed,
getAccordion,
} = useMenuSetting();
const { getShowLogo } = useRootSetting();
const { flatMenusRef, menusRef } = useSplitMenu(toRef(props, 'splitType'));
const showLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
const getMenuMode = computed(() => props.menuMode || unref(getMode));
const getMenuTheme = computed(() => props.theme || unref(getTheme));
const appendClass = computed(() => props.splitType === MenuSplitTyeEnum.TOP);
const showSearch = computed(() => {
return (
unref(getShowSearch) &&
props.showSearch &&
(unref(getCollapsedShowSearch) ? true : !unref(getCollapsed))
);
}); });
// get is Horizontal /**
const getIsHorizontalRef = computed(() => { * click menu
return unref(getProjectConfigRef).menuSetting.mode === MenuModeEnum.HORIZONTAL; * @param menu
}); */
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
// Route change split menu
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,
}
);
// Menu changes
watch(
[() => permissionStore.getLastBuildMenuTimeState, () => permissionStore.getBackMenuListState],
() => {
genMenus();
}
);
// split Menu changes
watch([() => appStore.getProjectConfig.menuSetting.split], () => {
if (props.splitType !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontalRef)) return;
genMenus();
});
// Handle left menu split
async function handleSplitLeftMenu(parentPath: string) {
const isSplitMenu = unref(getProjectConfigRef).menuSetting.split;
if (!isSplitMenu) return;
const { splitType } = props;
// spilt mode left
if (splitType === MenuSplitTyeEnum.LEFT) {
const children = await getChildrenMenus(parentPath);
if (!children) {
appStore.commitProjectConfigState({
menuSetting: {
hidden: false,
},
});
flatMenusRef.value = [];
menusRef.value = [];
return;
}
const flatChildren = await getFlatChildrenMenus(children);
appStore.commitProjectConfigState({
menuSetting: {
hidden: true,
},
});
flatMenusRef.value = flatChildren;
menusRef.value = children;
}
}
// get menus
async function genMenus() {
const isSplitMenu = unref(getProjectConfigRef).menuSetting.split;
// normal mode
const { splitType } = props;
if (splitType === MenuSplitTyeEnum.NONE || !isSplitMenu) {
flatMenusRef.value = await getFlatMenus();
menusRef.value = await getMenus();
return;
}
// split-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) { function handleMenuClick(menu: Menu) {
const { path } = menu; go(menu.path);
if (path) {
push(path);
const { splitType } = props;
// split mode top
if (splitType === MenuSplitTyeEnum.TOP) {
menuStore.commitCurrentTopSplitMenuPathState(path);
}
}
} }
/**
* before click menu
* @param menu
*/
async function beforeMenuClickFn(menu: Menu) { async function beforeMenuClickFn(menu: Menu) {
const { meta: { externalLink } = {} } = menu; const { meta: { externalLink } = {} } = menu;
if (externalLink) { if (externalLink) {
window.open(externalLink, '_blank'); openWindow(externalLink);
return false; return false;
} }
return true; return true;
} }
function handleClickSearchInput() { function handleClickSearchInput() {
if (menuStore.getCollapsedState) { unref(getCollapsed) && setMenuSetting({ collapsed: false });
menuStore.commitCollapsedState(false);
}
} }
const showSearchRef = computed(() => { function renderHeader() {
const { showSearch, type, mode } = unref(getProjectConfigRef).menuSetting; if (!unref(showLogo)) return null;
return ( return (
showSearch && <AppLogo
props.showSearch && showTitle={!unref(getCollapsed)}
!(type === MenuTypeEnum.MIX && mode === MenuModeEnum.HORIZONTAL) class={[`layout-menu__logo`, unref(getMenuTheme)]}
theme={unref(getMenuTheme)}
/>
); );
}); }
onMounted(() => {
genMenus();
});
return () => { return () => {
const {
showLogo,
menuSetting: {
type: menuType,
mode,
theme,
collapsed,
collapsedShowTitle,
collapsedShowSearch,
accordion,
},
} = unref(getProjectConfigRef);
const isSidebarType = menuType === MenuTypeEnum.SIDEBAR;
const isShowLogo = showLogo && isSidebarType;
const themeData = props.theme || theme;
return ( return (
<BasicMenu <BasicMenu
beforeClickFn={beforeMenuClickFn}
onMenuClick={handleMenuClick}
type={menuType}
mode={props.menuMode || mode}
class="layout-menu" class="layout-menu"
collapsedShowTitle={collapsedShowTitle} beforeClickFn={beforeMenuClickFn}
theme={themeData} isHorizontal={props.isHorizontal}
showLogo={isShowLogo} appendClass={unref(appendClass)}
search={unref(showSearchRef) && (collapsedShowSearch ? true : !collapsed)} type={unref(getType)}
mode={unref(getMenuMode)}
collapsedShowTitle={unref(getCollapsedShowTitle)}
theme={unref(getMenuTheme)}
showLogo={unref(showLogo)}
search={unref(showSearch)}
items={unref(menusRef)} items={unref(menusRef)}
flatItems={unref(flatMenusRef)} flatItems={unref(flatMenusRef)}
accordion={unref(getAccordion)}
onMenuClick={handleMenuClick}
onClickSearchInput={handleClickSearchInput} onClickSearchInput={handleClickSearchInput}
appendClass={props.splitType === MenuSplitTyeEnum.TOP}
isTop={props.isTop}
accordion={accordion}
> >
{{ {{
header: () => header: () => renderHeader(),
isShowLogo && (
<Logo
showTitle={!collapsed}
class={[`layout-menu__logo`, themeData]}
theme={themeData}
/>
),
}} }}
</BasicMenu> </BasicMenu>
); );

View File

@ -9,17 +9,5 @@
width: @logo-width; width: @logo-width;
height: @logo-width; height: @logo-width;
} }
&.light {
.logo-title {
color: @text-color-base;
}
}
&.dark {
.logo-title {
color: @white;
}
}
} }
} }

View File

@ -0,0 +1,113 @@
import type { Menu } from '/@/router/types';
import type { Ref } from 'vue';
import { watch, unref, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useThrottle } from '/@/hooks/core/useThrottle';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import {
getChildrenMenus,
getCurrentParentPath,
getFlatChildrenMenus,
getFlatMenus,
getMenus,
getShallowMenus,
} from '/@/router/menus';
import { permissionStore } from '/@/store/modules/permission';
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Menu array
const menusRef = ref<Menu[]>([]);
// flat menu array
const flatMenusRef = ref<Menu[]>([]);
const { currentRoute } = useRouter();
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
const splitNotLeft = computed(
() => unref(splitType) !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontal)
);
const splitLeft = computed(() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT);
const spiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
const normalType = computed(() => {
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
});
watch(
[() => unref(currentRoute).path, () => unref(splitType)],
async ([path]: [string, MenuSplitTyeEnum]) => {
if (unref(splitNotLeft)) return;
const parentPath = await getCurrentParentPath(path);
parentPath && throttleHandleSplitLeftMenu(parentPath);
},
{
immediate: true,
}
);
// Menu changes
watch(
[() => permissionStore.getLastBuildMenuTimeState, () => permissionStore.getBackMenuListState],
() => {
genMenus();
},
{
immediate: true,
}
);
// split Menu changes
watch([() => getSplit.value], () => {
if (unref(splitNotLeft)) return;
genMenus();
});
// Handle left menu split
async function handleSplitLeftMenu(parentPath: string) {
if (unref(splitLeft)) return;
// spilt mode left
const children = await getChildrenMenus(parentPath);
if (!children) {
setMenuSetting({ hidden: false });
flatMenusRef.value = [];
menusRef.value = [];
return;
}
const flatChildren = await getFlatChildrenMenus(children);
setMenuSetting({ hidden: true });
flatMenusRef.value = flatChildren;
menusRef.value = children;
}
// get menus
async function genMenus() {
// normal mode
if (unref(normalType)) {
flatMenusRef.value = await getFlatMenus();
menusRef.value = await getMenus();
return;
}
// split-top
if (unref(spiltTop)) {
const shallowMenus = await getShallowMenus();
flatMenusRef.value = shallowMenus;
menusRef.value = shallowMenus;
return;
}
}
return { flatMenusRef, menusRef };
}

View File

@ -33,7 +33,7 @@ export default defineComponent({
return tabStore.getTabsState; return tabStore.getTabsState;
}); });
// If you monitor routing changes, tab switching will be stuck. So use this method // If you monitor routing changes, tab switching will be stuck. So setting this method
watch( watch(
() => tabStore.getLastChangeRouteState, () => tabStore.getLastChangeRouteState,
() => { () => {

View File

@ -0,0 +1,77 @@
import './index.less';
import { computed, defineComponent, ref, unref } from 'vue';
import { Layout } from 'ant-design-vue';
import LayoutMenu from '/@/layouts/default/menu/LayoutMenu';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useTrigger, useDragLine, useSiderEvent } from './useLayoutSider';
export default defineComponent({
name: 'LayoutSideBar',
setup() {
const dragBarRef = ref<Nullable<HTMLDivElement>>(null);
const sideRef = ref<Nullable<HTMLDivElement>>(null);
const { getCollapsed, getMenuWidth, getSplit, getTheme } = useMenuSetting();
const { getTriggerAttr, getTriggerSlot } = useTrigger();
const { renderDragLine } = useDragLine(sideRef, dragBarRef);
const {
getCollapsedWidth,
onBreakpointChange,
onCollapseChange,
onSiderClick,
} = useSiderEvent();
const getMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.INLINE : null;
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.LEFT : MenuSplitTyeEnum.NONE;
});
function renderDefault() {
return (
<>
<LayoutMenu
theme={unref(getTheme)}
menuMode={unref(getMode)}
splitType={unref(getSplitType)}
/>
{renderDragLine()}
</>
);
}
return () => {
return (
<Layout.Sider
ref={sideRef}
class="layout-sidebar"
breakpoint="md"
collapsible
width={unref(getMenuWidth)}
collapsed={unref(getCollapsed)}
collapsedWidth={unref(getCollapsedWidth)}
theme={unref(getTheme)}
onClick={onSiderClick}
onCollapse={onCollapseChange}
onBreakpoint={onBreakpointChange}
{...unref(getTriggerAttr)}
>
{{
...unref(getTriggerSlot),
default: () => renderDefault(),
}}
</Layout.Sider>
);
};
},
});

View File

@ -0,0 +1,44 @@
@import (reference) '../../../design/index.less';
.layout-sidebar {
background-size: 100% 100%;
&.ant-layout-sider-dark {
background: @sider-dark-bg-color;
}
&:not(.ant-layout-sider-dark) {
border-right: 1px solid @border-color-light;
}
.ant-layout-sider-zero-width-trigger {
top: 40%;
z-index: 10;
}
&__darg-bar {
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: @primary-color;
box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
}
}
}
.ant-layout-sider-trigger {
height: 36px;
line-height: 36px;
}

View File

@ -0,0 +1,163 @@
import type { Ref } from 'vue';
import { computed, unref, onMounted, nextTick, ref } from 'vue';
import LayoutTrigger from '/@/layouts/default/LayoutTrigger';
import { TriggerEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useDebounce } from '/@/hooks/core/useDebounce';
/**
* Handle related operations of menu events
*/
export function useSiderEvent() {
const initRef = ref(false);
const brokenRef = ref(false);
const collapseRef = ref(true);
const { setMenuSetting, getCollapsed, getMiniWidthNumber, getShow } = useMenuSetting();
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
});
function onCollapseChange(val: boolean) {
if (initRef.value) {
collapseRef.value = val;
setMenuSetting({ collapsed: val });
} else {
!unref(getCollapsed) && setMenuSetting({ collapsed: val });
}
initRef.value = true;
}
function onBreakpointChange(broken: boolean) {
brokenRef.value = broken;
}
function onSiderClick(e: ChangeEvent) {
if (!e || !e.target || e.target.className !== 'basic-menu__content') return;
if (!unref(getCollapsed) || !unref(getShow)) return;
setMenuSetting({ collapsed: false });
}
return { getCollapsedWidth, onCollapseChange, onBreakpointChange, onSiderClick };
}
/**
* Handle related operations of menu folding
*/
export function useTrigger() {
const { getTrigger } = useMenuSetting();
const showTrigger = computed(() => {
const trigger = unref(getTrigger);
return trigger !== TriggerEnum.NONE && trigger === TriggerEnum.FOOTER;
});
const getTriggerAttr = computed(() => {
if (unref(showTrigger)) {
return {};
}
return {
trigger: null,
};
});
const getTriggerSlot = computed(() => {
if (unref(showTrigger)) {
return {
trigger: () => <LayoutTrigger />,
};
}
return {};
});
return { getTriggerAttr, getTriggerSlot };
}
/**
* Handle menu drag and drop related operations
* @param siderRef
* @param dragBarRef
*/
export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
const { getMiniWidthNumber, getCollapsed, setMenuSetting, getHasDrag } = useMenuSetting();
const getDragBarStyle = computed(() => {
if (unref(getCollapsed)) {
return { left: `${unref(getMiniWidthNumber)}px` };
}
return {};
});
onMounted(() => {
nextTick(() => {
const [exec] = useDebounce(changeWrapWidth, 20);
exec();
});
});
function renderDragLine() {
return (
<div
class={[`layout-sidebar__darg-bar`, !unref(getHasDrag) ? 'hide' : '']}
style={unref(getDragBarStyle)}
ref={dragBarRef}
/>
);
}
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 = 600;
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 = unref(siderRef).$el;
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
const width = parseInt(wrap.style.width);
const miniWidth = unref(getMiniWidthNumber);
if (!unref(getCollapsed)) {
width > miniWidth + 20
? setMenuSetting({ menuWidth: width })
: setMenuSetting({ collapsed: true });
} else {
width > miniWidth && setMenuSetting({ collapsed: false, menuWidth: width });
}
ele.releaseCapture?.();
};
}
function changeWrapWidth() {
const ele = unref(dragBarRef) as any;
const side = unref(siderRef);
const wrap = (side || {}).$el;
ele &&
(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 { renderDragLine };
}

View File

@ -5,12 +5,29 @@ import { useRouter } from 'vue-router';
import router from '/@/router'; import router from '/@/router';
import { tabStore } from '/@/store/modules/tab'; import { tabStore } from '/@/store/modules/tab';
import { appStore } from '/@/store/modules/app';
import { unique } from '/@/utils'; import { unique } from '/@/utils';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
export function useFrameKeepAlive() { export function useFrameKeepAlive() {
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
const { getShow } = useMultipleTabSetting();
const getFramePages = computed(() => {
const ret =
getAllFramePages((toRaw(router.getRoutes()) as unknown) as AppRouteRecordRaw[]) || [];
return ret;
});
const getOpenTabList = computed((): string[] => {
return tabStore.getTabsState.reduce((prev: string[], next) => {
if (next.meta && Reflect.has(next.meta, 'frameSrc')) {
prev.push(next.path!);
}
return prev;
}, []);
});
function getAllFramePages(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] { function getAllFramePages(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
let res: AppRouteRecordRaw[] = []; let res: AppRouteRecordRaw[] = [];
@ -30,26 +47,9 @@ export function useFrameKeepAlive() {
function showIframe(item: AppRouteRecordRaw) { function showIframe(item: AppRouteRecordRaw) {
return item.path === unref(currentRoute).path; return item.path === unref(currentRoute).path;
} }
const getFramePages = computed(() => {
const ret =
getAllFramePages((toRaw(router.getRoutes()) as unknown) as AppRouteRecordRaw[]) || [];
return ret;
});
const getOpenTabList = computed((): string[] => {
return tabStore.getTabsState.reduce((prev: string[], next) => {
if (next.meta && Reflect.has(next.meta, 'frameSrc')) {
prev.push(next.path!);
}
return prev;
}, []);
});
function hasRenderFrame(path: string) { function hasRenderFrame(path: string) {
const { return unref(getShow) ? unref(getOpenTabList).includes(path) : true;
multiTabsSetting: { show },
} = appStore.getProjectConfig;
return show ? unref(getOpenTabList).includes(path) : true;
} }
return { hasRenderFrame, getFramePages, showIframe, getAllFramePages }; return { hasRenderFrame, getFramePages, showIframe, getAllFramePages };
} }

View File

@ -1,105 +0,0 @@
<template>
<div class="app-logo anticon" :class="theme" @click="handleGoHome" :style="wrapStyle">
<img src="/@/assets/images/logo.png" />
<div v-if="show" class="logo-title ml-2 ellipsis">{{ globSetting.title }}</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch } from 'vue';
// hooks
import { useGlobSetting } from '/@/settings/use';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { useGo } from '/@/hooks/web/usePage';
import { PageEnum } from '/@/enums/pageEnum';
import { MenuTypeEnum } from '/@/enums/menuEnum';
import { menuStore } from '/@/store/modules/menu';
import { appStore } from '/@/store/modules/app';
export default defineComponent({
name: 'Logo',
props: {
showTitle: {
type: Boolean as PropType<boolean>,
default: true,
},
theme: {
type: String,
},
},
setup(props) {
const showRef = ref<boolean>(!!props.showTitle);
const globSetting = useGlobSetting();
const go = useGo();
function handleGoHome() {
go(PageEnum.BASE_HOME);
}
watch(
() => props.showTitle,
(show: boolean) => {
if (show) {
useTimeoutFn(() => {
showRef.value = show;
}, 280);
} else {
showRef.value = show;
}
}
);
const wrapStyle = computed(() => {
const { getCollapsedState } = menuStore;
const {
menuSetting: { menuWidth, type },
} = appStore.getProjectConfig;
const miniWidth = { minWidth: `${menuWidth}px` };
if (type !== MenuTypeEnum.SIDEBAR) {
return miniWidth;
}
return getCollapsedState ? {} : miniWidth;
});
return {
handleGoHome,
globSetting,
show: showRef,
wrapStyle,
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../design/index.less';
.app-logo {
display: flex;
align-items: center;
padding-left: 16px;
cursor: pointer;
// justify-content: center;
&.light {
border-bottom: 1px solid @border-color-base;
}
.logo-title {
font-size: 18px;
font-weight: 700;
opacity: 0;
transition: all 0.5s;
.respond-to(medium,{
opacity: 1;
});
}
// &.dark .logo-title {
// font-weight: 400;
// }
&.light .logo-title {
color: @primary-color;
}
}
</style>

View File

@ -1,75 +1,78 @@
import { computed, defineComponent, unref, Transition, KeepAlive, toRaw } from 'vue'; import type { FunctionalComponent } from 'vue';
import { computed, defineComponent, unref, Transition, KeepAlive } from 'vue';
import { RouterView, RouteLocation } from 'vue-router'; import { RouterView, RouteLocation } from 'vue-router';
import FrameLayout from '/@/layouts/iframe/index.vue'; import FrameLayout from '/@/layouts/iframe/index.vue';
import { useTransition } from './useTransition'; import { useTransition } from './useTransition';
import { useProjectSetting } from '/@/settings/use'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { tabStore } from '/@/store/modules/tab'; import { tabStore } from '/@/store/modules/tab';
import { appStore } from '/@/store/modules/app';
interface DefaultContext {
Component: FunctionalComponent;
route: RouteLocation;
}
export default defineComponent({ export default defineComponent({
name: 'PageLayout', name: 'PageLayout',
setup() { setup() {
const getProjectConfigRef = computed(() => appStore.getProjectConfig); const { getShow } = useMenuSetting();
const openCacheRef = computed(() => { const {
const { getOpenKeepAlive,
openKeepAlive, getRouterTransition,
multiTabsSetting: { show }, getOpenRouterTransition,
} = unref(getProjectConfigRef); getCanEmbedIFramePage,
return openKeepAlive && show; } = useRootSetting();
});
const getCacheTabsRef = computed(() => toRaw(tabStore.getKeepAliveTabsState) as string[]);
const { openPageLoading } = unref(getProjectConfigRef); const { getMax } = useMultipleTabSetting();
const transitionEvent = useTransition();
const openCacheRef = computed(() => unref(getOpenKeepAlive) && unref(getShow));
const getCacheTabsRef = computed(() => tabStore.getKeepAliveTabsState as string[]);
let on = {};
if (openPageLoading) {
const { on: transitionOn } = useTransition();
on = transitionOn;
}
const projectSetting = useProjectSetting();
return () => { return () => {
const {
routerTransition,
openRouterTransition,
multiTabsSetting: { max },
} = unref(getProjectConfigRef);
return ( return (
<div> <div>
<RouterView> <RouterView>
{{ {{
default: ({ Component, route }: { Component: any; route: RouteLocation }) => { default: ({ Component, route }: DefaultContext) => {
// No longer show animations that are already in the tab // No longer show animations that are already in the tab
const cacheTabs = unref(getCacheTabsRef); const cacheTabs = unref(getCacheTabsRef);
const isInCache = cacheTabs.includes(route.name as string); const isInCache = cacheTabs.includes(route.name as string);
const name = isInCache && route.meta.inTab ? 'fade' : null; const name = isInCache && route.meta.inTab ? 'fade' : null;
const Content = unref(openCacheRef) ? ( const renderComp = () => <Component key={route.fullPath} />;
<KeepAlive max={max} include={cacheTabs}>
<Component key={route.fullPath} /> const PageContent = unref(openCacheRef) ? (
<KeepAlive max={unref(getMax)} include={cacheTabs}>
{renderComp()}
</KeepAlive> </KeepAlive>
) : ( ) : (
<Component key={route.fullPath} /> renderComp()
); );
return openRouterTransition ? (
return unref(getOpenRouterTransition) ? (
<Transition <Transition
{...on} {...transitionEvent}
name={name || route.meta.transitionName || routerTransition} name={name || route.meta.transitionName || unref(getRouterTransition)}
mode="out-in" mode="out-in"
appear={true} appear={true}
> >
{() => Content} {() => PageContent}
</Transition> </Transition>
) : ( ) : (
Content PageContent
); );
}, },
}} }}
</RouterView> </RouterView>
{projectSetting.canEmbedIFramePage && <FrameLayout />} {unref(getCanEmbedIFramePage) && <FrameLayout />}
</div> </div>
); );
}; };

View File

@ -1,10 +1,11 @@
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper'; import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
export function useTransition() { export function useTransition() {
function handleAfterEnter() { function handleAfterEnter() {
const { openRouterTransition, openPageLoading } = appStore.getProjectConfig; const { getOpenPageLoading, getOpenRouterTransition } = useRootSetting();
if (!getOpenPageLoading.value || !getOpenRouterTransition.value) return;
if (!openRouterTransition || !openPageLoading) return;
// Close loading after the route switching animation ends // Close loading after the route switching animation ends
appStore.setPageLoadingAction(false); appStore.setPageLoadingAction(false);
} }
@ -15,9 +16,6 @@ export function useTransition() {
}); });
return { return {
handleAfterEnter, onAfterEnter: handleAfterEnter,
on: {
onAfterEnter: handleAfterEnter,
},
}; };
} }

View File

@ -49,7 +49,7 @@ if (isDevMode()) {
window.__APP__ = app; window.__APP__ = app;
} }
// If you do not need to use the mock service in the production environment, you can comment the code // If you do not need to setting the mock service in the production environment, you can comment the code
if (isProdMode() && isUseMock()) { if (isProdMode() && isUseMock()) {
setupProdMockServer(); setupProdMockServer();
} }

View File

@ -6,7 +6,7 @@ import { createProgressGuard } from './progressGuard';
import { createPermissionGuard } from './permissionGuard'; import { createPermissionGuard } from './permissionGuard';
import { createPageLoadingGuard } from './pageLoadingGuard'; import { createPageLoadingGuard } from './pageLoadingGuard';
import { useGlobSetting, useProjectSetting } from '/@/settings/use'; import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
import { getIsOpenTab, setCurrentTo } from '/@/utils/helper/routeHelper'; import { getIsOpenTab, setCurrentTo } from '/@/utils/helper/routeHelper';
import { setTitle } from '/@/utils/browser'; import { setTitle } from '/@/utils/browser';

View File

@ -7,6 +7,7 @@ import { filter } from '/@/utils/helper/treeHelper';
import router from '/@/router'; import router from '/@/router';
import { PermissionModeEnum } from '/@/enums/appEnum'; import { PermissionModeEnum } from '/@/enums/appEnum';
import { pathToRegexp } from 'path-to-regexp'; import { pathToRegexp } from 'path-to-regexp';
import modules from 'globby!/@/router/menus/modules/**/*.@(ts)'; import modules from 'globby!/@/router/menus/modules/**/*.@(ts)';
const menuModules: MenuModule[] = []; const menuModules: MenuModule[] = [];
@ -44,7 +45,6 @@ async function getAsyncMenus() {
// 获取深层扁平化菜单 // 获取深层扁平化菜单
export const getFlatMenus = async () => { export const getFlatMenus = async () => {
const menus = await getAsyncMenus(); const menus = await getAsyncMenus();
return flatMenus(menus); return flatMenus(menus);
}; };

View File

@ -9,7 +9,7 @@ import { isProdMode } from '/@/utils/env';
const setting: ProjectConfig = { const setting: ProjectConfig = {
// locale setting // locale setting
locale: { locale: {
// Locales // Locale
lang: 'zh_CN', lang: 'zh_CN',
// Default locale // Default locale
fallback: 'zh_CN', fallback: 'zh_CN',
@ -29,17 +29,22 @@ const setting: ProjectConfig = {
// Whether to show the configuration button // Whether to show the configuration button
showSettingButton: true, showSettingButton: true,
// 权限模式 // 权限模式
permissionMode: PermissionModeEnum.ROLE, permissionMode: PermissionModeEnum.ROLE,
// 网站灰色模式,用于可能悼念的日期开启 // 网站灰色模式,用于可能悼念的日期开启
grayMode: false, grayMode: false,
// 色弱模式 // 色弱模式
colorWeak: false, colorWeak: false,
// 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内 // 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内
fullContent: false, fullContent: false,
// content mode // content mode
contentMode: ContentEnum.FULL, contentMode: ContentEnum.FULL,
// 是否显示logo // 是否显示logo
showLogo: true, showLogo: true,
@ -58,11 +63,10 @@ const setting: ProjectConfig = {
showFullScreen: true, showFullScreen: true,
// 显示文档按钮 // 显示文档按钮
showDoc: true, showDoc: true,
// 是否显示github
showGithub: true,
// 显示消息中心按钮 // 显示消息中心按钮
showNotice: true, showNotice: true,
}, },
// 菜单配置 // 菜单配置
menuSetting: { menuSetting: {
// 菜单折叠 // 菜单折叠
@ -108,13 +112,16 @@ const setting: ProjectConfig = {
// 标签页缓存最大数量 // 标签页缓存最大数量
max: 12, max: 12,
}, },
// 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存 // 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存
openKeepAlive: true, openKeepAlive: true,
// 自动锁屏时间为0不锁屏。 单位分钟 默认0 // 自动锁屏时间为0不锁屏。 单位分钟 默认0
lockTime: 0, lockTime: 0,
// 显示面包屑 // 显示面包屑
showBreadCrumb: true, showBreadCrumb: true,
// 显示面包屑图标 // 显示面包屑图标
showBreadCrumbIcon: false, showBreadCrumbIcon: false,

View File

@ -3,7 +3,7 @@
*/ */
import { errorStore, ErrorInfo } from '/@/store/modules/error'; import { errorStore, ErrorInfo } from '/@/store/modules/error';
import { useProjectSetting } from '/@/settings/use'; import { useProjectSetting } from '/@/hooks/setting';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum'; import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { App } from 'vue'; import { App } from 'vue';

View File

@ -4,7 +4,7 @@ import type { I18n, I18nOptions } from 'vue-i18n';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
import localeMessages from '/@/locales'; import localeMessages from '/@/locales';
import { useLocale } from '/@/hooks/web/useLocale'; import { useLocale } from '/@/hooks/web/useLocale';
import { useLocaleSetting } from '/@/settings/use/useLocaleSetting'; import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting';
const { setupLocale } = useLocale(); const { setupLocale } = useLocale();

View File

@ -4,7 +4,7 @@ import { VuexModule, getModule, Module, Mutation, Action } from 'vuex-module-dec
import { formatToDateTime } from '/@/utils/dateUtil'; import { formatToDateTime } from '/@/utils/dateUtil';
import { ErrorTypeEnum } from '/@/enums/exceptionEnum'; import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { useProjectSetting } from '/@/settings/use'; import { useProjectSetting } from '/@/hooks/setting';
export interface ErrorInfo { export interface ErrorInfo {
type: ErrorTypeEnum; type: ErrorTypeEnum;

View File

@ -1,67 +0,0 @@
import store from '/@/store';
import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
import { VuexModule, Module, getModule, Mutation } from 'vuex-module-decorators';
import { appStore } from '/@/store/modules/app';
const NAME = 'menu';
hotModuleUnregisterModule(NAME);
@Module({ namespaced: true, name: NAME, dynamic: true, store })
class Menu extends VuexModule {
// 是否开始拖拽
private dragStartState = false;
private currentTopSplitMenuPathState = '';
/**
* @description:
*/
get getCollapsedState() {
return appStore.getProjectConfig.menuSetting.collapsed;
}
get getCurrentTopSplitMenuPathState() {
return this.currentTopSplitMenuPathState;
}
get getDragStartState() {
return this.dragStartState;
}
get getMenuWidthState() {
return appStore.getProjectConfig.menuSetting.menuWidth;
}
@Mutation
commitDragStartState(dragStart: boolean): void {
this.dragStartState = dragStart;
}
@Mutation
commitCurrentTopSplitMenuPathState(path: string): void {
this.currentTopSplitMenuPathState = path;
}
// 改变菜单展开状态
@Mutation
commitCollapsedState(collapsed: boolean): void {
// this.collapsedState = collapsed;
appStore.commitProjectConfigState({
menuSetting: {
collapsed: collapsed,
},
});
}
@Mutation
commitMenuWidthState(menuWidth: number): void {
// this.menuWidthState = menuWidth;
appStore.commitProjectConfigState({
menuSetting: {
menuWidth: menuWidth,
},
});
}
}
export const menuStore = getModule<Menu>(Menu);

View File

@ -97,11 +97,6 @@ class Permission extends VuexModule {
if (!roles) return true; if (!roles) return true;
return roleList.some((role) => roles.includes(role)); return roleList.some((role) => roles.includes(role));
}); });
// this.commitRoutesState(routes);
// Background permissions
// warn(
// `当前权限模式为:${PermissionModeEnum.ROLE},请将src/store/modules/permission.ts内的后台菜单获取函数注释,如果已注释可以忽略此信息!`
// );
// 如果确定不需要做后台动态权限,请将下面整个判断注释 // 如果确定不需要做后台动态权限,请将下面整个判断注释
} else if (permissionMode === PermissionModeEnum.BACK) { } else if (permissionMode === PermissionModeEnum.BACK) {
const messageKey = 'loadMenu'; const messageKey = 'loadMenu';

View File

@ -44,7 +44,6 @@ export interface HeaderSetting {
useLockPage: boolean; useLockPage: boolean;
// 显示文档按钮 // 显示文档按钮
showDoc: boolean; showDoc: boolean;
showGithub: boolean;
// 显示消息中心按钮 // 显示消息中心按钮
showNotice: boolean; showNotice: boolean;
} }

View File

@ -1,3 +1,4 @@
import { openWindow } from '..';
import { dataURLtoBlob, urlToBase64 } from './base64Conver'; import { dataURLtoBlob, urlToBase64 } from './base64Conver';
/** /**
@ -93,6 +94,6 @@ export function downloadByUrl({
url += '?download'; url += '?download';
} }
window.open(url, target); openWindow(url, { target });
return true; return true;
} }

View File

@ -1,5 +1,5 @@
import { getEnv } from '/@/utils/env'; import { getEnv } from '/@/utils/env';
import { useGlobSetting } from '/@/settings/use'; import { useGlobSetting } from '/@/hooks/setting';
import pkg from '../../../package.json'; import pkg from '../../../package.json';
const globSetting = useGlobSetting(); const globSetting = useGlobSetting();

View File

@ -10,7 +10,7 @@ import { AxiosTransform } from './axiosTransform';
import { checkStatus } from './checkStatus'; import { checkStatus } from './checkStatus';
import { useGlobSetting } from '/@/settings/use'; import { useGlobSetting } from '/@/hooks/setting';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum'; import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum';

View File

@ -11,6 +11,7 @@ export function getPopupContainer(node?: HTMLElement): HTMLElement {
} }
return document.body; return document.body;
} }
/** /**
* Add the object as a parameter to the URL * Add the object as a parameter to the URL
* @param baseUrl url * @param baseUrl url
@ -64,3 +65,16 @@ export function unique<T = any>(arr: T[], key: string): T[] {
export function es6Unique<T>(arr: T[]): T[] { export function es6Unique<T>(arr: T[]): T[] {
return Array.from(new Set(arr)); return Array.from(new Set(arr));
} }
export function openWindow(
url: string,
opt?: { target?: TargetContext | string; noopener?: boolean; noreferrer?: boolean }
) {
const { target = '__blank', noopener = true, noreferrer = true } = opt || {};
const feature: string[] = [];
noopener && feature.push('noopener=yes');
noreferrer && feature.push('noreferrer=yes');
window.open(url, target, feature.join(','));
}

View File

@ -45,6 +45,8 @@
import Icon from '/@/components/Icon/index'; import Icon from '/@/components/Icon/index';
import { openWindow } from '/@/utils';
export default defineComponent({ export default defineComponent({
components: { components: {
CollapseContainer, CollapseContainer,
@ -61,7 +63,7 @@
setup() { setup() {
return { return {
toIconify: () => { toIconify: () => {
window.open('https://iconify.design/', '__blank'); openWindow('https://iconify.design/');
}, },
}; };
}, },

View File

@ -16,23 +16,23 @@
</a-card> </a-card>
</div> </div>
<AppFooterToolbar> <AppPageFooter>
<template #right> <template #right>
<a-button type="primary" @click="submitAll">提交</a-button> <a-button type="primary" @click="submitAll">提交</a-button>
</template> </template>
</AppFooterToolbar> </AppPageFooter>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { BasicForm, useForm } from '/@/components/Form'; import { BasicForm, useForm } from '/@/components/Form';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import PersonTable from './PersonTable.vue'; import PersonTable from './PersonTable.vue';
import { AppFooterToolbar } from '/@/components/Application'; import { AppPageFooter } from '/@/components/Application';
import { schemas, taskSchemas } from './data'; import { schemas, taskSchemas } from './data';
export default defineComponent({ export default defineComponent({
components: { BasicForm, PersonTable, AppFooterToolbar }, components: { BasicForm, PersonTable, AppPageFooter },
setup() { setup() {
const tableRef = ref<{ getDataSource: () => any } | null>(null); const tableRef = ref<{ getDataSource: () => any } | null>(null);

View File

@ -72,7 +72,7 @@
// import { appStore } from '/@/store/modules/app'; // import { appStore } from '/@/store/modules/app';
import { useMessage } from '/@/hooks/web/useMessage'; import { useMessage } from '/@/hooks/web/useMessage';
import { useGlobSetting } from '/@/settings/use'; import { useGlobSetting } from '/@/hooks/setting';
import logo from '/@/assets/images/logo.png'; import logo from '/@/assets/images/logo.png';
export default defineComponent({ export default defineComponent({

View File

@ -30,7 +30,7 @@ function pathResolve(dir: string) {
const viteConfig: UserConfig = { const viteConfig: UserConfig = {
/** /**
* Entry. Use this to specify a js entry file in use cases where an * Entry. Use this to specify a js entry file in setting cases where an
* `index.html` does not exist (e.g. serving vite assets from a different host) * `index.html` does not exist (e.g. serving vite assets from a different host)
* @default 'index.html' * @default 'index.html'
*/ */
@ -51,7 +51,7 @@ const viteConfig: UserConfig = {
*/ */
open: false, open: false,
/** /**
* Set to `false` to disable minification, or specify the minifier to use. * Set to `false` to disable minification, or specify the minifier to setting.
* Available options are 'terser' or 'esbuild'. * Available options are 'terser' or 'esbuild'.
* @default 'terser' * @default 'terser'
*/ */
@ -112,7 +112,7 @@ const viteConfig: UserConfig = {
}, },
define: { define: {
__VERSION__: pkg.version, __VERSION__: pkg.version,
// use vue-i18-next // setting vue-i18-next
// Suppress warning // Suppress warning
__VUE_I18N_LEGACY_API__: false, __VUE_I18N_LEGACY_API__: false,
__VUE_I18N_FULL_INSTALL__: false, __VUE_I18N_FULL_INSTALL__: false,