feat(followup-audio): confirm role corrections and reextract cached transcripts
This commit is contained in:
@@ -84,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[]
|
||||
@@ -132,6 +142,7 @@ 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<FollowupTaskDetail>('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<FollowupChannelRoles, 'unconfirmed'> }, signal?: AbortSignal) => post<FollowupTaskDetail>('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. */
|
||||
|
||||
@@ -57,13 +57,24 @@
|
||||
<div class="summary-counts"><span>{{ detail.status === 'applied' ? '已写入' : '已选' }} <strong>{{ selectedCount }}</strong> 项</span><span>待核实 <strong>{{ pendingCount }}</strong> 项</span></div>
|
||||
</div>
|
||||
<el-alert v-if="detail.status === 'review' && !reviewItems.length" type="warning" :closable="false" title="未生成可写入信息" :description="detail.uncertainties.length ? `本次提炼保留了 ${detail.uncertainties.length} 条待核实原因,已展示在下方。请结合原文回听核对,尤其注意声道角色是否相符。` : '请查看回访摘要并回听录音;本次没有可供确认写入的结构化候选。'" show-icon />
|
||||
<section v-if="hasStereo" class="channel-role-box" aria-label="声道角色人工标注">
|
||||
<section v-if="hasStereo && !['queued', 'running'].includes(detail.status)" class="channel-role-box" aria-label="声道角色人工标注">
|
||||
<div v-if="savedChannelRoles !== 'unconfirmed' && !editingRoles && !roleDirty" class="role-summary"><span>{{ savedChannelRoles === 'left_service' ? '左客服端 / 右患者或家属端' : '右客服端 / 左患者或家属端' }} · 已确认</span><el-button v-if="canEditReview" link type="primary" @click="editingRoles = true">修改</el-button></div>
|
||||
<label v-else>声道角色(人工确认)<el-select :model-value="channelRoles" :disabled="!canEditReview || (busy && mutation !== 'save')" @update:model-value="setChannelRoles"><el-option label="角色待确认" value="unconfirmed" /><el-option label="左客服端 / 右患者或家属端" value="left_service" /><el-option label="右客服端 / 左患者或家属端" value="right_service" /></el-select></label>
|
||||
<p v-if="savedChannelRoles === 'unconfirmed' || editingRoles || roleDirty" class="muted">声道不等于已核实的说话人身份。角色标注不自动重新提炼;变更后保存草稿会取消全部采纳,须重新核对。</p>
|
||||
<p v-if="savedChannelRoles !== 'unconfirmed' && !roleDirty" class="muted">当前角色已确认,无需再次选择;只有左右标错时才需修改。</p>
|
||||
<p v-if="roleDirty" class="review-error" role="status">角色标注正在等待保存;保存后请逐项重新核对。</p>
|
||||
</section>
|
||||
<section v-if="detail.role_check?.conflict || (detail.can_reextract !== undefined && savedChannelRoles !== extractionRoles && savedChannelRoles !== 'unconfirmed')" class="role-reextract" aria-label="角色纠错与重新提炼">
|
||||
<strong>{{ detail.role_check?.conflict ? '声道角色与转写内容可能不一致' : '审核角色已修改,原提炼结果尚未更新' }}</strong>
|
||||
<p>{{ detail.role_check?.message || '修改角色标注不会自动改变已生成的建议,可按核实后的角色重新提炼。' }}</p>
|
||||
<blockquote v-for="(evidence, index) in detail.role_check?.evidence || []" :key="index"><span>{{ evidence.channel === 0 ? '左声道' : '右声道' }} · {{ audioTimestamp(evidence.start_ms) }}:{{ evidence.text }}</span><el-button v-if="detail.audio_available" link type="primary" @click="loadDialogue(evidence.start_ms)">回听依据</el-button></blockquote>
|
||||
<p>当前提炼依据:{{ extractionRoleLabel(extractionRoles) }}。{{ reextractTarget !== 'unconfirmed' ? `待确认的新角色:${extractionRoleLabel(reextractTarget)}。` : '请先回听并修改上方角色标注。' }}建议仅供核对,系统不会自动交换角色。</p>
|
||||
<el-button type="primary" plain :disabled="!canReextract || busy || dirty || reextractTarget === 'unconfirmed' || reextractTarget === extractionRoles" @click="reextractWithRoles">确认角色并重新提炼</el-button>
|
||||
<p v-if="dirty">请先完成当前单项核对或取消编辑,等待审核草稿保存后再重新提炼。</p>
|
||||
<p v-else-if="detail.reextract_block_reason">{{ detail.reextract_block_reason }}</p>
|
||||
<p class="muted">复用已完成的转写,不重复语音识别;旧结果保留,重新提炼不自动写入病例。</p>
|
||||
</section>
|
||||
<p v-if="(detail.extraction_revision || 0) > 0" class="muted">第 {{ (detail.extraction_revision || 0) + 1 }} 版提炼 · {{ extractionRoleLabel(extractionRoles) }} · 历史结果已保留</p>
|
||||
<FollowupAudioReview :key="selectedId" ref="reviewRef" v-model:items="reviewItems" :fields="detail.fields || capabilities.fields" :disabled="!canEditReview || uploading || (mutation !== '' && mutation !== 'save')" :can-daily="capabilities.can_daily" :applied="detail.status === 'applied'" :uncertainties="detail.uncertainties" :snapshot-items="detail.items" :channel-roles="savedChannelRoles" :save-state="saveState" :persisted-revision="persistedRevision" @saved-next="saveDraft(true)" @seek="loadDialogue">
|
||||
<template #player>
|
||||
<div class="audio-player">
|
||||
@@ -145,13 +156,13 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
FollowupAudioError, followupAudioDelete, followupAudioApply, followupAudioBlob, followupAudioCreate,
|
||||
FollowupAudioError, followupAudioReextract, followupAudioDelete, followupAudioApply, followupAudioBlob, followupAudioCreate,
|
||||
followupAudioDetail, followupAudioLists, followupAudioRetry, followupAudioSaveDraft,
|
||||
followupAudioUploadChunk, followupAudioUploadComplete, followupAudioUploadSession,
|
||||
type FollowupAudioChannel, type FollowupCapabilities, type FollowupChannelRoles, type FollowupReviewItem, type FollowupTaskDetail, type FollowupTaskSummary
|
||||
} from '@/api/followupAudio'
|
||||
import FollowupAudioReview from './FollowupAudioReview.vue'
|
||||
import { channelPlaybackLabel, dialogueWindows, dialoguePlaybackStart, audioTimestamp, changedReviewTarget, cloneReviewItems, createFollowupScope, followupAudioReady, followupCanApply, followupCanReview, followupChannelLabel, followupError, followupFeatureEnabled, followupProgressText, followupTaskStatus, hasStereoTranscript, normalizedChannelRoles, rebaseReviewItems, retainedTranscriptNotice, reviewFieldIssue, reviewIssue, reviewPending, uploadChunks, uploadIssue } from './followupAudioState'
|
||||
import { channelPlaybackLabel, dialogueWindows, dialoguePlaybackStart, audioTimestamp, changedReviewTarget, cloneReviewItems, createFollowupScope, followupAudioReady, followupCanApply, followupCanReview, followupChannelLabel, followupError, followupFeatureEnabled, followupProgressText, followupTaskStatus, hasStereoTranscript, normalizedChannelRoles, rebaseReviewItems, retainedTranscriptNotice, reviewBatchIssue, reviewFieldIssue, reviewIssue, reviewPending, uploadChunks, uploadIssue } from './followupAudioState'
|
||||
import { createFollowupAudioPlayback, type FollowupPlaybackChannel } from './followupAudioPlayback'
|
||||
const props = withDefaults(defineProps<{
|
||||
diagnosisId: number; viewOnly?: boolean; active?: boolean; capabilities: FollowupCapabilities;
|
||||
@@ -195,7 +206,7 @@ const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const uploadStage = ref('')
|
||||
const createUncertain = ref(false)
|
||||
const mutation = ref<'save' | 'apply' | 'retry' | 'delete' | ''>('')
|
||||
const mutation = ref<'save' | 'apply' | 'retry' | 'delete' | 'reextract' | ''>('')
|
||||
const errorMessage = ref('')
|
||||
const audioUrl = ref('')
|
||||
const audioElement = ref<HTMLAudioElement | null>(null)
|
||||
@@ -211,6 +222,12 @@ const audioReady = computed(() => followupAudioReady(props.capabilities))
|
||||
const canUpload = computed(() => !props.viewOnly && featureEnabled.value && audioReady.value && props.capabilities.can_upload && props.capabilities.models.length > 0)
|
||||
const canEditReview = computed(() => !props.viewOnly && followupCanReview(props.capabilities, !!detail.value?.preview_only) && detail.value?.status === 'review')
|
||||
const canApplyReview = computed(() => !props.viewOnly && followupCanApply(props.capabilities, !!detail.value?.preview_only) && detail.value?.status === 'review')
|
||||
const extractionRoles = computed(() => normalizedChannelRoles(detail.value?.extraction_channel_roles))
|
||||
const reextractTarget = computed<FollowupChannelRoles>(() => {
|
||||
if (savedChannelRoles.value !== 'unconfirmed' && savedChannelRoles.value !== extractionRoles.value) return savedChannelRoles.value
|
||||
return normalizedChannelRoles(detail.value?.role_check?.suggested_roles)
|
||||
})
|
||||
const canReextract = computed(() => !props.viewOnly && featureEnabled.value && detail.value?.status === 'review' && detail.value?.can_reextract === true)
|
||||
const selectedCount = computed(() => reviewItems.value.filter(item => item.selected).length)
|
||||
const pendingCount = computed(() => reviewItems.value.filter(item => reviewPending(item, detail.value?.fields || props.capabilities.fields, props.capabilities.can_daily)).length + (detail.value?.uncertainties.length || 0))
|
||||
const applyIssue = computed(() => previewOnly.value ? '测试预览仅保存审核草稿,不允许确认写入病历'
|
||||
@@ -221,7 +238,7 @@ const applyIssue = computed(() => previewOnly.value ? '测试预览仅保存审
|
||||
: hasStereo.value && savedChannelRoles.value === 'unconfirmed' ? '请先回听并确认声道角色,保存草稿后重新核对建议'
|
||||
: changedReviewTarget(reviewItems.value, detail.value?.items || [])
|
||||
? '写入目标或记录日期已更改,请先保存审核草稿取得新快照,再核对并采纳'
|
||||
: reviewItems.value.map(item => reviewFieldIssue(item, detail.value?.fields || props.capabilities.fields) || reviewIssue(item, props.capabilities.can_daily)).find(Boolean) || '')
|
||||
: reviewBatchIssue(reviewItems.value, detail.value?.fields || props.capabilities.fields) || reviewItems.value.map(item => reviewFieldIssue(item, detail.value?.fields || props.capabilities.fields) || reviewIssue(item, props.capabilities.can_daily)).find(Boolean) || '')
|
||||
const effectiveActive = computed(() => props.active && featureEnabled.value && documentVisible.value)
|
||||
const scope = createFollowupScope()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -509,6 +526,28 @@ async function applySelected() {
|
||||
} catch (error) { if (scope.current(ticket)) await handleReviewError(error) }
|
||||
finally { if (scope.current(ticket)) { mutation.value = ''; await refresh() } }
|
||||
}
|
||||
function extractionRoleLabel(roles: FollowupChannelRoles): string {
|
||||
return roles === 'left_service' ? '左客服 / 右患者或家属' : roles === 'right_service' ? '右客服 / 左患者或家属' : '角色未确认'
|
||||
}
|
||||
async function reextractWithRoles() {
|
||||
const roles = reextractTarget.value
|
||||
if (!canReextract.value || !detail.value || busy.value || roles === 'unconfirmed' || roles === extractionRoles.value) return
|
||||
if (dirty.value) { ElMessage.warning('请先完成或取消当前审核修改,等待草稿保存后再重新提炼'); return }
|
||||
const ticket = scope.capture(), id = detail.value.id, version = detail.value.version, diagnosisId = props.diagnosisId
|
||||
const current = () => scope.current(ticket) && diagnosisId === props.diagnosisId && selectedId.value === id
|
||||
clearAutosave(); clearTimer(); mutation.value = 'reextract'; errorMessage.value = ''
|
||||
let confirmed = false
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认已回听并核实角色为“${extractionRoleLabel(roles)}”?将保留旧版结果,复用已完成的转写重新提炼,不重复语音识别;新建议须重新审核,不自动写入病例。`, '确认角色并重新提炼', { type: 'warning', confirmButtonText: '角色正确,开始重新提炼', cancelButtonText: '返回回听' })
|
||||
if (!current() || !canReextract.value || dirty.value) return
|
||||
confirmed = true
|
||||
const fresh = await followupAudioReextract({ id, version, channel_roles: roles }, ticket.signal)
|
||||
if (!current()) return
|
||||
installDetail(fresh, false); saveState.value = 'idle'; editingRoles.value = false
|
||||
ElMessage.success('已提交重新提炼,将复用原转写;新结果仍需审核,不自动写入病例')
|
||||
} catch (error) { if (confirmed && current()) await handleReviewError(error) }
|
||||
finally { if (current()) { mutation.value = ''; scheduleAutosave(); await refresh() } }
|
||||
}
|
||||
function canDeleteTask(task: FollowupTaskSummary): boolean {
|
||||
return !props.viewOnly && featureEnabled.value && task.can_delete === true && ['review', 'failed'].includes(task.status)
|
||||
}
|
||||
@@ -627,6 +666,10 @@ h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.detail-heading { align-items: flex-start; }
|
||||
.recording-title { min-width: 0; }
|
||||
.recording-title h3 { overflow-wrap: anywhere; line-height: 1.6; }
|
||||
.role-reextract { padding: 12px; border: 1px solid var(--el-color-warning-light-5); background: var(--el-color-warning-light-9); border-radius: 6px; display: grid; gap: 8px; }
|
||||
.role-reextract p { margin: 0; line-height: 1.6; }
|
||||
.role-reextract blockquote { margin: 0; padding: 8px; background: var(--el-bg-color); white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.role-reextract :deep(.el-button) { justify-self: start; }
|
||||
.detail-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.detail-heading :deep(.el-tag) { flex-shrink: 0; }
|
||||
.detail-loading { padding: 32px; text-align: center; color: var(--el-text-color-secondary); }
|
||||
|
||||
@@ -70,6 +70,22 @@ export function fieldText(value: FollowupValue | undefined, field: FollowupField
|
||||
export function hasEditableReviewFields(item: FollowupReviewItem, fields: FollowupFields): boolean {
|
||||
return Object.keys(item.values).some(key => fields[item.kind]?.some(field => field.key === key && !field.readonly))
|
||||
}
|
||||
/** Prevent multiple selected writes to the same diagnosis field; never silently pick a last value. */
|
||||
export function reviewBatchIssue(items: FollowupReviewItem[], fields: FollowupFields): string {
|
||||
const touched = new Set<string>()
|
||||
for (const item of items.filter(item => item.selected)) {
|
||||
if (item.kind === 'diagnosis') {
|
||||
for (const key of Object.keys(item.values)) {
|
||||
if (touched.has(key)) {
|
||||
const label = fields.diagnosis?.find(field => field.key === key)?.label || key
|
||||
return `${label}存在多条选中建议,请合并内容到一项或仅保留一项后再确认写入`
|
||||
}
|
||||
touched.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
export function reviewFieldIssue(item: FollowupReviewItem, fields: FollowupFields): string {
|
||||
if (!item.selected) return ''
|
||||
for (const key of Object.keys(item.values)) {
|
||||
|
||||
@@ -38,6 +38,7 @@ function setupPanel(overrides = {}, propsPatch = {}, options = {}) {
|
||||
followupAudioUploadChunk: async (id, index, blob) => { calls.push(['chunk', id, index, blob.size]); return { index, received: true } },
|
||||
followupAudioUploadComplete: async id => { calls.push(['complete', id]); return { upload_id: id, duration_seconds: 10, sha256: 'test' } },
|
||||
followupAudioCreate: async payload => { calls.push(['create', payload]); return { task_id: 2, status: 'queued' } },
|
||||
followupAudioReextract: async payload => { calls.push(['reextract', payload]); return task(payload.id, { status: 'queued', version: payload.version + 1, items: [], extraction_revision: 1, extraction_channel_roles: payload.channel_roles }) },
|
||||
followupAudioDelete: async payload => { calls.push(['delete', payload]); return { id: payload.id, status: 'deleted', version: payload.version + 1 } },
|
||||
followupAudioRetry: async id => { calls.push(['retry', id]); return { id, status: 'queued' } },
|
||||
...overrides
|
||||
@@ -2008,3 +2009,45 @@ test('close and unmount abort pending capabilities and prevent any late state re
|
||||
} finally { f.dispose() }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const roleConflictTask = (extra={}) => stereoTask({items:[], channel_roles:'left_service',extraction_channel_roles:'left_service',extraction_revision:0,can_reextract:true,reextract_block_reason:'',role_check:{conflict:true,suggested_roles:'right_service',message:'合成角色冲突',evidence:[{channel:1,start_ms:0,end_ms:15000,text:'我是测试医院的医疗助理'}]},...extra})
|
||||
test('role correction requires explicit confirmation and submits versioned roles without uploading or applying',async()=>{
|
||||
let current=roleConflictTask();const confirmations=[]
|
||||
const f=setupPanel({followupAudioDetail:async()=>current,followupAudioLists:async()=>({items:[current]}),followupAudioReextract:async p=>{f.calls.push(['reextract',p]);current=roleConflictTask({status:'queued',version:2,extraction_revision:1,extraction_channel_roles:p.channel_roles,channel_roles:p.channel_roles,can_reextract:false,role_check:{conflict:false,suggested_roles:'unconfirmed',message:'',evidence:[]}});return current}}, {}, {confirm:async(message)=>{confirmations.push(message)}})
|
||||
await settle();await f.panel.openTask(1);assert.equal(f.panel.reextractTarget.value,'right_service');await f.panel.reextractWithRoles()
|
||||
assert.deepEqual(f.calls.find(c=>c[0]==='reextract')[1],{id:1,version:1,channel_roles:'right_service'});assert.match(confirmations[0],/右客服/);assert.match(confirmations[0],/不重复语音识别/)
|
||||
assert.equal(f.panel.detail.value.status,'queued');assert.equal(f.panel.detail.value.extraction_revision,1);assert.equal(f.calls.some(c=>['apply','create','session','chunk'].includes(c[0])),false);f.dispose()
|
||||
})
|
||||
test('role proposal never mutates manual roles or sends re-extraction until confirmed',async()=>{
|
||||
const f=setupPanel({followupAudioDetail:async()=>roleConflictTask()}, {}, {confirm:async()=>{throw new Error('cancel')}});await settle();await f.panel.openTask(1)
|
||||
assert.equal(f.panel.savedChannelRoles.value,'left_service');assert.equal(f.panel.channelRoles.value,'left_service');assert.equal(f.calls.some(c=>c[0]==='reextract'),false)
|
||||
await f.panel.reextractWithRoles();assert.equal(f.calls.some(c=>c[0]==='reextract'),false);assert.equal(f.panel.detail.value.status,'review');assert.equal(f.panel.mutation.value,'');f.dispose()
|
||||
})
|
||||
test('re-extraction blocks dirty, readonly, unavailable, same-role and processing contexts',async()=>{
|
||||
const f=setupPanel({followupAudioDetail:async()=>roleConflictTask()});await settle();await f.panel.openTask(1)
|
||||
f.panel.reviewRef.value={hasUnsavedChanges:()=>true};await f.panel.reextractWithRoles();assert.equal(f.calls.some(c=>c[0]==='reextract'),false)
|
||||
f.panel.reviewRef.value=null;f.props.viewOnly=true;await f.panel.reextractWithRoles();f.props.viewOnly=false
|
||||
for(const patch of [{can_reextract:false},{status:'running'},{status:'applied'},{role_check:{conflict:false,suggested_roles:'left_service',evidence:[],message:''}}]){f.panel.installDetail(roleConflictTask(patch),false);await f.panel.reextractWithRoles()}
|
||||
assert.equal(f.calls.some(c=>c[0]==='reextract'),false);f.dispose()
|
||||
})
|
||||
test('re-extraction confirmation and late results are fenced across patient changes',async()=>{
|
||||
const answer=deferred();const f=setupPanel({followupAudioDetail:async()=>roleConflictTask()}, {}, {confirm:()=>answer.promise});await settle();await f.panel.openTask(1)
|
||||
const pending=f.panel.reextractWithRoles();f.props.diagnosisId=20;await settle();answer.resolve();await pending;assert.equal(f.calls.some(c=>c[0]==='reextract'),false);f.dispose()
|
||||
const response=deferred();const g=setupPanel({followupAudioDetail:async id=>roleConflictTask({id}),followupAudioReextract:async p=>{g.calls.push(['reextract',p]);return response.promise}});await settle();await g.panel.openTask(1)
|
||||
const work=g.panel.reextractWithRoles();await settle();await g.panel.reextractWithRoles();assert.equal(g.calls.filter(c=>c[0]==='reextract').length,1);g.props.diagnosisId=30;await settle();await g.panel.openTask(2);response.resolve(roleConflictTask({status:'queued',version:2}));await work;assert.equal(g.panel.selectedId.value,2);assert.equal(g.panel.detail.value.id,2);g.dispose()
|
||||
})
|
||||
test('explicitly corrected review roles take priority over the advisory suggestion',async()=>{
|
||||
const f=setupPanel({followupAudioDetail:async()=>roleConflictTask({channel_roles:'right_service',role_check:{conflict:false,suggested_roles:'unconfirmed',message:'',evidence:[]}})});await settle();await f.panel.openTask(1);assert.equal(f.panel.reextractTarget.value,'right_service');f.dispose()
|
||||
const calls=[];const api=loadTs(path.join(root,'src/api/followupAudio.ts'),()=>({default:{post:async(config,options)=>{calls.push({config,options});return {code:1,data:roleConflictTask({status:'queued'})}}}}));await api.followupAudioReextract({id:1,version:1,channel_roles:'right_service'});assert.equal(calls[0].config.url,'/tcm.followupAudio/reextract');assert.equal(calls[0].options.isOpenRetry,false)
|
||||
})
|
||||
|
||||
|
||||
test('duplicate diagnosis suggestions are blocked clearly without losing distinct daily measurements',()=>{
|
||||
const fields={diagnosis:[{key:'symptoms',label:'症状',type:'text'},{key:'remark',label:'备注',type:'text'}]}
|
||||
const a=item({id:'a',kind:'diagnosis',values:{symptoms:'口干'}}),b=item({id:'b',kind:'diagnosis',values:{symptoms:'睡眠中断'}})
|
||||
assert.match(state.reviewBatchIssue([a,b],fields),/症状存在多条选中建议/)
|
||||
assert.equal(state.reviewBatchIssue([a,{...b,selected:false}],fields),'')
|
||||
assert.equal(state.reviewBatchIssue([item(),item({id:'another',record_time:'12:00'})],fields),'')
|
||||
assert.equal(a.values.symptoms,'口干');assert.equal(b.values.symptoms,'睡眠中断')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user