408249787@qq.com 2 hafta önce
ebeveyn
işleme
72e1d54171

+ 2 - 2
config/proxy.ts

@@ -2,7 +2,7 @@ export const proxy = {
 	// 开发环境配置
 	dev: {
 		// 本地地址
-		target: "http://127.0.0.1:5001",
+		target: "http://127.0.0.1:5001/api",
 		changeOrigin: true,
 		rewrite: (path: string) => path.replace("/dev", "")
 	},
@@ -26,4 +26,4 @@ export const proxy = {
 	}
 };
 
-export const value = "dev";
+export const value = "prod";

+ 23 - 2
pages.json

@@ -63,7 +63,7 @@
 				"disableScroll": true
 			}
 		},
-			{
+		{
 			"path": "pages/math/index",
 			"style": {
 				"navigationStyle": "custom",
@@ -127,6 +127,27 @@
 				"navigationStyle": "custom",
 				"disableScroll": true
 			}
+		},
+		{
+			"path": "pages/programming/index",
+			"style": {
+				"navigationStyle": "custom",
+				"disableScroll": true
+			}
+		},
+		{
+			"path": "pages/programming/progress",
+			"style": {
+				"navigationStyle": "custom",
+				"disableScroll": true
+			}
+		},
+		{
+			"path": "pages/programming/detail",
+			"style": {
+				"navigationStyle": "custom",
+				"disableScroll": true
+			}
 		}
 	],
 	"globalStyle": {
@@ -146,4 +167,4 @@
 		}
 	},
 	"uniIdRouter": {}
-}
+}

+ 8 - 4
pages/chinese/index.uvue

@@ -111,9 +111,8 @@ const userInfo = computed(() => user.info.value?.userInfo)
           <view v-for="category in dataList" :key="category.id" class="sidebar-item"
             :class="{ active: catalog?.id === category.id }" @tap="handleSelect(category)">
             <!-- <cl-image :src="category?.ossIconPath" mode="heightFix" class="!h-[28px]"></cl-image> -->
-            <cl-text ellipsis :pt="{
-              className: 'we'
-            }"> {{ category.name }}</cl-text>
+            <text class="we">{{ category.name }}</text>
+            <cl-tag type="success" rounded>{{ category.courseNum || 0 }}</cl-tag>
           </view>
         </scroll-view>
       </view>
@@ -121,6 +120,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
       <view class="cards-container">
         <view class="header">
           {{ catalog?.name }}
+          <cl-tag type="success" rounded>{{ catalog?.courseNum || 0 }}</cl-tag>
         </view>
         <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
           @scroll="handleScrollToLowerDebounce">
@@ -150,7 +150,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
 </template>
 <style lang="scss" scoped>
 .header {
-  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full;
+  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full flex items-center gap-2 flex-row;
   transform: translateX(-50%);
   text-align: center;
   background-color: #fff;
@@ -198,6 +198,10 @@ const userInfo = computed(() => user.info.value?.userInfo)
       .we {
         color: #fff !important;
         font-size: 1.6vw !important;
+        overflow: hidden;
+        flex: 1;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
 
       &.active {

+ 9 - 5
pages/english/index.uvue

@@ -111,9 +111,8 @@ const userInfo = computed(() => user.info.value?.userInfo)
             :class="{ active: catalog?.id === category.id }" @tap="handleSelect(category)">
             <!-- <cl-image :src="category?.ossIconPath" mode="heightFix" class="!h-[28px]"></cl-image> -->
             <!-- {{ category.name }} -->
-            <cl-text ellipsis :pt="{
-              className: 'we'
-            }"> {{ category.name }}</cl-text>
+            <text class="we">{{ category.name }}</text>
+            <cl-tag type="success" rounded>{{ category.courseNum || 0 }}</cl-tag>
           </view>
         </scroll-view>
       </view>
@@ -121,6 +120,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
       <view class="cards-container">
         <view class="header">
           {{ catalog?.name }}
+          <cl-tag type="success" rounded>{{ catalog?.courseNum || 0 }}</cl-tag>
         </view>
         <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
           @scroll="handleScrollToLowerDebounce">
@@ -150,7 +150,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
 </template>
 <style lang="scss" scoped>
 .header {
-  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full;
+  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full flex items-center gap-2 flex-row;
   transform: translateX(-50%);
   text-align: center;
   background-color: #fff;
@@ -181,7 +181,11 @@ const userInfo = computed(() => user.info.value?.userInfo)
 
       .we {
         color: #fff !important;
-        font-size: 1.6vw important;
+        font-size: 1.6vw !important;
+        overflow: hidden;
+        flex: 1;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
 
       &.active {

+ 49 - 5
pages/index/components/exchange.uvue

@@ -3,16 +3,23 @@ import { ref, onMounted } from 'vue'
 import { exchangeCode } from '@/services/user'
 import { dict } from '@/.cool/store'
 import { user, router } from '@/.cool'
-
 import { useUi } from "@/uni_modules/cool-ui";
+import { fetchSubjectFeeRecord } from '@/services/subject/record'
 
+const recordList = ref<any[]>([])
 const ui = useUi();
 const code = ref<string>('')
+const loading = ref<boolean>(false)
 onMounted(() => {
   if (!user.info.value?.userInfo) {
     user.logout()
     return
   }
+  fetchSubjectFeeRecord({
+    createdUserId: user.info.value?.userInfo.userId
+  }).then(res => {
+    recordList.value = res
+  })
 })
 async function handleExchange() {
 
@@ -23,6 +30,7 @@ async function handleExchange() {
     })
     return
   }
+  loading.value = true
   try {
     await exchangeCode({
       exchangeCode: code.value,
@@ -37,25 +45,45 @@ async function handleExchange() {
         console.log("用户已确认:", action);
       },
     });
+    loading.value = false
   } catch (err: any) {
     uni.showToast({
       title: err.message,
       icon: 'error'
     })
+    loading.value = false
   }
 }
 </script>
 <template>
-  <view class="content">
+  <view class="content" v-if="user.info.value?.userInfo && user.info.value?.userInfo?.memberLevel === 'default'">
     <view class="text-[30px] font-bold">
       兑换码
     </view>
     <cl-input v-model="code" :pt="{
       className: '!h-[40px] ',
     }" placeholder="请输入兑换码"></cl-input>
-    <view class="text-[18px] w-[140px] rounded-full text-black  text-center check
-               to-pink-500 py-[5px] " @tap="handleExchange()">
-      兑换
+    <cl-button type="warn" :pt="{
+      className: 'text-[18px] w-[140px] rounded-full text-black  text-center check to-pink-500 py-[5px] ',
+    }" @tap="handleExchange()" :loading="loading">兑换</cl-button>
+
+  </view>
+  <view class="content2" v-else>
+    <view v-for="item in recordList" :key="item" class="box flex flex-row  items-center gap-2">
+      <view class="h-full">
+        <image src="https://oss.xiaoxiongcode.com/static/个人中心/图层 13.png" mode="heightFix" class=" h-full" />
+      </view>
+      <view class="flex flex-col justify-around h-full">
+        <view class="text-[5vh]  font-bold text-[#FC500F]">
+          {{ item.mainTitle }}
+        </view>
+        <view class="text-[3vh] text-[#999]">
+          兑换时间:{{ item.createdTime }}
+        </view>
+        <view class="text-[3vh]  text-[#999]">
+          到期时间:{{ item.updateTime }}
+        </view>
+      </view>
     </view>
   </view>
 </template>
@@ -65,6 +93,22 @@ async function handleExchange() {
 
 }
 
+.content2 {
+  @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
+  overflow: hidden;
+  border-radius: 10px;
+}
+
+.box {
+  width: 120vh;
+  // 宽高比3/1
+  aspect-ratio: 4 / 1;
+  padding: 5px;
+  border-radius: 10px;
+  background: linear-gradient(0deg, #FFFBF7 0%, #FFE0CC 100%);
+  box-shadow: 3px 6px 4px 0px rgba(239, 103, 36, 0.48);
+}
+
 .check {
   background: linear-gradient(0deg, #FBD00E 0%, #FBEC92 100%);
 }

+ 83 - 0
pages/index/components/programming.uvue

@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { querySubjectInfo, querySubjectInfoNoLogin } from '@/services/subject/info'
+import type { SubjectInfoResult } from '@/services/subject/info'
+import { router, debounce, user } from "@/.cool";
+
+import { config } from '@/config'
+const isLoading = ref(true)
+const dataList = ref<SubjectInfoResult[]>([])
+async function getDataList() {
+  let res: SubjectInfoResult[] = []
+  if (user.token === 'Basic ZW5kOmVuZA==') {
+    res = await querySubjectInfoNoLogin({
+      delFlag: false,
+      subjectType: 'programming'
+    })
+  } else {
+    res = await querySubjectInfo({
+      delFlag: false,
+      subjectType: 'programming'
+    })
+  }
+  dataList.value = res
+}
+onMounted(async () => {
+  try {
+    await getDataList()
+    isLoading.value = false
+  } catch (err) {
+    console.log(err);
+    isLoading.value = false
+  }
+})
+function handleDetail(item: any) {
+  router.push({
+    path: "/pages/programming/index",
+    query: {
+      id: item.id,
+    }
+  });
+}
+</script>
+
+<template>
+  <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 dataList || []" :key="course.id"
+        @tap="handleDetail(course)">
+        <cl-image :src="course?.iconPath" mode="heightFix" class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>
+        <text class="text-[4vh] font-bold">{{
+          course.mainTitle }}</text>
+        <text class="text-[3vh] text-[#666]">{{
+          course.assistantTitle }}</text>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+
+
+<style lang="scss" scoped>
+.boxs {
+  @apply h-[60vh] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[1] pt-[10vh];
+}
+
+.jj {
+  border: 20px solid #00C6F1;
+
+  border-top: 15px solid #00000000;
+  border-right: 15px solid #00000000;
+  border-bottom: 15px solid #00000000;
+}
+
+.scroll-view_H {
+  width: 100%;
+  height: 100%;
+  flex-direction: row;
+}
+
+.scroll-view-item_H {
+  @apply w-[40vh] h-[50vh] mr-[20px] rounded-2xl border-b-[10px] p-1 flex items-center justify-between pb-[20px];
+}
+</style>

+ 6 - 3
pages/index/home.uvue

@@ -13,6 +13,8 @@ import MathModal from './components/math.uvue'
 import { bindWechatOpenId } from "@/services/user";
 import { useUi } from "@/uni_modules/cool-ui";
 import { exchangeCode } from '@/services/user'
+import ProgrammingModal from './components/programming.uvue'
+
 const ui = useUi();
 const menuList = computed(() => {
 	return [
@@ -42,7 +44,7 @@ const menuList = computed(() => {
 		// },
 		{
 			label: '少儿编程',
-			code: 'coding'
+			code: 'programming'
 		},
 		{
 			label: '兑换',
@@ -63,7 +65,7 @@ const icons = {
 	exchange: "https://oss.xiaoxiongcode.com/static/home/图层 5.png",
 	user: "https://oss.xiaoxiongcode.com/static/home/个人中心.png",
 	math: "https://oss.xiaoxiongcode.com/static/home/math.png",
-	coding: "https://oss.xiaoxiongcode.com/static/home/coding.png",
+	programming: "https://oss.xiaoxiongcode.com/static/home/coding.png",
 }
 const selected = ref<string>('physics')
 function handlePage(val: any) {
@@ -75,7 +77,7 @@ function handlePage(val: any) {
 		router.push({ path: '/pages/user/info' })
 		return
 	}
-	if (val.code === 'coding') {
+	if (val.code === 'programming' && (!userInfo.value || userInfo.value?.memberLevel === 'default')) {
 		visible2.value = true
 		return
 	}
@@ -266,6 +268,7 @@ async function handleExchange() {
 		<Game v-else-if="selected === 'game'" />
 		<Exchange v-else-if="selected === 'exchange'" />
 		<MathModal v-else-if="selected === 'math'" />
+		<ProgrammingModal v-else-if="selected === 'programming'" />
 	</cl-page>
 </template>
 <style lang="scss" scoped>

+ 8 - 4
pages/math/index.uvue

@@ -111,9 +111,8 @@ const userInfo = computed(() => user.info.value?.userInfo)
           <view v-for="category in dataList" :key="category.id" class="sidebar-item"
             :class="{ active: catalog?.id === category.id }" @tap="handleSelect(category)">
             <!-- <cl-image :src="category?.ossIconPath" mode="heightFix" class="!h-[28px]"></cl-image> -->
-            <cl-text ellipsis :pt="{
-              className: 'we'
-            }"> {{ category.name }}</cl-text>
+            <text class="we">{{ category.name }}</text>
+            <cl-tag type="success" rounded>{{ category.courseNum || 0 }}</cl-tag>
           </view>
         </scroll-view>
       </view>
@@ -121,6 +120,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
       <view class="cards-container">
         <view class="header">
           {{ catalog?.name }}
+          <cl-tag type="success" rounded>{{ catalog?.courseNum || 0 }}</cl-tag>
         </view>
         <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
           @scroll="handleScrollToLowerDebounce">
@@ -150,7 +150,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
 </template>
 <style lang="scss" scoped>
 .header {
-  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full;
+  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full flex items-center gap-2 flex-row;
   transform: translateX(-50%);
   text-align: center;
   background-color: #fff;
@@ -199,6 +199,10 @@ const userInfo = computed(() => user.info.value?.userInfo)
       .we {
         color: #fff !important;
         font-size: 1.6vw !important;
+        overflow: hidden;
+        flex: 1;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
 
       &.active {

+ 9 - 5
pages/mix/index.uvue

@@ -112,9 +112,8 @@ const userInfo = computed(() => user.info.value?.userInfo)
             :class="{ active: catalog?.id === category.id }" @tap="handleSelect(category)">
             <!-- <cl-image :src="category?.ossIconPath" mode="heightFix" class="!h-[28px]"></cl-image> -->
             <!-- {{ category.name }} -->
-            <cl-text ellipsis :pt="{
-              className: 'we'
-            }"> {{ category.name }}</cl-text>
+            <text class="we">{{ category.name }}</text>
+            <cl-tag type="success" rounded>{{ category.courseNum || 0 }}</cl-tag>
           </view>
         </scroll-view>
       </view>
@@ -122,6 +121,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
       <view class="cards-container">
         <view class="header">
           {{ catalog?.name }}
+          <cl-tag type="success" rounded>{{ catalog?.courseNum || 0 }}</cl-tag>
         </view>
         <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
           @scroll="handleScrollToLowerDebounce">
@@ -151,7 +151,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
 </template>
 <style lang="scss" scoped>
 .header {
-  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full;
+  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full flex flex-row items-center gap-2;
   transform: translateX(-50%);
   text-align: center;
   background-color: #fff;
@@ -171,7 +171,7 @@ const userInfo = computed(() => user.info.value?.userInfo)
     height: calc(100vh - 90px);
 
     .sidebar-item {
-      @apply text-white rounded-full py-1 px-2 cursor-pointer flex flex-row items-center;
+      @apply text-white rounded-full py-1 px-2 cursor-pointer flex flex-row justify-between items-center gap-1;
       margin-bottom: 8px;
       transition: all 0.3s ease;
 
@@ -183,6 +183,10 @@ const userInfo = computed(() => user.info.value?.userInfo)
       .we {
         color: #fff !important;
         font-size: 1.6vw !important;
+        overflow: hidden;
+        flex: 1;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
 
       &.active {

+ 127 - 0
pages/programming/detail.uvue

@@ -0,0 +1,127 @@
+<script setup lang='ts'>
+import Back from '@/components/back.uvue'
+import Loading from '@/components/loading.uvue'
+import { ref, onMounted, watchEffect, nextTick, onUnmounted } from 'vue'
+import { type SubjectCourseResult, fetchSubjectCourseApp, updateSubjectProgress } from '@/services/subject/course'
+import { router } from '@/.cool'
+const isLoading = ref(true)
+const showProgress = ref(true)
+const showControls = ref(true)
+const showVideo = ref(true)
+
+const data = ref({
+  videoSrc: '',
+})
+//当前进度
+
+const course = ref<SubjectCourseResult>()
+//通过路由参数获取课程id
+async function fetchCatalog() {
+  course.value = await fetchSubjectCourseApp({ id: router.query().id })
+  if (!course.value?.videoPath) {
+    return
+  }
+
+  data.value.videoSrc = course.value?.videoPath.includes('http') ? encodeURI(course.value?.videoPath) : ('https://oss.xiaoxiongcode.com' + encodeURI(course.value?.videoPath))
+}
+onMounted(async () => {
+  await fetchCatalog()
+  isLoading.value = false
+  setTimeout(() => {
+    isLoading.value = false
+  }, 1000)
+})
+async function handleEnded() {
+  router.back()
+}
+
+function handleControlsToggle(e) {
+  showControls.value = e.detail.show
+}
+
+
+
+</script>
+<template>
+  <Loading v-show="isLoading" />
+  <cl-page v-show="!isLoading">
+    <view class="w-full h-full">
+      <video id="video1" class="w-full h-full " :src="data.videoSrc" :show-center-play-btn="false"
+        :show-background-playback-button="false" :show-fullscreen-btn="false" :show-casting-button="false" autoplay
+        @controlstoggle="handleControlsToggle" @ended="handleEnded">
+      </video>
+      <view class="video-fullscreen_title" v-show="showControls">
+        <Back />
+        <view class="text-[20px] font-bold text-white">
+          {{ course?.mainTitle }}
+        </view>
+      </view>
+    </view>
+  </cl-page>
+</template>
+<style lang="scss" scoped>
+.translate50 {
+  transform: translateY(-50%);
+}
+
+.course-detail-page {
+  @apply flex flex-row items-center;
+  background-color: #3498DB;
+  height: 100vh;
+  color: black;
+  transition: all 1s ease-in-out;
+
+}
+
+.video-container {
+  width: 100vw;
+  height: 100vh;
+  top: 0;
+  left: 0;
+  transform: translate(0, 0);
+  animation: spin 2s linear 1;
+  transition: all 2s ease-in-out;
+}
+
+@keyframes spin {
+  0% {
+    opacity: 50%;
+  }
+
+  100% {
+    opacity: 1;
+  }
+}
+
+.hidden {
+  opacity: 0;
+  transition: all 3s ease-in-out;
+}
+
+.video-fullscreen_title {
+  @apply absolute top-3 left-0 pl-[80px] w-[100vw] z-10 flex items-center justify-between flex-row;
+  color: #fff;
+
+  .control-progress {
+    @apply flex flex-row gap-[3px] relative justify-end;
+    flex: 1;
+
+    .before {
+      @apply absolute top-1/2 right-0 w-[340px] h-[40px] bg-[#2BA0F3] rounded-l-full;
+      transform: translateY(-30%);
+    }
+  }
+}
+
+video::-webkit-media-controls-fullscreen-button,
+video::-webkit-media-controls-enter-fullscreen-button,
+video::-webkit-media-controls-rotate-button,
+video::-webkit-media-controls-seek-back-button,
+video::-webkit-media-controls-seek-forward-button {
+  display: none !important;
+}
+
+.mic {
+  @apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
+}
+</style>

+ 267 - 0
pages/programming/index.uvue

@@ -0,0 +1,267 @@
+<script setup lang='ts'>
+import Back from '@/components/back.uvue'
+import Loading from '@/components/loading.uvue'
+import { ref, onMounted, nextTick, computed } from 'vue'
+import { type SubjectCatalogResult, querySubjectCatalog, querySubjectCatalogNoLogin } from '@/services/subject/catalog'
+import { type SubjectCourseResult, getSubjectCoursePage, getSubjectCoursePageNoLogin } from '@/services/subject/course'
+import { type GetPage } from '@/services/types/index'
+
+import { config } from '@/config'
+import { dict } from '@/.cool/store'
+import { router, debounce, user } from "@/.cool";
+import Lock from '@/components/lock.uvue'
+
+const isLoading = ref(true)
+const dataList = ref<SubjectCatalogResult[]>([])
+const catalog = ref<SubjectCatalogResult>()
+const courseList = ref<SubjectCourseResult[]>([])
+const cardsScrollView = ref<any>(null)
+const pageNum = ref(1)
+const loading = ref(false)
+const finish = ref(false)
+async function getDataList() {
+  let res: SubjectCatalogResult[] = []
+  if (user.token === 'Basic ZW5kOmVuZA==') {
+    res = await querySubjectCatalogNoLogin({
+      subjectId: router.query().id
+    })
+  } else {
+    res = await querySubjectCatalog({
+      subjectId: router.query().id
+    })
+  }
+  dataList.value = res || []
+  catalog.value = res?.[0] as SubjectCatalogResult
+  handleSelect(catalog.value)
+}
+async function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
+  courseList.value = []
+  finish.value = false
+  pageNum.value = 1
+  await getInfo()
+  cardsScrollView.value.scrollTo({
+    top: 0, // 减去顶部偏移
+    animated: true
+  })
+}
+async function getInfo() {
+  loading.value = true
+  let res: GetPage<SubjectCourseResult> = {
+    total: 0,
+    rows: [],
+    pageNum: 1,
+    pageSize: 20,
+    pages: 0
+  }
+  if (user.token === 'Basic ZW5kOmVuZA==') {
+    res = await getSubjectCoursePageNoLogin({
+      catalogId: catalog.value?.id, pageSize: 12, pageNum: pageNum.value,
+    })
+  } else {
+    res = await getSubjectCoursePage({
+      catalogId: catalog.value?.id, pageSize: 12, pageNum: pageNum.value,
+    })
+  }
+  courseList.value = [...courseList.value, ...(res.rows || [])]
+  loading.value = false
+  finish.value = res.pages === pageNum.value
+}
+async function handleScrollToLower() {
+  if (loading.value || finish.value) {
+    return
+  }
+  pageNum.value++
+  await getInfo()
+}
+const handleScrollToLowerDebounce = debounce(handleScrollToLower, 1000)
+onMounted(async () => {
+  await getDataList()
+  isLoading.value = false
+})
+function handleDetail(item: SubjectCatalogResult) {
+  // if (!item.payFlag && !item.trialPlay) {
+  //   uni.showToast({
+  //     title: '请先购买',
+  //     icon: 'none'
+  //   })
+  //   return
+  // }
+  if (userInfo.value?.memberLevel === 'default') return
+  router.push({
+    path: "/pages/programming/progress",
+    query: {
+      id: item.id,
+    }
+  });
+}
+const userInfo = computed(() => user.info.value?.userInfo)
+</script>
+<template>
+  <Loading v-show="isLoading" />
+  <cl-page v-show="!isLoading">
+    <Back />
+    <!-- 顶部标题栏 -->
+    <image mode="aspectFill" src="https://oss.xiaoxiongcode.com/static/百科/bg.jpg" alt=""
+      class="w-full h-full object-cover" />
+    <view class="content">
+      <!-- 左侧导航菜单 -->
+      <view class="w-[20vw] h-[100vh] bg-[#a9e3f2bb] pt-[70px] pb-[20px] px-[20px]">
+        <scroll-view direction="vertical" :show-scrollbar="false" class="sidebar">
+          <view v-for="category in dataList" :key="category.id" class="sidebar-item"
+            :class="{ active: catalog?.id === category.id }" @tap="handleSelect(category)">
+            <!-- <cl-image :src="category?.ossIconPath" mode="heightFix" class="!h-[28px]"></cl-image> -->
+            <!-- {{ category.name }} -->
+            <text class="we">{{ category.name }}</text>
+            <cl-tag type="success" rounded>{{ category.courseNum || 0 }}</cl-tag>
+          </view>
+        </scroll-view>
+      </view>
+      <!-- 右侧卡片网格 -->
+      <view class="cards-container">
+        <view class="header">
+          {{ catalog?.name }}
+          <cl-tag type="success" rounded>{{ catalog?.courseNum || 0 }}</cl-tag>
+
+        </view>
+        <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
+          @scroll="handleScrollToLowerDebounce">
+          <view class="grid grid-cols-4 gap-4">
+            <view class="card" v-for="card in courseList" :key="card.id" :class="`category-${card.catalogId}`"
+              @tap="handleDetail(card)">
+              <view class="w-full h-[11vw] rounded-xl overflow-hidden">
+                <image :src="card?.ossIconPath" mode="aspectFill" class="w-full h-full object-cover"></image>
+              </view>
+              <cl-text ellipsis :pt="{
+                className: '!text-[1.5vw] !font-bold'
+              }">{{
+                card.sortNum }}、{{
+                  card.mainTitle }}</cl-text>
+              <cl-text ellipsis color="#666" :pt="{
+                className: '!text-[1.2vw]'
+              }">{{
+                card.assistantTitle }}</cl-text>
+              <Lock v-if="userInfo?.memberLevel === 'default'" :record="card" type="vip" />
+            </view>
+          </view>
+          <cl-loadmore :loading="loading" :finish="finish"></cl-loadmore>
+        </scroll-view>
+      </view>
+    </view>
+  </cl-page>
+</template>
+<style lang="scss" scoped>
+.header {
+  @apply absolute left-1/2 top-5 text-[#333] font-bold text-xl px-4 py-1 rounded-full flex items-center gap-2 flex-row;
+  transform: translateX(-50%);
+  text-align: center;
+  background-color: #fff;
+  white-space: nowrap;
+}
+
+.content {
+  display: flex;
+  flex-direction: row;
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+
+  .sidebar {
+    height: calc(100vh - 90px);
+
+    .sidebar-item {
+      @apply text-white rounded-full py-1 px-2 cursor-pointer flex flex-row items-center;
+      margin-bottom: 8px;
+      transition: all 0.3s ease;
+
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      width: 100%;
+
+      .we {
+        color: #fff !important;
+        font-size: 1.6vw !important;
+        overflow: hidden;
+        flex: 1;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      &.active {
+        background-color: rgba(255, 255, 255, 1);
+        color: #f3759b;
+        font-weight: bold;
+
+        .we {
+          color: #1899b9 !important;
+        }
+      }
+
+      .sidebar-icon {
+        font-size: 28px;
+        margin-right: 2px;
+      }
+    }
+  }
+
+  .cards-container {
+    @apply relative;
+    flex: 1;
+    width: 100%;
+    padding: 20px;
+    padding-top: 70px;
+    // background: linear-gradient(0deg, #2EB2FD, #0B85F4);
+
+    .cards {
+      height: calc(100vh - 90px);
+    }
+
+    .category-section {
+      margin-bottom: 40px;
+
+      .category-title {
+        @apply text-white font-bold text-xl mb-4;
+      }
+    }
+
+    .card {
+      @apply rounded-2xl border-[3px] border-[#1D4BD9] border-solid border-b-[6px] relative p-1 bg-white pb-3;
+      // background-color: rgba(255, 255, 255, 0.9);
+      // border-radius: 16px;
+      // padding: 16px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: space-between;
+      // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+      aspect-ratio: 8/9;
+
+      .card-content {
+        width: 100%;
+        height: 120px;
+        background-color: #E3F2FD;
+        border-radius: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 12px;
+        aspect-ratio: 3/4;
+
+        .question-mark {
+          font-size: 60px;
+          font-weight: bold;
+          color: #1E88E5;
+        }
+      }
+
+      .card-title {
+        @apply text-center font-bold absolute left-0 w-full text-[#0E3E87] text-[1.5vw];
+        bottom: 13%;
+      }
+    }
+  }
+}
+</style>

+ 115 - 0
pages/programming/progress.uvue

@@ -0,0 +1,115 @@
+<script setup lang="ts">
+import Back from '@/components/back.uvue'
+import Loading from '@/components/loading.uvue'
+import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
+import { type SubjectCourseResult, fetchSubjectCourseApp, updateSubjectProgress } from '@/services/subject/course'
+import { router, user } from '@/.cool'
+
+const course = ref<SubjectCourseResult>()
+const progress = ref(0)
+const isLoading = ref(false)
+const progressStatus = ref(0)
+//通过路由参数获取课程id
+async function fetchCatalog() {
+  course.value = await fetchSubjectCourseApp({ id: router.query().id })
+  if (!course.value?.courseUserProgress) {
+    await updateSubjectProgress({
+      courseId: course.value?.id,
+      mainProgress: 1,
+      assistantProgress: 1,
+      status: 0
+    })
+    progress.value = 1
+  } else if (course.value?.courseUserProgress.status == 0) {
+    progressStatus.value = course.value?.courseUserProgress.status
+    progress.value = course.value?.courseUserProgress.assistantProgress
+  } else {
+    progress.value = 6
+    progressStatus.value = course.value?.courseUserProgress.status
+  }
+}
+onShow(async () => {
+  isLoading.value = true
+  await fetchCatalog()
+  isLoading.value = false
+})
+
+const menuItems = [
+  { id: 1, name: '视频学习', icon: 'https://oss.xiaoxiongcode.com/static/home/6419.png', x: 0, y: 21 },
+  { id: 2, name: '实战指导', icon: 'https://oss.xiaoxiongcode.com/static/home/6418.png', x: 27, y: 19 },
+  { id: 3, name: '核心代码', icon: 'https://oss.xiaoxiongcode.com/static/home/6416.png', x: 80, y: 16 },
+  { id: 4, name: '开始编程', icon: 'https://oss.xiaoxiongcode.com/static/home/6417.png', x: 55, y: 7 },
+]
+//点击事件
+function handleClick(id: number) {
+  switch (id) {
+    case 1:
+      router.push({
+        path: "/pages/programming/detail",
+        query: {
+          id: router.query().id,
+        }
+      });
+      break
+    case 2:
+      progress.value = 2
+      break
+    case 3:
+      progress.value = 3
+      break
+    case 4:
+      progress.value = 4
+      break
+  }
+
+}
+</script>
+<template>
+  <Loading v-show="isLoading" />
+  <cl-page>
+    <Back />
+    <view class="main">
+      <!-- <image class="absolute top-0 left-0 z-[1] w-full h-full "  mode="aspectFill"  src="https://oss.xiaoxiongcode.com/static/home/641.png"   /> -->
+
+      <view class="text-[20px] font-bold text-[#333333] absolute top-6 left-20">
+        {{ course?.mainTitle }}
+      </view>
+      <view class="progress w-[90vw] h-[45vw] pb-[5vw] flex items-center justify-end ">
+        <image class="w-[80vw]" mode="widthFix" src="https://oss.xiaoxiongcode.com/static/home/形状 1.png" />
+        <view v-for="item in menuItems" :key="item.id"
+          class="w-[10vw] h-[13vw] absolute flex items-center justify-between z-[2]" :style="{
+            top: `${item.y}vw`,
+            left: `${item.x}vw`
+          }" @click="handleClick(item.id)">
+          <view class="w-[9vw] h-[9vw] flex items-center justify-center bg-[#fff] rounded-[2vw] box-shadow">
+            <image :src="item.icon" class="w-[4vw] " mode="widthFix" />
+            <text class="text-[2vw] text-[#000]">{{ item.name }}</text>
+          </view>
+          <view class="flex items-center justify-between gap-[1vw]">
+            <image src="https://oss.xiaoxiongcode.com/static/home/6415.png" class="w-[4vw] " mode="widthFix" />
+          </view>
+        </view>
+      </view>
+    </view>
+  </cl-page>
+</template>
+<style lang="scss" scoped>
+.main {
+  background: linear-gradient(0deg, #88C5F0, #D0ECFF);
+  width: 100vw;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+
+  .progress {
+    position: relative;
+    z-index: 2;
+  }
+}
+
+.box-shadow {
+  box-shadow: 4px 7px 10px 0px rgba(62, 166, 238, 0.44);
+}
+</style>

+ 1 - 1
pages/user/components/conversion.uvue

@@ -24,7 +24,7 @@ onMounted(() => {
               {{ item.mainTitle }}
             </view>
             <view class="text-[2vh] text-[#999]">
-              兑换时间:{{ item.createTime }}
+              兑换时间:{{ item.createdTime }}
             </view>
             <view class="text-[2vh]  text-[#999]">
               到期时间:{{ item.updateTime }}

+ 1 - 0
services/subject/catalog.ts

@@ -21,6 +21,7 @@ export interface SubjectCatalogResult {
     courseList?: SubjectCourseResult[]
     payFlag?: boolean
     trialPlay?: boolean
+    courseNum?: number
 }
 
 // export function getSubjectCatalogPage(parameter: any) {