diff --git a/pages/feedback/index.ts b/pages/feedback/index.ts index 6e03bb7..15ac961 100644 --- a/pages/feedback/index.ts +++ b/pages/feedback/index.ts @@ -22,6 +22,7 @@ import type { FeedbackSession, FeedbackSummaryRun, StudentProfile, + VoiceAudioSegment, VoiceLesson, VoiceLessonFinished } from '../../utils/types' @@ -58,6 +59,7 @@ interface VoiceLessonView { canAdd: boolean summaryEligible: boolean summarySelected: boolean + failedSegmentCount: number addActionText: string emptyText: string } @@ -144,6 +146,7 @@ function buildVoiceLessonViews( const transcript = lesson.transcript.trim() const isInDraft = Boolean(transcript) && content.includes(transcript) const summaryEligible = lesson.status === 'ready' && Boolean(transcript) + const failedSegmentCount = lesson.segments.filter((segment) => segment.status === 'failed').length let emptyText = '转录完成后可查看内容' if (lesson.status === 'failed') { emptyText = lesson.segments.find((segment) => segment.status === 'failed')?.errorMessage @@ -162,6 +165,7 @@ function buildVoiceLessonViews( canAdd: lesson.status === 'ready' && Boolean(transcript) && !isInDraft, summaryEligible, summarySelected: summaryEligible && !excluded.has(lesson.id), + failedSegmentCount, addActionText: isInDraft ? '已在输入框' : lesson.appliedDirectly @@ -1056,13 +1060,34 @@ Page({ if (!session) return const failedSegments = getFailedVoiceSegments(session) if (failedSegments.length === 0) return + await this.retryVoiceSegments(failedSegments) + }, + + async retryVoiceLesson(event: LessonActionEvent) { + const session = this.data.session + const lesson = session?.lessons.find((item) => item.id === event.currentTarget.dataset.lessonId) + if (!lesson) return + const failedSegments = lesson.segments.filter((segment) => segment.status === 'failed') + if (failedSegments.length === 0) return + await this.retryVoiceSegments(failedSegments, lesson.sequence) + }, + + async retryVoiceSegments( + failedSegments: VoiceAudioSegment[], + lessonSequence?: number + ) { + const session = this.data.session + if (!session || this.data.recording || this.data.voiceBusy) return logVoiceOperation('transcription_retry_clicked', { feedbackSessionId: session.id, + lessonSequence: lessonSequence || null, failedSegmentCount: failedSegments.length }) this.setData({ voiceBusy: true, - voiceStatus: `正在重试${failedSegments.length}段转录`, + voiceStatus: lessonSequence + ? `正在重试第${lessonSequence}节转录` + : `正在重试${failedSegments.length}段转录`, primaryActionText: '正在重试转录', errorMessage: '', errorRequestId: '' @@ -1078,9 +1103,13 @@ Page({ ) logVoiceOperation('transcription_retry_submitted', { feedbackSessionId: session.id, + lessonSequence: lessonSequence || null, segmentIds: failedSegments.map((segment) => segment.id) }) - wx.showToast({ title: '已重新提交,正在转录', icon: 'none' }) + wx.showToast({ + title: lessonSequence ? `第${lessonSequence}节已重新提交` : '已重新提交,正在转录', + icon: 'none' + }) } catch (error) { logVoiceOperation('transcription_retry_failed', { feedbackSessionId: session.id, @@ -1094,13 +1123,17 @@ Page({ } }, - confirmDiscardFailedSegments(): Promise { - const failedSegmentCount = this.data.session?.failedSegmentCount || 0 + confirmDiscardVoiceSegments( + failedSegmentCount: number, + lessonSequence?: number + ): Promise { return new Promise((resolve) => { wx.showModal({ - title: '放弃失败录音', - content: `将删除${failedSegmentCount}段失败录音,其余录音和反馈内容不受影响。此操作无法撤销。`, - confirmText: '确认放弃', + title: lessonSequence ? `删除第${lessonSequence}节录音` : '删除失败录音', + content: lessonSequence + ? `将删除第${lessonSequence}节中${failedSegmentCount}段转录失败的录音,其余语音记录不受影响。此操作无法撤销。` + : `将删除${failedSegmentCount}段失败录音,其余录音和反馈内容不受影响。此操作无法撤销。`, + confirmText: '确认删除', confirmColor: '#d13f3a', success: (result) => resolve(result.confirm), fail: () => resolve(false) @@ -1113,11 +1146,38 @@ Page({ const session = this.data.session if (!session) return const failedSegments = getFailedVoiceSegments(session) - if (failedSegments.length === 0 || !(await this.confirmDiscardFailedSegments())) return + if ( + failedSegments.length === 0 + || !(await this.confirmDiscardVoiceSegments(failedSegments.length)) + ) return + await this.discardVoiceSegments(failedSegments) + }, + + async discardVoiceLesson(event: LessonActionEvent) { + if (this.data.recording || this.data.voiceBusy) return + const session = this.data.session + const lesson = session?.lessons.find((item) => item.id === event.currentTarget.dataset.lessonId) + if (!session || !lesson) return + const failedSegments = lesson.segments.filter((segment) => segment.status === 'failed') + if ( + failedSegments.length === 0 + || !(await this.confirmDiscardVoiceSegments(failedSegments.length, lesson.sequence)) + ) return + await this.discardVoiceSegments(failedSegments, lesson.sequence) + }, + + async discardVoiceSegments( + failedSegments: VoiceAudioSegment[], + lessonSequence?: number + ) { + const session = this.data.session + if (!session) return this.setData({ voiceBusy: true, - voiceStatus: `正在放弃${failedSegments.length}段失败录音`, + voiceStatus: lessonSequence + ? `正在删除第${lessonSequence}节失败录音` + : `正在删除${failedSegments.length}段失败录音`, primaryActionText: '正在处理', errorMessage: '', errorRequestId: '' @@ -1133,9 +1193,13 @@ Page({ ) logVoiceOperation('failed_transcription_discarded', { feedbackSessionId: session.id, + lessonSequence: lessonSequence || null, segmentIds: failedSegments.map((segment) => segment.id) }) - wx.showToast({ title: '已放弃失败录音', icon: 'none' }) + wx.showToast({ + title: lessonSequence ? `第${lessonSequence}节失败录音已删除` : '失败录音已删除', + icon: 'none' + }) } catch (error) { this.showApiError(error) } finally { diff --git a/pages/feedback/index.wxml b/pages/feedback/index.wxml index 0382451..4cdb5a3 100644 --- a/pages/feedback/index.wxml +++ b/pages/feedback/index.wxml @@ -101,7 +101,7 @@ {{lesson.durationText}} · {{lesson.transcriptLength}}字 - {{lesson.statusText}} + {{lesson.statusText}} {{lesson.addActionText}} + + + + diff --git a/pages/feedback/index.wxss b/pages/feedback/index.wxss index e9dca4a..f2bf913 100644 --- a/pages/feedback/index.wxss +++ b/pages/feedback/index.wxss @@ -386,6 +386,10 @@ font-size: 22rpx; } +.voice-record-status-failed { + color: #b8322d; +} + .voice-record-transcript, .voice-record-empty { margin-top: 12rpx; @@ -434,6 +438,61 @@ opacity: 1; } +.voice-record-failure-actions { + display: flex; + width: 100%; + min-width: 0; + gap: 12rpx; + margin-top: 14rpx; +} + +.voice-record-retry-button, +.voice-record-delete-button { + display: flex; + flex: 1 1 0; + align-items: center; + justify-content: center; + width: 0; + min-width: 0; + max-width: none; + height: 62rpx; + margin: 0; + padding: 0; + border: 0; + border-radius: 10rpx; + font-size: 22rpx; + font-weight: 600; + line-height: 1; + transition: transform 100ms ease-out, background-color 100ms ease-out; +} + +.voice-record-retry-button { + background: #eaf3ff; + color: #007aff; +} + +.voice-record-delete-button { + background: #fff2f2; + color: #b8322d; +} + +.voice-record-retry-button::after, +.voice-record-delete-button::after { + border: 0; +} + +.voice-record-retry-button:active, +.voice-record-delete-button:active { + transform: scale(0.97); +} + +.voice-record-retry-button[disabled], +.voice-record-delete-button[disabled] { + background: #f1f3f6; + color: #aeaeb2; + opacity: 1; +} + .voice-summary-controls { padding: 18rpx 0; border-top: 1rpx solid #e5e5ea; @@ -589,6 +648,8 @@ .voice-button, .voice-state-actionable, .voice-state-arrow, + .voice-record-retry-button, + .voice-record-delete-button, .feedback-error-copy, .feedback-error-retry, .feedback-error-close { @@ -597,6 +658,8 @@ .voice-button:active, .voice-state-actionable:active, + .voice-record-retry-button:active, + .voice-record-delete-button:active, .feedback-error-copy:active, .feedback-error-retry:active, .feedback-error-close:active {