1、调整就餐点名

2、增加中文姓名排序依赖库
This commit is contained in:
ywyonui 2025-09-11 18:59:56 +08:00
parent 8a6cfd58c9
commit dee7d67bcc
12 changed files with 450 additions and 259 deletions

View File

@ -58,6 +58,7 @@
"lodash": "4.17.21",
"pinia": "2.0.23",
"pinia-plugin-persist-uni": "1.2.0",
"pinyin-pro": "^3.27.0",
"qrcode": "^1.5.3",
"uview-plus": "3.1.20",
"vconsole": "3.15.1",

8
pnpm-lock.yaml generated
View File

@ -74,6 +74,9 @@ importers:
pinia-plugin-persist-uni:
specifier: 1.2.0
version: 1.2.0(pinia@2.0.23(typescript@4.8.3)(vue@3.2.45))(vue@3.2.45)
pinyin-pro:
specifier: ^3.27.0
version: 3.27.0
qrcode:
specifier: ^1.5.3
version: 1.5.4
@ -2965,6 +2968,9 @@ packages:
typescript:
optional: true
pinyin-pro@3.27.0:
resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@ -7626,6 +7632,8 @@ snapshots:
optionalDependencies:
typescript: 4.8.3
pinyin-pro@3.27.0: {}
pirates@4.0.7: {}
pixelmatch@4.0.2:

View File

@ -1,5 +1,12 @@
import { get, post } from '@/utils/request'
/**
*
*/
export const jcBzFindPageApi = async (params: any) => {
return await get('/api/jcBz/findPage', params)
}
/**
*
*/
@ -8,16 +15,16 @@ export const getJcBzListApi = async (params: any) => {
}
/**
*
*
*/
export const getJcQdListApi = async (params: any) => {
export const jcQdFindPageApi = async (params: any) => {
return await get('/api/jcQd/findPage', params)
}
/**
*
*/
export const getJcDmPageApi = async (params: any) => {
export const jcDmFindPageApi = async (params: any) => {
return await get('/api/jcDm/findPage', params)
}
@ -60,20 +67,6 @@ export const getJcPtListApi = async (params: any) => {
return await get('/api/jcPt/findPage', params)
}
/**
* ID获取学生点名数据
* /
*/
export const getClassStudentDmDataApi = async (bjId: string, njId: string) => {
try {
const response = await get('/mobile/js/jc/getClassStudentDmData', { bjId, njId })
return response
} catch (error) {
console.error('获取班级学生点名数据失败:', error)
throw error
}
}
/**
*
* ID

15
src/api/base/xsApi.ts Normal file
View File

@ -0,0 +1,15 @@
import { get, post } from "@/utils/request";
/**
*
*/
export function findPageByNoXk(params: any) {
return get('/api/xs/findPageByNoXk', params);
}
/**
*
*/
export function findPageByNoJc(params: any) {
return get('/api/xs/findPageByNoJc', params);
}

View File

@ -614,6 +614,12 @@
"backgroundColor": "#f4f5f7"
}
},
{
"path": "pages/view/routine/jc/bzList",
"style": {
"navigationBarTitleText": "就餐标准列表"
}
},
{
"path": "pages/view/routine/jc/index",
"style": {

View File

@ -303,7 +303,7 @@ const sections = reactive<Section[]>([
text: "就餐点名",
show: true,
permissionKey: "routine-jcdm", //
path: "/pages/view/routine/jc/index",
path: "/pages/view/routine/jc/bzList",
},
{
id: "r9",

View File

@ -0,0 +1,152 @@
<template>
<view class="bz-list">
<!-- 使用 BasicListLayout 包裹记录列表 -->
<BasicListLayout @register="register" :fixed="false" class="flex-1">
<template #default="{ data }">
<view class="bz-card" @click="goToBz(data)">
<view class="card-header">
<view class="card-title">{{ data.bzMc }}</view>
</view>
<view class="divider"></view>
<view class="card-content">
<view class="info-row">
<text class="info-label">年级</text>
<text class="info-value">{{ data.njmc }}</text>
</view>
<view class="info-row">
<text class="info-label">说明</text>
<text class="info-value">{{ data.bzSm }}</text>
</view>
</view>
<view class="card-footer">
<text class="view-detail">前往点名 </text>
</view>
</view>
</template>
</BasicListLayout>
</view>
</template>
<script setup lang="ts">
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import { jcBzFindPageApi } from "@/api/base/jcApi";
import { useDataStore } from "@/store/modules/data";
const { setJcBz } = useDataStore();
//
const loading = ref(false);
const bzList = ref<any>([]);
const total = ref<any>(0);
// 使 BasicListLayout
const [register, { reload, setParam }] = useLayout({
api: async (params: any) => {
try {
const res = await jcBzFindPageApi(params);
console.log("API返回数据:", res); //
//
if (res && res.rows) {
bzList.value = res.rows;
total.value = res.records || res.total || 0;
} else {
bzList.value = [];
total.value = 0;
}
return res;
} catch (error) {
console.error("获取数据失败:", error);
bzList.value = [];
total.value = 0;
return { rows: [], total: 0 };
}
},
componentProps: {
auto: false,
},
});
const goToBz = (bz: any) => {
setJcBz(bz);
uni.navigateTo({ url: "/pages/view/routine/jc/index"});
};
//
onMounted(() => {
//
reload();
});
</script>
<style lang="scss" scoped>
.bz-list {
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.bz-card {
border: 1px solid #f0f0f0;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
.card-header {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
.divider {
height: 1px;
background-color: #eee;
margin-bottom: 15px;
}
.card-content {
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1px solid #f0f0f0;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.info-label {
font-size: 28rpx;
color: #666;
font-weight: normal;
flex: 0 0 160rpx;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: bold;
flex: 1;
}
.card-footer {
text-align: right;
}
}
</style>

View File

@ -80,22 +80,22 @@
<view class="section" v-if="curBj">
<view class="section-title">
学生状态列表
<text class="refresh-btn" @click="sxXsLb">刷新</text>
<text class="refresh-btn" @click="loadXsList">刷新</text>
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{ xsLb.length }}</text>
<text class="stat-number">{{ rsData.zrs }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<text class="stat-number unpaid">{{ hqZtSl('D') }}</text>
<text class="stat-label">未缴费</text>
<text class="stat-number unregistered">{{ rsData.bmRs }}</text>
<text class="stat-label">报名就餐</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ hqZtSl('E') }}</text>
<text class="stat-label">未报名</text>
<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>
@ -112,36 +112,36 @@
</view>
<!-- 学生列表 - 改为card形式 -->
<view class="student-list">
<view class="xs-list">
<!-- 已缴费学生列表 -->
<view v-if="yjfXs.length > 0" class="student-section">
<view class="section-subtitle">缴费学生 ({{ yjfXs.length }})</view>
<view class="student-grid">
<view v-if="bmXsList.length > 0" class="xs-section">
<view class="section-subtitle">报名学生 ({{ bmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="student in yjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
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="student-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'"
class="xs-avatar"
:src="imagUrl(xs.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 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(student.jcZt)"
@click="dkZtXz(student, 'paid')"
:class="getStatusClass(xs.jcZt)"
@click="dkZtXz(xs, 'paid')"
>
{{ hqZtWz(student.jcZt) }}
{{ hqZtWz(xs.jcZt) }}
<text class="status-arrow"></text>
</view>
</view>
@ -152,33 +152,33 @@
</view>
<!-- 未缴费/未报名学生列表 -->
<view v-if="wjfXs.length > 0" class="student-section">
<view class="section-subtitle">缴费/未报名学生 ({{ wjfXs.length }})</view>
<view class="student-grid">
<view v-if="unBmXsList.length > 0" class="xs-section">
<view class="section-subtitle">报名学生 ({{ unBmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="student in wjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
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="student-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'"
class="xs-avatar"
:src="imagUrl(xs.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 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(student.jcZt)"
:class="getStatusClass(xs.jcZt)"
>
{{ hqZtWz(student.jcZt) }}
{{ hqZtWz(xs.jcZt) }}
</view>
</view>
</view>
@ -187,7 +187,7 @@
</view>
</view>
<view v-if="xsLb.length === 0" class="empty-tip">
<view v-if="bmXsList.length === 0 && unBmXsList.length === 0" class="empty-tip">
暂无学生数据
</view>
</view>
@ -254,11 +254,14 @@ import { ref, computed } from 'vue'
import NjBjPicker from '@/pages/components/NjBjPicker/index.vue'
import BasicJsPicker from '@/components/BasicJsPicker/Picker.vue'
import DmPsComponent from '@/pages/components/dmPs/index.vue'
import { getClassStudentDmDataApi, submitJcDmDataApi } from '@/api/base/jcApi'
import { findPageByNoJc } from "@/api/base/xsApi";
import { jcQdFindPageApi, submitJcDmDataApi } from '@/api/base/jcApi'
import { imagUrl } from "@/utils";
import { sortChinese } from "@/utils/pinyinUtil"
import { useUserStore } from '@/store/modules/user'
import { useDataStore } from '@/store/modules/data'
const { getJs } = useUserStore()
const { getJcBz } = useDataStore();
/**
* 就餐点名组件
* 功能
@ -279,10 +282,16 @@ const props = withDefaults(defineProps<{
const curNj = ref<any>(null);
const curBj = ref<any>(null);
const xsLb = ref<any[]>([]) //
const xzJs = ref<any[]>([]) //
const jzZt = ref(false) //
//
const rsData = ref({
zrs: 0,
bmRs: 0,
unBmRs: 0
})
//
const ztXzK = ref(false) //
const yjfZtXz = ref<Array<{ text: string, value: string }>>([
@ -317,25 +326,20 @@ const dmPsRef = ref<any>(null);
//
const kTj = computed(() => {
return curBj.value && yjfXs.value.length > 0 //
return curBj.value && bmXsList.value.length > 0 //
})
// jfZtB
const yjfXs = computed(() => {
return xsLb.value.filter(student => student.studentType === 'paid')
})
// /
const wjfXs = computed(() => {
return xsLb.value.filter(student => student.studentType === 'unpaid')
})
//
const bmXsList = ref<any>([]);
//
const unBmXsList = ref<any>([]);
//
//
const changeNjBj = async (nj: any, bj: any) => {
curNj.value = nj
curBj.value = bj
await jzXsLb()
await loadXsList()
};
//
@ -435,40 +439,39 @@ const sxJsLb = () => {
console.log('刷新教师列表')
}
const jzXsLb = async () => {
//
const loadXsList = 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 = []
const params = {
bzId: getJcBz.id,
bjId: curBj.value.key,
njId: curNj.value.key,
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');
console.log('学生清单列表', bmXsList.value);
const resUn = await findPageByNoJc(params);
unBmXsList.value = resUn.rows || [];
unBmXsList.value = sortChinese(unBmXsList.value, 'xm');
console.log('未报名学生列表', unBmXsList.value);
rsData.value = {
zrs: (resQd.records || 0) + (resUn.records || 0),
bmRs: (resQd.records || 0),
unBmRs: (resUn.records || 0),
};
} catch (error) {
console.error('加载学生列表失败:', error)
uni.showToast({
@ -480,10 +483,6 @@ const jzXsLb = async () => {
}
}
const sxXsLb = () => {
jzXsLb()
}
const hqXsZtLx = (status: string) => {
switch (status) {
case 'A':
@ -519,18 +518,7 @@ const hqZtWz = (status: string) => {
}
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 '未缴费'
}
return bmXsList.value.filter((s:any) => s.jcZt === status).length
}
//
@ -551,21 +539,9 @@ const getStatusClass = (status: string) => {
}
}
//
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
const dkZtXz = (xs: any, type: 'paid' | 'unpaid') => {
dqXs.value = xs
//
if (type === 'paid') {
@ -575,7 +551,7 @@ const dkZtXz = (student: any, type: 'paid' | 'unpaid') => {
}
//
const currentIndex = dqZtXz.value.findIndex(option => option.value === student.jcZt)
const currentIndex = dqZtXz.value.findIndex(option => option.value === xs.jcZt)
mqXz.value = [currentIndex >= 0 ? currentIndex : 0]
ztXzK.value = true
}
@ -596,7 +572,7 @@ const qrXzZt = (e: any) => {
}
const tjDm = async () => {
if (!curBj.value || yjfXs.value.length === 0) { //
if (!curBj.value || bmXsList.value.length === 0) { //
uni.showToast({
title: '请先选择班级或有已缴费学生',
icon: 'none'
@ -635,24 +611,28 @@ const tjDm = async () => {
bjmc: curBj.value.title,
njmc: curNj.value.title,
dmJsId: getJs.id || '', // ID
dmTime: new Date(),
jcTime: new Date(),
zrs: rsData.value.zrs,
sdRs: hqZtSl('A'),
qjRs: hqZtSl('B'),
qqRs: hqZtSl('C'),
//
zp: photoUrls, //
sp: videoUrls, //
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 //
xsList: bmXsList.value.map((qd:any) => ({
xsId: qd.xsId,
xsXm: qd.xm, //
jcZt: qd.jcZt || 'A',
jcQdId: qd.id,
jcBzId: qd.bzId,
jzId: qd.jzId,
tx: qd.xstx //
})),
ptJsList: xzJs.value.map(teacher => ({
jsId: teacher.value,
jsXm: teacher.label,
pcZt: teacher.pcZt || 'A', //
tx: teacher.headPic //
ptJsList: xzJs.value.map(js => ({
jsId: js.value,
jsXm: js.label,
pcZt: js.pcZt || 'A', //
tx: js.headPic //
}))
}
@ -668,7 +648,8 @@ const tjDm = async () => {
//
curNj.value = null
curBj.value = null
xsLb.value = []
bmXsList.value = [];
unBmXsList.value = [];
xzJs.value = []
mediaData.value = {
photoList: [],
@ -782,28 +763,28 @@ const tjDm = async () => {
color: #666;
}
.student-section {
.xs-section {
margin-bottom: 30rpx;
}
.student-list {
.xs-list {
margin-bottom: 30rpx;
}
.student-grid {
.xs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
.xs-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.student-item:hover {
.xs-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
@ -820,7 +801,7 @@ const tjDm = async () => {
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.student-avatar {
.xs-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
@ -881,7 +862,7 @@ const tjDm = async () => {
color: #eb2f96;
}
.student-type {
.xs-type {
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: inline-block;
@ -1096,4 +1077,4 @@ const tjDm = async () => {
.teacher-name {
margin-bottom: 16rpx;
}
</style>
</style>

View File

@ -9,26 +9,18 @@
<view class="search-item">
<text class="label">班级</text>
<view class="flex-1">
<NjBjPicker
@change="changeNjBj"
icon-arrow="right"
:customStyle="{
borderRadius: '0',
padding: '0.5rem 0.625rem',
}"
/>
<NjBjPicker @change="changeNjBj" icon-arrow="right" :customStyle="{
borderRadius: '0',
padding: '0.5rem 0.625rem',
}" />
</view>
</view>
</view>
<view class="search-row">
<view class="search-item">
<text class="label">时间范围</text>
<uni-datetime-picker
type="daterange"
:value="[startTime, endTime]"
@change="onTimeRangeChange"
class="date-picker"
>
<uni-datetime-picker type="daterange" :value="[startTime, endTime]" @change="onTimeRangeChange"
class="date-picker">
<view class="picker-text">{{ getTimeRangeText() }}</view>
</uni-datetime-picker>
</view>
@ -45,10 +37,7 @@
</view>
<!-- 空状态 -->
<view
v-if="!loading && dmRecords && dmRecords.length === 0 && hasSearched"
class="empty-state"
>
<view v-if="!loading && dmRecords && dmRecords.length === 0 && hasSearched" class="empty-state">
<view class="empty-icon">📋</view>
<text class="empty-text">暂无点名记录</text>
<text class="empty-tip">请选择班级和时间范围进行搜索</text>
@ -81,7 +70,7 @@
<text class="info-value">{{ formatTime(data.jcTime) }}</text>
</view>
</view>
<view class="card-footer">
<text class="view-detail">查看详情 </text>
</view>
@ -95,10 +84,10 @@
import { ref, onMounted, computed } from "vue";
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
import NjBjPicker from "@/pages/components/NjBjPicker/index.vue";
import { getJcDmPageApi } from "@/api/base/jcApi";
import { jcDmFindPageApi } from "@/api/base/jcApi";
import { useDataStore } from "@/store/modules/data";
const { setData } = useDataStore();
const { setData, getJcBz } = useDataStore();
//
const props = withDefaults(
@ -123,7 +112,7 @@ const hasSearched = ref(false);
const [register, { reload, setParam }] = useLayout({
api: async (params: any) => {
try {
const res = await getJcDmPageApi(params);
const res = await jcDmFindPageApi(params);
console.log("API返回数据:", res); //
//
@ -208,6 +197,7 @@ const searchRecords = async () => {
//
setParam({
bzId: getJcBz.id,
njId: selectedClass.value.njId,
bjId: selectedClass.value.bjId,
startTime: startTime.value + " 00:00:00",
@ -233,22 +223,6 @@ const goToDetail = (dm: any) => {
});
};
const exportRecords = () => {
if (dmRecords.value.length === 0) {
uni.showToast({
title: "暂无数据可导出",
icon: "none",
});
return;
}
//
uni.showToast({
title: "导出功能开发中",
icon: "none",
});
};
const formatDate = (dateStr: string) => {
if (!dateStr) return "";
const date = new Date(dateStr);
@ -359,6 +333,7 @@ onMounted(() => {
.records-header {
padding: 0 30rpx;
margin-bottom: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
@ -386,10 +361,6 @@ onMounted(() => {
transition: all 0.3s ease;
}
.card-footer {
text-align: right;
padding-top: 16rpx;
@ -452,7 +423,7 @@ onMounted(() => {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}

View File

@ -68,68 +68,68 @@
</view>
<!-- 学生状态列表 -->
<view class="section" v-if="dmDetail.xsList && dmDetail.xsList.length > 0">
<view class="section">
<view class="section-title">
学生状态列表 ({{ dmDetail.xsList.length }})
学生状态列表 ({{ rsData.zrs }})
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{ dmDetail.xsList.length }}</text>
<text class="stat-number">{{ rsData.zrs }}</text>
<text class="stat-label">总人数</text>
</view>
<view class="stat-item">
<text class="stat-number normal">{{ getStatusCount('A') }}</text>
<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">{{ getStatusCount('B') }}</text>
<text class="stat-number leave">{{ hqZtSl('B') }}</text>
<text class="stat-label">请假</text>
</view>
<view class="stat-item">
<text class="stat-number absent">{{ getStatusCount('C') }}</text>
<text class="stat-number absent">{{ hqZtSl('C') }}</text>
<text class="stat-label">缺勤</text>
</view>
<view class="stat-item">
<text class="stat-number unpaid">{{ getStatusCount('D') }}</text>
<text class="stat-label">未缴费</text>
</view>
<view class="stat-item">
<text class="stat-number unregistered">{{ getStatusCount('E') }}</text>
<text class="stat-label">未报名</text>
</view>
</view>
<!-- 学生列表 - 分为两个部分 -->
<view class="student-list">
<view class="xs-list">
<!-- 已缴费学生列表 -->
<view v-if="yjfXs.length > 0" class="student-section">
<view class="section-subtitle">缴费学生 ({{ yjfXs.length }})</view>
<view class="student-grid">
<view v-if="dmXsList.length > 0" class="xs-section">
<view class="section-subtitle">已报名学生 ({{ dmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="student in yjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
v-for="xs in dmXsList"
: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="student-avatar"
:src="student.tx || '/static/images/default-avatar.png'"
class="xs-avatar"
:src="imagUrl(xs.tx) || '/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.xsXm }}</text>
<view class="xs-name mb-8">
<text class="font-14 cor-333">{{ xs.xsXm }}</text>
</view>
<view class="flex-row">
<view
class="status-tag readonly"
:class="getStatusClass(student.jcZt)"
:class="getStatusClass(xs.jcZt)"
>
{{ getStatusText(student.jcZt) }}
{{ getStatusText(xs.jcZt) }}
</view>
</view>
</view>
@ -139,32 +139,32 @@
</view>
<!-- 未缴费/未报名学生列表 -->
<view v-if="wjfXs.length > 0" class="student-section">
<view class="section-subtitle">缴费/未报名学生 ({{ wjfXs.length }})</view>
<view class="student-grid">
<view v-if="unBmXsList.length > 0" class="xs-section">
<view class="section-subtitle">报名学生 ({{ unBmXsList.length }})</view>
<view class="xs-grid">
<view
v-for="student in wjfXs"
:key="student.id"
class="student-item bg-white r-md p-12"
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="student-avatar"
:src="student.tx || '/static/images/default-avatar.png'"
class="xs-avatar"
:src="imagUrl(xs.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.xsXm }}</text>
<view class="xs-name mb-8">
<text class="font-14 cor-333">{{ xs.xsXm }}</text>
</view>
<view class="flex-row">
<view
class="status-tag readonly"
:class="getStatusClass(student.jcZt)"
:class="getStatusClass(xs.jcZt)"
>
{{ getStatusText(student.jcZt) }}
{{ getStatusText(xs.jcZt) }}
</view>
</view>
</view>
@ -173,7 +173,7 @@
</view>
</view>
<view v-if="dmDetail.xsList.length === 0" class="empty-tip">
<view v-if="dmXsList.length === 0" class="empty-tip">
暂无学生数据
</view>
</view>
@ -193,6 +193,8 @@
import { ref, onMounted, watch, computed } from 'vue'
import { getJcDmDetailApi } from '@/api/base/jcApi'
import { useDataStore } from '@/store/modules/data'
import { imagUrl } from "@/utils";
import { sortChinese } from "@/utils/pinyinUtil"
const { getData } = useDataStore()
//
@ -200,21 +202,16 @@ const loading = ref(false)
const dmDetail = ref<any>(null)
const error = ref('')
//
// ABC
const yjfXs = computed(() => {
if (!dmDetail.value?.xsList) return []
return dmDetail.value.xsList.filter((student: any) =>
['A', 'B', 'C'].includes(student.jcZt)
)
})
//
const dmXsList = ref<any>([]);
//
const unBmXsList = ref<any>([]);
// /DE
const wjfXs = computed(() => {
if (!dmDetail.value?.xsList) return []
return dmDetail.value.xsList.filter((student: any) =>
['D', 'E'].includes(student.jcZt)
)
//
const rsData = ref({
zrs: 0,
bmRs: 0,
unBmRs: 0
})
//
@ -228,7 +225,18 @@ const loadDetail = async () => {
const response = await getJcDmDetailApi(getData.id)
if (response.result) {
dmDetail.value = response.result
dmDetail.value = response.result;
dmXsList.value = response.result.dmXsList || [];
dmXsList.value = sortChinese(dmXsList.value, 'xsXm');
unBmXsList.value = response.result.unBmXsList || [];
unBmXsList.value = sortChinese(unBmXsList.value, 'xsxm');
rsData.value = {
zrs: unBmXsList.value.length + dmXsList.value.length,
bmRs: dmXsList.value.length,
unBmRs: unBmXsList.value.length,
}
} else {
error.value = response.message || '获取详情失败'
}
@ -240,6 +248,10 @@ const loadDetail = async () => {
}
}
const hqZtSl = (status: string) => {
return dmXsList.value.filter((s:any) => s.jcZt === status).length
}
const formatDateTime = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
@ -252,11 +264,6 @@ const formatDateTime = (dateStr: string) => {
return `${year}-${month}-${day} ${hours}:${minutes}`
}
const getStatusCount = (status: string) => {
if (!dmDetail.value?.xsList) return 0
return dmDetail.value.xsList.filter((s: any) => s.jcZt === status).length
}
const getStatusText = (status: string) => {
switch (status) {
case 'A':
@ -383,7 +390,7 @@ onMounted(() => {
border-bottom: 1px solid #f0f0f0;
}
.student-section {
.xs-section {
margin-bottom: 30rpx;
}
@ -463,24 +470,24 @@ onMounted(() => {
color: #666;
}
.teacher-list, .student-list {
.teacher-list, .xs-list {
margin-bottom: 30rpx;
}
.teacher-grid, .student-grid {
.teacher-grid, .xs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.teacher-item, .student-item {
.teacher-item, .xs-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.teacher-item:hover, .student-item:hover {
.teacher-item:hover, .xs-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
@ -497,7 +504,7 @@ onMounted(() => {
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.teacher-avatar, .student-avatar {
.teacher-avatar, .xs-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;

View File

@ -9,6 +9,7 @@ export const useDataStore = defineStore({
global: {},
file: {},
xs: {}, // 学生专用
jcBz: {}, // 就餐标准
}),
getters: {
getData(): any {
@ -28,7 +29,10 @@ export const useDataStore = defineStore({
},
getXs(): any {
return this.xs;
}
},
getJcBz(): any {
return this.jcBz;
},
},
actions: {
setData(data: any) {
@ -48,6 +52,9 @@ export const useDataStore = defineStore({
},
setXs(data: any) {
this.xs = data;
},
setJcBz(data: any) {
this.jcBz = data;
}
},
persist: {

50
src/utils/pinyinUtil.ts Normal file
View File

@ -0,0 +1,50 @@
// utils/pinyinUtils.ts
import { pinyin } from 'pinyin-pro';
/**
*
* @param text
* @returns
*/
export function chineseToPinyin(text: string): string {
try {
return pinyin(text, { toneType: 'none', type: 'array', mode: 'surname' }).join('');
} catch (error) {
console.error('拼音转换失败:', error);
return text;
}
}
/**
*
* @param text
* @returns
*/
export function getFirstLetter(text: string): string {
try {
return pinyin(text, { pattern: 'first', toneType: 'none' });
} catch (error) {
console.error('首字母提取失败:', error);
return text;
}
}
/**
*
* @param list
* @param key
* @returns
*/
export function sortChinese(list: any, key?: string) {
return list.sort((a:any, b: any) => {
const textA = key ? a[key] : a as unknown as string;
const textB = key ? b[key] : b as unknown as string;
const pinyinA = chineseToPinyin(textA);
const pinyinB = chineseToPinyin(textB);
return pinyinA.localeCompare(pinyinB, 'zh-Hans-CN', {
sensitivity: 'accent'
});
});
}