664 lines
16 KiB
Vue
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>
|