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", "lodash": "4.17.21",
"pinia": "2.0.23", "pinia": "2.0.23",
"pinia-plugin-persist-uni": "1.2.0", "pinia-plugin-persist-uni": "1.2.0",
"pinyin-pro": "^3.27.0",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"uview-plus": "3.1.20", "uview-plus": "3.1.20",
"vconsole": "3.15.1", "vconsole": "3.15.1",

8
pnpm-lock.yaml generated
View File

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

View File

@ -1,5 +1,12 @@
import { get, post } from '@/utils/request' 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) 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) return await get('/api/jcDm/findPage', params)
} }
@ -60,20 +67,6 @@ export const getJcPtListApi = async (params: any) => {
return await get('/api/jcPt/findPage', params) 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 * 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" "backgroundColor": "#f4f5f7"
} }
}, },
{
"path": "pages/view/routine/jc/bzList",
"style": {
"navigationBarTitleText": "就餐标准列表"
}
},
{ {
"path": "pages/view/routine/jc/index", "path": "pages/view/routine/jc/index",
"style": { "style": {

View File

@ -303,7 +303,7 @@ const sections = reactive<Section[]>([
text: "就餐点名", text: "就餐点名",
show: true, show: true,
permissionKey: "routine-jcdm", // permissionKey: "routine-jcdm", //
path: "/pages/view/routine/jc/index", path: "/pages/view/routine/jc/bzList",
}, },
{ {
id: "r9", 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" v-if="curBj">
<view class="section-title"> <view class="section-title">
学生状态列表 学生状态列表
<text class="refresh-btn" @click="sxXsLb">刷新</text> <text class="refresh-btn" @click="loadXsList">刷新</text>
</view> </view>
<!-- 统计信息 --> <!-- 统计信息 -->
<view class="stats-container"> <view class="stats-container">
<view class="stat-item"> <view class="stat-item">
<text class="stat-number">{{ xsLb.length }}</text> <text class="stat-number">{{ rsData.zrs }}</text>
<text class="stat-label">总人数</text> <text class="stat-label">总人数</text>
</view> </view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-number unpaid">{{ hqZtSl('D') }}</text> <text class="stat-number unregistered">{{ rsData.bmRs }}</text>
<text class="stat-label">未缴费</text> <text class="stat-label">报名就餐</text>
</view> </view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-number unregistered">{{ hqZtSl('E') }}</text> <text class="stat-number unregistered">{{ rsData.unBmRs }}</text>
<text class="stat-label">未报名</text> <text class="stat-label">未报名就餐</text>
</view> </view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-number normal">{{ hqZtSl('A') }}</text> <text class="stat-number normal">{{ hqZtSl('A') }}</text>
@ -112,36 +112,36 @@
</view> </view>
<!-- 学生列表 - 改为card形式 --> <!-- 学生列表 - 改为card形式 -->
<view class="student-list"> <view class="xs-list">
<!-- 已缴费学生列表 --> <!-- 已缴费学生列表 -->
<view v-if="yjfXs.length > 0" class="student-section"> <view v-if="bmXsList.length > 0" class="xs-section">
<view class="section-subtitle">缴费学生 ({{ yjfXs.length }})</view> <view class="section-subtitle">报名学生 ({{ bmXsList.length }})</view>
<view class="student-grid"> <view class="xs-grid">
<view <view
v-for="student in yjfXs" v-for="xs in bmXsList"
:key="student.id" :key="xs.id"
class="student-item bg-white r-md p-12" class="xs-item bg-white r-md p-12"
> >
<view class="flex-row items-center"> <view class="flex-row items-center">
<view class="avatar-container mr-8"> <view class="avatar-container mr-8">
<image <image
class="student-avatar" class="xs-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'" :src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill" mode="aspectFill"
></image> ></image>
</view> </view>
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="student-name mb-8"> <view class="xs-name mb-8">
<text class="font-14 cor-333">{{ student.xm }}</text> <text class="font-14 cor-333">{{ xs.xm }}</text>
</view> </view>
<view class="flex-row"> <view class="flex-row">
<!-- 已缴费学生可以切换状态 --> <!-- 已缴费学生可以切换状态 -->
<view <view
class="status-tag clickable" class="status-tag clickable"
:class="getStatusClass(student.jcZt)" :class="getStatusClass(xs.jcZt)"
@click="dkZtXz(student, 'paid')" @click="dkZtXz(xs, 'paid')"
> >
{{ hqZtWz(student.jcZt) }} {{ hqZtWz(xs.jcZt) }}
<text class="status-arrow"></text> <text class="status-arrow"></text>
</view> </view>
</view> </view>
@ -152,33 +152,33 @@
</view> </view>
<!-- 未缴费/未报名学生列表 --> <!-- 未缴费/未报名学生列表 -->
<view v-if="wjfXs.length > 0" class="student-section"> <view v-if="unBmXsList.length > 0" class="xs-section">
<view class="section-subtitle">缴费/未报名学生 ({{ wjfXs.length }})</view> <view class="section-subtitle">报名学生 ({{ unBmXsList.length }})</view>
<view class="student-grid"> <view class="xs-grid">
<view <view
v-for="student in wjfXs" v-for="xs in unBmXsList"
:key="student.id" :key="xs.id"
class="student-item bg-white r-md p-12" class="xs-item bg-white r-md p-12"
> >
<view class="flex-row items-center"> <view class="flex-row items-center">
<view class="avatar-container mr-8"> <view class="avatar-container mr-8">
<image <image
class="student-avatar" class="xs-avatar"
:src="imagUrl(student.xstx) || '/static/images/default-avatar.png'" :src="imagUrl(xs.xstx) || '/static/images/default-avatar.png'"
mode="aspectFill" mode="aspectFill"
></image> ></image>
</view> </view>
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="student-name mb-8"> <view class="xs-name mb-8">
<text class="font-12 cor-333">{{ student.xm }}</text> <text class="font-12 cor-333">{{ xs.xm }}</text>
</view> </view>
<view class="flex-row"> <view class="flex-row">
<!-- 未缴费/未报名学生只显示状态不能切换 --> <!-- 未缴费/未报名学生只显示状态不能切换 -->
<view <view
class="status-tag readonly" class="status-tag readonly"
:class="getStatusClass(student.jcZt)" :class="getStatusClass(xs.jcZt)"
> >
{{ hqZtWz(student.jcZt) }} {{ hqZtWz(xs.jcZt) }}
</view> </view>
</view> </view>
</view> </view>
@ -187,7 +187,7 @@
</view> </view>
</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>
</view> </view>
@ -254,11 +254,14 @@ import { ref, computed } from 'vue'
import NjBjPicker from '@/pages/components/NjBjPicker/index.vue' import NjBjPicker from '@/pages/components/NjBjPicker/index.vue'
import BasicJsPicker from '@/components/BasicJsPicker/Picker.vue' import BasicJsPicker from '@/components/BasicJsPicker/Picker.vue'
import DmPsComponent from '@/pages/components/dmPs/index.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 { imagUrl } from "@/utils";
import { sortChinese } from "@/utils/pinyinUtil"
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { useDataStore } from '@/store/modules/data'
const { getJs } = useUserStore() const { getJs } = useUserStore()
const { getJcBz } = useDataStore();
/** /**
* 就餐点名组件 * 就餐点名组件
* 功能 * 功能
@ -279,10 +282,16 @@ const props = withDefaults(defineProps<{
const curNj = ref<any>(null); const curNj = ref<any>(null);
const curBj = ref<any>(null); const curBj = ref<any>(null);
const xsLb = ref<any[]>([]) //
const xzJs = ref<any[]>([]) // const xzJs = ref<any[]>([]) //
const jzZt = ref(false) // const jzZt = ref(false) //
//
const rsData = ref({
zrs: 0,
bmRs: 0,
unBmRs: 0
})
// //
const ztXzK = ref(false) // const ztXzK = ref(false) //
const yjfZtXz = ref<Array<{ text: string, value: string }>>([ const yjfZtXz = ref<Array<{ text: string, value: string }>>([
@ -317,25 +326,20 @@ const dmPsRef = ref<any>(null);
// //
const kTj = computed(() => { const kTj = computed(() => {
return curBj.value && yjfXs.value.length > 0 // return curBj.value && bmXsList.value.length > 0 //
}) })
// jfZtB //
const yjfXs = computed(() => { const bmXsList = ref<any>([]);
return xsLb.value.filter(student => student.studentType === 'paid') //
}) const unBmXsList = ref<any>([]);
// /
const wjfXs = computed(() => {
return xsLb.value.filter(student => student.studentType === 'unpaid')
})
// //
// //
const changeNjBj = async (nj: any, bj: any) => { const changeNjBj = async (nj: any, bj: any) => {
curNj.value = nj curNj.value = nj
curBj.value = bj curBj.value = bj
await jzXsLb() await loadXsList()
}; };
// //
@ -435,40 +439,39 @@ const sxJsLb = () => {
console.log('刷新教师列表') console.log('刷新教师列表')
} }
const jzXsLb = async () => { //
const loadXsList = async () => {
if (!curBj.value) return if (!curBj.value) return
jzZt.value = true jzZt.value = true
try { try {
// 使API const params = {
const response = await getClassStudentDmDataApi( bzId: getJcBz.id,
curBj.value.key, bjId: curBj.value.key,
curNj.value.key njId: curNj.value.key,
) pageNo: 1,
rows: 1000
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 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) { } catch (error) {
console.error('加载学生列表失败:', error) console.error('加载学生列表失败:', error)
uni.showToast({ uni.showToast({
@ -480,10 +483,6 @@ const jzXsLb = async () => {
} }
} }
const sxXsLb = () => {
jzXsLb()
}
const hqXsZtLx = (status: string) => { const hqXsZtLx = (status: string) => {
switch (status) { switch (status) {
case 'A': case 'A':
@ -519,18 +518,7 @@ const hqZtWz = (status: string) => {
} }
const hqZtSl = (status: string) => { const hqZtSl = (status: string) => {
return xsLb.value.filter(s => s.jcZt === status).length return bmXsList.value.filter((s:any) => s.jcZt === status).length
}
//
const hqXsLxWz = (student: any) => {
if (student.studentType === 'paid') {
return '已缴费'
} else if (student.jcZt === 'E') {
return '未报名'
} else {
return '未缴费'
}
} }
// //
@ -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') => { const dkZtXz = (xs: any, type: 'paid' | 'unpaid') => {
dqXs.value = student dqXs.value = xs
// //
if (type === 'paid') { 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] mqXz.value = [currentIndex >= 0 ? currentIndex : 0]
ztXzK.value = true ztXzK.value = true
} }
@ -596,7 +572,7 @@ const qrXzZt = (e: any) => {
} }
const tjDm = async () => { const tjDm = async () => {
if (!curBj.value || yjfXs.value.length === 0) { // if (!curBj.value || bmXsList.value.length === 0) { //
uni.showToast({ uni.showToast({
title: '请先选择班级或有已缴费学生', title: '请先选择班级或有已缴费学生',
icon: 'none' icon: 'none'
@ -635,24 +611,28 @@ const tjDm = async () => {
bjmc: curBj.value.title, bjmc: curBj.value.title,
njmc: curNj.value.title, njmc: curNj.value.title,
dmJsId: getJs.id || '', // ID 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, // zp: photoUrls, //
sp: videoUrls, // sp: videoUrls, //
xsList: yjfXs.value.map(student => ({ xsList: bmXsList.value.map((qd:any) => ({
xsId: student.id, xsId: qd.xsId,
xsXm: student.xm, // xsXm: qd.xm, //
jcZt: student.jcZt || 'A', jcZt: qd.jcZt || 'A',
jcQdId: student.jcQdInfo?.qdId, jcQdId: qd.id,
jcBzId: student.jcQdInfo?.bzId, jcBzId: qd.bzId,
jzId: student.jcQdInfo?.jzId, jzId: qd.jzId,
tx: student.xstx // tx: qd.xstx //
})), })),
ptJsList: xzJs.value.map(teacher => ({ ptJsList: xzJs.value.map(js => ({
jsId: teacher.value, jsId: js.value,
jsXm: teacher.label, jsXm: js.label,
pcZt: teacher.pcZt || 'A', // pcZt: js.pcZt || 'A', //
tx: teacher.headPic // tx: js.headPic //
})) }))
} }
@ -668,7 +648,8 @@ const tjDm = async () => {
// //
curNj.value = null curNj.value = null
curBj.value = null curBj.value = null
xsLb.value = [] bmXsList.value = [];
unBmXsList.value = [];
xzJs.value = [] xzJs.value = []
mediaData.value = { mediaData.value = {
photoList: [], photoList: [],
@ -782,28 +763,28 @@ const tjDm = async () => {
color: #666; color: #666;
} }
.student-section { .xs-section {
margin-bottom: 30rpx; margin-bottom: 30rpx;
} }
.student-list { .xs-list {
margin-bottom: 30rpx; margin-bottom: 30rpx;
} }
.student-grid { .xs-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 20rpx; gap: 20rpx;
} }
.student-item { .xs-item {
position: relative; position: relative;
border-radius: 16rpx; border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s; transition: all 0.2s;
} }
.student-item:hover { .xs-item:hover {
transform: translateY(-2rpx); transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15); 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); box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
} }
.student-avatar { .xs-avatar {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
border-radius: 50%; border-radius: 50%;
@ -881,7 +862,7 @@ const tjDm = async () => {
color: #eb2f96; color: #eb2f96;
} }
.student-type { .xs-type {
padding: 6rpx 16rpx; padding: 6rpx 16rpx;
border-radius: 8rpx; border-radius: 8rpx;
display: inline-block; display: inline-block;

View File

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

View File

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

View File

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