增加搜索功能
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']
|
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
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 {
|
:root {
|
||||||
--white:#fff;
|
--white:#fff;
|
||||||
--primary-color: #409eff;
|
--primary-color: #409eff;
|
||||||
|
--primary-hover-color: #3c4ccf;
|
||||||
|
--primary-light-bg: #e7f3ff;
|
||||||
--success-color: #67c23a;
|
--success-color: #67c23a;
|
||||||
--warning-color: #e6a23c;
|
--warning-color: #e6a23c;
|
||||||
--danger-color: #f56c6c;
|
--danger-color: #f56c6c;
|
||||||
@ -21,6 +23,7 @@
|
|||||||
|
|
||||||
--background-color: #f5f5f5;
|
--background-color: #f5f5f5;
|
||||||
--background-color-page: #f0f2f5;
|
--background-color-page: #f0f2f5;
|
||||||
|
--background-color-hover: #f7f8fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局重置样式
|
// 全局重置样式
|
||||||
|
|||||||
@ -47,6 +47,11 @@ const router = createRouter({
|
|||||||
name: "downloadGames",
|
name: "downloadGames",
|
||||||
component: () => import("@/views/downloadGames/index.vue"),
|
component: () => import("@/views/downloadGames/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/search",
|
||||||
|
name: "search",
|
||||||
|
component: () => import("@/views/components/search.vue"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -81,7 +81,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
import defaultImage from "@/assets/imgs/default.png";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// Props定义
|
// Props定义
|
||||||
interface ResourceConfig {
|
interface ResourceConfig {
|
||||||
@ -115,7 +119,7 @@ const currentCategoryName = computed(() => {
|
|||||||
|
|
||||||
// 获取图片地址
|
// 获取图片地址
|
||||||
const getImageUrl = (imagePath: string) => {
|
const getImageUrl = (imagePath: string) => {
|
||||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
if (!imagePath) return defaultImage;
|
||||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -130,9 +134,14 @@ const fetchCategories = async () => {
|
|||||||
Array.isArray(response.data.data)
|
Array.isArray(response.data.data)
|
||||||
) {
|
) {
|
||||||
categories.value = response.data.data;
|
categories.value = response.data.data;
|
||||||
// 默认选择第一个分类
|
// 检查路由参数中的category ID,如果存在则选择对应分类,否则选择第一个分类
|
||||||
if (categories.value.length > 0) {
|
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();
|
fetchResources();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ElMessage } from "element-plus";
|
|||||||
import Header from "@/views/components/header.vue";
|
import Header from "@/views/components/header.vue";
|
||||||
import Footer from "@/views/components/footer.vue";
|
import Footer from "@/views/components/footer.vue";
|
||||||
import { article } from "@/api/article";
|
import { article } from "@/api/article";
|
||||||
|
import defaultImage from "@/assets/imgs/default.png";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -14,7 +15,9 @@ const loading = ref(true);
|
|||||||
|
|
||||||
// 获取图片地址
|
// 获取图片地址
|
||||||
const getImageUrl = (imagePath: string) => {
|
const getImageUrl = (imagePath: string) => {
|
||||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
if (!imagePath) {
|
||||||
|
return defaultImage;
|
||||||
|
}
|
||||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +59,10 @@ const fetchArticleDetail = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 确保defaultImage被使用,防止tree-shaking
|
||||||
|
if (defaultImage) {
|
||||||
|
console.log('Default image loaded:', defaultImage);
|
||||||
|
}
|
||||||
fetchArticleDetail();
|
fetchArticleDetail();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -537,6 +544,11 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
|
|
||||||
|
.prev,
|
||||||
|
.next {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ElMessage } from "element-plus";
|
|||||||
import Header from "@/views/components/header.vue";
|
import Header from "@/views/components/header.vue";
|
||||||
import Footer from "@/views/components/footer.vue";
|
import Footer from "@/views/components/footer.vue";
|
||||||
import { resource as resourceApi } from "@/api/resource";
|
import { resource as resourceApi } from "@/api/resource";
|
||||||
|
import defaultImage from "@/assets/imgs/default.png";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter(); // 新增:引入useRouter
|
const router = useRouter(); // 新增:引入useRouter
|
||||||
@ -27,7 +28,9 @@ const pageInfo = computed(() => ({
|
|||||||
|
|
||||||
// 获取图片地址
|
// 获取图片地址
|
||||||
const getImageUrl = (imagePath: string) => {
|
const getImageUrl = (imagePath: string) => {
|
||||||
if (!imagePath) return "/src/assets/imgs/default.png";
|
if (!imagePath) {
|
||||||
|
return defaultImage;
|
||||||
|
}
|
||||||
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
return import.meta.env.VITE_API_DOMAIN + imagePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -158,14 +161,12 @@ const fetchResourceDetail = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response: any = await resourceApi.getResourceDetail(resourceId.value);
|
const response: any = await resourceApi.getResourceDetail(resourceId.value);
|
||||||
|
if (response.data.code === 0) {
|
||||||
console.log("获取资源详情响应:", response);
|
resourceData.value = response.data.data;
|
||||||
if (response?.data?.code === 0 && response?.data?.data) {
|
console.log(resourceData.value);
|
||||||
resourceData.value = response.data.data.resource;
|
|
||||||
prevResource.value = response.data.data.prev_resource;
|
prevResource.value = response.data.data.prev_resource;
|
||||||
nextResource.value = response.data.data.next_resource;
|
nextResource.value = response.data.data.next_resource;
|
||||||
relatedResources.value = response.data.data.related_resources || [];
|
relatedResources.value = response.data.data.related_resources || [];
|
||||||
console.log("资源数据加载完成:", resourceData.value?.title);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("API响应格式错误:", response);
|
console.error("API响应格式错误:", response);
|
||||||
ElMessage.error("获取资源详情失败:数据格式错误");
|
ElMessage.error("获取资源详情失败:数据格式错误");
|
||||||
@ -196,8 +197,6 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 移除原有的fetchResourceDetail调用,由watch接管
|
|
||||||
// 页面加载时滚动到顶部
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -217,9 +216,7 @@ onMounted(() => {
|
|||||||
<el-breadcrumb-item :to="{ path: '/' }"
|
<el-breadcrumb-item :to="{ path: '/' }"
|
||||||
>首页</el-breadcrumb-item
|
>首页</el-breadcrumb-item
|
||||||
>
|
>
|
||||||
<el-breadcrumb-item :to="{ path: pageInfo.path }">{{
|
<el-breadcrumb-item>{{ resourceData.cate }}</el-breadcrumb-item>
|
||||||
pageInfo.name
|
|
||||||
}}</el-breadcrumb-item>
|
|
||||||
<el-breadcrumb-item>详情</el-breadcrumb-item>
|
<el-breadcrumb-item>详情</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
@ -332,6 +329,11 @@ onMounted(() => {
|
|||||||
@click="handleContentClick"
|
@click="handleContentClick"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<div class="py-5" style="color: #ccc; text-align: center">
|
||||||
|
-------- THE END --------
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider></el-divider>
|
||||||
<div class="disclaimers">
|
<div class="disclaimers">
|
||||||
<div class="disclaimer-item">
|
<div class="disclaimer-item">
|
||||||
<div class="disclaimer-title">免责声明:</div>
|
<div class="disclaimer-title">免责声明:</div>
|
||||||
@ -519,7 +521,7 @@ onMounted(() => {
|
|||||||
.top-content {
|
.top-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
background-color: #0081ff;
|
background-color: var(--primary-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.top-content-main {
|
.top-content-main {
|
||||||
@ -611,10 +613,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
border: 1px #0d6efd dashed;
|
border: 1px var(--primary-color) dashed;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
background-color: #0081ff12;
|
background-color: var(--primary-light-bg);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -626,11 +628,11 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.codebtn {
|
.codebtn {
|
||||||
color: #0d6efd;
|
color: var(--primary-color);
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
padding: 6px 20px;
|
padding: 6px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #0d6efd;
|
border: 1px solid var(--primary-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|||||||
@ -1,4 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ArrowUp } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
// 返回顶部功能
|
||||||
|
function goToTop() {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
@ -9,7 +18,7 @@
|
|||||||
<div class="mr-20">
|
<div class="mr-20">
|
||||||
<img src="@/assets/imgs/logo-light.png" alt="Logo" height="70" />
|
<img src="@/assets/imgs/logo-light.png" alt="Logo" height="70" />
|
||||||
<p class="text-white-50 my-4 f18" style="width: 400px">
|
<p class="text-white-50 my-4 f18" style="width: 400px">
|
||||||
美天智能科技,这里是介绍!
|
这里是介绍!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -85,6 +94,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tongji"></div>
|
<div class="tongji"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 回到顶部按钮 -->
|
||||||
|
<div class="go-to-top" @click="goToTop">
|
||||||
|
<el-icon class="go-to-top-icon">
|
||||||
|
<ArrowUp />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.footer {
|
.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>
|
</style>
|
||||||
|
|||||||
@ -1,12 +1,22 @@
|
|||||||
<script lang="ts" setup>
|
<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 { 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 searchText = ref("");
|
||||||
|
const searchType = ref("articles");
|
||||||
|
const searchInput = ref();
|
||||||
const username = ref("管理员"); // 这里可以从 store 获取
|
const username = ref("管理员"); // 这里可以从 store 获取
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 搜索类型选项
|
||||||
|
const searchTypeOptions = [
|
||||||
|
{ value: "articles", label: "文章" },
|
||||||
|
{ value: "resources", label: "资源" },
|
||||||
|
];
|
||||||
|
|
||||||
// 静态菜单配置
|
// 静态菜单配置
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
@ -26,14 +36,34 @@ const staticMenuItems: MenuItem[] = [
|
|||||||
// 方法
|
// 方法
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
if (searchText.value.trim()) {
|
if (searchText.value.trim()) {
|
||||||
console.log("搜索:", searchText.value);
|
// 跳转到搜索页面,并传递搜索关键词和类型
|
||||||
// TODO: 实现搜索逻辑
|
router.push({
|
||||||
// 搜索完成后可以隐藏搜索框
|
path: "/search",
|
||||||
showSearch.value = false;
|
query: {
|
||||||
|
keyword: searchText.value.trim(),
|
||||||
|
type: searchType.value,
|
||||||
|
},
|
||||||
|
replace: true, // 替换当前历史记录,避免页面刷新
|
||||||
|
});
|
||||||
|
// 关闭搜索弹窗
|
||||||
|
showSearchDialog.value = false;
|
||||||
searchText.value = "";
|
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) => {
|
const handleCommand = (command: string) => {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "profile":
|
case "profile":
|
||||||
@ -85,24 +115,10 @@ const handleCommand = (command: string) => {
|
|||||||
<!-- 右侧工具栏 -->
|
<!-- 右侧工具栏 -->
|
||||||
<div class="header-right">
|
<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-icon><Search /></el-icon>
|
||||||
</el-button>
|
</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">
|
<div class="user-info">
|
||||||
<span class="username">{{ username }}</span>
|
<span class="username">{{ username }}</span>
|
||||||
@ -130,6 +146,57 @@ const handleCommand = (command: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -181,13 +248,13 @@ const handleCommand = (command: string) => {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #409eff;
|
color: var(--primary-color);
|
||||||
background: rgba(64, 158, 255, 0.1);
|
background: var(--primary-light-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.router-link-active {
|
&.router-link-active {
|
||||||
color: #409eff;
|
color: var(--primary-color);
|
||||||
background: rgba(64, 158, 255, 0.1);
|
background: var(--primary-light-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,30 +265,28 @@ const handleCommand = (command: string) => {
|
|||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
|
|
||||||
.search-btn {
|
.search-btn {
|
||||||
color: #666;
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #409eff;
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 200px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-dropdown {
|
.user-dropdown {
|
||||||
@ -233,7 +298,7 @@ const handleCommand = (command: string) => {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.05);
|
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 {
|
.header-right {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.search-input {
|
|
||||||
width: 150px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
display: none; // 小屏幕隐藏用户名
|
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>
|
</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