2025-09-12 20:07:35 +08:00

588 lines
12 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="section">
<view class="section-title">
学生状态列表
<text class="refresh-btn" @click="loadXsList">刷新</text>
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{ rsData.zrs }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ rsData.bmRs }}</text>
<text class="stat-label">报名就餐</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ rsData.unBmRs }}</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="xs-list">
<!-- 已缴费学生列表 -->
<view v-if="bmXsList.length > 0" class="xs-section">
<view class="section-subtitle">已报名学生 ({{ bmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="xs in bmXsList"
:key="xs.id"
class="xs-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="xs-avatar"
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="xs-name mb-8">
<text class="font-14 cor-333">{{ xs.xm }}</text>
</view>
<view class="flex-row">
<!-- 已缴费学生可以切换状态 -->
<view
class="status-tag clickable"
:class="getStatusClass(xs.jcZt)"
@click="dkZtXz(xs)"
>
{{ hqZtWz(xs.jcZt) }}
<text class="status-arrow"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 未缴费/未报名学生列表 -->
<view v-if="unBmXsList.length > 0" class="xs-section">
<view class="section-subtitle">未报名学生 ({{ unBmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="xs in unBmXsList"
:key="xs.id"
class="xs-item bg-white r-md p-12"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="xs-avatar"
:src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="xs-name mb-8">
<text class="font-12 cor-333">{{ xs.xm }}</text>
</view>
<view class="flex-row">
<!-- 未缴费/未报名学生只显示状态不能切换 -->
<view
class="status-tag readonly"
:class="getStatusClass(xs.jcZt)"
>
{{ hqZtWz(xs.jcZt) }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="bmXsList.length === 0 && unBmXsList.length === 0" class="empty-tip">
暂无学生数据
</view>
</view>
<!-- 状态选择弹窗 -->
<u-picker
:defaultIndex="mqXz"
:show="ztVisible"
:columns="[ztList]"
@confirm="onChangeZt"
@cancel="ztVisible = false"
></u-picker>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { findPageByNoJc } from "@/api/base/xsApi";
import { jcQdFindPageApi } from '@/api/base/jcApi'
import { imagUrl } from "@/utils";
import { sortChinese } from "@/utils/pinyinUtil"
// 接收外部传入属性
const props = withDefaults(defineProps<{
bzId?: string
njId?: string
bjId?: string
}>(), {
bzId: '',
njId: '',
bjId: '',
});
// 人数
const rsData = ref({
zrs: 0,
bmRs: 0,
unBmRs: 0
});
const bmXsList = ref<any>([]);
const unBmXsList = ref<any>([]);
const jzZt = ref(false) // 加载状态
// 状态选择相关
const ztVisible = ref(false) // 状态选择可见
const ztList = ref<Array<{ text: string, value: string }>>([
{ text: '正常', value: 'A' },
{ text: '请假', value: 'B' },
{ text: '缺勤', value: 'C' }
])
const dqXs = ref<any>(null) // 当前学生
const mqXz = ref<any>([0]) // 默认选择
// 加载学生列表
const loadXsList = async () => {
if (!props.bzId || !props.bjId) return
jzZt.value = true
try {
const params = {
bzId: props.bzId,
bjId: props.bjId,
njId: props.njId,
pageNo: 1,
rows: 1000
}
const resQd = await jcQdFindPageApi(params);
bmXsList.value = (resQd.rows || []).map((item: any) => {
return {
...item,
jcZt: 'A',
xm: (item.xsxm || item.xm || '').trim()
}
});
bmXsList.value = sortChinese(bmXsList.value, 'xm');
const resUn = await findPageByNoJc(params);
unBmXsList.value = (resUn.rows || []).map((item: any) => {
return {
...item,
jcZt: 'E',
xm: (item.xsxm || item.xm || '').trim()
}
});
unBmXsList.value = sortChinese(unBmXsList.value, 'xm');
rsData.value = {
zrs: (resQd.records || 0) + (resUn.records || 0),
bmRs: (resQd.records || 0),
unBmRs: (resUn.records || 0),
};
} catch (error) {
console.error('加载学生列表失败:', error)
uni.showToast({
title: '加载学生列表失败',
icon: 'none'
})
} finally {
jzZt.value = false
}
}
// 监听班级ID变化
watch(() => props.bjId, () => {
if (props.bzId && props.bjId) {
loadXsList()
}
}, { immediate: true })
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 bmXsList.value.filter((s:any) => s.jcZt === status).length
}
// 获取状态对应的样式类
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 dkZtXz = (xs: any) => {
dqXs.value = xs;
// 找到当前状态在选项中的索引
const currentIndex = ztList.value.findIndex(option => option.value === xs.jcZt)
mqXz.value = [currentIndex >= 0 ? currentIndex : 0]
ztVisible.value = true
}
// 确认选择状态
const onChangeZt = (e: any) => {
if (dqXs.value && e.value && e.value[0]) {
const selectedStatus = ztList.value.find(
(option: any) => option.value === e.value[0].value
)
if (selectedStatus) {
// 更新当前学生状态
dqXs.value.jcZt = selectedStatus.value
}
}
ztVisible.value = false
}
const getDmXsList = () => {
let retList: any = [];
for(let qd of bmXsList.value) {
retList.push({
xsId: qd.xsId,
xsXm: qd.xm, // 传入学生姓名
jcZt: qd.jcZt,
jcQdId: qd.id,
jcBzId: qd.bzId,
jzId: qd.jzId,
tx: qd.xstx // 传入学生头像
});
}
for(let xs of unBmXsList.value) {
retList.push({
xsId: xs.id,
xsXm: xs.xm, // 传入学生姓名
jcZt: xs.jcZt,
jcQdId: '',
jcBzId: props.bzId,
jzId: '',
tx: xs.xstx // 传入学生头像
});
}
return retList;
}
defineExpose({
loadXsList,
getDmXsList
})
</script>
<style lang="scss" scoped>
.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;
}
}
}
.xs-section {
margin-bottom: 30rpx;
}
.xs-list {
margin-bottom: 30rpx;
}
.xs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.xs-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
&: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);
.xs-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;
}
.xs-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-8 {
margin-bottom: 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;
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
</style>