初始版本提交

This commit is contained in:
yaoyn
2024-02-05 09:15:37 +08:00
parent b52d4414be
commit 445292105f
1848 changed files with 236859 additions and 75 deletions

View File

@ -0,0 +1,16 @@
import { withInstall } from '/@/utils';
import appLogo from './src/AppLogo.vue';
import appProvider from './src/AppProvider.vue';
import appSearch from './src/search/AppSearch.vue';
import appLocalePicker from './src/AppLocalePicker.vue';
import appDarkModeToggle from './src/AppDarkModeToggle.vue';
import userPostChange from './src/UserPostChange.vue';
export { useAppProviderContext } from './src/useAppContext';
export const AppLogo = withInstall(appLogo);
export const AppProvider = withInstall(appProvider);
export const AppSearch = withInstall(appSearch);
export const AppLocalePicker = withInstall(appLocalePicker);
export const AppDarkModeToggle = withInstall(appDarkModeToggle);
export const UserPostChange = withInstall(userPostChange);

View File

@ -0,0 +1,76 @@
<template>
<div v-if="getShowDarkModeToggle" :class="getClass" @click="toggleDarkMode">
<div :class="`${prefixCls}-inner`"></div>
<SvgIcon size="14" name="sun" />
<SvgIcon size="14" name="moon" />
</div>
</template>
<script lang="ts" setup>
import { computed, unref } from 'vue';
import { SvgIcon } from '/@/components/Icon';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { updateHeaderBgColor, updateSidebarBgColor } from '/@/logics/theme/updateBackground';
import { updateDarkTheme } from '/@/logics/theme/dark';
import { ThemeEnum } from '/@/enums/appEnum';
const { prefixCls } = useDesign('dark-switch');
const { getDarkMode, setDarkMode, getShowDarkModeToggle } = useRootSetting();
const isDark = computed(() => getDarkMode.value === ThemeEnum.DARK);
const getClass = computed(() => [
prefixCls,
{
[`${prefixCls}--dark`]: unref(isDark),
},
]);
function toggleDarkMode() {
const darkMode = getDarkMode.value === ThemeEnum.DARK ? ThemeEnum.LIGHT : ThemeEnum.DARK;
setDarkMode(darkMode);
updateDarkTheme(darkMode);
updateHeaderBgColor();
updateSidebarBgColor();
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-dark-switch';
html[data-theme='dark'] {
.@{prefix-cls} {
border: 1px solid rgb(196 188 188);
}
}
.@{prefix-cls} {
position: relative;
display: flex;
width: 50px;
height: 26px;
padding: 0 6px;
margin-left: auto;
cursor: pointer;
background-color: #151515;
border-radius: 30px;
justify-content: space-between;
align-items: center;
&-inner {
position: absolute;
z-index: 1;
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 50%;
transition: transform 0.5s, background-color 0.5s;
will-change: transform;
}
&--dark {
.@{prefix-cls}-inner {
transform: translateX(calc(100% + 2px));
}
}
}
</style>

View File

@ -0,0 +1,89 @@
<!--
* @Author: Vben
* @Description: Multi-language switching component
-->
<template>
<Dropdown
placement="bottom"
:trigger="['click']"
:dropMenuList="languageTypeList"
:selectedKeys="selectedKeys"
@menu-event="handleMenuEvent"
overlayClassName="app-locale-picker-overlay"
>
<span class="cursor-pointer flex items-center">
<Icon icon="ri:translate" size="24" />
<span v-if="showText" class="ml-1">{{ getLocaleText }}</span>
</span>
</Dropdown>
</template>
<script lang="ts" setup>
import type { LocaleType } from '/#/config';
import type { DropMenu } from '/@/components/Dropdown';
import { ref, watchEffect, unref, computed, onMounted } from 'vue';
import { Dropdown } from '/@/components/Dropdown';
import { Icon } from '/@/components/Icon';
import { useLocale } from '/@/locales/useLocale';
// import { localeList } from '/@/settings/localeSetting';
import { getLanguageTypeList } from '/@/api/system/language';
const props = defineProps({
/**
* Whether to display text
*/
showText: { type: Boolean, default: true },
/**
* Whether to refresh the interface when changing
*/
reload: { type: Boolean },
});
const selectedKeys = ref<string[]>([]);
const languageTypeList = ref<DropMenu[]>([]);
onMounted(async () => {
const typeList = await getLanguageTypeList();
for (const item of typeList) {
languageTypeList.value.push({
text: item.name,
event: item.code,
} as DropMenu);
}
});
const { changeLocale, getLocale } = useLocale();
const getLocaleText = computed(() => {
const key = selectedKeys.value[0];
if (!key) {
return '';
}
return languageTypeList.value.find((item) => item.event === key)?.text;
});
watchEffect(() => {
selectedKeys.value = [unref(getLocale)];
});
async function toggleLocale(lang: LocaleType | string) {
await changeLocale(lang as LocaleType);
selectedKeys.value = [lang as string];
props.reload && location.reload();
}
function handleMenuEvent(menu: DropMenu) {
if (unref(getLocale) === menu.event) {
return;
}
toggleLocale(menu.event as string);
}
</script>
<style lang="less">
.app-locale-picker-overlay {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
</style>

View File

@ -0,0 +1,102 @@
<!--
* @Author: Vben
* @Description: logo component
-->
<template>
<div class="anticon" :class="getAppLogoClass" @click="goHome">
<a href="https://www.learun.cn/" target="_blank">
<img :src="logoConfig.menuLogoUrl || logo" width="24" style="height: 30px" />
<div class="truncate md:opacity-100" :class="getTitleClass" v-show="showTitle">
{{ logoConfig.shortName || title }}
</div>
</a>
</div>
</template>
<script lang="ts" setup>
import { computed, unref } from 'vue';
import { useGlobSetting } from '/@/hooks/setting';
import { useGo } from '/@/hooks/web/usePage';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useDesign } from '/@/hooks/web/useDesign';
import { PageEnum } from '/@/enums/pageEnum';
import { useUserStore } from '/@/store/modules/user';
import { useAppStore } from '/@/store/modules/app';
import logo from '/@/assets/images/logo.svg';
const props = defineProps({
/**
* The theme of the current parent component
*/
theme: { type: String, validator: (v: string) => ['light', 'dark'].includes(v) },
/**
* Whether to show title
*/
showTitle: { type: Boolean, default: true },
/**
* The title is also displayed when the menu is collapsed
*/
alwaysShowTitle: { type: Boolean },
});
const { prefixCls } = useDesign('app-logo');
const { getCollapsedShowTitle } = useMenuSetting();
const userStore = useUserStore();
const { title } = useGlobSetting();
const go = useGo();
const appStore = useAppStore();
const logoConfig = appStore.getLogoConfig;
const getAppLogoClass = computed(() => [
prefixCls,
props.theme,
{ 'collapsed-show-title': unref(getCollapsedShowTitle) },
]);
const getTitleClass = computed(() => [
`${prefixCls}__title`,
{
'xs:opacity-0': !props.alwaysShowTitle,
},
]);
function goHome() {
go(userStore.getUserInfo.homePath || PageEnum.BASE_HOME);
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-app-logo';
.@{prefix-cls} {
display: flex;
align-items: center;
padding-left: 7px;
cursor: pointer;
transition: all 0.2s ease;
&.light {
border-bottom: 1px solid @border-color-base;
}
&.collapsed-show-title {
padding-left: 20px;
}
&.light &__title {
color: @primary-color;
}
&.dark &__title {
color: #0096df;
}
&__title {
font-size: 20px;
font-weight: 400;
transition: all 0.5s;
line-height: normal;
}
}
a {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts">
import { defineComponent, toRefs, ref, unref } from 'vue';
import { createAppProviderContext } from './useAppContext';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
import { prefixCls } from '/@/settings/designSetting';
import { useAppStore } from '/@/store/modules/app';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
const props = {
/**
* class style prefix
*/
prefixCls: { type: String, default: prefixCls },
};
export default defineComponent({
name: 'AppProvider',
inheritAttrs: false,
props,
setup(props, { slots }) {
const isMobile = ref(false);
const isSetState = ref(false);
const appStore = useAppStore();
// Monitor screen breakpoint information changes
createBreakpointListen(({ screenMap, sizeEnum, width }) => {
const lgWidth = screenMap.get(sizeEnum.LG);
if (lgWidth) {
isMobile.value = width.value - 1 < lgWidth;
}
handleRestoreState();
});
const { prefixCls } = toRefs(props);
// Inject variables into the global
createAppProviderContext({ prefixCls, isMobile });
/**
* Used to maintain the state before the window changes
*/
function handleRestoreState() {
if (unref(isMobile)) {
if (!unref(isSetState)) {
isSetState.value = true;
const {
menuSetting: {
type: menuType,
mode: menuMode,
collapsed: menuCollapsed,
split: menuSplit,
},
} = appStore.getProjectConfig;
appStore.setProjectConfig({
menuSetting: {
type: MenuTypeEnum.SIDEBAR,
mode: MenuModeEnum.INLINE,
split: false,
},
});
appStore.setBeforeMiniInfo({ menuMode, menuCollapsed, menuType, menuSplit });
}
} else {
if (unref(isSetState)) {
isSetState.value = false;
const { menuMode, menuCollapsed, menuType, menuSplit } = appStore.getBeforeMiniInfo;
appStore.setProjectConfig({
menuSetting: {
type: menuType,
mode: menuMode,
collapsed: menuCollapsed,
split: menuSplit,
},
});
}
}
}
return () => slots.default?.();
},
});
</script>

View File

@ -0,0 +1,86 @@
<!--
* @Author: Vben
* @Description: post switching component
-->
<template>
<Tooltip :title="t('岗位切换')" placement="bottom" :mouseEnterDelay="0.5">
<Dropdown
placement="bottom"
:trigger="['click']"
:dropMenuList="formInfo.posts"
:selectedKeys="selectedKeys"
@menu-event="handleMenuEvent"
overlayClassName="app-locale-picker-overlay"
>
<span class="cursor-pointer flex items-center p-2">
<Icon icon="ant-design:user-switch-outlined" size="24" />
</span>
</Dropdown>
</Tooltip>
</template>
<script lang="ts" setup>
import type { DropMenu } from '/@/components/Dropdown';
import { ref, watchEffect, watch } from 'vue';
import { Dropdown } from '/@/components/Dropdown';
import { Icon } from '/@/components/Icon';
import { storeToRefs } from 'pinia';
import { Tooltip } from 'ant-design-vue';
import { useUserStore } from '/@/store/modules/user';
import { changePost } from '/@/api/system/post';
import { useI18n } from '/@/hooks/web/useI18n';
const userStore = useUserStore();
const { t } = useI18n();
const { userInfo } = storeToRefs(userStore);
const formInfo: any = ref({
posts: [],
postId: '',
});
getDropMenuList();
watch(
() => userInfo.value,
(val) => {
if (val) getDropMenuList();
},
);
const selectedKeys = ref<string[]>([]);
watchEffect(() => {
selectedKeys.value = [formInfo.value.postId];
});
function getDropMenuList() {
if (userInfo.value?.posts) {
formInfo.value = userInfo.value;
formInfo.value.posts.forEach((o) => {
o.text = o.name;
o.event = o.id;
});
}
}
async function toggleLocale(lang: string) {
let res = await changePost(lang);
selectedKeys.value = [lang as string];
formInfo.value.departmentId = res.departmentId;
formInfo.value.departmentName = res.departmentName;
formInfo.value.postId = res.postId;
formInfo.value.postName = res.postName;
userStore.setUserInfo(formInfo.value);
}
function handleMenuEvent(menu: DropMenu) {
if (formInfo.value.postId === menu.event) {
return;
}
toggleLocale(menu.event as string);
}
</script>
<style lang="less">
.app-locale-picker-overlay {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="tsx">
import { defineComponent, ref, unref } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import AppSearchModal from './AppSearchModal.vue';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'AppSearch',
setup() {
const showModal = ref(false);
const { t } = useI18n();
function changeModal(show: boolean) {
showModal.value = show;
}
return () => {
return (
<div class="p-1" onClick={changeModal.bind(null, true)}>
<Tooltip>
{{
title: () => t('搜索'),
default: () => <Icon size="20" icon="fa:search" />,
}}
</Tooltip>
<AppSearchModal onClose={changeModal.bind(null, false)} visible={unref(showModal)} />
</div>
);
};
},
});
</script>

View File

@ -0,0 +1,56 @@
<template>
<div :class="`${prefixCls}`">
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ant-design:enter-outlined" />
<span>{{ t('确认') }}</span>
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-up-outline" />
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="ion:arrow-down-outline" />
<span>{{ t('切换') }}</span>
<AppSearchKeyItem :class="`${prefixCls}-item`" icon="mdi:keyboard-esc" />
<span>{{ t('关闭') }}</span>
</div>
</template>
<script lang="ts" setup>
import AppSearchKeyItem from './AppSearchKeyItem.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useI18n } from '/@/hooks/web/useI18n';
const { prefixCls } = useDesign('app-search-footer');
const { t } = useI18n();
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-app-search-footer';
.@{prefix-cls} {
position: relative;
display: flex;
height: 44px;
padding: 0 16px;
font-size: 12px;
color: #666;
background-color: @component-background;
border-top: 1px solid @border-color-base;
border-radius: 0 0 16px 16px;
align-items: center;
flex-shrink: 0;
&-item {
display: flex;
width: 20px;
height: 18px;
padding-bottom: 2px;
margin-right: 0.4em;
background-color: 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 rgb(30 35 90 / 40%);
align-items: center;
justify-content: center;
&:nth-child(2),
&:nth-child(3),
&:nth-child(6) {
margin-left: 14px;
}
}
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<span :class="$attrs.class">
<Icon :icon="icon" />
</span>
</template>
<script lang="ts" setup>
import { Icon } from '/@/components/Icon';
defineProps({
icon: String,
});
</script>

View File

@ -0,0 +1,267 @@
<template>
<Teleport to="body">
<transition name="zoom-fade" mode="out-in">
<div :class="getClass" @click.stop v-if="visible">
<div :class="`${prefixCls}-content`" v-click-outside="handleClose">
<div :class="`${prefixCls}-input__wrapper`">
<a-input
:class="`${prefixCls}-input`"
:placeholder="t('搜索')"
ref="inputRef"
allow-clear
@change="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<span :class="`${prefixCls}-cancel`" @click="handleClose">
{{ t('取消') }}
</span>
</div>
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
{{ t('暂无搜索结果') }}
</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`">
<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`">
<Icon icon="ant-design:enter-outlined" :size="20" />
</div>
</li>
</ul>
<AppSearchFooter />
</div>
</div>
</transition>
</Teleport>
</template>
<script lang="ts" setup>
import { computed, unref, ref, watch, nextTick } from 'vue';
import { SearchOutlined } from '@ant-design/icons-vue';
import AppSearchFooter from './AppSearchFooter.vue';
import Icon from '/@/components/Icon';
// @ts-ignore
import vClickOutside from '/@/directives/clickOutside';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRefs } from '/@/hooks/core/useRefs';
import { useMenuSearch } from './useMenuSearch';
import { useI18n } from '/@/hooks/web/useI18n';
import { useAppInject } from '/@/hooks/web/useAppInject';
const props = defineProps({
visible: { type: Boolean },
});
const emit = defineEmits(['close']);
const scrollWrap = ref(null);
const inputRef = ref<Nullable<HTMLElement>>(null);
const { t } = useI18n();
const { prefixCls } = useDesign('app-search-modal');
const [refs, setRefs] = useRefs();
const { getIsMobile } = useAppInject();
const { handleSearch, searchResult, keyword, activeIndex, handleEnter, handleMouseenter } =
useMenuSearch(refs, scrollWrap, emit);
const getIsNotData = computed(() => !keyword || unref(searchResult).length === 0);
const getClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--mobile`]: unref(getIsMobile),
},
];
});
watch(
() => props.visible,
(visible: boolean) => {
visible &&
nextTick(() => {
unref(inputRef)?.focus();
});
},
);
function handleClose() {
searchResult.value = [];
emit('close');
}
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-app-search-modal';
@footer-prefix-cls: ~'@{namespace}-app-search-footer';
.@{prefix-cls} {
position: fixed;
top: 0;
left: 0;
z-index: 800;
display: flex;
width: 100%;
height: 100%;
padding-top: 50px;
background-color: rgb(0 0 0 / 25%);
justify-content: center;
&--mobile {
padding: 0;
> div {
width: 100%;
}
.@{prefix-cls}-input {
width: calc(100% - 38px);
}
.@{prefix-cls}-cancel {
display: inline-block;
}
.@{prefix-cls}-content {
width: 100%;
height: 100%;
border-radius: 0;
}
.@{footer-prefix-cls} {
display: none;
}
.@{prefix-cls}-list {
height: calc(100% - 80px);
max-height: unset;
&__item {
&-enter {
opacity: 0% !important;
}
}
}
}
&-content {
position: relative;
width: 632px;
margin: 0 auto auto;
background-color: @component-background;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 25%);
flex-direction: column;
}
&-input__wrapper {
display: flex;
padding: 14px 14px 0;
justify-content: space-between;
align-items: center;
}
&-input {
width: 100%;
height: 48px;
font-size: 1.5em;
color: #1c1e21;
border-radius: 6px;
span[role='img'] {
color: #999;
}
}
&-cancel {
display: none;
font-size: 1em;
color: #666;
}
&-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-color: @component-background;
border-radius: 4px;
box-shadow: 0 1px 3px 0 #d4d9e1;
align-items: center;
> div:first-child,
> div:last-child {
display: flex;
align-items: center;
}
&--active {
color: #fff;
background-color: @primary-color;
.@{prefix-cls}-list__item-enter {
opacity: 100%;
}
}
&-icon {
width: 30px;
}
&-text {
flex: 1;
}
&-enter {
width: 30px;
opacity: 0%;
}
}
}
}
</style>

View File

@ -0,0 +1,169 @@
import type { Menu } from '/@/router/types';
import { ref, unref, Ref, nextTick } from 'vue';
import { getMenus } from '/@/router/menus';
import { cloneDeep } from 'lodash-es';
import { filter, forEach } from '/@/utils/helper/treeHelper';
import { useGo } from '/@/hooks/web/usePage';
import { useScrollTo } from '/@/hooks/event/useScrollTo';
import { onKeyStroke, useDebounceFn } from '@vueuse/core';
import { useI18n } from '/@/hooks/web/useI18n';
import { testRegLength } from '/@/utils/event/design';
export interface SearchResult {
name: string;
path: string;
icon?: string;
}
// 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('.*');
testRegLength(str);
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 = useDebounceFn(search, 200);
async function getCurSysMenus() {
const list = await getMenus();
menuList = cloneDeep(list);
forEach(menuList, (item) => {
item.name = t(item.name);
});
}
async function search(e: ChangeEvent) {
e?.stopPropagation();
await getCurSysMenus();
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) && !item.hideMenu;
});
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, hideMenu, meta } = item;
if (!hideMenu && reg.test(name) && (!children?.length || meta?.hideChildrenInMenu)) {
ret.push({
name: parent?.name ? `${parent.name} > ${name}` : name,
path,
icon,
});
}
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
ret.push(...handlerSearchResult(children, reg, item));
}
});
return ret;
}
// Activate when the mouse moves to a certain line
function handleMouseenter(e: any) {
const index = e.target.dataset.index;
activeIndex.value = Number(index);
}
// Arrow key up
function handleUp() {
if (!searchResult.value.length) return;
activeIndex.value--;
if (activeIndex.value < 0) {
activeIndex.value = searchResult.value.length - 1;
}
handleScroll();
}
// Arrow key down
function handleDown() {
if (!searchResult.value.length) return;
activeIndex.value++;
if (activeIndex.value > searchResult.value.length - 1) {
activeIndex.value = 0;
}
handleScroll();
}
// When the keyboard up and down keys move to an invisible place
// the scroll bar needs to scroll automatically
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();
}
// enter keyboard event
async 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();
await nextTick();
go(to.path);
}
// close search modal
function handleClose() {
searchResult.value = [];
emit('close');
}
// enter search
onKeyStroke('Enter', handleEnter);
// Monitor keyboard arrow keys
onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown);
// esc close
onKeyStroke('Escape', handleClose);
return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter };
}

View File

@ -0,0 +1,17 @@
import { InjectionKey, Ref } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface AppProviderContextProps {
prefixCls: Ref<string>;
isMobile: Ref<boolean>;
}
const key: InjectionKey<AppProviderContextProps> = Symbol();
export function createAppProviderContext(context: AppProviderContextProps) {
return createContext<AppProviderContextProps>(context, key);
}
export function useAppProviderContext() {
return useContext<AppProviderContextProps>(key);
}