fix(followup-audio): preserve roles chosen before first audio file

This commit is contained in:
2026-10-10 15:03:06 +08:00
parent b4acad8510
commit fc91908cf9
2 changed files with 51 additions and 3 deletions
@@ -59,6 +59,7 @@
<section v-if="hasStereo" class="channel-role-box" aria-label="声道角色人工标注">
<label>声道角色(人工确认)<el-select :model-value="channelRoles" :disabled="!canEditReview || busy" @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 class="muted">声道不等于已核实的说话人身份。角色标注不自动重新提炼;变更后保存草稿会取消全部采纳,须重新核对。</p>
<p v-if="savedChannelRoles !== 'unconfirmed' && !roleDirty" class="muted">当前角色已确认,无需再次选择;只有左右标错时才需修改。</p>
<p v-if="roleDirty" class="review-error" role="status">角色标注尚未保存,请先保存草稿,再逐项核对。</p>
</section>
<FollowupAudioReview :key="selectedId" ref="reviewRef" v-model:items="reviewItems" :fields="detail.fields || capabilities.fields" :disabled="!canEditReview || busy" :can-daily="capabilities.can_daily" :applied="detail.status === 'applied'" :uncertainties="detail.uncertainties" :snapshot-items="detail.items" :channel-roles="savedChannelRoles" @seek="loadDialogue" />
@@ -292,7 +293,11 @@ async function backToList() {
stop(); resetRoles(); selectedId.value = 0; detail.value = null; reviewItems.value = []; snapshot.value = '[]'; detailSections.value = []; errorMessage.value = ''
await refresh()
}
function openUpload() { if (canUpload.value && !busy.value && !createUncertain.value) uploadVisible.value = true }
function openUpload() {
if (!canUpload.value || busy.value || createUncertain.value) return
if (!file.value) uploadChannelRoles.value = 'unconfirmed'
uploadVisible.value = true
}
function closeUpload(done?: () => void) { if (uploading.value) return; uploadVisible.value = false; done?.() }
function statusType(status: string) {
if (status === 'failed' || status === 'needs_reconciliation') return 'danger'
@@ -300,7 +305,16 @@ function statusType(status: string) {
if (status === 'review') return 'warning'
return 'info'
}
function chooseFile(event: Event) { uploadChannelRoles.value = 'unconfirmed'; file.value = (event.target as HTMLInputElement).files?.[0] || null; uploadProgress.value = 0; uploadStage.value = '' }
function chooseFile(event: Event) {
const nextFile = (event.target as HTMLInputElement).files?.[0] || null
// Cancelling the picker or selecting the first file must not discard a role chosen earlier.
if (!nextFile) return
if (file.value && nextFile !== file.value) {
uploadChannelRoles.value = 'unconfirmed'
ElMessage.warning('录音已更换,请重新核对本次录音的声道角色')
}
file.value = nextFile; uploadProgress.value = 0; uploadStage.value = ''
}
async function submitUpload() {
if (busy.value || createUncertain.value) return
const problem = uploadIssue(file.value, recordedAt.value, modelKey.value, props.capabilities, props.viewOnly)
+35 -1
View File
@@ -1512,10 +1512,44 @@ test('upload sends explicitly selected pre-extraction roles and clears them on p
})
test('a new recording never inherits the previous file role assignment', async () => {
test('a replacement recording never inherits the previous file role assignment', async () => {
const f = setupPanel(); await settle()
f.panel.file.value = new File(['previous'], 'previous.wav')
f.panel.uploadChannelRoles.value = 'left_service'
f.panel.chooseFile({ target: { files: [new File(['synthetic'], 'new.wav')] } })
assert.equal(f.panel.uploadChannelRoles.value, 'unconfirmed')
f.dispose()
})
for (const order of ['role-first', 'file-first']) test(`upload ${order} preserves roles through create and initial review without reselection`, async () => {
let createdRole = 'unconfirmed'
const f = setupPanel({
followupAudioCreate: async payload => { createdRole = payload.channel_roles; return { task_id: 2, status: 'queued' } },
followupAudioDetail: async id => stereoTask({ id, channel_roles: createdRole })
})
await settle(); f.panel.openUpload()
if (order === 'role-first') f.panel.uploadChannelRoles.value = 'left_service'
f.panel.chooseFile({ target: { files: [new File(['synthetic'], 'first.wav')] } })
if (order === 'file-first') f.panel.uploadChannelRoles.value = 'left_service'
f.panel.recordedAt.value = '2026-09-29 09:00:00'
await f.panel.submitUpload()
assert.equal(createdRole, 'left_service')
assert.equal(f.panel.channelRoles.value, 'left_service')
assert.equal(f.panel.savedChannelRoles.value, 'left_service')
assert.equal(f.panel.roleDirty.value, false)
assert.doesNotMatch(f.panel.applyIssue.value, /声道角色/)
f.dispose()
})
test('cancelled file selection keeps current file and roles; new upload starts unconfirmed', async () => {
const f = setupPanel(); await settle(); f.panel.openUpload()
const recording = new File(['synthetic'], 'existing.wav')
f.panel.file.value = recording; f.panel.uploadChannelRoles.value = 'left_service'
f.panel.chooseFile({ target: { files: [] } })
assert.equal(f.panel.file.value, recording)
assert.equal(f.panel.uploadChannelRoles.value, 'left_service')
f.panel.file.value = null; f.panel.closeUpload(); f.panel.openUpload()
assert.equal(f.panel.uploadChannelRoles.value, 'unconfirmed')
f.dispose()
})