feat(router): 新增课程列表页面并更新搜索功能
在课程页面新增课程列表页面,并更新搜索栏功能以支持模糊搜索和页面跳转。搜索栏现在支持在课程页面跳转到课程列表页,并在课程列表页更新URL参数
Showing
4 changed files
with
164 additions
and
8 deletions
| ... | @@ -6,26 +6,62 @@ | ... | @@ -6,26 +6,62 @@ |
| 6 | <input | 6 | <input |
| 7 | type="text" | 7 | type="text" |
| 8 | :placeholder="placeholder" | 8 | :placeholder="placeholder" |
| 9 | + v-model="localValue" | ||
| 9 | class="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400" | 10 | class="bg-transparent outline-none flex-1 text-gray-700 placeholder-gray-400" |
| 10 | @input="handleSearch" | 11 | @input="handleSearch" |
| 12 | + @blur="handleBlur" | ||
| 11 | /> | 13 | /> |
| 12 | </FrostedGlass> | 14 | </FrostedGlass> |
| 13 | </template> | 15 | </template> |
| 14 | 16 | ||
| 15 | <script setup> | 17 | <script setup> |
| 16 | -import { defineProps, defineEmits } from 'vue' | 18 | +import { defineProps, defineEmits, watch, ref } from 'vue' |
| 19 | +import { useRouter } from 'vue-router' | ||
| 17 | import FrostedGlass from './FrostedGlass.vue' | 20 | import FrostedGlass from './FrostedGlass.vue' |
| 18 | 21 | ||
| 22 | +const router = useRouter() | ||
| 23 | + | ||
| 19 | const props = defineProps({ | 24 | const props = defineProps({ |
| 20 | placeholder: { | 25 | placeholder: { |
| 21 | type: String, | 26 | type: String, |
| 22 | default: '搜索' | 27 | default: '搜索' |
| 28 | + }, | ||
| 29 | + modelValue: { | ||
| 30 | + type: String, | ||
| 31 | + default: '' | ||
| 32 | + }, | ||
| 33 | + isCoursePage: { | ||
| 34 | + type: Boolean, | ||
| 35 | + default: false | ||
| 23 | } | 36 | } |
| 24 | }) | 37 | }) |
| 25 | 38 | ||
| 26 | -const emit = defineEmits(['search']) | 39 | +const emit = defineEmits(['search', 'blur', 'update:modelValue']) |
| 40 | + | ||
| 41 | +const localValue = ref(props.modelValue) | ||
| 42 | + | ||
| 43 | +watch(() => props.modelValue, (newValue) => { | ||
| 44 | + localValue.value = newValue | ||
| 45 | +}) | ||
| 46 | + | ||
| 47 | +const handleSearch = () => { | ||
| 48 | + emit('update:modelValue', localValue.value) | ||
| 49 | + emit('search', localValue.value) | ||
| 50 | +} | ||
| 27 | 51 | ||
| 28 | -const handleSearch = (e) => { | 52 | +const handleBlur = () => { |
| 29 | - emit('search', e.target.value) | 53 | + const query = localValue.value |
| 54 | + emit('blur', query) | ||
| 55 | + // 根据页面类型决定路由行为 | ||
| 56 | + if (props.isCoursePage) { | ||
| 57 | + // 在课程页面,跳转到课程列表页 | ||
| 58 | + router.push({ | ||
| 59 | + path: '/courses-list', | ||
| 60 | + query: { keyword: localValue.value } | ||
| 61 | + }) | ||
| 62 | + } else { | ||
| 63 | + // 在课程列表页,只更新URL参数 | ||
| 64 | + router.replace({ query: { ...router.currentRoute.value.query, keyword: localValue.value } }) | ||
| 65 | + } | ||
| 30 | } | 66 | } |
| 31 | </script> | 67 | </script> | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-21 13:06:38 | 4 | + * @LastEditTime: 2025-03-21 14:38:09 |
| 5 | * @FilePath: /mlaj/src/router/index.js | 5 | * @FilePath: /mlaj/src/router/index.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -19,7 +19,7 @@ const routes = [ | ... | @@ -19,7 +19,7 @@ const routes = [ |
| 19 | path: '/courses', | 19 | path: '/courses', |
| 20 | name: 'Courses', | 20 | name: 'Courses', |
| 21 | component: () => import('../views/courses/CoursesPage.vue'), | 21 | component: () => import('../views/courses/CoursesPage.vue'), |
| 22 | - meta: { title: '课程列表' }, | 22 | + meta: { title: '课程' }, |
| 23 | }, | 23 | }, |
| 24 | { | 24 | { |
| 25 | path: '/courses/:id', | 25 | path: '/courses/:id', |
| ... | @@ -34,6 +34,12 @@ const routes = [ | ... | @@ -34,6 +34,12 @@ const routes = [ |
| 34 | meta: { title: '课程评价' } | 34 | meta: { title: '课程评价' } |
| 35 | }, | 35 | }, |
| 36 | { | 36 | { |
| 37 | + path: '/courses-list', | ||
| 38 | + name: 'CourseList', | ||
| 39 | + component: () => import('../views/courses/CourseListPage.vue'), | ||
| 40 | + meta: { title: '课程列表' } | ||
| 41 | + }, | ||
| 42 | + { | ||
| 37 | path: '/profile', | 43 | path: '/profile', |
| 38 | name: 'Profile', | 44 | name: 'Profile', |
| 39 | component: () => import('../views/profile/ProfilePage.vue'), | 45 | component: () => import('../views/profile/ProfilePage.vue'), | ... | ... |
src/views/courses/CourseListPage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-03-21 14:31:21 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-03-21 14:54:47 | ||
| 5 | + * @FilePath: /mlaj/src/views/courses/CourseListPage.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <AppLayout title="课程列表"> | ||
| 10 | + <div class="pb-16"> | ||
| 11 | + <!-- Search Bar --> | ||
| 12 | + <div class="pb-2"> | ||
| 13 | + <SearchBar placeholder="搜索" v-model="keyword" @search="handleSearch" @blur="handleBlur" /> | ||
| 14 | + </div> | ||
| 15 | + | ||
| 16 | + <!-- Course List --> | ||
| 17 | + <div class="px-4"> | ||
| 18 | + <div class="space-y-4"> | ||
| 19 | + <CourseCard v-for="course in courses" :key="course.id" :course="course" /> | ||
| 20 | + </div> | ||
| 21 | + | ||
| 22 | + <!-- Load More --> | ||
| 23 | + <div | ||
| 24 | + v-if="hasMore" | ||
| 25 | + class="py-4 text-center text-gray-500 text-sm" | ||
| 26 | + @click="loadMore" | ||
| 27 | + > | ||
| 28 | + 加载更多 | ||
| 29 | + </div> | ||
| 30 | + <div | ||
| 31 | + v-else | ||
| 32 | + class="py-4 text-center text-gray-400 text-sm" | ||
| 33 | + > | ||
| 34 | + 没有更多课程了 | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | + </div> | ||
| 38 | + </AppLayout> | ||
| 39 | +</template> | ||
| 40 | + | ||
| 41 | +<script setup> | ||
| 42 | +import { ref, onMounted, watchEffect } from 'vue'; | ||
| 43 | +import { useRoute } from 'vue-router'; | ||
| 44 | +import AppLayout from '@/components/layout/AppLayout.vue'; | ||
| 45 | +import SearchBar from '@/components/ui/SearchBar.vue'; | ||
| 46 | +import CourseCard from '@/components/ui/CourseCard.vue'; | ||
| 47 | +import { courses as mockCourses } from '@/utils/mockData'; | ||
| 48 | + | ||
| 49 | +const $route = useRoute(); | ||
| 50 | +const courses = ref([]); | ||
| 51 | +const hasMore = ref(true); | ||
| 52 | +const page = ref(1); | ||
| 53 | +const keyword = ref(''); | ||
| 54 | + | ||
| 55 | +// 搜索课程列表 | ||
| 56 | +const searchCourses = () => { | ||
| 57 | + // 实际项目中这里会调用搜索API | ||
| 58 | + const filteredCourses = keyword.value | ||
| 59 | + ? mockCourses.filter(course => | ||
| 60 | + (course.title?.toLowerCase().includes(keyword.value.toLowerCase()) || | ||
| 61 | + course.description?.toLowerCase().includes(keyword.value.toLowerCase())) ?? false | ||
| 62 | + ) | ||
| 63 | + : [...mockCourses]; | ||
| 64 | + courses.value = filteredCourses; | ||
| 65 | + hasMore.value = filteredCourses.length >= 10; | ||
| 66 | + page.value = 1; | ||
| 67 | +}; | ||
| 68 | + | ||
| 69 | +// 监听路由参数变化 | ||
| 70 | +watchEffect(() => { | ||
| 71 | + const queryKeyword = $route.query.keyword; | ||
| 72 | + if (keyword.value !== queryKeyword) { | ||
| 73 | + keyword.value = queryKeyword || ''; | ||
| 74 | + searchCourses(); | ||
| 75 | + } | ||
| 76 | +}); | ||
| 77 | + | ||
| 78 | +// Search handler | ||
| 79 | +const handleSearch = (query) => { | ||
| 80 | + keyword.value = query; | ||
| 81 | + searchCourses(); | ||
| 82 | +}; | ||
| 83 | + | ||
| 84 | +// Blur handler | ||
| 85 | +const handleBlur = (query) => { | ||
| 86 | + keyword.value = query; | ||
| 87 | + searchCourses(); | ||
| 88 | +}; | ||
| 89 | + | ||
| 90 | +// Load more courses | ||
| 91 | +const loadMore = () => { | ||
| 92 | + // 实际项目中这里会调用分页API | ||
| 93 | + if (page.value < 3) { | ||
| 94 | + const filteredCourses = keyword.value | ||
| 95 | + ? mockCourses.filter(course => | ||
| 96 | + course.title.toLowerCase().includes(keyword.value.toLowerCase()) || | ||
| 97 | + course.description.toLowerCase().includes(keyword.value.toLowerCase()) | ||
| 98 | + ) | ||
| 99 | + : [...mockCourses]; | ||
| 100 | + courses.value = [...courses.value, ...filteredCourses]; | ||
| 101 | + console.warn(courses.value); | ||
| 102 | + | ||
| 103 | + page.value += 1; | ||
| 104 | + hasMore.value = page.value < 3; | ||
| 105 | + } else { | ||
| 106 | + hasMore.value = false; | ||
| 107 | + } | ||
| 108 | +}; | ||
| 109 | +</script> |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | <div class="pb-16"> | 3 | <div class="pb-16"> |
| 4 | <!-- Search Bar --> | 4 | <!-- Search Bar --> |
| 5 | <div class="pb-2"> | 5 | <div class="pb-2"> |
| 6 | - <SearchBar placeholder="搜索" @search="handleSearch" /> | 6 | + <SearchBar placeholder="搜索" @search="handleSearch" @blur="handleBlur" :isCoursePage="true" /> |
| 7 | </div> | 7 | </div> |
| 8 | 8 | ||
| 9 | <!-- Featured Course Banner --> | 9 | <!-- Featured Course Banner --> |
| ... | @@ -52,7 +52,7 @@ | ... | @@ -52,7 +52,7 @@ |
| 52 | <div class="px-4 mb-4"> | 52 | <div class="px-4 mb-4"> |
| 53 | <div class="flex justify-between items-center mb-2"> | 53 | <div class="flex justify-between items-center mb-2"> |
| 54 | <h2 class="font-medium">超值线上课</h2> | 54 | <h2 class="font-medium">超值线上课</h2> |
| 55 | - <router-link to="#" class="text-xs text-gray-500 flex items-center"> | 55 | + <router-link to="/courses-list" class="text-xs text-gray-500 flex items-center"> |
| 56 | 更多 | 56 | 更多 |
| 57 | <svg | 57 | <svg |
| 58 | xmlns="http://www.w3.org/2000/svg" | 58 | xmlns="http://www.w3.org/2000/svg" |
| ... | @@ -98,6 +98,11 @@ const handleSearch = (query) => { | ... | @@ -98,6 +98,11 @@ const handleSearch = (query) => { |
| 98 | console.log("Searching for:", query); | 98 | console.log("Searching for:", query); |
| 99 | // Would implement actual search logic here | 99 | // Would implement actual search logic here |
| 100 | }; | 100 | }; |
| 101 | +// Blur handler | ||
| 102 | +const handleBlur = (query) => { | ||
| 103 | + // 实际项目中这里会调用搜索API | ||
| 104 | + console.log('Searching for:', query); | ||
| 105 | +}; | ||
| 101 | 106 | ||
| 102 | // Current time for the countdown timer | 107 | // Current time for the countdown timer |
| 103 | const todayDate = new Date(); | 108 | const todayDate = new Date(); | ... | ... |
-
Please register or login to post a comment