1018 lines
22 KiB
Vue
Raw Normal View History

2025-08-11 22:42:29 +08:00
<template>
<view class="start-dm-content">
<!-- 班级选择器 -->
<view class="section">
<view class="section-title">选择班级</view>
<NjBjPicker @change="changeNjBj" icon-arrow="right" :customStyle="{ backgroundColor: '#fff', borderRadius: '0', padding: '12px 15px' }" />
<!-- 班级选择提示 -->
<view v-if="!curBj" class="class-tip">
<text class="tip-icon"></text>
<text class="tip-text">请先选择班级</text>
</view>
</view>
<!-- 陪餐教师选择 -->
<view class="section" v-if="curBj">
<view class="section-title">陪餐教师</view>
<JsPicker
:multiple="true"
@change="jsXz"
placeholder="请选择陪餐教师"
/>
<!-- <view v-if="xzJs.length > 0" class="selected-teachers">
<view
v-for="teacher in xzJs"
:key="teacher.value"
class="teacher-tag"
>
{{ teacher.label }}
<text class="remove-btn" @click="scJs(teacher.value)">×</text>
</view>
</view> -->
</view>
<!-- 教师陪餐状态列表 -->
<view class="section" v-if="xzJs.length > 0">
<view class="section-title">
教师陪餐状态
<text class="refresh-btn" @click="sxJsLb">刷新</text>
</view>
<view class="teacher-list">
<view class="teacher-grid">
<view
v-for="teacher in xzJs"
:key="teacher.value"
class="teacher-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="teacher-avatar"
:src="imagUrl(teacher.headPic) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="teacher-name mb-8">
<text class="font-14 cor-333">{{ teacher.label }}</text>
</view>
<view class="flex-row">
<!-- 教师可以切换陪餐状态 -->
<view
class="status-tag clickable"
:class="getTeacherStatusClass(teacher.pcZt)"
@click="dkJsZtXz(teacher)"
>
{{ hqJsZtWz(teacher.pcZt) }}
<text class="status-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 学生状态列表 -->
<view class="section" v-if="curBj">
<view class="section-title">
学生状态列表
<text class="refresh-btn" @click="sxXsLb">刷新</text>
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{ xsLb.length }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<text class="stat-number unpaid">{{ hqZtSl('D') }}</text>
<text class="stat-label">未缴费</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ hqZtSl('E') }}</text>
<text class="stat-label">未报名</text>
</view>
<view class="stat-item">
<text class="stat-number normal">{{ hqZtSl('A') }}</text>
<text class="stat-label">正常</text>
</view>
<view class="stat-item">
<text class="stat-number leave">{{ hqZtSl('B') }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-item">
<text class="stat-number absent">{{ hqZtSl('C') }}</text>
<text class="stat-label">缺勤</text>
</view>
</view>
<!-- 学生列表 - 改为card形式 -->
<view class="student-list">
<!-- 已缴费学生列表 -->
<view v-if="yjfXs.length > 0" class="student-section">
<view class="section-subtitle">已缴费学生 ({{ yjfXs.length }})</view>
<view class="student-grid">
<view
v-for="student in yjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xm }}</text>
</view>
<view class="flex-row">
<!-- 已缴费学生可以切换状态 -->
<view
class="status-tag clickable"
:class="getStatusClass(student.jcZt)"
@click="dkZtXz(student, 'paid')"
>
{{ hqZtWz(student.jcZt) }}
<text class="status-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 未缴费/未报名学生列表 -->
<view v-if="wjfXs.length > 0" class="student-section">
<view class="section-subtitle">未缴费/未报名学生 ({{ wjfXs.length }})</view>
<view class="student-grid">
<view
v-for="student in wjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-12 cor-333">{{ student.xm }}</text>
</view>
<view class="flex-row">
<!-- 未缴费/未报名学生只显示状态不能切换 -->
<view
class="status-tag readonly"
:class="getStatusClass(student.jcZt)"
>
{{ hqZtWz(student.jcZt) }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="xsLb.length === 0" class="empty-tip">
暂无学生数据
</view>
</view>
</view>
<!-- 状态选择弹窗 -->
<u-picker
:defaultIndex="mqXz"
:show="ztXzK"
:columns="[dqZtXz]"
@confirm="qrXzZt"
@cancel="ztXzK = false"
></u-picker>
<!-- 教师状态选择弹窗 -->
<u-picker
:defaultIndex="jsMqXz"
:show="jsZtXzK"
:columns="[jsZtXz]"
@confirm="qrJsZtXz"
@cancel="jsZtXzK = false"
></u-picker>
<!-- 加载提示 -->
<view v-if="jzZt" class="loading-overlay">
<view class="loading-content">
<text>加载中...</text>
</view>
</view>
</view>
<!-- 提交按钮 - 固定在底部 -->
<view class="fixed-bottom" v-if="curBj">
<button
class="submit-btn"
:disabled="!kTj"
@click="tjDm"
>
提交点名
</button>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import NjBjPicker from '@/pages/components/NjBjPicker/index.vue'
import JsPicker from '@/pages/components/JsPicker/index.vue'
import { getClassStudentDmDataApi, submitJcDmDataApi } from '@/api/base/jcApi'
import { imagUrl } from "@/utils";
import { useUserStore } from '@/store/modules/user'
const { getJs } = useUserStore()
/**
* 就餐点名组件
* 功能
* 1. 选择班级获取学生列表
* 2. 将学生分为两类已缴费可切换正常/请假/缺勤和未缴费/未报名状态为未缴费/未报名
* 3. 选择陪餐教师
* 4. 批量提交点名记录包含jsId和dmPcId字段
*/
// 接收外部传入属性
const props = withDefaults(defineProps<{
title?: string
}>(), {
title: '点名'
});
// 响应式数据
const curNj = ref<any>(null);
const curBj = ref<any>(null);
const xsLb = ref<any[]>([]) // 学生列表
const xzJs = ref<any[]>([]) // 选择的教师
const jzZt = ref(false) // 加载状态
// 状态选择相关
const ztXzK = ref(false) // 状态选择可见
const yjfZtXz = ref<Array<{ text: string, value: string }>>([
{ text: '正常', value: 'A' },
{ text: '请假', value: 'B' },
{ text: '缺勤', value: 'C' }
])
const wjfZtXz = ref<Array<{ text: string, value: string }>>([
{ text: '未缴费', value: 'D' },
{ text: '未报名', value: 'E' }
])
const dqZtXz = ref<Array<{ text: string, value: string }>>([])
const dqXs = ref<any>(null) // 当前学生
const mqXz = ref<any>([0]) // 默认选择
// 教师陪餐状态选择相关
const jsZtXz = ref<Array<{ text: string, value: string }>>([
{ text: '正常陪餐', value: 'A' },
{ text: '请假', value: 'B' },
{ text: '缺勤', value: 'C' }
])
const dqJs = ref<any>(null) // 当前教师
const jsZtXzK = ref(false) // 教师状态选择可见
const jsMqXz = ref<any>([0]) // 教师状态默认选择
// 计算属性
const kTj = computed(() => {
return curBj.value && yjfXs.value.length > 0 // 改为检查已缴费学生数量
})
// 已缴费学生列表有就餐清单且jfZt为B
const yjfXs = computed(() => {
return xsLb.value.filter(student => student.studentType === 'paid')
})
// 未缴费/未报名学生列表
const wjfXs = computed(() => {
return xsLb.value.filter(student => student.studentType === 'unpaid')
})
// 方法
// 改变了年级班级
const changeNjBj = async (nj: any, bj: any) => {
curNj.value = nj
curBj.value = bj
await jzXsLb()
};
const jsXz = (teachers: any[]) => {
xzJs.value = teachers.map(teacher => ({
...teacher,
pcZt: 'A' // 默认正常陪餐状态
}))
}
const scJs = (teacherId: string) => {
xzJs.value = xzJs.value.filter(t => t.id !== teacherId)
}
// 获取教师状态文字
const hqJsZtWz = (status: string) => {
switch (status) {
case 'A':
return '正常陪餐'
case 'B':
return '请假'
case 'C':
return '缺勤'
default:
return '正常陪餐'
}
}
// 获取教师状态对应的样式类
const getTeacherStatusClass = (status: string) => {
switch (status) {
case 'A':
return 'status-normal'
case 'B':
return 'status-leave'
case 'C':
return 'status-absent'
default:
return 'status-normal'
}
}
// 打开教师状态选择器
const dkJsZtXz = (teacher: any) => {
dqJs.value = teacher
// 找到当前状态在选项中的索引
const currentIndex = jsZtXz.value.findIndex(option => option.value === teacher.pcZt)
jsMqXz.value = [currentIndex >= 0 ? currentIndex : 0]
jsZtXzK.value = true
}
// 确认选择教师状态
const qrJsZtXz = (e: any) => {
if (dqJs.value && e.value && e.value[0]) {
const selectedStatus = jsZtXz.value.find(
(option: any) => option.value === e.value[0].value
)
if (selectedStatus) {
// 更新当前教师状态
dqJs.value.pcZt = selectedStatus.value
}
}
jsZtXzK.value = false
}
// 刷新教师列表
const sxJsLb = () => {
// 这里可以添加刷新教师状态的逻辑
console.log('刷新教师列表')
}
const jzXsLb = async () => {
if (!curBj.value) return
jzZt.value = true
try {
// 使用新的API接口获取班级学生点名数据
const response = await getClassStudentDmDataApi(
curBj.value.key,
curNj.value.key
)
if (response.result) {
// 合并已缴费和未缴费学生列表
const paidStudents = response.result.paidStudents || []
const unpaidStudents = response.result.unpaidStudents || []
// 为每个学生设置默认就餐状态
const allStudents = [
...paidStudents.map((student: any) => ({
...student,
jcZt: 'A', // 已缴费学生默认正常状态
studentType: 'paid'
})),
...unpaidStudents.map((student: any) => ({
...student,
jcZt: student.hasJcQd ? 'D' : 'E', // 未缴费或未报名
studentType: 'unpaid'
}))
]
xsLb.value = allStudents
} else {
xsLb.value = []
}
} catch (error) {
console.error('加载学生列表失败:', error)
uni.showToast({
title: '加载学生列表失败',
icon: 'none'
})
} finally {
jzZt.value = false
}
}
const sxXsLb = () => {
jzXsLb()
}
const hqXsZtLx = (status: string) => {
switch (status) {
case 'A':
return 'zt-zc'
case 'B':
return 'zt-qj'
case 'C':
return 'zt-qq'
case 'D':
return 'zt-wjf'
case 'E':
return 'zt-wbm'
default:
return 'zt-zc'
}
}
const hqZtWz = (status: string) => {
switch (status) {
case 'A':
return '正常'
case 'B':
return '请假'
case 'C':
return '缺勤'
case 'D':
return '未缴费'
case 'E':
return '未报名'
default:
return '正常'
}
}
const hqZtSl = (status: string) => {
return xsLb.value.filter(s => s.jcZt === status).length
}
// 获取学生类型文本
const hqXsLxWz = (student: any) => {
if (student.studentType === 'paid') {
return '已缴费'
} else if (student.jcZt === 'E') {
return '未报名'
} else {
return '未缴费'
}
}
// 获取状态对应的样式类
const getStatusClass = (status: string) => {
switch (status) {
case 'A':
return 'status-normal'
case 'B':
return 'status-leave'
case 'C':
return 'status-absent'
case 'D':
return 'status-unpaid'
case 'E':
return 'status-unregistered'
default:
return 'status-normal'
}
}
// 获取学生类型对应的样式类
const getTypeClass = (studentType: string) => {
switch (studentType) {
case 'paid':
return 'cor-success'
case 'unpaid':
return 'cor-warning'
default:
return 'cor-666'
}
}
// 打开状态选择器
const dkZtXz = (student: any, type: 'paid' | 'unpaid') => {
dqXs.value = student
// 根据学生类型设置对应的状态选项
if (type === 'paid') {
dqZtXz.value = yjfZtXz.value
} else {
dqZtXz.value = wjfZtXz.value
}
// 找到当前状态在选项中的索引
const currentIndex = dqZtXz.value.findIndex(option => option.value === student.jcZt)
mqXz.value = [currentIndex >= 0 ? currentIndex : 0]
ztXzK.value = true
}
// 确认选择状态
const qrXzZt = (e: any) => {
if (dqXs.value && e.value && e.value[0]) {
const selectedStatus = dqZtXz.value.find(
(option: any) => option.value === e.value[0].value
)
if (selectedStatus) {
// 更新当前学生状态
dqXs.value.jcZt = selectedStatus.value
}
}
ztXzK.value = false
}
const tjDm = async () => {
if (!curBj.value || yjfXs.value.length === 0) { // 改为检查已缴费学生数量
uni.showToast({
title: '请先选择班级或有已缴费学生',
icon: 'none'
})
return
}
jzZt.value = true
try {
// 准备点名数据
const dmData: any = {
bjId: curBj.value.key,
njId: curNj.value.key,
bjmc: curBj.value.title,
njmc: curNj.value.title,
dmJsId: getJs.id || '', // 点名教师ID
dmTime: new Date(),
xsList: yjfXs.value.map(student => ({
xsId: student.id,
xsXm: student.xm, // 传入学生姓名
jcZt: student.jcZt || 'A',
jcQdId: student.jcQdInfo?.qdId,
jcBzId: student.jcQdInfo?.bzId,
jzId: student.jcQdInfo?.jzId,
tx: student.xstx // 传入学生头像
})),
ptJsList: xzJs.value.map(teacher => ({
jsId: teacher.value,
jsXm: teacher.label,
pcZt: teacher.pcZt || 'A', // 添加陪餐状态
tx: teacher.headPic // 传入教师头像
}))
}
// 提交点名数据
const response = await submitJcDmDataApi(dmData)
if (response.result) {
uni.showToast({
title: '提交成功',
icon: 'success'
})
// 重置表单
curNj.value = null
curBj.value = null
xsLb.value = []
xzJs.value = []
// 返回上一页
uni.navigateBack()
} else {
throw new Error(response.message || '提交失败')
}
} catch (error) {
console.error('提交失败:', error)
uni.showToast({
title: error instanceof Error ? error.message : '提交失败',
icon: 'none'
})
} finally {
jzZt.value = false
}
}
</script>
<style lang="scss" scoped>
.start-dm-content {
padding: 20rpx;
padding-bottom: 120rpx; /* 为固定底部按钮留出空间 */
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-subtitle {
font-size: 28rpx;
font-weight: bold;
color: #666;
margin: 20rpx 0 15rpx 0;
padding: 10rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.refresh-btn {
font-size: 24rpx;
color: #007aff;
font-weight: normal;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
flex-wrap: wrap;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.normal {
color: #52c41a;
}
&.leave {
color: #faad14;
}
&.absent {
color: #ff4d4f;
}
&.unpaid {
color: #722ed1;
}
&.unregistered {
color: #eb2f96;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.student-section {
margin-bottom: 30rpx;
}
.student-list {
margin-bottom: 30rpx;
}
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.student-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 92rpx;
height: 92rpx;
border-radius: 50%;
padding: 6rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.student-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
&.clickable {
cursor: pointer;
transition: all 0.2s;
&:hover {
opacity: 0.8;
transform: scale(1.05);
}
}
&.readonly {
opacity: 0.8;
cursor: not-allowed;
}
.status-arrow {
font-size: 16rpx;
opacity: 0.8;
}
}
.status-normal {
background-color: #e6f7ff;
color: #52c41a;
}
.status-leave {
background-color: #fff7e6;
color: #faad14;
}
.status-absent {
background-color: #fff2f0;
color: #ff4d4f;
}
.status-unpaid {
background-color: #f9f0ff;
color: #722ed1;
}
.status-unregistered {
background-color: #fff0f6;
color: #eb2f96;
}
.student-type {
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: inline-block;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120rpx;
}
.cor-success {
color: #52c41a;
}
.cor-warning {
color: #faad14;
}
.cor-666 {
color: #666;
}
.cor-333 {
color: #333;
}
.flex-row {
display: flex;
flex-direction: row;
}
.justify-end {
justify-content: flex-end;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 16rpx;
}
.mb-3 {
margin-bottom: 6rpx;
}
.mt-8 {
margin-top: 16rpx;
}
.font-14 {
font-size: 28rpx;
}
.font-12 {
font-size: 24rpx;
}
.font-bold {
font-weight: bold;
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
.selected-teachers {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 20rpx;
}
.teacher-tag {
display: flex;
align-items: center;
background-color: #e6f7ff;
color: #007aff;
padding: 12rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.remove-btn {
margin-left: 12rpx;
font-size: 28rpx;
color: #ff4d4f;
cursor: pointer;
}
.submit-btn {
width: 100%;
height: 80rpx;
background-color: #007aff;
color: #fff;
border: none;
border-radius: 40rpx;
font-size: 32rpx;
font-weight: bold;
&:disabled {
background-color: #d9d9d9;
color: #999;
}
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-content {
background-color: #fff;
padding: 40rpx;
border-radius: 16rpx;
color: #333;
font-size: 28rpx;
}
.class-tip {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 15rpx 20rpx;
background-color: #fffbe6;
border: 1rpx solid #ffe58f;
border-radius: 12rpx;
color: #faad14;
font-size: 28rpx;
font-weight: bold;
.tip-icon {
margin-right: 10rpx;
font-size: 32rpx;
}
}
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx;
background-color: #fff;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.1);
z-index: 10;
}
.teacher-list {
margin-bottom: 30rpx;
}
.teacher-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.teacher-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.teacher-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.teacher-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.teacher-name {
margin-bottom: 16rpx;
}
</style>