Files
zyt/admin/src/views/tcm/diagnosis/components/ConsultationTranscriptPanel.vue
T
2026-10-22 11:02:57 +08:00

169 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section class="consultation-transcripts">
<header class="transcript-toolbar">
<div>
<h3>面诊文字对话</h3>
<p>按每次通话分别查看已保存的文字记录。</p>
</div>
<el-button :loading="loading" @click="refresh"><el-icon><Refresh /></el-icon>刷新</el-button>
</header>
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon class="transcript-error" />
<div v-loading="loading" class="transcript-body">
<template v-if="records.length">
<aside class="transcript-sessions" aria-label="历次面诊记录">
<el-input v-model="search" clearable placeholder="搜索面诊时间或对话内容" :prefix-icon="Search" />
<p class="session-count">{{ filteredRecords.length }} 条面诊记录</p>
<div class="session-list">
<button
v-for="row in pageRecords"
:key="row.id"
type="button"
class="session-item"
:class="{ selected: selectedId === row.id }"
:aria-pressed="selectedId === row.id"
@click="selectedId = row.id"
>
<span class="session-time">{{ row.start_time_text || '开始时间未记录' }}</span>
<span class="session-meta">{{ consultationCallLabel(row) }} · 记录 #{{ row.id }}</span>
<el-tag size="small" :type="consultationTranscriptStatus(row).type">{{ consultationTranscriptStatus(row).label }}</el-tag>
<span class="session-preview">{{ preview(row) }}</span>
</button>
</div>
<el-pagination
v-if="filteredRecords.length > PAGE_SIZE"
v-model:current-page="page"
:page-size="PAGE_SIZE"
:total="filteredRecords.length"
layout="prev, pager, next"
small
/>
</aside>
<article v-if="selectedRecord" class="transcript-detail" :key="`${diagnosisId}-${selectedRecord.id}`">
<header class="transcript-detail-header">
<div>
<h4>{{ selectedRecord.start_time_text || '开始时间未记录' }} · {{ consultationCallLabel(selectedRecord) }}</h4>
<p>
通话记录 #{{ selectedRecord.id }}
<span v-if="selectedRecord.duration_text"> · 时长 {{ selectedRecord.duration_text }}</span>
<span v-if="selectedRecord.end_time_text"> · 结束 {{ selectedRecord.end_time_text }}</span>
</p>
</div>
<el-button v-if="selectedText" :loading="copying" @click="copyTranscript">复制全文</el-button>
</header>
<div class="transcript-status">
<el-tag :type="consultationTranscriptStatus(selectedRecord).type">{{ consultationTranscriptStatus(selectedRecord).label }}</el-tag>
<span>{{ callStatusText(selectedRecord.status) }}</span>
<span v-if="selectedRecord.transcription_segment_count">{{ selectedRecord.transcription_segment_count }} 段对话</span>
</div>
<p v-if="selectedRecord.transcription_status === 'partial'" class="transcript-note">本次文字记录不完整,以下展示已保存的内容。</p>
<p v-else-if="selectedRecord.transcription_status === 'failed' && selectedText" class="transcript-note">本次转写未完成,以下展示已保存的内容。</p>
<p v-else-if="selectedRecord.transcription_status === 'running' && selectedText" class="transcript-note">转写仍在进行,可刷新查看最新保存的文字。</p>
<pre v-if="selectedText" class="transcript-text">{{ selectedText }}</pre>
<el-empty v-else :description="consultationTranscriptEmptyText(selectedRecord)" :image-size="72" />
</article>
<el-empty v-else description="没有匹配的面诊记录,请调整搜索内容" class="transcript-detail" :image-size="72" />
</template>
<el-empty v-else-if="!loading && !error" description="该诊单暂无面诊通话记录" :image-size="80" class="transcript-empty" />
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Refresh, Search } from '@element-plus/icons-vue'
import type { ConsultationCallRecord } from '@/api/tcm'
import { useConsultationTranscripts } from '@/hooks/useConsultationTranscripts'
import {
consultationCallLabel,
consultationTranscriptEmptyText,
consultationTranscriptStatus,
consultationTranscriptText
} from '@/utils/consultation-transcripts'
import feedback from '@/utils/feedback'
const props = defineProps<{ diagnosisId: number }>()
const { records, loading, error, refresh } = useConsultationTranscripts(() => props.diagnosisId)
const search = ref('')
const selectedId = ref<number | null>(null)
const page = ref(1)
const copying = ref(false)
const PAGE_SIZE = 10
const filteredRecords = computed(() => {
const term = search.value.trim().toLocaleLowerCase()
return records.value.filter(row => row.diagnosis_id === props.diagnosisId &&
(!term || `${row.start_time_text || ''} ${row.id} ${consultationTranscriptText(row)}`.toLocaleLowerCase().includes(term)))
})
const pageRecords = computed(() => filteredRecords.value.slice((page.value - 1) * PAGE_SIZE, page.value * PAGE_SIZE))
const selectedRecord = computed(() => filteredRecords.value.find(row => row.id === selectedId.value))
const selectedText = computed(() => consultationTranscriptText(selectedRecord.value))
watch(filteredRecords, rows => {
if (!rows.some(row => row.id === selectedId.value)) {
const selected = rows.find(row => consultationTranscriptText(row)) || rows[0]
selectedId.value = selected?.id ?? null
page.value = selected ? Math.floor(rows.indexOf(selected) / PAGE_SIZE) + 1 : 1
}
page.value = Math.min(page.value, Math.max(1, Math.ceil(rows.length / PAGE_SIZE)))
}, { immediate: true, flush: 'sync' })
watch(search, () => { page.value = 1 }, { flush: 'sync' })
function preview(row: ConsultationCallRecord): string {
const text = consultationTranscriptText(row).replace(/\s+/g, ' ')
return text ? text.slice(0, 90) + (text.length > 90 ? '…' : '') : consultationTranscriptEmptyText(row)
}
function callStatusText(value?: number): string {
return ({ 1: '进行中', 2: '已结束', 3: '未接听', 4: '已取消' } as Record<number, string>)[Number(value)] || '状态未记录'
}
async function copyTranscript() {
const text = selectedText.value
if (!text || copying.value) return
if (!navigator.clipboard?.writeText) {
feedback.msgWarning('当前浏览器不支持自动复制,请选中文字复制')
return
}
copying.value = true
try {
await navigator.clipboard.writeText(text)
feedback.msgSuccess('已复制本次面诊文字对话')
} catch {
feedback.msgWarning('复制失败,请选中文字复制')
} finally {
copying.value = false
}
}
</script>
<style scoped>
.consultation-transcripts { padding: 4px 0 16px; }
.transcript-toolbar, .transcript-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.transcript-toolbar { margin-bottom: 20px; }
.transcript-toolbar h3, .transcript-detail h4 { margin: 0; color: var(--el-text-color-primary); font-weight: 600; }
.transcript-toolbar h3 { font-size: 18px; }
.transcript-toolbar p, .transcript-detail-header p { margin: 7px 0 0; color: var(--el-text-color-secondary); line-height: 1.7; font-size: 13px; }
.transcript-error { margin-bottom: 16px; }
.transcript-body { display: flex; min-height: 360px; border-top: 1px solid var(--el-border-color-lighter); }
.transcript-sessions { flex: 0 0 282px; min-width: 0; padding: 18px 18px 0 0; border-right: 1px solid var(--el-border-color-lighter); }
.session-count { margin: 12px 0 8px; color: var(--el-text-color-secondary); font-size: 12px; }
.session-list { display: flex; flex-direction: column; gap: 8px; max-height: 580px; overflow-y: auto; margin-bottom: 12px; }
.session-item { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; width: 100%; padding: 13px 12px; border: 1px solid var(--el-border-color-lighter); border-radius: 7px; background: var(--el-bg-color); text-align: left; cursor: pointer; font: inherit; }
.session-item:hover, .session-item.selected { border-color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
.session-item:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: -2px; }
.session-time { color: var(--el-text-color-primary); font-size: 14px; font-weight: 600; }
.session-meta, .session-preview { color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.6; }
.session-preview { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.transcript-detail { flex: 1; min-width: 0; padding: 20px 0 0 24px; }
.transcript-detail h4 { font-size: 16px; line-height: 1.7; }
.transcript-status { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin: 16px 0; color: var(--el-text-color-secondary); font-size: 13px; }
.transcript-note { padding: 10px 12px; background: var(--el-color-warning-light-9); color: var(--el-color-warning-dark-2); font-size: 13px; line-height: 1.7; }
.transcript-text { margin: 0; padding: 20px 0; color: var(--el-text-color-primary); white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; font-size: 15px; line-height: 2; user-select: text; }
.transcript-empty { flex: 1; }
@media (max-width: 900px) {
.transcript-body { flex-direction: column; }
.transcript-sessions { flex-basis: auto; padding: 16px 0; border-right: 0; border-bottom: 1px solid var(--el-border-color-lighter); }
.session-list { max-height: 300px; }
.transcript-detail { padding: 20px 0 0; }
.transcript-detail-header { flex-wrap: wrap; }
}
</style>