| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- <template>
- <div class="article-report">
- <div class="article-report__scroll">
- <!-- 1. 整篇总评(还原 enspeak OverallReport scoreLayout="compactStars") -->
- <ArticleOverallReport
- :report="report"
- :show-restart="showRestart"
- @restart="emit('restart')"
- />
- <!-- 2. 逐段详情 -->
- <div class="report-paragraphs">
- <div class="report-paragraphs__head">
- <span class="report-paragraphs__title">{{ lang.ssArticleParagraphsTitle }}</span>
- <button
- v-if="recordedIdxs.length"
- class="report-paragraphs__toggle"
- @click="toggleAll"
- >
- {{ allExpanded ? lang.ssArticleCollapseAll : lang.ssArticleExpandAll }}
- </button>
- </div>
- <div
- v-for="(paragraph, index) in sortedParagraphs"
- :key="paragraph.idx"
- class="paragraph-block"
- >
- <!-- 段落分隔线 -->
- <div v-if="index > 0" class="paragraph-divider">
- <span class="paragraph-divider__line"></span>
- <span class="paragraph-divider__label">{{ paragraphLabel(index) }}</span>
- <span class="paragraph-divider__line"></span>
- </div>
- <div class="paragraph-shell">
- <ArticleParagraphCard
- :paragraph="paragraph"
- :expanded="expandedIdxs.has(paragraph.idx)"
- :demo-audio-loading="loadingId === `demo-${paragraph.idx}`"
- :demo-audio-playing="playingId === `demo-${paragraph.idx}`"
- :own-audio-loading="loadingId === `own-${paragraph.idx}`"
- :own-audio-playing="playingId === `own-${paragraph.idx}`"
- :own-audio-available="Boolean(paragraph.audioUrl)"
- @toggle="toggleOne(paragraph.idx)"
- @play-demo="emit('play-demo', paragraph.idx)"
- @play-own="emit('play-own', paragraph.idx)"
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, ref, watch } from 'vue'
- import { lang } from '@/main'
- import ArticleOverallReport from './ArticleOverallReport.vue'
- import ArticleParagraphCard from './ArticleParagraphCard.vue'
- import type { ArticleReadingReport } from '@/types/articleReading'
- const props = withDefaults(defineProps<{
- report: ArticleReadingReport
- playingId: string | null
- loadingId: string | null
- /** 学生自己的报告页给「再来一次」;老师端 StudentReportModal 不给。 */
- showRestart?: boolean
- }>(), { showRestart: false })
- const emit = defineEmits<{
- (event: 'play-demo', paragraphIdx: number): void
- (event: 'play-own', paragraphIdx: number): void
- (event: 'restart'): void
- }>()
- const sortedParagraphs = computed(() => [...props.report.paragraphs].sort((a, b) => a.idx - b.idx))
- // 只有已出分的段落有评价区可展开(原型同款)
- const recordedIdxs = computed(() =>
- sortedParagraphs.value.filter(paragraph => paragraph.status === 'completed').map(p => p.idx),
- )
- const expandedIdxs = ref<Set<number>>(new Set())
- // 默认展开全部已录段落;换一份报告(老师逐个学生看)时重新初始化
- watch(
- () => `${props.report.sessionId}|${recordedIdxs.value.join(',')}`,
- () => {
- expandedIdxs.value = new Set(recordedIdxs.value)
- },
- { immediate: true },
- )
- const allExpanded = computed(() =>
- recordedIdxs.value.length > 0 && recordedIdxs.value.every(idx => expandedIdxs.value.has(idx)),
- )
- function toggleAll() {
- expandedIdxs.value = allExpanded.value ? new Set() : new Set(recordedIdxs.value)
- }
- function toggleOne(idx: number) {
- const next = new Set(expandedIdxs.value)
- if (next.has(idx)) next.delete(idx)
- else next.add(idx)
- expandedIdxs.value = next
- }
- function paragraphLabel(index: number): string {
- return String(lang.ssArticleParagraphIndex).replace('{n}', String(index + 1))
- }
- </script>
- <style lang="scss" scoped>
- .article-report {
- width: 100%;
- height: 100%;
- background: #fff;
- overflow: hidden;
- }
- .article-report__scroll {
- height: 100%;
- overflow-y: auto;
- padding: 24px;
- max-width: 720px;
- margin: 0 auto;
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .report-paragraphs__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- margin-bottom: 8px;
- }
- .report-paragraphs__title {
- font-size: 12px;
- font-weight: 500;
- color: #6b7280;
- }
- .report-paragraphs__toggle {
- border: none;
- background: transparent;
- padding: 0;
- font-size: 10px;
- color: #f97316;
- cursor: pointer;
- transition: color 0.2s;
- &:hover { color: #ea580c; }
- }
- // 段落之间的「第 N 段」分隔线
- .paragraph-divider {
- display: flex;
- align-items: center;
- gap: 8px;
- margin: 12px 0;
- }
- .paragraph-divider__line {
- flex: 1;
- height: 1px;
- background: #e5e7eb;
- }
- .paragraph-divider__label {
- font-size: 9px;
- color: #9ca3af;
- white-space: nowrap;
- }
- .paragraph-shell {
- padding: 16px;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- background: #fff;
- }
- </style>
|