Просмотр исходного кода

feat(speaking): add StudentReportModal.vue

jimmylee 2 месяцев назад
Родитель
Сommit
9ce0904ac9
1 измененных файлов с 201 добавлено и 0 удалено
  1. 201 0
      src/views/Student/components/SpeakingClassPanel/StudentReportModal.vue

+ 201 - 0
src/views/Student/components/SpeakingClassPanel/StudentReportModal.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="modal-overlay" @click="$emit('close')">
+    <div class="modal-card" @click.stop>
+      <div class="modal-header">
+        <div>
+          <h3 class="modal-title">
+            {{ student.name }}{{ titleSuffix }}
+          </h3>
+          <p class="modal-subtitle">
+            <span v-if="student.rawStatus !== 'completed'" class="badge-in-progress">
+              {{ (lang as any).ssSpkInProgressBadge }}
+            </span>
+          </p>
+        </div>
+        <button class="close-btn" @click="$emit('close')">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <line x1="18" y1="6" x2="6" y2="18" />
+            <line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+
+      <div class="modal-body">
+        <div v-if="loading" class="state-block">
+          <div class="loader" />
+        </div>
+
+        <div v-else-if="loadError" class="state-block">
+          <p class="error-msg">{{ (lang as any).ssSpkLoadFailed }}</p>
+          <button class="retry-btn" @click="loadReport">{{ (lang as any).ssSpkRetry }}</button>
+        </div>
+
+        <div v-else-if="reportPending" class="state-block">
+          <p>{{ (lang as any).ssSpkReportGenerating }}</p>
+        </div>
+
+        <div v-else-if="hasNoMessages" class="state-block">
+          <p>{{ (lang as any).ssSpkActiveEmpty }}</p>
+        </div>
+
+        <template v-else>
+          <OverallReport
+            v-if="showOverall && report?.evaluation"
+            :evaluation="report.evaluation"
+            :role="role"
+            :scoreDisplayMode="'numeric'"
+            @restart="$emit('close')"
+            @complete="$emit('close')"
+          />
+          <DetailedReport
+            v-if="report?.evaluation?.sentenceEvaluations?.length"
+            :sentenceEvaluations="report.evaluation.sentenceEvaluations"
+            :class="{ 'mt-block': showOverall }"
+          />
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { lang } from '@/main'
+import { createDialogueApi } from '@/views/Editor/EnglishSpeaking/services/llmService'
+import OverallReport from '@/views/Editor/EnglishSpeaking/preview/OverallReport.vue'
+import DetailedReport from '@/views/Editor/EnglishSpeaking/preview/DetailedReport.vue'
+import type { PreviewAIRole, DialogueReport } from '@/types/englishSpeaking'
+import type { ClassStudentSummary } from './types'
+
+const props = defineProps<{
+  student: ClassStudentSummary
+  role: PreviewAIRole
+}>()
+defineEmits<{ close: [] }>()
+
+const loading = ref(false)
+const loadError = ref(false)
+const report = ref<DialogueReport | null>(null)
+
+const titleSuffix = computed(() =>
+  props.student.rawStatus === 'completed'
+    ? (lang as any).ssSpkReportTitleSuffix
+    : (lang as any).ssSpkRecordTitleSuffix,
+)
+
+const reportPending = computed(() =>
+  props.student.rawStatus === 'completed'
+  && !!report.value
+  && report.value.status !== 'ready'
+  && report.value.status !== 'failed',
+)
+
+const showOverall = computed(() =>
+  props.student.rawStatus === 'completed'
+  && report.value?.status === 'ready',
+)
+
+const hasNoMessages = computed(() =>
+  !loading.value
+  && !loadError.value
+  && !!report.value
+  && (!report.value.evaluation
+    || (report.value.evaluation.sentenceEvaluations || []).length === 0)
+  && props.student.rawStatus !== 'completed',
+)
+
+async function loadReport() {
+  if (!props.student.sessionId) return
+  loading.value = true
+  loadError.value = false
+  try {
+    const api = createDialogueApi()
+    report.value = await api.getReport(props.student.sessionId)
+  }
+  catch (e) {
+    console.error('[StudentReportModal] getReport failed:', e)
+    loadError.value = true
+  }
+  finally {
+    loading.value = false
+  }
+}
+
+onMounted(loadReport)
+watch(() => props.student.sessionId, loadReport)
+</script>
+
+<style lang="scss" scoped>
+.modal-overlay {
+  position: fixed; inset: 0;
+  background: rgba(0,0,0,.5);
+  display: flex; align-items: center; justify-content: center;
+  z-index: 50;
+}
+.modal-card {
+  background: #fff;
+  border-radius: 12px;
+  width: 100%; max-width: 720px;
+  max-height: 85vh;
+  margin: 0 16px;
+  display: flex; flex-direction: column;
+  overflow: hidden;
+}
+.modal-header {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 16px 24px;
+  border-bottom: 1px solid #e5e7eb;
+}
+.modal-title {
+  font-size: 16px; font-weight: 600; color: #111827; margin: 0;
+}
+.modal-subtitle {
+  font-size: 13px; color: #6b7280; margin: 2px 0 0;
+}
+.badge-in-progress {
+  margin-left: 8px;
+  color: #d97706;
+  font-weight: 500;
+}
+.close-btn {
+  background: transparent; border: none;
+  color: #9ca3af; cursor: pointer;
+  &:hover { color: #4b5563; }
+}
+.modal-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px 24px;
+}
+
+.state-block {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+  gap: 12px;
+  color: #6b7280;
+  font-size: 13px;
+}
+.error-msg { color: #dc2626; }
+.retry-btn {
+  padding: 6px 16px;
+  background: #f3f4f6;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 13px;
+  &:hover { background: #e5e7eb; }
+}
+.loader {
+  width: 24px; height: 24px;
+  border: 2px solid #e5e7eb;
+  border-top-color: #facc15;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
+
+.mt-block { margin-top: 16px; }
+</style>