|
@@ -0,0 +1,139 @@
|
|
|
+<script lang="ts">
|
|
|
+export default {
|
|
|
+ name: "Search",
|
|
|
+};
|
|
|
+</script>
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, watch } from "vue";
|
|
|
+import { Search } from "@element-plus/icons-vue";
|
|
|
+import { watchDebounced, useFocus } from "@vueuse/core";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import _ from "lodash";
|
|
|
+
|
|
|
+const { t } = useI18n();
|
|
|
+
|
|
|
+const input = ref("");
|
|
|
+const input$ = ref();
|
|
|
+const { focused } = useFocus(computed(() => input$.value?.input));
|
|
|
+const suggestions = ref<unknown[]>([]);
|
|
|
+const loading = ref(false);
|
|
|
+
|
|
|
+const suggestionVisible = computed(() => {
|
|
|
+ // TEST
|
|
|
+ // return true
|
|
|
+ const isValidData = suggestions.value.length > 0;
|
|
|
+ return focused.value && (isValidData || loading.value);
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => input.value,
|
|
|
+ (val) => {
|
|
|
+ console.log(input$.value);
|
|
|
+ loading.value = !!val;
|
|
|
+ if (!val) {
|
|
|
+ suggestions.value = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const fetchSuggestions = (mock) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ // MOCK
|
|
|
+ // TODO should we use local search and gpt search together?
|
|
|
+ resolve(["vue", "react", mock]);
|
|
|
+ }, 1000);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+watchDebounced(
|
|
|
+ () => input.value,
|
|
|
+ async (taggedInput) => {
|
|
|
+ if (!taggedInput) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const result = await fetchSuggestions(taggedInput);
|
|
|
+ if (taggedInput === input.value) {
|
|
|
+ suggestions.value = result as unknown[];
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { debounce: 500 }
|
|
|
+);
|
|
|
+</script>
|
|
|
+<template>
|
|
|
+ <div class="search-container">
|
|
|
+ <el-popover
|
|
|
+ :visible="suggestionVisible"
|
|
|
+ :show-arrow="false"
|
|
|
+ :offset="0"
|
|
|
+ :teleported="false"
|
|
|
+ width="100%"
|
|
|
+ >
|
|
|
+ <template #reference>
|
|
|
+ <div class="search-trigger" :class="{ 'has-content': suggestionVisible }">
|
|
|
+ <el-input
|
|
|
+ :ref="(el) => (input$ = el)"
|
|
|
+ v-model="input"
|
|
|
+ clearable
|
|
|
+ :prefix-icon="Search"
|
|
|
+ :placeholder="t('请输入关键词,如:课程、协同、AI')"
|
|
|
+ ></el-input>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="search-content">
|
|
|
+ <template v-if="loading">
|
|
|
+ <span>loading</span>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <ul>
|
|
|
+ <li v-for="(suggest, _index) in suggestions" :key="_index">{{ suggest }}</li>
|
|
|
+ </ul>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </el-popover>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<i18n locale="zh-HK">
|
|
|
+{
|
|
|
+ "请输入关键词,如:课程、协同、AI": "TODO",
|
|
|
+}
|
|
|
+</i18n>
|
|
|
+<style lang="scss" scoped>
|
|
|
+.search-container {
|
|
|
+ width: 514px;
|
|
|
+ position: relative;
|
|
|
+ .search-trigger {
|
|
|
+ border: 1px solid #aeccfe;
|
|
|
+ padding: 1px;
|
|
|
+ width: 100%;
|
|
|
+ height: 52px;
|
|
|
+ border-radius: 26px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.2s;
|
|
|
+ :deep(.el-input) {
|
|
|
+ .el-input__wrapper {
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &:has(input:focus) {
|
|
|
+ border: none;
|
|
|
+ box-shadow: var(--el-box-shadow-light);
|
|
|
+ }
|
|
|
+ &.has-content {
|
|
|
+ border-bottom: 1px solid #e2eeff;
|
|
|
+ border-radius: 26px 26px 0 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ :deep(.el-popover) {
|
|
|
+ border-radius: 0 0 26px 26px;
|
|
|
+ border: none;
|
|
|
+ clip-path: inset(0px -10px -10px -10px);
|
|
|
+ }
|
|
|
+ .search-content {
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|