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 @@
说明:该章节从 README 迁移到本文件,避免 README 过长。后续新增变更建议追加在文件顶部。
## 2026-01-29 15:30:00
- 新增活动列表测试页面:[/src/views/test/ActivityListTestPage.vue](file:///Users/huyirui/program/itomix/git/mlaj/src/views/test/ActivityListTestPage.vue)
- 实现高保真 UI 还原(导航栏、筛选栏、活动列表卡片)
- 配置测试路由:`/test/activity-list`
## 2026-01-26 13:40:00
- 优化打卡卡片组件(CheckinCard):
- 增加长文本折叠功能:内容超过5行自动显示省略号,并提供“全文/收起”切换按钮
......
......@@ -4,6 +4,7 @@
*/
import vue from 'eslint-plugin-vue'
import prettier from 'eslint-config-prettier'
import vueParser from 'vue-eslint-parser'
export default [
{
......@@ -32,6 +33,14 @@ export default [
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: vueParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
globals: {
// 浏览器环境
window: 'readonly',
......
......@@ -104,6 +104,7 @@
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.0",
"vitest": "^3.2.0"
"vitest": "^3.2.0",
"vue-eslint-parser": "^10.2.0"
}
}
......
......@@ -177,6 +177,9 @@ importers:
vitest:
specifier: ^3.2.0
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)
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0(eslint@9.39.2(jiti@1.21.7))
packages:
......
......@@ -94,6 +94,8 @@ declare module 'vue' {
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanTab: typeof import('vant/es')['Tab']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VanTimePicker: typeof import('vant/es')['TimePicker']
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-20 10:23:54
* @LastEditTime: 2026-01-29 00:08:03
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
......@@ -16,8 +16,8 @@ export const routes = [
meta: {
title: '欢迎页',
requiresAuth: false,
hideInMenu: true
}
hideInMenu: true,
},
},
{
path: '/',
......@@ -264,6 +264,18 @@ export const routes = [
meta: { title: 'test' },
},
{
path: '/test/course-list',
name: 'CourseListTest',
component: () => import('../views/test/CourseListTestPage.vue'),
meta: { title: '课程列表测试' },
},
{
path: '/test/activity-list',
name: 'ActivityListTest',
component: () => import('../views/test/ActivityListTestPage.vue'),
meta: { title: '活动列表测试' },
},
{
path: '/test/icon',
name: 'IconTest',
component: () => import('../views/IconTestPage.vue'),
......@@ -280,19 +292,20 @@ export const routes = [
name: 'upload_video',
component: () => import('../views/upload_video.vue'),
meta: { title: 'upload_video' },
}, {
},
{
path: '/auth',
component: () => import('@/views/auth.vue'),
meta: {
title: '微信授权页面',
}
},
},
{
path: '/study',
component: () => import('@/views/study/studyPage.vue'),
meta: {
title: '学习页面',
}
},
},
{
path: '/studyDetail/:id',
......@@ -300,7 +313,7 @@ export const routes = [
meta: {
title: '学习详情页面',
requiresAuth: true,
}
},
},
{
path: '/pdfPreview',
......@@ -308,14 +321,14 @@ export const routes = [
component: () => import('@/views/study/PdfPreviewPage.vue'),
meta: {
title: 'PDF预览',
}
},
},
{
path: '/profile/studyCourse/:id',
component: () => import('@/views/profile/StudyCoursePage.vue'),
meta: {
title: '课程集合页面',
}
},
},
...checkinRoutes,
...teacherRoutes,
......
<template>
<div class="pb-safe min-h-screen bg-gray-50">
<!-- 顶部导航栏 -->
<van-nav-bar fixed placeholder z-index="50" class="nav-bar">
<template #left>
<van-icon name="arrow-left" size="22" color="#333" />
</template>
<template #title>
<span class="text-lg font-medium text-gray-800">最新活动</span>
</template>
<template #right>
<van-icon name="ellipsis" size="22" color="#333" />
</template>
</van-nav-bar>
<!-- 筛选栏 -->
<div
class="sticky top-[46px] z-40 flex items-center justify-between bg-white px-3 py-2 shadow-sm"
>
<div class="flex items-center gap-6">
<!-- 全部 -->
<div class="relative text-[15px] font-bold text-gray-900">
全部
<div
class="absolute -bottom-2 left-1/2 h-[3px] w-4 -translate-x-1/2 rounded-full bg-gray-800"
></div>
</div>
<!-- 活动地点 -->
<div class="flex items-center gap-1 text-[14px] text-gray-600">
<span>活动地点</span>
<van-icon name="play" class="rotate-90 text-[10px] text-gray-400" />
</div>
<!-- 活动状态 -->
<div class="flex items-center gap-1 text-[14px] text-gray-600">
<span>活动状态</span>
<van-icon name="play" class="rotate-90 text-[10px] text-gray-400" />
</div>
</div>
<!-- 筛选图标 -->
<van-icon name="filter-o" size="18" color="#333" />
</div>
<!-- 活动列表 -->
<div class="space-y-3 p-3">
<div
v-for="item in activityList"
:key="item.id"
class="flex gap-3 rounded-xl bg-white p-3 shadow-sm transition-transform active:scale-[0.99]"
>
<!-- 左侧封面图 -->
<div
class="relative h-[100px] w-[100px] flex-shrink-0 overflow-hidden rounded-lg bg-gray-100"
>
<img
:src="getOptimizedImage(item.imageUrl)"
:alt="item.title"
class="h-full w-full object-cover"
/>
</div>
<!-- 右侧内容 -->
<div class="flex flex-1 flex-col justify-between py-0.5">
<!-- 标题 -->
<h3 class="line-clamp-2 text-[15px] font-bold leading-snug text-gray-900">
{{ item.fullTitle }}
</h3>
<!-- 信息区域 -->
<div class="space-y-1.5">
<!-- 地点与状态 -->
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">地点: {{ item.location }}</span>
<span
class="rounded border px-1.5 py-0.5 text-[10px]"
:class="getStatusStyle(item.status)"
>
{{ item.status }}
</span>
</div>
<!-- 时间 -->
<div class="text-xs text-gray-400">时间: {{ item.period }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { activities } from '@/utils/mockData'
/**
* 处理图片链接,如果是指定CDN域名则添加压缩参数
* @param {string} url - 图片链接
* @returns {string} - 处理后的链接
*/
const getOptimizedImage = url => {
if (!url) return ''
if (url.includes('cdn.ipadbiz.cn')) {
// 检查是否已有参数
return url.includes('?')
? `${url}&imageMogr2/thumbnail/200x/strip/quality/70`
: `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
}
return url
}
// 处理活动数据,拼接标题
const activityList = computed(() =>
activities.map(item => ({
...item,
fullTitle: item.subtitle ? `${item.title} ${item.subtitle}` : item.title,
}))
)
/**
* 获取状态样式
* @param {string} status - 状态文本
* @returns {string} - Tailwind 类名
*/
const getStatusStyle = status => {
if (status === '活动中') {
return 'bg-blue-50 text-blue-500 border-blue-100'
}
if (status === '进行中' || status === '即将开始') {
return 'bg-orange-50 text-orange-500 border-orange-100'
}
return 'bg-gray-50 text-gray-500 border-gray-100'
}
</script>
<style lang="less" scoped>
// 自定义导航栏样式以匹配设计图
:deep(.van-nav-bar) {
--van-nav-bar-height: 46px;
.van-nav-bar__content {
height: 46px;
}
}
// 隐藏滚动条但保持滚动
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
<template>
<div class="pb-safe min-h-screen bg-gray-50">
<!-- 顶部导航栏 -->
<van-nav-bar title="课程" left-arrow fixed placeholder class="nav-bar" />
<!-- 选项卡 -->
<div class="bg-white shadow-sm">
<van-tabs
v-model:active="activeTab"
:line-width="40"
:line-height="3"
color="#4caf50"
title-active-color="#4caf50"
title-inactive-color="#666666"
:swipeable="false"
>
<van-tab title="全部" title-class="tab-title"></van-tab>
<van-tab title="已购买" title-class="tab-title"></van-tab>
<van-tab title="免费" title-class="tab-title"></van-tab>
</van-tabs>
</div>
<!-- 课程列表 -->
<div class="p-3 pb-20">
<div
v-for="course in courses"
:key="course.id"
class="mb-3 cursor-pointer overflow-hidden rounded-xl bg-white shadow-md transition-transform active:scale-95"
@click="handleCourseClick(course)"
>
<!-- 课程封面 -->
<div class="relative h-44 w-full overflow-hidden">
<img :src="course.cover" :alt="course.title" class="h-full w-full object-cover" />
<!-- 免费标签 -->
<div
v-if="course.isFree"
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"
>
免费
</div>
</div>
<!-- 课程信息 -->
<div class="p-3">
<!-- 课程标题 -->
<h3 class="mb-2 line-clamp-2 text-base font-semibold leading-snug text-gray-900">
{{ course.title }}
</h3>
<!-- 统计信息 -->
<div class="mb-2 flex items-center gap-3">
<span class="flex items-center gap-1 text-xs text-gray-500">
<van-icon name="user-o" :size="12" color="#4caf50" />
{{ course.studentCount }}
</span>
<span class="flex items-center gap-1 text-xs text-gray-500">
<van-icon name="video-o" :size="12" color="#4caf50" />
{{ course.lessonCount }}课时
</span>
</div>
<!-- 讲师信息 -->
<div class="flex items-center gap-1.5">
<img
:src="course.teacherAvatar"
:alt="course.teacherName"
class="h-5 w-5 rounded-full object-cover"
/>
<span class="text-sm text-gray-600">{{ course.teacherName }}</span>
</div>
</div>
</div>
</div>
<!-- 底部导航栏(装饰用) -->
<van-tabbar
v-model="activeTabbar"
fixed
:safe-area-inset-bottom="true"
active-color="#4caf50"
inactive-color="#999999"
>
<van-tabbar-item icon="wap-home-o">首页</van-tabbar-item>
<van-tabbar-item icon="apps-o">课程</van-tabbar-item>
<van-tabbar-item icon="chat-o">活动</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 当前激活的选项卡
const activeTab = ref(0)
// 底部导航栏激活项
const activeTabbar = ref(1)
// 模拟课程数据
const courses = ref([
{
id: 1,
title: '家庭教育指导师(初级)第12期',
cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-1.jpg',
isFree: true,
studentCount: 1289,
lessonCount: 24,
teacherName: '张老师',
teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-1.jpg',
},
{
id: 2,
title: '智慧父母成长营第8期',
cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-2.jpg',
isFree: false,
studentCount: 856,
lessonCount: 16,
teacherName: '李老师',
teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-2.jpg',
},
{
id: 3,
title: '儿童心理学基础课程',
cover: 'https://cdn.ipadbiz.cn/public/mlaj/course-cover-3.jpg',
isFree: true,
studentCount: 2341,
lessonCount: 32,
teacherName: '王老师',
teacherAvatar: 'https://cdn.ipadbiz.cn/public/mlaj/teacher-3.jpg',
},
])
// 点击课程卡片
const handleCourseClick = course => {
console.log('点击课程:', course.title)
}
</script>
<style lang="less" scoped>
// 仅用于修改 Vant 组件的深层样式
:deep(.nav-bar) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.van-nav-bar__title {
color: #ffffff;
font-size: 18px;
font-weight: 600;
}
.van-icon {
color: #ffffff;
}
}
:deep(.van-tabs) {
.van-tabs__nav {
padding-bottom: 0;
}
.van-tab {
flex: 1;
padding: 0;
.tab-title {
font-size: 15px;
font-weight: 500;
}
}
.van-tabs__line {
bottom: 0;
}
}
:deep(.van-tabbar) {
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
// TailwindCSS 不支持的样式(自定义安全区域)
.pb-safe {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
// TailwindCSS 不支持的样式(行数限制)
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>