hookehuyr

feat(router): 新增课程列表页面并更新搜索功能

在课程页面新增课程列表页面,并更新搜索栏功能以支持模糊搜索和页面跳转。搜索栏现在支持在课程页面跳转到课程列表页,并在课程列表页更新URL参数
...@@ -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'),
......
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();
......