169 lines
10 KiB
Vue
169 lines
10 KiB
Vue
<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>
|