kindergarten_java/reading-platform-frontend/src/views/teacher/feedback/FeedbackView.vue

851 lines
19 KiB
Vue
Raw Normal View History

<template>
<div class="feedback-view">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-title">
<div class="title-icon-wrapper">
<MessageOutlined />
</div>
<div class="title-text">
<h2>课程反馈</h2>
<p>查看我的课程反馈与评分记录</p>
</div>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">{{ stats.totalFeedbacks }}</span>
<span class="stat-label">反馈总数</span>
</div>
<div class="stat-item">
<span class="stat-value quality">{{ stats.avgDesignQuality.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-label">参与度</span>
</div>
<div class="stat-item">
<span class="stat-value achievement">{{ stats.avgGoalAchievement.toFixed(1) }}</span>
<span class="stat-label">目标达成</span>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="filters">
<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 class="card-header">
<div class="course-info">
<div class="course-icon-wrapper">
<BookOutlined />
</div>
<div class="course-details">
<h4 class="course-name">{{ feedback.lesson?.course?.name }}</h4>
<p class="picture-book">{{ feedback.lesson?.course?.pictureBookName || '-' }}</p>
</div>
</div>
<div class="feedback-time">
<ClockCircleOutlined />
<span>{{ formatDate(feedback.lesson?.startDatetime) }}</span>
</div>
</div>
<div class="card-body">
<div class="class-info">
<TeamOutlined class="class-icon" />
<span class="class-name">{{ feedback.lesson?.class?.name || '-' }}</span>
</div>
<div class="ratings-grid">
<div class="rating-item">
<div class="rating-header">
<BgColorsOutlined class="rating-icon design-icon" />
<span class="rating-label">设计质量</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.designQuality" disabled :count="5" style="font-size: 14px;" />
</div>
</div>
<div class="rating-item">
<div class="rating-header">
<TeamOutlined class="rating-icon participation-icon" />
<span class="rating-label">参与度</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.participation" disabled :count="5" style="font-size: 14px;" />
</div>
</div>
<div class="rating-item">
<div class="rating-header">
<AimOutlined class="rating-icon achievement-icon" />
<span class="rating-label">目标达成</span>
</div>
<div class="rating-stars">
<a-rate :value="feedback.goalAchievement" disabled :count="5" style="font-size: 14px;" />
</div>
</div>
</div>
<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 ? '...' : '' }}
</p>
</div>
</div>
<div class="card-actions">
<a-button type="link" size="small" @click="handleView(feedback)">
<FileTextOutlined />
查看详情
</a-button>
</div>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && feedbacks.length === 0">
<div class="empty-icon-wrapper">
<MessageOutlined />
</div>
<p>暂无课程反馈</p>
<p class="empty-hint">完成课程并提交反馈后会在这里显示</p>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading">
<a-spin size="large" />
<p>加载中...</p>
</div>
<!-- 分页 -->
<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"
/>
</div>
<!-- 反馈详情弹窗 -->
<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" />
<span>{{ currentFeedback?.lesson?.course?.name || '反馈详情' }}</span>
</div>
</template>
<div class="detail-content" v-if="currentFeedback">
<div class="detail-header">
<div class="course-cover">
<ReadOutlined class="cover-icon" />
</div>
<div class="course-meta">
<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>
</div>
</div>
</div>
<div class="detail-ratings">
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.designQuality || 0 }}</span>
<span class="score-max">/5</span>
</div>
<div class="rating-icon-wrapper design">
<BgColorsOutlined />
</div>
<div class="rating-name">设计质量</div>
<a-rate :value="currentFeedback.designQuality" disabled :count="5" style="font-size: 12px;" />
</div>
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.participation || 0 }}</span>
<span class="score-max">/5</span>
</div>
<div class="rating-icon-wrapper participation">
<TeamOutlined />
</div>
<div class="rating-name">参与度</div>
<a-rate :value="currentFeedback.participation" disabled :count="5" style="font-size: 12px;" />
</div>
<div class="rating-card">
<div class="rating-score">
<span class="score-value">{{ currentFeedback.goalAchievement || 0 }}</span>
<span class="score-max">/5</span>
</div>
<div class="rating-icon-wrapper achievement">
<AimOutlined />
</div>
<div class="rating-name">目标达成</div>
<a-rate :value="currentFeedback.goalAchievement" disabled :count="5" style="font-size: 12px;" />
</div>
</div>
<div class="detail-section" v-if="currentFeedback.pros">
<div class="section-header">
<div class="section-icon-wrapper pros">
<StarFilled />
</div>
<h4>课程优点</h4>
</div>
<p class="section-content">{{ currentFeedback.pros }}</p>
</div>
<div class="detail-section" v-if="currentFeedback.suggestions">
<div class="section-header">
<div class="section-icon-wrapper suggestions">
<BulbOutlined />
</div>
<h4>改进建议</h4>
</div>
<p class="section-content">{{ currentFeedback.suggestions }}</p>
</div>
<div class="detail-section" v-if="currentFeedback.activitiesDone && currentFeedback.activitiesDone.length">
<div class="section-header">
<div class="section-icon-wrapper activities">
<TrophyOutlined />
</div>
<h4>完成的活动</h4>
</div>
<div class="activity-tags">
<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,
HomeOutlined,
CalendarOutlined,
StarFilled,
BulbOutlined,
TrophyOutlined,
} from '@ant-design/icons-vue';
import { getTeacherFeedbacks, getTeacherFeedbackStats, type LessonFeedback, type FeedbackStats } from '@/api/teacher';
const loading = ref(false);
const detailModalVisible = ref(false);
const feedbacks = ref<LessonFeedback[]>([]);
const currentFeedback = ref<LessonFeedback | null>(null);
const stats = ref<FeedbackStats>({
totalFeedbacks: 0,
avgDesignQuality: 0,
avgParticipation: 0,
avgGoalAchievement: 0,
courseStats: {},
});
const filters = reactive({
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 getTeacherFeedbacks({
pageNum: pagination.current,
pageSize: pagination.pageSize,
});
feedbacks.value = result.items;
pagination.total = Number(result.total);
} catch (error) {
message.error('获取反馈列表失败');
} finally {
loading.value = false;
}
};
const fetchStats = async () => {
try {
stats.value = await getTeacherFeedbackStats();
} catch (error) {
console.error('Failed to fetch stats:', 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();
});
</script>
<style scoped>
.feedback-view {
min-height: 100%;
padding: 0;
}
/* 页面头部 */
.page-header {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 16px;
padding: 24px 32px;
margin-bottom: 24px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-title {
display: flex;
align-items: center;
gap: 16px;
}
.title-icon-wrapper {
width: 64px;
height: 64px;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
}
.title-text h2 {
color: white;
font-size: 24px;
font-weight: 700;
margin: 0;
}
.title-text p {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 4px 0 0 0;
}
.header-stats {
display: flex;
gap: 32px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 28px;
font-weight: 700;
color: white;
}
.stat-value.quality {
color: #FFD93D;
}
.stat-value.participation {
color: #74b9ff;
}
.stat-value.achievement {
color: #FFD93D;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
/* 操作栏 */
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.filters {
display: flex;
gap: 12px;
}
/* 反馈卡片网格 */
.feedback-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.feedback-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border-top: 4px solid #FF8C42;
}
.feedback-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
background: linear-gradient(135deg, #FFF4EC 0%, #FFFFFF 100%);
border-bottom: 1px solid #F0F0F0;
}
.course-info {
display: flex;
align-items: center;
gap: 12px;
}
.course-icon-wrapper {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 22px;
}
.course-name {
font-size: 15px;
font-weight: 600;
color: #2D3436;
margin: 0;
}
.picture-book {
font-size: 12px;
color: #636E72;
margin: 4px 0 0 0;
}
.feedback-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: #B2BEC3;
}
.card-body {
padding: 16px;
}
.class-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 8px 12px;
background: #FFF4EC;
border-radius: 8px;
}
.class-icon {
color: #FF8C42;
}
.class-name {
font-size: 13px;
color: #666;
}
.ratings-grid {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.rating-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #FAFAFA;
border-radius: 8px;
}
.rating-header {
display: flex;
align-items: center;
gap: 6px;
}
.rating-icon {
font-size: 14px;
}
.design-icon {
color: #667eea;
}
.participation-icon {
color: #f5576c;
}
.achievement-icon {
color: #FF8C42;
}
.rating-label {
font-size: 12px;
color: #636E72;
}
.feedback-summary {
padding-top: 12px;
border-top: 1px solid #F0F0F0;
}
.summary-text {
font-size: 13px;
color: #636E72;
line-height: 1.5;
margin: 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
border-top: 1px solid #F0F0F0;
background: #FAFAFA;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
background: white;
border-radius: 16px;
}
.empty-icon-wrapper {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
font-size: 36px;
color: white;
}
.empty-state p {
color: #636E72;
font-size: 16px;
margin-bottom: 8px;
}
.empty-hint {
font-size: 13px !important;
color: #B2BEC3 !important;
}
/* 加载状态 */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
}
.loading-state p {
color: #636E72;
margin-top: 16px;
}
/* 分页 */
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 24px;
background: white;
border-radius: 12px;
}
/* 详情弹窗 */
.modal-title {
display: flex;
align-items: center;
gap: 8px;
}
.modal-title-icon {
color: #FF8C42;
font-size: 18px;
}
.detail-content {
padding: 0;
}
.detail-header {
display: flex;
gap: 20px;
padding: 20px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
border-radius: 16px;
margin-bottom: 24px;
}
.course-cover {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.cover-icon {
font-size: 36px;
color: white;
}
.course-meta h3 {
color: white;
font-size: 18px;
font-weight: 600;
margin: 0 0 4px 0;
}
.picture-book-name {
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
margin: 0 0 12px 0;
}
.meta-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.meta-tag {
padding: 4px 10px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
font-size: 12px;
color: white;
display: inline-flex;
align-items: center;
gap: 4px;
}
.detail-ratings {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.rating-card {
text-align: center;
padding: 20px 16px;
background: #F8F9FA;
border-radius: 16px;
}
.rating-score {
margin-bottom: 12px;
}
.score-value {
font-size: 32px;
font-weight: 700;
color: #2D3436;
}
.score-max {
font-size: 14px;
color: #B2BEC3;
}
.rating-icon-wrapper {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: white;
}
.rating-icon-wrapper.design {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.rating-icon-wrapper.participation {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.rating-icon-wrapper.achievement {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
}
.rating-name {
font-size: 14px;
font-weight: 600;
color: #2D3436;
margin-bottom: 8px;
}
.detail-section {
margin-bottom: 20px;
padding: 16px;
background: #F8F9FA;
border-radius: 12px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.section-icon-wrapper {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
}
.section-icon-wrapper.pros {
background: linear-gradient(135deg, #FFD93D 0%, #FF9500 100%);
}
.section-icon-wrapper.suggestions {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.section-icon-wrapper.activities {
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
}
.section-header h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #2D3436;
}
.section-content {
font-size: 14px;
line-height: 1.8;
color: #636E72;
margin: 0;
}
.activity-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
</style>