StudentReportModal.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <div class="modal-overlay" @click="$emit('close')">
  3. <div class="modal-card" @click.stop>
  4. <div class="modal-header">
  5. <div>
  6. <h3 class="modal-title">
  7. {{ student.name }}{{ titleSuffix }}
  8. </h3>
  9. <p class="modal-subtitle">
  10. <span v-if="student.rawStatus !== 'completed'" class="badge-in-progress">
  11. {{ (lang as any).ssSpkInProgressBadge }}
  12. </span>
  13. </p>
  14. </div>
  15. <button class="close-btn" @click="$emit('close')">
  16. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  17. <line x1="18" y1="6" x2="6" y2="18" />
  18. <line x1="6" y1="6" x2="18" y2="18" />
  19. </svg>
  20. </button>
  21. </div>
  22. <div class="modal-body">
  23. <div v-if="loading" class="state-block">
  24. <div class="loader" />
  25. </div>
  26. <div v-else-if="loadError" class="state-block">
  27. <p class="error-msg">{{ (lang as any).ssSpkLoadFailed }}</p>
  28. <button class="retry-btn" @click="loadReport">{{ (lang as any).ssSpkRetry }}</button>
  29. </div>
  30. <div v-else-if="reportPending" class="state-block">
  31. <p>{{ (lang as any).ssSpkReportGenerating }}</p>
  32. </div>
  33. <div v-else-if="hasNoMessages" class="state-block">
  34. <p>{{ (lang as any).ssSpkActiveEmpty }}</p>
  35. </div>
  36. <template v-else>
  37. <OverallReport
  38. :evaluation="showOverall ? (report?.evaluation ?? null) : null"
  39. :role="role"
  40. :scoreDisplayMode="'numeric'"
  41. @restart="$emit('close')"
  42. @complete="$emit('close')"
  43. />
  44. <DetailedReport
  45. v-if="report?.evaluation?.sentenceEvaluations?.length"
  46. :sentenceEvaluations="report.evaluation.sentenceEvaluations"
  47. :class="{ 'mt-block': showOverall }"
  48. />
  49. </template>
  50. </div>
  51. </div>
  52. </div>
  53. </template>
  54. <script lang="ts" setup>
  55. import { ref, computed, onMounted, watch } from 'vue'
  56. import { lang } from '@/main'
  57. import { createDialogueApi } from '@/views/Editor/EnglishSpeaking/services/llmService'
  58. import OverallReport from '@/views/Editor/EnglishSpeaking/preview/OverallReport.vue'
  59. import DetailedReport from '@/views/Editor/EnglishSpeaking/preview/DetailedReport.vue'
  60. import type { PreviewAIRole, DialogueReport } from '@/types/englishSpeaking'
  61. import type { ClassStudentSummary } from './types'
  62. const props = defineProps<{
  63. student: ClassStudentSummary
  64. role: PreviewAIRole
  65. }>()
  66. defineEmits<{ close: [] }>()
  67. const loading = ref(false)
  68. const loadError = ref(false)
  69. const report = ref<DialogueReport | null>(null)
  70. const titleSuffix = computed(() =>
  71. props.student.rawStatus === 'completed'
  72. ? (lang as any).ssSpkReportTitleSuffix
  73. : (lang as any).ssSpkRecordTitleSuffix,
  74. )
  75. const reportPending = computed(() =>
  76. props.student.rawStatus === 'completed'
  77. && !!report.value
  78. && report.value.status !== 'ready'
  79. && report.value.status !== 'failed',
  80. )
  81. const showOverall = computed(() =>
  82. props.student.rawStatus === 'completed'
  83. && report.value?.status === 'ready',
  84. )
  85. const hasNoMessages = computed(() =>
  86. !loading.value
  87. && !loadError.value
  88. && !!report.value
  89. && (!report.value.evaluation
  90. || (report.value.evaluation.sentenceEvaluations || []).length === 0)
  91. && props.student.rawStatus !== 'completed',
  92. )
  93. async function loadReport() {
  94. if (!props.student.sessionId) return
  95. loading.value = true
  96. loadError.value = false
  97. try {
  98. const api = createDialogueApi()
  99. report.value = await api.getReport(props.student.sessionId)
  100. }
  101. catch (e) {
  102. console.error('[StudentReportModal] getReport failed:', e)
  103. loadError.value = true
  104. }
  105. finally {
  106. loading.value = false
  107. }
  108. }
  109. onMounted(loadReport)
  110. watch(() => props.student.sessionId, loadReport)
  111. </script>
  112. <style lang="scss" scoped>
  113. .modal-overlay {
  114. position: fixed; inset: 0;
  115. background: rgba(0,0,0,.5);
  116. display: flex; align-items: center; justify-content: center;
  117. z-index: 50;
  118. }
  119. .modal-card {
  120. background: #fff;
  121. border-radius: 12px;
  122. width: 100%; max-width: 720px;
  123. max-height: 85vh;
  124. margin: 0 16px;
  125. display: flex; flex-direction: column;
  126. overflow: hidden;
  127. }
  128. .modal-header {
  129. display: flex; align-items: center; justify-content: space-between;
  130. padding: 16px 24px;
  131. border-bottom: 1px solid #e5e7eb;
  132. }
  133. .modal-title {
  134. font-size: 16px; font-weight: 600; color: #111827; margin: 0;
  135. }
  136. .modal-subtitle {
  137. font-size: 13px; color: #6b7280; margin: 2px 0 0;
  138. }
  139. .badge-in-progress {
  140. margin-left: 8px;
  141. color: #d97706;
  142. font-weight: 500;
  143. }
  144. .close-btn {
  145. background: transparent; border: none;
  146. color: #9ca3af; cursor: pointer;
  147. &:hover { color: #4b5563; }
  148. }
  149. .modal-body {
  150. flex: 1;
  151. overflow-y: auto;
  152. padding: 16px 24px;
  153. }
  154. .state-block {
  155. display: flex;
  156. flex-direction: column;
  157. align-items: center;
  158. justify-content: center;
  159. padding: 40px 0;
  160. gap: 12px;
  161. color: #6b7280;
  162. font-size: 13px;
  163. }
  164. .error-msg { color: #dc2626; }
  165. .retry-btn {
  166. padding: 6px 16px;
  167. background: #f3f4f6;
  168. border: 1px solid #e5e7eb;
  169. border-radius: 6px;
  170. cursor: pointer;
  171. font-size: 13px;
  172. &:hover { background: #e5e7eb; }
  173. }
  174. .loader {
  175. width: 24px; height: 24px;
  176. border: 2px solid #e5e7eb;
  177. border-top-color: #facc15;
  178. border-radius: 50%;
  179. animation: spin 1s linear infinite;
  180. }
  181. @keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
  182. .mt-block { margin-top: 16px; }
  183. </style>