热搜功能实现

1. 新增热搜类型定义 HotSearchItem 接口
2. 新增通用响应类型 ApiResponse 和 AxiosResponse 接口
3. 实现热搜列表展示功能
4. 实现搜索历史记录保存和展示功能
5. 优化搜索逻辑,添加搜索后状态更新
This commit is contained in:
Huangzhe
2025-05-14 10:51:00 +08:00
parent 7a5670ac66
commit cc008ab99c
11 changed files with 258 additions and 27 deletions

View File

@@ -32,7 +32,7 @@
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "Vue.volar",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"css.validate": false, //用来校验CSS文件中的语法错误和潜在的问题 "css.validate": false, //用来校验CSS文件中的语法错误和潜在的问题

5
components.d.ts vendored
View File

@@ -33,7 +33,6 @@ declare module 'vue' {
RichText: typeof import('./src/components/RichText.vue')['default'] RichText: typeof import('./src/components/RichText.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SimpleLayout: typeof import('./src/components/Layout/SimpleLayout.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet'] VanActionSheet: typeof import('vant/es')['ActionSheet']
VanButton: typeof import('vant/es')['Button'] VanButton: typeof import('vant/es')['Button']
VanCard: typeof import('vant/es')['Card'] VanCard: typeof import('vant/es')['Card']
@@ -41,11 +40,8 @@ declare module 'vue' {
VanCellGroup: typeof import('vant/es')['CellGroup'] VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox'] VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup'] VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanCol: typeof import('vant/es')['Col']
VanDivider: typeof import('vant/es')['Divider'] VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field'] VanField: typeof import('vant/es')['Field']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon'] VanIcon: typeof import('vant/es')['Icon']
VanList: typeof import('vant/es')['List'] VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar'] VanNavBar: typeof import('vant/es')['NavBar']
@@ -54,7 +50,6 @@ declare module 'vue' {
VanPopup: typeof import('vant/es')['Popup'] VanPopup: typeof import('vant/es')['Popup']
VanRadio: typeof import('vant/es')['Radio'] VanRadio: typeof import('vant/es')['Radio']
VanRadioGroup: typeof import('vant/es')['RadioGroup'] VanRadioGroup: typeof import('vant/es')['RadioGroup']
VanRow: typeof import('vant/es')['Row']
VanSearch: typeof import('vant/es')['Search'] VanSearch: typeof import('vant/es')['Search']
VanSpace: typeof import('vant/es')['Space'] VanSpace: typeof import('vant/es')['Space']
VanStepper: typeof import('vant/es')['Stepper'] VanStepper: typeof import('vant/es')['Stepper']

View File

@@ -75,3 +75,14 @@ export function saveTemplates(sn, data) {
data data
}); });
} }
/**
* 获取推荐列表
* @returns { import('src/api/types/response').HotSearchResponse}
*/
export function hotSearch() {
return request({
url: `/console/hot_search/list`,
method: 'post',
});
}

38
src/api/types/hotSearch.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
/**
* 热搜项类型定义
*/
export interface HotSearchItem {
/** 热搜ID */
id: number;
/** 热搜序列号 */
sn: string;
/** 热搜状态 */
status: number;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
/** 删除时间为null表示未删除 */
deleted_at: null | string;
/** 热搜关键词 */
key_word: string;
/** 显示顺序 */
display_order: number;
/** 创建用户ID */
created_user_id: number;
/** 更新用户ID */
updated_user_id: number;
}
/**
* 热搜列表响应类型
*/
export interface HotSearchListResponse {
list: HotSearchItem[];
total?: number;
}
/**
* 兼容旧类型
*/
type HotSearch = HotSearchItem;

95
src/api/types/response.d.ts vendored Normal file
View File

@@ -0,0 +1,95 @@
import { HotSearchItem } from './hotSearch';
/**
* 分页元数据接口
*/
export interface MetaData {
/** 起始记录 */
from: number;
/** 总记录数 */
total: number;
/** 当前页码 */
current_page: number;
/** 最后页码 */
last_page: number;
/** 每页记录数 */
per_page: number;
}
/**
* 通用API响应接口
*/
export interface ApiResponse<T> {
/** 响应状态码0表示成功 */
code: number;
/** 响应消息成功时为null */
message: string | null;
/** 响应数据 */
data: T;
/** 分页元数据,可选 */
meta?: MetaData;
}
/**
* Axios响应头部接口
*/
export interface AxiosResponseHeaders {
'content-type': string;
[key: string]: string;
}
/**
* Axios配置接口
*/
export interface AxiosConfig {
transitional: {
silentJSONParsing: boolean;
forcedJSONParsing: boolean;
clarifyTimeoutError: boolean;
};
adapter: string[];
transformRequest: any[];
transformResponse: any[];
timeout: number;
xsrfCookieName: string;
xsrfHeaderName: string;
maxContentLength: number;
maxBodyLength: number;
env: Record<string, any>;
headers: {
Accept: string;
'Content-Type': string;
Authorization: string;
Source: string;
remoteIp: string;
[key: string]: string;
};
baseURL: string;
url: string;
method: string;
allowAbsoluteUrls: boolean;
[key: string]: any;
}
/**
* 完整的Axios响应接口
*/
export interface AxiosResponse<T> {
/** 响应数据 */
data: ApiResponse<T>;
/** HTTP状态码 */
status: number;
/** HTTP状态文本 */
statusText: string;
/** 响应头 */
headers: AxiosResponseHeaders;
/** 请求配置 */
config: AxiosConfig;
/** 请求对象 */
request: Record<string, any>;
}
/**
* 热搜列表响应类型
*/
export type HotSearchResponse = AxiosResponse<HotSearchItem[]>;

View File

@@ -48,7 +48,7 @@ function useSetPieChart(
// 检测边界范围 dom 和 data 是否存在 // 检测边界范围 dom 和 data 是否存在
onMounted(() => { onMounted(() => {
console.log(dom); // console.log(dom);
if (!dom || data.length === 0) return; if (!dom || data.length === 0) return;
// 在 dom 挂载之后,显示饼图 // 在 dom 挂载之后,显示饼图
pieChart.value = chart.init(dom.value); pieChart.value = chart.init(dom.value);

View File

@@ -1,5 +1,6 @@
import { nextTick, ref, watch } from 'vue'; import { nextTick, ref, watch } from 'vue';
import { getSurveysPage } from '@/api/home'; import { getSurveysPage } from '@/api/home';
import { saveSearchHistory } from "@/views/HomeSearch/components/Recommend/hooks/useRecommend"
// 问卷 // 问卷
const surveys = ref([]); const surveys = ref([]);
@@ -17,7 +18,7 @@ const dirty = ref(true);
// loading 状态 // loading 状态
const loading = ref(false); const loading = ref(false);
async function handleSearch() { async function handleSearch () {
// loading 状态开启 // loading 状态开启
// loading.value = true; // loading.value = true;
const params = { const params = {
@@ -43,21 +44,25 @@ async function handleSearch() {
/** /**
* 更新 搜索关键字信息 * 更新 搜索关键字信息
*/ */
async function updateKeyword() { async function updateKeyword () {
// 排除边界条件 // 排除边界条件
if (!keyword.value) return; if (!keyword.value) return;
// 重新获取数据 // 重新获取数据
await handleSearch(); await handleSearch();
// 处理后置条件,点击搜索之后,索引更新, // 处理后置条件,点击搜索之后,索引更新,
index.value = 1; index.value = 1;
// 把关键词添加到 localStorage 中
saveSearchHistory(keyword.value)
// 打开页面展示状态
loading.value = true
} }
/** /**
* *
* @param value * @param value
*/ */
function updatePageCount(value: number) { function updatePageCount (value: number) {
pageCount.value = value; pageCount.value = value;
} }
@@ -78,3 +83,4 @@ watch(index, async () => {
}); });
export { keyword, updateKeyword as handleSearch, lastIndex, loading, surveys, index }; export { keyword, updateKeyword as handleSearch, lastIndex, loading, surveys, index };

View File

@@ -2,9 +2,16 @@
import Search from '@/components/Search/Index.vue'; import Search from '@/components/Search/Index.vue';
import TemplateMarket from '@/views/HomeSearch/components/TemplateMarket/Index.vue'; import TemplateMarket from '@/views/HomeSearch/components/TemplateMarket/Index.vue';
import MineSurvey from '@/views/HomeSearch/components/MineSurvey/Index.vue'; import MineSurvey from '@/views/HomeSearch/components/MineSurvey/Index.vue';
import { handleSearch, keyword } from '@/views/HomeSearch/Hooks/useSurveySearch'; import { handleSearch, keyword, loading } from '@/views/HomeSearch/Hooks/useSurveySearch';
import Layout from '@/components/Layout/CommonLayout.vue'; import Layout from '@/components/Layout/CommonLayout.vue';
import { visible } from '@/views/HomeSearch/Hooks/useHomeSearch'; import { visible } from '@/views/HomeSearch/Hooks/useHomeSearch';
import RecommendTag from '@/views/HomeSearch/components/Recommend/Index.vue';
import { onMounted } from 'vue';
onMounted(() => {
// 当页面取消挂载时,重置页面展示的状态
loading.value = false
})
</script> </script>
<template> <template>
@@ -12,24 +19,31 @@ import { visible } from '@/views/HomeSearch/Hooks/useHomeSearch';
<search v-model:value="keyword" :search="handleSearch" /> <search v-model:value="keyword" :search="handleSearch" />
<!-- 广告区域 --> <!-- 广告区域 -->
<!-- 我的问卷区域 --> <section v-if="loading">
<layout v-if="visible.mineSurvey" title="我的任务"> <!-- 我的问卷区域 -->
<template #content> <layout v-if="visible.mineSurvey" title="我的任务">
<mine-survey /> <template #content>
</template> <mine-survey />
</layout> </template>
<!-- 更多模板区域 --> </layout>
<layout v-if="visible.templateMarket" title="问卷模板"> <!-- 更多模板区域 -->
<template #content> <layout v-if="visible.templateMarket" title="问卷模板">
<template-market /> <template #content>
</template> <template-market />
</layout> </template>
</layout>
</section>
<section v-else>
<recommend-tag></recommend-tag>
</section>
<!-- <div class="search-result">--> <!-- <div class="search-result">-->
<!-- <div class="search-result-item" v-for="item in searchResult" :key="item.id">--> <!-- <div class="search-result-item" v-for="item in searchResult" :key="item.id">-->
<!-- {{ item.name }}--> <!-- {{ item.name }}-->
<!-- </div>--> <!-- </div>-->
<!-- </div>--> <!-- </div>-->
</section> </section>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import Layout from "@/components/Layout/CommonLayout.vue";
import { list, useFetchRecommon, history } from "./hooks/useRecommend";
useFetchRecommon()
</script>
<template>
<section class="recommend-container">
<layout title="热门搜索">
<template #content>
<el-space>
<el-tag v-for="tag in list">{{ tag.key_word }}</el-tag>
</el-space>
</template>
</layout>
<layout title="搜索历史">
<template #content>
<el-space>
<el-tag v-for="tag in history">{{ tag }}</el-tag>
</el-space>
</template>
</layout>
</section>
</template>
<style scoped lang="scss">
@use "@/assets/css/theme";
.recommend-container {
margin-top: 20px;
padding: 0 theme.$card-radius;
}
</style>

View File

@@ -0,0 +1,36 @@
import { hotSearch } from "@/api/home";
import type { HotSearch } from "@/api/types/hotSearch";
import { ref } from "vue";
// 从服务器接受的列表内容
const list = ref<HotSearch[]>([])
// 历史列表
const history = ref<Set<string>>()
initialHistory()
async function useFetchRecommon () {
const { data } = await hotSearch()
list.value = data.data
}
// 初始化历史
function initialHistory () {
const historyStr = localStorage.getItem('history')
if (historyStr) {
history.value = new Set(JSON.parse(historyStr))
} else {
history.value = new Set()
}
}
// 保存历史
function saveSearchHistory (val: string) {
if (!history.value) {
history.value = new Set()
}
history.value.add(val)
localStorage.setItem('history', JSON.stringify(Array.from(history.value)))
}
export { useFetchRecommon, list,saveSearchHistory, history }

View File

@@ -10,7 +10,7 @@ import postCssPxToRem from 'postcss-pxtorem';
import legacy from '@vitejs/plugin-legacy'; import legacy from '@vitejs/plugin-legacy';
// shift + alt 快速定位到对应组件 // shift + alt 快速定位到对应组件
import { codeInspectorPlugin } from 'code-inspector-plugin'; import { codeInspectorPlugin } from 'code-inspector-plugin';
import tailwindcss from '@tailwindcss/vite'; // import tailwindcss from '@tailwindcss/vite';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
// 接收 mode 参数 // 接收 mode 参数
@@ -77,7 +77,7 @@ export default defineConfig(({ mode }) => {
codeInspectorPlugin({ codeInspectorPlugin({
bundler: 'vite' bundler: 'vite'
}), }),
tailwindcss() // tailwindcss()
], ],
resolve: { resolve: {
alias: { alias: {