优化内容: - 参考课程管理页实现,将各页 a-input-search 改为 :enter-button="false", 使搜索图标在输入框内与边框一体,视觉为一个整体 - 教师/学生/家长/班级管理:移除自定义 #prefix(SearchOutlined), 避免双放大镜或图标与输入框分离,并移除未使用的 SearchOutlined 导入 - 家长管理:主列表搜索 + 弹窗内学生搜索均使用一体式搜索框 - 课程管理:授权弹窗内课程搜索去掉 prefix,增加 allow-clear 与 :enter-button="false" - 校本课程包、阅读任务、课程反馈、成长档案、任务模板:为搜索框增加 :enter-button="false" 涉及页面: - 教师管理 TeacherListView - 学生管理 StudentListView - 家长管理 ParentListView - 班级管理 ClassListView - 课程管理 CourseListView(含授权弹窗) - 校本课程包 SchoolCourseListView - 阅读任务 TaskListView - 课程反馈 FeedbackView - 成长档案 GrowthRecordView - 任务模板 TaskTemplateView Made-with: Cursor
398 lines
16 KiB
Vue
398 lines
16 KiB
Vue
<template>
|
|
<div class="min-h-100vh bg-[linear-gradient(180deg,#FFF8F0_0%,#FFFFFF_100%)] px-4 py-4 md:px-6 md:py-6">
|
|
<!-- 页面头部 -->
|
|
<div class="rounded-[20px] py-6 px-8 mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
<div class="flex justify-between items-center gap-4 max-md:flex-col max-md:items-start">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-16 h-16 rounded-2xl flex items-center justify-center text-[32px] text-white bg-white/20">
|
|
<MessageOutlined />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-white text-2xl font-700 m-0">课程反馈</h2>
|
|
<p class="text-white/80 text-sm mt-1 m-0">查看教师课程反馈与评分情况</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-8">
|
|
<div class="text-center">
|
|
<span class="block text-[28px] font-700 text-white">{{ stats.totalFeedbacks }}</span>
|
|
<span class="text-xs text-white/80">反馈总数</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<span class="block text-[28px] font-700 text-[#FFD93D]">{{ stats.avgDesignQuality.toFixed(1) }}</span>
|
|
<span class="text-xs text-white/80">设计质量</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<span class="block text-[28px] font-700 text-[#74b9ff]">{{ stats.avgParticipation.toFixed(1) }}</span>
|
|
<span class="text-xs text-white/80">参与度</span>
|
|
</div>
|
|
<div class="text-center">
|
|
<span class="block text-[28px] font-700 text-[#FFD93D]">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
|
|
<span class="text-xs text-white/80">目标达成</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 操作栏 -->
|
|
<div
|
|
class="flex justify-between items-center gap-3 flex-wrap mb-6 py-4 px-5 bg-white rounded-2xl shadow-[0_2px_8px_rgba(0,0,0,0.04)] filter-bar max-md:flex-col max-md:items-stretch"
|
|
>
|
|
<div class="flex gap-3 flex-wrap w-full md:w-auto">
|
|
<a-select
|
|
v-model:value="filters.teacherId"
|
|
placeholder="选择教师"
|
|
allow-clear
|
|
class="w-full md:w-[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="搜索课程名称"
|
|
class="w-full md:w-[200px]"
|
|
@search="handleFilter"
|
|
allow-clear
|
|
:enter-button="false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 反馈卡片网格 -->
|
|
<div class="grid gap-5 mb-6 feedback-grid" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));" v-if="!loading && feedbacks.length > 0">
|
|
<div
|
|
v-for="feedback in feedbacks"
|
|
:key="feedback.id"
|
|
class="bg-white rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.05)] transition-all duration-300 border-t-4 border-t-[#11998e] hover:translate-y-[-4px] hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)]"
|
|
>
|
|
<div class="flex justify-between items-start py-4 px-4 bg-[linear-gradient(135deg,#F8F9FA_0%,#FFFFFF_100%)] border-b border-[#F0F0F0]">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white text-[22px] bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
<BookOutlined />
|
|
</div>
|
|
<div>
|
|
<h4 class="text-[15px] font-600 text-[#2D3436] m-0">{{ feedback.lesson?.course?.name }}</h4>
|
|
<p class="text-xs text-[#636E72] mt-1 m-0">{{ feedback.lesson?.course?.pictureBookName || '-' }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1 text-[11px] text-[#B2BEC3]">
|
|
<ClockCircleOutlined />
|
|
<span>{{ formatDate(feedback.lesson?.startDatetime) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-4">
|
|
<div class="flex items-center gap-3 mb-4 p-3 bg-[#F8F9FA] rounded-xl">
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center text-base font-600 text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
{{ feedback.teacher?.name?.charAt(0) || '师' }}
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span class="text-sm font-600 text-[#2D3436]">{{ feedback.teacher?.name }}</span>
|
|
<span class="text-xs text-[#636E72]">{{ feedback.lesson?.class?.name || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-2.5 mb-3">
|
|
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
|
|
<div class="flex items-center gap-1.5">
|
|
<BgColorsOutlined class="text-sm text-[#667eea]" />
|
|
<span class="text-xs text-[#636E72]">设计质量</span>
|
|
</div>
|
|
<a-rate :value="feedback.designQuality" disabled :count="5" class="text-sm" />
|
|
</div>
|
|
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
|
|
<div class="flex items-center gap-1.5">
|
|
<TeamOutlined class="text-sm text-[#f5576c]" />
|
|
<span class="text-xs text-[#636E72]">参与度</span>
|
|
</div>
|
|
<a-rate :value="feedback.participation" disabled :count="5" class="text-sm" />
|
|
</div>
|
|
<div class="flex justify-between items-center py-2 px-3 bg-[#FAFAFA] rounded-lg">
|
|
<div class="flex items-center gap-1.5">
|
|
<AimOutlined class="text-sm text-[#FF8C42]" />
|
|
<span class="text-xs text-[#636E72]">目标达成</span>
|
|
</div>
|
|
<a-rate :value="feedback.goalAchievement" disabled :count="5" class="text-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-3 border-t border-[#F0F0F0]" v-if="feedback.pros || feedback.suggestions">
|
|
<p class="text-[13px] text-[#636E72] leading-normal m-0">
|
|
{{ (feedback.pros || feedback.suggestions || '')?.substring(0, 60) }}{{ (feedback.pros || feedback.suggestions || '').length > 60 ? '...' : '' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end py-3 px-4 border-t border-[#F0F0F0] bg-[#FAFAFA]">
|
|
<a-button type="link" size="small" @click="handleView(feedback)">
|
|
<FileTextOutlined />
|
|
查看详情
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 空状态 -->
|
|
<div class="flex flex-col items-center justify-center py-20 bg-white rounded-2xl" v-if="!loading && feedbacks.length === 0">
|
|
<div class="w-20 h-20 rounded-full flex items-center justify-center mb-4 text-[36px] text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
<MessageOutlined />
|
|
</div>
|
|
<p class="text-[#636E72] text-base mb-2">暂无课程反馈</p>
|
|
<p class="text-[13px] text-[#B2BEC3]">教师完成课程后会在这里显示反馈记录</p>
|
|
</div>
|
|
|
|
<!-- 加载状态 -->
|
|
<div class="flex flex-col items-center justify-center py-20" v-if="loading">
|
|
<a-spin size="large" />
|
|
<p class="text-[#636E72] mt-4">加载中...</p>
|
|
</div>
|
|
|
|
<!-- 分页 -->
|
|
<div class="flex justify-center py-6 bg-white rounded-2xl" 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"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 反馈详情弹窗 -->
|
|
<a-modal
|
|
v-model:open="detailModalVisible"
|
|
width="700px"
|
|
:footer="null"
|
|
>
|
|
<template #title>
|
|
<div class="flex items-center gap-2">
|
|
<MessageOutlined class="text-[#11998e] text-lg" />
|
|
<span>{{ currentFeedback?.lesson?.course?.name || '反馈详情' }}</span>
|
|
</div>
|
|
</template>
|
|
<div class="p-0" v-if="currentFeedback">
|
|
<div class="flex gap-5 py-5 px-5 rounded-2xl mb-6 bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
<div class="w-20 h-20 rounded-xl flex items-center justify-center bg-white/20">
|
|
<ReadOutlined class="text-[36px] text-white" />
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="text-white text-lg font-600 m-0 mb-1">{{ currentFeedback.lesson?.course?.name }}</h3>
|
|
<p class="text-white/80 text-[13px] m-0 mb-3">{{ currentFeedback.lesson?.course?.pictureBookName || '-' }}</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><UserOutlined /> {{ currentFeedback.teacher?.name }}</span>
|
|
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><HomeOutlined /> {{ currentFeedback.lesson?.class?.name }}</span>
|
|
<span class="py-1 px-2.5 rounded-xl text-xs text-white bg-white/20 inline-flex items-center gap-1"><CalendarOutlined /> {{ formatDate(currentFeedback.lesson?.startDatetime) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-3 gap-4 mb-6 detail-ratings">
|
|
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
|
|
<div class="mb-3">
|
|
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.designQuality || 0 }}</span>
|
|
<span class="text-sm text-[#B2BEC3]">/5</span>
|
|
</div>
|
|
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
|
|
<BgColorsOutlined />
|
|
</div>
|
|
<div class="text-sm font-600 text-[#2D3436] mb-2">设计质量</div>
|
|
<a-rate :value="currentFeedback.designQuality" disabled :count="5" class="text-xs" />
|
|
</div>
|
|
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
|
|
<div class="mb-3">
|
|
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.participation || 0 }}</span>
|
|
<span class="text-sm text-[#B2BEC3]">/5</span>
|
|
</div>
|
|
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#f093fb_0%,#f5576c_100%)]">
|
|
<TeamOutlined />
|
|
</div>
|
|
<div class="text-sm font-600 text-[#2D3436] mb-2">参与度</div>
|
|
<a-rate :value="currentFeedback.participation" disabled :count="5" class="text-xs" />
|
|
</div>
|
|
<div class="text-center py-5 px-4 bg-[#F8F9FA] rounded-2xl">
|
|
<div class="mb-3">
|
|
<span class="text-[32px] font-700 text-[#2D3436]">{{ currentFeedback.goalAchievement || 0 }}</span>
|
|
<span class="text-sm text-[#B2BEC3]">/5</span>
|
|
</div>
|
|
<div class="w-12 h-12 mx-auto mb-3 rounded-full flex items-center justify-center text-[22px] text-white bg-[linear-gradient(135deg,#FF8C42_0%,#FFB347_100%)]">
|
|
<AimOutlined />
|
|
</div>
|
|
<div class="text-sm font-600 text-[#2D3436] mb-2">目标达成</div>
|
|
<a-rate :value="currentFeedback.goalAchievement" disabled :count="5" class="text-xs" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.pros">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#FFD93D_0%,#FF9500_100%)]">
|
|
<StarFilled />
|
|
</div>
|
|
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">课程优点</h4>
|
|
</div>
|
|
<p class="text-sm leading-[1.8] text-[#636E72] m-0">{{ currentFeedback.pros }}</p>
|
|
</div>
|
|
|
|
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.suggestions">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#667eea_0%,#764ba2_100%)]">
|
|
<BulbOutlined />
|
|
</div>
|
|
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">改进建议</h4>
|
|
</div>
|
|
<p class="text-sm leading-[1.8] text-[#636E72] m-0">{{ currentFeedback.suggestions }}</p>
|
|
</div>
|
|
|
|
<div class="mb-5 p-4 bg-[#F8F9FA] rounded-xl" v-if="currentFeedback.activitiesDone && currentFeedback.activitiesDone.length">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm text-white bg-[linear-gradient(135deg,#11998e_0%,#38ef7d_100%)]">
|
|
<TrophyOutlined />
|
|
</div>
|
|
<h4 class="m-0 text-[15px] font-600 text-[#2D3436]">完成的活动</h4>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a-tag v-for="(activity, index) in currentFeedback.activitiesDone" :key="index" color="green">
|
|
{{ activity }}
|
|
</a-tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted } from 'vue';
|
|
import { message } from 'ant-design-vue';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
MessageOutlined,
|
|
BookOutlined,
|
|
ClockCircleOutlined,
|
|
BgColorsOutlined,
|
|
TeamOutlined,
|
|
AimOutlined,
|
|
FileTextOutlined,
|
|
ReadOutlined,
|
|
UserOutlined,
|
|
HomeOutlined,
|
|
CalendarOutlined,
|
|
StarFilled,
|
|
BulbOutlined,
|
|
TrophyOutlined,
|
|
} from '@ant-design/icons-vue';
|
|
import { getSchoolFeedbacks, getFeedbackStats, type LessonFeedback, type FeedbackStats } from '@/api/teacher';
|
|
import { getTeachers, type Teacher } from '@/api/school';
|
|
|
|
const loading = ref(false);
|
|
const detailModalVisible = ref(false);
|
|
|
|
const feedbacks = ref<LessonFeedback[]>([]);
|
|
const teachers = ref<Teacher[]>([]);
|
|
const currentFeedback = ref<LessonFeedback | null>(null);
|
|
|
|
const stats = ref<FeedbackStats>({
|
|
totalFeedbacks: 0,
|
|
avgDesignQuality: 0,
|
|
avgParticipation: 0,
|
|
avgGoalAchievement: 0,
|
|
courseStats: {},
|
|
});
|
|
|
|
const filters = reactive({
|
|
teacherId: undefined as number | undefined,
|
|
keyword: '',
|
|
});
|
|
|
|
const pagination = reactive({
|
|
current: 1,
|
|
pageSize: 12,
|
|
total: 0,
|
|
});
|
|
|
|
const formatDate = (date?: string) => {
|
|
if (!date) return '-';
|
|
return dayjs(date).format('MM-DD HH:mm');
|
|
};
|
|
|
|
const fetchFeedbacks = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const result = await getSchoolFeedbacks({
|
|
page: pagination.current,
|
|
pageSize: pagination.pageSize,
|
|
teacherId: filters.teacherId,
|
|
});
|
|
feedbacks.value = result.items;
|
|
pagination.total = result.total;
|
|
} catch (error) {
|
|
message.error('获取反馈列表失败');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
stats.value = await getFeedbackStats();
|
|
} catch (error) {
|
|
console.error('Failed to fetch stats:', error);
|
|
}
|
|
};
|
|
|
|
const fetchTeachers = async () => {
|
|
try {
|
|
const result = await getTeachers({ pageSize: 100 });
|
|
teachers.value = result.items;
|
|
} catch (error) {
|
|
console.error('Failed to fetch teachers:', error);
|
|
}
|
|
};
|
|
|
|
const handlePageChange = (page: number, pageSize: number) => {
|
|
pagination.current = page;
|
|
pagination.pageSize = pageSize;
|
|
fetchFeedbacks();
|
|
};
|
|
|
|
const handleFilter = () => {
|
|
pagination.current = 1;
|
|
fetchFeedbacks();
|
|
};
|
|
|
|
const handleView = (record: LessonFeedback) => {
|
|
currentFeedback.value = record;
|
|
detailModalVisible.value = true;
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchFeedbacks();
|
|
fetchStats();
|
|
fetchTeachers();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
@media (max-width: 768px) {
|
|
.feedback-grid {
|
|
grid-template-columns: 1fr !important;
|
|
}
|
|
|
|
.detail-ratings {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.filter-bar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.filter-bar :deep(.ant-select),
|
|
.filter-bar :deep(.ant-input-search) {
|
|
width: 100% !important;
|
|
}
|
|
}
|
|
</style>
|