717 lines
16 KiB
Vue
717 lines
16 KiB
Vue
<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.png,isEnrolled=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>
|