kindergarten/reading-platform-frontend/src/views/school/school-courses/SchoolCourseDetailView.vue

658 lines
18 KiB
Vue
Raw Normal View History

<template>
<div class="school-course-detail-page">
<!-- 顶部导航 -->
<div class="detail-header">
<div class="header-left">
<a-button type="text" @click="router.back()">
<ArrowLeftOutlined />
</a-button>
<div class="course-title">
<h2>{{ detail?.name || '校本课程包详情' }}</h2>
<a-tag :color="detail?.status === 'ACTIVE' ? 'success' : 'default'">
{{ detail?.status === 'ACTIVE' ? '启用' : '禁用' }}
</a-tag>
</div>
</div>
<div class="header-actions">
<a-button @click="showReserveModal">
<CalendarOutlined /> 预约
</a-button>
<a-button type="primary" @click="handleEdit">
<EditOutlined /> 编辑
</a-button>
</div>
</div>
<a-spin :spinning="loading">
<div class="detail-content">
<!-- 基本信息 -->
<div class="section-card">
<div class="section-header">
<span class="section-title">
<InfoCircleOutlined /> 基本信息
</span>
</div>
<div class="section-body">
<div class="info-grid">
<div class="info-item">
<span class="info-label">校本课程包名称</span>
<span class="info-value">{{ detail?.name }}</span>
</div>
<div class="info-item">
<span class="info-label">基于课程包</span>
<span class="info-value">{{ detail?.sourceCourse?.name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">创建者</span>
<span class="info-value">{{ detail?.creator?.name || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">使用次数</span>
<span class="info-value">
<a-badge :count="detail?.usageCount || 0" :number-style="{ backgroundColor: '#52c41a' }" />
</span>
</div>
<div class="info-item">
<span class="info-label">创建时间</span>
<span class="info-value">{{ formatDate(detail?.createdAt) }}</span>
</div>
<div class="info-item">
<span class="info-label">更新时间</span>
<span class="info-value">{{ formatDate(detail?.updatedAt) }}</span>
</div>
</div>
<div class="info-full" v-if="detail?.description">
<span class="info-label">描述</span>
<span class="info-value">{{ detail?.description }}</span>
</div>
<div class="info-full" v-if="detail?.changesSummary">
<span class="info-label">修改说明</span>
<span class="info-value">{{ detail?.changesSummary }}</span>
</div>
</div>
</div>
<!-- 课程配置 -->
<div class="section-card" v-if="detail?.lessons && detail.lessons.length > 0">
<div class="section-header">
<span class="section-title">
<AppstoreOutlined /> 课程配置
</span>
<a-tag>{{ detail.lessons.length }} 个课程</a-tag>
</div>
<div class="section-body">
<div class="lesson-cards">
<div
v-for="lesson in detail.lessons"
:key="lesson.id"
class="lesson-card"
>
<div class="lesson-header">
<a-tag :color="getLessonTypeColor(lesson.lessonType)">
{{ getLessonTypeName(lesson.lessonType) }}
</a-tag>
</div>
<div class="lesson-body">
<div class="lesson-section" v-if="lesson.objectives">
<div class="lesson-section-title">教学目标</div>
<div class="lesson-section-content">{{ lesson.objectives }}</div>
</div>
<div class="lesson-section" v-if="lesson.preparation">
<div class="lesson-section-title">教学准备</div>
<div class="lesson-section-content">{{ lesson.preparation }}</div>
</div>
<div class="lesson-section" v-if="lesson.changeNote">
<div class="lesson-section-title change-note">
<EditOutlined /> 修改备注
</div>
<div class="lesson-section-content highlighted">{{ lesson.changeNote }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 预约/排课记录 -->
<div class="section-card">
<div class="section-header">
<span class="section-title">
<CalendarOutlined /> 预约/排课记录
</span>
<a-button type="link" size="small" @click="showReserveModal">
<PlusOutlined /> 新增预约
</a-button>
</div>
<div class="section-body">
<a-tabs v-model:activeKey="reservationTab">
<a-tab-pane key="upcoming" :tab="`即将上课 (${upcomingReservations.length})`">
<a-table
:columns="reservationColumns"
:data-source="upcomingReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'scheduledDate'">
{{ formatDateTime(record.scheduledDate) }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" danger @click="cancelReserve(record)">
取消
</a-button>
</a-space>
</template>
</template>
</a-table>
<div v-if="upcomingReservations.length === 0" class="empty-state">
暂无即将上课的预约
</div>
</a-tab-pane>
<a-tab-pane key="history" :tab="`历史记录 (${historyReservations.length})`">
<a-table
:columns="reservationColumns"
:data-source="historyReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="{ pageSize: 5 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'scheduledDate'">
{{ formatDateTime(record.scheduledDate) }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
</template>
</template>
</a-table>
<div v-if="historyReservations.length === 0" class="empty-state">
暂无历史记录
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
</div>
</a-spin>
<!-- 预约弹窗 -->
<a-modal
v-model:open="reserveModalVisible"
title="预约校本课程包"
width="500px"
@ok="handleReserve"
:confirmLoading="reserveLoading"
>
<div class="reserve-modal" v-if="detail">
<div class="course-info">
<span class="label">课程包名称</span>
<span class="value">{{ detail.name }}</span>
</div>
<a-divider />
<a-form layout="vertical">
<a-form-item label="授课教师" required>
<a-select v-model:value="reserveForm.teacherId" placeholder="选择授课教师" show-search>
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="授课班级" required>
<a-select v-model:value="reserveForm.classId" placeholder="选择授课班级">
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="预约时间" required>
<a-date-picker
v-model:value="reserveForm.scheduledDate"
show-time
format="YYYY-MM-DD HH:mm"
placeholder="选择预约时间"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="reserveForm.note" :rows="2" placeholder="备注信息(可选)" />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import {
ArrowLeftOutlined,
CalendarOutlined,
EditOutlined,
InfoCircleOutlined,
AppstoreOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import {
getSchoolCourseDetail,
getReservations,
createReservation,
cancelReservation,
type SchoolCourse,
type CreateReservationData,
} from '@/api/school-course';
import { getTeachers, getClasses } from '@/api/school';
import type { Teacher, ClassInfo } from '@/api/school';
const router = useRouter();
const route = useRoute();
const loading = ref(false);
const detail = ref<SchoolCourse | null>(null);
// 预约相关
const reserveModalVisible = ref(false);
const reserveLoading = ref(false);
const reserveForm = ref<CreateReservationData>({
teacherId: undefined as any,
classId: undefined as any,
scheduledDate: undefined as any,
note: '',
});
// 数据
const teachers = ref<Teacher[]>([]);
const classes = ref<ClassInfo[]>([]);
const reservations = ref<any[]>([]);
const reservationLoading = ref(false);
const reservationTab = ref('upcoming');
const reservationColumns = [
{ title: '预约时间', key: 'scheduledDate', width: 160 },
{ title: '教师', dataIndex: ['teacher', 'name'], key: 'teacherName', width: 100 },
{ title: '班级', dataIndex: ['class', 'name'], key: 'className', width: 100 },
{ title: '状态', key: 'status', width: 80 },
{ title: '操作', key: 'action', width: 80 },
];
const lessonTypeNames: Record<string, string> = {
INTRO: '导入课',
COLLECTIVE: '集体课',
DOMAIN_HEALTH: '健康领域',
DOMAIN_LANGUAGE: '语言领域',
DOMAIN_SOCIAL: '社会领域',
DOMAIN_SCIENCE: '科学领域',
DOMAIN_ART: '艺术领域',
};
const lessonTypeColors: Record<string, string> = {
INTRO: 'cyan',
COLLECTIVE: 'green',
DOMAIN_HEALTH: 'red',
DOMAIN_LANGUAGE: 'orange',
DOMAIN_SOCIAL: 'purple',
DOMAIN_SCIENCE: 'geekblue',
DOMAIN_ART: 'magenta',
};
const getLessonTypeName = (type: string) => lessonTypeNames[type] || type;
const getLessonTypeColor = (type: string) => lessonTypeColors[type] || 'default';
// 计算属性
const upcomingReservations = computed(() => {
const now = new Date();
return reservations.value.filter(r => new Date(r.scheduledDate) >= now);
});
const historyReservations = computed(() => {
const now = new Date();
return reservations.value.filter(r => new Date(r.scheduledDate) < now);
});
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
PENDING: 'blue',
COMPLETED: 'green',
CANCELLED: 'red',
};
return colors[status] || 'default';
};
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
PENDING: '待上课',
COMPLETED: '已完成',
CANCELLED: '已取消',
};
return texts[status] || status;
};
const formatDate = (date?: string) => {
if (!date) return '-';
return new Date(date).toLocaleDateString('zh-CN');
};
const formatDateTime = (date?: string) => {
if (!date) return '-';
return new Date(date).toLocaleString('zh-CN');
};
const fetchData = async () => {
loading.value = true;
try {
const id = Number(route.params.id);
const res = await getSchoolCourseDetail(id);
detail.value = (res as any).data || res;
} catch (error) {
message.error('获取详情失败');
} finally {
loading.value = false;
}
};
const fetchBaseData = async () => {
try {
const [teacherRes, classRes] = await Promise.all([
getTeachers({ pageSize: 1000 }),
getClasses(),
]);
teachers.value = (teacherRes as any).items || [];
classes.value = classRes as any;
} catch (error) {
console.error('获取基础数据失败', error);
}
};
const loadReservations = async () => {
if (!detail.value) return;
reservationLoading.value = true;
try {
const res = await getReservations(detail.value.id);
reservations.value = (res as any) || [];
} catch (error) {
console.error('获取预约列表失败', error);
} finally {
reservationLoading.value = false;
}
};
const handleEdit = () => {
router.push(`/school/school-courses/${route.params.id}/edit`);
};
const showReserveModal = () => {
reserveForm.value = {
teacherId: undefined as any,
classId: undefined as any,
scheduledDate: undefined as any,
note: '',
};
reserveModalVisible.value = true;
};
const handleReserve = async () => {
if (!detail.value) return;
if (!reserveForm.value.teacherId) {
message.warning('请选择授课教师');
return;
}
if (!reserveForm.value.classId) {
message.warning('请选择授课班级');
return;
}
if (!reserveForm.value.scheduledDate) {
message.warning('请选择预约时间');
return;
}
reserveLoading.value = true;
try {
const data = {
...reserveForm.value,
scheduledDate: reserveForm.value.scheduledDate.format('YYYY-MM-DD HH:mm'),
};
await createReservation(detail.value.id, data);
message.success('预约成功');
reserveModalVisible.value = false;
await loadReservations();
} catch (error) {
console.error('预约失败', error);
message.error('预约失败');
} finally {
reserveLoading.value = false;
}
};
const cancelReserve = async (record: any) => {
try {
await cancelReservation(record.id);
message.success('取消成功');
await loadReservations();
} catch (error) {
message.error('取消失败');
}
};
onMounted(() => {
fetchData();
fetchBaseData().then(() => {
loadReservations();
});
});
</script>
<style scoped lang="scss">
.school-course-detail-page {
min-height: 100vh;
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 50%, #F0FDF4 100%);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
z-index: 100;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.course-title {
display: flex;
align-items: center;
gap: 12px;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
}
.header-actions {
display: flex;
gap: 8px;
}
}
.detail-content {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.section-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 24px;
.section-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
}
.section-body {
padding: 20px 24px;
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
.info-label {
font-size: 12px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
}
.info-full {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #f0f0f0;
display: flex;
flex-direction: column;
gap: 4px;
.info-label {
font-size: 12px;
color: #666;
}
.info-value {
font-size: 14px;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
}
}
/* 课程卡片 */
.lesson-cards {
display: grid;
gap: 16px;
}
.lesson-card {
border: 1px solid #e8e8e8;
border-radius: 12px;
overflow: hidden;
.lesson-header {
padding: 12px 16px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.lesson-body {
padding: 16px;
}
.lesson-section {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.lesson-section-title {
font-size: 12px;
color: #666;
margin-bottom: 4px;
&.change-note {
color: #1890ff;
}
}
.lesson-section-content {
font-size: 13px;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
&.highlighted {
background: #e6f7ff;
padding: 8px 12px;
border-radius: 4px;
border-left: 3px solid #1890ff;
}
}
}
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
/* 预约弹窗 */
.reserve-modal {
.course-info {
background: #f9f9f9;
border-radius: 8px;
padding: 12px 16px;
display: flex;
gap: 8px;
.label {
color: #666;
}
.value {
font-weight: 500;
}
}
}
</style>