Skip to content

Commit 3e3d7dd

Browse files
yyyCodeclaude
andcommitted
feat: add frontend search page, API layer, and nav entry
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7224598 commit 3e3d7dd

4 files changed

Lines changed: 201 additions & 0 deletions

File tree

vue/src/api/search.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { request } from './http'
2+
3+
export function searchArticles(keyword, page = 0, size = 20) {
4+
return request(
5+
`/api/v1/articles/search?keyword=${encodeURIComponent(keyword)}&page=${page}&size=${size}`
6+
)
7+
}

vue/src/components/BlogHeader.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
文章
2626
</router-link>
2727

28+
<router-link to="/search" class="site-nav-link" active-class="active">
29+
<span class="site-nav-ico" aria-hidden="true">
30+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
31+
<circle cx="11" cy="11" r="8" />
32+
<path d="M21 21l-4.35-4.35" />
33+
</svg>
34+
</span>
35+
搜索
36+
</router-link>
37+
2838
<router-link to="/feedback" class="site-nav-link" active-class="active">
2939
<span class="site-nav-ico" aria-hidden="true">
3040
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">

vue/src/router/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ChangelogListView from '../views/ChangelogListView.vue'
1010
import ChangelogDetailView from '../views/ChangelogDetailView.vue'
1111
import AboutView from '../views/AboutView.vue'
1212
import FeedbackView from '../views/FeedbackView.vue'
13+
import SearchResultsView from '../views/SearchResultsView.vue'
1314
import SiteAuthView from '../views/SiteAuthView.vue'
1415
import ConsoleLayout from '../layouts/ConsoleLayout.vue'
1516
import ConsoleDashboardView from '../views/ConsoleDashboardView.vue'
@@ -43,6 +44,11 @@ const routes = [
4344
name: 'about',
4445
component: AboutView
4546
},
47+
{
48+
path: '/search',
49+
name: 'search',
50+
component: SearchResultsView
51+
},
4652
{
4753
path: '/feedback',
4854
name: 'feedback',
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<template>
2+
<div class="search-results-page">
3+
<div class="page-grid">
4+
<main class="page-main">
5+
<div class="search-header">
6+
<h1 class="search-heading">搜索结果</h1>
7+
<p v-if="keyword" class="search-keyword">
8+
关键词:<strong>{{ keyword }}</strong>
9+
<span v-if="total > 0" class="search-total">,共 {{ total }} 篇</span>
10+
</p>
11+
</div>
12+
13+
<div v-if="loading" class="search-status">搜索中...</div>
14+
15+
<div v-else-if="error" class="search-status search-status-error">
16+
{{ error }}
17+
</div>
18+
19+
<div v-else-if="items.length === 0" class="search-status">
20+
<template v-if="keyword">未找到与 "{{ keyword }}" 相关的文章</template>
21+
<template v-else>请输入搜索关键词</template>
22+
</div>
23+
24+
<div v-else class="article-list">
25+
<ArticleCard v-for="article in items" :key="article.id" :article="article" />
26+
</div>
27+
28+
<div v-if="total > size" class="search-pager">
29+
<button
30+
class="btn"
31+
:disabled="page <= 0"
32+
@click="goPage(page - 1)"
33+
>
34+
上一页
35+
</button>
36+
<span class="search-pager-info">{{ page + 1 }} / {{ totalPages }}</span>
37+
<button
38+
class="btn"
39+
:disabled="page + 1 >= totalPages"
40+
@click="goPage(page + 1)"
41+
>
42+
下一页
43+
</button>
44+
</div>
45+
</main>
46+
47+
<aside class="page-side">
48+
<ProfileCard />
49+
<BlogInfoCard />
50+
</aside>
51+
</div>
52+
</div>
53+
</template>
54+
55+
<script setup>
56+
import { computed, onMounted, ref, watch } from 'vue'
57+
import { useRoute, useRouter } from 'vue-router'
58+
import { searchArticles } from '../api/search'
59+
import ArticleCard from '../components/ArticleCard.vue'
60+
import ProfileCard from '../components/ProfileCard.vue'
61+
import BlogInfoCard from '../components/BlogInfoCard.vue'
62+
63+
const route = useRoute()
64+
const router = useRouter()
65+
66+
const loading = ref(false)
67+
const error = ref('')
68+
const items = ref([])
69+
const total = ref(0)
70+
const page = ref(0)
71+
const size = 20
72+
73+
const keyword = computed(() => {
74+
const q = route.query.q
75+
return typeof q === 'string' ? q.trim() : ''
76+
})
77+
78+
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / size)))
79+
80+
async function doSearch() {
81+
const kw = keyword.value
82+
if (!kw) {
83+
items.value = []
84+
total.value = 0
85+
return
86+
}
87+
loading.value = true
88+
error.value = ''
89+
try {
90+
const result = await searchArticles(kw, page.value, size)
91+
items.value = result.items || []
92+
total.value = result.total || 0
93+
} catch (e) {
94+
error.value = e.message || '搜索失败'
95+
items.value = []
96+
total.value = 0
97+
} finally {
98+
loading.value = false
99+
}
100+
}
101+
102+
function goPage(p) {
103+
page.value = p
104+
doSearch()
105+
}
106+
107+
watch(keyword, () => {
108+
page.value = 0
109+
doSearch()
110+
})
111+
112+
onMounted(() => {
113+
doSearch()
114+
})
115+
</script>
116+
117+
<style scoped>
118+
.search-results-page {
119+
padding: 24px 0 48px;
120+
}
121+
122+
.search-header {
123+
margin-bottom: 28px;
124+
}
125+
126+
.search-heading {
127+
font-size: 24px;
128+
font-weight: 700;
129+
color: var(--text);
130+
margin: 0 0 8px;
131+
}
132+
133+
.search-keyword {
134+
font-size: 14px;
135+
color: var(--muted);
136+
margin: 0;
137+
}
138+
139+
.search-keyword strong {
140+
color: var(--accent);
141+
}
142+
143+
.search-total {
144+
color: var(--muted);
145+
}
146+
147+
.search-status {
148+
color: var(--muted);
149+
font-size: 14px;
150+
padding: 32px 0;
151+
text-align: center;
152+
}
153+
154+
.search-status-error {
155+
color: #cf1322;
156+
}
157+
158+
.article-list {
159+
display: flex;
160+
flex-direction: column;
161+
gap: 0;
162+
}
163+
164+
.search-pager {
165+
display: flex;
166+
align-items: center;
167+
justify-content: center;
168+
gap: 16px;
169+
margin-top: 28px;
170+
}
171+
172+
.search-pager-info {
173+
font-size: 13px;
174+
color: var(--muted);
175+
min-width: 60px;
176+
text-align: center;
177+
}
178+
</style>

0 commit comments

Comments
 (0)