提交 dddda5b2 编写于 作者: V vben

feat: add search page

上级 596e7062
## Wip
### ✨ Features
- 移除左侧菜单搜索,新增顶部菜单搜索功能
## 2.0.0-rc.13 (2020-12-10)
## (破坏性更新) Breaking changes
......
import AppLocalePicker from './src/AppLocalePicker.vue';
import AppLogo from './src/AppLogo.vue';
import AppProvider from './src/AppProvider.vue';
import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
withInstall(AppLocalePicker, AppLogo, AppProvider);
export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue'));
export const AppProvider = createAsyncComponent(() => import('./src/AppProvider.vue'));
export const AppSearch = createAsyncComponent(() => import('./src/search/AppSearch.vue'));
export const AppLogo = createAsyncComponent(() => import('./src/AppLogo.vue'));
export { useAppProviderContext } from './src/useAppContext';
withInstall(AppLocalePicker, AppLogo, AppProvider, AppSearch);
export { AppLocalePicker, AppLogo, AppProvider };
export { useAppProviderContext } from './src/useAppContext';
<template>
<div :class="prefixCls" v-if="getShowSearch" @click="handleSearch">
<Tooltip>
<template #title> {{ t('component.app.search') }} </template>
<SearchOutlined />
</Tooltip>
<transition name="zoom-fade" mode="out-in">
<AppSearchModal @close="handleClose" v-if="showModal" />
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import AppSearchModal from './AppSearchModal.vue';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { SearchOutlined } from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'AppSearch',
components: { AppSearchModal, Tooltip, SearchOutlined },
setup() {
const showModal = ref(false);
const { prefixCls } = useDesign('app-search');
const { getShowSearch } = useHeaderSetting();
const { t } = useI18n();
function handleSearch() {
showModal.value = true;
}
return {
t,
prefixCls,
showModal,
getShowSearch,
handleClose: () => {
showModal.value = false;
},
handleSearch,
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../../design/index.less';
@prefix-cls: ~'@{namespace}-app-search';
.@{prefix-cls} {
padding: 0 10px;
}
</style>
<template>
<div :class="`${prefixCls}`">
<span :class="`${prefixCls}__item`">
<g-icon icon="ant-design:enter-outlined" />
</span>
<span>{{ t('component.app.toSearch') }}</span>
<span :class="`${prefixCls}__item`">
<g-icon icon="bi:arrow-up" />
</span>
<span :class="`${prefixCls}__item`">
<g-icon icon="bi:arrow-down" />
</span>
<span>{{ t('component.app.toNavigate') }}</span>
<span :class="`${prefixCls}__item`">
<g-icon icon="mdi:keyboard-esc" />
</span>
<span>{{ t('component.app.toClose') }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'AppSearchFooter',
components: {},
setup() {
const { prefixCls } = useDesign('app-search-footer');
const { t } = useI18n();
return {
prefixCls,
t,
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../../design/index.less';
@prefix-cls: ~'@{namespace}-app-search-footer';
.@{prefix-cls} {
position: relative;
display: flex;
height: 44px;
padding: 0 16px;
font-size: 12px;
color: #666;
background: rgb(255 255 255);
border-radius: 0 0 8px 8px;
box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgba(69, 98, 155, 0.12);
align-items: center;
flex-shrink: 0;
&__item {
display: flex;
width: 20px;
height: 18px;
padding-bottom: 2px;
margin-right: 0.4em;
background: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
border-radius: 2px;
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
align-items: center;
justify-content: center;
&:nth-child(2),
&:nth-child(3),
&:nth-child(6) {
margin-left: 14px;
}
}
}
</style>
<template>
<div :class="prefixCls" @click.stop>
<ClickOutSide @clickOutside="handleClose">
<div :class="`${prefixCls}-content`">
<a-input
:class="`${prefixCls}-input`"
:placeholder="t('component.app.search')"
allow-clear
@change="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
{{ t('component.app.searchNotData') }}
</div>
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
<li
:ref="setRefs(index)"
v-for="(item, index) in searchResult"
:key="item.path"
:data-index="index"
@mouseenter="handleMouseenter"
@click="handleEnter"
:class="[
`${prefixCls}-list__item`,
{
[`${prefixCls}-list__item--active`]: activeIndex === index,
},
]"
>
<div :class="`${prefixCls}-list__item-icon`">
<g-icon :icon="item.icon || 'mdi:form-select'" :size="20" />
</div>
<div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div>
<div :class="`${prefixCls}-list__item-enter`">
<g-icon icon="ant-design:enter-outlined" :size="20" />
</div>
</li>
</ul>
<AppSearchFooter />
</div>
</ClickOutSide>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, unref, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRefs } from '/@/hooks/core/useRefs';
import { useMenuSearch } from './useMenuSearch';
import { SearchOutlined } from '@ant-design/icons-vue';
import AppSearchFooter from './AppSearchFooter.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { ClickOutSide } from '/@/components/ClickOutSide';
export default defineComponent({
name: 'AppSearchModal',
components: { SearchOutlined, ClickOutSide, AppSearchFooter },
emits: ['close'],
setup(_, { emit }) {
const scrollWrap = ref<ElRef>(null);
const { prefixCls } = useDesign('app-search-modal');
const { t } = useI18n();
const [refs, setRefs] = useRefs();
const {
handleSearch,
searchResult,
keyword,
activeIndex,
handleEnter,
handleMouseenter,
} = useMenuSearch(refs, scrollWrap, emit);
const getIsNotData = computed(() => {
return !keyword || unref(searchResult).length === 0;
});
return {
t,
prefixCls,
handleSearch,
searchResult,
activeIndex,
getIsNotData,
handleEnter,
setRefs,
scrollWrap,
handleMouseenter,
handleClose: () => {
emit('close');
},
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../../design/index.less';
@prefix-cls: ~'@{namespace}-app-search-modal';
.@{prefix-cls} {
position: fixed;
top: 0;
left: 0;
z-index: 100;
display: flex;
width: 100%;
height: 100%;
padding-top: 50px;
// background: #656c85cc;
background: rgba(0, 0, 0, 0.8);
justify-content: center;
// backdrop-filter: blur(2px);
&-content {
position: relative;
width: 532px;
// padding: 14px;
margin: 0 auto auto auto;
background: #f5f6f7;
border-radius: 6px;
box-shadow: inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5), 0 3px 8px 0 #555a64;
flex-direction: column;
}
&-input {
width: calc(100% - 28px);
height: 56px;
margin: 14px 14px 0 14px;
font-size: 1.5em;
color: #1c1e21;
span[role='img'] {
color: #999;
}
}
&-not-data {
display: flex;
width: 100%;
height: 100px;
font-size: 0.9;
color: rgb(150 159 175);
align-items: center;
justify-content: center;
}
&-list {
max-height: 472px;
padding: 0 14px;
padding-bottom: 20px;
margin: 0 auto;
margin-top: 14px;
overflow: auto;
&__item {
position: relative;
display: flex;
width: 100%;
height: 56px;
padding-bottom: 4px;
padding-left: 14px;
margin-top: 8px;
font-size: 14px;
color: @text-color-base;
cursor: pointer;
// background: @primary-color;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px 0 #d4d9e1;
align-items: center;
&--active {
color: #fff;
background: @primary-color;
.@{prefix-cls}-list__item-enter {
opacity: 1;
}
}
&-icon {
width: 30px;
}
&-text {
flex: 1;
}
&-enter {
width: 30px;
opacity: 0;
}
}
}
}
</style>
import { cloneDeep } from 'lodash-es';
import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { getMenus } from '/@/router/menus';
import type { Menu } from '/@/router/types';
import { filter, forEach } from '/@/utils/helper/treeHelper';
import { useDebounce } from '/@/hooks/core/useDebounce';
import { useGo } from '/@/hooks/web/usePage';
import { useScrollTo } from '/@/hooks/event/useScrollTo';
export interface SearchResult {
name: string;
path: string;
icon?: string;
}
const enum KeyCodeEnum {
UP = 38,
DOWN = 40,
ENTER = 13,
ESC = 27,
}
// Translate special characters
function transform(c: string) {
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'];
return code.includes(c) ? `\\${c}` : c;
}
function createSearchReg(key: string) {
const keys = [...key].map((item) => transform(item));
const str = ['', ...keys, ''].join('.*');
return new RegExp(str);
}
export function useMenuSearch(refs: Ref<HTMLElement[]>, scrollWrap: Ref<ElRef>, emit: EmitType) {
const searchResult = ref<SearchResult[]>([]);
const keyword = ref('');
const activeIndex = ref(-1);
let menuList: Menu[] = [];
const { t } = useI18n();
const go = useGo();
const [handleSearch] = useDebounce(search, 200);
onBeforeMount(async () => {
const list = await getMenus();
menuList = cloneDeep(list);
forEach(menuList, (item) => {
item.name = t(item.name);
});
document.addEventListener('keydown', registerKeyDown);
});
onBeforeUnmount(() => {
document.removeEventListener('keydown', registerKeyDown);
});
function search(e: ChangeEvent) {
e?.stopPropagation();
const key = e.target.value;
keyword.value = key.trim();
if (!key) {
searchResult.value = [];
return;
}
const reg = createSearchReg(unref(keyword));
const filterMenu = filter(menuList, (item) => {
return reg.test(item.name);
});
searchResult.value = handlerSearchResult(filterMenu, reg);
activeIndex.value = 0;
}
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
const ret: SearchResult[] = [];
filterMenu.forEach((item) => {
const { name, path, icon, children } = item;
if (reg.test(name) && !children?.length) {
ret.push({
name: parent?.name ? `${parent.name} > ${name}` : name,
path,
icon,
});
}
if (Array.isArray(children) && children.length) {
ret.push(...handlerSearchResult(children, reg, item));
}
});
return ret;
}
function handleMouseenter(e: ChangeEvent) {
const index = e.target.dataset.index;
activeIndex.value = Number(index);
}
function handleUp() {
if (!searchResult.value.length) return;
activeIndex.value--;
if (activeIndex.value < 0) {
activeIndex.value = searchResult.value.length - 1;
}
handleScroll();
}
function handleDown() {
if (!searchResult.value.length) return;
activeIndex.value++;
if (activeIndex.value > searchResult.value.length - 1) {
activeIndex.value = 0;
}
handleScroll();
}
function handleScroll() {
const refList = unref(refs);
if (!refList || !Array.isArray(refList) || refList.length === 0 || !unref(scrollWrap)) return;
const index = unref(activeIndex);
const currentRef = refList[index];
if (!currentRef) return;
const wrapEl = unref(scrollWrap);
if (!wrapEl) return;
const scrollHeight = currentRef.offsetTop + currentRef.offsetHeight;
const wrapHeight = wrapEl.offsetHeight;
const { start } = useScrollTo({
el: wrapEl,
duration: 100,
to: scrollHeight - wrapHeight,
});
start();
}
function handleEnter() {
if (!searchResult.value.length) return;
const result = unref(searchResult);
const index = unref(activeIndex);
if (result.length === 0 || index < 0) {
return;
}
const to = result[index];
handleClose();
go(to.path);
}
function handleClose() {
emit('close');
}
function registerKeyDown(e: KeyboardEvent) {
const keyCode = window.event ? e.keyCode : e.which;
switch (keyCode) {
case KeyCodeEnum.UP:
handleUp();
break;
case KeyCodeEnum.DOWN:
handleDown();
break;
case KeyCodeEnum.ENTER:
handleEnter();
break;
case KeyCodeEnum.ESC:
handleClose();
break;
}
}
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
}
import Authority from './src/index.vue';
import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
withInstall(Authority);
export const Authority = createAsyncComponent(() => import('./src/index.vue'));
export { Authority };
withInstall(Authority);
import BasicArrow from './src/BasicArrow.vue';
import BasicHelp from './src/BasicHelp.vue';
import BasicTitle from './src/BasicTitle.vue';
import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
withInstall(BasicArrow, BasicHelp, BasicTitle);
export const BasicArrow = createAsyncComponent(() => import('./src/BasicArrow.vue'));
export const BasicHelp = createAsyncComponent(() => import('./src/BasicHelp.vue'));
export const BasicTitle = createAsyncComponent(() => import('./src/BasicTitle.vue'));
export { BasicArrow, BasicHelp, BasicTitle };
withInstall(BasicArrow, BasicHelp, BasicTitle);
......@@ -31,18 +31,33 @@
// Speed: 1x
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.2s, transform 0.25s;
transition: opacity 0.25s, transform 0.3s;
}
.fade-bottom-enter-from,
.fade-bottom-enter {
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-8%);
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(8%);
transform: translateY(10%);
}
// fade-scale
.fade-scale-leave-active,
.fade-scale-enter-active {
transition: all 0.28s;
}
.fade-scale-enter-from {
opacity: 0;
transform: scale(1.2);
}
.fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
// ///////////////////////////////////////////////
......
......@@ -13,15 +13,15 @@
// zoom-fade
.zoom-fade-enter-active,
.zoom-fade-leave-active {
transition: transform 0.15s, opacity 0.2s ease-out;
transition: transform 0.2s, opacity 0.3s ease-out;
}
.zoom-fade-enter-from {
opacity: 0;
transform: scale(0.97);
transform: scale(0.92);
}
.zoom-fade-leave-to {
opacity: 0;
transform: scale(1.03);
transform: scale(1.06);
}
......@@ -39,4 +39,5 @@ export enum RouterTransitionEnum {
FADE_SIDE = 'fade-slide',
FADE = 'fade',
FADE_BOTTOM = 'fade-bottom',
FADE_SCALE = 'fade-scale',
}
import { ref, onBeforeUpdate } from 'vue';
import { ref, onBeforeUpdate, Ref } from 'vue';
export function useRefs() {
const refs = ref([] as Element[]);
export function useRefs(): [Ref<HTMLElement[]>, (index: number) => (el: HTMLElement) => void] {
const refs = ref<HTMLElement[]>([]);
onBeforeUpdate(() => {
refs.value = [];
});
const setRefs = (index: number) => (el: Element) => {
const setRefs = (index: number) => (el: HTMLElement) => {
refs.value[index] = el;
};
......
......@@ -51,6 +51,8 @@ const getFixed = computed(() => unref(getHeaderSetting).fixed);
const getHeaderBgColor = computed(() => unref(getHeaderSetting).bgColor);
const getShowSearch = computed(() => unref(getHeaderSetting).showSearch);
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
......@@ -87,6 +89,7 @@ export function useHeaderSetting() {
getHeaderSetting,
getShowDoc,
getShowSearch,
getHeaderTheme,
getShowRedo,
getUseLockPage,
......
@import (reference) '../../../design/index.less';
.layout-content {
position: relative;
flex: 1 1 auto;
min-height: 0;
&.fixed {
width: 1200px;
margin: 0 auto;
}
&__loading {
position: absolute;
top: 200px;
z-index: @page-loading-z-index;
}
}
import './index.less';
import { defineComponent, unref } from 'vue';
import { Loading } from '/@/components/Loading';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import PageLayout from '/@/layouts/page/index';
export default defineComponent({
name: 'LayoutContent',
setup() {
const { getOpenPageLoading } = useTransitionSetting();
const { getLayoutContentMode, getPageLoading } = useRootSetting();
return () => {
return (
<div class={['layout-content', unref(getLayoutContentMode)]}>
{unref(getOpenPageLoading) && (
<Loading
loading={unref(getPageLoading)}
background="rgba(240, 242, 245, 0.6)"
absolute
class="layout-content__loading"
/>
)}
<PageLayout />
</div>
);
};
},
});
<template>
<div :class="[prefixCls, getLayoutContentMode]">
<transition name="fade">
<Loading
v-if="getOpenPageLoading"
:loading="getPageLoading"
background="rgba(240, 242, 245, 0.6)"
absolute
:class="`${prefixCls}__loading`"
/>
</transition>
<PageLayout />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import PageLayout from '/@/layouts/page/index';
import { Loading } from '/@/components/Loading';
import Transition from '/@/views/demo/comp/lazy/Transition.vue';
export default defineComponent({
name: 'LayoutContent',
components: { PageLayout, Loading, Transition },
setup() {
const { prefixCls } = useDesign('layout-content');
const { getOpenPageLoading } = useTransitionSetting();
const { getLayoutContentMode, getPageLoading } = useRootSetting();
return {
prefixCls,
getOpenPageLoading,
getLayoutContentMode,
getPageLoading,
};
},
});
</script>
<style lang="less">
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-content';
.@{prefix-cls} {
position: relative;
flex: 1 1 auto;
min-height: 0;
&.fixed {
width: 1200px;
margin: 0 auto;
}
&__loading {
position: absolute;
top: 200px;
z-index: @page-loading-z-index;
}
}
</style>
......@@ -18,7 +18,7 @@ import { AppLogo } from '/@/components/Application';
import UserDropdown from './UserDropdown';
import LayoutMenu from '../menu';
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
import LockAction from '../lock/LockAction';
import LockAction from './actions/LockAction';
import LayoutTrigger from '../LayoutTrigger';
import NoticeAction from './notice/NoticeActionItem.vue';
import {
......@@ -28,6 +28,8 @@ import {
LockOutlined,
BugOutlined,
} from '@ant-design/icons-vue';
import { AppSearch } from '/@/components/Application';
import { useModal } from '/@/components/Modal';
import { useFullscreen } from '/@/hooks/web/useFullScreen';
......@@ -200,6 +202,8 @@ export default defineComponent({
function renderAction() {
return (
<div class={`layout-header__action`}>
{unref(isPc) && <AppSearch class="layout-header__action-item" />}
{unref(getUseErrorHandle) && unref(isPc) && (
<TooltipItem title={t('layout.header.tooltipErrorLog')}>
{() => (
......
.multiple-tab-header {
flex: 0 0 auto;
transition: width 0.2s;
&.dark {
margin-left: -1px;
......
......@@ -131,7 +131,8 @@
}
}
&-icon {
&-icon,
span[role='img'] {
color: @text-color-base;
}
}
......
......@@ -4,9 +4,9 @@ import { defineComponent, unref, computed, ref } from 'vue';
import { Layout, BackTop } from 'ant-design-vue';
import LayoutHeader from './header/LayoutHeader';
import LayoutContent from './content';
import LayoutContent from './content/index.vue';
import LayoutFooter from './footer';
import LayoutLockPage from './lock/index.vue';
import LayoutLockPage from '/@/views/sys/lock/index.vue';
import LayoutSideBar from './sider';
import SettingBtn from './setting/index.vue';
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
......
<template>
<transition name="fade-bottom">
<LockPage v-if="getIsLock" />
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import LockPage from '/@/views/sys/lock/index.vue';
import { getIsLock } from '/@/hooks/web/useLockPage';
export default defineComponent({
name: 'LayoutLockPage',
components: { LockPage },
setup() {
return { getIsLock };
},
});
</script>
......@@ -204,7 +204,6 @@ export default defineComponent({
getCollapsedShowTitle,
getMenuFixed,
getCollapsed,
getShowSearch,
getCanDrag,
getTopMenuAlign,
getAccordion,
......@@ -214,7 +213,12 @@ export default defineComponent({
getSplit,
} = useMenuSetting();
const { getShowHeader, getFixed: getHeaderFixed, getHeaderBgColor } = useHeaderSetting();
const {
getShowHeader,
getFixed: getHeaderFixed,
getHeaderBgColor,
getShowSearch,
} = useHeaderSetting();
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting();
......@@ -274,10 +278,10 @@ export default defineComponent({
}),
renderSwitchItem(t('layout.setting.menuSearch'), {
handler: (e) => {
baseHandler(HandlerEnum.MENU_SHOW_SEARCH, e);
baseHandler(HandlerEnum.HEADER_SEARCH, e);
},
def: unref(getShowSearch),
disabled: !unref(getShowMenuRef),
disabled: !unref(getShowHeader),
}),
renderSwitchItem(t('layout.setting.menuAccordion'), {
handler: (e) => {
......
......@@ -28,6 +28,8 @@ export enum HandlerEnum {
HEADER_THEME,
HEADER_FIXED,
HEADER_SEARCH,
TABS_SHOW_QUICK,
TABS_SHOW,
......@@ -94,6 +96,7 @@ export const routerTransitionOptions = [
RouterTransitionEnum.ZOOM_OUT,
RouterTransitionEnum.FADE_SIDE,
RouterTransitionEnum.FADE_BOTTOM,
RouterTransitionEnum.FADE_SCALE,
].map((item) => {
return {
label: item,
......
......@@ -119,6 +119,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
updateHeaderBgColor(value);
return { headerSetting: { bgColor: value } };
case HandlerEnum.HEADER_SEARCH:
return { headerSetting: { showSearch: value } };
case HandlerEnum.HEADER_FIXED:
return { headerSetting: { fixed: value } };
......
......@@ -100,7 +100,7 @@ export default defineComponent({
flex: `0 0 ${width}`,
maxWidth: width,
minWidth: width,
transition: 'all 0.15s',
transition: 'all 0.2s',
};
}
);
......
......@@ -30,7 +30,7 @@ export default defineComponent({
return () => {
return (
<>
<div>
<RouterView>
{{
default: ({ Component, route }: DefaultContext) => {
......@@ -65,7 +65,7 @@ export default defineComponent({
}}
</RouterView>
{unref(getCanEmbedIFramePage) && <FrameLayout />}
</>
</div>
);
};
},
......
export default {
search: 'Search',
searchNotData: 'No search results yet',
toSearch: 'to search',
toNavigate: 'to navigate',
toClose: 'to close',
};
......@@ -3,6 +3,7 @@ export default {
dropdownItemDoc: 'Document',
dropdownItemLoginOut: 'Login Out',
search: 'Search',
tooltipErrorLog: 'Error log',
tooltipLock: 'Lock screen',
tooltipNotify: 'Notification',
......
......@@ -40,7 +40,7 @@ export default {
sidebarTheme: 'Menu theme',
menuDrag: 'Drag Sidebar',
menuSearch: 'Sidebar search',
menuSearch: 'Menu search',
menuAccordion: 'Sidebar accordion',
menuCollapse: 'Collapse menu',
collapseMenuDisplayName: 'Collapse menu display name',
......
export default {
search: '搜索',
searchNotData: '暂无搜索结果',
toSearch: '确认',
toNavigate: '切换',
toClose: '关闭',
};
......@@ -4,6 +4,7 @@ export default {
dropdownItemLoginOut: '退出系统',
// tooltip
search: '搜索',
tooltipErrorLog: '错误日志',
tooltipLock: '锁定屏幕',
tooltipNotify: '消息通知',
......
......@@ -39,7 +39,7 @@ export default {
sidebarTheme: '菜单主题',
menuDrag: '侧边菜单拖拽',
menuSearch: '侧边菜单搜索',
menuSearch: '菜单搜索',
menuAccordion: '侧边菜单手风琴模式',
menuCollapse: '折叠菜单',
collapseMenuDisplayName: '折叠菜单显示名称',
......
......@@ -45,13 +45,13 @@ async function getAsyncMenus() {
}
// 获取深层扁平化菜单
export const getFlatMenus = async () => {
export const getFlatMenus = async (): Promise<Menu[]> => {
const menus = await getAsyncMenus();
return flatMenus(menus);
};
// 获取菜单 树级
export const getMenus = async () => {
export const getMenus = async (): Promise<Menu[]> => {
const menus = await getAsyncMenus();
const routes = router.getRoutes();
return !isBackMode() ? filter(menus, basicFilter(routes)) : menus;
......@@ -65,7 +65,7 @@ export async function getCurrentParentPath(currentPath: string) {
}
// 获取1级菜单,删除children
export async function getShallowMenus() {
export async function getShallowMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus();
const routes = router.getRoutes();
const shallowMenuList = menus.map((item) => ({ ...item, children: undefined }));
......
......@@ -3,6 +3,7 @@ import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
import modules from 'globby!/@/router/routes/modules/**/*.@(ts)';
import { PageEnum } from '/@/enums/pageEnum';
import { t } from '/@/hooks/web/useI18n';
......@@ -15,6 +16,15 @@ Object.keys(modules).forEach((key) => {
export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
export const RootRoute: AppRouteRecordRaw = {
path: '/',
name: 'Root',
redirect: PageEnum.BASE_HOME,
meta: {
title: 'Root',
},
};
export const LoginRoute: AppRouteRecordRaw = {
path: '/login',
name: 'Login',
......@@ -25,4 +35,4 @@ export const LoginRoute: AppRouteRecordRaw = {
};
// 基础路由 不用权限
export const basicRoutes = [LoginRoute, REDIRECT_ROUTE];
export const basicRoutes = [LoginRoute, RootRoute, REDIRECT_ROUTE];
......@@ -70,6 +70,8 @@ const setting: ProjectConfig = {
showDoc: true,
// Whether to show the notification button
showNotice: true,
// Whether to display the menu search
showSearch: true,
},
// Menu configuration
......@@ -101,8 +103,6 @@ const setting: ProjectConfig = {
split: false,
// Top menu layout
topMenuAlign: 'center',
// Hide the search box when the menu is collapsed
collapsedShowSearch: false,
// Fold trigger position
trigger: TriggerEnum.HEADER,
// Turn on accordion mode, only show a menu
......
......@@ -17,7 +17,6 @@ export interface MenuSetting {
type: MenuTypeEnum;
theme: ThemeEnum;
topMenuAlign: 'start' | 'center' | 'end';
collapsedShowSearch: boolean;
trigger: TriggerEnum;
accordion: boolean;
}
......@@ -45,6 +44,8 @@ export interface HeaderSetting {
showDoc: boolean;
// 显示消息中心按钮
showNotice: boolean;
showSearch: boolean;
}
export interface LocaleSetting {
......
import { defineAsyncComponent } from 'vue';
import { Spin } from 'ant-design-vue';
export function createAsyncComponent(loader: any) {
return defineAsyncComponent({
loader: loader,
loadingComponent: <Spin spinning={true} />,
// The error component will be displayed if a timeout is
// provided and exceeded. Default: Infinity.
timeout: 3000,
// Defining if component is suspensible. Default: true.
// suspensible: false,
delay: 100,
/**
*
* @param {*} error Error message object
* @param {*} retry A function that indicating whether the async component should retry when the loader promise rejects
* @param {*} fail End of failure
* @param {*} attempts Maximum allowed retries number
*/
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
// retry on fetch errors, 3 max attempts
retry();
} else {
// Note that retry/fail are like resolve/reject of a promise:
// one of them must be called for the error handling to continue.
fail();
}
},
});
}
......@@ -124,7 +124,7 @@ export function filter<T = any>(
tree: T[],
func: (n: T) => boolean,
config: Partial<TreeHelperConfig> = {}
) {
): T[] {
config = getConfig(config);
const children = config.children as string;
function listFilter(list: T[]) {
......@@ -142,7 +142,7 @@ export function forEach<T = any>(
tree: T[],
func: (n: T) => any,
config: Partial<TreeHelperConfig> = {}
) {
): void {
config = getConfig(config);
const list: any[] = [...tree];
const { children } = config;
......@@ -155,7 +155,7 @@ export function forEach<T = any>(
/**
* @description: 提取tree指定结构
*/
export function treeMap(treeData: any[], opt: { children?: string; conversion: Fn }) {
export function treeMap<T = any>(treeData: T[], opt: { children?: string; conversion: Fn }): T[] {
return treeData.map((item) => treeMapEach(item, opt));
}
......
<template>
<div :class="prefixCls">
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
<LockOutlined />
<span>{{ t('sys.lock.unlock') }}</span>
</div>
<div :class="`${prefixCls}__date`">
<div :class="`${prefixCls}__hour`">
{{ hour }}
<span class="meridiem" v-show="showDate">{{ meridiem }}</span>
</div>
<div :class="`${prefixCls}__minute`">{{ minute }} </div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div :class="`${prefixCls}-entry__header`">
<img src="/@/assets/images/header.jpg" :class="`${prefixCls}-entry__header-img`" />
<p :class="`${prefixCls}-entry__header-name`">{{ realName }}</p>
</div>
<InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" />
<span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef">
{{ t('sys.lock.alert') }}
</span>
<div :class="`${prefixCls}-entry__footer`">
<a-button
type="link"
size="small"
class="mt-2 mr-2"
:disabled="loadingRef"
@click="handleShowForm(true)"
>
{{ t('sys.lock.back') }}
</a-button>
<a-button
type="link"
size="small"
class="mt-2 mr-2"
:disabled="loadingRef"
@click="goLogin"
>
{{ t('sys.lock.backToLogin') }}
</a-button>
<a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loadingRef">
{{ t('sys.lock.entry') }}
</a-button>
</div>
</div>
</div>
</transition>
<div :class="`${prefixCls}__footer-date`">
<div class="time" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span>
</div>
<div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { Alert, Input } from 'ant-design-vue';
import { userStore } from '/@/store/modules/user';
import { lockStore } from '/@/store/modules/lock';
import { useI18n } from '/@/hooks/web/useI18n';
import { useNow } from './useNow';
import { useDesign } from '/@/hooks/web/useDesign';
import { LockOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'LockPage',
components: { Alert, LockOutlined, InputPassword: Input.Password },
setup() {
const passwordRef = ref('');
const loadingRef = ref(false);
const errMsgRef = ref(false);
const showDate = ref(true);
const { prefixCls } = useDesign('lock-page');
const { start, stop, ...state } = useNow(true);
const { t } = useI18n();
const realName = computed(() => {
const { realName } = userStore.getUserInfoState || {};
return realName;
});
/**
* @description: unLock
*/
async function unLock() {
if (!passwordRef.value) {
return;
}
let password = passwordRef.value;
try {
loadingRef.value = true;
const res = await lockStore.unLockAction({ password });
errMsgRef.value = !res;
} finally {
loadingRef.value = false;
}
}
function goLogin() {
userStore.loginOut(true);
lockStore.resetLockInfo();
}
function handleShowForm(show = false) {
showDate.value = show;
}
return {
goLogin,
realName,
unLock,
errMsgRef,
loadingRef,
t,
prefixCls,
showDate,
password: passwordRef,
handleShowForm,
...state,
};
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-lock-page';
.@{prefix-cls} {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 3000;
display: flex;
width: 100vw;
height: 100vh;
// background: rgba(23, 27, 41);
background: #000;
align-items: center;
justify-content: center;
&__unlock {
position: absolute;
top: 0;
left: 50%;
display: flex;
height: 50px;
padding-top: 20px;
font-size: 18px;
color: #fff;
cursor: pointer;
transform: translate(-50%, 0);
flex-direction: column;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
}
&__date {
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
}
&__hour {
position: relative;
margin-right: 80px;
.meridiem {
position: absolute;
top: 20px;
left: 20px;
font-size: 26px;
}
@media (max-width: @screen-xs) {
margin-right: 20px;
}
}
&__hour,
&__minute {
display: flex;
width: 40%;
height: 74%;
// font-size: 50em;
font-weight: 700;
color: #bababa;
background: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
// .respond-to(large-only, { font-size: 25em;});
// .respond-to(large-only, { font-size: 30em;});
@media (min-width: @screen-xxxl-min) {
font-size: 46em;
}
@media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
font-size: 38em;
}
@media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) {
font-size: 30em;
}
@media (min-width: @screen-md-max) and (max-width: @screen-lg-max) {
font-size: 23em;
}
@media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
font-size: 19em;
}
@media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
font-size: 13em;
}
@media (max-width: @screen-xs) {
height: 50%;
font-size: 6em;
border-radius: 20px;
}
}
&__footer-date {
position: absolute;
bottom: 20px;
left: 50%;
font-family: helvetica;
color: #bababa;
transform: translate(-50%, 0);
.time {
font-size: 50px;
.meridiem {
font-size: 32px;
}
}
.date {
font-size: 26px;
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: @error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>
<template>
<div :class="prefixCls">
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
<LockOutlined />
<span>{{ t('sys.lock.unlock') }}</span>
</div>
<div :class="`${prefixCls}__date`">
<div :class="`${prefixCls}__hour`">
{{ hour }}
<span class="meridiem" v-show="showDate">{{ meridiem }}</span>
</div>
<div :class="`${prefixCls}__minute`">{{ minute }} </div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div :class="`${prefixCls}-entry__header`">
<img src="/@/assets/images/header.jpg" :class="`${prefixCls}-entry__header-img`" />
<p :class="`${prefixCls}-entry__header-name`">{{ realName }}</p>
</div>
<InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" />
<span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef">
{{ t('sys.lock.alert') }}
</span>
<div :class="`${prefixCls}-entry__footer`">
<a-button
type="link"
size="small"
class="mt-2 mr-2"
:disabled="loadingRef"
@click="handleShowForm(true)"
>
{{ t('sys.lock.back') }}
</a-button>
<a-button
type="link"
size="small"
class="mt-2 mr-2"
:disabled="loadingRef"
@click="goLogin"
>
{{ t('sys.lock.backToLogin') }}
</a-button>
<a-button class="mt-2" type="link" size="small" @click="unLock()" :loading="loadingRef">
{{ t('sys.lock.entry') }}
</a-button>
</div>
</div>
</div>
</transition>
<div :class="`${prefixCls}__footer-date`">
<div class="time" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span>
</div>
<div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
</div>
</div>
<transition name="fade-bottom" mode="out-in">
<LockPage v-if="getIsLock" />
</transition>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import { Alert, Input } from 'ant-design-vue';
import { userStore } from '/@/store/modules/user';
import { lockStore } from '/@/store/modules/lock';
import { useI18n } from '/@/hooks/web/useI18n';
import { useNow } from './useNow';
import { useDesign } from '/@/hooks/web/useDesign';
import { LockOutlined } from '@ant-design/icons-vue';
import { defineComponent } from 'vue';
import LockPage from './LockPage.vue';
import { getIsLock } from '/@/hooks/web/useLockPage';
export default defineComponent({
name: 'LockPage',
components: { Alert, LockOutlined, InputPassword: Input.Password },
name: 'Lock',
components: { LockPage },
setup() {
const passwordRef = ref('');
const loadingRef = ref(false);
const errMsgRef = ref(false);
const showDate = ref(true);
const { prefixCls } = useDesign('lock-page');
const { start, stop, ...state } = useNow(true);
const { t } = useI18n();
const realName = computed(() => {
const { realName } = userStore.getUserInfoState || {};
return realName;
});
/**
* @description: unLock
*/
async function unLock() {
if (!passwordRef.value) {
return;
}
let password = passwordRef.value;
try {
loadingRef.value = true;
const res = await lockStore.unLockAction({ password });
errMsgRef.value = !res;
} finally {
loadingRef.value = false;
}
}
function goLogin() {
userStore.loginOut(true);
lockStore.resetLockInfo();
}
function handleShowForm(show = false) {
showDate.value = show;
}
return {
goLogin,
realName,
unLock,
errMsgRef,
loadingRef,
t,
prefixCls,
showDate,
password: passwordRef,
handleShowForm,
...state,
};
return { getIsLock };
},
});
</script>
<style lang="less" scoped>
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-lock-page';
.@{prefix-cls} {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 3000;
display: flex;
width: 100vw;
height: 100vh;
// background: rgba(23, 27, 41);
background: #000;
align-items: center;
justify-content: center;
&__unlock {
position: absolute;
top: 0;
left: 50%;
display: flex;
height: 50px;
padding-top: 20px;
font-size: 18px;
color: #fff;
cursor: pointer;
transform: translate(-50%, 0);
flex-direction: column;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
}
&__date {
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
}
&__hour {
position: relative;
margin-right: 80px;
.meridiem {
position: absolute;
top: 20px;
left: 20px;
font-size: 26px;
}
@media (max-width: @screen-xs) {
margin-right: 20px;
}
}
&__hour,
&__minute {
display: flex;
width: 40%;
height: 74%;
// font-size: 50em;
font-weight: 700;
color: #bababa;
background: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
// .respond-to(large-only, { font-size: 25em;});
// .respond-to(large-only, { font-size: 30em;});
@media (min-width: @screen-xxxl-min) {
font-size: 46em;
}
@media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
font-size: 38em;
}
@media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) {
font-size: 30em;
}
@media (min-width: @screen-md-max) and (max-width: @screen-lg-max) {
font-size: 23em;
}
@media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
font-size: 19em;
}
@media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
font-size: 13em;
}
@media (max-width: @screen-xs) {
height: 50%;
font-size: 6em;
border-radius: 20px;
}
}
&__footer-date {
position: absolute;
bottom: 20px;
left: 50%;
font-family: helvetica;
color: #bababa;
transform: translate(-50%, 0);
.time {
font-size: 50px;
.meridiem {
font-size: 32px;
}
}
.date {
font-size: 26px;
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: @error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册