591 lines
13 KiB
Vue
591 lines
13 KiB
Vue
<template>
|
||
<!-- 使用 BasicListLayout 包裹记录列表 -->
|
||
<BasicListLayout @register="register" :fixed="true" class="flex-1">
|
||
<template #top>
|
||
<!-- 课程信息卡片 -->
|
||
<view class="course-card">
|
||
<view class="flex-row items-center mb-15">
|
||
<view class="course-icon flex-center mr-10">
|
||
<u-icon name="calendar" color="#4080ff" size="20"></u-icon>
|
||
</view>
|
||
<text class="font-16 font-bold">{{ xkkc.xkkcMc || xkkc.kcmc }}</text>
|
||
<text class="font-14 cor-999 ml-10">{{ todayInfo.date }} ({{ todayInfo.weekName }})</text>
|
||
</view>
|
||
</view>
|
||
<!-- 搜索筛选区域 -->
|
||
<view class="search-section">
|
||
<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="!hasSearched" class="initial-tip">
|
||
<view class="tip-icon">🔍</view>
|
||
<text class="tip-text">请选择时间范围开始搜索</text>
|
||
</view>
|
||
</template>
|
||
|
||
<template #default="{ data }">
|
||
<view class="record-card" @click="viewRecordDetail(data)">
|
||
<view class="record-header">
|
||
<view class="record-time">
|
||
<u-icon name="clock" color="#666" size="14"></u-icon>
|
||
<text class="time-text">{{ formatDateTime(data.dmTime) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-stats">
|
||
<view class="stat-item">
|
||
<text class="stat-number">{{ data.zrs || 0 }}</text>
|
||
<text class="stat-label">总人数</text>
|
||
</view>
|
||
<view class="stat-item present">
|
||
<text class="stat-number">{{ data.sdRs || 0 }}</text>
|
||
<text class="stat-label">实到</text>
|
||
</view>
|
||
<view class="stat-item leave">
|
||
<text class="stat-number">{{ data.qjRs || 0 }}</text>
|
||
<text class="stat-label">请假</text>
|
||
</view>
|
||
<view class="stat-item absent">
|
||
<text class="stat-number">{{ data.qqRs || 0 }}</text>
|
||
<text class="stat-label">缺勤</text>
|
||
</view>
|
||
<view class="stat-item overtime">
|
||
<text class="stat-number">{{ data.cdRs || 0 }}</text>
|
||
<text class="stat-label">迟到</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-footer">
|
||
<text class="teacher-name">教师:{{ data.createdUserName || '未知' }}</text>
|
||
<view class="view-detail">
|
||
<text class="detail-text">查看详情</text>
|
||
<u-icon name="arrow-right" color="#4080ff" size="12"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<template #bottom>
|
||
<!-- 返回按钮 -->
|
||
<view class="bottom-actions mx-15 mb-15">
|
||
<button class="back-btn" @click="goBack">返回</button>
|
||
</view>
|
||
</template>
|
||
</BasicListLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from "vue";
|
||
import { useUserStore } from "@/store/modules/user";
|
||
import { useDataStore } from "@/store/modules/data";
|
||
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
|
||
import { getXkDmPageApi } from "@/api/base/xkApi";
|
||
import dayjs from "dayjs";
|
||
|
||
const { getJs } = useUserStore();
|
||
const { getData, setData } = useDataStore();
|
||
|
||
const js = computed(() => getJs);
|
||
const xkkc = computed(() => getData);
|
||
|
||
// 今日信息
|
||
const now = dayjs();
|
||
let wDay = now.day();
|
||
if (wDay === 0) {
|
||
wDay = 7;
|
||
}
|
||
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||
const todayInfo = ref({
|
||
date: now.format("YYYY-MM-DD"),
|
||
weekName: wdNameList[wDay - 1],
|
||
});
|
||
|
||
// 响应式数据
|
||
const loading = ref(false);
|
||
const startTime = ref("");
|
||
const endTime = ref("");
|
||
const dmRecords = ref<any>([]);
|
||
const totalCount = ref<any>(0);
|
||
const hasSearched = ref(false);
|
||
|
||
// 使用 BasicListLayout
|
||
const [register, { reload, setParam }] = useLayout({
|
||
api: async (params: any) => {
|
||
try {
|
||
const res = await getXkDmPageApi({
|
||
...params,
|
||
xkkcId: xkkc.value.id,
|
||
jsId: js.value.id,
|
||
sidx: 'dmTime',
|
||
sord: 'desc'
|
||
});
|
||
console.log("API返回数据:", res, xkkc.value);
|
||
|
||
if (res) {
|
||
dmRecords.value = res?.rows || [];
|
||
totalCount.value = res?.total || 0;
|
||
return res;
|
||
} else {
|
||
dmRecords.value = [];
|
||
totalCount.value = 0;
|
||
return { rows: [], total: 0, page: 1, pageSize: 10 };
|
||
}
|
||
} catch (error) {
|
||
console.error("获取数据失败:", error);
|
||
dmRecords.value = [];
|
||
totalCount.value = 0;
|
||
return { rows: [], total: 0, page: 1, pageSize: 10 };
|
||
}
|
||
},
|
||
componentProps: {
|
||
auto: false,
|
||
},
|
||
});
|
||
|
||
// 时间范围选择
|
||
const onTimeRangeChange = (e: any) => {
|
||
if (e && Array.isArray(e)) {
|
||
const [start, end] = e;
|
||
startTime.value = start;
|
||
endTime.value = end;
|
||
} else if (e && typeof e === "string") {
|
||
startTime.value = e;
|
||
endTime.value = e;
|
||
}
|
||
};
|
||
|
||
const getTimeRangeText = () => {
|
||
if (!startTime.value || !endTime.value) return "选择时间范围";
|
||
if (startTime.value === endTime.value) {
|
||
return formatDate(startTime.value);
|
||
}
|
||
return `${formatDate(startTime.value)} - ${formatDate(endTime.value)}`;
|
||
};
|
||
|
||
const searchRecords = async () => {
|
||
if (!startTime.value || !endTime.value) {
|
||
uni.showToast({
|
||
title: "请选择时间范围",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 验证开始时间不能大于结束时间
|
||
if (startTime.value > endTime.value) {
|
||
uni.showToast({
|
||
title: "开始时间不能大于结束时间",
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
hasSearched.value = true;
|
||
|
||
console.log("搜索参数:", {
|
||
startTime: startTime.value + " 00:00:00",
|
||
endTime: endTime.value + " 23:59:59",
|
||
pageNo: 1,
|
||
});
|
||
|
||
// 设置搜索参数并重新加载
|
||
setParam({
|
||
startTime: startTime.value + " 00:00:00",
|
||
endTime: endTime.value + " 23:59:59",
|
||
pageNo: 1,
|
||
});
|
||
reload();
|
||
};
|
||
|
||
const resetSearch = () => {
|
||
startTime.value = "";
|
||
endTime.value = "";
|
||
dmRecords.value = [];
|
||
totalCount.value = 0;
|
||
hasSearched.value = false;
|
||
};
|
||
|
||
// 格式化日期时间
|
||
const formatDateTime = (dateTime: string | Date) => {
|
||
if (!dateTime) return '';
|
||
return dayjs(dateTime).format('MM-DD HH:mm');
|
||
};
|
||
|
||
const formatDate = (dateStr: string) => {
|
||
if (!dateStr) return "";
|
||
const date = new Date(dateStr);
|
||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
|
||
2,
|
||
"0"
|
||
)}-${String(date.getDate()).padStart(2, "0")}`;
|
||
};
|
||
|
||
// 获取记录状态样式类
|
||
const getRecordStatusClass = (status: string) => {
|
||
switch (status) {
|
||
case 'A': return 'status-active';
|
||
case 'B': return 'status-pending';
|
||
case 'C': return 'status-cancelled';
|
||
default: return 'status-active';
|
||
}
|
||
};
|
||
|
||
// 获取记录状态文本
|
||
const getRecordStatusText = (status: string) => {
|
||
switch (status) {
|
||
case 'A': return '正常';
|
||
case 'B': return '待处理';
|
||
case 'C': return '已取消';
|
||
default: return '正常';
|
||
}
|
||
};
|
||
|
||
// 查看记录详情
|
||
const viewRecordDetail = (record: any) => {
|
||
// 将记录信息存储到store中
|
||
setData({
|
||
...xkkc.value,
|
||
dmRecord: record
|
||
});
|
||
|
||
// 跳转到详情页面
|
||
uni.navigateTo({
|
||
url: '/pages/view/routine/xk/dmXsList'
|
||
});
|
||
};
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 设置默认时间范围为最近一周
|
||
const today = new Date();
|
||
const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||
|
||
startTime.value = oneWeekAgo.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>
|
||
|
||
<style scoped lang="scss">
|
||
.course-card {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||
margin: 30rpx;
|
||
padding: 30rpx;
|
||
}
|
||
|
||
.course-icon {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 4px;
|
||
background-color: rgba(64, 128, 255, 0.1);
|
||
}
|
||
|
||
.search-section {
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 30rpx;
|
||
margin-left: 30rpx;
|
||
margin-right: 30rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.search-row {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.search-item {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
white-space: nowrap;
|
||
flex: 0 0 160rpx;
|
||
}
|
||
|
||
.date-picker {
|
||
flex: 1;
|
||
}
|
||
|
||
.picker-text {
|
||
padding: 16rpx 20rpx;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
border: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.search-btn,
|
||
.reset-btn {
|
||
flex: 1;
|
||
height: 70rpx;
|
||
border-radius: 35rpx;
|
||
font-size: 28rpx;
|
||
border: none;
|
||
}
|
||
|
||
.search-btn {
|
||
background-color: #007aff;
|
||
color: #fff;
|
||
}
|
||
|
||
.reset-btn {
|
||
background-color: #f5f5f5;
|
||
color: #666;
|
||
}
|
||
|
||
.records-header {
|
||
padding: 0 30rpx;
|
||
margin-bottom: 20rpx;
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.record-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 30rpx;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.record-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
|
||
.record-time {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
|
||
.time-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.record-status {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
|
||
&.status-active {
|
||
background: rgba(40, 121, 255, 0.1);
|
||
color: #2879ff;
|
||
}
|
||
|
||
&.status-pending {
|
||
background: rgba(255, 153, 0, 0.1);
|
||
color: #ff9900;
|
||
}
|
||
|
||
&.status-cancelled {
|
||
background: rgba(255, 77, 79, 0.1);
|
||
color: #ff4d4f;
|
||
}
|
||
}
|
||
}
|
||
|
||
.record-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
padding: 10px 5px;
|
||
border-radius: 8px;
|
||
background: #f8f9fa;
|
||
|
||
&.present {
|
||
background: rgba(40, 121, 255, 0.1);
|
||
.stat-number { color: #2879ff; }
|
||
}
|
||
|
||
&.leave {
|
||
background: rgba(255, 153, 0, 0.1);
|
||
.stat-number { color: #ff9900; }
|
||
}
|
||
|
||
&.absent {
|
||
background: rgba(255, 77, 79, 0.1);
|
||
.stat-number { color: #ff4d4f; }
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.record-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 15px;
|
||
border-top: 1px solid #f0f0f0;
|
||
|
||
.teacher-name {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.view-detail {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
|
||
.detail-text {
|
||
font-size: 14px;
|
||
color: #4080ff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 100rpx 0;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 80rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.empty-tip {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.initial-tip {
|
||
text-align: center;
|
||
padding: 50rpx 0;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.tip-icon {
|
||
font-size: 60rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.tip-text {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.bottom-actions {
|
||
.back-btn {
|
||
width: 100%;
|
||
height: 44px;
|
||
background: #4080ff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 22px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
// 工具类
|
||
.mx-15 { margin-left: 15px; margin-right: 15px; }
|
||
.my-15 { margin-top: 15px; margin-bottom: 15px; }
|
||
.mb-15 { margin-bottom: 15px; }
|
||
.mb-30 { margin-bottom: 30px; }
|
||
.mr-10 { margin-right: 10px; }
|
||
.ml-10 { margin-left: 10px; }
|
||
.bg-white { background-color: white; }
|
||
.white-bg-color { background-color: white; }
|
||
.r-md { border-radius: 8px; }
|
||
.p-15 { padding: 15px; }
|
||
.flex-row { display: flex; flex-direction: row; }
|
||
.items-center { align-items: center; }
|
||
.font-16 { font-size: 16px; }
|
||
.font-14 { font-size: 14px; }
|
||
.font-bold { font-weight: bold; }
|
||
.cor-999 { color: #999; }
|
||
</style>
|
||
|