global-search.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <script setup lang="ts">
  2. import type { MenuRecordRaw } from '@vben/types';
  3. import { onMounted, onUnmounted, ref, watch } from 'vue';
  4. import { $t } from '@vben/locales';
  5. import {
  6. IcRoundArrowDownward,
  7. IcRoundArrowUpward,
  8. IcRoundSearch,
  9. IcRoundSubdirectoryArrowLeft,
  10. MdiKeyboardEsc,
  11. } from '@vben-core/iconify';
  12. import {
  13. Dialog,
  14. DialogContent,
  15. DialogDescription,
  16. DialogFooter,
  17. DialogHeader,
  18. DialogTitle,
  19. DialogTrigger,
  20. } from '@vben-core/shadcn-ui';
  21. import { isWindowsOs } from '@vben-core/toolkit';
  22. import { useMagicKeys, useToggle, whenever } from '@vueuse/core';
  23. import SearchPanel from './search-panel.vue';
  24. defineOptions({
  25. name: 'GlobalSearch',
  26. });
  27. const props = withDefaults(
  28. defineProps<{ enableShortcutKey?: boolean; menus: MenuRecordRaw[] }>(),
  29. {
  30. enableShortcutKey: true,
  31. menus: () => [],
  32. },
  33. );
  34. const [open, toggleOpen] = useToggle();
  35. const keyword = ref('');
  36. function handleClose() {
  37. open.value = false;
  38. keyword.value = '';
  39. }
  40. const keys = useMagicKeys();
  41. const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
  42. whenever(cmd, () => {
  43. if (props.enableShortcutKey) {
  44. open.value = true;
  45. }
  46. });
  47. const preventDefaultBrowserSearchHotKey = (event: KeyboardEvent) => {
  48. if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
  49. event.preventDefault();
  50. }
  51. };
  52. const toggleKeydownListener = () => {
  53. if (props.enableShortcutKey) {
  54. window.addEventListener('keydown', preventDefaultBrowserSearchHotKey);
  55. } else {
  56. window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
  57. }
  58. };
  59. watch(() => props.enableShortcutKey, toggleKeydownListener);
  60. onMounted(() => {
  61. toggleKeydownListener();
  62. onUnmounted(() => {
  63. window.removeEventListener('keydown', preventDefaultBrowserSearchHotKey);
  64. });
  65. });
  66. </script>
  67. <template>
  68. <div>
  69. <Dialog :open="open">
  70. <DialogTrigger as-child>
  71. <div
  72. class="md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none"
  73. @click="toggleOpen()"
  74. >
  75. <IcRoundSearch
  76. class="text-muted-foreground group-hover:text-foreground size-4 group-hover:opacity-100"
  77. />
  78. <span
  79. class="text-muted-foreground group-hover:text-foreground hidden text-sm duration-300 md:block"
  80. >
  81. {{ $t('widgets.search.title') }}
  82. </span>
  83. <span
  84. v-if="enableShortcutKey"
  85. class="bg-background border-foreground/60 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block"
  86. >
  87. {{ isWindowsOs() ? 'Ctrl' : '⌘' }}
  88. <kbd>K</kbd>
  89. </span>
  90. <span v-else></span>
  91. </div>
  92. </DialogTrigger>
  93. <DialogContent
  94. class="top-0 h-full w-full -translate-y-0 border-none p-0 shadow-xl sm:top-[10%] sm:h-[unset] sm:w-[600px] sm:rounded-2xl"
  95. @close="handleClose"
  96. >
  97. <DialogHeader>
  98. <DialogTitle
  99. class="border-border flex h-12 items-center gap-5 border-b px-5 font-normal"
  100. >
  101. <IcRoundSearch class="mt-1 size-4" />
  102. <input
  103. v-model="keyword"
  104. :placeholder="$t('widgets.search.search-navigate')"
  105. class="ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 text-sm outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent"
  106. />
  107. </DialogTitle>
  108. <DialogDescription />
  109. </DialogHeader>
  110. <SearchPanel :keyword="keyword" :menus="menus" @close="handleClose" />
  111. <DialogFooter
  112. class="text-muted-foreground border-border hidden flex-row rounded-b-2xl border-t px-4 py-2 text-xs sm:flex sm:justify-start sm:gap-x-4"
  113. >
  114. <div class="flex items-center">
  115. <IcRoundSubdirectoryArrowLeft class="mr-1" />
  116. {{ $t('widgets.search.select') }}
  117. </div>
  118. <div class="flex items-center">
  119. <IcRoundArrowUpward class="mr-2" />
  120. <IcRoundArrowDownward class="mr-2" />
  121. {{ $t('widgets.search.navigate') }}
  122. </div>
  123. <div class="flex items-center">
  124. <MdiKeyboardEsc class="mr-1" />
  125. {{ $t('widgets.search.close') }}
  126. </div>
  127. </DialogFooter>
  128. </DialogContent>
  129. </Dialog>
  130. </div>
  131. </template>