OL-LearnPlatform-Frontend/src/views/SearchResults.vue
2025-09-12 03:11:18 +08:00

717 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="search-results-page">
<!-- 背景图片 -->
<div class="background-container">
<img src="/serch/背景.png" alt="背景" class="background-image" />
</div>
<!-- 主要内容 -->
<div class="main-content">
<div class="container">
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">资源库·搜索</h1>
</div>
<!-- 搜索框 -->
<div class="search-container">
<div class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索课程..."
class="search-input"
@keyup.enter="handleSearch"
/>
<button class="search-btn" @click="handleSearch">
<img
src="https://lanhu-oss-proxy.lanhuapp.com/SketchPng4617c5d0e102114051f8321ef18af957e78bb797ad196789befab13c980617fc"
alt="搜索"
class="search-icon"
/>
</button>
</div>
</div>
<!-- 搜索结果统计 -->
<div class="search-stats" v-if="searchResults.length > 0 || hasSearched">
<span class="stats-text">显示结果{{ searchResults.length }}</span>
<div class="sort-dropdown">
<span>筛选</span>
<span class="dropdown-icon"></span>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<n-spin size="large" />
<p>正在搜索...</p>
</div>
<!-- 搜索结果 -->
<div class="search-results" v-else-if="searchResults.length > 0">
<!-- 课程网格 -->
<div class="courses-grid">
<div class="course-card" v-for="course in searchResults" :key="course.id">
<div class="course-image">
<img :src="course.thumbnail" :alt="course.title" />
<!-- AI伴学标签 -->
<div v-if="shouldShowAiTag(course)" class="ai-companion-tag">
<img src="/images/aiCompanion/AI伴学标签@2x.png" alt="AI伴学" class="ai-tag-image">
</div>
</div>
<div class="course-info">
<h3 class="course-title">{{ getCourseTitle(course) }}</h3>
<div class="course-meta">
<div class="course-chapters">
<img src="/images/courses/课程总章数.png" alt="课程章节" class="meta-icon">
<span>共9章54节</span>
</div>
<div class="course-duration">
<img src="/images/courses/课程总时长.png" alt="课程时长" class="meta-icon">
<span>12小时43分钟</span>
</div>
</div>
<div class="course-footer">
<div class="course-stats">
<span class="course-students">讲师: {{ getCourseInstructors(course) }}</span>
</div>
<button
:class="getButtonClass(course)"
@click="goToCourseDetail(course)"
>
<img
v-if="shouldShowButtonIcon(course)"
:src="getButtonIcon(course)"
alt="AI图标"
class="button-icon"
/>
{{ getButtonText(course) }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else-if="hasSearched && !loading" class="no-results">
<div class="no-results-content">
<div class="no-results-icon">🔍</div>
<h3>未找到相关课程</h3>
<p>请尝试其他关键词或检查拼写</p>
</div>
</div>
<!-- 初始状态 -->
<div v-else class="initial-state">
<div class="initial-content">
<div class="placeholder-icon">🔍</div>
<h3>输入关键词搜索课程</h3>
<p>发现更多优质学习资源</p>
</div>
</div>
<!-- 分页 -->
<div v-if="searchResults.length > 0" class="pagination-container">
<n-pagination
v-model:page="currentPage"
:page-count="totalPages"
:page-size="pageSize"
show-size-picker
:page-sizes="[10, 20, 30, 50]"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { CourseApi } from '@/api'
import type { Course } from '@/api/types'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const userStore = useUserStore()
// 响应式数据
const searchKeyword = ref('')
const searchResults = ref<Course[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const totalResults = ref(0)
// 计算属性
const totalPages = computed(() => Math.ceil(totalResults.value / pageSize.value))
// 搜索方法
const handleSearch = async () => {
if (!searchKeyword.value.trim()) {
message.warning('请输入搜索关键词')
return
}
loading.value = true
hasSearched.value = true
try {
console.log('🔍 开始搜索:', searchKeyword.value)
const response = await CourseApi.searchCourses({
keyword: searchKeyword.value.trim(),
limit: pageSize.value.toString(),
page: currentPage.value
})
console.log('📊 搜索结果:', response)
if (response.code === 200 || response.code === 0) {
searchResults.value = response.data || []
totalResults.value = response.total || searchResults.value.length
// 更新URL参数
router.replace({
query: {
...route.query,
keyword: searchKeyword.value,
page: currentPage.value.toString()
}
})
} else {
message.error(response.message || '搜索失败')
searchResults.value = []
}
} catch (error: any) {
console.error('❌ 搜索失败:', error)
message.error('搜索失败,请稍后重试')
searchResults.value = []
} finally {
loading.value = false
}
}
// 分页处理
const handlePageChange = (page: number) => {
currentPage.value = page
handleSearch()
}
const handlePageSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
handleSearch()
}
// 课程相关方法复用自courses页面
const shouldShowAiTag = (course: any) => {
return course.hasAiCompanion || course.aiEnabled || course.izAi === 1
}
const getCourseTitle = (course: any) => {
return course.title || course.name || '课程标题'
}
const getCourseInstructors = (course: any) => {
if (course.instructors && course.instructors.length > 0) {
return course.instructors.map((instructor: any) => instructor.name).join(', ')
}
if (course.teacherList && course.teacherList.length > 0) {
return course.teacherList.map((teacher: any) => teacher.name).join(', ')
}
return course.instructor || course.school || '未知讲师'
}
// 获取按钮文本复用courses页面逻辑
const getButtonText = (course: any) => {
const isAi = course?.izAi === 1
const isEnrolled = course?.isEnrolled === true
console.log('🔍 按钮文本逻辑:', {
courseId: course?.id,
courseName: course?.title || course?.name,
isAi,
isEnrolled,
izAi: course?.izAi
})
if (isAi) {
// AI伴学模式isEnrolled=true显示"去学习"isEnrolled=false显示"去报名"
return isEnrolled ? '去学习' : '去报名'
} else {
// 普通模式isEnrolled=true显示"去学习"isEnrolled=false显示"去报名"
return isEnrolled ? '去学习' : '去报名'
}
}
// 获取按钮样式类复用courses页面逻辑
const getButtonClass = (course: any) => {
const isAi = course?.izAi === 1
return isAi ? 'enroll-btn ai-btn' : 'enroll-btn'
}
// 是否显示按钮图标复用courses页面逻辑
const shouldShowButtonIcon = (course: any) => {
return course?.izAi === 1
}
// 获取按钮图标复用courses页面逻辑
const getButtonIcon = (course: any) => {
const isEnrolled = course?.isEnrolled === true
// isEnrolled=true时显示courseAi.pngisEnrolled=false时显示courseAii.png
return isEnrolled ? '/images/course/courseAi.png' : '/images/course/courseAii.png'
}
// 跳转到课程详情页复用courses页面逻辑
const goToCourseDetail = async (course: any) => {
try {
// 检查用户是否已登录
if (!userStore.isLoggedIn) {
console.log('用户未登录跳转到AI伴学页面')
router.push(`/ai-companion?courseId=${course.id}`)
return
}
console.log('检查课程报名状态课程ID:', course.id)
// 调用报名状态检查接口
const response = await CourseApi.checkEnrollmentStatus(String(course.id))
if ((response.code === 0 || response.code === 200) && response.data) {
const isEnrolled = response.data.result
if (isEnrolled) {
// 已报名,跳转到已兑换页面
console.log('用户已报名,跳转到已兑换页面')
router.push(`/course/${course.id}/exchanged`)
} else {
// 未报名跳转到AI伴学页面
console.log('用户未报名跳转到AI伴学页面')
router.push(`/ai-companion?courseId=${course.id}`)
}
} else {
// 查询失败默认跳转到AI伴学页面
console.warn('查询报名状态失败跳转到AI伴学页面')
router.push(`/ai-companion?courseId=${course.id}`)
}
} catch (error) {
console.error('检查报名状态时发生错误:', error)
// 发生错误时默认跳转到AI伴学页面
router.push(`/ai-companion?courseId=${course.id}`)
}
}
// 组件挂载时处理URL参数
onMounted(() => {
const keyword = (route.query.keyword || route.query.q) as string
const page = parseInt(route.query.page as string) || 1
if (keyword) {
searchKeyword.value = keyword
currentPage.value = page
handleSearch()
}
})
</script>
<style scoped>
.search-results-page {
min-height: 100vh;
position: relative;
background: #F5F8FB;
}
.background-container {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 0;
}
.background-image {
width: 100%;
height: auto;
display: block;
}
.main-content {
position: relative;
z-index: 1;
padding: 40px 0;
}
.container {
width: 1420px;
margin: 0 auto;
padding: 0 0;
box-sizing: border-box;
}
.page-header {
text-align: center;
margin-bottom: 40px;
}
.page-title {
font-size: 32px;
font-weight: 600;
color: #333;
margin: 0;
}
.search-container {
display: flex;
justify-content: center;
margin-bottom: 40px;
}
.search-box {
position: relative;
width: 100%;
max-width: 600px;
}
.search-input {
width: 100%;
height: 48px;
padding: 0 60px 0 20px;
border: 1px solid #E6E6E6;
border-radius: 24px;
font-size: 16px;
background: white;
outline: none;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: #0288D1;
}
.search-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.search-icon {
width: 20px;
height: 20px;
}
.search-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.stats-text {
font-size: 16px;
color: #666;
}
.sort-dropdown {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #666;
}
.dropdown-icon {
font-size: 12px;
color: #666;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
gap: 20px;
}
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
column-gap: 20px;
row-gap: 24px;
margin-bottom: 40px;
width: 100%;
box-sizing: border-box;
justify-content: center;
}
.course-card {
background: white;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
width: 268px;
height: 350px;
display: flex;
flex-direction: column;
}
.course-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.course-image {
width: 100%;
height: 180px;
overflow: hidden;
border-radius: 8px 8px 0 0;
position: relative;
flex-shrink: 0;
}
.course-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* AI伴学标签样式 */
.ai-companion-tag {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.ai-tag-image {
width: auto;
height: auto;
max-width: 60px;
max-height: 30px;
display: block;
}
.course-info {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.course-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin: 0 0 12px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.course-meta {
display: flex;
justify-content: left;
gap: 10px;
font-size: 12px;
color: #999;
margin-bottom: 16px;
}
.course-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.course-chapters,
.course-duration {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
.meta-icon {
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: middle;
}
.course-students {
color: #999;
font-size: 12px;
}
.enroll-btn {
background: #0286D5;
color: white;
border: none;
padding: 6px 10px;
border-radius: 20px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.enroll-btn:hover {
background: #40a9ff;
}
/* AI伴学按钮样式 */
.enroll-btn.ai-btn {
background: linear-gradient(135deg, #33C4FF 0%, #0088D1 100%);
}
.enroll-btn.ai-btn:hover {
background: linear-gradient(135deg, #2bb3ff 0%, #0077b8 100%);
}
/* 按钮图标样式 */
.button-icon {
width: 15px;
height: 15px;
object-fit: contain;
flex-shrink: 0;
margin-top: 2px; /* 向下调整图标位置 */
}
.no-results,
.initial-state {
display: flex;
justify-content: center;
align-items: center;
padding: 80px 0;
}
.no-results-content,
.initial-content {
text-align: center;
}
.no-results-icon,
.placeholder-icon {
font-size: 80px;
margin-bottom: 24px;
opacity: 0.6;
display: flex;
justify-content: center;
align-items: center;
}
.no-results-content h3,
.initial-content h3 {
font-size: 24px;
color: #333;
margin: 0 0 12px 0;
}
.no-results-content p,
.initial-content p {
font-size: 16px;
color: #666;
margin: 0;
}
.pagination-container {
display: flex;
justify-content: center;
padding: 40px 0;
}
/* 响应式设计 */
@media (min-width: 1600px) {
.courses-grid {
grid-template-columns: repeat(5, 1fr);
}
}
@media (min-width: 1400px) {
.container {
max-width: 1420px;
}
}
@media (max-width: 1400px) {
.courses-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1420px) {
.container {
max-width: calc(100vw - 160px);
padding: 0 20px;
}
}
@media (max-width: 992px) {
.courses-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.container {
max-width: calc(100vw - 40px);
padding: 0 20px;
}
.courses-grid {
grid-template-columns: repeat(2, 1fr);
column-gap: 16px;
row-gap: 20px;
}
.search-stats {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
}
@media (max-width: 768px) {
.container {
margin: 0 20px;
}
}
@media (max-width: 480px) {
.courses-grid {
grid-template-columns: 1fr;
}
.container {
margin: 0 16px;
}
}
</style>