Преглед на файлове

feat: 签到记录模块

XiaoYu преди 1 година
родител
ревизия
6eac9987f4
променени са 23 файла, в които са добавени 921 реда и са изтрити 309 реда
  1. 60 8
      api/checkin.ts
  2. 7 0
      pages.json
  3. 67 69
      pages/components/NavBar/NavBar.vue
  4. 279 0
      pages/record/teacher-class-details.vue
  5. 91 97
      pages/record/teacher.vue
  6. 0 0
      unpackage/dist/dev/.sourcemap/mp-weixin/api/checkin.js.map
  7. 0 0
      unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map
  8. 0 0
      unpackage/dist/dev/.sourcemap/mp-weixin/pages/components/NavBar/NavBar.js.map
  9. 0 0
      unpackage/dist/dev/.sourcemap/mp-weixin/pages/record/teacher-class-details.js.map
  10. 0 0
      unpackage/dist/dev/.sourcemap/mp-weixin/pages/record/teacher.js.map
  11. 22 0
      unpackage/dist/dev/mp-weixin/api/checkin.js
  12. 1 0
      unpackage/dist/dev/mp-weixin/app.js
  13. 2 1
      unpackage/dist/dev/mp-weixin/app.json
  14. 1 1
      unpackage/dist/dev/mp-weixin/common/vendor.js
  15. 28 16
      unpackage/dist/dev/mp-weixin/pages/components/NavBar/NavBar.js
  16. 10 17
      unpackage/dist/dev/mp-weixin/pages/components/NavBar/NavBar.wxss
  17. 132 0
      unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.js
  18. 6 0
      unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.json
  19. 0 0
      unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.wxml
  20. 127 0
      unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.wxss
  21. 48 58
      unpackage/dist/dev/mp-weixin/pages/record/teacher.js
  22. 1 1
      unpackage/dist/dev/mp-weixin/pages/record/teacher.wxml
  23. 39 41
      unpackage/dist/dev/mp-weixin/pages/record/teacher.wxss

+ 60 - 8
api/checkin.ts

@@ -73,6 +73,7 @@ interface StudentClass {
 }
 export type GetStudentClassesResponse = StudentClass[];
 
+
 // 获取教师所有签到任务响应
 export type GetAllTeacherCheckinTasksResponse = CreateCheckinTaskItem[];
 
@@ -89,6 +90,31 @@ interface SubmitCheckinRequest {
 
 }
 
+// 【关键新增】教师 - 获取所有课程响应
+interface TeacherCourse {
+    courseId: number;
+    courseName: string;
+    description: string;
+    startTime: string;
+    endTime: string;
+    teacherPid: number;
+    teacherNames: string[];
+    // ... 其他字段
+}
+
+export type GetAllTeacherCoursesResponse = TeacherCourse[];
+
+// 【关键新增】获取课程下的所有班级响应
+interface CourseClass {
+    classId: number;
+    className: string;
+    classTime: string;
+    stuNumber: number;
+    teacherId: number;
+    // ... 其他字段
+}
+export type GetClassesByCourseIdResponse = CourseClass[];
+
 
 // --- API 接口封装 ---
 
@@ -107,6 +133,23 @@ export const getCheckinStatusApi = (signTaskId: string | number): Promise<GetChe
   });
 };
 
+// 获取教师所有课程的API
+export const getAllTeacherCoursesApi = (teacherPid: string | number): Promise<GetAllTeacherCoursesResponse> => {
+    return request<GetAllTeacherCoursesResponse>({
+        url: `/course/all/teacher`,
+        method: 'GET',
+        params: { teacherPid }
+    });
+};
+
+//获取课程下的所有班级的API
+export const getClassesByCourseIdApi = (courseId: string | number): Promise<GetClassesByCourseIdResponse> => {
+    return request<GetClassesByCourseIdResponse>({
+        url: `/class/course/${courseId}`,
+        method: 'GET'
+    });
+};
+
 export const endCheckinApi = (params: EndCheckinParams): Promise<void> => {
   return request<void>({
     url: '/sign/end',
@@ -116,14 +159,14 @@ export const endCheckinApi = (params: EndCheckinParams): Promise<void> => {
   });
 };
 
-// 【关键新增】获取二维码内容的API
-export const getQrCodeApi = (signTaskId: string | number, expire: number): Promise<GetQrCodeResponse> => {
-    return request<GetQrCodeResponse>({
-        url: `/sign/qr`,
-        method: 'GET',
-        params: { signTaskId, expire }
-    });
-};
+// // 【关键新增】获取二维码内容的API
+// export const getQrCodeApi = (signTaskId: string | number, expire: number): Promise<GetQrCodeResponse> => {
+//     return request<GetQrCodeResponse>({
+//         url: `/sign/qr`,
+//         method: 'GET',
+//         params: { signTaskId, expire }
+//     });
+// };
 
 export const getActiveCheckinApi = (params: { officialNumber: string | number }): Promise<GetActiveCheckinResponse> => {
   return request<GetActiveCheckinResponse>({
@@ -149,6 +192,15 @@ export const getClassByStudentApi = (officialNumber: string | number): Promise<G
     });
 };
 
+//获取某个具体班级信息的API
+export const getClassByClassIdApi = (classId: string | number):Promise<StudentClass> =>{
+	return request<StudentClass>({
+		url:`/class/${classId}`,
+		method: 'GET'
+	});
+};
+	
+
 export const submitCheckinApi = (data: SubmitCheckinRequest): Promise<void> => {
   return request<void>({
     url: '/sign/submit',

+ 7 - 0
pages.json

@@ -91,6 +91,13 @@
 			{
 				"navigationBarTitleText" : ""
 			}
+		},
+		{
+			"path" : "pages/record/teacher-class-details",
+			"style" : 
+			{
+				"navigationBarTitleText" : ""
+			}
 		}
 	],
 	"globalStyle": {

+ 67 - 69
pages/components/NavBar/NavBar.vue

@@ -1,49 +1,44 @@
 <template>
   <view class="navbar-container" :style="{ height: totalHeight + 'px' }">
-    <!-- 状态栏占位 -->
     <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
     
-    <!-- 胶囊区域占位 -->
     <view class="capsule-area" :style="{ height: capsuleAreaHeight + 'px' }">
-		<text v-if="!isHomePage" class="back" @click="goHome"> 返回 </text>
-	</view>
+      <text v-if="showBack" class="back" @click="goBack"> 返回 </text>
+    </view>
     
-    <!-- 导航栏内容 - 显示在胶囊下方 -->
     <view class="navbar-content">
       <view class="navbar-left">
-		<img 
-		class="logo"
-		src="https://sns-poki-1251524319.cos.ap-shanghai.myqcloud.com/slytherin/58f02e08e90c48b182ad0d6d1b517437.png" alt="logo" />
-        <!-- 彩色SEECODER标题 -->
-		<view class="title-container">
-		  <text class="title-letter letter-s">S</text>
-		  <text class="title-letter letter-e1">E</text>
-		  <text class="title-letter letter-e2">E</text>
-		  <text class="title-letter letter-c">C</text>
-		  <text class="title-letter letter-o">O</text>
-		  <text class="title-letter letter-d">D</text>
-		  <text class="title-letter letter-e3">E</text>
-		  <text class="title-letter letter-r">R</text>
-		</view>
+        <img 
+        class="logo"
+        src="https://sns-poki-1251524319.cos.ap-shanghai.myqcloud.com/slytherin/58f02e08e90c48b182ad0d6d1b517437.png" alt="logo" />
+        <view class="title-container">
+          <text class="title-letter letter-s">S</text>
+          <text class="title-letter letter-e1">E</text>
+          <text class="title-letter letter-e2">E</text>
+          <text class="title-letter letter-c">C</text>
+          <text class="title-letter letter-o">O</text>
+          <text class="title-letter letter-d">D</text>
+          <text class="title-letter letter-e3">E</text>
+          <text class="title-letter letter-r">R</text>
+        </view>
       </view>
       <view class="date">{{ currentDate }}</view>
     </view>
     
-    <!-- 底部分割线 -->
     <view class="navbar-border"></view>
   </view>
 </template>
 
-<script setup>
-import { ref, onMounted } from 'vue'
+<script setup lang="ts">
+import { ref, onMounted, defineExpose, nextTick } from 'vue'
 
 // 响应式数据
 const currentDate = ref('')
 const statusBarHeight = ref(0)
-const capsuleAreaHeight = ref(44) // 胶囊区域高度
-const contentHeight = ref(50) // 内容区域高度
-const totalHeight = ref(100) // 总高度
-const isHomePage = ref(false)
+const capsuleAreaHeight = ref(44)
+const contentHeight = ref(50)
+const totalHeight = ref(100)
+const showBack = ref(false) // 【关键修改】控制返回按钮的显示
 
 // 获取当前日期
 const getCurrentDate = () => {
@@ -63,15 +58,10 @@ const getSystemInfo = () => {
   // #ifdef MP-WEIXIN
   if (uni.getMenuButtonBoundingClientRect) {
     const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
-    
-    // 胶囊区域高度 = 胶囊底部位置 - 状态栏高度
     capsuleAreaHeight.value = menuButtonInfo.bottom - statusBarHeight.value
-    // 内容区域高度保持固定
     contentHeight.value = 50
-    // 总高度 = 状态栏 + 胶囊区域 + 内容区域 + 边框
     totalHeight.value = statusBarHeight.value + capsuleAreaHeight.value + contentHeight.value + 2
   } else {
-    // 非微信小程序的默认高度
     capsuleAreaHeight.value = 44
     contentHeight.value = 50
     totalHeight.value = statusBarHeight.value + capsuleAreaHeight.value + contentHeight.value + 2
@@ -79,38 +69,51 @@ const getSystemInfo = () => {
   // #endif
   
   // #ifndef MP-WEIXIN
-  // 其他平台的默认高度
   capsuleAreaHeight.value = 44
   contentHeight.value = 50
   totalHeight.value = statusBarHeight.value + capsuleAreaHeight.value + contentHeight.value + 2
   // #endif
 }
-// 1. 检查当前页面是否为首页
-const checkIfHomePage = () => {
-  const pages = getCurrentPages()
-  // 如果页面栈不存在或为空,则默认为首页
-  if (!pages || pages.length === 0) {
-    isHomePage.value = true
-    return
-  }
-  // 获取当前页面实例
-  const currentPage = pages[pages.length - 1]
-  // 判断当前页面的路由是否为首页路径
-  // 请确保你的首页路径是 'pages/index/index'
-  isHomePage.value = currentPage.route === 'pages/index/index'
-}
 
-// 2. 修改点击事件,统一返回首页
-const goHome = () => {
-  uni.reLaunch({
-    url: '/pages/index/index'
-  })
-}
+// 【关键修改】检查是否需要显示返回按钮
+const checkShowBack = () => {
+    // 不需要返回按钮的页面路径列表
+    const noBackButtonPages = [
+        'pages/index/index',
+        'pages/login/index',
+        'pages/welcome/index'
+    ];
+    
+    const pages = getCurrentPages();
+    if (pages && pages.length > 0) {
+        const currentPage = pages[pages.length - 1];
+        // 如果当前页面不是页面栈的第一个,且不在不显示按钮的列表中
+        showBack.value = pages.length > 0 && !noBackButtonPages.includes(currentPage.route);
+    } else {
+        showBack.value = false;
+    }
+};
+
+// 【关键修改】返回上一个页面的函数
+const goBack = () => {
+    // 使用 uni.navigateBack 返回上一个页面
+    uni.navigateBack({
+        delta: 1,
+        fail: () => {
+            // 如果返回失败(如页面栈只有一页),则回到首页
+            uni.reLaunch({
+                url: '/pages/index/index'
+            });
+        }
+    });
+};
 
 onMounted(() => {
   getCurrentDate()
   getSystemInfo()
-  checkIfHomePage()
+  nextTick(() => {
+    checkShowBack();
+  });
 })
 
 // 暴露总高度给父组件使用
@@ -131,7 +134,7 @@ defineExpose({
   flex-direction: column;
   border-bottom-left-radius: 30rpx;
   border-bottom-right-radius: 30rpx;
-  overflow: hidden; /* 防止子元素溢出圆角 */
+  overflow: hidden;
 }
 
 .status-bar {
@@ -144,7 +147,6 @@ defineExpose({
   align-items: center;
   background-color: #ffffff;
   flex-shrink: 0;
-  /* 这个区域为胶囊按钮预留空间 */
 }
 .back {
   font-size: 36rpx;
@@ -168,8 +170,6 @@ defineExpose({
   background-color: #e0e0e0;
   border-bottom-left-radius: 20rpx;
   border-bottom-right-radius: 20rpx;
-  /* flex-shrink: 0; */
-  /* overflow: hidden; */
 }
 
 .navbar-left {
@@ -182,10 +182,9 @@ defineExpose({
   height: 64rpx;
   border-radius: 6rpx;
   margin-right: 12rpx;
-  background-color: #f5f5f5; /* 加载时的背景色 */
+  background-color: #f5f5f5;
 }
 
-/* 彩色标题样式 */
 .title-container {
   display: flex;
   align-items: center;
@@ -197,41 +196,40 @@ defineExpose({
   line-height: 1;
 }
 
-/* 每个字母的颜色 */
 .letter-s {
-  color: #F39C12; /* 橙色 */
+  color: #F39C12;
 }
 
 .letter-e1 {
-  color: #5DADE2; /* 浅蓝色 */
+  color: #5DADE2;
 }
 
 .letter-e2 {
-  color: #58D68D; /* 绿色 */
+  color: #58D68D;
 }
 
 .letter-c {
-  color: #3498DB; /* 深蓝色 */
+  color: #3498DB;
 }
 
 .letter-o {
-  color: #F39C12; /* 橙色 */
+  color: #F39C12;
 }
 
 .letter-d {
-  color: #5DADE2; /* 浅蓝色 */
+  color: #5DADE2;
 }
 
 .letter-e3 {
-  color: #58D68D; /* 绿色 */
+  color: #58D68D;
 }
 
 .letter-r {
-  color: #3498DB; /* 深蓝色 */
+  color: #3498DB;
 }
 
 .date {
   font-size: 26rpx;
   color: #666666;
 }
-</style>
+</style>

+ 279 - 0
pages/record/teacher-class-details.vue

@@ -0,0 +1,279 @@
+<template>
+  <view class="container">
+    <NavBar ref="navBarRef" title="班级签到记录" :showBack="true" />
+    <view class="content-wrapper" style=" paddingTop:140px">
+      <view class="header">
+        <text class="header-title">{{ className + ' 签到记录' || '签到记录' }}</text>
+      </view>
+      <view class="record-view">
+        <text v-if="loading" class="loading-text">正在加载记录...</text>
+        <view v-else-if="tasks.length > 0" class="task-list">
+          <view v-for="task in tasks" :key="task.id" class="task-item">
+            <view class="item-header" @click="toggleDetails(task.id)">
+              <view class="item-info-left">
+                <text class="item-id">任务ID: {{ task.id }}</text>
+                <text class="item-time">开始于 {{ formatTime(task.createTime) }}</text>
+              </view>
+              <view class="item-info-right">
+                <text class="item-status-tag" :class="task.status">{{ formatStatus(task.status) }}</text>
+              </view>
+            </view>
+            
+            <view v-if="task.id === expandedTaskId" class="item-details-expanded">
+			  <view class="detail-row">
+				<text class="detail-label">签到码:</text>
+				<text class="detail-value">{{ task.signCode }}</text>
+			  </view>
+			  <view class="detail-row">
+				<text class="detail-label">签到方式:</text>
+				<text class="detail-value">{{ task.signType === 'NUMBER' ? '数字签到' : '二维码签到' }}</text>
+			  </view>
+			  <view class="detail-row">
+				<text class="detail-label">签到人数:</text>
+				<text class="detail-value">{{ task.signedNumber }} / {{ task.totalNumber }}</text>
+			  </view>
+			  <view class="detail-row">
+				<text class="detail-label">结束时间:</text>
+				<text class="detail-value">{{ formatTime(task.endTime) }}</text>
+			  </view>
+			  
+			  <view class="student-list-box signed">
+				<text class="list-title">已签到 ({{ signedStudents.length }})</text>
+				<view v-if="signedStudents.length > 0" class="student-names">
+				  <text v-for="student in signedStudents" :key="student.id" class="student-name">
+					{{ student.studentName }}
+				  </text>
+				</view>
+				<text v-else class="empty-list-tip" style="color: #999;font-size: 14px;margin-left: 8px;">暂无学生签到</text>
+			  </view>
+			  
+			  <view class="student-list-box not-signed">
+				  <text class="list-title">未签到 ({{ task.totalNumber - task.signedNumber }})</text>
+			  </view>
+			</view>
+		  </view>
+		</view>
+		<view v-else class="empty-text">
+		  <text>暂无签到记录</text>
+		</view>
+	  </view>
+	</view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick, watch } from 'vue'
+import { useUserStore } from '@/store/user'
+import { getCheckinStatusApi, getClassByClassIdApi, getClassCheckinDetailsApi } from '@/api/checkin'
+import NavBar from '../components/NavBar/NavBar.vue'
+import { onLoad } from '@dcloudio/uni-app'
+
+const navHeight = ref(88)
+const navBarRef = ref(null)
+
+const userStore = useUserStore()
+const loading = ref(false)
+const tasks = ref([])
+const className = ref('')
+const expandedTaskId = ref(null)
+const signedStudents = ref([])
+
+onLoad(async (options) => {
+    await nextTick()
+    navHeight.value = navBarRef.value?.totalNavHeight.value || 88
+    watch(() => navBarRef.value?.totalNavHeight.value, (val) => { if (val) navHeight.value = val }, { immediate: true })
+
+    if (options.classId) {
+        loading.value = true
+        await fetchClassDetails(options.classId)
+        loading.value = false
+    }
+})
+
+const fetchClassDetails = async (classId) => {
+    try {
+        const response = await getClassCheckinDetailsApi(classId)
+        tasks.value = response.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())
+		const classDetail = await getClassByClassIdApi(classId);
+        className.value = classDetail.className; // 假设从其他地方获取班级名
+    } catch (error) {
+        console.error('获取班级签到记录失败:', error)
+        uni.showToast({ title: '获取记录失败', icon: 'none' })
+    }
+}
+
+const toggleDetails = async (taskId) => {
+  if (expandedTaskId.value === taskId) {
+    expandedTaskId.value = null;
+    signedStudents.value = [];
+  } else {
+    expandedTaskId.value = taskId;
+    uni.showLoading({ title: '加载名单...' });
+    try {
+      const res = await getCheckinStatusApi(taskId);
+      if (res && res.signRecords) {
+        signedStudents.value = res.signRecords.filter(s => s.signStatus.toUpperCase() === 'SIGNED');
+      }
+    } catch (error) {
+      console.error('获取签到名单失败:', error);
+      uni.showToast({ title: '加载名单失败', icon: 'none' });
+    } finally {
+      uni.hideLoading();
+    }
+  }
+}
+
+// 辅助函数
+const formatStatus = (status) => {
+    const statusMap = {
+        NOT_START: '未开始',
+        PROCESSING: '进行中',
+        FINISHED: '已结束',
+        CANCELLED: '已取消'
+    }
+    return statusMap[status] || '未知'
+}
+
+const formatTime = (time) => {
+    if (!time) return '不限'
+    const date = new Date(time)
+    return date.toLocaleString()
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background-color: #f5f5f7;
+}
+
+.content-wrapper {
+  padding: 20rpx 30rpx;
+  box-sizing: border-box;
+}
+
+.header {
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+
+.header-title {
+  font-size: 48rpx;
+  font-weight: 500;
+  color: #333;
+}
+
+.loading-text, .empty-text {
+  text-align: center;
+  margin-top: 50rpx;
+  font-size: 32rpx;
+  color: #999;
+}
+
+.record-view {
+  padding-bottom: 50rpx;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.task-item {
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  gap: 15rpx;
+  position: relative;
+}
+
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 10rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.item-info-left {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 8rpx;
+}
+.item-id {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+.item-time {
+  font-size: 24rpx;
+  color: #666;
+}
+.item-info-right {
+  display: flex;
+  align-items: center;
+  gap: 15rpx;
+}
+.item-status-tag {
+  font-size: 24rpx;
+  font-weight: bold;
+  padding: 8rpx 16rpx;
+  border-radius: 8rpx;
+}
+.item-status-tag.PROCESSING {
+  background-color: #EAF7EA;
+  color: #5CB85C;
+}
+.item-status-tag.FINISHED {
+  background-color: #ffeaea;
+  color: #e54d42;
+}
+.item-status-tag.NOT_START {
+  background-color: #f0f8ff;
+  color: #7DB4D6;
+}
+.item-status-tag.CANCELLED {
+  background-color: #e9e9e9;
+  color: #999;
+}
+.arrow {
+  font-size: 28rpx;
+  color: #999;
+  transform: rotate(90deg);
+  transition: transform 0.2s ease-in-out;
+}
+.item-details {
+  padding-top: 20rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 10rpx;
+}
+.detail-row {
+  display: flex;
+  font-size: 28rpx;
+  color: #666;
+}
+.detail-label {
+  font-weight: bold;
+  width: 150rpx;
+  flex-shrink: 0;
+}
+.detail-value {
+  flex: 1;
+}
+.student-names{
+	display: flex;
+}
+.student-name{
+	width: auto;
+	background: #e9e9e9; 
+	padding: 8rpx 12rpx; 
+	border-radius: 6rpx; 
+	font-size: 26rpx; 
+	color: #555;
+}
+</style>

+ 91 - 97
pages/record/teacher.vue

@@ -1,33 +1,28 @@
 <template>
   <view class="container">
     <NavBar ref="navBarRef" title="教师签到记录" :showBack="true" />
-    <view class="content-wrapper" :style="{ paddingTop: navHeight + 'px' }">
+    <view class="content-wrapper" style=" paddingTop: 140px">
       <view class="header">
-        <text class="header-title">教师签到记录</text>
+        <text class="header-title">所有课程与班级</text>
       </view>
-      <view class="teacher-view">
-        <text v-if="loading" class="loading-text">正在加载记录...</text>
-        <view v-else-if="Object.keys(groupedRecords).length > 0">
-          <view v-for="(course, courseId) in groupedRecords" :key="courseId" class="course-group">
-            <text class="course-name">{{ course.courseName }}</text>
-            <view v-for="(cls, classId) in course.classes" :key="classId" class="class-group">
-              <text class="class-name">{{ cls.className }}</text>
-              <view class="task-list">
-                <view v-for="task in cls.tasks" :key="task.id" class="task-item">
-                  <text class="task-status" :class="task.status">{{ formatStatus(task.status) }}</text>
-                  <text class="task-info">签到码: {{ task.signCode }}</text>
-                  <text class="task-info">签到方式: {{ task.signType }}</text>
-                  <text class="task-info">总人数: {{ task.totalNumber }}</text>
-                  <text class="task-info">已签到: {{ task.signedNumber }}</text>
-                  <text class="task-info">发起时间: {{ formatTime(task.createTime) }}</text>
-                  <text class="task-info">结束时间: {{ formatTime(task.endTime) }}</text>
-                </view>
+      <view class="record-view">
+        <text v-if="loading" class="loading-text">正在加载课程...</text>
+        <view v-else-if="coursesWithClasses.length > 0" class="course-list">
+          <view v-for="course in coursesWithClasses" :key="course.courseId" class="course-item">
+            <view class="course-header" @click="toggleExpand(course.courseId)">
+              <text class="course-name">{{ course.courseName }}</text>
+              <text class="arrow" :class="{ 'expanded': course.expanded }">></text>
+            </view>
+            <view v-if="course.expanded" class="class-list-container">
+              <view v-for="cls in course.classes" :key="cls.classId" class="class-item" @click="navigateToClassDetails(cls.classId)">
+                <text class="class-name">{{ cls.className }}</text>
+                <text class="arrow">></text>
               </view>
             </view>
           </view>
         </view>
         <view v-else class="empty-text">
-          <text>暂无签到记录</text>
+          <text>暂无课程</text>
         </view>
       </view>
     </view>
@@ -37,7 +32,7 @@
 <script setup lang="ts">
 import { ref, computed, onMounted, nextTick, watch } from 'vue'
 import { useUserStore } from '@/store/user'
-import { getAllTeacherCheckinTasksApi } from '@/api/checkin'
+import { getAllTeacherCoursesApi, getClassesByCourseIdApi } from '@/api/checkin'
 import NavBar from '../components/NavBar/NavBar.vue'
 
 const navHeight = ref(88)
@@ -45,61 +40,59 @@ const navBarRef = ref(null)
 
 const userStore = useUserStore()
 const loading = ref(false)
-const groupedRecords = ref({})
+const coursesWithClasses = ref([])
+const expandedCourseId = ref(null)
 
 onMounted(async () => {
     await nextTick()
     navHeight.value = navBarRef.value?.totalNavHeight.value || 88
     watch(() => navBarRef.value?.totalNavHeight.value, (val) => { if (val) navHeight.value = val }, { immediate: true })
 
-    if (userStore.isLoggedIn && userStore.userInfo.role === 2) {
+    if (userStore.isLoggedIn && userStore.userInfo.role === "TEACHER") {
         loading.value = true
-        await fetchTeacherRecords()
+        await fetchTeacherCourses()
         loading.value = false
     }
 })
 
-const fetchTeacherRecords = async () => {
+const fetchTeacherCourses = async () => {
     const teacherId = userStore.userInfo.pid
     if (!teacherId) return
     
     try {
-        const allTasks = await getAllTeacherCheckinTasksApi(teacherId)
-        const grouped = {}
-        allTasks.forEach(task => {
-            const courseId = task.courseId
-            const classId = task.classId
-            const courseName = '课程名 ' + task.courseId
-            const className = '班级名 ' + task.classId
-            
-            if (!grouped[courseId]) {
-                grouped[courseId] = { courseName, classes: {} }
-            }
-            if (!grouped[courseId].classes[classId]) {
-                grouped[courseId].classes[classId] = { className, tasks: [] }
-            }
-            grouped[courseId].classes[classId].tasks.push(task)
-        })
-        groupedRecords.value = grouped
+        const courses = await getAllTeacherCoursesApi(teacherId)
+        coursesWithClasses.value = courses.map(course => ({
+            ...course,
+            classes: [], // 预留一个空数组来存放班级
+            expanded: false // 控制收起展开状态
+        }))
     } catch (error) {
-        console.error('获取教师签到记录失败:', error)
-        uni.showToast({ title: '获取记录失败', icon: 'none' })
+        console.error('获取教师课程失败:', error)
+        uni.showToast({ title: '获取课程失败', icon: 'none' })
     }
 }
 
-const formatStatus = (status) => {
-    const statusMap = {
-        NOT_START: '未开始',
-        PROCESSING: '进行中',
-        FINISHED: '已结束',
-        CANCELLED: '已取消'
+const toggleExpand = async (courseId) => {
+    const course = coursesWithClasses.value.find(c => c.courseId === courseId)
+    if (course) {
+        course.expanded = !course.expanded
+        if (course.expanded && course.classes.length === 0) {
+            // 如果是展开状态,且班级列表为空,则发起请求获取
+            try {
+                const classes = await getClassesByCourseIdApi(courseId)
+                course.classes = classes
+            } catch (error) {
+                console.error('获取班级列表失败:', error)
+                uni.showToast({ title: '获取班级失败', icon: 'none' })
+            }
+        }
     }
-    return statusMap[status] || '未知'
 }
-const formatTime = (time) => {
-    if (!time) return '不限'
-    const date = new Date(time)
-    return date.toLocaleString()
+
+const navigateToClassDetails = (classId) => {
+    uni.navigateTo({
+        url: `/pages/record/teacher-class-details?classId=${classId}`
+    })
 }
 </script>
 
@@ -132,68 +125,69 @@ const formatTime = (time) => {
   color: #999;
 }
 
-.course-group, .class-group {
-  margin-bottom: 40rpx;
-}
-
-.course-name {
-  font-size: 38rpx;
-  font-weight: bold;
-  color: #2E5F8A;
-  margin-bottom: 20rpx;
-  display: block;
-  border-left: 8rpx solid #7DB4D6;
-  padding-left: 15rpx;
-}
-
-.class-name {
-  font-size: 34rpx;
-  font-weight: bold;
-  color: #333;
-  margin: 20rpx 0;
-  display: block;
+.record-view {
+  padding-bottom: 50rpx;
 }
 
-.task-list {
+.course-list {
   display: flex;
   flex-direction: column;
   gap: 20rpx;
 }
 
-.task-item {
+.course-item {
   background-color: #ffffff;
   border-radius: 16rpx;
-  padding: 30rpx;
   box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  grid-gap: 10rpx 20rpx;
-  position: relative;
+  overflow: hidden;
 }
 
-.task-status {
-  position: absolute;
-  top: 20rpx;
-  right: 20rpx;
-  font-size: 28rpx;
+.course-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.course-name {
+  font-size: 38rpx;
   font-weight: bold;
+  color: #2E5F8A;
 }
 
-.task-status.PROCESSING {
-  color: #5CB85C;
+.class-list-container {
+  padding: 20rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 10rpx;
 }
-.task-status.FINISHED {
-  color: #e54d42;
+
+.class-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
 }
-.task-status.NOT_START {
-  color: #7DB4D6;
+
+.class-item:last-child {
+  border-bottom: none;
 }
-.task-status.CANCELLED {
-  color: #999;
+
+.class-name {
+  font-size: 32rpx;
+  color: #333;
 }
 
-.task-info {
+.arrow {
   font-size: 28rpx;
-  color: #666;
+  color: #999;
+  transform: rotate(90deg);
+  transition: transform 0.2s ease-in-out;
+}
+
+.arrow.expanded {
+  transform: rotate(270deg);
 }
 </style>

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/api/checkin.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/app.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/pages/components/NavBar/NavBar.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/pages/record/teacher-class-details.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/.sourcemap/mp-weixin/pages/record/teacher.js.map


+ 22 - 0
unpackage/dist/dev/mp-weixin/api/checkin.js

@@ -13,6 +13,19 @@ const getCheckinStatusApi = (signTaskId) => {
     method: "GET"
   });
 };
+const getAllTeacherCoursesApi = (teacherPid) => {
+  return utils_request.request({
+    url: `/course/all/teacher`,
+    method: "GET",
+    params: { teacherPid }
+  });
+};
+const getClassesByCourseIdApi = (courseId) => {
+  return utils_request.request({
+    url: `/class/course/${courseId}`,
+    method: "GET"
+  });
+};
 const endCheckinApi = (params) => {
   return utils_request.request({
     url: "/sign/end",
@@ -33,6 +46,12 @@ const getClassByStudentApi = (officialNumber) => {
     method: "GET"
   });
 };
+const getClassByClassIdApi = (classId) => {
+  return utils_request.request({
+    url: `/class/${classId}`,
+    method: "GET"
+  });
+};
 const submitCheckinApi = (data) => {
   return utils_request.request({
     url: "/sign/submit",
@@ -57,9 +76,12 @@ const getStudentStatusApi = (signTaskId, officialNumber) => {
 exports.createCheckinApi = createCheckinApi;
 exports.endCheckinApi = endCheckinApi;
 exports.getAllTeacherCheckinTasksApi = getAllTeacherCheckinTasksApi;
+exports.getAllTeacherCoursesApi = getAllTeacherCoursesApi;
 exports.getCheckinStatusApi = getCheckinStatusApi;
+exports.getClassByClassIdApi = getClassByClassIdApi;
 exports.getClassByStudentApi = getClassByStudentApi;
 exports.getClassCheckinDetailsApi = getClassCheckinDetailsApi;
+exports.getClassesByCourseIdApi = getClassesByCourseIdApi;
 exports.getStudentStatusApi = getStudentStatusApi;
 exports.submitCheckinApi = submitCheckinApi;
 //# sourceMappingURL=../../.sourcemap/mp-weixin/api/checkin.js.map

+ 1 - 0
unpackage/dist/dev/mp-weixin/app.js

@@ -17,6 +17,7 @@ if (!Math) {
   "./pages/record/index.js";
   "./pages/record/teacher.js";
   "./pages/record/student.js";
+  "./pages/record/teacher-class-details.js";
 }
 const _sfc_main = {
   __name: "App",

+ 2 - 1
unpackage/dist/dev/mp-weixin/app.json

@@ -12,7 +12,8 @@
     "pages/checkin/teacher/select-class",
     "pages/record/index",
     "pages/record/teacher",
-    "pages/record/student"
+    "pages/record/student",
+    "pages/record/teacher-class-details"
   ],
   "window": {
     "navigationBarTextStyle": "black",

+ 1 - 1
unpackage/dist/dev/mp-weixin/common/vendor.js

@@ -7088,7 +7088,7 @@ function isConsoleWritable() {
 function initRuntimeSocketService() {
   const hosts = "26.151.250.215,192.168.3.21,192.168.160.1,192.168.171.1,127.0.0.1";
   const port = "8090";
-  const id = "mp-weixin_u4NkN8";
+  const id = "mp-weixin_7ivbJF";
   const lazy = typeof swan !== "undefined";
   let restoreError = lazy ? () => {
   } : initOnError();

+ 28 - 16
unpackage/dist/dev/mp-weixin/pages/components/NavBar/NavBar.js

@@ -1,6 +1,6 @@
 "use strict";
 const common_vendor = require("../../../common/vendor.js");
-const _sfc_main = {
+const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
   __name: "NavBar",
   setup(__props, { expose: __expose }) {
     const currentDate = common_vendor.ref("");
@@ -8,7 +8,7 @@ const _sfc_main = {
     const capsuleAreaHeight = common_vendor.ref(44);
     const contentHeight = common_vendor.ref(50);
     const totalHeight = common_vendor.ref(100);
-    const isHomePage = common_vendor.ref(false);
+    const showBack = common_vendor.ref(false);
     const getCurrentDate = () => {
       const now = /* @__PURE__ */ new Date();
       const year = now.getFullYear();
@@ -30,24 +30,36 @@ const _sfc_main = {
         totalHeight.value = statusBarHeight.value + capsuleAreaHeight.value + contentHeight.value + 2;
       }
     };
-    const checkIfHomePage = () => {
+    const checkShowBack = () => {
+      const noBackButtonPages = [
+        "pages/index/index",
+        "pages/login/index",
+        "pages/welcome/index"
+      ];
       const pages = getCurrentPages();
-      if (!pages || pages.length === 0) {
-        isHomePage.value = true;
-        return;
+      if (pages && pages.length > 0) {
+        const currentPage = pages[pages.length - 1];
+        showBack.value = pages.length > 0 && !noBackButtonPages.includes(currentPage.route);
+      } else {
+        showBack.value = false;
       }
-      const currentPage = pages[pages.length - 1];
-      isHomePage.value = currentPage.route === "pages/index/index";
     };
-    const goHome = () => {
-      common_vendor.index.reLaunch({
-        url: "/pages/index/index"
+    const goBack = () => {
+      common_vendor.index.navigateBack({
+        delta: 1,
+        fail: () => {
+          common_vendor.index.reLaunch({
+            url: "/pages/index/index"
+          });
+        }
       });
     };
     common_vendor.onMounted(() => {
       getCurrentDate();
       getSystemInfo();
-      checkIfHomePage();
+      common_vendor.nextTick$1(() => {
+        checkShowBack();
+      });
     });
     __expose({
       totalNavHeight: totalHeight
@@ -55,9 +67,9 @@ const _sfc_main = {
     return (_ctx, _cache) => {
       return common_vendor.e({
         a: statusBarHeight.value + "px",
-        b: !isHomePage.value
-      }, !isHomePage.value ? {
-        c: common_vendor.o(goHome)
+        b: showBack.value
+      }, showBack.value ? {
+        c: common_vendor.o(goBack)
       } : {}, {
         d: capsuleAreaHeight.value + "px",
         e: common_vendor.t(currentDate.value),
@@ -65,7 +77,7 @@ const _sfc_main = {
       });
     };
   }
-};
+});
 const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-2921a85e"]]);
 wx.createComponent(Component);
 //# sourceMappingURL=../../../../.sourcemap/mp-weixin/pages/components/NavBar/NavBar.js.map

+ 10 - 17
unpackage/dist/dev/mp-weixin/pages/components/NavBar/NavBar.wxss

@@ -10,7 +10,7 @@
   flex-direction: column;
   border-bottom-left-radius: 30rpx;
   border-bottom-right-radius: 30rpx;
-  overflow: hidden; /* 防止子元素溢出圆角 */
+  overflow: hidden;
 }
 .status-bar.data-v-2921a85e {
   background-color: #ffffff;
@@ -21,7 +21,6 @@
   align-items: center;
   background-color: #ffffff;
   flex-shrink: 0;
-  /* 这个区域为胶囊按钮预留空间 */
 }
 .back.data-v-2921a85e {
   font-size: 36rpx;
@@ -43,8 +42,6 @@
   background-color: #e0e0e0;
   border-bottom-left-radius: 20rpx;
   border-bottom-right-radius: 20rpx;
-  /* flex-shrink: 0; */
-  /* overflow: hidden; */
 }
 .navbar-left.data-v-2921a85e {
   display: flex;
@@ -55,10 +52,8 @@
   height: 64rpx;
   border-radius: 6rpx;
   margin-right: 12rpx;
-  background-color: #f5f5f5; /* 加载时的背景色 */
+  background-color: #f5f5f5;
 }
-
-/* 彩色标题样式 */
 .title-container.data-v-2921a85e {
   display: flex;
   align-items: center;
@@ -68,31 +63,29 @@
   font-weight: 600;
   line-height: 1;
 }
-
-/* 每个字母的颜色 */
 .letter-s.data-v-2921a85e {
-  color: #F39C12; /* 橙色 */
+  color: #F39C12;
 }
 .letter-e1.data-v-2921a85e {
-  color: #5DADE2; /* 浅蓝色 */
+  color: #5DADE2;
 }
 .letter-e2.data-v-2921a85e {
-  color: #58D68D; /* 绿色 */
+  color: #58D68D;
 }
 .letter-c.data-v-2921a85e {
-  color: #3498DB; /* 深蓝色 */
+  color: #3498DB;
 }
 .letter-o.data-v-2921a85e {
-  color: #F39C12; /* 橙色 */
+  color: #F39C12;
 }
 .letter-d.data-v-2921a85e {
-  color: #5DADE2; /* 浅蓝色 */
+  color: #5DADE2;
 }
 .letter-e3.data-v-2921a85e {
-  color: #58D68D; /* 绿色 */
+  color: #58D68D;
 }
 .letter-r.data-v-2921a85e {
-  color: #3498DB; /* 深蓝色 */
+  color: #3498DB;
 }
 .date.data-v-2921a85e {
   font-size: 26rpx;

+ 132 - 0
unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.js

@@ -0,0 +1,132 @@
+"use strict";
+const common_vendor = require("../../common/vendor.js");
+const store_user = require("../../store/user.js");
+const api_checkin = require("../../api/checkin.js");
+if (!Math) {
+  NavBar();
+}
+const NavBar = () => "../components/NavBar/NavBar.js";
+const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
+  __name: "teacher-class-details",
+  setup(__props) {
+    const navHeight = common_vendor.ref(88);
+    const navBarRef = common_vendor.ref(null);
+    store_user.useUserStore();
+    const loading = common_vendor.ref(false);
+    const tasks = common_vendor.ref([]);
+    const className = common_vendor.ref("");
+    const expandedTaskId = common_vendor.ref(null);
+    const signedStudents = common_vendor.ref([]);
+    common_vendor.onLoad(async (options) => {
+      var _a;
+      await common_vendor.nextTick$1();
+      navHeight.value = ((_a = navBarRef.value) == null ? void 0 : _a.totalNavHeight.value) || 88;
+      common_vendor.watch(() => {
+        var _a2;
+        return (_a2 = navBarRef.value) == null ? void 0 : _a2.totalNavHeight.value;
+      }, (val) => {
+        if (val)
+          navHeight.value = val;
+      }, { immediate: true });
+      if (options.classId) {
+        loading.value = true;
+        await fetchClassDetails(options.classId);
+        loading.value = false;
+      }
+    });
+    const fetchClassDetails = async (classId) => {
+      try {
+        const response = await api_checkin.getClassCheckinDetailsApi(classId);
+        tasks.value = response.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
+        const classDetail = await api_checkin.getClassByClassIdApi(classId);
+        className.value = classDetail.className;
+      } catch (error) {
+        common_vendor.index.__f__("error", "at pages/record/teacher-class-details.vue:100", "获取班级签到记录失败:", error);
+        common_vendor.index.showToast({ title: "获取记录失败", icon: "none" });
+      }
+    };
+    const toggleDetails = async (taskId) => {
+      if (expandedTaskId.value === taskId) {
+        expandedTaskId.value = null;
+        signedStudents.value = [];
+      } else {
+        expandedTaskId.value = taskId;
+        common_vendor.index.showLoading({ title: "加载名单..." });
+        try {
+          const res = await api_checkin.getCheckinStatusApi(taskId);
+          if (res && res.signRecords) {
+            signedStudents.value = res.signRecords.filter((s) => s.signStatus.toUpperCase() === "SIGNED");
+          }
+        } catch (error) {
+          common_vendor.index.__f__("error", "at pages/record/teacher-class-details.vue:118", "获取签到名单失败:", error);
+          common_vendor.index.showToast({ title: "加载名单失败", icon: "none" });
+        } finally {
+          common_vendor.index.hideLoading();
+        }
+      }
+    };
+    const formatStatus = (status) => {
+      const statusMap = {
+        NOT_START: "未开始",
+        PROCESSING: "进行中",
+        FINISHED: "已结束",
+        CANCELLED: "已取消"
+      };
+      return statusMap[status] || "未知";
+    };
+    const formatTime = (time) => {
+      if (!time)
+        return "不限";
+      const date = new Date(time);
+      return date.toLocaleString();
+    };
+    return (_ctx, _cache) => {
+      return common_vendor.e({
+        a: common_vendor.sr(navBarRef, "f0be71e2-0", {
+          "k": "navBarRef"
+        }),
+        b: common_vendor.p({
+          title: "班级签到记录",
+          showBack: true
+        }),
+        c: common_vendor.t(className.value + " 签到记录" || "签到记录"),
+        d: loading.value
+      }, loading.value ? {} : tasks.value.length > 0 ? {
+        f: common_vendor.f(tasks.value, (task, k0, i0) => {
+          return common_vendor.e({
+            a: common_vendor.t(task.id),
+            b: common_vendor.t(formatTime(task.createTime)),
+            c: common_vendor.t(formatStatus(task.status)),
+            d: common_vendor.n(task.status),
+            e: common_vendor.o(($event) => toggleDetails(task.id), task.id),
+            f: task.id === expandedTaskId.value
+          }, task.id === expandedTaskId.value ? common_vendor.e({
+            g: common_vendor.t(task.signCode),
+            h: common_vendor.t(task.signType === "NUMBER" ? "数字签到" : "二维码签到"),
+            i: common_vendor.t(task.signedNumber),
+            j: common_vendor.t(task.totalNumber),
+            k: common_vendor.t(formatTime(task.endTime)),
+            l: common_vendor.t(signedStudents.value.length),
+            m: signedStudents.value.length > 0
+          }, signedStudents.value.length > 0 ? {
+            n: common_vendor.f(signedStudents.value, (student, k1, i1) => {
+              return {
+                a: common_vendor.t(student.studentName),
+                b: student.id
+              };
+            })
+          } : {}, {
+            o: common_vendor.t(task.totalNumber - task.signedNumber)
+          }) : {}, {
+            p: task.id
+          });
+        })
+      } : {}, {
+        e: tasks.value.length > 0
+      });
+    };
+  }
+});
+const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-f0be71e2"]]);
+wx.createPage(MiniProgramPage);
+//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/record/teacher-class-details.js.map

+ 6 - 0
unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.json

@@ -0,0 +1,6 @@
+{
+  "navigationBarTitleText": "",
+  "usingComponents": {
+    "nav-bar": "../components/NavBar/NavBar"
+  }
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.wxml


+ 127 - 0
unpackage/dist/dev/mp-weixin/pages/record/teacher-class-details.wxss

@@ -0,0 +1,127 @@
+
+.container.data-v-f0be71e2 {
+  min-height: 100vh;
+  background-color: #f5f5f7;
+}
+.content-wrapper.data-v-f0be71e2 {
+  padding: 20rpx 30rpx;
+  box-sizing: border-box;
+}
+.header.data-v-f0be71e2 {
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.header-title.data-v-f0be71e2 {
+  font-size: 48rpx;
+  font-weight: 500;
+  color: #333;
+}
+.loading-text.data-v-f0be71e2, .empty-text.data-v-f0be71e2 {
+  text-align: center;
+  margin-top: 50rpx;
+  font-size: 32rpx;
+  color: #999;
+}
+.record-view.data-v-f0be71e2 {
+  padding-bottom: 50rpx;
+}
+.task-list.data-v-f0be71e2 {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.task-item.data-v-f0be71e2 {
+  background-color: #ffffff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
+  display: flex;
+  flex-direction: column;
+  gap: 15rpx;
+  position: relative;
+}
+.item-header.data-v-f0be71e2 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 10rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.item-info-left.data-v-f0be71e2 {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 8rpx;
+}
+.item-id.data-v-f0be71e2 {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+.item-time.data-v-f0be71e2 {
+  font-size: 24rpx;
+  color: #666;
+}
+.item-info-right.data-v-f0be71e2 {
+  display: flex;
+  align-items: center;
+  gap: 15rpx;
+}
+.item-status-tag.data-v-f0be71e2 {
+  font-size: 24rpx;
+  font-weight: bold;
+  padding: 8rpx 16rpx;
+  border-radius: 8rpx;
+}
+.item-status-tag.PROCESSING.data-v-f0be71e2 {
+  background-color: #EAF7EA;
+  color: #5CB85C;
+}
+.item-status-tag.FINISHED.data-v-f0be71e2 {
+  background-color: #ffeaea;
+  color: #e54d42;
+}
+.item-status-tag.NOT_START.data-v-f0be71e2 {
+  background-color: #f0f8ff;
+  color: #7DB4D6;
+}
+.item-status-tag.CANCELLED.data-v-f0be71e2 {
+  background-color: #e9e9e9;
+  color: #999;
+}
+.arrow.data-v-f0be71e2 {
+  font-size: 28rpx;
+  color: #999;
+  transform: rotate(90deg);
+  transition: transform 0.2s ease-in-out;
+}
+.item-details.data-v-f0be71e2 {
+  padding-top: 20rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 10rpx;
+}
+.detail-row.data-v-f0be71e2 {
+  display: flex;
+  font-size: 28rpx;
+  color: #666;
+}
+.detail-label.data-v-f0be71e2 {
+  font-weight: bold;
+  width: 150rpx;
+  flex-shrink: 0;
+}
+.detail-value.data-v-f0be71e2 {
+  flex: 1;
+}
+.student-names.data-v-f0be71e2{
+	display: flex;
+}
+.student-name.data-v-f0be71e2{
+	width: auto;
+	background: #e9e9e9; 
+	padding: 8rpx 12rpx; 
+	border-radius: 6rpx; 
+	font-size: 26rpx; 
+	color: #555;
+}

+ 48 - 58
unpackage/dist/dev/mp-weixin/pages/record/teacher.js

@@ -13,7 +13,8 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
     const navBarRef = common_vendor.ref(null);
     const userStore = store_user.useUserStore();
     const loading = common_vendor.ref(false);
-    const groupedRecords = common_vendor.ref({});
+    const coursesWithClasses = common_vendor.ref([]);
+    common_vendor.ref(null);
     common_vendor.onMounted(async () => {
       var _a;
       await common_vendor.nextTick$1();
@@ -25,52 +26,49 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
         if (val)
           navHeight.value = val;
       }, { immediate: true });
-      if (userStore.isLoggedIn && userStore.userInfo.role === 2) {
+      if (userStore.isLoggedIn && userStore.userInfo.role === "TEACHER") {
         loading.value = true;
-        await fetchTeacherRecords();
+        await fetchTeacherCourses();
         loading.value = false;
       }
     });
-    const fetchTeacherRecords = async () => {
+    const fetchTeacherCourses = async () => {
       const teacherId = userStore.userInfo.pid;
       if (!teacherId)
         return;
       try {
-        const allTasks = await api_checkin.getAllTeacherCheckinTasksApi(teacherId);
-        const grouped = {};
-        allTasks.forEach((task) => {
-          const courseId = task.courseId;
-          const classId = task.classId;
-          const courseName = "课程名 " + task.courseId;
-          const className = "班级名 " + task.classId;
-          if (!grouped[courseId]) {
-            grouped[courseId] = { courseName, classes: {} };
-          }
-          if (!grouped[courseId].classes[classId]) {
-            grouped[courseId].classes[classId] = { className, tasks: [] };
-          }
-          grouped[courseId].classes[classId].tasks.push(task);
-        });
-        groupedRecords.value = grouped;
+        const courses = await api_checkin.getAllTeacherCoursesApi(teacherId);
+        coursesWithClasses.value = courses.map((course) => ({
+          ...course,
+          classes: [],
+          // 预留一个空数组来存放班级
+          expanded: false
+          // 控制收起展开状态
+        }));
       } catch (error) {
-        common_vendor.index.__f__("error", "at pages/record/teacher.vue:85", "获取教师签到记录失败:", error);
-        common_vendor.index.showToast({ title: "获取记录失败", icon: "none" });
+        common_vendor.index.__f__("error", "at pages/record/teacher.vue:70", "获取教师课程失败:", error);
+        common_vendor.index.showToast({ title: "获取课程失败", icon: "none" });
       }
     };
-    const formatStatus = (status) => {
-      const statusMap = {
-        NOT_START: "未开始",
-        PROCESSING: "进行中",
-        FINISHED: "已结束",
-        CANCELLED: "已取消"
-      };
-      return statusMap[status] || "未知";
+    const toggleExpand = async (courseId) => {
+      const course = coursesWithClasses.value.find((c) => c.courseId === courseId);
+      if (course) {
+        course.expanded = !course.expanded;
+        if (course.expanded && course.classes.length === 0) {
+          try {
+            const classes = await api_checkin.getClassesByCourseIdApi(courseId);
+            course.classes = classes;
+          } catch (error) {
+            common_vendor.index.__f__("error", "at pages/record/teacher.vue:85", "获取班级列表失败:", error);
+            common_vendor.index.showToast({ title: "获取班级失败", icon: "none" });
+          }
+        }
+      }
     };
-    const formatTime = (time) => {
-      if (!time)
-        return "不限";
-      const date = new Date(time);
-      return date.toLocaleString();
+    const navigateToClassDetails = (classId) => {
+      common_vendor.index.navigateTo({
+        url: `/pages/record/teacher-class-details?classId=${classId}`
+      });
     };
     return (_ctx, _cache) => {
       return common_vendor.e({
@@ -82,35 +80,27 @@ const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
           showBack: true
         }),
         c: loading.value
-      }, loading.value ? {} : Object.keys(groupedRecords.value).length > 0 ? {
-        e: common_vendor.f(groupedRecords.value, (course, courseId, i0) => {
-          return {
+      }, loading.value ? {} : coursesWithClasses.value.length > 0 ? {
+        e: common_vendor.f(coursesWithClasses.value, (course, k0, i0) => {
+          return common_vendor.e({
             a: common_vendor.t(course.courseName),
-            b: common_vendor.f(course.classes, (cls, classId, i1) => {
+            b: course.expanded ? 1 : "",
+            c: common_vendor.o(($event) => toggleExpand(course.courseId), course.courseId),
+            d: course.expanded
+          }, course.expanded ? {
+            e: common_vendor.f(course.classes, (cls, k1, i1) => {
               return {
                 a: common_vendor.t(cls.className),
-                b: common_vendor.f(cls.tasks, (task, k2, i2) => {
-                  return {
-                    a: common_vendor.t(formatStatus(task.status)),
-                    b: common_vendor.n(task.status),
-                    c: common_vendor.t(task.signCode),
-                    d: common_vendor.t(task.signType),
-                    e: common_vendor.t(task.totalNumber),
-                    f: common_vendor.t(task.signedNumber),
-                    g: common_vendor.t(formatTime(task.createTime)),
-                    h: common_vendor.t(formatTime(task.endTime)),
-                    i: task.id
-                  };
-                }),
-                c: classId
+                b: cls.classId,
+                c: common_vendor.o(($event) => navigateToClassDetails(cls.classId), cls.classId)
               };
-            }),
-            c: courseId
-          };
+            })
+          } : {}, {
+            f: course.courseId
+          });
         })
       } : {}, {
-        d: Object.keys(groupedRecords.value).length > 0,
-        f: navHeight.value + "px"
+        d: coursesWithClasses.value.length > 0
       });
     };
   }

+ 1 - 1
unpackage/dist/dev/mp-weixin/pages/record/teacher.wxml

@@ -1 +1 @@
-<view class="container data-v-d3091b11"><nav-bar wx:if="{{b}}" class="r data-v-d3091b11" u-r="navBarRef" u-i="d3091b11-0" bind:__l="__l" u-p="{{b}}"/><view class="content-wrapper data-v-d3091b11" style="{{'padding-top:' + f}}"><view class="header data-v-d3091b11"><text class="header-title data-v-d3091b11">教师签到记录</text></view><view class="teacher-view data-v-d3091b11"><text wx:if="{{c}}" class="loading-text data-v-d3091b11">正在加载记录...</text><view wx:elif="{{d}}" class="data-v-d3091b11"><view wx:for="{{e}}" wx:for-item="course" wx:key="c" class="course-group data-v-d3091b11"><text class="course-name data-v-d3091b11">{{course.a}}</text><view wx:for="{{course.b}}" wx:for-item="cls" wx:key="c" class="class-group data-v-d3091b11"><text class="class-name data-v-d3091b11">{{cls.a}}</text><view class="task-list data-v-d3091b11"><view wx:for="{{cls.b}}" wx:for-item="task" wx:key="i" class="task-item data-v-d3091b11"><text class="{{['task-status', 'data-v-d3091b11', task.b]}}">{{task.a}}</text><text class="task-info data-v-d3091b11">签到码: {{task.c}}</text><text class="task-info data-v-d3091b11">签到方式: {{task.d}}</text><text class="task-info data-v-d3091b11">总人数: {{task.e}}</text><text class="task-info data-v-d3091b11">已签到: {{task.f}}</text><text class="task-info data-v-d3091b11">发起时间: {{task.g}}</text><text class="task-info data-v-d3091b11">结束时间: {{task.h}}</text></view></view></view></view></view><view wx:else class="empty-text data-v-d3091b11"><text class="data-v-d3091b11">暂无签到记录</text></view></view></view></view>
+<view class="container data-v-d3091b11"><nav-bar wx:if="{{b}}" class="r data-v-d3091b11" u-r="navBarRef" u-i="d3091b11-0" bind:__l="__l" u-p="{{b}}"/><view class="content-wrapper data-v-d3091b11" style="padding-top:140px"><view class="header data-v-d3091b11"><text class="header-title data-v-d3091b11">所有课程与班级</text></view><view class="record-view data-v-d3091b11"><text wx:if="{{c}}" class="loading-text data-v-d3091b11">正在加载课程...</text><view wx:elif="{{d}}" class="course-list data-v-d3091b11"><view wx:for="{{e}}" wx:for-item="course" wx:key="f" class="course-item data-v-d3091b11"><view class="course-header data-v-d3091b11" bindtap="{{course.c}}"><text class="course-name data-v-d3091b11">{{course.a}}</text><text class="{{['arrow', 'data-v-d3091b11', course.b && 'expanded']}}">></text></view><view wx:if="{{course.d}}" class="class-list-container data-v-d3091b11"><view wx:for="{{course.e}}" wx:for-item="cls" wx:key="b" class="class-item data-v-d3091b11" bindtap="{{cls.c}}"><text class="class-name data-v-d3091b11">{{cls.a}}</text><text class="arrow data-v-d3091b11">></text></view></view></view></view><view wx:else class="empty-text data-v-d3091b11"><text class="data-v-d3091b11">暂无课程</text></view></view></view></view>

+ 39 - 41
unpackage/dist/dev/mp-weixin/pages/record/teacher.wxss

@@ -22,60 +22,58 @@
   font-size: 32rpx;
   color: #999;
 }
-.course-group.data-v-d3091b11, .class-group.data-v-d3091b11 {
-  margin-bottom: 40rpx;
-}
-.course-name.data-v-d3091b11 {
-  font-size: 38rpx;
-  font-weight: bold;
-  color: #2E5F8A;
-  margin-bottom: 20rpx;
-  display: block;
-  border-left: 8rpx solid #7DB4D6;
-  padding-left: 15rpx;
-}
-.class-name.data-v-d3091b11 {
-  font-size: 34rpx;
-  font-weight: bold;
-  color: #333;
-  margin: 20rpx 0;
-  display: block;
+.record-view.data-v-d3091b11 {
+  padding-bottom: 50rpx;
 }
-.task-list.data-v-d3091b11 {
+.course-list.data-v-d3091b11 {
   display: flex;
   flex-direction: column;
   gap: 20rpx;
 }
-.task-item.data-v-d3091b11 {
+.course-item.data-v-d3091b11 {
   background-color: #ffffff;
   border-radius: 16rpx;
-  padding: 30rpx;
   box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  grid-gap: 10rpx 20rpx;
-  position: relative;
-}
-.task-status.data-v-d3091b11 {
-  position: absolute;
-  top: 20rpx;
-  right: 20rpx;
-  font-size: 28rpx;
+  overflow: hidden;
+}
+.course-header.data-v-d3091b11 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.course-name.data-v-d3091b11 {
+  font-size: 38rpx;
   font-weight: bold;
+  color: #2E5F8A;
+}
+.class-list-container.data-v-d3091b11 {
+  padding: 20rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 10rpx;
 }
-.task-status.PROCESSING.data-v-d3091b11 {
-  color: #5CB85C;
+.class-item.data-v-d3091b11 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
 }
-.task-status.FINISHED.data-v-d3091b11 {
-  color: #e54d42;
+.class-item.data-v-d3091b11:last-child {
+  border-bottom: none;
 }
-.task-status.NOT_START.data-v-d3091b11 {
-  color: #7DB4D6;
+.class-name.data-v-d3091b11 {
+  font-size: 32rpx;
+  color: #333;
 }
-.task-status.CANCELLED.data-v-d3091b11 {
+.arrow.data-v-d3091b11 {
+  font-size: 28rpx;
   color: #999;
+  transform: rotate(90deg);
+  transition: transform 0.2s ease-in-out;
 }
-.task-info.data-v-d3091b11 {
-  font-size: 28rpx;
-  color: #666;
+.arrow.expanded.data-v-d3091b11 {
+  transform: rotate(270deg);
 }

Някои файлове не бяха показани, защото твърде много файлове са промени