zhxy-jzd/src/pages/base/course-selection/club-selection.vue
2025-05-16 16:16:41 +08:00

1027 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="interest-course">
<!-- 选课信息头部 - 固定部分 -->
<view class="selection-header">
<view class="header-content">
<view class="title-section">
<view class="title">
<text v-if="kcData && kcData.xkmc">{{ kcData.xkmc }}</text>
<text v-else>选课信息</text>
</view>
<!-- <view class="subtitle">互动兴趣课程</view> -->
</view>
<!-- 学生选择部分 -->
<view class="student-selector-bar" @click="showStudentSelector">
<view class="user-avatar">
<image
:src="currentStudent.avatar || '/static/base/home/11222.png'"
class="w-full h-full"
></image>
</view>
<view class="student-info">
<text class="student-name">{{ currentStudent.xm }}</text>
<text class="student-class"
>{{ currentStudent.njmc }} {{ currentStudent.bjmc }}</text
>
</view>
<view class="switch-btn" v-if="studentList.length > 1">切换</view>
</view>
<view class="countdown-section">
<template v-if="!isEnrollmentEnded">
<view class="countdown-title">{{ countdownTitle }}</view>
<view class="countdown-timer">
<view class="time-block">
<text class="time-value">{{ countdownTime.hours }}</text>
<text class="time-unit"></text>
</view>
<text class="time-separator">:</text>
<view class="time-block">
<text class="time-value">{{ countdownTime.minutes }}</text>
<text class="time-unit"></text>
</view>
<text class="time-separator">:</text>
<view class="time-block">
<text class="time-value">{{ countdownTime.seconds }}</text>
<text class="time-unit"></text>
</view>
</view>
</template>
<template v-else>
<view class="enrollment-ended">
<u-icon name="info-circle" color="#FF9900" size="20"></u-icon>
<text>选课已经结束</text>
</view>
</template>
</view>
</view>
</view>
<!-- 可滚动的内容区域 -->
<view class="scrollable-content">
<!-- 课程网格列表 -->
<view class="course-grid" v-if="courseListData.length > 0">
<view
v-for="(course, index) in courseListData"
:key="course.id || index"
class="course-item"
:class="{ selected: course.isSelected }"
@click="toggleSelection(course)"
>
<view class="course-name">{{ course.kcmc }}</view>
<view class="register-info">
<text>报名情况</text>
<text class="register-count">{{ course.ybmr }}</text>
<text> | {{ course.maxNum }}</text>
</view>
<view class="detail-btn" @click.stop="viewCourseDetail(course)"
>详情</view
>
<view v-if="course.isSelected" class="selected-mark">
<uni-icons
type="checkbox-filled"
color="#3FBF72"
size="22"
></uni-icons>
</view>
</view>
</view>
<!-- 暂无数据提示 -->
<view v-else class="empty-course-list">
<view class="empty-icon">
<u-icon name="list" size="50" color="#C8C9CC"></u-icon>
</view>
<view class="empty-text">暂无课程数据</view>
</view>
</view>
<!-- 底部报名按钮 - 固定部分 -->
<view class="register-btn-container">
<view class="selected-count-info" v-if="getSelectedCount > 0">
已选 {{ getSelectedCount }} 门课程
</view>
<view class="register-btn" @click="submitRegistration">点击报名</view>
</view>
<view>
<!-- 学生选择弹窗 -->
<u-popup
:show="showSelector"
@close="showSelector = false"
mode="bottom"
round="10"
>
<view class="student-selector">
<view class="selector-header">
<text class="selector-title">选择学生</text>
<u-icon
name="close"
size="20"
@click="showSelector = false"
></u-icon>
</view>
<view class="student-list">
<view
v-for="(student, index) in studentList"
:key="index"
class="student-item"
:class="{
'student-item-active': currentStudent.id === student.id,
}"
@click="switchStudent(student)"
>
<view class="student-avatar">
<image
:src="student.avatar || '/static/base/home/11222.png'"
class="w-full h-full"
></image>
</view>
<view class="student-info">
<text class="student-name">{{ student.xm }}</text>
<text class="student-class"
>{{ student.njmc }} {{ student.bjmc }}</text
>
</view>
<u-icon
v-if="currentStudent.id === student.id"
name="checkmark"
color="#409EFF"
size="20"
></u-icon>
</view>
</view>
</view>
</u-popup>
</view>
</view>
</template>
<script setup lang="ts">
import { navigateTo } from "@/utils/uniapp";
import { ref, computed, reactive, onBeforeUnmount, watch } from "vue";
import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data";
import { xkAddXkqdApi, xkListApi, xkXkqdApi } from "@/api/base/server";
import dayjs from "dayjs";
const { getUser } = useUserStore();
const { getData, setKcData } = useDataStore();
const { sign_file } = getData;
// 倒计时相关
const countdownTime = reactive({
hours: "00",
minutes: "00",
seconds: "00",
});
// 倒计时标题文本
const countdownTitle = computed(() => {
return kcStatus.value ? "距离选课结束还剩" : "距离选课开始还剩";
});
// 当前选中的学生
let countdownTimer: number | null = null;
const remainTime = ref("00:00:00");
// 学生列表数据
const studentList = computed(() => {
return getUser.xsList;
});
const currentStudent = ref();
// 控制选择器显示状态
const showSelector = ref(false);
if (studentList.value.length > 1) {
showSelector.value = true;
}
const kcData = ref();
const kcStatus = ref(false);
// 添加选课是否已结束的标记
const isEnrollmentEnded = ref(false);
// 封装检查学生报名状态的通用方法
function checkStudentEnrollmentApi(student: any): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!student || !student.id || !student.njId) {
console.error("学生信息不完整:", student);
resolve(false);
return;
}
// 调用选课签到接口检查学生是否已报名
xkXkqdApi({
njId: student.njId,
xsId: student.id,
xklxId: "962488654", // 课程类型ID
})
.then((res) => {
// 根据接口返回的result判断是否已报名
if (res && res.resultCode === 1) {
resolve(!!res.result); // 转换为布尔值返回
} else {
// 接口调用成功但返回错误
console.warn("检查报名状态接口返回错误:", res);
resolve(false);
}
})
.catch((error) => {
// 接口调用失败
console.error("调用检查报名状态接口失败:", error);
reject(error);
});
});
}
// 页面初始化时检查当前学生是否已报名
const checkInitialEnrollment = (currentStudent: any) => {
if (!currentStudent) return;
uni.showLoading({
title: "加载中...",
});
// 使用封装的API函数检查学生报名状态
checkStudentEnrollmentApi(currentStudent)
.then((isEnrolled) => {
if (isEnrolled) {
uni.hideLoading();
uni.reLaunch({
url: `/pages/base/course-selection/enrolled`,
});
} else {
loadCourseList(currentStudent);
}
})
.catch(() => {
uni.hideLoading();
loadCourseList(currentStudent);
});
};
if (studentList.value.length > 0 && studentList.value.length === 1) {
currentStudent.value = studentList.value[0];
// 检查当前学生是否已报名
checkInitialEnrollment(currentStudent.value);
}
// 加载课程列表
const loadCourseList = (currentStudent: any) => {
if (!currentStudent) {
uni.hideLoading();
return;
}
xkListApi({
njId: currentStudent.njId,
xklxId: "962488654",
})
.then((res) => {
uni.hideLoading();
if (res.resultCode == 1) {
if (res.result) {
kcData.value = res.result;
//获取当前时间
const now = dayjs().valueOf();
//获取选课开始时间
const xkkstime = dayjs(res.result.xkkstime).valueOf();
//获取选课结束时间
const xkjstime = dayjs(res.result.xkjstime).valueOf();
// 检查是否已经超过选课结束时间
if (now > xkjstime) {
kcStatus.value = true;
isEnrollmentEnded.value = true;
countdownTime.hours = "00";
countdownTime.minutes = "00";
countdownTime.seconds = "00";
}
//判断选课是否开始
else if (now < xkkstime) {
kcStatus.value = false;
startCountdown(xkkstime);
} else {
kcStatus.value = true;
startCountdown(xkjstime);
}
} else {
uni.reLaunch({
url: "/pages/base/course-selection/notopen",
});
}
}
})
.catch(() => {
uni.hideLoading();
});
};
// 显示学生选择器
function showStudentSelector() {
if (studentList.value.length > 1) {
showSelector.value = true;
}
}
// 切换学生
function switchStudent(student: any) {
currentStudent.value = student;
showSelector.value = false;
// 显示加载中
uni.showLoading({
title: "加载中...",
});
// 使用封装的API函数检查学生是否已报名
checkStudentEnrollmentApi(student)
.then((isEnrolled) => {
uni.hideLoading();
if (isEnrolled) {
// 如果已报名,跳转到已报名页面
uni.navigateTo({
url: `/pages/base/course-selection/enrolled?studentId=${student.id}`,
});
} else {
// 未报名,显示切换成功提示并刷新课程列表
uni.showToast({
title: `已切换到${student.xm}`,
icon: "none",
});
// 加载当前学生的课程列表
loadCourseList(student);
}
})
.catch((error) => {
uni.hideLoading();
uni.showToast({
title: `已切换到${student.xm}`,
icon: "none",
});
console.error("检查学生报名状态出错:", error);
// 出错时也加载课程列表
loadCourseList(student);
});
}
// 启动倒计时
const startCountdown = (endTimeStamp: number) => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
const updateCountdown = () => {
const now = new Date().getTime();
const timeLeft = endTimeStamp - now;
if (timeLeft <= 0) {
// 倒计时结束
countdownTime.hours = "00";
countdownTime.minutes = "00";
countdownTime.seconds = "00";
remainTime.value = "00:00:00";
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
// 判断当前倒计时是选课开始还是选课结束
if (!kcStatus.value && kcData.value) {
// 如果是选课开始倒计时结束,则切换到选课结束倒计时
kcStatus.value = true; // 更新状态为选课已开始
uni.showToast({
title: "选课已开始",
icon: "none",
});
// 开始选课结束倒计时
const xkjstime = dayjs(kcData.value.xkjstime).valueOf();
startCountdown(xkjstime);
} else {
// 如果是选课结束倒计时结束
isEnrollmentEnded.value = true; // 标记选课已结束
uni.showToast({
title: "选课已结束",
icon: "none",
});
}
return;
}
// 计算小时、分钟和秒
const hours = Math.floor(timeLeft / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
// 格式化显示
countdownTime.hours = hours < 10 ? `0${hours}` : `${hours}`;
countdownTime.minutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
countdownTime.seconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
// 更新remainTime(兼容原有逻辑)
remainTime.value = `${countdownTime.hours}:${countdownTime.minutes}:${countdownTime.seconds}`;
};
// 立即执行一次
updateCountdown();
// 每秒更新一次
countdownTimer = setInterval(updateCountdown, 1000) as unknown as number;
};
// 从课程数据中提取课程列表
const displayCourseList = computed(() => {
if (
!kcData.value ||
!kcData.value.xkkcs ||
!Array.isArray(kcData.value.xkkcs)
) {
return [];
}
// 获取本地存储的已选课程ID数组
const selectedCourseIds = uni.getStorageSync("selectedCourseIds") || [];
// 为课程添加选中状态属性
return kcData.value.xkkcs.map((course: any) => ({
...course,
isSelected: selectedCourseIds.includes(course.id),
}));
});
// 可修改的课程列表数据
const courseListData = ref<any[]>([]);
// 监听计算属性变化,更新可修改的数据
watch(
displayCourseList,
(newVal) => {
courseListData.value = JSON.parse(JSON.stringify(newVal));
},
{ immediate: true }
);
// 选择或取消选择课程
const toggleSelection = (course: any) => {
// 只检查课程是否已满,不判断选课是否开始
if (course.ybmr >= course.maxNum) {
uni.showToast({
title: "该课程名额已满",
icon: "none",
});
return;
}
const courseIndex = courseListData.value.findIndex(
(item) => item.id === course.id
);
if (courseIndex === -1) return;
// 切换当前课程的选中状态
courseListData.value[courseIndex].isSelected =
!courseListData.value[courseIndex].isSelected;
// 获取本地存储的已选课程ID数组
let selectedCourseIds = uni.getStorageSync("selectedCourseIds") || [];
if (courseListData.value[courseIndex].isSelected) {
// 添加到已选数组
if (!selectedCourseIds.includes(course.id)) {
selectedCourseIds.push(course.id);
}
} else {
// 从已选数组中移除
selectedCourseIds = selectedCourseIds.filter(
(id: string) => id !== course.id
);
}
// 更新本地存储
uni.setStorageSync("selectedCourseIds", selectedCourseIds);
};
// 查看课程详情
const viewCourseDetail = (course: any) => {
setKcData(course);
uni.navigateTo({
url: `/pages/base/course-selection/detail`,
});
};
// 提交报名
const submitRegistration = () => {
// 直接根据当前时间和选课时间判断是否可以报名
if (!kcData.value || !kcData.value.xkkstime || !kcData.value.xkjstime) {
uni.showToast({
title: "选课信息不完整,请联系校方!",
icon: "none",
});
return;
}
const now = dayjs().valueOf();
const xkkstime = dayjs(kcData.value.xkkstime).valueOf();
const xkjstime = dayjs(kcData.value.xkjstime).valueOf();
// 检查是否在选课时间范围内
if (now < xkkstime) {
uni.showToast({
title: "选课时间未开始,请等待!",
icon: "none",
});
return;
}
if (now > xkjstime) {
uni.showToast({
title: "选课已结束,请等待下一次选课!",
icon: "none",
});
return;
}
const selectedCourses = courseListData.value.filter(
(course: any) => course.isSelected
);
if (selectedCourses.length === 0) {
uni.showToast({
title: "请至少选择一门课程",
icon: "none",
});
return;
}
// 检查所有选中的课程是否有已满的
const fullCourses = selectedCourses.filter(
(course) => course.ybmr >= course.maxNum
);
if (fullCourses.length > 0) {
uni.showToast({
title: `"${fullCourses[0].kcmc}"名额已满,请重新选择!`,
icon: "none",
});
return;
}
// 构建课程名称列表用于显示
const courseNames = selectedCourses
.map((course) => `"${course.kcmc}"`)
.join("、");
uni.showModal({
title: "确认报名",
content: `您确定要为${currentStudent.value.xm}报名以下课程吗?\n${courseNames}`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: "报名中...",
});
try {
// 依次提交每个选中的课程
for (const course of selectedCourses) {
const result = await xkAddXkqdApi({
njId: currentStudent.value.njId,
xsId: currentStudent.value.id,
xklxId: course.id,
sign_file,
});
if (result.resultCode !== 1) {
throw new Error(
`课程"${course.kcmc}"报名失败: ${result.message || "未知错误"}`
);
}
}
// 全部成功报名
uni.hideLoading();
uni.showToast({
title: "报名成功!",
icon: "success",
duration: 2000,
});
// 清除选课记录
uni.removeStorageSync("selectedCourseIds");
// 延迟后跳转到已报名页面
setTimeout(() => {
uni.reLaunch({
url: `/pages/base/course-selection/enrolled`,
});
}, 2000);
} catch (error: any) {
uni.hideLoading();
uni.showToast({
title: error.message || "报名失败,请稍后重试",
icon: "none",
duration: 3000,
});
}
}
},
});
};
// 页面卸载前清除定时器
onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
});
// 计算已选课程数量
const getSelectedCount = computed(() => {
return courseListData.value.filter((course) => course.isSelected).length;
});
</script>
<style lang="scss" scoped>
.interest-course {
min-height: 100%;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
height: 44px;
background-color: #fff;
.nav-left {
width: 40px;
height: 40px;
display: flex;
align-items: center;
}
.nav-title {
font-size: 18px;
font-weight: 500;
color: #333;
}
.nav-right {
width: 40px;
display: flex;
justify-content: flex-end;
}
}
.selection-header {
background: linear-gradient(135deg, #4a90e2, #2879ff);
padding: 20px 15px;
color: #fff;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 12px rgba(40, 121, 255, 0.2);
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 10;
.header-content {
display: flex;
flex-direction: column;
gap: 15px;
.title-section {
.title {
font-size: 24px;
font-weight: bold;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
}
}
// 学生选择栏样式
.student-selector-bar {
display: flex;
align-items: center;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 10px;
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
overflow: hidden;
margin-right: 10px;
}
.student-info {
flex: 1;
display: flex;
flex-direction: column;
.student-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 2px;
}
.student-class {
font-size: 12px;
opacity: 0.8;
}
}
.switch-btn {
padding: 4px 12px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 20px;
font-size: 13px;
}
}
.countdown-section {
.countdown-title {
font-size: 14px;
margin-bottom: 8px;
}
.countdown-timer {
display: flex;
align-items: center;
.time-block {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 6px;
min-width: 40px;
padding: 5px 8px;
text-align: center;
.time-value {
font-size: 20px;
font-weight: bold;
display: block;
}
.time-unit {
font-size: 12px;
opacity: 0.9;
}
}
.time-separator {
font-size: 20px;
font-weight: bold;
margin: 0 5px;
}
}
}
}
}
// 可滚动内容区域样式
.scrollable-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; // 增强iOS滚动体验
}
.course-grid {
display: flex;
flex-wrap: wrap;
padding: 15px 15px 0 15px;
.course-item {
position: relative;
width: calc(50% - 10px);
margin-bottom: 15px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
box-sizing: border-box;
border: 1px solid transparent;
transition: all 0.3s ease;
&:nth-child(odd) {
margin-right: 10px;
}
&:nth-child(even) {
margin-left: 10px;
}
&.selected {
border: 1px solid #3fbf72;
background-color: rgba(63, 191, 114, 0.05);
box-shadow: 0 2px 8px rgba(63, 191, 114, 0.15);
}
.course-name {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
}
.register-info {
font-size: 14px;
color: #666;
margin-bottom: 12px;
.register-count {
color: #2879ff;
}
}
.detail-btn {
display: inline-block;
color: #2879ff;
font-size: 14px;
}
.selected-mark {
position: absolute;
top: -6px;
right: -6px;
}
}
}
.register-btn-container {
position: sticky;
bottom: 0;
left: 0;
right: 0;
padding: 15px;
background-color: #fff;
z-index: 10;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.selected-count-info {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.register-btn {
height: 50px;
line-height: 50px;
text-align: center;
background-color: #2879ff;
color: #fff;
border-radius: 25px;
font-size: 16px;
font-weight: 500;
}
.multi-select-tip {
font-size: 12px;
color: #909399;
text-align: center;
}
}
/* 学生选择器弹窗样式 */
.student-selector {
background-color: #ffffff;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding-bottom: 20px;
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f2f2f2;
.selector-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.student-list {
padding: 0 15px;
.student-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f2f2f2;
&:last-child {
border-bottom: none;
}
&-active {
background-color: rgba(64, 158, 255, 0.05);
}
.student-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
background-color: #f0f0f0;
overflow: hidden;
flex-shrink: 0;
}
.student-info {
flex: 1;
margin-left: 12px;
.student-name {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.student-class {
font-size: 13px;
color: #606266;
}
}
}
}
}
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
// 暂无数据样式
.empty-course-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
.empty-icon {
margin-bottom: 20px;
background-color: #f5f6f7;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #909399;
max-width: 80%;
line-height: 1.5;
}
}
/* 选课已结束样式 */
.enrollment-ended {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px 15px;
font-size: 16px;
font-weight: 500;
color: #fff;
gap: 8px;
}
</style>