Переглянути джерело

feat: 添加虚拟实验功能并优化页面交互

- 在首页添加虚拟实验入口点击事件
- 新增虚拟实验相关API接口文件
- 优化目录页面滚动定位逻辑,使用动态窗口高度计算
- 添加支付状态检查逻辑,防止未支付用户操作
- 重构测试页面布局和交互逻辑
- 在字典存储中添加窗口尺寸获取方法
whj 3 днів тому
батько
коміт
0040dce1be

+ 11 - 2
.cool/store/dict.ts

@@ -16,15 +16,24 @@ export class Dict {
 		'dict:children': {},
 		'dict:value:obj': {},
 	}); // 存储所有字典数据
-
+	private windowWidth: number = 0
+	private windowHeight: number = 0
 	constructor() {
 		const dictData = storage.get("dict");
 		if (isNull(dictData)) {
 			return;
 		}
+		const res = uni.getSystemInfoSync()
+		this.windowWidth = res.windowWidth
+		this.windowHeight = res.windowHeight
 		this.data = dictData;
 	}
-
+	getWindowWidth() {
+		return this.windowWidth
+	}
+	getWindowHeight() {
+		return this.windowHeight
+	}
 	getConfigValueByType(type: string) {
 		return this.data['config:open:value'][type]
 	}

+ 8 - 1
components/lock.uvue

@@ -5,10 +5,17 @@ const props = defineProps({
   record: {
     type: Object,
     default: () => ({})
+  },
+  isPay: {
+    type: Boolean,
+    default: false
   }
 })
 const visible = ref(false)
 function handleOpen() {
+  if (!props.isPay) {
+    return
+  }
   visible.value = true
 }
 async function getPayStatus(outTradeNo: string) {
@@ -21,7 +28,7 @@ async function getPayStatus(outTradeNo: string) {
 }
 
 const handlePay = async () => {
-  console.log(props.record)
+
   try {
     const { prepayId, outTradeNo } = await wechatPay({
       subjectId: props.record.subjectId,

+ 4 - 3
pages/catalog/index.uvue

@@ -56,19 +56,20 @@ function handleSelect(val: SubjectCatalogResult) {
   uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec((rect) => {
     if (cardsScrollView.value && rect[0]) {
       cardsScrollView.value.scrollTo({
-        left: rect[0].left - 215, // 减去顶部偏移
+        left: rect[0].left - dict.getWindowHeight() / 2, // 减去顶部偏移
         animated: true
       })
     }
   })
 }
 async function onScroll(e: any) {
+  console.log();
   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 <= 215) {
+        if (rect.left <= (dict.getWindowHeight() / 2)) {
           catalog.value = category
         }
       }
@@ -109,7 +110,7 @@ async function onScroll(e: any) {
           <view>
             <Progress :num="2" :percentage="course.courseUserProgress?.mainProgress || 0" />
           </view>
-          <Lock v-if="!course.trialPlay && !course.payFlag" :record="course" />
+          <Lock v-if="!course.trialPlay && !course.payFlag" :record="course" isPay />
         </view>
       </scroll-view>
     </view>

+ 2 - 1
pages/index/components/physics.uvue

@@ -19,7 +19,8 @@ function handleView(url) {
 
       </view>
     </view>
-    <view class="flex-1 flex flex-col items-center justify-between gap-2 bg1 py-3">
+    <view class="flex-1 flex flex-col items-center justify-between gap-2 bg1 py-3"
+      @tap="handleView('/pages/test/index')">
       <cl-image src="https://oss.xiaoxiongcode.com/static/home/17.png" mode="widthFix" width="40%" height="auto" />
       <text class="text-[14px]">趣味虚拟实验</text>
     </view>

+ 124 - 153
pages/test/index.uvue

@@ -1,206 +1,177 @@
-<script setup lang='ts'>
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { fetchSubjectAppInfo } from '@/services/subject/info'
+import { querySubjectCourseTest } from '@/services/subject/test'
+import type { SubjectCatalogResult } from '@/services/subject/catalog'
+import type { SubjectCourseTestResult } from '@/services/subject/test'
+import Lock from '@/components/lock.uvue'
 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 type { SubjectCatalogResult } from '@/services/subject/catalog'
 import { config } from '@/config'
 import { dict } from '@/.cool/store'
+import { router } from "@/.cool";
 
-const activeCategory = ref<string>()
-const activeTab = ref<string>('knowledge')
 const isLoading = ref(true)
-const categories = ref<SubjectCatalogResult[]>([])
-const cards = ref<SubjectKnowledgeCardResult[]>([])
-const cardsScrollView = ref<any>(null)
-const categoryRefs = ref<any>()
+const visible = ref<boolean>(false)
+const dataList = ref<SubjectCatalogResult[]>([])
+const catalog = ref<SubjectCatalogResult>()
+const testList = ref<SubjectCourseTestResult[]>()
 async function getDataList() {
   const id = dict.getValueByLabelMapByType('index_subject_id')['物理']
-  const res = await getSubjectKnowledgeCard({ subjectId: id })
-  cards.value = res.userCardList || []
-  categories.value = res.catalogList || []
+  const res = await fetchSubjectAppInfo({ id })
+  dataList.value = res.catalogList || []
+  catalog.value = res?.catalogList?.[0]
+  const testRes = await querySubjectCourseTest()
+  testList.value = testRes || []
+
 }
-function handleSelect(categoryId: string) {
-  activeCategory.value = categoryId
-  uni.createSelectorQuery().select(`.category-${categoryId}`).boundingClientRect().exec((rect) => {
+onMounted(async () => {
+  try {
+    await getDataList()
+    isLoading.value = false
+  } catch (err) {
+    console.log(err);
+    isLoading.value = false
+  }
+})
+const cardsScrollView = ref<any>(null)
+
+function handleDetail(item: SubjectCourseTestResult) {
+  router.push({
+    path: "/pages/catalog/web-view",
+    query: {
+      src: item.animationPath,
+      progress: 2,
+    }
+  });
+}
+function handleSelect(val: SubjectCatalogResult) {
+  catalog.value = val
+  visible.value = false
+  uni.createSelectorQuery().select(`.category-${val.id}`).boundingClientRect().exec((rect) => {
     if (cardsScrollView.value && rect[0]) {
       cardsScrollView.value.scrollTo({
-        top: rect[0].top - 70, // 减去顶部偏移
+        left: rect[0].left - 50, // 减去顶部偏移
         animated: true
       })
     }
   })
 }
-
 async function onScroll(e: any) {
-  categories.value.forEach(async (category) => {
+  console.log();
+  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.top <= 150 && rect.bottom >= 150) {
-          activeCategory.value = category.id
+        if (rect.left <= 50) {
+          catalog.value = category
         }
       }
     })
   })
 }
-onMounted(async () => {
-  await getDataList()
-  isLoading.value = false
-  nextTick(() => {
-    handleSelect(categories.value[0].id as string)
-  })
-})
 </script>
+
 <template>
   <Loading v-show="isLoading" />
   <cl-page v-show="!isLoading">
     <Back />
-    <!-- 顶部标题栏 -->
-    <view class="content">
-      <!-- 左侧导航菜单 -->
-      <scroll-view direction="vertical" :show-scrollbar="false" class="sidebar">
-        <view v-for="category in categories" :key="category.id" class="sidebar-item"
-          :class="{ active: activeCategory === category.id }" @tap="handleSelect(category.id as string)">
-          <cl-image :src="config.baseUrl + category?.fileList?.[0]?.url" mode="heightFix" class="!h-[28px]"></cl-image>
-          <text class="sidebar-text">{{ category.name }}</text>
+    <img src="https://oss.xiaoxiongcode.com/static/home/2.png" alt="" class="w-full h-full object-cover" />
+    <!-- 顶部右侧光标签 -->
+    <view class="light-tag" @tap="visible = true">
+      <image class="light-icon" v-if="catalog?.fileList?.[0]?.url" :src="config.baseUrl + catalog?.fileList?.[0]?.url">
+      </image>
+      <text class="light-text">{{ catalog?.name }}</text>
+      <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" ref="cardsScrollView"
+        @scroll="onScroll">
+        <view class="scroll-view-item_H bg-[white]" v-for="test in testList || []" :class="`category-${test.catalogId}`"
+          :key="test.id" @tap="handleDetail(test)" ref="categoryRefs">
+          <cl-image :src="test.animationJavascriptPath" mode="heightFix"
+            class="!w-full !h-[26vh] mb-[2px] rounded-xl"></cl-image>
+          <text class="text-[16px] font-bold">{{
+            test.name }}</text>
+          <text class="text-[14px] text-[#666]">{{
+            test.remark }}</text>
+          <Lock v-if="!test.lockFlag" :record="test" />
         </view>
       </scroll-view>
-      <!-- 右侧卡片网格 -->
-      <view class="cards-container">
-        <view class="header">
-          <view class="title-tabs">
-            <view class="tab" :class="{ active: activeTab === 'knowledge' }" @tap="activeTab = 'knowledge'">知识卡
-            </view>
-            <view class="tab" :class="{ active: activeTab === 'honor' }" @tap="activeTab = 'honor'">荣誉</view>
-          </view>
-        </view>
-        <scroll-view direction="vertical" :show-scrollbar="false" class="cards" ref="cardsScrollView"
-          @scroll="onScroll">
-          <view class="grid grid-cols-5 gap-4">
-            <view class="card" v-for="card in cards" :key="card.id" :class="`category-${card.catalogId}`"
-              ref="categoryRefs">
-              <image v-if="card.userCardId" :src="config.baseUrl + card.iconPath" class="w-full h-full" />
-              <image v-else src="https://oss.xiaoxiongcode.com/static/home/21.png" class="w-full h-full" />
-              <view class="card-title">{{ card.cardName }}</view>
+    </view>
+    <cl-popup v-model="visible" :show-header="false" direction="center" :size="600">
+      <view class="p-4">
+        <cl-row :gutter="0">
+          <cl-col :span="6" v-for="item in dataList || []" :key="item.id" :pt="{
+            className: '!p-2'
+          }" @tap="handleSelect(item)">
+            <view class="select-item" :class="{ selected: item.id === catalog?.id }">
+              <image :src="config.baseUrl + item?.fileList?.[0]?.url" class="w-[30rpx] h-[30rpx] mb-[2px]"></image>
+              <text>{{ item.name }}</text>
             </view>
-          </view>
-        </scroll-view>
+          </cl-col>
+        </cl-row>
       </view>
-    </view>
+    </cl-popup>
   </cl-page>
 </template>
-<style lang="scss" scoped>
-.header {
-  @apply absolute left-1/2 top-5;
-  transform: translateX(-50%);
-  text-align: center;
 
-  .title-tabs {
-    @apply flex flex-row items-center justify-center rounded-full;
-    background-color: #9AD2FA;
-
-    .tab {
-      @apply rounded-full;
-      padding: 5px 10px;
-      font-size: 16px;
-      transition: all 0.3s ease;
-
-      &.active {
-        background-color: white;
-        color: #1E88E5;
-        font-weight: bold;
-      }
-    }
-  }
+<style lang="scss" scoped>
+.boxs {
+  @apply w-[calc(100vw-100px)] h-[50vh] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[1];
 }
 
-.content {
-  display: flex;
+.scroll-view_H {
+  width: 100%;
+  height: 100%;
   flex-direction: row;
+}
 
-  .sidebar {
-    width: 200px;
-    background-color: #116FE9;
-    padding: 20px;
-    padding-top: 70px;
-    height: 100vh;
-
-    .sidebar-item {
-      @apply text-white rounded-full py-1 cursor-pointer flex flex-row items-center;
-      margin-bottom: 8px;
-      transition: all 0.3s ease;
-      font-size: 12px;
-      width: 100%;
+.scroll-view-item_H {
+  @apply w-[40vh] h-[50vh] mr-[20px] rounded-2xl border-[5px] border-[#1D4BD9] border-solid border-b-[10px] p-1 flex items-center relative;
+}
 
-      &.active {
-        background-color: rgba(255, 255, 255, 1);
-        color: #1E88E5;
-        font-weight: bold;
-      }
+.light-tag {
+  @apply absolute top-3 left-1/2 z-[1] flex flex-row items-center bg-white px-3 py-2 font-bold rounded-full shadow-md;
+  transform: translateX(-50%);
 
-      .sidebar-icon {
-        font-size: 28px;
-        margin-right: 2px;
-      }
-    }
+  .light-icon {
+    width: 20px;
+    height: 20px;
+    margin-right: 3px;
   }
 
-  .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;
+  .light-text {
+    font-size: 16px;
+    min-width: 100px;
+    padding-right: 5px;
+  }
+}
 
-      .category-title {
-        @apply text-white font-bold text-xl mb-4;
-      }
-    }
+.select-item {
+  @apply flex items-center justify-center rounded-xl border-[3px] border-[#1D4BD9] border-solid border-b-[5px] px-4 py-2 font-bold;
+}
 
-    .card {
-      // background-color: rgba(255, 255, 255, 0.9);
-      // border-radius: 16px;
-      // padding: 16px;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      // box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-      aspect-ratio: 3/4;
+.selected {
+  @apply border-green-500;
+}
 
-      .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;
+.footer {
+  @apply absolute bottom-2 right-5 z-[1] flex flex-row items-center justify-center gap-4;
+}
 
-        .question-mark {
-          font-size: 60px;
-          font-weight: bold;
-          color: #1E88E5;
-        }
-      }
+.text-stroke-custom {
+  color: white;
+  text-shadow:
+    /* 左上角投影 */
+    -1px -1px 0 #1D4BD9,
+    /* 右上角投影 */
+    1px -1px 0 #1D4BD9,
+    /* 左下角投影 */
+    -1px 1px 0 #1D4BD9,
+    /* 右下角投影 */
+    1px 1px 0 #1D4BD9;
 
-      .card-title {
-        @apply text-center font-bold absolute left-0 w-full text-[#0E3E87] text-[1.5vw];
-        bottom: 13%;
-      }
-    }
-  }
 }
 </style>

+ 33 - 0
services/subject/test.ts

@@ -0,0 +1,33 @@
+import { usePost, stringify, useGet } from "@/.cool";
+
+
+export interface SubjectCourseTestResult {
+    id?: string
+    name?: string
+    subjectId?: string
+    catalogId?: string
+    courseId?: string
+    animationPath?: string
+    animationJavascriptPath?: string
+    animationCssPath?: string
+    animationImagePath?: string
+    sortNum?: string
+    remark?: string
+    updateUserId?: string
+    updateUserName?: string
+    createdUserId?: string
+    createdUserName?: string
+    createdTime?: string
+    updateTime?: string
+    lockFlag?: boolean
+}
+
+export function querySubjectCourseTest(parameter?: any) {
+    return useGet(`/subject/test`, parameter) as Promise<SubjectCourseTestResult[]>
+}
+
+export function fetchSubjectCourseTest(parameter: any) {
+    return useGet(`/subject/test/${parameter.id}`) as Promise<SubjectCourseTestResult>
+}
+
+