增加搜索功能
This commit is contained in:
parent
b86941fc9c
commit
cd00fadea3
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@ -22,7 +22,9 @@ declare module 'vue' {
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
BIN
frontend/public/default.png
Normal file
BIN
frontend/public/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
16
frontend/src/api/search.ts
Normal file
16
frontend/src/api/search.ts
Normal file
@ -0,0 +1,16 @@
|
||||
//进行接口API的统一管理
|
||||
import { request } from "./axios";
|
||||
|
||||
export class search {
|
||||
/**
|
||||
* @description 首页搜索数据
|
||||
* @param {string} keyword - 关键字
|
||||
* @param {string} type - 类型
|
||||
* @param {number} page - 页码
|
||||
* @param {number} limit - 每页条数
|
||||
* @return {Promise} 返回请求结果
|
||||
*/
|
||||
static async getArticleDetail(keyword: string, type: string, page: number = 1, limit: number = 10) {
|
||||
return request("/index/search/apiSearch", { keyword, type, page, limit }, "get");
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,8 @@
|
||||
:root {
|
||||
--white:#fff;
|
||||
--primary-color: #409eff;
|
||||
--primary-hover-color: #3c4ccf;
|
||||
--primary-light-bg: #e7f3ff;
|
||||
--success-color: #67c23a;
|
||||
--warning-color: #e6a23c;
|
||||
--danger-color: #f56c6c;
|
||||
@ -21,6 +23,7 @@
|
||||
|
||||
--background-color: #f5f5f5;
|
||||
--background-color-page: #f0f2f5;
|
||||
--background-color-hover: #f7f8fd;
|
||||
}
|
||||
|
||||
// 全局重置样式
|
||||
|
||||
@ -47,6 +47,11 @@ const router = createRouter({
|
||||
name: "downloadGames",
|
||||
component: () => import("@/views/downloadGames/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/search",
|
||||
name: "search",
|
||||
component: () => import("@/views/components/search.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -81,7 +81,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import defaultImage from "@/assets/imgs/default.png";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Props定义
|
||||
interface ResourceConfig {
|
||||
@ -115,7 +119,7 @@ const currentCategoryName = computed(() => {
|
||||
|
||||
// 获取图片地址
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
||||
if (!imagePath) return defaultImage;
|
||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||
};
|
||||
|
||||
@ -130,9 +134,14 @@ const fetchCategories = async () => {
|
||||
Array.isArray(response.data.data)
|
||||
) {
|
||||
categories.value = response.data.data;
|
||||
// 默认选择第一个分类
|
||||
// 检查路由参数中的category ID,如果存在则选择对应分类,否则选择第一个分类
|
||||
if (categories.value.length > 0) {
|
||||
selectedCategory.value = categories.value[0].id;
|
||||
const categoryIdFromRoute = route.query.category as string;
|
||||
if (categoryIdFromRoute && categories.value.some(cat => cat.id === categoryIdFromRoute)) {
|
||||
selectedCategory.value = categoryIdFromRoute;
|
||||
} else {
|
||||
selectedCategory.value = categories.value[0].id;
|
||||
}
|
||||
fetchResources();
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -5,6 +5,7 @@ import { ElMessage } from "element-plus";
|
||||
import Header from "@/views/components/header.vue";
|
||||
import Footer from "@/views/components/footer.vue";
|
||||
import { article } from "@/api/article";
|
||||
import defaultImage from "@/assets/imgs/default.png";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -14,7 +15,9 @@ const loading = ref(true);
|
||||
|
||||
// 获取图片地址
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
||||
if (!imagePath) {
|
||||
return defaultImage;
|
||||
}
|
||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||
};
|
||||
|
||||
@ -56,6 +59,10 @@ const fetchArticleDetail = async () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 确保defaultImage被使用,防止tree-shaking
|
||||
if (defaultImage) {
|
||||
console.log('Default image loaded:', defaultImage);
|
||||
}
|
||||
fetchArticleDetail();
|
||||
});
|
||||
</script>
|
||||
@ -537,6 +544,11 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
margin: 30px 0;
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { ElMessage } from "element-plus";
|
||||
import Header from "@/views/components/header.vue";
|
||||
import Footer from "@/views/components/footer.vue";
|
||||
import { resource as resourceApi } from "@/api/resource";
|
||||
import defaultImage from "@/assets/imgs/default.png";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter(); // 新增:引入useRouter
|
||||
@ -27,7 +28,9 @@ const pageInfo = computed(() => ({
|
||||
|
||||
// 获取图片地址
|
||||
const getImageUrl = (imagePath: string) => {
|
||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
||||
if (!imagePath) {
|
||||
return defaultImage;
|
||||
}
|
||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||
};
|
||||
|
||||
@ -158,14 +161,12 @@ const fetchResourceDetail = async () => {
|
||||
|
||||
try {
|
||||
const response: any = await resourceApi.getResourceDetail(resourceId.value);
|
||||
|
||||
console.log("获取资源详情响应:", response);
|
||||
if (response?.data?.code === 0 && response?.data?.data) {
|
||||
resourceData.value = response.data.data.resource;
|
||||
if (response.data.code === 0) {
|
||||
resourceData.value = response.data.data;
|
||||
console.log(resourceData.value);
|
||||
prevResource.value = response.data.data.prev_resource;
|
||||
nextResource.value = response.data.data.next_resource;
|
||||
relatedResources.value = response.data.data.related_resources || [];
|
||||
console.log("资源数据加载完成:", resourceData.value?.title);
|
||||
} else {
|
||||
console.error("API响应格式错误:", response);
|
||||
ElMessage.error("获取资源详情失败:数据格式错误");
|
||||
@ -196,8 +197,6 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 移除原有的fetchResourceDetail调用,由watch接管
|
||||
// 页面加载时滚动到顶部
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
</script>
|
||||
@ -217,9 +216,7 @@ onMounted(() => {
|
||||
<el-breadcrumb-item :to="{ path: '/' }"
|
||||
>首页</el-breadcrumb-item
|
||||
>
|
||||
<el-breadcrumb-item :to="{ path: pageInfo.path }">{{
|
||||
pageInfo.name
|
||||
}}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ resourceData.cate }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>详情</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
@ -332,6 +329,11 @@ onMounted(() => {
|
||||
@click="handleContentClick"
|
||||
></div>
|
||||
|
||||
<div class="py-5" style="color: #ccc; text-align: center">
|
||||
-------- THE END --------
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div class="disclaimers">
|
||||
<div class="disclaimer-item">
|
||||
<div class="disclaimer-title">免责声明:</div>
|
||||
@ -519,7 +521,7 @@ onMounted(() => {
|
||||
.top-content {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: #0081ff;
|
||||
background-color: var(--primary-color);
|
||||
position: relative;
|
||||
|
||||
.top-content-main {
|
||||
@ -611,10 +613,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.value {
|
||||
border: 1px #0d6efd dashed;
|
||||
border: 1px var(--primary-color) dashed;
|
||||
padding: 3px 6px;
|
||||
font-size: 13px;
|
||||
background-color: #0081ff12;
|
||||
background-color: var(--primary-light-bg);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
@ -626,11 +628,11 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
|
||||
.codebtn {
|
||||
color: #0d6efd;
|
||||
color: var(--primary-color);
|
||||
margin-left: 20px;
|
||||
padding: 6px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #0d6efd;
|
||||
border: 1px solid var(--primary-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #fff;
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowUp } from "@element-plus/icons-vue";
|
||||
|
||||
// 返回顶部功能
|
||||
function goToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="footer">
|
||||
@ -9,7 +18,7 @@
|
||||
<div class="mr-20">
|
||||
<img src="@/assets/imgs/logo-light.png" alt="Logo" height="70" />
|
||||
<p class="text-white-50 my-4 f18" style="width: 400px">
|
||||
美天智能科技,这里是介绍!
|
||||
这里是介绍!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -85,6 +94,13 @@
|
||||
</div>
|
||||
<div class="tongji"></div>
|
||||
</section>
|
||||
|
||||
<!-- 回到顶部按钮 -->
|
||||
<div class="go-to-top" @click="goToTop">
|
||||
<el-icon class="go-to-top-icon">
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
@ -279,4 +295,47 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回到顶部按钮样式
|
||||
.go-to-top {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: var(--primary-color, #409eff);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover-color, #337ecc);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.go-to-top-icon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - 小屏幕调整位置
|
||||
@media (max-width: 768px) {
|
||||
.go-to-top {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
|
||||
.go-to-top-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { ref, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Search, User, Setting, SwitchButton } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ElMessage, ElDialog } from "element-plus";
|
||||
|
||||
// 响应式数据
|
||||
const showSearch = ref(false);
|
||||
const showSearchDialog = ref(false);
|
||||
const searchText = ref("");
|
||||
const searchType = ref("articles");
|
||||
const searchInput = ref();
|
||||
const username = ref("管理员"); // 这里可以从 store 获取
|
||||
const router = useRouter();
|
||||
|
||||
// 搜索类型选项
|
||||
const searchTypeOptions = [
|
||||
{ value: "articles", label: "文章" },
|
||||
{ value: "resources", label: "资源" },
|
||||
];
|
||||
|
||||
// 静态菜单配置
|
||||
interface MenuItem {
|
||||
@ -26,14 +36,34 @@ const staticMenuItems: MenuItem[] = [
|
||||
// 方法
|
||||
const handleSearch = () => {
|
||||
if (searchText.value.trim()) {
|
||||
console.log("搜索:", searchText.value);
|
||||
// TODO: 实现搜索逻辑
|
||||
// 搜索完成后可以隐藏搜索框
|
||||
showSearch.value = false;
|
||||
// 跳转到搜索页面,并传递搜索关键词和类型
|
||||
router.push({
|
||||
path: "/search",
|
||||
query: {
|
||||
keyword: searchText.value.trim(),
|
||||
type: searchType.value,
|
||||
},
|
||||
replace: true, // 替换当前历史记录,避免页面刷新
|
||||
});
|
||||
// 关闭搜索弹窗
|
||||
showSearchDialog.value = false;
|
||||
searchText.value = "";
|
||||
searchType.value = "articles"; // 重置为默认值
|
||||
}
|
||||
};
|
||||
|
||||
const openSearchDialog = async () => {
|
||||
showSearchDialog.value = true;
|
||||
await nextTick();
|
||||
searchInput.value?.focus();
|
||||
};
|
||||
|
||||
const closeSearchDialog = () => {
|
||||
showSearchDialog.value = false;
|
||||
searchText.value = "";
|
||||
searchType.value = "articles";
|
||||
};
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
switch (command) {
|
||||
case "profile":
|
||||
@ -85,24 +115,10 @@ const handleCommand = (command: string) => {
|
||||
<!-- 右侧工具栏 -->
|
||||
<div class="header-right">
|
||||
<!-- 搜索按钮 -->
|
||||
<el-button link class="search-btn" @click="showSearch = !showSearch">
|
||||
<el-button link class="search-btn" @click="openSearchDialog">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
|
||||
<!-- 搜索框(可展开) -->
|
||||
<el-input
|
||||
v-if="showSearch"
|
||||
v-model="searchText"
|
||||
placeholder="搜索..."
|
||||
size="small"
|
||||
class="search-input"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info">
|
||||
<span class="username">{{ username }}</span>
|
||||
@ -130,6 +146,57 @@ const handleCommand = (command: string) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showSearchDialog"
|
||||
title="搜索"
|
||||
width="600px"
|
||||
:before-close="closeSearchDialog"
|
||||
center
|
||||
>
|
||||
<div class="search-dialog-content">
|
||||
<div class="search-form-row">
|
||||
<div class="search-form-item">
|
||||
<el-select
|
||||
v-model="searchType"
|
||||
placeholder="选择类型"
|
||||
size="large"
|
||||
class="search-type-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in searchTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="search-form-item">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="请输入搜索关键词..."
|
||||
size="large"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
ref="searchInput"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeSearchDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -181,13 +248,13 @@ const handleCommand = (command: string) => {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
color: var(--primary-color);
|
||||
background: var(--primary-light-bg);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
color: #409eff;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
color: var(--primary-color);
|
||||
background: var(--primary-light-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,30 +265,28 @@ const handleCommand = (command: string) => {
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.search-btn {
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.username {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
@ -233,7 +298,7 @@ const handleCommand = (command: string) => {
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -266,11 +331,6 @@ const handleCommand = (command: string) => {
|
||||
.header-right {
|
||||
gap: 8px;
|
||||
|
||||
.search-input {
|
||||
width: 150px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none; // 小屏幕隐藏用户名
|
||||
}
|
||||
@ -288,4 +348,67 @@ const handleCommand = (command: string) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索弹窗样式
|
||||
.search-dialog-content {
|
||||
padding: 20px 15px;
|
||||
|
||||
.search-form-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.search-form-item {
|
||||
&:first-child {
|
||||
flex: 0 0 20%; // 4/10 = 40%
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex: 0 0 80%; // 6/10 = 60%
|
||||
}
|
||||
|
||||
.search-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-type-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
.el-input__inner {
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// 搜索弹窗响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.search-dialog-content {
|
||||
.search-form-row {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.search-form-item {
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
332
frontend/src/views/components/search.vue
Normal file
332
frontend/src/views/components/search.vue
Normal file
@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import Header from "@/views/components/header.vue";
|
||||
import Footer from "@/views/components/footer.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { search } from "@/api/search";
|
||||
|
||||
// 搜索关键词、结果、加载状态
|
||||
const searchText = ref("");
|
||||
const searchType = ref("articles");
|
||||
const loading = ref(false);
|
||||
const results = ref<any[]>([]);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const hasSearched = ref(false);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const totalPages = ref(0);
|
||||
|
||||
// 搜索类型选项
|
||||
const searchTypeOptions = [
|
||||
{ value: "articles", label: "文章" },
|
||||
{ value: "resources", label: "资源" },
|
||||
];
|
||||
|
||||
const handleSearch = async (page: number = 1) => {
|
||||
if (!searchText.value.trim()) {
|
||||
ElMessage.warning("请输入搜索关键词");
|
||||
return;
|
||||
}
|
||||
results.value = [];
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await search.getArticleDetail(
|
||||
searchText.value.trim(),
|
||||
searchType.value,
|
||||
page,
|
||||
pageSize.value
|
||||
);
|
||||
results.value = res.data.data?.items || [];
|
||||
total.value = res.data.data?.total || 0;
|
||||
totalPages.value = res.data.data?.total_pages || 0;
|
||||
currentPage.value = page;
|
||||
hasSearched.value = true;
|
||||
|
||||
// 确保分页数据正确
|
||||
if (total.value > 0 && totalPages.value <= 1) {
|
||||
totalPages.value = Math.ceil(total.value / pageSize.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("搜索失败:", error);
|
||||
ElMessage.error("搜索失败,请稍后重试");
|
||||
results.value = [];
|
||||
total.value = 0;
|
||||
totalPages.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleResultClick = (item: any) => {
|
||||
// 根据搜索类型跳转到对应的详情页
|
||||
if (searchType.value === "articles") {
|
||||
router.push({ path: "/article", query: { id: item.id } });
|
||||
} else if (searchType.value === "resources") {
|
||||
router.push({ path: "/resource", query: { id: item.id } });
|
||||
}
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handleCurrentChange = (page: number) => {
|
||||
handleSearch(page);
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pageSize.value = size;
|
||||
handleSearch(1);
|
||||
};
|
||||
|
||||
// 如果支持路由传参,可以自动填充搜索内容
|
||||
if (route.query.keyword) {
|
||||
searchText.value = String(route.query.keyword);
|
||||
if (route.query.type) {
|
||||
searchType.value = String(route.query.type);
|
||||
}
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
// 监听路由参数变化,自动重新搜索
|
||||
watch(
|
||||
() => route.query,
|
||||
(newQuery) => {
|
||||
if (newQuery.keyword) {
|
||||
const keyword = String(newQuery.keyword);
|
||||
const type = newQuery.type ? String(newQuery.type) : "articles";
|
||||
|
||||
// 只有当参数真正变化时才重新搜索
|
||||
if (keyword !== searchText.value || type !== searchType.value) {
|
||||
searchText.value = keyword;
|
||||
searchType.value = type;
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header />
|
||||
|
||||
<div class="search-page-container">
|
||||
<div class="search-bar">
|
||||
<el-select
|
||||
v-model="searchType"
|
||||
placeholder="选择类型"
|
||||
class="search-type-select"
|
||||
size="large"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in searchTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<input
|
||||
v-model="searchText"
|
||||
@keyup.enter="handleSearch"
|
||||
placeholder="请输入搜索关键词"
|
||||
class="search-input"
|
||||
/>
|
||||
<button @click="handleSearch" class="search-btn">搜索</button>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
<div class="search-results-area">
|
||||
<template v-if="loading">
|
||||
<div class="search-loading">正在搜索,请稍候…</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="hasSearched">
|
||||
<template v-if="results.length === 0">
|
||||
<div class="search-no-result">没有找到相关内容</div>
|
||||
</template>
|
||||
<ul v-else class="search-result-list">
|
||||
<li
|
||||
class="search-result-item"
|
||||
v-for="(item, idx) in results"
|
||||
:key="item.id"
|
||||
@click="handleResultClick(item)"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="result-title">
|
||||
<span class="tag" v-if="item.category?.name">{{
|
||||
item.category.name
|
||||
}}</span>
|
||||
<div class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
<div class="result-meta">
|
||||
<span class="result-author" v-if="item.author">
|
||||
<i class="el-icon-user"></i> {{ item.author }}
|
||||
</span>
|
||||
<span class="result-date" v-if="item.publishdate">
|
||||
<i class="el-icon-time"></i> {{ item.publishdate }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="hasSearched && total > 0" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-page-container {
|
||||
max-width: 800px;
|
||||
margin: 120px auto 20px;
|
||||
padding: 30px 20px 60px;
|
||||
background: #fff;
|
||||
min-height: 70vh;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 24px rgba(76, 80, 130, 0.08);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-type-select {
|
||||
width: 100px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 320px;
|
||||
height: 38px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 5px;
|
||||
padding: 0 15px;
|
||||
transition: border-color 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.search-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
height: 38px;
|
||||
padding: 0 28px;
|
||||
border-radius: 5px;
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.search-btn:hover {
|
||||
background: var(--primary-hover-color);
|
||||
}
|
||||
|
||||
.search-results-area {
|
||||
margin-top: 22px;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
text-align: center;
|
||||
color: #777;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.search-no-result {
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 17px;
|
||||
padding: 60px 0 80px;
|
||||
}
|
||||
|
||||
.search-result-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 18px 20px 12px 20px;
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.search-result-item:focus,
|
||||
.search-result-item:hover {
|
||||
background: var(--background-color-hover);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
color: #262f46;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.result-title .tag {
|
||||
background: var(--primary-light-bg);
|
||||
color: var(--primary-color);
|
||||
font-size: 13px;
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
margin-left: 8px;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-title .title{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
|
||||
.result-author,
|
||||
.result-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.el-icon-user,
|
||||
.el-icon-time {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user