Bladeren bron

refactor: 重构学科页面业务逻辑,优化交互体验

1. 替换fetchSubjectConfigInfo为fetchSubjectAppInfo接口
2. 新增横向滚动导航自动选中与滚动定位功能
3. 统一学科列表数据结构与渲染逻辑
4. 修复catalog页面多余的categoryRefs引用
5. 优化首页悬浮按钮样式与图标
408249787@qq.com 1 week geleden
bovenliggende
commit
c374d341e1
8 gewijzigde bestanden met toevoegingen van 184 en 43 verwijderingen
  1. 1 1
      pages/card/index.uvue
  2. 2 3
      pages/catalog/index.uvue
  3. 34 6
      pages/chinese/index.uvue
  4. 35 6
      pages/english/index.uvue
  5. 35 6
      pages/game/index.uvue
  6. 8 8
      pages/index/home.uvue
  7. 35 7
      pages/math/index.uvue
  8. 34 6
      pages/mix/index.uvue

+ 1 - 1
pages/card/index.uvue

@@ -3,7 +3,7 @@ import Back from '@/components/back.uvue'
 import Loading from '@/components/loading.uvue'
 import { ref, onMounted, nextTick } from 'vue'
 import { type SubjectKnowledgeCardResult, getSubjectKnowledgeCard } from '@/services/subject/card'
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
 import { config } from '@/config'
 import { dict } from '@/.cool/store'

+ 2 - 3
pages/catalog/index.uvue

@@ -15,7 +15,7 @@ const isLoading = ref(true)
 const visible = ref<boolean>(false)
 const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
-const courseList = ref<SubjectCourseResult>()
+const courseList = ref<SubjectCourseResult[]>()
 const record = ref<any>()
 async function getDataList() {
   const id = dict.getValueByLabelMapByType('index_subject_id')['物理']
@@ -36,7 +36,6 @@ onShow(async () => {
   }
 })
 const cardsScrollView = ref<any>(null)
-const categoryRefs = ref<any>()
 
 function handleDetail(item: SubjectCatalogResult) {
   // if (!item.payFlag && !item.trialPlay) {
@@ -127,7 +126,7 @@ function handleClose(val: boolean) {
       <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false" ref="cardsScrollView"
         @scroll="debouncedOnScroll">
         <view class="scroll-view-item_H bg-[white]" v-for="course in courseList || []"
-          :class="`category-${course.catalogId}`" :key="course.id" @tap="handleDetail(course)" ref="categoryRefs">
+          :class="`category-${course.catalogId}`" :key="course.id" @tap="handleDetail(course)" >
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="scaleToFill"
             class="!w-full !h-[28vh] mb-[5px] rounded-xl"></cl-image>
           <text class="text-[4vh] font-bold">{{

+ 34 - 6
pages/chinese/index.uvue

@@ -1,9 +1,10 @@
 <script lang="ts" setup>
 import { user } from '@/.cool'
 import { ref, onMounted, computed } from 'vue'
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
 import Progress from '../catalog/components/progress.uvue'
+import type { SubjectCourseResult } from '@/services/subject/course'
 import Back from '@/components/back.uvue'
 import Lock from '@/components/lock.uvue'
 import Loading from '@/components/loading.uvue'
@@ -14,13 +15,15 @@ const isLoading = ref(true)
 const visible = ref<boolean>(false)
 const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
+const courseList = ref<SubjectCourseResult[]>()
 const record = ref<any>()
 async function getDataList() {
   const id = dict.getValueByLabelMapByType('index_subject_id')['语文']
-  const res = await fetchSubjectConfigInfo({ id,delFlag: false })
+  const res = await fetchSubjectAppInfo({ id,delFlag: false })
   record.value = res
   dataList.value = res.catalogList || []
   catalog.value = res?.catalogList?.[0]
+  courseList.value = res?.courseList || []
 }
 const userInfo = computed(() => user.info.value?.userInfo)
 
@@ -33,9 +36,17 @@ onMounted(async () => {
     isLoading.value = false
   }
 })
-function handleSelect(item: SubjectCatalogResult) {
-  catalog.value = item
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
   visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec(async (rect) => {
+    if (cardsScrollView.value && rect[0]) {
+      cardsScrollView.value.scrollTo({
+        left: scrollLeft.value + rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
+        animated: true
+      })
+    }
+  })
 }
 function handleDetail(item: SubjectCatalogResult) {
   if (!item.payFlag && !item.trialPlay) {
@@ -52,6 +63,23 @@ function handleDetail(item: SubjectCatalogResult) {
     }
   });
 }
+const cardsScrollView = ref<any>(null)
+const scrollLeft = ref<any>(0)
+function debouncedOnScroll(e: any) {
+  console.log(e);
+  scrollLeft.value = e.detail.scrollLeft
+  dataList.value.forEach(async (category) => {
+    const selector = `.category-${category.id}`
+    await uni.createSelectorQuery().selectAll(selector).boundingClientRect().exec((rects) => {
+      for (const rect of rects[0]) {
+        if (rect.left <= (dict.getWindowHeight() / 2) && rect.left > 0) {
+          catalog.value = category
+          return
+        }
+      }
+    })
+  })
+}
 </script>
 
 <template>
@@ -73,8 +101,8 @@ function handleDetail(item: SubjectCatalogResult) {
       <cl-icon name="arrow-left-right-line" color="primary"></cl-icon>
     </view>
     <view class="boxs">
-      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false">
-        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id"
+      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false"  ref="cardsScrollView"  @scroll="debouncedOnScroll">
+        <view class="scroll-view-item_H bg-[white]"  :class="`category-${course.catalogId}`" v-for="course in courseList || []" :key="course.id"
           @tap="handleDetail(course)">
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="heightFix"
             class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>

+ 35 - 6
pages/english/index.uvue

@@ -1,13 +1,15 @@
 <script lang="ts" setup>
 import { user } from '@/.cool'
 import { ref, onMounted, computed } from 'vue'
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
+import type { SubjectCourseResult } from '@/services/subject/course'
 import Progress from '../catalog/components/progress.uvue'
 import Back from '@/components/back.uvue'
 import Lock from '@/components/lock.uvue'
 import Loading from '@/components/loading.uvue'
 import { config } from '@/config'
+import { dict } from '@/.cool/store'
 import { router } from "@/.cool";
 const isLoading = ref(true)
 const visible = ref<boolean>(false)
@@ -15,11 +17,13 @@ const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
 const record = ref<any>()
 const userInfo = computed(() => user.info.value?.userInfo)
+const courseList = ref<SubjectCourseResult[]>()
 async function getDataList() {
-  const res = await fetchSubjectConfigInfo({ id: router.query().id,delFlag: false })
+  const res = await fetchSubjectAppInfo({ id: router.query().id,delFlag: false })
   record.value = res
   dataList.value = res.catalogList || []
   catalog.value = res?.catalogList?.[0]
+  courseList.value = res?.courseList || []
 }
 onMounted(async () => {
   try {
@@ -30,9 +34,19 @@ onMounted(async () => {
     isLoading.value = false
   }
 })
-function handleSelect(item: SubjectCatalogResult) {
-  catalog.value = item
+const scrollLeft = ref<any>(0)
+const cardsScrollView = ref<any>(null)
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
   visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec(async (rect) => {
+    if (cardsScrollView.value && rect[0]) {
+      cardsScrollView.value.scrollTo({
+        left: scrollLeft.value + rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
+        animated: true
+      })
+    }
+  })
 }
 function handleDetail(item: SubjectCatalogResult) {
   if (!item.payFlag && !item.trialPlay) {
@@ -49,6 +63,21 @@ function handleDetail(item: SubjectCatalogResult) {
     }
   });
 }
+function debouncedOnScroll(e: any) {
+  console.log(e);
+  scrollLeft.value = e.detail.scrollLeft
+  dataList.value.forEach(async (category) => {
+    const selector = `.category-${category.id}`
+    await uni.createSelectorQuery().selectAll(selector).boundingClientRect().exec((rects) => {
+      for (const rect of rects[0]) {
+        if (rect.left <= (dict.getWindowHeight() / 2) && rect.left > 0) {
+          catalog.value = category
+          return
+        }
+      }
+    })
+  })
+}
 </script>
 
 <template>
@@ -71,8 +100,8 @@ function handleDetail(item: SubjectCatalogResult) {
       <cl-icon name="arrow-left-right-line" color="primary"></cl-icon>
     </view>
     <view class="boxs">
-      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false">
-        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id"
+      <scroll-view class="scroll-view_H" direction="horizontal" ref="cardsScrollView" :show-scrollbar="false" @scroll="debouncedOnScroll">
+        <view class="scroll-view-item_H bg-[white]"  :class="`category-${course.catalogId}`" v-for="course in courseList || []" :key="course.id"
           @tap="handleDetail(course)">
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="heightFix"
             class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>

+ 35 - 6
pages/game/index.uvue

@@ -1,8 +1,10 @@
 <script lang="ts" setup>
 import { user } from '@/.cool'
 import { ref, onMounted, computed } from 'vue'
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
+import type { SubjectCourseResult } from '@/services/subject/course'
+
 import Progress from '../catalog/components/progress.uvue'
 import Back from '@/components/back.uvue'
 import Lock from '@/components/lock.uvue'
@@ -16,12 +18,14 @@ const visible = ref<boolean>(false)
 const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
 const record = ref<any>()
+const courseList = ref<SubjectCourseResult[]>()
 async function getDataList() {
   const id = dict.getValueByLabelMapByType('index_subject_id')['娱乐']
-  const res = await fetchSubjectConfigInfo({ id,delFlag: false })
+  const res = await fetchSubjectAppInfo({ id,delFlag: false })
   record.value = res
   dataList.value = res.catalogList || []
   catalog.value = res?.catalogList?.[0]
+  courseList.value = res?.courseList || []
 }
 onMounted(async () => {
   try {
@@ -32,9 +36,34 @@ onMounted(async () => {
     isLoading.value = false
   }
 })
-function handleSelect(item: SubjectCatalogResult) {
-  catalog.value = item
+const cardsScrollView = ref<any>(null)
+const scrollLeft = ref<any>(0)
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
   visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec(async (rect) => {
+    if (cardsScrollView.value && rect[0]) {
+      cardsScrollView.value.scrollTo({
+        left: scrollLeft.value + rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
+        animated: true
+      })
+    }
+  })
+}
+function debouncedOnScroll(e: any) {
+  console.log(e);
+  scrollLeft.value = e.detail.scrollLeft
+  dataList.value.forEach(async (category) => {
+    const selector = `.category-${category.id}`
+    await uni.createSelectorQuery().selectAll(selector).boundingClientRect().exec((rects) => {
+      for (const rect of rects[0]) {
+        if (rect.left <= (dict.getWindowHeight() / 2) && rect.left > 0) {
+          catalog.value = category
+          return
+        }
+      }
+    })
+  })
 }
 function handleDetail(item: SubjectCatalogResult) {
   if (!item.payFlag && !item.trialPlay) {
@@ -72,8 +101,8 @@ function handleDetail(item: SubjectCatalogResult) {
       <cl-icon name="arrow-left-right-line" color="primary"></cl-icon>
     </view>
     <view class="boxs">
-      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false">
-        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id"
+      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false" ref="cardsScrollView" @scroll="debouncedOnScroll">
+        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id" :class="`category-${course.catalogId}`"
           @tap="handleDetail(course)">
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="heightFix"
             class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>

+ 8 - 8
pages/index/home.uvue

@@ -84,19 +84,20 @@ function copyUrl() {
 			<view class="w-[87vw] relative z-[2] p-1 px-3 flex flex-row items-center justify-between gap-1 mx-auto ">
 				<view v-for="item in menuList" :key="item.code"
 					class="flex flex-col items-center p-1 px-4 justify-center gap-1  transition-all duration-300"
-					@tap="handlePage(item)" :class="{ 'selected': item.code === selected }" >
+					@tap="handlePage(item)" :class="{ 'selected': item.code === selected }">
 					<image :src="icons[item.code]" mode="aspectFill" class="w-[4vw] h-[4vw]"></image>
 					<text class="text-[1.5vw] text-[#1E1E1E]">{{ item.label }}</text>
 				</view>
 
 			</view>
 		</view>
-		<cl-float-view :left="20" :bottom="200" >
-			<view class="flex flex-col items-center p-1 px-4 justify-center gap-1 w-[40px] h-[40px] transition-all duration-300"
+		<cl-float-view :left="20" :bottom="100">
+			<view
+				class="flex flex-col items-center p-1 px-4 justify-center gap-1 w-[40px] h-[40px] transition-all duration-300"
 				@tap="visible = true">
 				<view class="bg-[#09ba07] p-[1vw] rounded-full w-[40px] h-[40px] flex items-center justify-center"
 					@tap="visible = true">
-					<image src="https://oss.xiaoxiongcode.com/static/个人中心/微信.png" class="w-[20px] h-[20px]"></image>
+					<cl-icon name="customer-service-line"  :size="20" color="#FFF"></cl-icon>
 				</view>
 			</view>
 		</cl-float-view>
@@ -119,16 +120,15 @@ function copyUrl() {
 		<cl-popup v-model="visible2" :size="400" :show-header="false" direction="center">
 			<view class="flex flex-col items-center justify-center gap-4 py-7 bg-slate-50">
 				<cl-text class="text-center " color="#09ba07" :size="30"> ——会员福利—— </cl-text>
-				<image src="https://oss.xiaoxiongcode.com/static/home/coding.png" 
-					class="w-32 h-32" />
+				<image src="https://oss.xiaoxiongcode.com/static/home/coding.png" class="w-32 h-32" />
 				<view class="text-center " v-if="userInfo?.memberLevel !== 'default'">
-					 <cl-text class="text-center" color="#999" :size="14">账号:小程序登录手机账号  </cl-text>
+					<cl-text class="text-center" color="#999" :size="14">账号:小程序登录手机账号 </cl-text>
 					<cl-text class="text-center" color="#999" :size="14"> 密码:初始密码为123456 </cl-text>
 					<cl-text class="text-center" color="#999" :size="14">少儿编程网址: www.xiaoxiongcode.com </cl-text>
 					<cl-button type="primary" size="small" @tap="copyUrl"> 复制网址 </cl-button>
 				</view>
 				<view class="text-center " v-else>
-					 <cl-text class="text-center" color="#999" :size="14"> 您不是会员,无法使用会员福利 </cl-text>
+					<cl-text class="text-center" color="#999" :size="14"> 您不是会员,无法使用会员福利 </cl-text>
 				</view>
 			</view>
 		</cl-popup>

+ 35 - 7
pages/math/index.uvue

@@ -1,8 +1,10 @@
 <script lang="ts" setup>
 import { user } from '@/.cool'
 import { ref, onMounted, computed } from 'vue' 
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
+import type { SubjectCourseResult } from '@/services/subject/course'
+
 import Progress from '../catalog/components/progress.uvue'
 import Back from '@/components/back.uvue'
 import Lock from '@/components/lock.uvue'
@@ -15,13 +17,14 @@ const visible = ref<boolean>(false)
 const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
 const record = ref<any>()
-
+const courseList = ref<SubjectCourseResult[]>()
 async function getDataList() {
   const id = dict.getValueByLabelMapByType('index_subject_id')['数学']
-  const res = await fetchSubjectConfigInfo({ id,delFlag: false, })
+  const res = await fetchSubjectAppInfo({ id,delFlag: false, })
   record.value = res
   dataList.value = res.catalogList || []
   catalog.value = res?.catalogList?.[0]
+  courseList.value = res?.courseList || []
 }
 onMounted(async () => {
   try {
@@ -32,9 +35,34 @@ onMounted(async () => {
     isLoading.value = false
   }
 })
-function handleSelect(item: SubjectCatalogResult) {
-  catalog.value = item
+const cardsScrollView = ref<any>(null)
+const scrollLeft = ref<any>(0)
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
   visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec(async (rect) => {
+    if (cardsScrollView.value && rect[0]) {
+      cardsScrollView.value.scrollTo({
+        left: scrollLeft.value + rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
+        animated: true
+      })
+    }
+  })
+}
+function debouncedOnScroll(e: any) {
+  console.log(e);
+  scrollLeft.value = e.detail.scrollLeft
+  dataList.value.forEach(async (category) => {
+    const selector = `.category-${category.id}`
+    await uni.createSelectorQuery().selectAll(selector).boundingClientRect().exec((rects) => {
+      for (const rect of rects[0]) {
+        if (rect.left <= (dict.getWindowHeight() / 2) && rect.left > 0) {
+          catalog.value = category
+          return
+        }
+      }
+    })
+  })
 }
 function handleDetail(item: SubjectCatalogResult) {
   if (!item.payFlag && !item.trialPlay) {
@@ -73,8 +101,8 @@ const userInfo = computed(() => user.info.value?.userInfo)
       <cl-icon name="arrow-left-right-line" color="primary"></cl-icon>
     </view>
     <view class="boxs">
-      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false">
-        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id"
+      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false" ref="cardsScrollView" @scroll="debouncedOnScroll">
+        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :class="`category-${course.catalogId}`" :key="course.id"
           @tap="handleDetail(course)">
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="heightFix"
             class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>

+ 34 - 6
pages/mix/index.uvue

@@ -1,8 +1,9 @@
 <script lang="ts" setup>
 import { user } from '@/.cool'
 import { ref, onMounted, computed } from 'vue'
-import { fetchSubjectConfigInfo } from '@/services/subject/info'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
 import type { SubjectCatalogResult } from '@/services/subject/catalog'
+import type { SubjectCourseResult } from '@/services/subject/course'
 import Progress from '../catalog/components/progress.uvue'
 import Back from '@/components/back.uvue'
 import Lock from '@/components/lock.uvue'
@@ -15,11 +16,13 @@ const dataList = ref<SubjectCatalogResult[]>([])
 const catalog = ref<SubjectCatalogResult>()
 const record = ref<any>()
 const userInfo = computed(() => user.info.value?.userInfo)
+const courseList = ref<SubjectCourseResult[]>()
 async function getDataList() {
-  const res = await fetchSubjectConfigInfo({ id: router.query().id,delFlag: false })
+  const res = await fetchSubjectAppInfo({ id: router.query().id,delFlag: false })
   record.value = res
   dataList.value = res.catalogList || []
   catalog.value = res?.catalogList?.[0]
+  courseList.value = res?.courseList || []
 }
 onMounted(async () => {
   try {
@@ -30,9 +33,34 @@ onMounted(async () => {
     isLoading.value = false
   }
 })
-function handleSelect(item: SubjectCatalogResult) {
-  catalog.value = item
+const cardsScrollView = ref<any>(null)
+const scrollLeft = ref<any>(0)
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
   visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec(async (rect) => {
+    if (cardsScrollView.value && rect[0]) {
+      cardsScrollView.value.scrollTo({
+        left: scrollLeft.value + rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
+        animated: true
+      })
+    }
+  })
+}
+function debouncedOnScroll(e: any) {
+  console.log(e);
+  scrollLeft.value = e.detail.scrollLeft
+  dataList.value.forEach(async (category) => {
+    const selector = `.category-${category.id}`
+    await uni.createSelectorQuery().selectAll(selector).boundingClientRect().exec((rects) => {
+      for (const rect of rects[0]) {
+        if (rect.left <= (dict.getWindowHeight() / 2) && rect.left > 0) {
+          catalog.value = category
+          return
+        }
+      }
+    })
+  })
 }
 function handleDetail(item: SubjectCatalogResult) {
 
@@ -65,8 +93,8 @@ function handleDetail(item: SubjectCatalogResult) {
       <cl-icon name="arrow-left-right-line" color="primary"></cl-icon>
     </view>
     <view class="boxs">
-      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false">
-        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :key="course.id"
+      <scroll-view class="scroll-view_H" direction="horizontal" :show-scrollbar="false" ref="cardsScrollView" @scroll="debouncedOnScroll">
+        <view class="scroll-view-item_H bg-[white]" v-for="course in catalog?.courseList || []" :class="`category-${course.catalogId}`" :key="course.id"
           @tap="handleDetail(course)">
           <cl-image :src="config.baseUrl + course?.fileList?.[0]?.url" mode="heightFix"
             class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>