| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- <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
- :evaluation="showOverall ? (report?.evaluation ?? null) : null"
- :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>
|