hookehuyr

feat(课程): 新增课程列表二维码页面和图片卡片组件

添加课程列表二维码页面路由和视图组件
创建课程图片卡片组件用于二维码页面展示
更新组件类型声明文件以包含新组件
...@@ -16,6 +16,7 @@ declare module 'vue' { ...@@ -16,6 +16,7 @@ declare module 'vue' {
16 CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default'] 16 CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default']
17 ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] 17 ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default']
18 CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] 18 CourseCard: typeof import('./components/ui/CourseCard.vue')['default']
19 + CourseImageCard: typeof import('./components/ui/CourseImageCard.vue')['default']
19 CourseList: typeof import('./components/courses/CourseList.vue')['default'] 20 CourseList: typeof import('./components/courses/CourseList.vue')['default']
20 FormPage: typeof import('./components/infoEntry/formPage.vue')['default'] 21 FormPage: typeof import('./components/infoEntry/formPage.vue')['default']
21 FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] 22 FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default']
......
1 +<!--
2 + * @Date: 2025-01-21 14:31:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-10-20 17:56:14
5 + * @FilePath: /mlaj/src/components/ui/CourseImageCard.vue
6 + * @Description: 课程图片卡片组件 - 一行显示一张图片,高度自适应
7 +-->
8 +<template>
9 + <router-link
10 + :to="linkTo || `/courses/${course.id}`"
11 + class="block bg-white rounded-lg overflow-hidden shadow-sm mb-4"
12 + >
13 + <div class="relative">
14 + <!-- 课程封面图片 -->
15 + <img
16 + :src="course.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
17 + :alt="course.title"
18 + class="w-full h-auto object-cover"
19 + style="aspect-ratio: 16/9;"
20 + />
21 +
22 + <!-- 已购标识 -->
23 + <div
24 + v-if="course.is_buy"
25 + class="absolute top-2 left-2 bg-orange-500 text-white text-xs px-2 py-1 rounded font-medium"
26 + style="background-color: rgba(249, 115, 22, 0.85)"
27 + >
28 + 已购
29 + </div>
30 +
31 + <!-- 价格标签 -->
32 + <div class="absolute bottom-2 right-2 bg-opacity-60 text-white text-xs px-2 py-1 rounded" style="background-color: rgba(249, 115, 22, 0.85)">
33 + <span v-if="course?.price !== '0.00'" class="font-semibold">¥{{ course.price }}</span>
34 + <span v-else class="font-semibold">免费</span>
35 + </div>
36 + </div>
37 +
38 + <!-- 课程信息 -->
39 + <div class="p-3">
40 + <h3 class="font-medium text-sm mb-2 line-clamp-2 text-gray-900">{{ course.title }}</h3>
41 + <div class="text-gray-500 text-xs mb-2 line-clamp-1">{{ course.subtitle }}</div>
42 + <div class="flex justify-between items-center text-gray-400 text-xs">
43 + <span>已更新{{ course.count }}期</span>
44 + <span>{{ course.buy_count }}人订阅</span>
45 + </div>
46 + </div>
47 + </router-link>
48 +</template>
49 +
50 +<script setup>
51 +/**
52 + * 课程图片卡片组件的属性定义
53 + */
54 +defineProps({
55 + // 课程数据对象
56 + course: {
57 + type: Object,
58 + required: true
59 + },
60 + // 自定义链接地址
61 + linkTo: {
62 + type: String,
63 + default: ''
64 + }
65 +})
66 +</script>
67 +
68 +<style scoped>
69 +/* 文本截断样式 */
70 +.line-clamp-1 {
71 + display: -webkit-box;
72 + -webkit-line-clamp: 1;
73 + -webkit-box-orient: vertical;
74 + overflow: hidden;
75 +}
76 +
77 +.line-clamp-2 {
78 + display: -webkit-box;
79 + -webkit-line-clamp: 2;
80 + -webkit-box-orient: vertical;
81 + overflow: hidden;
82 +}
83 +</style>
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-06-17 16:47:13 4 + * @LastEditTime: 2025-10-20 17:37:47
5 * @FilePath: /mlaj/src/router/routes.js 5 * @FilePath: /mlaj/src/router/routes.js
6 * @Description: 路由地址映射配置 6 * @Description: 路由地址映射配置
7 */ 7 */
...@@ -40,6 +40,12 @@ export const routes = [ ...@@ -40,6 +40,12 @@ export const routes = [
40 meta: { title: '课程列表' }, 40 meta: { title: '课程列表' },
41 }, 41 },
42 { 42 {
43 + path: '/courses-list-qr',
44 + name: 'CourseListQR',
45 + component: () => import('../views/courses/CourseListQRPage.vue'),
46 + meta: { title: '课程列表二维码' },
47 + },
48 + {
43 path: '/profile', 49 path: '/profile',
44 name: 'Profile', 50 name: 'Profile',
45 component: () => import('../views/profile/ProfilePage.vue'), 51 component: () => import('../views/profile/ProfilePage.vue'),
......
1 +<!--
2 + * @Date: 2025-03-21 14:31:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-05-21 16:57:36
5 + * @FilePath: /mlaj/src/views/courses/CourseListQRPage.vue
6 + * @Description: 课程列表页面 - 支持列表和卡片两种显示模式
7 +-->
8 +<template>
9 + <AppLayout title="课程列表">
10 + <div class="pb-16">
11 + <!-- Course List -->
12 + <div class="px-4">
13 + <van-list
14 + v-model:loading="loading"
15 + :finished="finished"
16 + finished-text="没有更多课程了"
17 + @load="onLoad"
18 + class="space-y-4"
19 + >
20 + <!-- 根据type参数显示不同组件 -->
21 + <template v-if="displayType === 'card'">
22 + <CourseImageCard v-for="course in courses" :key="course.id" :course="course" />
23 + </template>
24 + <template v-else>
25 + <CourseCard v-for="course in courses" :key="course.id" :course="course" />
26 + </template>
27 + </van-list>
28 + </div>
29 + </div>
30 + </AppLayout>
31 +</template>
32 +
33 +<script setup>
34 +import { ref, onMounted, computed } from 'vue';
35 +import { useRoute } from 'vue-router';
36 +import AppLayout from '@/components/layout/AppLayout.vue';
37 +import CourseCard from '@/components/ui/CourseCard.vue';
38 +import CourseImageCard from '@/components/ui/CourseImageCard.vue';
39 +// 导入接口
40 +import { getCourseListAPI } from "@/api/course";
41 +import { List } from 'vant';
42 +
43 +const $route = useRoute();
44 +const courses = ref([]);
45 +const loading = ref(false);
46 +const finished = ref(false);
47 +const limit = ref(5);
48 +const page = ref(0);
49 +
50 +// 根据URL参数确定显示类型
51 +const displayType = computed(() => {
52 + return $route.query.type || 'list'; // 默认为list模式
53 +});
54 +
55 +// 获取URL参数中的ID
56 +const courseId = computed(() => {
57 + return $route.query.id || '';
58 +});
59 +
60 +// Load more courses
61 +const onLoad = async () => {
62 + const nextPage = page.value;
63 + const params = {
64 + limit: limit.value,
65 + page: nextPage
66 + };
67 +
68 + // 如果有ID参数,添加到请求中
69 + if (courseId.value) {
70 + params.id = courseId.value;
71 + }
72 +
73 + const res = await getCourseListAPI(params);
74 + if (res.code) {
75 + courses.value = [...courses.value, ...res.data];
76 + finished.value = res.data.length < limit.value;
77 + page.value = nextPage + 1;
78 + }
79 + loading.value = false;
80 +};
81 +</script>