Browse Source

feat(page): enable live search and add popular articles section

main
Mohan 1 month ago
parent
commit
d9fecfdc3d
  1. 82
      src/views/SearchView.vue

82
src/views/SearchView.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { searchApi, tagApi } from '@/api/client'
import { articleApi, searchApi, tagApi } from '@/api/client'
import type { ArticleListItem, Tag } from '@/generated/api' import type { ArticleListItem, Tag } from '@/generated/api'
import ArticleCard from '@/components/ArticleCard.vue' import ArticleCard from '@/components/ArticleCard.vue'
import Pagination from '@/components/Pagination.vue' import Pagination from '@/components/Pagination.vue'
@ -18,6 +18,34 @@ const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const recommendedTags = ref<Tag[]>([]) const recommendedTags = ref<Tag[]>([])
const popularArticles = ref<ArticleListItem[]>([])
let debounceTimer: ReturnType<typeof setTimeout> | undefined
watch(keyword, () => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
page.value = 1
syncRoute()
doSearch()
}, 300)
})
watch(
() => route.query.q,
(q) => {
const next = (q as string) ?? ''
if (next !== keyword.value) keyword.value = next
}
)
function syncRoute() {
const q = keyword.value.trim()
const current = (route.query.q as string) ?? ''
if ((q || '') !== (current || '')) {
router.replace({ name: 'Search', query: q ? { q } : {} })
}
}
async function loadRecommendedTags() { async function loadRecommendedTags() {
try { try {
@ -30,6 +58,16 @@ async function loadRecommendedTags() {
} }
} }
async function loadPopularArticles() {
try {
const resp = await articleApi.apiArticlesPopularGet(5)
popularArticles.value = (resp.data.data as ArticleListItem[]) ?? []
} catch (e) {
console.error(e)
popularArticles.value = []
}
}
function goTag(tag: Tag) { function goTag(tag: Tag) {
router.push({ name: 'Home', query: { tag: tag.slug } }) router.push({ name: 'Home', query: { tag: tag.slug } })
} }
@ -57,9 +95,9 @@ async function doSearch() {
} }
function submit() { function submit() {
const q = keyword.value.trim()
clearTimeout(debounceTimer)
page.value = 1 page.value = 1
router.push({ name: 'Search', query: q ? { q } : {} })
syncRoute()
doSearch() doSearch()
} }
@ -68,17 +106,9 @@ function onPageChange(p: number) {
doSearch() doSearch()
} }
watch(
() => route.query.q,
(q) => {
keyword.value = (q as string) ?? ''
page.value = 1
doSearch()
}
)
onMounted(() => { onMounted(() => {
loadRecommendedTags() loadRecommendedTags()
loadPopularArticles()
if (keyword.value) doSearch() if (keyword.value) doSearch()
}) })
</script> </script>
@ -90,6 +120,7 @@ onMounted(() => {
<button class="search-btn" type="submit">搜索</button> <button class="search-btn" type="submit">搜索</button>
</form> </form>
<template v-if="!keyword">
<div v-if="recommendedTags.length" class="search-recommend"> <div v-if="recommendedTags.length" class="search-recommend">
<span class="section-label">标签推荐</span> <span class="section-label">标签推荐</span>
<div class="tag-chips"> <div class="tag-chips">
@ -99,16 +130,24 @@ onMounted(() => {
</div> </div>
</div> </div>
<div v-if="popularArticles.length" class="popular-section">
<span class="section-label">热门文章</span>
<div class="popular-list">
<ArticleCard v-for="article in popularArticles" :key="article.id" :article="article" />
</div>
</div>
</template>
<p v-if="error" class="error-text">{{ error }}</p> <p v-if="error" class="error-text">{{ error }}</p>
<p v-else-if="loading" class="hint-text">搜索中...</p> <p v-else-if="loading" class="hint-text">搜索中...</p>
<template v-else>
<p v-if="keyword && results.length === 0" class="hint-text">未找到相关文章</p>
<div v-else-if="results.length > 0">
<template v-else-if="keyword">
<p v-if="results.length === 0" class="hint-text">未找到相关文章</p>
<div v-else>
<ArticleCard v-for="article in results" :key="article.id" :article="article" /> <ArticleCard v-for="article in results" :key="article.id" :article="article" />
<Pagination :page="page" :total-pages="totalPages" @change="onPageChange" /> <Pagination :page="page" :total-pages="totalPages" @change="onPageChange" />
</div> </div>
<p v-else class="hint-text">输入关键词开始搜索</p>
</template> </template>
<p v-else class="hint-text">输入关键词开始搜索</p>
</div> </div>
</template> </template>
@ -127,6 +166,17 @@ onMounted(() => {
margin-bottom: 20px; margin-bottom: 20px;
} }
.popular-section {
margin-bottom: 20px;
}
.popular-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 10px;
}
.section-label { .section-label {
color: var(--ac-text-secondary); color: var(--ac-text-secondary);
font-size: 13px; font-size: 13px;

Loading…
Cancel
Save