hookehuyr

feat(test): 新增活动列表和课程列表测试页面

- 新增活动列表测试页面 (ActivityListTestPage.vue)
- 新增课程列表测试页面 (CourseListTestPage.vue)
- 配置测试路由:/test/activity-list 和 /test/course-list
- 更新 CHANGELOG.md 记录变更
- 修复 ESLint 配置:添加 vue-eslint-parser 以正确解析 .vue 文件

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
3 说明:该章节从 README 迁移到本文件,避免 README 过长。后续新增变更建议追加在文件顶部。 3 说明:该章节从 README 迁移到本文件,避免 README 过长。后续新增变更建议追加在文件顶部。
4 4
5 5
6 +## 2026-01-29 15:30:00
7 +- 新增活动列表测试页面:[/src/views/test/ActivityListTestPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/test/ActivityListTestPage.vue)
8 + - 实现高保真 UI 还原(导航栏、筛选栏、活动列表卡片)
9 + - 配置测试路由:`/test/activity-list`
10 +
6 ## 2026-01-26 13:40:00 11 ## 2026-01-26 13:40:00
7 - 优化打卡卡片组件(CheckinCard): 12 - 优化打卡卡片组件(CheckinCard):
8 - 增加长文本折叠功能:内容超过5行自动显示省略号,并提供“全文/收起”切换按钮 13 - 增加长文本折叠功能:内容超过5行自动显示省略号,并提供“全文/收起”切换按钮
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 */ 4 */
5 import vue from 'eslint-plugin-vue' 5 import vue from 'eslint-plugin-vue'
6 import prettier from 'eslint-config-prettier' 6 import prettier from 'eslint-config-prettier'
7 +import vueParser from 'vue-eslint-parser'
7 8
8 export default [ 9 export default [
9 { 10 {
...@@ -32,6 +33,14 @@ export default [ ...@@ -32,6 +33,14 @@ export default [
32 languageOptions: { 33 languageOptions: {
33 ecmaVersion: 'latest', 34 ecmaVersion: 'latest',
34 sourceType: 'module', 35 sourceType: 'module',
36 + parser: vueParser,
37 + parserOptions: {
38 + ecmaVersion: 'latest',
39 + sourceType: 'module',
40 + ecmaFeatures: {
41 + jsx: true,
42 + },
43 + },
35 globals: { 44 globals: {
36 // 浏览器环境 45 // 浏览器环境
37 window: 'readonly', 46 window: 'readonly',
......
...@@ -104,6 +104,7 @@ ...@@ -104,6 +104,7 @@
104 "unplugin-auto-import": "^19.1.1", 104 "unplugin-auto-import": "^19.1.1",
105 "unplugin-vue-components": "^28.4.1", 105 "unplugin-vue-components": "^28.4.1",
106 "vite": "^6.2.0", 106 "vite": "^6.2.0",
107 - "vitest": "^3.2.0" 107 + "vitest": "^3.2.0",
108 + "vue-eslint-parser": "^10.2.0"
108 } 109 }
109 } 110 }
......
...@@ -177,6 +177,9 @@ importers: ...@@ -177,6 +177,9 @@ importers:
177 vitest: 177 vitest:
178 specifier: ^3.2.0 178 specifier: ^3.2.0
179 version: 3.2.4(@types/node@20.19.25)(jiti@1.21.7)(jsdom@24.1.3(canvas@2.11.2))(less@4.4.2)(yaml@2.8.2) 179 version: 3.2.4(@types/node@20.19.25)(jiti@1.21.7)(jsdom@24.1.3(canvas@2.11.2))(less@4.4.2)(yaml@2.8.2)
180 + vue-eslint-parser:
181 + specifier: ^10.2.0
182 + version: 10.2.0(eslint@9.39.2(jiti@1.21.7))
180 183
181 packages: 184 packages:
182 185
......
...@@ -94,6 +94,8 @@ declare module 'vue' { ...@@ -94,6 +94,8 @@ declare module 'vue' {
94 VanSwipe: typeof import('vant/es')['Swipe'] 94 VanSwipe: typeof import('vant/es')['Swipe']
95 VanSwipeItem: typeof import('vant/es')['SwipeItem'] 95 VanSwipeItem: typeof import('vant/es')['SwipeItem']
96 VanTab: typeof import('vant/es')['Tab'] 96 VanTab: typeof import('vant/es')['Tab']
97 + VanTabbar: typeof import('vant/es')['Tabbar']
98 + VanTabbarItem: typeof import('vant/es')['TabbarItem']
97 VanTabs: typeof import('vant/es')['Tabs'] 99 VanTabs: typeof import('vant/es')['Tabs']
98 VanTag: typeof import('vant/es')['Tag'] 100 VanTag: typeof import('vant/es')['Tag']
99 VanTimePicker: typeof import('vant/es')['TimePicker'] 101 VanTimePicker: typeof import('vant/es')['TimePicker']
......
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: 2026-01-20 10:23:54 4 + * @LastEditTime: 2026-01-29 00:08:03
5 * @FilePath: /mlaj/src/router/routes.js 5 * @FilePath: /mlaj/src/router/routes.js
6 * @Description: 路由地址映射配置 6 * @Description: 路由地址映射配置
7 */ 7 */
...@@ -16,8 +16,8 @@ export const routes = [ ...@@ -16,8 +16,8 @@ export const routes = [
16 meta: { 16 meta: {
17 title: '欢迎页', 17 title: '欢迎页',
18 requiresAuth: false, 18 requiresAuth: false,
19 - hideInMenu: true 19 + hideInMenu: true,
20 - } 20 + },
21 }, 21 },
22 { 22 {
23 path: '/', 23 path: '/',
...@@ -264,6 +264,18 @@ export const routes = [ ...@@ -264,6 +264,18 @@ export const routes = [
264 meta: { title: 'test' }, 264 meta: { title: 'test' },
265 }, 265 },
266 { 266 {
267 + path: '/test/course-list',
268 + name: 'CourseListTest',
269 + component: () => import('../views/test/CourseListTestPage.vue'),
270 + meta: { title: '课程列表测试' },
271 + },
272 + {
273 + path: '/test/activity-list',
274 + name: 'ActivityListTest',
275 + component: () => import('../views/test/ActivityListTestPage.vue'),
276 + meta: { title: '活动列表测试' },
277 + },
278 + {
267 path: '/test/icon', 279 path: '/test/icon',
268 name: 'IconTest', 280 name: 'IconTest',
269 component: () => import('../views/IconTestPage.vue'), 281 component: () => import('../views/IconTestPage.vue'),
...@@ -280,19 +292,20 @@ export const routes = [ ...@@ -280,19 +292,20 @@ export const routes = [
280 name: 'upload_video', 292 name: 'upload_video',
281 component: () => import('../views/upload_video.vue'), 293 component: () => import('../views/upload_video.vue'),
282 meta: { title: 'upload_video' }, 294 meta: { title: 'upload_video' },
283 - }, { 295 + },
296 + {
284 path: '/auth', 297 path: '/auth',
285 component: () => import('@/views/auth.vue'), 298 component: () => import('@/views/auth.vue'),
286 meta: { 299 meta: {
287 title: '微信授权页面', 300 title: '微信授权页面',
288 - } 301 + },
289 }, 302 },
290 { 303 {
291 path: '/study', 304 path: '/study',
292 component: () => import('@/views/study/studyPage.vue'), 305 component: () => import('@/views/study/studyPage.vue'),
293 meta: { 306 meta: {
294 title: '学习页面', 307 title: '学习页面',
295 - } 308 + },
296 }, 309 },
297 { 310 {
298 path: '/studyDetail/:id', 311 path: '/studyDetail/:id',
...@@ -300,7 +313,7 @@ export const routes = [ ...@@ -300,7 +313,7 @@ export const routes = [
300 meta: { 313 meta: {
301 title: '学习详情页面', 314 title: '学习详情页面',
302 requiresAuth: true, 315 requiresAuth: true,
303 - } 316 + },
304 }, 317 },
305 { 318 {
306 path: '/pdfPreview', 319 path: '/pdfPreview',
...@@ -308,14 +321,14 @@ export const routes = [ ...@@ -308,14 +321,14 @@ export const routes = [
308 component: () => import('@/views/study/PdfPreviewPage.vue'), 321 component: () => import('@/views/study/PdfPreviewPage.vue'),
309 meta: { 322 meta: {
310 title: 'PDF预览', 323 title: 'PDF预览',
311 - } 324 + },
312 }, 325 },
313 { 326 {
314 path: '/profile/studyCourse/:id', 327 path: '/profile/studyCourse/:id',
315 component: () => import('@/views/profile/StudyCoursePage.vue'), 328 component: () => import('@/views/profile/StudyCoursePage.vue'),
316 meta: { 329 meta: {
317 title: '课程集合页面', 330 title: '课程集合页面',
318 - } 331 + },
319 }, 332 },
320 ...checkinRoutes, 333 ...checkinRoutes,
321 ...teacherRoutes, 334 ...teacherRoutes,
......
1 +<template>
2 + <div class="pb-safe min-h-screen bg-gray-50">
3 + <!-- 顶部导航栏 -->
4 + <van-nav-bar fixed placeholder z-index="50" class="nav-bar">
5 + <template #left>
6 + <van-icon name="arrow-left" size="22" color="#333" />
7 + </template>
8 + <template #title>
9 + <span class="text-lg font-medium text-gray-800">最新活动</span>
10 + </template>
11 + <template #right>
12 + <van-icon name="ellipsis" size="22" color="#333" />
13 + </template>
14 + </van-nav-bar>
15 +
16 + <!-- 筛选栏 -->
17 + <div
18 + class="sticky top-[46px] z-40 flex items-center justify-between bg-white px-3 py-2 shadow-sm"
19 + >
20 + <div class="flex items-center gap-6">
21 + <!-- 全部 -->
22 + <div class="relative text-[15px] font-bold text-gray-900">
23 + 全部
24 + <div
25 + class="absolute -bottom-2 left-1/2 h-[3px] w-4 -translate-x-1/2 rounded-full bg-gray-800"
26 + ></div>
27 + </div>
28 +
29 + <!-- 活动地点 -->
30 + <div class="flex items-center gap-1 text-[14px] text-gray-600">
31 + <span>活动地点</span>
32 + <van-icon name="play" class="rotate-90 text-[10px] text-gray-400" />
33 + </div>
34 +
35 + <!-- 活动状态 -->
36 + <div class="flex items-center gap-1 text-[14px] text-gray-600">
37 + <span>活动状态</span>
38 + <van-icon name="play" class="rotate-90 text-[10px] text-gray-400" />
39 + </div>
40 + </div>
41 +
42 + <!-- 筛选图标 -->
43 + <van-icon name="filter-o" size="18" color="#333" />
44 + </div>
45 +
46 + <!-- 活动列表 -->
47 + <div class="space-y-3 p-3">
48 + <div
49 + v-for="item in activityList"
50 + :key="item.id"
51 + class="flex gap-3 rounded-xl bg-white p-3 shadow-sm transition-transform active:scale-[0.99]"
52 + >
53 + <!-- 左侧封面图 -->
54 + <div
55 + class="relative h-[100px] w-[100px] flex-shrink-0 overflow-hidden rounded-lg bg-gray-100"
56 + >
57 + <img
58 + :src="getOptimizedImage(item.imageUrl)"
59 + :alt="item.title"
60 + class="h-full w-full object-cover"
61 + />
62 + </div>
63 +
64 + <!-- 右侧内容 -->
65 + <div class="flex flex-1 flex-col justify-between py-0.5">
66 + <!-- 标题 -->
67 + <h3 class="line-clamp-2 text-[15px] font-bold leading-snug text-gray-900">
68 + {{ item.fullTitle }}
69 + </h3>
70 +
71 + <!-- 信息区域 -->
72 + <div class="space-y-1.5">
73 + <!-- 地点与状态 -->
74 + <div class="flex items-center gap-2">
75 + <span class="text-xs text-gray-500">地点: {{ item.location }}</span>
76 + <span
77 + class="rounded border px-1.5 py-0.5 text-[10px]"
78 + :class="getStatusStyle(item.status)"
79 + >
80 + {{ item.status }}
81 + </span>
82 + </div>
83 +
84 + <!-- 时间 -->
85 + <div class="text-xs text-gray-400">时间: {{ item.period }}</div>
86 + </div>
87 + </div>
88 + </div>
89 + </div>
90 + </div>
91 +</template>
92 +
93 +<script setup>
94 +import { ref, computed } from 'vue'
95 +import { activities } from '@/utils/mockData'
96 +
97 +/**
98 + * 处理图片链接,如果是指定CDN域名则添加压缩参数
99 + * @param {string} url - 图片链接
100 + * @returns {string} - 处理后的链接
101 + */
102 +const getOptimizedImage = url => {
103 + if (!url) return ''
104 + if (url.includes('cdn.ipadbiz.cn')) {
105 + // 检查是否已有参数
106 + return url.includes('?')
107 + ? `${url}&imageMogr2/thumbnail/200x/strip/quality/70`
108 + : `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
109 + }
110 + return url
111 +}
112 +
113 +// 处理活动数据,拼接标题
114 +const activityList = computed(() =>
115 + activities.map(item => ({
116 + ...item,
117 + fullTitle: item.subtitle ? `${item.title} ${item.subtitle}` : item.title,
118 + }))
119 +)
120 +
121 +/**
122 + * 获取状态样式
123 + * @param {string} status - 状态文本
124 + * @returns {string} - Tailwind 类名
125 + */
126 +const getStatusStyle = status => {
127 + if (status === '活动中') {
128 + return 'bg-blue-50 text-blue-500 border-blue-100'
129 + }
130 + if (status === '进行中' || status === '即将开始') {
131 + return 'bg-orange-50 text-orange-500 border-orange-100'
132 + }
133 + return 'bg-gray-50 text-gray-500 border-gray-100'
134 +}
135 +</script>
136 +
137 +<style lang="less" scoped>
138 +// 自定义导航栏样式以匹配设计图
139 +:deep(.van-nav-bar) {
140 + --van-nav-bar-height: 46px;
141 + .van-nav-bar__content {
142 + height: 46px;
143 + }
144 +}
145 +
146 +// 隐藏滚动条但保持滚动
147 +.hide-scrollbar::-webkit-scrollbar {
148 + display: none;
149 +}
150 +.hide-scrollbar {
151 + -ms-overflow-style: none;
152 + scrollbar-width: none;
153 +}
154 +</style>
1 +<template>
2 + <div class="pb-safe min-h-screen bg-gray-50">
3 + <!-- 顶部导航栏 -->
4 + <van-nav-bar title="课程" left-arrow fixed placeholder class="nav-bar" />
5 +
6 + <!-- 选项卡 -->
7 + <div class="bg-white shadow-sm">
8 + <van-tabs
9 + v-model:active="activeTab"
10 + :line-width="40"
11 + :line-height="3"
12 + color="#4caf50"
13 + title-active-color="#4caf50"
14 + title-inactive-color="#666666"
15 + :swipeable="false"
16 + >
17 + <van-tab title="全部" title-class="tab-title"></van-tab>
18 + <van-tab title="已购买" title-class="tab-title"></van-tab>
19 + <van-tab title="免费" title-class="tab-title"></van-tab>
20 + </van-tabs>
21 + </div>
22 +
23 + <!-- 课程列表 -->
24 + <div class="p-3 pb-20">
25 + <div
26 + v-for="course in courses"
27 + :key="course.id"
28 + class="mb-3 cursor-pointer overflow-hidden rounded-xl bg-white shadow-md transition-transform active:scale-95"
29 + @click="handleCourseClick(course)"
30 + >
31 + <!-- 课程封面 -->
32 + <div class="relative h-44 w-full overflow-hidden">
33 + <img :src="course.cover" :alt="course.title" class="h-full w-full object-cover" />
34 + <!-- 免费标签 -->
35 + <div
36 + v-if="course.isFree"
37 + class="absolute left-3 top-3 rounded-full bg-gradient-to-r from-purple-500 to-indigo-500 px-3 py-1 text-xs font-semibold text-white shadow-md"
38 + >
39 + 免费
40 + </div>
41 + </div>
42 +
43 + <!-- 课程信息 -->
44 + <div class="p-3">
45 + <!-- 课程标题 -->
46 + <h3 class="mb-2 line-clamp-2 text-base font-semibold leading-snug text-gray-900">
47 + {{ course.title }}
48 + </h3>
49 +
50 + <!-- 统计信息 -->
51 + <div class="mb-2 flex items-center gap-3">
52 + <span class="flex items-center gap-1 text-xs text-gray-500">
53 + <van-icon name="user-o" :size="12" color="#4caf50" />
54 + {{ course.studentCount }}
55 + </span>
56 + <span class="flex items-center gap-1 text-xs text-gray-500">
57 + <van-icon name="video-o" :size="12" color="#4caf50" />
58 + {{ course.lessonCount }}课时
59 + </span>
60 + </div>
61 +
62 + <!-- 讲师信息 -->
63 + <div class="flex items-center gap-1.5">
64 + <img
65 + :src="course.teacherAvatar"
66 + :alt="course.teacherName"
67 + class="h-5 w-5 rounded-full object-cover"
68 + />
69 + <span class="text-sm text-gray-600">{{ course.teacherName }}</span>
70 + </div>
71 + </div>
72 + </div>
73 + </div>
74 +
75 + <!-- 底部导航栏(装饰用) -->
76 + <van-tabbar
77 + v-model="activeTabbar"
78 + fixed
79 + :safe-area-inset-bottom="true"
80 + active-color="#4caf50"
81 + inactive-color="#999999"
82 + >
83 + <van-tabbar-item icon="wap-home-o">首页</van-tabbar-item>
84 + <van-tabbar-item icon="apps-o">课程</van-tabbar-item>
85 + <van-tabbar-item icon="chat-o">活动</van-tabbar-item>
86 + <van-tabbar-item icon="user-o">我的</van-tabbar-item>
87 + </van-tabbar>
88 + </div>
89 +</template>
90 +
91 +<script setup>
92 +import { ref } from 'vue'
93 +
94 +// 当前激活的选项卡
95 +const activeTab = ref(0)
96 +
97 +// 底部导航栏激活项
98 +const activeTabbar = ref(1)
99 +
100 +// 模拟课程数据
101 +const courses = ref([
102 + {
103 + id: 1,
104 + title: '家庭教育指导师(初级)第12期',
105 + cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-1.jpg',
106 + isFree: true,
107 + studentCount: 1289,
108 + lessonCount: 24,
109 + teacherName: '张老师',
110 + teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-1.jpg',
111 + },
112 + {
113 + id: 2,
114 + title: '智慧父母成长营第8期',
115 + cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-2.jpg',
116 + isFree: false,
117 + studentCount: 856,
118 + lessonCount: 16,
119 + teacherName: '李老师',
120 + teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-2.jpg',
121 + },
122 + {
123 + id: 3,
124 + title: '儿童心理学基础课程',
125 + cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-3.jpg',
126 + isFree: true,
127 + studentCount: 2341,
128 + lessonCount: 32,
129 + teacherName: '王老师',
130 + teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-3.jpg',
131 + },
132 +])
133 +
134 +// 点击课程卡片
135 +const handleCourseClick = course => {
136 + console.log('点击课程:', course.title)
137 +}
138 +</script>
139 +
140 +<style lang="less" scoped>
141 +// 仅用于修改 Vant 组件的深层样式
142 +:deep(.nav-bar) {
143 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
144 +
145 + .van-nav-bar__title {
146 + color: #ffffff;
147 + font-size: 18px;
148 + font-weight: 600;
149 + }
150 +
151 + .van-icon {
152 + color: #ffffff;
153 + }
154 +}
155 +
156 +:deep(.van-tabs) {
157 + .van-tabs__nav {
158 + padding-bottom: 0;
159 + }
160 +
161 + .van-tab {
162 + flex: 1;
163 + padding: 0;
164 +
165 + .tab-title {
166 + font-size: 15px;
167 + font-weight: 500;
168 + }
169 + }
170 +
171 + .van-tabs__line {
172 + bottom: 0;
173 + }
174 +}
175 +
176 +:deep(.van-tabbar) {
177 + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
178 +}
179 +
180 +// TailwindCSS 不支持的样式(自定义安全区域)
181 +.pb-safe {
182 + padding-bottom: constant(safe-area-inset-bottom);
183 + padding-bottom: env(safe-area-inset-bottom);
184 +}
185 +
186 +// TailwindCSS 不支持的样式(行数限制)
187 +.line-clamp-2 {
188 + display: -webkit-box;
189 + -webkit-box-orient: vertical;
190 + -webkit-line-clamp: 2;
191 + overflow: hidden;
192 +}
193 +</style>