kindergarten/reading-platform-frontend/src/views/teacher/lessons/LessonListView.vue
zhonghua ca361d6d2b feat(移动端): 优化教师端上课记录筛选排版
- 筛选区改为可换行/移动端堆叠布局,避免按钮被挤压或横向溢出

Made-with: Cursor
2026-03-04 14:14:15 +08:00

664 lines
16 KiB
Vue

<template>
<div class="lesson-list-view">
<!-- 页面标题 -->
<div class="page-header">
<h2>上课记录</h2>
<p class="page-desc">查看您的授课历史和课堂记录</p>
</div>
<!-- 筛选区域 -->
<div class="filter-section">
<div class="filter-row">
<a-select
v-model:value="filters.status"
placeholder="课程状态"
class="filter-status"
allowClear
@change="loadLessons"
>
<a-select-option value="PLANNED">已计划</a-select-option>
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
<a-select-option value="COMPLETED">已完成</a-select-option>
<a-select-option value="CANCELLED">已取消</a-select-option>
</a-select>
<a-range-picker
v-model:value="filters.dateRange"
class="filter-range"
@change="loadLessons"
/>
<a-button class="filter-reset" @click="resetFilters">重置</a-button>
</div>
</div>
<!-- 记录列表 -->
<a-spin :spinning="loading">
<div class="lesson-list" v-if="lessons.length > 0">
<div
v-for="lesson in lessons"
:key="lesson.id"
class="lesson-card"
@click="viewDetail(lesson)"
>
<div class="card-left">
<div class="lesson-status" :class="getStatusClass(lesson.status)">
{{ getStatusText(lesson.status) }}
</div>
</div>
<div class="card-content">
<div class="card-header">
<h3 class="course-name">{{ lesson.course?.name || '未知课程' }}</h3>
<span class="lesson-time">
<ClockCircleOutlined />
{{ formatDateTime(lesson.startDatetime || lesson.plannedDatetime) }}
</span>
</div>
<div class="card-body">
<div class="info-item">
<TeamOutlined />
<span>{{ lesson.class?.name || '未知班级' }}</span>
</div>
<div class="info-item" v-if="lesson.actualDuration">
<FieldTimeOutlined />
<span>实际时长: {{ lesson.actualDuration }} 分钟</span>
</div>
<div class="info-item" v-if="lesson.overallRating">
<StarOutlined />
<a-rate :value="Number(lesson.overallRating)" disabled :count="5" style="font-size: 12px;" />
</div>
</div>
<div class="card-footer" v-if="lesson.completionNote">
<FileTextOutlined />
<span class="note-preview">{{ lesson.completionNote }}</span>
</div>
</div>
<div class="card-action">
<RightOutlined />
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-else>
<InboxOutlined class="empty-icon" />
<p>暂无上课记录</p>
<p class="empty-hint">完成授课后,记录将显示在这里</p>
</div>
</a-spin>
<!-- 分页 -->
<div class="pagination-section" v-if="total > pageSize">
<a-pagination
v-model:current="currentPage"
:total="total"
:page-size="pageSize"
show-size-changer
:page-size-options="['10', '20', '50']"
@change="onPageChange"
@showSizeChange="onPageSizeChange"
/>
</div>
<!-- 详情抽屉 -->
<a-drawer
v-model:open="detailDrawerVisible"
title="授课记录详情"
placement="right"
:width="520"
>
<div class="detail-content" v-if="selectedLesson">
<a-descriptions :column="1" bordered>
<a-descriptions-item label="课程名称">
{{ selectedLesson.course?.name }}
</a-descriptions-item>
<a-descriptions-item label="授课班级">
{{ selectedLesson.class?.name }}
</a-descriptions-item>
<a-descriptions-item label="课程状态">
<a-tag :color="getStatusColor(selectedLesson.status)">
{{ getStatusText(selectedLesson.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="计划时间">
{{ formatDateTime(selectedLesson.plannedDatetime) }}
</a-descriptions-item>
<a-descriptions-item label="开始时间" v-if="selectedLesson.startDatetime">
{{ formatDateTime(selectedLesson.startDatetime) }}
</a-descriptions-item>
<a-descriptions-item label="结束时间" v-if="selectedLesson.endDatetime">
{{ formatDateTime(selectedLesson.endDatetime) }}
</a-descriptions-item>
<a-descriptions-item label="实际时长" v-if="selectedLesson.actualDuration">
{{ selectedLesson.actualDuration }} 分钟
</a-descriptions-item>
</a-descriptions>
<!-- 评价信息 -->
<div class="rating-section" v-if="selectedLesson.status === 'COMPLETED'">
<h4>课堂评价</h4>
<div class="rating-item" v-if="selectedLesson.overallRating">
<span class="rating-label">整体评分</span>
<a-rate :value="Number(selectedLesson.overallRating)" disabled />
</div>
<div class="rating-item" v-if="selectedLesson.participationRating">
<span class="rating-label">参与度评分</span>
<a-rate :value="Number(selectedLesson.participationRating)" disabled />
</div>
<div class="note-section" v-if="selectedLesson.completionNote">
<h4>课堂备注</h4>
<p class="note-content">{{ selectedLesson.completionNote }}</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<!-- 已计划状态 -->
<template v-if="selectedLesson.status === 'PLANNED'">
<a-button type="primary" block @click="startPlannedLesson" style="margin-bottom: 12px;">
开始上课
</a-button>
<a-button block @click="goToPrepare" style="margin-bottom: 12px;">
进入备课
</a-button>
<a-button danger block @click="cancelLesson">
取消预约
</a-button>
</template>
<!-- 进行中状态 -->
<a-button
v-if="selectedLesson.status === 'IN_PROGRESS'"
type="primary"
block
@click="goToLesson"
>
继续上课
</a-button>
<!-- 已完成状态 -->
<a-button
v-if="selectedLesson.status === 'COMPLETED'"
type="primary"
block
@click="goToRecords"
style="margin-bottom: 12px;"
>
课后记录
</a-button>
<a-button
v-if="selectedLesson.status === 'COMPLETED'"
block
@click="goToCourseDetail"
>
查看课程
</a-button>
</div>
</div>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import {
ClockCircleOutlined,
TeamOutlined,
FieldTimeOutlined,
StarOutlined,
FileTextOutlined,
RightOutlined,
InboxOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import * as teacherApi from '@/api/teacher';
import dayjs from 'dayjs';
const router = useRouter();
const loading = ref(false);
const lessons = ref<any[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const filters = reactive({
status: undefined as string | undefined,
dateRange: undefined as [dayjs.Dayjs, dayjs.Dayjs] | undefined,
});
const detailDrawerVisible = ref(false);
const selectedLesson = ref<any>(null);
// 状态映射
const statusMap: Record<string, { text: string; color: string; class: string }> = {
PLANNED: { text: '已计划', color: 'blue', class: 'status-planned' },
IN_PROGRESS: { text: '进行中', color: 'orange', class: 'status-progress' },
COMPLETED: { text: '已完成', color: 'green', class: 'status-completed' },
CANCELLED: { text: '已取消', color: 'default', class: 'status-cancelled' },
};
const getStatusText = (status: string) => statusMap[status]?.text || status;
const getStatusColor = (status: string) => statusMap[status]?.color || 'default';
const getStatusClass = (status: string) => statusMap[status]?.class || '';
const formatDateTime = (date: string | Date | null) => {
if (!date) return '-';
return dayjs(date).format('YYYY-MM-DD HH:mm');
};
const loadLessons = async () => {
loading.value = true;
try {
const params: any = {
page: currentPage.value,
pageSize: pageSize.value,
};
if (filters.status) {
params.status = filters.status;
}
const data = await teacherApi.getLessons(params);
lessons.value = data.items || [];
total.value = data.total || 0;
} catch (error: any) {
message.error(error.response?.data?.message || '获取上课记录失败');
} finally {
loading.value = false;
}
};
const resetFilters = () => {
filters.status = undefined;
filters.dateRange = null;
currentPage.value = 1;
loadLessons();
};
const onPageChange = (page: number) => {
currentPage.value = page;
loadLessons();
};
const onPageSizeChange = (current: number, size: number) => {
currentPage.value = 1;
pageSize.value = size;
loadLessons();
};
const viewDetail = (lesson: any) => {
selectedLesson.value = lesson;
detailDrawerVisible.value = true;
};
const goToPrepare = () => {
if (selectedLesson.value?.course?.id) {
router.push(`/teacher/courses/${selectedLesson.value.course.id}/prepare`);
detailDrawerVisible.value = false;
}
};
const goToLesson = () => {
if (selectedLesson.value?.id) {
router.push(`/teacher/lessons/${selectedLesson.value.id}`);
detailDrawerVisible.value = false;
}
};
const goToRecords = () => {
if (selectedLesson.value?.id) {
router.push(`/teacher/lessons/${selectedLesson.value.id}/records`);
detailDrawerVisible.value = false;
}
};
const goToCourseDetail = () => {
if (selectedLesson.value?.course?.id) {
router.push(`/teacher/courses/${selectedLesson.value.course.id}`);
detailDrawerVisible.value = false;
}
};
// 开始已预约的课程
const startPlannedLesson = async () => {
if (!selectedLesson.value?.id) return;
Modal.confirm({
title: '开始上课',
content: '确认要开始上课吗?',
okText: '确认开始',
cancelText: '取消',
onOk: async () => {
try {
await teacherApi.startLesson(selectedLesson.value.id);
message.success('正在进入课堂...');
detailDrawerVisible.value = false;
router.push(`/teacher/lessons/${selectedLesson.value.id}`);
} catch (error: any) {
message.error(error.response?.data?.message || '开始上课失败');
}
},
});
};
// 取消预约
const cancelLesson = () => {
if (!selectedLesson.value?.id) return;
Modal.confirm({
title: '取消预约',
content: '确定要取消这个预约吗?取消后可以在"上课记录"中查看。',
okText: '确认取消',
cancelText: '返回',
okType: 'danger',
onOk: async () => {
try {
await teacherApi.cancelLesson(selectedLesson.value.id);
message.success('预约已取消');
detailDrawerVisible.value = false;
loadLessons();
} catch (error: any) {
message.error(error.response?.data?.message || '取消失败');
}
},
});
};
onMounted(() => {
loadLessons();
});
</script>
<style scoped lang="scss">
.lesson-list-view {
overflow-x: hidden;
min-width: 0;
.page-header {
margin-bottom: 24px;
h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.page-desc {
margin: 0;
color: #999;
font-size: 14px;
}
}
.filter-section {
margin-bottom: 20px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.filter-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
min-width: 0;
}
.filter-status {
width: 140px;
min-width: 0;
}
.filter-range {
width: 260px;
min-width: 0;
max-width: 100%;
}
.filter-reset {
white-space: nowrap;
}
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-status,
.filter-range {
width: 100% !important;
}
.filter-reset {
width: 100%;
}
/* RangePicker 内部输入避免撑出容器 */
:deep(.ant-picker) {
width: 100%;
max-width: 100%;
}
}
.lesson-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.lesson-card {
display: flex;
align-items: stretch;
background: white;
border: 1px solid #f0f0f0;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #FF8C42;
box-shadow: 0 4px 12px rgba(255, 140, 66, 0.1);
.card-action {
color: #FF8C42;
}
}
.card-left {
display: flex;
align-items: center;
padding-right: 16px;
.lesson-status {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
text-align: center;
line-height: 1.3;
&.status-planned {
background: #E3F2FD;
color: #1976D2;
}
&.status-progress {
background: #FFF3E0;
color: #F57C00;
}
&.status-completed {
background: #E8F5E9;
color: #388E3C;
}
&.status-cancelled {
background: #F5F5F5;
color: #999;
}
}
}
.card-content {
flex: 1;
min-width: 0;
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
.course-name {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.lesson-time {
font-size: 13px;
color: #999;
display: flex;
align-items: center;
gap: 4px;
}
}
.card-body {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 8px;
.info-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666;
.anticon {
color: #999;
}
}
}
.card-footer {
display: flex;
align-items: flex-start;
gap: 6px;
padding-top: 8px;
border-top: 1px dashed #f0f0f0;
font-size: 13px;
color: #999;
.anticon {
margin-top: 2px;
}
.note-preview {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.card-action {
display: flex;
align-items: center;
padding-left: 16px;
color: #d9d9d9;
font-size: 16px;
transition: color 0.2s;
}
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
.empty-icon {
font-size: 64px;
color: #d9d9d9;
margin-bottom: 16px;
}
p {
margin: 4px 0;
}
.empty-hint {
font-size: 13px;
color: #bfbfbf;
}
}
.pagination-section {
margin-top: 24px;
text-align: center;
}
}
// 详情抽屉样式
.detail-content {
.rating-section {
margin-top: 24px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.rating-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.rating-label {
width: 100px;
font-size: 13px;
color: #666;
}
}
}
.note-section {
margin-top: 24px;
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.note-content {
padding: 12px;
background: #fafafa;
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
color: #666;
white-space: pre-wrap;
}
}
.action-section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
}
</style>