前端接口修复

This commit is contained in:
zhonghua 2026-03-11 18:19:11 +08:00
parent 97fec4f450
commit 4f717dc8d7
8 changed files with 176 additions and 279 deletions

View File

@ -420,10 +420,11 @@ export interface FeedbackQueryParams {
}
export interface FeedbackStats {
totalFeedbacks: number;
avgDesignQuality: number;
avgParticipation: number;
avgGoalAchievement: number;
totalFeedbacks?: number;
avgDesignQuality?: number;
avgParticipation?: number;
avgGoalAchievement?: number;
courseStats: Record<number, { count: number; avgRating: number }>;
}

View File

@ -17,6 +17,7 @@ declare module 'vue' {
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
@ -25,6 +26,7 @@ declare module 'vue' {
AImage: typeof import('ant-design-vue/es')['Image']
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
@ -50,14 +52,19 @@ declare module 'vue' {
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
AUpload: typeof import('ant-design-vue/es')['Upload']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']

View File

@ -13,11 +13,7 @@
</div>
</div>
<div class="header-actions">
<a-range-picker
v-model:value="dateRange"
:placeholder="['开始日期', '结束日期']"
style="width: 240px;"
/>
<a-range-picker v-model:value="dateRange" :placeholder="['开始日期', '结束日期']" style="width: 240px;" />
<a-button class="export-btn">
<DownloadOutlined class="btn-icon" />
导出报告
@ -71,13 +67,8 @@
<!-- 标签页 -->
<div class="report-tabs">
<div class="tab-header">
<div
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ 'active': activeTab === tab.key }"
@click="activeTab = tab.key"
>
<div v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ 'active': activeTab === tab.key }"
@click="activeTab = tab.key">
<BarChartOutlined v-if="tab.icon === 'bar-chart'" class="tab-icon" />
<SolutionOutlined v-else-if="tab.icon === 'solution'" class="tab-icon" />
<ReadOutlined v-else-if="tab.icon === 'read'" class="tab-icon" />
@ -143,8 +134,9 @@
<div class="teacher-info">
<div class="teacher-name">{{ teacher.name }}</div>
<div class="teacher-rating">
<StarFilled v-for="i in 5" :key="i" class="star" :class="{ 'filled': i <= Math.round(teacher.avgRating) }" />
<span class="rating-value">{{ teacher.avgRating.toFixed(1) }}</span>
<StarFilled v-for="i in 5" :key="i" class="star"
:class="{ 'filled': i <= Math.round(teacher.avgRating) }" />
<span class="rating-value">{{ (teacher.avgRating || 0).toFixed(1) }}</span>
</div>
</div>
</div>
@ -187,14 +179,20 @@
<div class="course-info">
<div class="course-name">{{ course.name }}</div>
<div class="course-stats-inline">
<span><BookOutlined style="margin-right: 4px;" />授课{{ course.lessonCount }}</span>
<span><SolutionOutlined style="margin-right: 4px;" />{{ course.teacherCount }}位教师</span>
<span><TeamOutlined style="margin-right: 4px;" />{{ course.studentCount }}名学生</span>
<span>
<BookOutlined style="margin-right: 4px;" />授课{{ course.lessonCount }}
</span>
<span>
<SolutionOutlined style="margin-right: 4px;" />{{ course.teacherCount }}位教师
</span>
<span>
<TeamOutlined style="margin-right: 4px;" />{{ course.studentCount }}名学生
</span>
</div>
</div>
<div class="course-rating">
<StarFilled class="rating-stars" />
<span class="rating-value">{{ course.avgRating.toFixed(1) }}</span>
<span class="rating-value">{{ (course.avgRating || 0).toFixed(1) }}</span>
</div>
<div class="course-action">
<a-button type="link" size="small" @click="viewCourseDetail(course)">
@ -221,18 +219,24 @@
</div>
<div class="student-stats">
<div class="stat-row">
<span class="stat-label"><BookOutlined style="margin-right: 4px;" />参与课程</span>
<span class="stat-label">
<BookOutlined style="margin-right: 4px;" />参与课程
</span>
<span class="stat-value">{{ student.lessonCount }} </span>
</div>
<div class="stat-row">
<span class="stat-label"><AimOutlined style="margin-right: 4px;" />专注度</span>
<span class="stat-label">
<AimOutlined style="margin-right: 4px;" />专注度
</span>
<div class="progress-mini">
<div class="progress-fill" :style="{ width: student.avgFocus * 20 + '%' }"></div>
</div>
<span class="stat-value">{{ student.avgFocus }}/5</span>
</div>
<div class="stat-row">
<span class="stat-label"><FireOutlined style="margin-right: 4px;" />参与度</span>
<span class="stat-label">
<FireOutlined style="margin-right: 4px;" />参与度
</span>
<div class="progress-mini pink">
<div class="progress-fill" :style="{ width: student.avgParticipation * 20 + '%' }"></div>
</div>
@ -247,12 +251,8 @@
</div>
<!-- 教师详情弹窗 -->
<a-modal
v-model:open="teacherDetailVisible"
:title="`${selectedTeacher?.name} - 教师报告详情`"
width="600px"
:footer="null"
>
<a-modal v-model:open="teacherDetailVisible" :title="`${selectedTeacher?.name} - 教师报告详情`" width="600px"
:footer="null">
<div v-if="selectedTeacher" class="detail-content">
<div class="detail-header">
<div class="detail-avatar">
@ -262,7 +262,7 @@
<h3>{{ selectedTeacher.name }}</h3>
<div class="detail-rating">
<StarFilled v-for="i in 5" :key="i" :class="{ 'filled': i <= Math.round(selectedTeacher.avgRating) }" />
<span class="rating-text">{{ selectedTeacher.avgRating.toFixed(1) }} </span>
<span class="rating-text">{{ (selectedTeacher.avgRating || 0).toFixed(1) }} </span>
</div>
</div>
</div>
@ -283,14 +283,17 @@
</div>
<a-divider />
<div class="detail-section">
<h4><BookOutlined style="margin-right: 8px;" />教学概况</h4>
<h4>
<BookOutlined style="margin-right: 8px;" />教学概况
</h4>
<p class="detail-desc">
{{ selectedTeacher.name }} 老师共完成 {{ selectedTeacher.lessonCount }} 次授课
使用了 {{ selectedTeacher.courseCount }} 门不同的课程
累计获得 {{ selectedTeacher.feedbackCount }} 次教学反馈
<template v-if="selectedTeacher.avgRating > 0">
平均评分为 {{ selectedTeacher.avgRating.toFixed(1) }}
{{ selectedTeacher.avgRating >= 4.5 ? '教学效果优秀!' : selectedTeacher.avgRating >= 3.5 ? '教学效果良好。' : '继续努力!' }}
平均评分为 {{ (selectedTeacher.avgRating || 0).toFixed(1) }}
{{ selectedTeacher.avgRating >= 4.5 ? '教学效果优秀!' : selectedTeacher.avgRating >= 3.5 ? '教学效果良好。' : '继续努力!'
}}
</template>
<template v-else>
暂无评分数据
@ -301,12 +304,8 @@
</a-modal>
<!-- 课程详情弹窗 -->
<a-modal
v-model:open="courseDetailVisible"
:title="`${selectedCourse?.name} - 课程报告详情`"
width="600px"
:footer="null"
>
<a-modal v-model:open="courseDetailVisible" :title="`${selectedCourse?.name} - 课程报告详情`" width="600px"
:footer="null">
<div v-if="selectedCourse" class="detail-content">
<div class="detail-header">
<div class="detail-avatar course-avatar">
@ -316,7 +315,7 @@
<h3>{{ selectedCourse.name }}</h3>
<div class="detail-rating">
<StarFilled v-for="i in 5" :key="i" :class="{ 'filled': i <= Math.round(selectedCourse.avgRating) }" />
<span class="rating-text">{{ selectedCourse.avgRating.toFixed(1) }} </span>
<span class="rating-text">{{ (selectedCourse.avgRating || 0).toFixed(1) }} </span>
</div>
</div>
</div>
@ -337,13 +336,15 @@
</div>
<a-divider />
<div class="detail-section">
<h4><ReadOutlined style="margin-right: 8px;" />课程概况</h4>
<h4>
<ReadOutlined style="margin-right: 8px;" />课程概况
</h4>
<p class="detail-desc">
{{ selectedCourse.name }}共被授课 {{ selectedCourse.lessonCount }}
{{ selectedCourse.teacherCount }} 位教师使用该课程进行教学
累计覆盖 {{ selectedCourse.studentCount }} 名学生
<template v-if="selectedCourse.avgRating > 0">
课程平均评分为 {{ selectedCourse.avgRating.toFixed(1) }}
课程平均评分为 {{ (selectedCourse.avgRating || 0).toFixed(1) }}
{{ selectedCourse.avgRating >= 4.5 ? '深受师生好评!' : selectedCourse.avgRating >= 3.5 ? '反馈良好。' : '有待改进。' }}
</template>
<template v-else>
@ -412,7 +413,7 @@ const studentData = ref<StudentReport[]>([]);
const totalLessons = computed(() => overviewData.value.totalLessons);
const activeTeacherCount = computed(() => overviewData.value.activeTeacherCount);
const usedCourseCount = computed(() => overviewData.value.usedCourseCount);
const avgRating = computed(() => overviewData.value.avgRating.toFixed(1));
const avgRating = computed(() => (overviewData.value.avgRating || 0).toFixed(1));
//
const loadData = async () => {

View File

@ -18,15 +18,15 @@
<span class="stat-label">反馈总数</span>
</div>
<div class="stat-item">
<span class="stat-value quality">{{ stats.avgDesignQuality.toFixed(1) }}</span>
<span class="stat-value quality">{{ (stats.avgDesignQuality || 0).toFixed(1) }}</span>
<span class="stat-label">设计质量</span>
</div>
<div class="stat-item">
<span class="stat-value participation">{{ stats.avgParticipation.toFixed(1) }}</span>
<span class="stat-value participation">{{ (stats.avgParticipation || 0).toFixed(1) }}</span>
<span class="stat-label">参与度</span>
</div>
<div class="stat-item">
<span class="stat-value achievement">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
<span class="stat-value achievement">{{ (stats.avgGoalAchievement || 0).toFixed(1) }}</span>
<span class="stat-label">目标达成</span>
</div>
</div>
@ -36,34 +36,20 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<a-select
v-model:value="filters.teacherId"
placeholder="选择教师"
allow-clear
style="width: 150px;"
@change="handleFilter"
>
<a-select v-model:value="filters.teacherId" placeholder="选择教师" allow-clear style="width: 150px;"
@change="handleFilter">
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
</a-select>
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索课程名称"
style="width: 200px;"
@search="handleFilter"
allow-clear
/>
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称" style="width: 200px;"
@search="handleFilter" allow-clear />
</div>
</div>
<!-- 反馈卡片网格 -->
<div class="feedback-grid" v-if="!loading && feedbacks.length > 0">
<div
v-for="feedback in feedbacks"
:key="feedback.id"
class="feedback-card"
>
<div v-for="feedback in feedbacks" :key="feedback.id" class="feedback-card">
<div class="card-header">
<div class="course-info">
<div class="course-icon-wrapper">
@ -123,7 +109,8 @@
<div class="feedback-summary" v-if="feedback.pros || feedback.suggestions">
<p class="summary-text">
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros || feedback.suggestions || '').length > 60 ? '...' : '' }}
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros ||
feedback.suggestions || '').length > 60 ? '...' : '' }}
</p>
</div>
</div>
@ -154,23 +141,13 @@
<!-- 分页 -->
<div class="pagination-wrapper" v-if="feedbacks.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
<a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange" />
</div>
<!-- 反馈详情弹窗 -->
<a-modal
v-model:open="detailModalVisible"
width="700px"
:footer="null"
class="feedback-detail-modal"
>
<a-modal v-model:open="detailModalVisible" width="700px" :footer="null" class="feedback-detail-modal">
<template #title>
<div class="modal-title">
<MessageOutlined class="modal-title-icon" />
@ -186,9 +163,15 @@
<h3>{{ currentFeedback.lesson?.course?.name }}</h3>
<p class="picture-book-name">{{ currentFeedback.lesson?.course?.pictureBookName || '-' }}</p>
<div class="meta-tags">
<span class="meta-tag"><UserOutlined /> {{ currentFeedback.teacher?.name }}</span>
<span class="meta-tag"><HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}</span>
<span class="meta-tag"><CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}</span>
<span class="meta-tag">
<UserOutlined /> {{ currentFeedback.teacher?.name }}
</span>
<span class="meta-tag">
<HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}
</span>
<span class="meta-tag">
<CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}
</span>
</div>
</div>
</div>

View File

@ -55,21 +55,12 @@
<span>校本课程包列表</span>
</template>
<template #extra>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索课程包名称"
style="width: 200px"
@search="handleSearch"
/>
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程包名称" style="width: 200px"
@search="handleSearch" />
</template>
<a-table
:columns="columns"
:data-source="filteredData"
:loading="loading"
row-key="id"
:pagination="{ pageSize: 10 }"
>
<a-table :columns="columns" :data-source="filteredData" :loading="loading" row-key="id"
:pagination="{ pageSize: 10 }">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="course-name">
@ -81,11 +72,8 @@
</template>
<template v-else-if="column.key === 'sourceCourse'">
<div class="source-info">
<img
v-if="record.sourceCourse?.coverImagePath"
:src="getFileUrl(record.sourceCourse.coverImagePath)"
class="cover"
/>
<img v-if="record.sourceCourse?.coverImagePath" :src="getFileUrl(record.sourceCourse.coverImagePath)"
class="cover" />
<div v-else class="cover-placeholder">
<BookOutlined />
</div>
@ -143,13 +131,8 @@
</a-card>
<!-- 预约弹窗 -->
<a-modal
v-model:open="reserveModalVisible"
title="预约校本课程包"
width="500px"
@ok="handleReserve"
:confirmLoading="reserveLoading"
>
<a-modal v-model:open="reserveModalVisible" title="预约校本课程包" width="500px" @ok="handleReserve"
:confirmLoading="reserveLoading">
<div class="reserve-modal" v-if="selectedCourse">
<div class="course-info">
<span class="label">课程包名称</span>
@ -174,35 +157,21 @@
</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-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>
<a-alert
v-if="conflictInfo"
:type="conflictInfo.hasConflict ? 'error' : 'success'"
:message="conflictInfo.message"
show-icon
/>
<a-alert v-if="conflictInfo" :type="conflictInfo.hasConflict ? 'error' : 'success'"
:message="conflictInfo.message" show-icon />
</div>
</a-modal>
<!-- 排课弹窗 -->
<a-modal
v-model:open="scheduleModalVisible"
title="排课管理"
width="800px"
:footer="null"
>
<a-modal v-model:open="scheduleModalVisible" title="排课管理" width="800px" :footer="null">
<div class="schedule-modal" v-if="selectedCourse">
<div class="course-info-header">
<span>课程包{{ selectedCourse.name }}</span>
@ -213,14 +182,8 @@
<a-tabs v-model:activeKey="scheduleTab">
<a-tab-pane key="upcoming" tab="即将上课">
<a-table
:columns="reservationColumns"
:data-source="upcomingReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="false"
>
<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 === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
@ -237,14 +200,8 @@
</div>
</a-tab-pane>
<a-tab-pane key="history" tab="历史记录">
<a-table
:columns="reservationColumns"
:data-source="historyReservations"
:loading="reservationLoading"
row-key="id"
size="small"
:pagination="{ pageSize: 5 }"
>
<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 === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
@ -390,8 +347,8 @@ const getStatusText = (status: string) => {
const fetchData = async () => {
loading.value = true;
try {
const res = await getSchoolCourseList() as any;
dataSource.value = res || [];
const { items } = await getSchoolCourseList() as any;
dataSource.value = items || [];
} catch (error) {
console.error('获取校本课程包列表失败', error);
} finally {

View File

@ -111,7 +111,8 @@
<span>今日课程</span>
</div>
<a-button type="link" class="view-all-btn" @click="router.push('/teacher/lessons')">
查看全部 <RightOutlined />
查看全部
<RightOutlined />
</a-button>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
@ -124,12 +125,8 @@
<p class="empty-hint">快去课程中心备课吧</p>
</div>
<div v-else class="lesson-list">
<div
v-for="lesson in todayLessons"
:key="lesson.id"
class="lesson-item"
:class="{ 'finished': lesson.status === 'FINISHED' }"
>
<div v-for="lesson in todayLessons" :key="lesson.id" class="lesson-item"
:class="{ 'finished': lesson.status === 'FINISHED' }">
<div class="lesson-time">
<div class="time-value">{{ formatTime(lesson.plannedDatetime) }}</div>
<div class="time-duration">{{ lesson.duration }}分钟</div>
@ -142,18 +139,10 @@
</div>
</div>
<div class="lesson-action">
<button
v-if="lesson.status === 'FINISHED'"
class="action-btn finished"
disabled
>
<button v-if="lesson.status === 'FINISHED'" class="action-btn finished" disabled>
<CheckOutlined /> 已结束
</button>
<button
v-else
class="action-btn start"
@click="startLesson(lesson)"
>
<button v-else class="action-btn start" @click="startLesson(lesson)">
<PlayCircleOutlined /> 开始上课
</button>
</div>
@ -173,7 +162,8 @@
<span>推荐课程</span>
</div>
<a-button type="link" class="view-all-btn" @click="router.push('/teacher/courses')">
查看全部 <RightOutlined />
查看全部
<RightOutlined />
</a-button>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
@ -185,18 +175,10 @@
<p class="empty-text">暂无推荐课程</p>
</div>
<div v-else class="recommend-list">
<div
v-for="course in recommendedCourses"
:key="course.id"
class="recommend-item"
@click="viewCourse(course)"
>
<div v-for="course in recommendedCourses" :key="course.id" class="recommend-item"
@click="viewCourse(course)">
<div class="recommend-cover">
<img
v-if="course.coverImagePath"
:src="getImageUrl(course.coverImagePath)"
class="cover-img"
/>
<img v-if="course.coverImagePath" :src="getImageUrl(course.coverImagePath)" class="cover-img" />
<div v-else class="cover-placeholder">
<BookFilled />
</div>
@ -209,7 +191,7 @@
<FireOutlined /> {{ course.usageCount }}次使用
</span>
<span v-if="course.avgRating > 0" class="meta-item">
<StarFilled class="star-icon" /> {{ course.avgRating.toFixed(1) }}
<StarFilled class="star-icon" /> {{ (course.avgRating || 0).toFixed(1) }}
</span>
</div>
</div>
@ -240,12 +222,8 @@
<p class="empty-text">暂无近期活动</p>
</div>
<div v-else class="activity-timeline">
<div
v-for="(item, index) in recentActivities"
:key="item.id"
class="activity-item"
:class="'type-' + item.type"
>
<div v-for="(item, index) in recentActivities" :key="item.id" class="activity-item"
:class="'type-' + item.type">
<div class="activity-dot">
<component :is="getActivityIcon(item.type)" />
</div>
@ -873,10 +851,21 @@ onUnmounted(() => {
margin-top: 4px;
}
.stat-card.class-card { border-left: 4px solid #FF8C42; }
.stat-card.student-card { border-left: 4px solid #52c41a; }
.stat-card.lesson-card { border-left: 4px solid #1890ff; }
.stat-card.course-card { border-left: 4px solid #722ed1; }
.stat-card.class-card {
border-left: 4px solid #FF8C42;
}
.stat-card.student-card {
border-left: 4px solid #52c41a;
}
.stat-card.lesson-card {
border-left: 4px solid #1890ff;
}
.stat-card.course-card {
border-left: 4px solid #722ed1;
}
/* 图表区域 */
.charts-section {

View File

@ -22,13 +22,8 @@
<div class="filter-bar">
<div class="filter-item">
<span class="filter-label">年级</span>
<a-select
v-model:value="filters.grade"
placeholder="全部年级"
style="width: 120px;"
allowClear
@change="handleFilterChange"
>
<a-select v-model:value="filters.grade" placeholder="全部年级" style="width: 120px;" allowClear
@change="handleFilterChange">
<a-select-option value="小班">
小班
</a-select-option>
@ -43,13 +38,8 @@
</div>
<div class="filter-item">
<span class="filter-label">领域</span>
<a-select
v-model:value="filters.domain"
placeholder="全部领域"
style="width: 120px;"
allowClear
@change="handleFilterChange"
>
<a-select v-model:value="filters.domain" placeholder="全部领域" style="width: 120px;" allowClear
@change="handleFilterChange">
<a-select-option value="健康">健康</a-select-option>
<a-select-option value="语言">语言</a-select-option>
<a-select-option value="社会">社会</a-select-option>
@ -58,26 +48,24 @@
</a-select>
</div>
<div class="filter-item search-box">
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索课程名称..."
style="width: 240px;"
@search="handleFilterChange"
>
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称..." style="width: 240px;"
@search="handleFilterChange">
<template #prefix>
<SearchOutlined style="color: #FF8C42;" />
</template>
</a-input-search>
</div>
<div class="filter-item filter-right">
<a-select
v-model:value="filters.sort"
style="width: 130px;"
@change="handleFilterChange"
>
<a-select-option value="popular"><FireOutlined /> 最受欢迎</a-select-option>
<a-select-option value="newest"><StarOutlined /> 最新发布</a-select-option>
<a-select-option value="rating"><StarFilled /> 评分最高</a-select-option>
<a-select v-model:value="filters.sort" style="width: 130px;" @change="handleFilterChange">
<a-select-option value="popular">
<FireOutlined /> 最受欢迎
</a-select-option>
<a-select-option value="newest">
<StarOutlined /> 最新发布
</a-select-option>
<a-select-option value="rating">
<StarFilled /> 评分最高
</a-select-option>
</a-select>
</div>
</div>
@ -85,27 +73,22 @@
<!-- 课程列表 -->
<a-spin :spinning="loading">
<div class="course-grid">
<div
v-for="course in courses"
:key="course.id"
class="course-card"
@click="viewCourseDetail(course)"
>
<div v-for="course in courses" :key="course.id" class="course-card" @click="viewCourseDetail(course)">
<!-- 封面区域 -->
<div class="course-cover">
<img
v-if="course.pictureUrl"
:src="getImageUrl(course.pictureUrl)"
class="cover-image"
/>
<img v-if="course.pictureUrl" :src="getImageUrl(course.pictureUrl)" class="cover-image" />
<div v-else class="cover-placeholder">
<div class="placeholder-icon"><BookFilled /></div>
<div class="placeholder-icon">
<BookFilled />
</div>
<div class="placeholder-text">精彩绘本</div>
</div>
<!-- 评分徽章 -->
<div class="rating-badge" v-if="course.avgRating > 0">
<span class="rating-star"><StarFilled /></span>
<span class="rating-value">{{ course.avgRating.toFixed(1) }}</span>
<span class="rating-star">
<StarFilled />
</span>
<span class="rating-value">{{ (course.avgRating || 0).toFixed(1) }}</span>
</div>
</div>
@ -118,18 +101,10 @@
<!-- 标签区域 -->
<div class="course-tags">
<a-tag
v-for="tag in course.gradeTags"
:key="'g-' + tag"
:style="getGradeTagStyle(tag)"
>
<a-tag v-for="tag in course.gradeTags" :key="'g-' + tag" :style="getGradeTagStyle(tag)">
{{ tag }}
</a-tag>
<a-tag
v-for="tag in course.domainTags"
:key="'d-' + tag"
:style="getDomainTagStyle(tag)"
>
<a-tag v-for="tag in course.domainTags" :key="'d-' + tag" :style="getDomainTagStyle(tag)">
{{ tag }}
</a-tag>
</div>
@ -148,7 +123,9 @@
<!-- 操作按钮 -->
<button class="prepare-btn" @click.stop="prepareCourse(course)">
<span class="btn-icon"><EditOutlined /></span>
<span class="btn-icon">
<EditOutlined />
</span>
开始备课
</button>
</div>
@ -166,13 +143,8 @@
<!-- 分页 -->
<div class="pagination-wrapper" v-if="pagination.total > pagination.pageSize">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
show-less-items
@change="handlePageChange"
/>
<a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
:total="pagination.total" show-less-items @change="handlePageChange" />
</div>
</a-spin>
</div>

View File

@ -18,15 +18,15 @@
<span class="stat-label">反馈总数</span>
</div>
<div class="stat-item">
<span class="stat-value quality">{{ stats.avgDesignQuality.toFixed(1) }}</span>
<span class="stat-value quality">{{ (stats.avgDesignQuality || 0).toFixed(1) }}</span>
<span class="stat-label">设计质量</span>
</div>
<div class="stat-item">
<span class="stat-value participation">{{ stats.avgParticipation.toFixed(1) }}</span>
<span class="stat-value participation">{{ (stats.avgParticipation || 0).toFixed(1) }}</span>
<span class="stat-label">参与度</span>
</div>
<div class="stat-item">
<span class="stat-value achievement">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
<span class="stat-value achievement">{{ (stats.avgGoalAchievement || 0).toFixed(1) }}</span>
<span class="stat-label">目标达成</span>
</div>
</div>
@ -36,23 +36,14 @@
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<a-input-search
v-model:value="filters.keyword"
placeholder="搜索课程名称"
style="width: 200px;"
@search="handleFilter"
allow-clear
/>
<a-input-search v-model:value="filters.keyword" placeholder="搜索课程名称" style="width: 200px;"
@search="handleFilter" allow-clear />
</div>
</div>
<!-- 反馈卡片网格 -->
<div class="feedback-grid" v-if="!loading && feedbacks.length > 0">
<div
v-for="feedback in feedbacks"
:key="feedback.id"
class="feedback-card"
>
<div v-for="feedback in feedbacks" :key="feedback.id" class="feedback-card">
<div class="card-header">
<div class="course-info">
<div class="course-icon-wrapper">
@ -107,7 +98,8 @@
<div class="feedback-summary" v-if="feedback.pros || feedback.suggestions">
<p class="summary-text">
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros || feedback.suggestions || '').length > 60 ? '...' : '' }}
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros ||
feedback.suggestions || '').length > 60 ? '...' : '' }}
</p>
</div>
</div>
@ -138,23 +130,13 @@
<!-- 分页 -->
<div class="pagination-wrapper" v-if="feedbacks.length > 0">
<a-pagination
v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
<a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange" />
</div>
<!-- 反馈详情弹窗 -->
<a-modal
v-model:open="detailModalVisible"
width="700px"
:footer="null"
class="feedback-detail-modal"
>
<a-modal v-model:open="detailModalVisible" width="700px" :footer="null" class="feedback-detail-modal">
<template #title>
<div class="modal-title">
<MessageOutlined class="modal-title-icon" />
@ -170,8 +152,12 @@
<h3>{{ currentFeedback.lesson?.course?.name }}</h3>
<p class="picture-book-name">{{ currentFeedback.lesson?.course?.pictureBookName || '-' }}</p>
<div class="meta-tags">
<span class="meta-tag"><HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}</span>
<span class="meta-tag"><CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}</span>
<span class="meta-tag">
<HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}
</span>
<span class="meta-tag">
<CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}
</span>
</div>
</div>
</div>
@ -319,6 +305,7 @@ const fetchFeedbacks = async () => {
const fetchStats = async () => {
try {
stats.value = await getTeacherFeedbackStats();
console.log('fetchStats', stats.value);
} catch (error) {
console.error('Failed to fetch stats:', error);
}