diff --git a/admin/src/api/followupAudio.ts b/admin/src/api/followupAudio.ts index 57671b940..864f92be5 100644 --- a/admin/src/api/followupAudio.ts +++ b/admin/src/api/followupAudio.ts @@ -75,6 +75,8 @@ export interface FollowupTaskSummary { version: number created_at: number | string expires_at: number | string + can_delete?: boolean + delete_block_reason?: string can_retry: boolean audio_available: boolean preview_only?: boolean @@ -82,6 +84,16 @@ export interface FollowupTaskSummary { pipeline_progress?: { transcribed: number; total: number } } export interface FollowupTaskDetail extends FollowupTaskSummary { + extraction_revision?: number + extraction_channel_roles?: FollowupChannelRoles + can_reextract?: boolean + reextract_block_reason?: string + role_check?: { + conflict: boolean + suggested_roles: FollowupChannelRoles + message: string + evidence: Array<{ channel: FollowupAudioChannel; start_ms: number; end_ms: number; text: string }> + } summary: string transcript: string transcript_segments?: FollowupTranscriptSegment[] @@ -130,6 +142,8 @@ export const followupAudioUploadComplete = (uploadId: string, signal?: AbortSign export const followupAudioCreate = (data: { diagnosis_id: number; upload_id: string; recorded_at: string; model_key: string; channel_roles?: FollowupChannelRoles }, signal?: AbortSignal) => post<{ task_id: number; status: string; reused?: boolean; reuse_message?: string }>('create', data, signal) export const followupAudioSaveDraft = (data: { id: number; version: number; items: FollowupReviewItem[]; channel_roles?: FollowupChannelRoles }, signal?: AbortSignal) => post('saveDraft', data, signal) export const followupAudioApply = (data: { id: number; version: number; items: FollowupReviewItem[] }, signal?: AbortSignal) => post<{ id: number; status: 'applied'; applied_items: unknown[] }>('apply', data, signal) +export const followupAudioReextract = (data: { id: number; version: number; channel_roles: Exclude }, signal?: AbortSignal) => post('reextract', data, signal) +export const followupAudioDelete = (data: { id: number; version: number }, signal?: AbortSignal) => post<{ id: number; status: 'deleted'; version: number }>('delete', data, signal) export const followupAudioRetry = (id: number, signal?: AbortSignal) => post<{ id: number; status: string }>('retry', { id }, signal) /** The existing request interceptor puts the token in a header, never in the audio URL. */ export async function followupAudioBlob(id: number, signal?: AbortSignal): Promise { diff --git a/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue b/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue index 80f94b3f8..69b5e1970 100644 --- a/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue +++ b/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue @@ -205,8 +205,8 @@
运动
{{ record.exercise_type || '—' }}
-
时长
{{ record.duration ?? '—' }} 分钟
-
强度
{{ record.intensity_text || intensityLabel(record.intensity) }}
+
时长
{{ record.duration == null ? '未记录' : `${record.duration} 分钟` }}
+
强度
{{ intensityLabel(record.intensity) }}
备注
{{ record.note || '—' }}
@@ -343,11 +343,13 @@ - - + + + 留空表示未记录 - - + + + @@ -492,8 +494,8 @@ interface ExerciseRecord { patient_id?: number record_date?: string exercise_type?: string - duration?: number - intensity?: number + duration?: number | null + intensity?: number | null intensity_text?: string images?: string[] note?: string @@ -547,6 +549,11 @@ const trackingSubmitting = ref(false) const bloodForm = ref(createBloodForm()) const dietForm = ref(createDietForm()) const exerciseForm = ref(createExerciseForm()) +// el-option uses a string sentinel; persisted unknowns remain SQL/JSON null. +const exerciseIntensity = computed({ + get: () => exerciseForm.value.intensity ?? '', + set: (value) => { exerciseForm.value.intensity = value === '' ? null : value } +}) const trackingForm = ref({ note_date: '', existing_content: '', @@ -719,8 +726,8 @@ function recordTimePeriodLabel(record: BloodRecord) { return labels[period] || period || '未提供时段' } -function intensityLabel(value?: number) { - return ({ 1: '低强度', 2: '中强度', 3: '高强度' } as Record)[Number(value)] || '—' +function intensityLabel(value?: number | null) { + return ({ 1: '低强度', 2: '中强度', 3: '高强度' } as Record)[Number(value)] || '未记录' } function bloodDetailFields(record: BloodRecord) { @@ -880,7 +887,7 @@ function getCell(metric: string, date: string): { value: string; isHigh: boolean return { value: count ? '✓' : '', isHigh: false, hasRecord: count > 0 } } if (metric === 'exercise') { - return { value: exercise.map((record) => record.duration ? `${record.duration}min` : record.exercise_type || '已记录').join(' / '), isHigh: false, hasRecord: exercise.length > 0 } + return { value: exercise.map((record) => record.duration != null ? `${record.duration}min` : record.exercise_type || '已记录').join(' / '), isHigh: false, hasRecord: exercise.length > 0 } } if (metric === 'tracking') { return { value: tracking?.content ? summarizeTracking(tracking.content) : '', isHigh: false, hasRecord: !!tracking?.content } @@ -970,7 +977,12 @@ function openDietRecord(source: DietRecord | undefined, date: string) { function openExerciseRecord(source: ExerciseRecord | undefined, date: string) { if (!canEditDailyRecord.value) return - exerciseForm.value = { ...createExerciseForm(date), ...source, images: [...(source?.images || [])] } + exerciseForm.value = { + ...createExerciseForm(date), ...source, + // Existing omissions must not inherit the defaults used only for manual new records. + ...(source ? { duration: source.duration ?? null, intensity: source.intensity ?? null } : {}), + images: [...(source?.images || [])] + } exerciseDialogVisible.value = true } @@ -1063,12 +1075,12 @@ async function submitExercise() { feedback.msgWarning('请选择运动类型') return } - if (!exerciseForm.value.duration) { + if (!exerciseForm.value.id && !exerciseForm.value.duration) { feedback.msgWarning('请输入运动时长') return } const targetDiagnosisId = diagnosisId.value - const payload = { ...exerciseForm.value } + const payload = { ...exerciseForm.value, duration: exerciseForm.value.duration ?? null, intensity: exerciseForm.value.intensity ?? null } exerciseSubmitting.value = true try { if (payload.id) { diff --git a/admin/src/views/tcm/diagnosis/components/FollowupAudioPanel.vue b/admin/src/views/tcm/diagnosis/components/FollowupAudioPanel.vue index 7d0570075..58b6d0664 100644 --- a/admin/src/views/tcm/diagnosis/components/FollowupAudioPanel.vue +++ b/admin/src/views/tcm/diagnosis/components/FollowupAudioPanel.vue @@ -34,8 +34,8 @@ - - + +
@@ -43,7 +43,7 @@

{{ detail.file_name }}

通话时间 {{ detail.recorded_at }}
- {{ followupTaskStatus(detail) }} +
{{ followupTaskStatus(detail) }}删除录音

{{ followupProgressText(detail) }}

@@ -56,13 +56,25 @@

{{ detail.summary || '暂无回访摘要' }}

{{ detail.status === 'applied' ? '已写入' : '已选' }} {{ selectedCount }} 项待核实 {{ pendingCount }} 项
-
+ +
{{ savedChannelRoles === 'left_service' ? '左客服端 / 右患者或家属端' : '右客服端 / 左患者或家属端' }} · 已确认修改

声道不等于已核实的说话人身份。角色标注不自动重新提炼;变更后保存草稿会取消全部采纳,须重新核对。

当前角色已确认,无需再次选择;只有左右标错时才需修改。

角色标注正在等待保存;保存后请逐项重新核对。

+
+ {{ detail.role_check?.conflict ? '声道角色与转写内容可能不一致' : '审核角色已修改,原提炼结果尚未更新' }} +

{{ detail.role_check?.message || '修改角色标注不会自动改变已生成的建议,可按核实后的角色重新提炼。' }}

+
{{ evidence.channel === 0 ? '左声道' : '右声道' }} · {{ audioTimestamp(evidence.start_ms) }}:{{ evidence.text }}回听依据
+

当前提炼依据:{{ extractionRoleLabel(extractionRoles) }}。{{ reextractTarget !== 'unconfirmed' ? `待确认的新角色:${extractionRoleLabel(reextractTarget)}。` : '请先回听并修改上方角色标注。' }}建议仅供核对,系统不会自动交换角色。

+ 确认角色并重新提炼 +

请先完成当前单项核对或取消编辑,等待审核草稿保存后再重新提炼。

+

{{ detail.reextract_block_reason }}

+

复用已完成的转写,不重复语音识别;旧结果保留,重新提炼不自动写入病例。

+
+

第 {{ (detail.extraction_revision || 0) + 1 }} 版提炼 · {{ extractionRoleLabel(extractionRoles) }} · 历史结果已保留