refactor(search): 搜索结果页补类型并去掉重复的 usePageSeo 调用
代码审查(standards)发现的唯一 should-fix 项:新增的搜索结果页通篇用 any, 而仓库既有 Article / CaseItem / Tender 类型且 sibling 页面一律在用。 - 引入 Article、CaseItem、Tender 类型,新增 SearchResponse / Section<T> 描述 /api/search 的响应形状(Section 用联合类型表达「成功 | 故障」, 让 `'error' in s` 的判别在类型层面成立) - section() 改为泛型 + 按 key 推导载荷类型,去掉 7 处 any - 补充注释说明为何用 useFetch 而非 useRequestFetch(匿名只读页无需转 Cookie,与各列表页一致) 同时处理一个 nitpick:search.vue 与 SearchResult.vue 重复调用 usePageSeo, 且页面层那次漏传 siteInfo(后写覆盖先写,重复设置只造成「哪次生效」的歧义)。 按 sibling 惯例只保留组件内那次(它带 siteInfo,且标题能带回关键词)。 行为未变:回归实测「消防」总数 9、23 条详情链接;「测试」标书命中 2 条、 价格 0.01 与截止时间正常渲染、默认 Tab 正确落在标书(第一个非空类)。 eslint 对该文件从 7 个 warning 降到 0。
This commit is contained in:
@@ -135,6 +135,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { Article, CaseItem } from '~/types'
|
||||
import type { Tender } from '~/types/tender'
|
||||
|
||||
/**
|
||||
* /api/search 的响应形状(见 server/api/search.get.ts)
|
||||
*
|
||||
* 某类上游失败时该类返回 `{ error: true }` 而非 `{ list: [] }`——前端据此把
|
||||
* 「接口故障」与「确实没有结果」区分开,不能混为一谈。
|
||||
*/
|
||||
type Section<T> = { list: T[]; count: number } | { error: true }
|
||||
|
||||
interface SearchResponse {
|
||||
article: Section<Article>
|
||||
case: Section<CaseItem>
|
||||
tender: Section<Tender>
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一搜索结果页 /search?keywords=xxx
|
||||
@@ -172,31 +188,38 @@ watch(keywords, (kw) => {
|
||||
|
||||
await fetchSiteInfo()
|
||||
|
||||
// 服务端/客户端都直接 await,避免先闪空白(与各列表页的做法一致)
|
||||
const { data } = await useFetch<any>('/api/search', {
|
||||
// 服务端/客户端都直接 await,避免先闪空白(与各列表页的做法一致)。
|
||||
// 用 useFetch(非裸 $fetch):SSR 期间它会转发请求头,租户中间件据此解析租户;
|
||||
// 本页是匿名只读页,不需要转发 Cookie,故与各列表页一致用 useFetch 即可。
|
||||
const { data } = await useFetch<SearchResponse>('/api/search', {
|
||||
key: `search-${keywords.value || 'empty'}`,
|
||||
query: { keywords: keywords.value || undefined },
|
||||
watch: [keywords]
|
||||
})
|
||||
|
||||
/** 取某一类的 { list, count };失败或缺失时返回 null */
|
||||
function section(key: SectionKey): { list: any[]; count: number } | null {
|
||||
const s = data.value?.[key]
|
||||
if (!s || s.error) return null
|
||||
/** 某一类的类型 */
|
||||
type SectionPayload<K extends SectionKey> =
|
||||
K extends 'article' ? Article : K extends 'case' ? CaseItem : Tender
|
||||
|
||||
/** 取某一类的 { list, count };该类失败或数据缺失时返回 null */
|
||||
function section<K extends SectionKey>(key: K): { list: SectionPayload<K>[]; count: number } | null {
|
||||
const s = data.value?.[key] as Section<SectionPayload<K>> | undefined
|
||||
if (!s || 'error' in s) return null
|
||||
return { list: Array.isArray(s.list) ? s.list : [], count: s.count ?? 0 }
|
||||
}
|
||||
|
||||
function isFailed(key: SectionKey): boolean {
|
||||
return !!data.value?.[key]?.error
|
||||
const s = data.value?.[key]
|
||||
return !!s && 'error' in s
|
||||
}
|
||||
|
||||
const articleSection = computed(() => section('article'))
|
||||
const caseSection = computed(() => section('case'))
|
||||
const tenderSection = computed(() => section('tender'))
|
||||
|
||||
const articleList = computed<any[]>(() => articleSection.value?.list ?? [])
|
||||
const caseList = computed<any[]>(() => caseSection.value?.list ?? [])
|
||||
const tenderList = computed<any[]>(() => tenderSection.value?.list ?? [])
|
||||
const articleList = computed(() => articleSection.value?.list ?? [])
|
||||
const caseList = computed(() => caseSection.value?.list ?? [])
|
||||
const tenderList = computed(() => tenderSection.value?.list ?? [])
|
||||
|
||||
/**
|
||||
* 整页"没有找到"态:**只有当三类都正常返回且都为空**时才显示。
|
||||
@@ -257,7 +280,8 @@ watch(keywords, () => {
|
||||
|
||||
const tabs = TAB_ORDER.map(key => ({ key, label: TAB_LABEL[key] }))
|
||||
|
||||
const activeList = computed<any[]>(() => {
|
||||
/** 当前 Tab 的条目列表;类型为三类之并集,模板内按 activeTab 分支渲染 */
|
||||
const activeList = computed<(Article | CaseItem | Tender)[]>(() => {
|
||||
if (activeTab.value === 'article') return articleList.value
|
||||
if (activeTab.value === 'case') return caseList.value
|
||||
return tenderList.value
|
||||
@@ -274,7 +298,7 @@ const moreLink = computed(() => {
|
||||
})
|
||||
|
||||
/** 文章摘要有两个可能字段名(上游 overview / summary),取第一个非空 */
|
||||
function summaryOf(item: any): string {
|
||||
function summaryOf(item: Article): string {
|
||||
return item?.overview || item?.summary || ''
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user