完善就餐点名列表和详情页面

This commit is contained in:
ywyonui 2025-08-11 23:45:13 +08:00
parent d0859759c1
commit 1a066437dc
3 changed files with 301 additions and 273 deletions

View File

@ -1,259 +1,303 @@
<template> <template>
<view class="record-content"> <view class="record-content">
<!-- 搜索筛选区域 -->
<view class="search-section">
<view class="search-row">
<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' }" />
</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"
>
<view class="picker-text">{{ getTimeRangeText() }}</view>
</uni-datetime-picker>
</view>
</view>
<view class="search-row">
<button class="search-btn" @click="searchRecords">搜索</button>
<button class="reset-btn" @click="resetSearch">重置</button>
</view>
</view>
<!-- 使用 BasicListLayout 包裹记录列表 --> <!-- 使用 BasicListLayout 包裹记录列表 -->
<BasicListLayout @register="register" :fixed="false"> <BasicListLayout @register="register" :fixed="false" class="flex-1">
<template #top> <template #top>
<view class="records-header" v-if="dmRecords.length > 0"> <!-- 搜索筛选区域 -->
<view class="section-title"> <view class="search-section">
点名记录 ({{ totalCount }}) <view class="search-row">
<text class="export-btn" @click="exportRecords">导出</text> <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',
}"
/>
</view>
</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"
>
<view class="picker-text">{{ getTimeRangeText() }}</view>
</uni-datetime-picker>
</view>
</view>
<view class="search-row">
<button class="search-btn" @click="searchRecords">搜索</button>
<button class="reset-btn" @click="resetSearch">重置</button>
</view>
</view>
<!-- 记录头部信息 -->
<view class="records-header" v-if="dmRecords && dmRecords.length > 0">
<view class="section-title"> 点名记录 ({{ totalCount }}) </view>
</view>
<!-- 空状态 -->
<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>
</view>
<!-- 初始提示 -->
<view v-if="!hasSearched" class="initial-tip">
<view class="tip-icon">🔍</view>
<text class="tip-text">请选择班级和时间范围开始搜索</text>
</view> </view>
</template> </template>
<template #default="{ data }"> <template #default="{ data }">
<view <view class="record-card" @click="goToDetail(data)">
class="record-card"
@click="goToDetail(data)"
>
<view class="card-header">
<view class="time-info">
<text class="record-date">{{ formatDate(data.dmTime) }}</text>
<text class="record-time">{{ formatTime(data.dmTime) }}</text>
</view>
<view class="status-badge">
<text class="status-text">已点名</text>
</view>
</view>
<view class="card-content"> <view class="card-content">
<view class="class-info"> <view class="info-row">
<text class="class-name">{{ data.njmc }} {{ data.bjmc }}</text> <text class="info-label">年级</text>
<text class="info-value">{{ data.njmc }}</text>
</view> </view>
<view class="info-row">
<view class="stats-row"> <text class="info-label">班级</text>
<view class="stat-item"> <text class="info-value">{{ data.bjmc }}</text>
<text class="stat-label">学生</text> </view>
<text class="stat-value">{{ data.xsCount }}</text> <view class="info-row">
</view> <text class="info-label">就餐日期</text>
<view class="stat-item"> <text class="info-value">{{ formatDate(data.jcTime) }}</text>
<text class="stat-label">陪餐教师</text> </view>
<text class="stat-value">{{ data.jsCount }}</text> <view class="info-row">
</view> <text class="info-label">就餐时间</text>
<text class="info-value">{{ formatTime(data.jcTime) }}</text>
</view> </view>
</view> </view>
<view class="card-footer"> <view class="card-footer">
<text class="view-detail">点击查看详情 </text> <text class="view-detail">查看详情 </text>
</view> </view>
</view> </view>
</template> </template>
</BasicListLayout> </BasicListLayout>
<!-- 空状态 -->
<view v-if="!loading && dmRecords.length === 0 && hasSearched" class="empty-state">
<view class="empty-icon">📋</view>
<text class="empty-text">暂无点名记录</text>
<text class="empty-tip">请选择班级和时间范围进行搜索</text>
</view>
<!-- 初始提示 -->
<view v-if="!hasSearched" class="initial-tip">
<view class="tip-icon">🔍</view>
<text class="tip-text">请选择班级和时间范围开始搜索</text>
</view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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 { getJcDmPageApi } from "@/api/base/jcApi";
import { useDataStore } from '@/store/modules/data' import { useDataStore } from "@/store/modules/data";
const { setData } = useDataStore() const { setData } = useDataStore();
// //
const props = withDefaults(defineProps<{ const props = withDefaults(
title?: string defineProps<{
}>(), { title?: string;
title: '点名列表' }>(),
}); {
title: "点名列表",
}
);
// //
const loading = ref(false) const loading = ref(false);
const startTime = ref('') const startTime = ref("");
const endTime = ref('') const endTime = ref("");
const selectedClass = ref<any>(null) const selectedClass = ref<any>(null);
const dmRecords = ref<any[]>([]) const dmRecords = ref<any>([]);
const totalCount = ref(0) const totalCount = ref<any>(0);
const hasSearched = ref(false) const hasSearched = ref(false);
// 使 BasicListLayout // 使 BasicListLayout
const [register, { reload, setParam }] = useLayout({ const [register, { reload, setParam }] = useLayout({
api: getJcDmPageApi, api: async (params: any) => {
try {
const res = await getJcDmPageApi(params);
console.log("API返回数据:", res); //
//
if (res && res.rows) {
dmRecords.value = res.rows;
totalCount.value = res.total || 0;
} else {
dmRecords.value = [];
totalCount.value = 0;
}
return res;
} catch (error) {
console.error("获取数据失败:", error);
dmRecords.value = [];
totalCount.value = 0;
return { rows: [], total: 0 };
}
},
componentProps: { componentProps: {
auto: false auto: false,
} },
}) });
// //
const changeNjBj = async (nj: any, bj: any) => { const changeNjBj = async (nj: any, bj: any) => {
selectedClass.value = { selectedClass.value = {
njId: nj.key, njId: nj.key,
bjId: bj.key, bjId: bj.key,
} };
}; };
const onTimeRangeChange = (e: any) => { const onTimeRangeChange = (e: any) => {
// uni-datetime-picker // uni-datetime-picker
if (e && Array.isArray(e)) { if (e && Array.isArray(e)) {
const [start, end] = e const [start, end] = e;
startTime.value = start startTime.value = start;
endTime.value = end endTime.value = end;
} else if (e && typeof e === 'string') { } else if (e && typeof e === "string") {
// //
startTime.value = e startTime.value = e;
endTime.value = e endTime.value = e;
} }
} };
const getTimeRangeText = () => { const getTimeRangeText = () => {
if (!startTime.value || !endTime.value) return '选择时间范围' if (!startTime.value || !endTime.value) return "选择时间范围";
if (startTime.value === endTime.value) { if (startTime.value === endTime.value) {
return formatDate(startTime.value) return formatDate(startTime.value);
} }
return `${formatDate(startTime.value)} - ${formatDate(endTime.value)}` return `${formatDate(startTime.value)} - ${formatDate(endTime.value)}`;
} };
const searchRecords = async () => { const searchRecords = async () => {
if (!selectedClass.value || !startTime.value || !endTime.value) { if (!selectedClass.value || !startTime.value || !endTime.value) {
uni.showToast({ uni.showToast({
title: '请选择班级和时间范围', title: "请选择班级和时间范围",
icon: 'none' icon: "none",
}) });
return return;
} }
// //
if (startTime.value > endTime.value) { if (startTime.value > endTime.value) {
uni.showToast({ uni.showToast({
title: '开始时间不能大于结束时间', title: "开始时间不能大于结束时间",
icon: 'none' icon: "none",
}) });
return return;
} }
hasSearched.value = true hasSearched.value = true;
//
console.log("搜索参数:", {
njId: selectedClass.value.njId,
bjId: selectedClass.value.bjId,
startTime: startTime.value + " 00:00:00",
endTime: endTime.value + " 23:59:59",
pageNo: 1,
});
// //
setParam({ setParam({
njId: selectedClass.value.njId, njId: selectedClass.value.njId,
bjId: selectedClass.value.bjId, bjId: selectedClass.value.bjId,
startTime: startTime.value, startTime: startTime.value + " 00:00:00",
endTime: endTime.value, endTime: endTime.value + " 23:59:59",
pageNo: 1 pageNo: 1,
}); });
reload() reload();
} };
const resetSearch = () => { const resetSearch = () => {
selectedClass.value = null selectedClass.value = null;
startTime.value = '' startTime.value = "";
endTime.value = '' endTime.value = "";
dmRecords.value = [] dmRecords.value = [];
totalCount.value = 0 totalCount.value = 0;
hasSearched.value = false hasSearched.value = false;
} };
const goToDetail = (dm: any) => { const goToDetail = (dm: any) => {
setData(dm) setData(dm);
uni.navigateTo({ uni.navigateTo({
url: '/pages/view/routine/jc/detail' url: "/pages/view/routine/jc/detail",
}) });
} };
const exportRecords = () => { const exportRecords = () => {
if (dmRecords.value.length === 0) { if (dmRecords.value.length === 0) {
uni.showToast({ uni.showToast({
title: '暂无数据可导出', title: "暂无数据可导出",
icon: 'none' icon: "none",
}) });
return return;
} }
// //
uni.showToast({ uni.showToast({
title: '导出功能开发中', title: "导出功能开发中",
icon: 'none' 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);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
} 2,
"0"
)}-${String(date.getDate()).padStart(2, "0")}`;
};
const formatTime = (timeStr: string) => { const formatTime = (timeStr: string) => {
if (!timeStr) return '' if (!timeStr) return "";
const date = new Date(timeStr) const date = new Date(timeStr);
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` return `${String(date.getHours()).padStart(2, "0")}:${String(
} date.getMinutes()
).padStart(2, "0")}`;
};
// //
onMounted(() => { onMounted(() => {
// //
const today = new Date() const today = new Date();
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000) const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
startTime.value = oneWeekAgo.toISOString().split('T')[0] startTime.value = oneWeekAgo.toISOString().split("T")[0];
endTime.value = today.toISOString().split('T')[0] endTime.value = today.toISOString().split("T")[0];
//
console.log("组件初始化完成:", {
startTime: startTime.value,
endTime: endTime.value,
dmRecords: dmRecords.value,
totalCount: totalCount.value,
});
// //
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.record-content { .record-content {
padding: 20rpx; display: flex;
flex: 1 0 1px;
} }
.search-section { .search-section {
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 30rpx; padding: 30rpx;
margin-bottom: 20rpx; margin: 20rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
} }
@ -261,7 +305,7 @@ onMounted(() => {
display: flex; display: flex;
gap: 20rpx; gap: 20rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -293,7 +337,8 @@ onMounted(() => {
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
} }
.search-btn, .reset-btn { .search-btn,
.reset-btn {
flex: 1; flex: 1;
height: 70rpx; height: 70rpx;
border-radius: 35rpx; border-radius: 35rpx;
@ -312,18 +357,18 @@ onMounted(() => {
} }
.records-header { .records-header {
padding: 0 30rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
}
} }
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.export-btn { .export-btn {
font-size: 24rpx; font-size: 24rpx;
@ -339,103 +384,11 @@ onMounted(() => {
background-color: #fff; background-color: #fff;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.12);
}
&:last-child {
margin-bottom: 0;
}
} }
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1px solid #f0f0f0;
}
.time-info {
display: flex;
align-items: baseline;
gap: 16rpx;
}
.record-date {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.record-time {
font-size: 24rpx;
color: #666;
}
.status-badge {
padding: 8rpx 16rpx;
background-color: #e0f7fa;
border-radius: 20rpx;
border: 1px solid #b2ebf2;
display: flex;
align-items: center;
justify-content: center;
}
.status-text {
font-size: 24rpx;
color: #007bff;
font-weight: bold;
}
.card-content {
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1px solid #f0f0f0;
}
.class-info {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 16rpx;
}
.class-name {
color: #1890ff;
}
.stats-row {
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.stat-item {
display: flex;
align-items: center;
gap: 8rpx;
flex: 1;
justify-content: center;
padding: 12rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.stat-value {
font-size: 26rpx;
color: #333;
font-weight: bold;
}
.card-footer { .card-footer {
text-align: right; text-align: right;
@ -488,4 +441,34 @@ onMounted(() => {
color: #333; color: #333;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.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;
}
</style> </style>

View File

@ -91,6 +91,14 @@
<text class="stat-number absent">{{ getStatusCount('C') }}</text> <text class="stat-number absent">{{ getStatusCount('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>
<!-- 学生列表 --> <!-- 学生列表 -->
@ -100,6 +108,7 @@
v-for="student in dmDetail.xsList" v-for="student in dmDetail.xsList"
:key="student.id" :key="student.id"
class="student-item bg-white r-md p-12" class="student-item bg-white r-md p-12"
:class="getStudentItemClass(student.jcZt)"
> >
<view class="flex-row items-center"> <view class="flex-row items-center">
<view class="avatar-container mr-8"> <view class="avatar-container mr-8">
@ -112,6 +121,9 @@
<view class="flex-1 overflow-hidden"> <view class="flex-1 overflow-hidden">
<view class="student-name mb-8"> <view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xsXm }}</text> <text class="font-14 cor-333">{{ student.xsXm }}</text>
<text v-if="student.jcZt === 'D' || student.jcZt === 'E'" class="status-indicator">
{{ student.jcZt === 'D' ? '未缴费' : '未报名' }}
</text>
</view> </view>
<view class="flex-row"> <view class="flex-row">
<view <view
@ -228,6 +240,17 @@ const getStatusClass = (status: string) => {
} }
} }
const getStudentItemClass = (status: string) => {
switch (status) {
case 'D':
return 'status-unpaid-item'
case 'E':
return 'status-unregistered-item'
default:
return ''
}
}
const getTeacherStatusText = (status: string) => { const getTeacherStatusText = (status: string) => {
switch (status) { switch (status) {
case 'A': case 'A':
@ -328,7 +351,7 @@ onMounted(() => {
.info-label { .info-label {
font-size: 28rpx; font-size: 28rpx;
color: #666; color: #666;
width: 120rpx; width: 160rpx;
flex-shrink: 0; flex-shrink: 0;
} }
@ -339,21 +362,20 @@ onMounted(() => {
} }
.stats-container { .stats-container {
display: flex; display: grid;
justify-content: space-around; grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
margin-bottom: 30rpx; margin-bottom: 30rpx;
padding: 20rpx; padding: 20rpx;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 12rpx; border-radius: 12rpx;
flex-wrap: wrap;
gap: 60rpx;
} }
.stat-item { .stat-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
min-width: 120rpx; text-align: center;
} }
.stat-number { .stat-number {
@ -453,6 +475,27 @@ onMounted(() => {
color: #eb2f96; color: #eb2f96;
} }
.status-unpaid-item {
border-left: 8rpx solid #ff4d4f;
background-color: #fff0f0;
}
.status-unregistered-item {
border-left: 8rpx solid #eb2f96;
background-color: #fff0f6;
}
.status-indicator {
font-size: 20rpx;
margin-left: 16rpx;
color: #ff4d4f;
background-color: #fff0f0;
padding: 4rpx 12rpx;
border-radius: 8rpx;
border: 1rpx solid #ff4d4f;
font-weight: bold;
}
.empty-tip { .empty-tip {
text-align: center; text-align: center;
color: #999; color: #999;

View File

@ -53,6 +53,8 @@ const switchTab = (tab: string) => {
.dm-container { .dm-container {
min-height: 100vh; min-height: 100vh;
background-color: #f5f5f5; background-color: #f5f5f5;
display: flex;
flex-direction: column;
} }
.tab-container { .tab-container {