重新封装模块

This commit is contained in:
ywyonui 2025-06-29 19:24:41 +08:00
parent ee2e2fc16c
commit c12aa5c773
9 changed files with 834 additions and 1124 deletions

View File

@ -62,6 +62,13 @@ export const xsXkListApi = async (params: any) => {
return await get("/mobile/jz/xsxk/list", params); return await get("/mobile/jz/xsxk/list", params);
}; };
/**
*
*/
export const xsXkkcListApi = async (params: any) => {
return await get("/mobile/jz/xkkc/list", params);
};
/** /**
* *
*/ */

View File

@ -0,0 +1,208 @@
<template>
<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>
</template>
<script setup lang="ts">
import dayjs from "dayjs";
//
const props = defineProps<{
xk: any //
}>();
// emit
const emit = defineEmits(['over'])
//
const countdownTime = reactive({
hours: "00",
minutes: "00",
seconds: "00",
});
const kcStatus = ref(false);
//
const countdownTitle = computed(() => {
return kcStatus.value ? "距离选课结束还剩" : "距离选课开始还剩";
});
//
let countdownTimer: number | null = null;
const remainTime = ref("00:00:00");
//
const isEnrollmentEnded = ref(false);
//
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 && props.xk) {
//
kcStatus.value = true; //
uni.showToast({
title: "选课已开始",
icon: "none",
});
//
const xkjstime = dayjs(props.xk.xkjstime).valueOf();
startCountdown(xkjstime);
} else {
//
isEnrollmentEnded.value = true; //
emit("over");
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 changeXk = (xk: any) => {
//
const now = dayjs().valueOf();
//
const xkkstime = dayjs(xk.xkkstime).valueOf();
//
const xkjstime = dayjs(xk.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);
}
}
//
onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
});
watch(() => props.xk, (newVal) => {
changeXk(newVal);
});
</script>
<style lang="scss" scoped>
.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;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<view>
<view class="title-section" @click="showPicker">
<view class="title">
<text v-if="curXk && curXk.xkmc">{{ curXk.xkmc }}</text>
<text v-else>选课信息</text>
</view>
<view class="switch-btn" v-if="xkList.length > 1">切换</view>
</view>
<!-- 俱乐部选择弹窗 -->
<u-popup :show="showFlag" @close="showFlag = false" mode="bottom" round="10">
<view class="xk-picker">
<view class="xk-header">
<text class="xk-title">选择俱乐部</text>
<u-icon name="close" size="20" @click="showFlag = false"></u-icon>
</view>
<view class="xk-list">
<view v-for="(xk, index) in xkList" :key="index" class="xk-item" :class="{
'xk-item-active': curXk.id === xk.id
}" @click="switchXk(xk)">
<view class="xk-info">
<text class="xk-name">{{ xk.xkmc }}</text>
</view>
<u-icon v-if="curXk.id === xk.id" name="checkmark" color="#409EFF" size="20"></u-icon>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { xsXkkcListApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user";
const { getCurXs } = useUserStore();
//
const props = defineProps<{
xsId: string,
njId: string,
xklxId: string; // Id962488654 / 816059832
}>();
// emit
const emit = defineEmits(['change'])
const curXs = computed(() => getCurXs);
//
const xkList = ref<any>([]);
//
const curXk = ref<any>({});
//
const showFlag = ref(false);
//
const showPicker = () => {
showFlag.value = true;
}
//
const loadXkList = async () => {
uni.showLoading({
title: "加载中...",
});
xsXkkcListApi(props)
.then((res) => {
if (res.resultCode == 1) {
if (res.result && res.result.length) {
xkList.value = res.result;
curXk.value = res.result[0];
} else {
xkList.value = [];
curXk.value = {};
}
switchXk(curXk.value);
uni.hideLoading();
}
})
.catch(() => {
uni.hideLoading();
});
}
//
function switchXk(xk: any) {
curXk.value = xk;
showFlag.value = false;
//
uni.showToast({
title: `已切换到${xk.xkmc}`,
icon: "none",
});
emit("change", xk);
}
//
watch(() => props.xsId, (newVal) => {
console.log("当前学生信息变更", newVal);
loadXkList();
});
//
if (props.xsId && props.njId) {
loadXkList();
}
</script>
<style lang="scss" scoped>
/* 全局图片样式 */
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.title-section {
display: flex;
align-items: center;
.title {
flex: 1 0 1px;
font-size: 24px;
font-weight: bold;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
}
.switch-btn {
padding: 5px 15px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 15px;
font-size: 15px;
}
}
/* 选择器弹窗样式 */
.xk-picker {
background-color: #ffffff;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding-bottom: 20px;
.xk-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f2f2f2;
.xk-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.xk-list {
padding: 0 15px;
.xk-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);
}
.xk-info {
flex: 1;
margin-left: 12px;
.xk-name {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,353 @@
<template>
<view>
<view class="user-content" v-if="!isBar">
<view class="user-avatar">
<image :src="curXs.xstxUrl || '/static/base/home/11222.png'" class="avatar-img"></image>
<view class="avatar-ring"></view>
</view>
<view class="user-details">
<text class="user-name">{{ curXs.xm }}</text>
<view class="user-class-container">
<view class="class-tag">
<text class="user-class">{{ curXs.njmc }} {{ curXs.bjmc }}</text>
</view>
</view>
</view>
<view class="btn-group">
<view class="switch-btn" @click="showPicker" v-if="xsList && xsList.length > 1">
<u-icon name="arrow-down" size="12" color="#fff"></u-icon>
<text>切换</text>
</view>
</view>
</view>
<view class="xs-bar" @click="showPicker" v-else>
<view class="user-avatar">
<image :src="curXs.xstxUrl || '/static/base/home/11222.png'" class="w-full h-full"></image>
</view>
<view class="xs-info">
<text class="xs-name">{{ curXs.xm }}</text>
<text class="xs-bj">{{ curXs.njmc }} {{ curXs.bjmc }}</text>
</view>
<view class="switch-btn" v-if="xsList.length > 1">切换</view>
</view>
<u-popup :show="showFlag" @close="showFlag = false" mode="bottom" round="20">
<view class="xs-picker">
<view class="xs-header">
<text class="xs-title">选择学生</text>
<view class="close-btn" @click="showFlag = false">
<u-icon name="close" size="18" color="#909399"></u-icon>
</view>
</view>
<view class="xs-list">
<view v-for="(xs, index) in xsList" :key="index" class="xs-item"
:class="{ 'xs-item-active': curXs.id === xs.id }" @click="switchXs(xs)">
<view class="xs-tx">
<image :src="xs.xstxUrl || '/static/base/home/11222.png'" class="tx-img"></image>
</view>
<view class="xs-info">
<text class="xs-name">{{ xs.xm }}</text>
<text class="xs-class">{{ xs.njmc }} {{ xs.bjmc }}</text>
</view>
<view class="check-icon" v-if="curXs.id === xs.id">
<u-icon name="checkmark" color="#4A90E2" size="18"></u-icon>
</view>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from "@/store/modules/user";
const { getUser, setCurXs, getCurXs } = useUserStore();
//
const props = defineProps<{
isBar: boolean;
}>();
// emit
const emit = defineEmits(['change'])
//
const xsList = computed(() => getUser.xsList)
//
const curXs = computed(() => getCurXs);
//
const showFlag = ref(false);
//
const showPicker = () => {
showFlag.value = true;
}
//
const switchXs = (xs: any) => {
setCurXs(xs);
showFlag.value = false;
emit('change', xs);
//
uni.showToast({
title: `已切换到${xs.xm}`,
icon: "none",
});
}
// bar
if (props.isBar && getUser.xsList.length > 1) {
showPicker();
}
</script>
<style lang="scss" scoped>
/** 当前学生信息 */
.user-content {
display: flex;
align-items: center;
position: relative;
z-index: 2;
.user-avatar {
position: relative;
width: 70px;
height: 70px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 16px rgba(74, 144, 226, 0.3);
border: 3px solid #ffffff;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-ring {
position: absolute;
top: -3px;
left: -3px;
width: calc(100% + 6px);
height: calc(100% + 6px);
border-radius: 50%;
border: 2px solid rgba(74, 144, 226, 0.3);
animation: pulse 2s infinite;
}
}
.user-details {
flex: 1;
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.user-name {
font-size: 18px;
font-weight: 600;
color: white;
margin-bottom: 8px;
line-height: 1.2;
}
.user-class-container {
display: flex;
align-items: center;
.class-tag {
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: #ffffff;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
.user-class {
line-height: 1;
}
}
}
}
.switch-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 8px 16px;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: #ffffff;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
transition: all 0.3s ease;
flex-shrink: 0;
min-width: 60px;
height: 16px;
&:active {
transform: scale(0.95);
}
text {
line-height: 1;
}
}
}
//
.xs-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;
}
.xs-info {
flex: 1;
display: flex;
flex-direction: column;
.xs-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 2px;
}
.xs-bj {
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;
}
}
/* 学生选择器弹窗样式 */
.xs-picker {
background-color: #ffffff;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
padding-bottom: 20px;
.xs-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f2f2f2;
.xs-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
.xs-list {
padding: 0 15px;
.xs-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);
}
.xs-tx {
width: 45px;
height: 45px;
border-radius: 50%;
background-color: #f0f0f0;
overflow: hidden;
flex-shrink: 0;
.tx-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.xs-info {
flex: 1;
margin-left: 12px;
.xs-name {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.xs-class {
font-size: 13px;
color: #606266;
}
}
}
}
}
/* 动画效果 */
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式优化 */
@media (max-width: 375px) {
.user-content .user-avatar {
width: 60px;
height: 60px;
}
}
</style>

View File

@ -14,7 +14,7 @@
<view class="student-selector-bar" @click="showStudentSelector"> <view class="student-selector-bar" @click="showStudentSelector">
<view class="user-avatar"> <view class="user-avatar">
<image <image
:src="currentStudent.avatar || '/static/base/home/11222.png'" :src="currentStudent.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full" class="w-full h-full"
></image> ></image>
</view> </view>
@ -132,7 +132,7 @@
> >
<view class="student-avatar"> <view class="student-avatar">
<image <image
:src="student.avatar || '/static/base/home/11222.png'" :src="student.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full" class="w-full h-full"
></image> ></image>
</view> </view>

View File

@ -3,75 +3,29 @@
<!-- 选课信息头部 - 固定部分 --> <!-- 选课信息头部 - 固定部分 -->
<view class="selection-header"> <view class="selection-header">
<view class="header-content"> <view class="header-content">
<view class="title-section"> <!-- 选课类型选择部分 -->
<view class="title"> <XkPicker xklx-id="816059832" :xs-id="curXs.id" :nj-id="curXs.njId" @change="switchXk" />
<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"> <XsPicker :is-bar="true" />
<view class="user-avatar"> <!-- 倒计时-->
<image <XkCountdown :xk="curXk" @over="xkTimeOver" />
:src="curXs.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full"
></image>
</view>
<view class="student-info">
<text class="student-name">{{ curXs.xm }}</text>
<text class="student-class"
>{{ curXs.njmc }} {{ curXs.bjmc }}</text
>
</view>
<view class="switch-btn" v-if="xsList.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> </view>
<!-- 可滚动的内容区域 --> <!-- 可滚动的内容区域 -->
<view class="scrollable-content"> <view class="scrollable-content">
<!-- 课程网格列表 --> <!-- 课程网格列表 -->
<view class="course-grid" v-if="xkkcList.length > 0"> <view class="course-grid" v-if="courseListData.length > 0">
<view <view
v-for="(course, index) in xkkcList" v-for="(course, index) in courseListData"
:key="course.id || index" :key="course.id || index"
class="course-item" class="course-item"
:class="{ selected: course.isSelected }" :class="{ selected: course.isSelected }"
@click="toggleSelection(course)"
> >
<view class="course-name">{{ course.kcmc }}</view> <view class="course-name">{{ course.kcmc }}</view>
<view class="register-info"> <view class="register-info">
<text>报名情况</text> <text>上课人数</text>
<text class="register-count">{{ course.ybmr }}</text> <text class="register-count">{{ course.ybmr }}</text>
<text> | {{ course.maxNum }}</text>
</view> </view>
<view class="detail-btn" @click.stop="viewCourseDetail(course)" <view class="detail-btn" @click.stop="viewCourseDetail(course)"
>详情</view >详情</view
@ -95,421 +49,41 @@
</view> </view>
</view> </view>
<!-- 底部报名按钮 - 固定部分 -->
<view class="register-btn-container">
<view class="register-btn" @click="submitRegistration">
<text v-if="selectedCoursesCount > 0">
点击报名 (已选{{ selectedCoursesCount }})
</text>
<text v-else>点击报名</text>
</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="(xs, index) in xsList"
:key="index"
class="student-item"
:class="{
'student-item-active': curXs.id === xs.id,
}"
@click="switchXs(xs)"
>
<view class="student-avatar">
<image
:src="xs.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full"
></image>
</view>
<view class="student-info">
<text class="student-name">{{ xs.xm }}</text>
<text class="student-class"
>{{ xs.njmc }} {{ xs.bjmc }}</text
>
</view>
<u-icon
v-if="curXs.id === xs.id"
name="checkmark"
color="#409EFF"
size="20"
></u-icon>
</view>
</view>
</view>
</u-popup>
</view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import XsPicker from "@/pages/base/components/XsPicker/index.vue"
ref, import XkPicker from "@/pages/base/components/XkPicker/index.vue"
computed, import XkCountdown from "@/pages/base/components/XkCountdown/index.vue"
reactive,
onBeforeUnmount,
watch,
onMounted,
} from "vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
import {
xkAddXkqdApi,
xkListApi,
xkxkbmInfoApi,
xkXkqdApi,
} from "@/api/base/server";
import dayjs from "dayjs";
const { getUser } = useUserStore(); const { getCurXs } = useUserStore();
const { getData, setKcData, setData } = useDataStore(); const { setKcData } = useDataStore();
const { sign_file } = getData;
// const curXs = computed(() => getCurXs);
const countdownTime = reactive({
hours: "00",
minutes: "00",
seconds: "00",
});
// const curXk = ref();
const countdownTitle = computed(() => {
return kcStatus.value ? "距离选课结束还剩" : "距离选课开始还剩";
});
//
let countdownTimer: number | null = null;
const remainTime = ref("00:00:00");
//
const xsList = computed(() => {
return getUser.xsList;
});
const curXs = ref<any>({});
//
const showSelector = ref(false);
const kcData = ref();
const kcStatus = ref(false); const kcStatus = ref(false);
// const courseInfo = ref<any>({});
const isEnrollmentEnded = ref(false); //
const switchXk = (xk: any) => {
// curXk.value = xk;
let pollTimer: number | null = null;
const curXkkc = ref({});
//
function checkStudentEnrollmentApi(xs: any): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!xs || !xs.id || !xs.njId) {
console.error("学生信息不完整:", xs);
resolve(false);
return;
} }
// kcData //
if (!kcData.value || !kcData.value.id) { const xkTimeOver = (val: any) => {
console.error("课程数据未加载:", kcData.value); console.log(val);
resolve(false);
return;
} }
//
xkXkqdApi({
njId: xs.njId,
xsId: xs.id,
xklxId: "816059832", // ID
xkId: kcData.value.id,
})
.then((res) => {
console.log(1122, res);
// result
if (res && res.resultCode === 1) {
setData({
...getData,
kcData,
studentInfo: xs,
enrolledCourse: res.result,
});
resolve(res.result.length > 0); //
} else {
//
console.warn("检查报名状态接口返回错误:", res);
resolve(false);
}
})
.catch((error) => {
//
console.error("调用检查报名状态接口失败:", error);
reject(error);
});
});
}
//
const pollEnrollmentCount = () => {
xkxkbmInfoApi({ xkId: kcData.value.id })
.then((res) => {
if (res && res.resultCode === 1 && Array.isArray(res.result)) {
updateEnrollmentCount(res.result);
}
})
.catch((error) => {
console.error("获取报名人数失败:", error);
});
};
//
const updateEnrollmentCount = (
data: Array<{ xkkcId: string; bmrs: number }>
) => {
if (!Array.isArray(xkkcList.value) || xkkcList.value.length === 0)
return;
let hasUpdates = false;
xkkcList.value.forEach((course) => {
const newCount = data.find((item) => item.xkkcId === course.id);
if (newCount && course.ybmr !== newCount.bmrs) {
course.ybmr = newCount.bmrs;
hasUpdates = true;
}
});
//
if (hasUpdates) {
xkkcList.value = [...xkkcList.value];
}
};
//
const startPolling = (interval = 5000) => {
//
stopPolling();
//
pollEnrollmentCount();
//
pollTimer = setInterval(pollEnrollmentCount, interval) as unknown as number;
};
//
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
};
//
onMounted(() => {
if (xsList.value.length > 0 && xsList.value.length === 1) {
showSelector.value = true;
}
//
watch(xkkcList, (newVal) => {
if (newVal.length > 0 && !pollTimer) {
startPolling();
}
});
});
//
const checkInitialEnrollment = (curXs: any) => {
if (!curXs) return;
// 使API
checkStudentEnrollmentApi(curXs)
.then((isEnrolled) => {
if (isEnrolled) {
uni.hideLoading();
uni.reLaunch({
url: `/pages/base/course-selection/enrolled`,
});
}
})
.catch(() => {
//
console.log("检查学生报名状态失败,继续正常流程");
});
};
//
const loadXkList = (curXs: any) => {
if (!curXs) {
uni.hideLoading();
return;
}
xkListApi({
njId: curXs.njId,
xklxId: "816059832",
})
.then((res) => {
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);
}
//
checkInitialEnrollment(curXs);
} else {
uni.reLaunch({
url: "/pages/base/course-selection/notopen",
});
}
uni.hideLoading();
}
})
.catch(() => {
uni.hideLoading();
});
};
//
const showStudentSelector = () => {
if (xsList.value.length > 1) {
showSelector.value = true;
}
}
//
const switchXs = (xs: any) => {
curXs.value = xs;
showSelector.value = false;
//
uni.showLoading({
title: "加载中...",
});
//
uni.showToast({
title: `已切换到${xs.xm}`,
icon: "none",
});
//
loadXkList(xs);
}
//
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(() => { const displayCourseList = computed(() => {
if ( if (
!kcData.value || !curXk.value ||
!kcData.value.xkkcs || !curXk.value.xkkcs ||
!Array.isArray(kcData.value.xkkcs) !Array.isArray(curXk.value.xkkcs)
) { ) {
return []; return [];
} }
@ -518,87 +92,38 @@ const displayCourseList = computed(() => {
const selectedCourseIds = uni.getStorageSync("selectedCourseIds") || []; const selectedCourseIds = uni.getStorageSync("selectedCourseIds") || [];
// //
return kcData.value.xkkcs.map((course: any) => ({ return curXk.value.xkkcs.map((course: any) => ({
...course, ...course,
isSelected: selectedCourseIds.includes(course.id), isSelected: selectedCourseIds.includes(course.id),
})); }));
}); });
// //
const xkkcList = ref<any[]>([]); const courseListData = ref<any[]>([]);
// //
const selectedCoursesCount = computed(() => { const selectedCoursesCount = computed(() => {
return xkkcList.value.filter((course: any) => course.isSelected).length; return courseListData.value.filter((course: any) => course.isSelected).length;
}); });
// //
watch( watch(
displayCourseList, displayCourseList,
(newVal) => { (newVal) => {
xkkcList.value = JSON.parse(JSON.stringify(newVal)); courseListData.value = JSON.parse(JSON.stringify(newVal));
// curXkkc // courseInfo
const selectedCourseIds = uni.getStorageSync("selectedCourseIds") || []; const selectedCourseIds = uni.getStorageSync("selectedCourseIds") || [];
if (selectedCourseIds.length > 0) { if (selectedCourseIds.length > 0) {
const selectedCourses = newVal.filter((course: any) => const selectedCourses = newVal.filter((course: any) =>
selectedCourseIds.includes(course.id) selectedCourseIds.includes(course.id)
); );
curXkkc.value = selectedCourses; courseInfo.value = selectedCourses;
} }
}, },
{ immediate: true } { immediate: true }
); );
//
const toggleSelection = (course: any) => {
//
if (course.ybmr >= course.maxNum) {
uni.showToast({
title: "该课程名额已满",
icon: "none",
});
return;
}
const courseIndex = xkkcList.value.findIndex(
(item) => item.id === course.id
);
if (courseIndex === -1) return;
//
xkkcList.value[courseIndex].isSelected =
!xkkcList.value[courseIndex].isSelected;
// ID
const selectedCourseIds = xkkcList.value
.filter((item: any) => item.isSelected)
.map((item: any) => item.id);
// ID
uni.setStorageSync("selectedCourseIds", selectedCourseIds);
// curXkkc
curXkkc.value = xkkcList.value.filter(
(item: any) => item.isSelected
);
//
if (xkkcList.value[courseIndex].isSelected) {
uni.showToast({
title: `已选择 ${course.kcmc}`,
icon: "none",
duration: 1500,
});
} else {
uni.showToast({
title: `已取消选择 ${course.kcmc}`,
icon: "none",
duration: 1500,
});
}
};
// //
const viewCourseDetail = (course: any) => { const viewCourseDetail = (course: any) => {
setKcData(course); setKcData(course);
@ -607,116 +132,8 @@ const viewCourseDetail = (course: any) => {
}); });
}; };
//
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 = xkkcList.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: `您确定要为${curXs.value.xm}报名以下课程吗?\n\n${courseNames}`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: "报名中...",
});
// ID
const selectedCourseIds = selectedCourses
.map((course) => course.id)
.join(",");
const res = await xkAddXkqdApi({
xsId: curXs.value.id,
xkkcId: selectedCourseIds,
qmFile: sign_file,
xklxId: "816059832",
});
uni.hideLoading();
if (res.resultCode == 1) {
setData({
...getData,
kcData,
studentInfo: curXs.value,
enrolledCourse: res.result,
});
uni.showToast({
title: "报名成功",
icon: "none",
});
setTimeout(() => {
uni.reLaunch({
url: `/pages/base/course-selection/enrolled`,
});
}, 1500);
}
}
},
});
};
// //
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
stopPolling();
}); });
</script> </script>
@ -775,97 +192,6 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
gap: 15px; 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;
}
}
}
} }
} }
@ -959,82 +285,6 @@ onBeforeUnmount(() => {
} }
} }
/* 学生选择器弹窗样式 */
.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 { .empty-course-list {
display: flex; display: flex;

View File

@ -18,7 +18,7 @@
<view class="student-info"> <view class="student-info">
<view class="student-avatar"> <view class="student-avatar">
<image <image
:src="studentInfo.avatar || '/static/base/home/11222.png'" :src="studentInfo.xstxUrl || '/static/base/home/11222.png'"
mode="aspectFill" mode="aspectFill"
></image> ></image>
</view> </view>
@ -164,7 +164,7 @@ const studentInfo = ref({
xm: getData.value.studentInfo.xm, xm: getData.value.studentInfo.xm,
njmc: getData.value.studentInfo.njmc, njmc: getData.value.studentInfo.njmc,
bjmc: getData.value.studentInfo.bjmc, bjmc: getData.value.studentInfo.bjmc,
avatar: getData.value.studentInfo.avatar, xstxUrl: getData.value.studentInfo.xstxUrl,
}); });
// //

View File

@ -14,7 +14,7 @@
<view class="student-selector-bar" @click="showStudentSelector"> <view class="student-selector-bar" @click="showStudentSelector">
<view class="user-avatar"> <view class="user-avatar">
<image <image
:src="currentStudent.avatar || '/static/base/home/11222.png'" :src="currentStudent.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full" class="w-full h-full"
></image> ></image>
</view> </view>
@ -129,7 +129,7 @@
> >
<view class="student-avatar"> <view class="student-avatar">
<image <image
:src="student.avatar || '/static/base/home/11222.png'" :src="student.xstxUrl || '/static/base/home/11222.png'"
class="w-full h-full" class="w-full h-full"
></image> ></image>
</view> </view>

View File

@ -8,34 +8,14 @@
<view class="banner-overlay"> <view class="banner-overlay">
</view> </view>
</view> </view>
<view class="user-content"> <!-- 学生信息 -->
<view class="user-avatar"> <XsPicker :is-bar="false" @change="switchXs" />
<image :src="curXs.xstxUrl || '/static/base/home/11222.png'" class="avatar-img"></image>
<view class="avatar-ring"></view> <view class="glxs-btn" @click="goToGlxs">
</view> <u-icon name="plus" size="12" color="#fff"></u-icon>
<view class="user-details">
<text class="user-name">{{ curXs.xm }}</text>
<view class="user-class-container">
<view class="class-tag">
<text class="user-class">{{ curXs.njmc }} {{ curXs.bjmc }}</text>
</view>
</view>
</view>
<view class="btn-group">
<view class="switch-btn" @click="goToGlxs">
<u-icon name="arrow-down" size="12" color="#fff"></u-icon>
<text>新增学生</text> <text>新增学生</text>
</view> </view>
<view class="switch-btn" @click="showXsSelector" v-if="xsList && xsList.length > 1">
<u-icon name="arrow-down" size="12" color="#fff"></u-icon>
<text>切换</text>
</view> </view>
</view>
</view>
</view>
<!-- 学校横幅 -->
<!-- -->
<!-- 功能菜单 --> <!-- 功能菜单 -->
<view class="menu-section"> <view class="menu-section">
@ -91,51 +71,16 @@
</view> </view>
</view> </view>
<!-- 学生选择弹窗 -->
<u-popup
:show="showSelector"
@close="showSelector = false"
mode="bottom"
round="20"
>
<view class="student-selector">
<view class="selector-header">
<text class="selector-title">选择学生</text>
<view class="close-btn" @click="showSelector = false">
<u-icon name="close" size="18" color="#909399"></u-icon>
</view>
</view>
<view class="student-list">
<view
v-for="(xs, index) in xsList"
:key="index"
class="student-item"
:class="{ 'student-item-active': curXs.id === xs.id }"
@click="switchXs(xs)"
>
<view class="student-avatar">
<image :src="xs.xstxUrl || '/static/base/home/11222.png'" class="avatar-img"></image>
</view>
<view class="student-info">
<text class="student-name">{{ xs.xm }}</text>
<text class="student-class">{{ xs.njmc }} {{ xs.bjmc }}</text>
</view>
<view class="check-icon" v-if="curXs.id === xs.id">
<u-icon name="checkmark" color="#4A90E2" size="18"></u-icon>
</view>
</view>
</view>
</view>
</u-popup>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import XsPicker from "@/pages/base/components/XsPicker/index.vue"
import { cmsArticlePageApi } from "@/api/base/server"; import { cmsArticlePageApi } from "@/api/base/server";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useDataStore } from "@/store/modules/data"; import { useDataStore } from "@/store/modules/data";
const { getUser, setCurXs, getCurXs } = useUserStore(); const { getCurXs } = useUserStore();
const { setData, getAppCode } = useDataStore(); const { setData, getAppCode } = useDataStore();
// //
@ -199,24 +144,8 @@ const announcements = ref<any>([
} }
]) ])
//
const xsList = ref([
{
id: "1",
xm: "陶亦菲",
njmc: "2年级",
bjmc: "01班",
njmcId: "",
xstx: "/static/base/home/11222.png",
xstxUrl: ''
}
]);
// //
let curXs = ref(xsList.value[0]); let curXs = computed(() => getCurXs)
//
const showSelector = ref(false);
let pageParams = ref({ let pageParams = ref({
page: 1, page: 1,
@ -240,23 +169,10 @@ function handleMenuClick(item: any) {
} }
} }
//
function showXsSelector() {
showSelector.value = true;
}
// //
function switchXs(xs: any) { function switchXs(xs: any) {
curXs.value = xs; curXs = xs;
showSelector.value = false;
getArticleList(); getArticleList();
//
setCurXs(xs);
//
uni.showToast({
title: `已切换到${xs.xm}`,
icon: "none",
});
} }
// //
@ -279,8 +195,6 @@ const getArticleList = async () => {
}; };
onMounted(async () => { onMounted(async () => {
xsList.value = getUser.xsList;
curXs.value = getCurXs;
getArticleList(); getArticleList();
}); });
</script> </script>
@ -337,76 +251,10 @@ onMounted(async () => {
} }
} }
.user-content { .glxs-btn {
display: flex;
align-items: center;
position: relative;
z-index: 2;
.user-avatar {
position: relative;
width: 70px;
height: 70px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 16px rgba(74, 144, 226, 0.3);
border: 3px solid #ffffff;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-ring {
position: absolute; position: absolute;
top: -3px; right: 0;
left: -3px; bottom: 0;
width: calc(100% + 6px);
height: calc(100% + 6px);
border-radius: 50%;
border: 2px solid rgba(74, 144, 226, 0.3);
animation: pulse 2s infinite;
}
}
.user-details {
flex: 1;
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
.user-name {
font-size: 18px;
font-weight: 600;
color: white;
margin-bottom: 8px;
line-height: 1.2;
}
.user-class-container {
display: flex;
align-items: center;
.class-tag {
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: #ffffff;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
.user-class {
line-height: 1;
}
}
}
}
.switch-btn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -414,7 +262,8 @@ onMounted(async () => {
padding: 8px 16px; padding: 8px 16px;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%); background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: #ffffff; color: #ffffff;
border-radius: 20px; border-top-left-radius: 20px;
border-bottom-right-radius: 20px;
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3); box-shadow: 0 4px 12px rgba(74, 144, 226, 0.3);
@ -423,16 +272,11 @@ onMounted(async () => {
min-width: 60px; min-width: 60px;
height: 16px; height: 16px;
&:active {
transform: scale(0.95);
}
text { text {
line-height: 1; line-height: 1;
} }
} }
} }
}
/* 功能菜单 */ /* 功能菜单 */
.menu-section { .menu-section {
@ -662,160 +506,12 @@ onMounted(async () => {
} }
} }
/* 学生选择器弹窗样式 */
.student-selector {
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
border-top-left-radius: 20px;
border-top-right-radius: 20px;
padding-bottom: 30px;
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f2f5;
.selector-title {
font-size: 18px;
font-weight: 600;
color: #303133;
line-height: 1;
}
.close-btn {
width: 32px;
height: 32px;
background-color: #f5f7fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
&:active {
transform: scale(0.9);
background-color: #e6e8eb;
}
}
}
.student-list {
padding: 0 20px;
.student-item {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f5f7fa;
transition: all 0.3s ease;
border-radius: 12px;
margin-bottom: 8px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
&-active {
background: linear-gradient(135deg, rgba(74, 144, 226, 0.08) 0%, rgba(53, 122, 189, 0.05) 100%);
padding: 16px 12px;
}
.student-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.15);
border: 2px solid #ffffff;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.student-info {
flex: 1;
margin-left: 15px;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
.student-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
line-height: 1.2;
}
.student-class {
font-size: 13px;
color: #606266;
font-weight: 400;
line-height: 1;
}
}
.check-icon {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(74, 144, 226, 0.3);
flex-shrink: 0;
}
}
}
}
/* 动画效果 */
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式优化 */ /* 响应式优化 */
@media (max-width: 375px) { @media (max-width: 375px) {
.content-container { .content-container {
padding: 12px; padding: 12px;
} }
.user-info-card .user-content .user-avatar {
width: 60px;
height: 60px;
}
.grid-menu .grid-item { .grid-menu .grid-item {
padding: 15px 8px; padding: 15px 8px;
} }